月更
This commit is contained in:
328
AntDesignWPF/Controls/Notification/NotificationManager.cs
Normal file
328
AntDesignWPF/Controls/Notification/NotificationManager.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
AntDesignWPF/Controls/Notification/NotificationView.xaml
Normal file
80
AntDesignWPF/Controls/Notification/NotificationView.xaml
Normal file
@@ -0,0 +1,80 @@
|
||||
<Window x:Class="AntDesignWPF.Controls.NotificationView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Width="384"
|
||||
Height="Auto"
|
||||
mc:Ignorable="d"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Loaded="Window_Loaded"
|
||||
ShowInTaskbar="False"
|
||||
SizeToContent="Height"
|
||||
Title="NotificationWindow"
|
||||
Topmost="True"
|
||||
WindowStyle="None">
|
||||
<Grid>
|
||||
<Border Name="RootBorder" Margin="8" Background="{DynamicResource AntDesign.Brush.BackgroundContainer}" CornerRadius="4" BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect ShadowDepth="0" BlurRadius="10" Color="#000000" Opacity="0.1"/>
|
||||
</Border.Effect>
|
||||
<Grid Margin="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Icon Section (CORRECTED) -->
|
||||
<Path Name="IconPath" Grid.Column="0" Stretch="Uniform" Width="20" Height="20" Margin="0,0,16,0" VerticalAlignment="Top">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Style.Triggers>
|
||||
<!-- Success -->
|
||||
<DataTrigger Binding="{Binding Type}" Value="Success">
|
||||
<Setter Property="Data" Value="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
<Setter Property="Fill" Value="{StaticResource AntDesign.Brush.Success}"/>
|
||||
</DataTrigger>
|
||||
<!-- Info -->
|
||||
<DataTrigger Binding="{Binding Type}" Value="Info">
|
||||
<Setter Property="Data" Value="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
||||
<Setter Property="Fill" Value="{StaticResource AntDesign.Brush.Info}"/>
|
||||
</DataTrigger>
|
||||
<!-- Warning -->
|
||||
<DataTrigger Binding="{Binding Type}" Value="Warning">
|
||||
<Setter Property="Data" Value="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
||||
<Setter Property="Fill" Value="{StaticResource AntDesign.Brush.Warning}"/>
|
||||
</DataTrigger>
|
||||
<!-- Error -->
|
||||
<DataTrigger Binding="{Binding Type}" Value="Error">
|
||||
<Setter Property="Data" Value="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
<Setter Property="Fill" Value="{StaticResource AntDesign.Brush.Error}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
|
||||
<!-- Content Section -->
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Name="TitleTextBlock" Text="{Binding Title}" FontWeight="Bold" FontSize="16" Foreground="{DynamicResource AntDesign.Brush.TextPrimary}" Margin="0,0,0,4"/>
|
||||
<TextBlock Name="MessageTextBlock" Text="{Binding Message}" Foreground="{DynamicResource AntDesign.Brush.TextSecondary}" TextWrapping="Wrap" FontSize="14"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Close Button -->
|
||||
<Button Grid.Column="2" Name="CloseButton" VerticalAlignment="Top" Margin="8,-4,-4,-4" Click="CloseButton_Click" Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid Background="Transparent">
|
||||
<Path Data="M 0,0 L 8,8 M 8,0 L 0,8" Stroke="#999999" StrokeThickness="1.5" VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- The <Grid.Style> block has been removed from here -->
|
||||
</Grid>
|
||||
</Window>
|
||||
71
AntDesignWPF/Controls/Notification/NotificationView.xaml.cs
Normal file
71
AntDesignWPF/Controls/Notification/NotificationView.xaml.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AntDesignWPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// NotificationWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class NotificationView : Window
|
||||
{
|
||||
//public NotificationWindow()
|
||||
//{
|
||||
// InitializeComponent();
|
||||
//}
|
||||
private readonly DispatcherTimer _closeTimer;
|
||||
|
||||
// Dependency Properties to allow data binding
|
||||
public string Title { get; set; }
|
||||
public string Message { get; set; }
|
||||
public NotificationType Type { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
|
||||
public NotificationView(string title, string message, NotificationType type, TimeSpan duration)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = title;
|
||||
Message = message;
|
||||
Type = type;
|
||||
Duration = duration;
|
||||
|
||||
DataContext = this;
|
||||
|
||||
if (duration > TimeSpan.Zero)
|
||||
{
|
||||
_closeTimer = new DispatcherTimer { Interval = duration };
|
||||
_closeTimer.Tick += (s, e) => CloseAnimation();
|
||||
_closeTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_closeTimer?.Stop();
|
||||
CloseAnimation();
|
||||
}
|
||||
|
||||
private void CloseAnimation()
|
||||
{
|
||||
var anim = new DoubleAnimation(0, TimeSpan.FromSeconds(0.3));
|
||||
anim.Completed += (s, _) => Close();
|
||||
BeginAnimation(OpacityProperty, anim);
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Start open animation
|
||||
var anim = new DoubleAnimation(0, 1, TimeSpan.FromSeconds(0.3));
|
||||
BeginAnimation(OpacityProperty, anim);
|
||||
}
|
||||
}
|
||||
|
||||
// Enum to define notification types
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user