This commit is contained in:
GG Z
2025-07-31 20:12:24 +08:00
parent 4f6cd2137c
commit f209e7d3ad
426 changed files with 15854 additions and 6612 deletions

View 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;
}
}
}
}

View 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>

View 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
}
}