328 lines
12 KiB
C#
328 lines
12 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|
||
} |