Files
ShrlAlgoToolkit/AntDesignWPF/Controls/Notification/NotificationManager.cs
2025-07-31 20:12:24 +08:00

328 lines
12 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.ComponentModel;
namespace AntDesignWPF.Controls;
public enum NotificationPlacement
{
TopRight,
TopLeft,
BottomRight,
BottomLeft
}
//public static class NotificationManager
//{
// private static readonly Dictionary<NotificationPlacement, List<NotificationView>> _activeNotifications = new Dictionary<NotificationPlacement, List<NotificationView>>
// {
// { NotificationPlacement.TopRight, new List<NotificationView>() },
// { NotificationPlacement.TopLeft, new List<NotificationView>() },
// { NotificationPlacement.BottomRight, new List<NotificationView>() },
// { NotificationPlacement.BottomLeft, new List<NotificationView>() }
// };
// private static readonly object _lock = new object();
// public static void Show(string title, string message, NotificationType type = NotificationType.Info, NotificationPlacement placement = NotificationPlacement.TopRight, int durationSeconds = 3)
// {
// var duration = TimeSpan.FromSeconds(durationSeconds);
// Application.Current.Dispatcher.Invoke(() =>
// {
// var notificationWindow = new NotificationView(title, message, type, duration);
// notificationWindow.Closed += (s, e) =>
// {
// lock (_lock)
// {
// _activeNotifications[placement].Remove(notificationWindow);
// Reposition(placement);
// }
// };
// lock (_lock)
// {
// _activeNotifications[placement].Add(notificationWindow);
// Reposition(placement);
// }
// notificationWindow.Show();
// });
// }
// private static void Reposition(NotificationPlacement placement)
// {
// var workArea = SystemParameters.WorkArea;
// var notifications = _activeNotifications[placement];
// double currentTop = workArea.Top + 10;
// double currentBottom = workArea.Bottom - 10;
// foreach (var window in notifications)
// {
// // 如果窗口尚未加载,先在屏幕外显示以获取其实际尺寸
// if (!window.IsLoaded)
// {
// window.WindowStartupLocation = WindowStartupLocation.Manual;
// window.Left = -9999;
// window.Top = -9999;
// // 调用Show()会使窗口加载并经过布局计算
// // 对于非模态窗口,这不会阻塞代码执行
// window.Show();
// }
// // 经过Show()之后ActualHeight和ActualWidth就已经是准确的了
// if (placement == NotificationPlacement.TopRight || placement == NotificationPlacement.TopLeft)
// {
// window.Top = currentTop;
// currentTop += window.ActualHeight + 10;
// }
// else // BottomRight or BottomLeft
// {
// window.Top = currentBottom - window.ActualHeight;
// currentBottom -= (window.ActualHeight + 10);
// }
// if (placement == NotificationPlacement.TopRight || placement == NotificationPlacement.BottomRight)
// {
// // 在这里应该使用ActualWidth特别是当窗口宽度为"Auto"时
// window.Left = workArea.Right - window.ActualWidth - 10;
// }
// else // TopLeft or BottomLeft
// {
// window.Left = workArea.Left + 10;
// }
// }
// }
//}
public static class NotificationManager
{
// 使用 ConditionalWeakTable 来将通知列表与父窗口关联。
// 这有助于自动处理内存,当父窗口被垃圾回收时,关联的通知列表也会被回收,避免了内存泄漏。
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Window, Dictionary<NotificationPlacement, List<NotificationView>>> _windowNotifications =
new System.Runtime.CompilerServices.ConditionalWeakTable<Window, Dictionary<NotificationPlacement, List<NotificationView>>>();
// 保留一个用于全局通知的静态字典(当 owner 为 null 时使用)
private static readonly Dictionary<NotificationPlacement, List<NotificationView>> _globalNotifications =
new Dictionary<NotificationPlacement, List<NotificationView>>
{
{ NotificationPlacement.TopRight, new List<NotificationView>() },
{ NotificationPlacement.TopLeft, new List<NotificationView>() },
{ NotificationPlacement.BottomRight, new List<NotificationView>() },
{ NotificationPlacement.BottomLeft, new List<NotificationView>() }
};
private static readonly object _lock = new object();
/// <summary>
/// 显示一个通知。
/// </summary>
/// <param name="owner">通知要附着到的父窗口。如果为 null则显示为全局通知。</param>
/// <param name="title">标题</param>
/// <param name="message">消息</param>
/// <param name="type">类型</param>
/// <param name="placement">位置</param>
/// <param name="durationSeconds">显示时长(秒)</param>
public static void Show(Window owner, string title, string message, NotificationType type = NotificationType.Info, NotificationPlacement placement = NotificationPlacement.TopRight, int durationSeconds = 3)
{
var duration = TimeSpan.FromSeconds(durationSeconds);
// 必须在UI线程上执行
Application.Current.Dispatcher.Invoke(() =>
{
var notificationWindow = new NotificationView(title, message, type, duration);
notificationWindow.Owner = owner; // 设置所有者
// 获取或创建与此窗口关联的通知列表
var activeNotifications = GetNotificationListForOwner(owner);
notificationWindow.Closed += (s, e) =>
{
lock (_lock)
{
activeNotifications[placement].Remove(notificationWindow);
// 如果这是父窗口的最后一个通知,则取消事件订阅
if (owner != null && !HasActiveNotifications(owner))
{
UnsubscribeFromOwnerEvents(owner);
}
Reposition(owner, placement);
}
};
lock (_lock)
{
// 如果是第一个为该窗口创建的通知,则订阅事件
if (owner != null && !HasActiveNotifications(owner))
{
SubscribeToOwnerEvents(owner);
}
activeNotifications[placement].Add(notificationWindow);
Reposition(owner, placement);
}
// Show() 必须在 Reposition 之后调用,以确保初始位置正确
notificationWindow.Show();
});
}
// 重载一个 Show 方法以保持与旧 API 的兼容性(全局通知)
public static void Show(string title, string message, NotificationType type = NotificationType.Info, NotificationPlacement placement = NotificationPlacement.TopRight, int durationSeconds = 3)
{
Show(null, title, message, type, placement, durationSeconds);
}
private static Dictionary<NotificationPlacement, List<NotificationView>> GetNotificationListForOwner(Window owner)
{
if (owner == null)
{
return _globalNotifications;
}
lock (_lock)
{
return _windowNotifications.GetValue(owner, key =>
{
// 当 ConditionalWeakTable 中没有找到该窗口的条目时,此工厂方法被调用
return new Dictionary<NotificationPlacement, List<NotificationView>>
{
{ NotificationPlacement.TopRight, new List<NotificationView>() },
{ NotificationPlacement.TopLeft, new List<NotificationView>() },
{ NotificationPlacement.BottomRight, new List<NotificationView>() },
{ NotificationPlacement.BottomLeft, new List<NotificationView>() }
};
});
}
}
private static bool HasActiveNotifications(Window owner)
{
if (owner == null) return false;
var lists = GetNotificationListForOwner(owner);
foreach (var list in lists.Values)
{
if (list.Count > 0) return true;
}
return false;
}
private static void SubscribeToOwnerEvents(Window owner)
{
owner.LocationChanged += OnOwnerMoved;
owner.SizeChanged += OnOwnerMoved;
owner.StateChanged += OnOwnerStateChanged;
owner.Closing += OnOwnerClosing; // 确保在父窗口关闭时清理
}
private static void UnsubscribeFromOwnerEvents(Window owner)
{
owner.LocationChanged -= OnOwnerMoved;
owner.SizeChanged -= OnOwnerMoved;
owner.StateChanged -= OnOwnerStateChanged;
owner.Closing -= OnOwnerClosing;
}
private static void OnOwnerClosing(object sender, CancelEventArgs e)
{
if (sender is Window owner)
{
// 不需要手动关闭通知因为设置了Owner会自动关闭
// 但需要取消订阅
UnsubscribeFromOwnerEvents(owner);
}
}
private static void OnOwnerStateChanged(object sender, EventArgs e)
{
if (sender is Window owner)
{
var notifications = GetNotificationListForOwner(owner);
var visibility = (owner.WindowState == WindowState.Minimized) ? Visibility.Collapsed : Visibility.Visible;
foreach (var list in notifications.Values)
{
foreach (var notificationView in list)
{
notificationView.Visibility = visibility;
}
}
// 恢复时重新定位
if (visibility == Visibility.Visible)
{
RepositionAll(owner);
}
}
}
private static void OnOwnerMoved(object sender, EventArgs e)
{
if (sender is Window owner)
{
RepositionAll(owner);
}
}
private static void RepositionAll(Window owner)
{
Reposition(owner, NotificationPlacement.TopRight);
Reposition(owner, NotificationPlacement.TopLeft);
Reposition(owner, NotificationPlacement.BottomRight);
Reposition(owner, NotificationPlacement.BottomLeft);
}
private static void Reposition(Window owner, NotificationPlacement placement)
{
var activeNotifications = GetNotificationListForOwner(owner);
var notifications = activeNotifications[placement];
if (notifications.Count == 0) return;
// 获取定位基准区域
Rect workArea;
if (owner != null && owner.IsLoaded)
{
// 如果父窗口最小化,则不进行重定位
if (owner.WindowState == WindowState.Minimized) return;
workArea = new Rect(owner.Left, owner.Top, owner.ActualWidth, owner.ActualHeight);
}
else
{
workArea = SystemParameters.WorkArea;
}
double currentTop = workArea.Top + 10;
double currentBottom = workArea.Bottom - 10;
foreach (var window in notifications)
{
// 确保窗口已加载以获取其实际尺寸
if (!window.IsLoaded)
{
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Left = -9999;
window.Top = -9999;
window.Show();
window.Activate(); // 激活以确保尺寸计算完成
}
if (placement == NotificationPlacement.TopRight || placement == NotificationPlacement.TopLeft)
{
window.Top = currentTop;
currentTop += window.ActualHeight + 10;
}
else // BottomRight or BottomLeft
{
window.Top = currentBottom - window.ActualHeight;
currentBottom -= (window.ActualHeight + 10);
}
if (placement == NotificationPlacement.TopRight || placement == NotificationPlacement.BottomRight)
{
window.Left = workArea.Right - window.ActualWidth - 10;
}
else // TopLeft or BottomLeft
{
window.Left = workArea.Left + 10;
}
}
}
}