在桌面应用里,通知提示框看似只是一个小组件,但真正落到工业监控、金融终端或企业业务系统中,要求通常并不低:既要不打断操作,又要稳定显示,还得支持多条消息连续弹出。本文基于 WPF 和 C#,把一个接近 Windows 风格的 Toast 提示框拆成界面设计、主窗口调度和通知窗口动画三部分,方便你判断这套实现是否适合直接用于现有项目,或者作为后续封装的基础。
这个通知提示框能实现什么效果
示例中的主窗口只保留一个触发按钮。每次点击后,系统都会在屏幕右下角弹出一个新的提示框,并按顺序堆叠显示。
每个提示框包含标题、正文内容和关闭按钮,同时具备以下几个核心特性:
- 从右下角滑入显示,退出时再滑出;
- 支持多条消息连续弹出,彼此不会重叠;
- 默认显示
5秒后自动关闭; - 用户也可以手动点击“×”立即关闭。
这一套行为组合起来,基本就是很多业务系统里常见的轻量级消息推送机制。
提示框界面如何设计
界面层采用 WPF 的无边框窗口思路,去掉默认标题栏和系统边框,让提示框更接近系统通知的视觉形态。同时把窗口保持在最上层,避免被业务主界面遮挡。
原文给出的 XAML 代码块没有完整展开,但从属性设计上看,核心配置如下:
这里最关键的是三个属性:
ResizeMode="NoResize":禁止用户调整大小,避免通知窗体形态失控;WindowStyle="None":移除默认窗口样式,便于自定义视觉效果;Topmost="True":保证提示框始终位于前景层。
如果你后续还要继续打磨样式,通常会在这个基础上再补上圆角、阴影、边距、关闭按钮悬停态,以及标题和正文的排版约束。
主窗口怎样管理多条通知
先定义通知数据模型
主窗口和提示框之间通过一个简单的数据模型传递标题与内容:
class NotifyData
{
public string Title { get; set; }
public string Content { get; set; }
}
这个模型很轻,已经足够支撑基础提示功能。若你的项目需要区分消息级别,后续也可以继续扩展,比如增加 Type、Duration、Icon 等字段。
创建、堆叠与回收提示框
真正控制通知生成的是主窗口后台逻辑:
public partial class MainWindow : Window
{
int i = 0;
public static List _dialogs = new List();
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
i++;
NotifyData data = new NotifyData();
data.Title = "提示";
data.Content = "XXX余额不足,XXX余额不足,XXX余额不足,XXX余额不足" + i;
Window1 dialog = new Window1();
dialog.Closed += Dialog_Closed;
dialog.TopFrom = GetTopFrom();
dialog.DataContext = data;
dialog.Show();
_dialogs.Add(dialog);
}
private void Dialog_Closed(object sender, EventArgs e)
{
var closedDialog = sender as Window1;
_dialogs.Remove(closedDialog);
}
double GetTopFrom()
{
double topFrom = System.Windows.SystemParameters.WorkArea.Bottom - 10;
bool isContinueFind = _dialogs.Any(o => o.TopFrom == topFrom);
while (isContinueFind)
{
topFrom = topFrom - 160;
isContinueFind = _dialogs.Any(o => o.TopFrom == topFrom);
}
if (topFrom <= 0)
topFrom = System.Windows.SystemParameters.WorkArea.Bottom - 10;
return topFrom;
}
}
这段代码的重点不是“弹一个窗”,而是把多窗体管理的几个关键问题一起处理掉了:
- 点击按钮时创建新的
NotifyData和Window1实例; - 通过
DataContext把标题和内容传给提示框; - 用
_dialogs统一记录当前已打开的通知窗口; - 在
Closed事件中回收实例,避免列表残留失效对象。
为什么它能避免多个提示框重叠
GetTopFrom() 是这套实现里最实用的一部分。它以工作区底部 System.Windows.SystemParameters.WorkArea.Bottom - 10 作为起点,先把第一条通知放在右下角,再逐条向上偏移 160 像素,直到找到一个未被占用的位置。
逻辑上可以概括为:
- 先取屏幕工作区底部位置作为初始坐标;
- 检查这个坐标是否已被现有通知使用;
- 如果已占用,就继续减去
160; - 如果已经超出顶部范围,就重新回到底部位置。
这意味着在连续触发通知时,消息会按固定间距向上堆叠,而不是直接覆盖上一条。对于企业系统常见的批量提醒场景,这种定位方式足够直观,也便于后续统一调整间距和高度。
通知窗口如何实现滑动动画和自动关闭
通知窗口自身负责三件事:接收数据、进入动画、退出动画。原文给出的核心代码如下:
public partial class Window1 : Window
{
public double TopFrom { get; set; }
public Window1()
{
InitializeComponent();
this.Loaded += NotificationWindow_Loaded;
}
private void NotificationWindow_Loaded(object sender, RoutedEventArgs e)
{
NotifyData data = this.DataContext as NotifyData;
if (data != null)
{
tbContent.Text = data.Content;
tbTitle.Text = data.Title;
}
Window1 self = sender as Window1;
if (self != null)
{
double right = SystemParameters.WorkArea.Right - 10;
self.Top = TopFrom - 160;
DoubleAnimation animation = new DoubleAnimation();
animation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
animation.From = right;
animation.To = right - self.ActualWidth;
self.BeginAnimation(Window.LeftProperty, animation);
Task.Factory.StartNew(delegate
{
int seconds = 5;
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(seconds));
this.Dispatcher.Invoke(delegate
{
animation = new DoubleAnimation();
animation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
animation.Completed += (s, a) => { self.Close(); };
animation.From = right - self.ActualWidth;
animation.To = right;
self.BeginAnimation(Window.LeftProperty, animation);
});
});
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
double right = SystemParameters.WorkArea.Right;
DoubleAnimation animation = new DoubleAnimation();
animation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
animation.Completed += (s, a) => { this.Close(); };
animation.From = right - this.ActualWidth;
animation.To = right;
this.BeginAnimation(Window.LeftProperty, animation);
}
}
加载时先完成内容绑定与定位
在 NotificationWindow_Loaded 事件里,窗口首先从 DataContext 中读取 NotifyData,再把标题和内容赋给 tbTitle 与 tbContent。随后根据 TopFrom 和屏幕右侧工作区坐标计算当前位置。
这里的处理顺序很重要:先拿到内容,再算位置,最后执行动画,能保证弹出的提示框在视觉上是一次性完整出现的。
进入和退出动画是怎么做的
动画效果由 DoubleAnimation 完成,移动目标是窗口的 LeftProperty:
- 进入动画:从屏幕右侧边缘开始,持续
500毫秒滑入; - 退出动画:显示结束后,再用同样的
500毫秒从当前位置滑出; - 退出完成后:通过
animation.Completed += (s, a) => { self.Close(); };真正关闭窗口。
这种做法的优点是实现简单,而且用户能明确感知到消息的出现与消失,不会像瞬时弹出、瞬时消失那样突兀。
自动关闭与手动关闭如何配合
自动关闭由下面这段逻辑触发:
- 使用
Task.Factory.StartNew启动后台任务; - 通过
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(seconds))等待5秒; - 再借助
Dispatcher.Invoke切回 UI 线程执行关闭动画。
同时,窗口上的“×”按钮会调用 Button_Click,直接执行同一套滑出动画,并在动画结束后关闭当前提示框。
也就是说,这个组件同时提供了两种退出路径:
- 用户无操作时,
5秒后自动关闭; - 用户希望立即处理时,可以手动关闭。
这也是桌面通知体验里比较常见、也比较稳妥的交互方式。
这套实现适合用在哪些场景
从功能边界来看,这个 WPF 通知提示框已经具备了基础可用性,适合直接放进以下类型的桌面项目中:
- 工业监控系统中的设备状态提醒;
- 金融或交易系统中的余额、风控、成交提示;
- 企业内部系统中的审批、消息、异常反馈;
- 桌面客户端里的轻量级事件通知。
它的工程价值主要体现在四点:
- 支持多消息堆叠显示;
- 自动计算弹窗位置,减少遮挡;
- 具备滑动动画,交互反馈更完整;
- 结构清晰,便于继续封装和复用。
如果你后续准备进一步工程化,优先可以考虑的方向包括:统一样式资源、封装消息队列、增加不同通知类型,以及把时长、位置和动画参数抽成可配置项。















