Files
ShrlAlgoToolkit/AntDesignWPF/Controls/Notification/NotificationManager.cs

328 lines
12 KiB
C#
Raw Normal View History

2025-07-31 20:12:24 +08:00
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;
}
}
}
}