WPF UI 系统托盘集成指南:Wpf.Ui.Tray 模块的架构、用法与 Win32 实现原理
【免费下载链接】wpfuiWPF UI provides the Fluent experience in your known and loved WPF framework. Intuitive design, themes, navigation and new immersive controls. All natively and effortlessly.项目地址: https://gitcode.com/GitHub_Trending/wp/wpfui
Wpf.Ui.Tray 是 WPF UI 库中负责系统托盘(通知区域)集成的独立模块,它为 WPF 应用提供原生托盘图标、右键菜单与鼠标事件能力。本文以该模块的源码为据,完整覆盖从 XAML 控件、服务式 API 到 Shell_NotifyIcon 底层调用链的实现细节,帮助你快速把托盘图标接入自己的 WPF UI 应用,并理解其背后"隐藏窗口 + 回调消息"的工作原理。
模块概览与引用方式
Wpf.Ui.Tray 位于仓库 src/Wpf.Ui.Tray,是一个独立程序集,其工程文件 Wpf.Ui.Tray.csproj 中声明的包名为WPF-UI.Tray,目标框架为net10.0-windows、net9.0-windows、net8.0-windows、net481、net472、net462,并直接项目引用 src/Wpf.Ui/Wpf.Ui.csproj,即依赖 WPF UI 主库。
使用方式有两种:
- 在工程中直接引用
Wpf.Ui.Tray项目(如 src/Wpf.Ui.Gallery/Wpf.Ui.Gallery.csproj 所示); - 或在 XAML 中引入托盘命名空间:
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"模块包含的核心类型
模块 README(src/Wpf.Ui.Tray/README.md)列出了全部对外类型,按职责可划分为四层:
| 类型 | 命名空间 | 职责 |
|---|---|---|
NotifyIcon | Wpf.Ui.Tray.Controls | 作为FrameworkElement的托盘图标控件,可在 XAML 中声明并使用依赖属性与路由事件 |
NotifyIconService/INotifyIconService | Wpf.Ui.Tray | 面向代码的服务式 API,负责注册/注销图标、设置父窗口 |
TrayManager/TrayHandler/TrayData | Wpf.Ui.Tray | 内部核心:管理与 Shell32 交互、消息钩子窗口、图标注册表 |
RoutedNotifyIconEvent/NotifyIconEventHandler/INotifyIcon/Hicon | Wpf.Ui.Tray | 事件委托、内部契约与 HICON 转换工具 |
其中TrayManager、TrayHandler、TrayData、INotifyIcon、Hicon、NotifyIconEventHandler均为internal类型,对外可见的是控件与服务两个入口。
方式一:XAML 控件式用法(推荐)
NotifyIcon继承自System.Windows.FrameworkElement并实现IDisposable(见 src/Wpf.Ui.Tray/Controls/NotifyIcon.cs),可以直接放进窗口的 XAML 树中。仓库内多个示例项目都采用这种写法,例如 samples/Wpf.Ui.Demo.Mvvm/Views/MainWindow.xaml:
<tray:NotifyIcon Grid.Row="0" FocusOnLeftClick="True" Icon="pack://application:,,,/Assets/applicationIcon-256.png" MenuOnRightClick="True" TooltipText="WPF UI - MVVM Demo"> <tray:NotifyIcon.Menu> <ContextMenu ItemsSource="{Binding ViewModel.TrayMenuItems, Mode=OneWay}" /> </tray:NotifyIcon.Menu> </tray:NotifyIcon>控件注册了以下依赖属性(均为PropertyMetadata声明于 src/Wpf.Ui.Tray/Controls/NotifyIcon.cs):
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TooltipText | string | 空字符串 | 鼠标悬停时显示的提示文本 |
Icon | ImageSource | null | 托盘图标,通常用pack://URI 指向应用资源 |
Menu | ContextMenu | null | 右键菜单,支持数据绑定 |
MenuOnRightClick | bool | true | 单击右键时是否弹出Menu |
FocusOnLeftClick | bool | true | 单击左键时是否聚焦Application.MainWindow |
MenuFontSize | double | 14d | 菜单字体大小 |
与普通 WPF 控件不同,托盘图标无需你手动调用注册方法:NotifyIcon重写了OnRender(src/Wpf.Ui.Tray/Controls/NotifyIcon.cs),在首次渲染且尚未注册时自动完成InitializeIcon()与Register()。也就是说,把控件放进 XAML 树即可生效。
鼠标事件:六个路由事件
NotifyIcon注册了六个以Bubble路由策略传播的RoutedEvent(src/Wpf.Ui.Tray/Controls/NotifyIcon.cs),对应左/右/中键的单击与双击:
<tray:NotifyIcon LeftClick="OnTrayLeftClick" LeftDoubleClick="OnTrayLeftDoubleClick" ... />private void OnTrayLeftClick(object sender, RoutedEventArgs e) { // 处理单击事件 }事件委托类型为RoutedNotifyIconEvent(定义于 src/Wpf.Ui.Tray/RoutedNotifyIconEvent.cs),签名为void (NotifyIcon sender, RoutedEventArgs e)。注意:FocusOnLeftClick与MenuOnRightClick的默认行为(聚焦主窗口、弹菜单)仍然会执行,事件只是在此基础上的额外通知。
菜单数据绑定与菜单点击处理
仓库的 Gallery 项目给出了完整的"托盘菜单 + 点击分发"范例。菜单项在 src/Wpf.Ui.Gallery/ViewModels/Windows/MainWindowViewModel.cs 中以ObservableCollection<Control>声明,每个MenuItem通过Tag标记动作:
[ObservableProperty] private ObservableCollection<Control> _trayMenuItems = [ new Wpf.Ui.Controls.MenuItem { Header = "Home", Tag = "tray_home", Icon = new SymbolIcon { Symbol = SymbolRegular.Home24 } }, new Wpf.Ui.Controls.MenuItem { Header = "Settings", Tag = "tray_settings", Icon = new SymbolIcon { Symbol = SymbolRegular.Settings24 } }, new Separator(), new Wpf.Ui.Controls.MenuItem { Header = "Close", Tag = "tray_close", Icon = new SymbolIcon { Symbol = SymbolRegular.Dismiss24 } }, ];在 src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml.cs 中,窗口构造函数遍历菜单项订阅Click事件,并按Tag分发:
private void OnTrayMenuItemClick(object sender, RoutedEventArgs e) { if (sender is not Wpf.Ui.Controls.MenuItem menuItem) return; switch (menuItem.Tag?.ToString()) { case "tray_home": HandleTrayHomeClick(); // 恢复窗口并导航到主页 break; case "tray_settings": HandleTraySettingsClick(); // 恢复窗口并导航到设置页 break; case "tray_close": HandleTrayCloseClick(); // Application.Current.Shutdown(); break; } }这种"菜单项绑定 + Tag 分发"是托盘菜单的典型实践。XAML 侧还需要为ContextMenu提供正确的DataContext才能完成绑定,Gallery 的做法是通过Source={x:Reference NavigationView}显式指定绑定源(src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml)。
NotifyIcon自身也会维护DataContext到Menu的传递:构造函数订阅DataContextChanged,并在OnMenuChanged中为没有DataContext的菜单补充当前数据上下文(src/Wpf.Ui.Tray/Controls/NotifyIcon.cs),这使得 MVVM 场景下菜单项绑定开箱即用。
方式二:服务式 API(代码驱动)
如果你更习惯用代码而非 XAML 驱动,可以使用NotifyIconService。它实现接口INotifyIconService(契约见 src/Wpf.Ui.Tray/INotifyIconService.cs),核心成员包括:
- 属性:
Id(图标在 Shell 中的标识)、IsRegistered(是否已注册)、TooltipText、ContextMenu、Icon; - 方法:
Register()/Unregister()/SetParentWindow(Window)。
典型用法(实现位于 src/Wpf.Ui.Tray/NotifyIconService.cs):
var trayService = new NotifyIconService { TooltipText = "WPF UI App", Icon = new BitmapImage(new Uri("pack://application:,,,/Assets/app.png")), ContextMenu = myContextMenu, }; trayService.SetParentWindow(mainWindow); // 绑定父窗口,窗口关闭时自动释放图标 trayService.Register(); // 注册到系统托盘SetParentWindow会订阅父窗口的Closing事件(src/Wpf.Ui.Tray/NotifyIconService.cs),父窗口关闭时自动Dispose并注销托盘图标。服务类还提供六个受保护的虚方法OnLeftClick、OnLeftDoubleClick、OnRightClick、OnRightDoubleClick、OnMiddleClick、OnMiddleDoubleClick(默认空实现),供你继承重写鼠标行为,与控件的六个路由事件一一对应。
底层原理:Shell_NotifyIcon 与消息钩子
托盘功能最终都汇聚到内部静态类TrayManager(src/Wpf.Ui.Tray/TrayManager.cs),它负责与 Windows Shell 的全部交互,其底层是 Shell32 的Shell_NotifyIconP/Invoke(声明于 src/Wpf.Ui.Tray/Interop/Shell32.cs)。
注册流程(NIM.ADD)
Register的完整流程如下(src/Wpf.Ui.Tray/TrayManager.cs):
- 取得父窗口的
HwndSource:优先使用调用方传入的Window,否则回退到Application.Current.MainWindow; - 为图标分配自增
Id(TrayData.NotifyIcons.Count + 1); - 创建一个隐藏子窗口
TrayHandler,窗口名为wpfui_th_{父窗口句柄}_{Id},并挂载WndProc钩子; - 组装
NOTIFYICONDATA结构:uFlags = NIF.MESSAGE,uCallbackMessage = WM.TRAYMOUSEMESSAGE,同时按需设置NIF.TIP(ToolTip)与NIF.ICON(图标); - 调用
Shell_NotifyIcon(NIM.ADD, ...)完成注册,并把实例加入TrayData.NotifyIcons静态集合。
其中WM.TRAYMOUSEMESSAGE = 0x800(即WM_USER + 1024)定义于 src/Wpf.Ui.Tray/Interop/User32.cs,是硬编码的、与 WinFormsShell_NotifyIcon兼容的私有消息号——托盘上的鼠标事件会以该消息的形式投递到隐藏窗口。
消息分发(WndProc)
TrayHandler继承自HwndSource(src/Wpf.Ui.Tray/TrayHandler.cs),以"零大小、透明、无尺寸"的窗口样式创建,仅作为托盘消息的接收者存在。所有鼠标消息在InternalNotifyIconManager.WndProc(src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs)中分发:
| Shell 消息(lParam) | 触发动作 |
|---|---|
WM.LBUTTONDOWN | 触发OnLeftClick;若FocusOnLeftClick为真则调用FocusApp()恢复并聚焦主窗口 |
WM.LBUTTONDBLCLK | 触发OnLeftDoubleClick |
WM.RBUTTONDOWN | 触发OnRightClick;若MenuOnRightClick为真则调用OpenMenu() |
WM.RBUTTONDBLCLK | 触发OnRightDoubleClick |
WM.MBUTTONDOWN/WM.MBUTTONDBLCLK | 触发中键单击/双击 |
WM.DESTROY | 自动Dispose并注销图标 |
FocusApp()(src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs)会先恢复最小化窗口,再通过置顶标志翻转确保窗口被带到前台并Focus()。OpenMenu()(同文件 L167-L188)在弹出菜单前先调用SetForegroundWindow把钩子窗口置前,避免菜单出现在任务栏后方。
修改与删除
- 动态修改图标或 ToolTip 分别走
ModifyIcon()/ModifyToolTip(),内部以NIM.MODIFY重发NOTIFYICONDATA(src/Wpf.Ui.Tray/TrayManager.cs); - 卸载图标调用
Shell_NotifyIcon(NIM.DELETE, ...)(同文件 L141-L153)。
图标句柄(HICON)转换
NOTIFYICONDATA.hIcon需要的是 Win32HICON句柄,由内部工具类Hicon(src/Wpf.Ui.Tray/Hicon.cs)负责转换:
FromSource(ImageSource):把BitmapSource的像素复制到托管内存,按Format32bppPArgb包装成System.Drawing.Bitmap后调用GetHicon()取得句柄;多帧图像(如 ICO 解码帧)默认取第一帧;FromApp():通过Icon.ExtractAssociatedIcon提取进程主模块关联的图标作为兜底;TrayManager.ReloadHicon(src/Wpf.Ui.Tray/TrayManager.cs)在替换图标前会先DestroyIcon释放旧句柄,避免 GDI 资源泄漏。
与主题系统的联动
InternalNotifyIconManager构造时订阅了ApplicationThemeManager.Changed(src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs),主题切换时会调用ContextMenu?.UpdateDefaultStyle()并重新布局,使托盘右键菜单跟随 WPF UI 的主题变化。也就是说,托盘菜单与主界面共享同一套 Fluent 主题体验,无需额外处理。
生命周期与资源释放
托盘图标涉及 GDI 图标句柄、隐藏窗口等非托管资源,务必注意释放时机:
- 控件方式:
NotifyIcon实现了IDisposable,析构函数兜底调用Dispose(false);Dispose会先Unregister()再释放内部管理器(src/Wpf.Ui.Tray/Controls/NotifyIcon.cs)。控件从可视化树移除时,靠 GC 与析构回收,频繁创建/销毁时建议显式调用Dispose(); - 服务方式:
SetParentWindow绑定的父窗口关闭时会自动Dispose;未绑定父窗口时需手动Unregister(); TrayManager顶部有一段 TODO 注释(src/Wpf.Ui.Tray/TrayManager.cs)指出一个已知边界:若主窗口被调试器强制销毁或直接析构,系统不会向其子窗口发送WM_CLOSE/WM_DESTROY,此时需要额外的检测机制才能保证图标被移除——这是使用"关闭窗口即隐藏到托盘"模式时值得注意的细节。
适用前提与限制
- Wpf.Ui.Tray 依赖 Windows 通知区域 API(
shell32.dll、user32.dll,见 src/Wpf.Ui.Tray/Interop/Libraries.cs),因此仅适用于 Windows 平台,工程目标框架均带-windows后缀或为 .NET Framework; - 工程除
net462外均引用System.Drawing.Common(src/Wpf.Ui.Tray/Wpf.Ui.Tray.csproj),因为Hicon依赖System.Drawing完成ImageSource到HICON的转换(源码注释也将其标记为待改进点); - 托盘菜单的 ToolTip 文本(
szTip)在NOTIFYICONDATA中限定为 128 字符(见 src/Wpf.Ui.Tray/Interop/Shell32.cs),超长提示会被截断。
至此,从 XAML 声明、服务式调用,到Shell_NotifyIcon注册、隐藏窗口消息分发与资源回收,Wpf.Ui.Tray 的完整链路已经清晰。若需查看更多真实用法,可直接参考 src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml 与 samples/Wpf.Ui.Demo.Mvvm/Views/MainWindow.xaml 两个成品示例。
【免费下载链接】wpfuiWPF UI provides the Fluent experience in your known and loved WPF framework. Intuitive design, themes, navigation and new immersive controls. All natively and effortlessly.项目地址: https://gitcode.com/GitHub_Trending/wp/wpfui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考