WPF Toast 消息提示组件:纯代码实现,零依赖即插即用

做过 WPF 开发的同学一定遇到过这个需求:操作成功弹个绿色提示,操作失败弹个红色警告。在 Web 端有 Element UI 的 ElMessage,在 Android 有 Toast.makeText(),但 WPF 原生并没有提供类似组件。

本文分享一个纯 C# 代码实现的 Toast 组件,不需要写任何 XAML,复制一个 .cs 文件到项目里就能用。

效果预览

  • 从窗口底部弹出,带淡入 + 上滑动画
  • 支持四种类型:成功(绿)、失败(红)、警告(橙)、信息(蓝)
  • 多条 Toast 自动从下往上堆叠,互不遮挡
  • 自动消失(默认 3 秒),带淡出动画

完整代码

新建 MToast.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Threading;

namespace WpfApp.Component
{
/// <summary>
/// Toast 类型
/// </summary>
public enum ToastType
{
Success,
Error,
Warning,
Info
}

/// <summary>
/// 全局 Toast 消息提示组件(纯代码实现,零 XAML 依赖)
///
/// 使用方式:
/// MToast.Success("保存成功");
/// MToast.Error("网络请求失败");
/// MToast.Warning("请注意,余额不足");
/// MToast.Info("这是一条信息提示");
/// </summary>
public class MToast
{
#region 单例

private static MToast? _instance;
private static readonly object _lock = new();
private readonly List<Border> _activeToasts = new();

public static MToast Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
_instance ??= new MToast();
}
}
return _instance;
}
}

#endregion

#region 快捷方法

/// <summary>
/// 显示 Toast 提示
/// </summary>
/// <param name="message">提示内容</param>
/// <param name="type">类型:Success / Error / Warning / Info</param>
/// <param name="duration">持续时间(毫秒),默认 3000ms</param>
public static void Show(string message, ToastType type = ToastType.Info, int duration = 3000)
{
Instance.ShowInternal(message, type, duration);
}

public static void Success(string message, int duration = 3000)
{
Show(message, ToastType.Success, duration);
}

public static void Error(string message, int duration = 3000)
{
Show(message, ToastType.Error, duration);
}

public static void Warning(string message, int duration = 3000)
{
Show(message, ToastType.Warning, duration);
}

public static void Info(string message, int duration = 3000)
{
Show(message, ToastType.Info, duration);
}

#endregion

#region 核心逻辑

private void ShowInternal(string message, ToastType type, int duration)
{
var toast = CreateToast(message, type);

// 获取当前活动窗口
var window = Application.Current?.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive)
?? Application.Current?.MainWindow;
if (window == null) { return; }

var container = GetContainer(window);
if (container == null) { return; }

// 计算位置(从底部向上排列)
int offset = _activeToasts.Count * 60;
toast.Margin = new Thickness(0, 0, 0, 20 + offset);

container.Children.Add(toast);
_activeToasts.Add(toast);

// 入场动画:淡入 + 上滑
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
toast.BeginAnimation(UIElement.OpacityProperty, fadeIn);

var slideIn = new DoubleAnimation(30, 0, TimeSpan.FromMilliseconds(200))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
var transform = new TranslateTransform();
toast.RenderTransform = transform;
transform.BeginAnimation(TranslateTransform.YProperty, slideIn);

// 自动关闭
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(duration)
};
timer.Tick += (s, e) =>
{
timer.Stop();
HideToast(toast);
};
timer.Start();
}

private void HideToast(Border toast)
{
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200));
fadeOut.Completed += (s, e) =>
{
var parent = VisualTreeHelper.GetParent(toast) as Panel;
parent?.Children.Remove(toast);
_activeToasts.Remove(toast);
};
toast.BeginAnimation(UIElement.OpacityProperty, fadeOut);
}

#endregion

#region 创建 Toast 元素

private static Border CreateToast(string message, ToastType type)
{
var (icon, bgColor) = GetToastStyle(type);

return new Border
{
Background = bgColor,
CornerRadius = new CornerRadius(8),
Padding = new Thickness(16, 12, 16, 12),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Bottom,
Effect = new DropShadowEffect
{
BlurRadius = 10,
ShadowDepth = 2,
Opacity = 0.3
},
Child = new StackPanel
{
Orientation = Orientation.Horizontal,
Children =
{
new TextBlock
{
Text = icon,
FontSize = 18,
Foreground = Brushes.White,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
FontFamily = new FontFamily("Segoe UI Symbol")
},
new TextBlock
{
Text = message,
Foreground = Brushes.White,
FontSize = 14,
VerticalAlignment = VerticalAlignment.Center
}
}
}
};
}

/// <summary>
/// 根据类型返回图标和背景色
/// </summary>
private static (string icon, SolidColorBrush bgColor) GetToastStyle(ToastType type)
{
return type switch
{
ToastType.Success => ("✓", new SolidColorBrush(Color.FromRgb(76, 175, 80))),
ToastType.Error => ("✗", new SolidColorBrush(Color.FromRgb(244, 67, 54))),
ToastType.Warning => ("⚠", new SolidColorBrush(Color.FromRgb(255, 152, 0))),
_ => ("ℹ", new SolidColorBrush(Color.FromRgb(33, 150, 243)))
};
}

#endregion

#region 容器获取

/// <summary>
/// 获取窗口的根容器(Grid),不是 Grid 则自动包装一层
/// </summary>
private static Panel? GetContainer(Window window)
{
if (window.Content is Grid grid)
{
return grid;
}

// 根元素不是 Grid,包装一层
var oldContent = window.Content;
var newRoot = new Grid();
window.Content = newRoot;

if (oldContent != null)
{
newRoot.Children.Add(oldContent as UIElement);
}

return newRoot;
}

#endregion
}
}

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 成功提示
MToast.Success("保存成功");

// 错误提示
MToast.Error("网络请求失败");

// 警告提示
MToast.Warning("余额不足");

// 普通信息
MToast.Info("这是一条信息提示");

// 自定义持续时间(5秒)
MToast.Show("这条消息显示5秒", ToastType.Info, 5000);

设计要点

1. 自动容器适配

Toast 需要依附到一个 Panel 上才能显示。组件会自动检查当前窗口的根元素:

  • 如果是 Grid → 直接使用
  • 如果不是 → 自动包装一层 Grid,把原内容放进去

这样不管你的窗口根元素是什么,都能正常工作。

2. 多 Toast 堆叠

通过 _activeToasts 列表追踪当前活跃的 Toast,每新增一个就在底部偏移 count * 60px,实现从下往上堆叠的效果。Toast 消失后自动从列表移除。

3. 入场/退场动画

  • 入场Opacity 0→1(200ms 淡入)+ TranslateTransform.Y 30→0(200ms 上滑),缓动函数用 CubicEase.Out
  • 退场Opacity 1→0(200ms 淡出),动画完成后从父容器移除

4. 纯代码零依赖

整个组件不需要任何 XAML 文件,不需要第三方库,只依赖 WPF 原生的 System.Windows 命名空间。一个 .cs 文件搞定。