Files
ShrlAlgoToolkit/NeuWPF/NeoUI/Controls/Toast/Toast.cs
ShrlAlgo 955a01f564 整理
2025-08-20 12:10:35 +08:00

265 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows.Documents;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace NeoUI.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