WPF 右下角通知弹窗组件:支持自动关闭和倒计时

桌面应用经常需要"非阻塞"的通知提示——不像 MessageBox 那样必须点确定才能继续操作,而是在屏幕角落弹一个通知,过几秒自动消失。

本文分享一个右下角通知弹窗组件,带自动倒计时关闭,支持自定义按钮。

效果预览

  • 从屏幕右下角弹出,带淡入/淡出动画
  • 标题 + 内容 + 可选的确认/取消按钮
  • 支持自动倒计时关闭(显示剩余秒数)
  • 纯静态方法调用,一行代码弹出

MNotification.xaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<Window x:Class="WpfApp.Component.MNotification"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None"
ResizeMode="NoResize"
WindowStartupLocation="Manual"
AllowsTransparency="True"
Background="Transparent"
SizeToContent="WidthAndHeight"
MinWidth="300"
MaxWidth="450"
Topmost="True"
ShowInTaskbar="False">

<Border CornerRadius="8"
Background="#F5F5F5"
BorderBrush="#CCCCCC"
BorderThickness="1"
Padding="20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<!-- 关闭按钮 -->
<Button x:Name="CloseButton"
Grid.Row="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Width="24" Height="24"
Background="Transparent"
BorderBrush="Transparent"
Content="✕"
FontSize="12"
Foreground="#999999"
Cursor="Hand"
Click="OnCloseClick"/>

<!-- 标题 -->
<TextBlock x:Name="TitleText"
Grid.Row="0"
Text="提示"
FontSize="18"
FontWeight="Medium"
Foreground="#333333"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,0,30,0"/>

<!-- 内容 -->
<TextBlock x:Name="MessageText"
Grid.Row="1"
Text="这是一条提示信息"
FontSize="14"
Foreground="#666666"
TextWrapping="Wrap"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,20,0,20"/>

<!-- 按钮区域 -->
<StackPanel Grid.Row="2"
x:Name="ButtonPanel"
Orientation="Horizontal"
HorizontalAlignment="Center"
Visibility="Collapsed">
<Button x:Name="CancelButton"
Content="取消"
MinWidth="80"
Height="32"
Background="Transparent"
BorderBrush="#CCCCCC"
BorderThickness="1"
Foreground="#666666"
Margin="0,0,10,0"
Click="OnCancelClick"/>
<Button x:Name="ConfirmButton"
Content="确定"
MinWidth="80"
Height="32"
Background="#2196F3"
BorderBrush="Transparent"
Foreground="White"
Cursor="Hand"
Click="OnConfirmClick"/>
</StackPanel>
</Grid>
</Border>
</Window>

MNotification.xaml.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Threading;

namespace WpfApp.Component
{
/// <summary>
/// 通用通知弹窗,位于屏幕右下角
///
/// 使用方式:
/// MNotification.Show("提示", "操作成功");
/// MNotification.Show("余额不足", "请及时充值", confirmText: "去充值");
/// MNotification.Show("提示", "3秒后关闭", autoCloseSeconds: 3);
/// </summary>
public partial class MNotification : Window
{
#region 字段

private DispatcherTimer? _closeTimer;
private int _countdownSeconds = 5;

#endregion

#region 构造

public MNotification()
{
InitializeComponent();
Loaded += OnLoaded;
}

#endregion

#region 静态调用

/// <summary>
/// 显示通知
/// </summary>
/// <param name="title">标题</param>
/// <param name="message">提示内容</param>
/// <param name="autoCloseSeconds">自动关闭秒数,0=不自动关闭</param>
/// <param name="confirmText">确定按钮文字,null=隐藏</param>
/// <param name="cancelText">取消按钮文字,null=隐藏</param>
public static void Show(string title, string message,
int autoCloseSeconds = 5,
string? confirmText = "确定",
string? cancelText = null)
{
var notification = new MNotification();
notification.TitleText.Text = title;
notification.MessageText.Text = message;

// 设置按钮
if (confirmText != null)
{
notification.ConfirmButton.Content = confirmText;
notification.ConfirmButton.Visibility = Visibility.Visible;
notification.ButtonPanel.Visibility = Visibility.Visible;
}
else
{
notification.ConfirmButton.Visibility = Visibility.Collapsed;
}

if (cancelText != null)
{
notification.CancelButton.Content = cancelText;
notification.CancelButton.Visibility = Visibility.Visible;
notification.ButtonPanel.Visibility = Visibility.Visible;
}
else
{
notification.CancelButton.Visibility = Visibility.Collapsed;
}

// 自动关闭
if (autoCloseSeconds > 0)
{
notification._countdownSeconds = autoCloseSeconds;
notification.StartCountdown();
}

notification.Show();
}

#endregion

#region 窗口定位与动画

private void OnLoaded(object sender, RoutedEventArgs e)
{
// 定位到屏幕右下角
var workingArea = SystemParameters.WorkArea;
Left = workingArea.Right - ActualWidth - 20;
Top = workingArea.Bottom - ActualHeight - 20;

// 淡入动画
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
BeginAnimation(OpacityProperty, fadeIn);
}

#endregion

#region 倒计时

private void StartCountdown()
{
_closeTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};

_closeTimer.Tick += (s, e) =>
{
_countdownSeconds--;

if (_countdownSeconds <= 0)
{
_closeTimer!.Stop();
CloseWithAnimation();
}
};

_closeTimer.Start();
}

private void CloseWithAnimation()
{
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200));
fadeOut.Completed += (s, e) => Close();
BeginAnimation(OpacityProperty, fadeOut);
}

#endregion

#region 事件处理

private void OnCloseClick(object sender, RoutedEventArgs e)
{
_closeTimer?.Stop();
CloseWithAnimation();
}

private void OnCancelClick(object sender, RoutedEventArgs e)
{
_closeTimer?.Stop();
CloseWithAnimation();
}

private void OnConfirmClick(object sender, RoutedEventArgs e)
{
_closeTimer?.Stop();
CloseWithAnimation();
}

#endregion
}
}

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基本用法(5秒后自动关闭)
MNotification.Show("提示", "操作成功");

// 纯通知(无按钮,3秒自动消失)
MNotification.Show("", "已复制到剪贴板", autoCloseSeconds: 3, confirmText: null);

// 带确认按钮
MNotification.Show("余额不足", "请及时充值", confirmText: "去充值");

// 带确认和取消
MNotification.Show("新版本可用", "检测到新版本,是否立即更新?",
confirmText: "更新", cancelText: "稍后");

// 不自动关闭(需用户手动点按钮)
MNotification.Show("重要通知", "请仔细阅读此内容", autoCloseSeconds: 0);

设计要点

1. 右下角精准定位

Loaded 事件里通过 SystemParameters.WorkArea 获取屏幕工作区(排除任务栏),计算窗口位置到右下角偏移 20px:

1
2
Left = workingArea.Right - ActualWidth - 20;
Top = workingArea.Bottom - ActualHeight - 20;

必须在 Loaded 后定位,因为 SizeToContent="WidthAndHeight" 模式下窗口实际宽高在加载完成后才确定。

2. DispatcherTimer 倒计时

DispatcherTimer 每秒减一,到 0 时触发 CloseWithAnimation()。手动点关闭/确认/取消按钮时先 _closeTimer?.Stop() 停掉计时器,避免重复关闭。

3. 灵活的按钮配置

confirmTextcancelText 参数都接受 null

confirmText cancelText 效果
"确定" null 只显示确定按钮
"更新" "稍后" 显示两个按钮
null null 不显示按钮(纯通知)

4. Topmost 置顶

Topmost="True" 确保通知始终在最前面,即使用户正在操作其他窗口也能看到。

5. 不显示在任务栏

ShowInTaskbar="False" 避免通知窗口在任务栏占位置,它只是一个临时提示。