using System.Diagnostics; using System.Runtime.CompilerServices; using System.Windows.Documents; using System.Windows.Media.Animation; using System.Windows.Threading; namespace NeoUI.Controls; /// /// 提供静态方法来显示各种类型的轻量级通知,支持屏幕级别和窗口级别的消息提示。 /// 通过调用Screen属性可以获取一个IToastService实例,用于在整个应用范围内显示通知。 /// 使用For方法为特定的窗口创建一个IToastService实例,允许在该窗口上下文中显示通知。 /// public static class Toast { /// /// 获取一个IToastService实例,该实例允许在整个应用程序范围内显示屏幕级别的通知。 /// 通过这个属性,可以调用相关的显示方法来发送不同类型的通知消息,如成功、信息、错误和警告等。 /// public static IToastService Screen { get; } private static readonly ConditionalWeakTable WindowManagers = new(); static Toast() { Screen = new ToastManager(new ScreenHost()); } /// /// 为指定的窗口创建一个IToastService实例,以便在该窗口上下文中显示轻量级通知。 /// /// 要为其创建通知服务的窗口对象。如果传递null,则会抛出异常。 /// 与给定窗口关联的IToastService实例,用于显示通知。 /// 当提供的window参数为null时抛出。 public static IToastService For(Window window) { if (window == null) throw new ArgumentNullException(nameof(window)); return WindowManagers.GetValue(window, w => new ToastManager(new WindowHost(w))); } } /// /// 定义了用于显示轻量级通知的方法。此接口支持多种类型的提示消息,包括成功、错误、警告和信息等,并允许设置消息的显示时长。 /// 通过实现该接口的服务,可以在应用或特定窗口中展示相应的通知。 /// public interface IToastService { /// /// 显示一条轻量级通知。 /// /// 要显示的消息内容。 /// 消息的类型,默认为信息类型。可选值包括成功、信息、错误和警告等。 /// 消息的显示时长。如果未指定,则使用默认值。 void Show(string message, ToastType type = ToastType.Info, TimeSpan? duration = null); /// /// 显示一条成功类型的轻量级通知。 /// /// 要显示的消息内容。 /// 消息的显示时长。如果未指定,则使用默认值。 void ShowSuccess(string message, TimeSpan? duration = null); /// /// 显示一条错误类型的轻量级通知。 /// /// 要显示的错误消息内容。 /// 消息的显示时长。如果未指定,则使用默认值。 void ShowError(string message, TimeSpan? duration = null); /// /// 显示一条警告类型的轻量级通知。 /// /// 要显示的消息内容。 /// 消息的显示时长,如果未指定则使用默认值。 void ShowWarning(string message, TimeSpan? duration = null); /// /// 显示一条信息类型的轻量级通知。 /// /// 要显示的消息内容。 /// 消息的显示时长。如果未指定,则使用默认值。 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 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