功能完善
This commit is contained in:
264
NeuWPF/NeoUI/Controls/Toast/Toast.cs
Normal file
264
NeuWPF/NeoUI/Controls/Toast/Toast.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
|
||||
|
||||
namespace NeumUI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 提供静态方法来显示各种类型的轻量级通知,支持屏幕级别和窗口级别的消息提示。
|
||||
/// 通过调用Screen属性可以获取一个IToastService实例,用于在整个应用范围内显示通知。
|
||||
/// 使用For方法为特定的窗口创建一个IToastService实例,允许在该窗口上下文中显示通知。
|
||||
/// </summary>
|
||||
public static class Toast
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取一个IToastService实例,该实例允许在整个应用程序范围内显示屏幕级别的通知。
|
||||
/// 通过这个属性,可以调用相关的显示方法来发送不同类型的通知消息,如成功、信息、错误和警告等。
|
||||
/// </summary>
|
||||
public static IToastService Screen { get; }
|
||||
private static readonly ConditionalWeakTable<Window, IToastService> WindowManagers = new();
|
||||
static Toast() { Screen = new ToastManager(new ScreenHost()); }
|
||||
|
||||
/// <summary>
|
||||
/// 为指定的窗口创建一个IToastService实例,以便在该窗口上下文中显示轻量级通知。
|
||||
/// </summary>
|
||||
/// <param name="window">要为其创建通知服务的窗口对象。如果传递null,则会抛出异常。</param>
|
||||
/// <returns>与给定窗口关联的IToastService实例,用于显示通知。</returns>
|
||||
/// <exception cref="ArgumentNullException">当提供的window参数为null时抛出。</exception>
|
||||
public static IToastService For(Window window)
|
||||
{
|
||||
if (window == null) throw new ArgumentNullException(nameof(window));
|
||||
return WindowManagers.GetValue(window, w => new ToastManager(new WindowHost(w)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义了用于显示轻量级通知的方法。此接口支持多种类型的提示消息,包括成功、错误、警告和信息等,并允许设置消息的显示时长。
|
||||
/// 通过实现该接口的服务,可以在应用或特定窗口中展示相应的通知。
|
||||
/// </summary>
|
||||
public interface IToastService
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示一条轻量级通知。
|
||||
/// </summary>
|
||||
/// <param name="message">要显示的消息内容。</param>
|
||||
/// <param name="type">消息的类型,默认为信息类型。可选值包括成功、信息、错误和警告等。</param>
|
||||
/// <param name="duration">消息的显示时长。如果未指定,则使用默认值。</param>
|
||||
void Show(string message, ToastType type = ToastType.Info, TimeSpan? duration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示一条成功类型的轻量级通知。
|
||||
/// </summary>
|
||||
/// <param name="message">要显示的消息内容。</param>
|
||||
/// <param name="duration">消息的显示时长。如果未指定,则使用默认值。</param>
|
||||
void ShowSuccess(string message, TimeSpan? duration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示一条错误类型的轻量级通知。
|
||||
/// </summary>
|
||||
/// <param name="message">要显示的错误消息内容。</param>
|
||||
/// <param name="duration">消息的显示时长。如果未指定,则使用默认值。</param>
|
||||
void ShowError(string message, TimeSpan? duration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示一条警告类型的轻量级通知。
|
||||
/// </summary>
|
||||
/// <param name="message">要显示的消息内容。</param>
|
||||
/// <param name="duration">消息的显示时长,如果未指定则使用默认值。</param>
|
||||
void ShowWarning(string message, TimeSpan? duration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示一条信息类型的轻量级通知。
|
||||
/// </summary>
|
||||
/// <param name="message">要显示的消息内容。</param>
|
||||
/// <param name="duration">消息的显示时长。如果未指定,则使用默认值。</param>
|
||||
void ShowInfo(string message, TimeSpan? duration = null);
|
||||
}
|
||||
|
||||
#region Internal Implementation
|
||||
|
||||
internal class ToastManager(IToastHost host) : IToastService
|
||||
{
|
||||
public void Show(string message, ToastType type, TimeSpan? duration = null)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var toast = new ToastControl { Message = message, Type = type };
|
||||
|
||||
var fadeInAnim = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350));
|
||||
|
||||
var timer = new DispatcherTimer { Interval = duration ?? TimeSpan.FromSeconds(4) };
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
var fadeOutAnim = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(350));
|
||||
fadeOutAnim.Completed += (_, _) => host.RemoveToast(toast);
|
||||
toast.BeginAnimation(UIElement.OpacityProperty, fadeOutAnim);
|
||||
};
|
||||
|
||||
toast.Loaded += (_, _) =>
|
||||
{
|
||||
toast.BeginAnimation(UIElement.OpacityProperty, fadeInAnim);
|
||||
timer.Start();
|
||||
};
|
||||
|
||||
host.AddToast(toast);
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowSuccess(string message, TimeSpan? duration = null) => Show(message, ToastType.Success, duration);
|
||||
public void ShowError(string message, TimeSpan? duration = null) => Show(message, ToastType.Error, duration);
|
||||
public void ShowWarning(string message, TimeSpan? duration = null) => Show(message, ToastType.Warning, duration);
|
||||
public void ShowInfo(string message, TimeSpan? duration = null) => Show(message, ToastType.Info, duration);
|
||||
}
|
||||
|
||||
internal interface IToastHost
|
||||
{
|
||||
void AddToast(ToastControl toast);
|
||||
void RemoveToast(ToastControl toast);
|
||||
}
|
||||
|
||||
internal class WindowHost : IToastHost
|
||||
{
|
||||
private readonly StackPanel toastContainer;
|
||||
|
||||
public WindowHost(Window window)
|
||||
{
|
||||
toastContainer = new StackPanel();
|
||||
|
||||
// Use a style to apply margin between toasts, which is a robust approach.
|
||||
var childStyle = new Style(typeof(ToastControl));
|
||||
childStyle.Setters.Add(new Setter(FrameworkElement.MarginProperty, new Thickness(0, 8, 0, 8)));
|
||||
toastContainer.Resources.Add(typeof(ToastControl), childStyle);
|
||||
|
||||
var adornerLayer = AdornerLayer.GetAdornerLayer(window.Content as Visual);
|
||||
if (adornerLayer == null) throw new InvalidOperationException("Cannot find AdornerLayer on the specified window.");
|
||||
|
||||
adornerLayer.Add(new ToastAdorner(window.Content as UIElement, toastContainer));
|
||||
}
|
||||
|
||||
public void AddToast(ToastControl toast) => toastContainer.Children.Add(toast);
|
||||
public void RemoveToast(ToastControl toast) => toastContainer.Children.Remove(toast);
|
||||
|
||||
|
||||
private class ToastAdorner : Adorner
|
||||
{
|
||||
private readonly UIElement child; // This is our StackPanel
|
||||
|
||||
public ToastAdorner(UIElement? adornedElement, UIElement child) : base(adornedElement)
|
||||
{
|
||||
this.child = child;
|
||||
// Add the child to the visual tree. This is essential.
|
||||
AddVisualChild(this.child);
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount => 1;
|
||||
protected override Visual GetVisualChild(int index) => child;
|
||||
|
||||
// We override Measure to tell our child (StackPanel) how much space it has.
|
||||
protected override Size MeasureOverride(Size constraint)
|
||||
{
|
||||
// Give the child all the space it could possibly want.
|
||||
// It will then measure itself and its children, establishing its DesiredSize.
|
||||
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
// The Adorner itself doesn't need to return a specific size, as it's a transparent overlay.
|
||||
return base.MeasureOverride(constraint);
|
||||
}
|
||||
|
||||
// ArrangeOverride is where we do the actual positioning.
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
// 'finalSize' is the full size of the element we are adorning (e.g., the window's content area).
|
||||
// '_child.DesiredSize' is the size our StackPanel reported it needs after being measured.
|
||||
|
||||
// Calculate the X coordinate to center the child (StackPanel).
|
||||
var x = (finalSize.Width - child.DesiredSize.Width) / 2;
|
||||
|
||||
// The Y coordinate is 0, placing it at the very top.
|
||||
double y = 0;
|
||||
|
||||
// Create the final rectangle for our child, positioned correctly.
|
||||
var finalRect = new Rect(new Point(x, y), child.DesiredSize);
|
||||
|
||||
// Arrange the child (our StackPanel) in the calculated centered rectangle.
|
||||
child.Arrange(finalRect);
|
||||
|
||||
// The Adorner itself arranges to fill the entire space.
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class ScreenHost : IToastHost
|
||||
{
|
||||
private readonly List<Window> toastWindows = [];
|
||||
private readonly object @lock = new();
|
||||
private const double Spacing = 15;
|
||||
|
||||
public void AddToast(ToastControl toast)
|
||||
{
|
||||
var window = new Window
|
||||
{
|
||||
Content = toast,
|
||||
WindowStyle = WindowStyle.None,
|
||||
AllowsTransparency = true,
|
||||
Background = Brushes.Transparent,
|
||||
ShowInTaskbar = false,
|
||||
Topmost = true,
|
||||
Owner = null,
|
||||
SizeToContent = SizeToContent.WidthAndHeight,
|
||||
Left = -9999
|
||||
};
|
||||
|
||||
window.Loaded += (_, _) =>
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
toastWindows.Add(window);
|
||||
RepositionToasts();
|
||||
}
|
||||
};
|
||||
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
toastWindows.Remove(window);
|
||||
RepositionToasts();
|
||||
}
|
||||
};
|
||||
|
||||
window.Show();
|
||||
}
|
||||
|
||||
public void RemoveToast(ToastControl toast)
|
||||
{
|
||||
Debug.Assert(toastWindows != null, nameof(toastWindows) + " != null");
|
||||
var window = toastWindows.FirstOrDefault(w => Equals(w.Content, toast));
|
||||
window?.Close();
|
||||
}
|
||||
|
||||
private void RepositionToasts()
|
||||
{
|
||||
var workArea = SystemParameters.WorkArea;
|
||||
var currentTop = Spacing; // Start from the top with spacing.
|
||||
|
||||
foreach (var window in toastWindows)
|
||||
{
|
||||
// Horizontally center the window on the screen.
|
||||
var left = workArea.Left + (workArea.Width - window.ActualWidth) / 2;
|
||||
|
||||
var moveAnim = new DoubleAnimation(currentTop, TimeSpan.FromMilliseconds(350))
|
||||
{ EasingFunction = new QuinticEase { EasingMode = EasingMode.EaseOut } };
|
||||
|
||||
window.Left = left; // Set horizontal position directly.
|
||||
window.BeginAnimation(Window.TopProperty, moveAnim); // Animate vertical position.
|
||||
|
||||
// Update the top position for the next toast.
|
||||
currentTop += window.ActualHeight + Spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
40
NeuWPF/NeoUI/Controls/Toast/ToastControl.xaml
Normal file
40
NeuWPF/NeoUI/Controls/Toast/ToastControl.xaml
Normal file
@@ -0,0 +1,40 @@
|
||||
<UserControl
|
||||
HorizontalAlignment="Center"
|
||||
MinHeight="50"
|
||||
Width="320"
|
||||
mc:Ignorable="d"
|
||||
x:Class="NeumUI.Controls.ToastControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:NeumUI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Border
|
||||
Background="#F1F1F1"
|
||||
BorderBrush="#E0E0E0"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
x:Name="RootBorder">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect
|
||||
BlurRadius="8"
|
||||
Direction="270"
|
||||
Opacity="0.15"
|
||||
ShadowDepth="1" />
|
||||
</Border.Effect>
|
||||
<StackPanel Margin="15,12" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="16"
|
||||
Margin="0,0,14,0"
|
||||
Text=""
|
||||
VerticalAlignment="Center"
|
||||
x:Name="IconTextBlock" />
|
||||
<TextBlock
|
||||
Text="This is a toast message."
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"
|
||||
x:Name="MessageTextBlock" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
116
NeuWPF/NeoUI/Controls/Toast/ToastControl.xaml.cs
Normal file
116
NeuWPF/NeoUI/Controls/Toast/ToastControl.xaml.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
namespace NeumUI.Controls;
|
||||
|
||||
|
||||
// 定义通知类型
|
||||
/// <summary>
|
||||
/// 该枚举定义了ToastControl控件中可以使用的不同类型的通知。每种类型对应不同的视觉样式和图标,用于在用户界面上显示不同性质的消息,如成功、信息、警告或错误。
|
||||
/// </summary>
|
||||
public enum ToastType
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示成功通知的枚举值。当设置为该值时,ToastControl控件将使用预定义的颜色和图标来显示一条成功消息。背景颜色为浅绿色(#F0F9EB),前景颜色为深绿色(#67C23A),并且会显示一个表示成功的图标。
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// 表示信息通知的枚举值。当设置为该值时,ToastControl控件将使用预定义的颜色和图标来显示一条普通消息。背景颜色为浅蓝色(#E6F7FF),前景颜色为深蓝色(#409EFF),并且会显示一个表示信息的图标。
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// 表示错误通知的枚举值。当设置为该值时,ToastControl控件将使用预定义的颜色和图标来显示一条错误消息。背景颜色为浅红色(#FEF2F2),前景颜色为深红色(#F56C6C),并且会显示一个表示错误的图标。
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// 表示警告通知的枚举值。当设置为该值时,ToastControl控件将使用预定义的颜色和图标来显示一条警告消息。背景颜色为浅黄色(#FDF6EC),前景颜色为橙色(#E6A23C),并且会显示一个表示警告的图标。
|
||||
/// </summary>
|
||||
Warning
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ToastControl 是一个自定义的 WPF 控件,用于在用户界面中显示简短的通知消息。它支持不同类型的通知(如成功、信息、警告和错误),并能够根据通知类型自动调整其视觉样式。
|
||||
/// </summary>
|
||||
public partial class ToastControl
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public ToastControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// 依赖属性,用于从外部设置消息和类型
|
||||
/// <summary>
|
||||
/// 该依赖属性用于存储和获取ToastControl控件中显示的消息文本。通过设置此属性,可以动态地更改用户界面上显示的通知消息内容。
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
|
||||
nameof(Message), typeof(string), typeof(ToastControl), new PropertyMetadata(string.Empty, OnMessageChanged));
|
||||
|
||||
/// <summary>
|
||||
/// 该依赖属性用于存储和获取ToastControl控件中显示的通知类型。通过设置此属性,可以指定通知的种类(如成功、信息、警告或错误),从而影响通知的视觉表现,比如颜色和图标。
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty TypeProperty = DependencyProperty.Register(
|
||||
nameof(Type), typeof(ToastType), typeof(ToastControl), new PropertyMetadata(ToastType.Info, OnTypeChanged));
|
||||
|
||||
/// <summary>
|
||||
/// 用于获取或设置ToastControl控件中显示的消息文本。通过更改此属性,可以动态更新用户界面上的通知消息内容。
|
||||
/// </summary>
|
||||
public string Message
|
||||
{
|
||||
get => (string)GetValue(MessageProperty);
|
||||
set => SetValue(MessageProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该依赖属性用于存储和获取ToastControl控件中显示的通知类型。通过设置此属性,可以指定通知的种类(如成功、信息、错误或警告),从而影响用户界面上通知的视觉呈现方式。
|
||||
/// </summary>
|
||||
public ToastType Type
|
||||
{
|
||||
get => (ToastType)GetValue(TypeProperty);
|
||||
set => SetValue(TypeProperty, value);
|
||||
}
|
||||
|
||||
private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((ToastControl)d).MessageTextBlock.Text = (string)e.NewValue;
|
||||
}
|
||||
|
||||
// 当类型改变时,更新颜色和图标
|
||||
private static void OnTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (ToastControl)d;
|
||||
var icon = "";
|
||||
Brush background = Brushes.Gray;
|
||||
Brush foreground = Brushes.White;
|
||||
|
||||
switch ((ToastType)e.NewValue)
|
||||
{
|
||||
case ToastType.Success:
|
||||
icon = "\uE930"; // Success icon
|
||||
background = new SolidColorBrush(Color.FromRgb(240, 249, 235));
|
||||
foreground = new SolidColorBrush(Color.FromRgb(103, 194, 58));
|
||||
break;
|
||||
case ToastType.Info:
|
||||
icon = "\uE946"; // Info icon
|
||||
background = new SolidColorBrush(Color.FromRgb(237, 246, 253));
|
||||
foreground = new SolidColorBrush(Color.FromRgb(64, 158, 255));
|
||||
break;
|
||||
case ToastType.Warning:
|
||||
icon = "\uE7BA"; // Warning icon
|
||||
background = new SolidColorBrush(Color.FromRgb(253, 246, 236));
|
||||
foreground = new SolidColorBrush(Color.FromRgb(230, 162, 60));
|
||||
break;
|
||||
case ToastType.Error:
|
||||
icon = "\uEA39"; // Error icon
|
||||
background = new SolidColorBrush(Color.FromRgb(254, 242, 242));
|
||||
foreground = new SolidColorBrush(Color.FromRgb(245, 108, 108));
|
||||
break;
|
||||
}
|
||||
control.IconTextBlock.Text = icon;
|
||||
control.IconTextBlock.Foreground = foreground;
|
||||
control.RootBorder.Background = background;
|
||||
control.RootBorder.BorderBrush = background;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user