
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/wpfuiWpf.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:trayhttp://schemas.lepo.co/wpfui/2022/xaml/tray模块包含的核心类型模块 READMEsrc/Wpf.Ui.Tray/README.md列出了全部对外类型按职责可划分为四层类型命名空间职责NotifyIconWpf.Ui.Tray.Controls作为FrameworkElement的托盘图标控件可在 XAML 中声明并使用依赖属性与路由事件NotifyIconService/INotifyIconServiceWpf.Ui.Tray面向代码的服务式 API负责注册/注销图标、设置父窗口TrayManager/TrayHandler/TrayDataWpf.Ui.Tray内部核心管理与 Shell32 交互、消息钩子窗口、图标注册表RoutedNotifyIconEvent/NotifyIconEventHandler/INotifyIcon/HiconWpf.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.xamltray:NotifyIcon Grid.Row0 FocusOnLeftClickTrue Iconpack://application:,,,/Assets/applicationIcon-256.png MenuOnRightClickTrue TooltipTextWPF UI - MVVM Demo tray:NotifyIcon.Menu ContextMenu ItemsSource{Binding ViewModel.TrayMenuItems, ModeOneWay} / /tray:NotifyIcon.Menu /tray:NotifyIcon控件注册了以下依赖属性均为PropertyMetadata声明于 src/Wpf.Ui.Tray/Controls/NotifyIcon.cs属性类型默认值说明TooltipTextstring空字符串鼠标悬停时显示的提示文本IconImageSourcenull托盘图标通常用pack://URI 指向应用资源MenuContextMenunull右键菜单支持数据绑定MenuOnRightClickbooltrue单击右键时是否弹出MenuFocusOnLeftClickbooltrue单击左键时是否聚焦Application.MainWindowMenuFontSizedouble14d菜单字体大小与普通 WPF 控件不同托盘图标无需你手动调用注册方法NotifyIcon重写了OnRendersrc/Wpf.Ui.Tray/Controls/NotifyIcon.cs在首次渲染且尚未注册时自动完成InitializeIcon()与Register()。也就是说把控件放进 XAML 树即可生效。鼠标事件六个路由事件NotifyIcon注册了六个以Bubble路由策略传播的RoutedEventsrc/Wpf.Ui.Tray/Controls/NotifyIcon.cs对应左/右/中键的单击与双击tray:NotifyIcon LeftClickOnTrayLeftClick LeftDoubleClickOnTrayLeftDoubleClick ... /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 中以ObservableCollectionControl声明每个MenuItem通过Tag标记动作[ObservableProperty] private ObservableCollectionControl _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.csvar 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 与消息钩子托盘功能最终都汇聚到内部静态类TrayManagersrc/Wpf.Ui.Tray/TrayManager.cs它负责与 Windows Shell 的全部交互其底层是 Shell32 的Shell_NotifyIconP/Invoke声明于 src/Wpf.Ui.Tray/Interop/Shell32.cs。注册流程NIM.ADDRegister的完整流程如下src/Wpf.Ui.Tray/TrayManager.cs取得父窗口的HwndSource优先使用调用方传入的Window否则回退到Application.Current.MainWindow为图标分配自增IdTrayData.NotifyIcons.Count 1创建一个隐藏子窗口TrayHandler窗口名为wpfui_th_{父窗口句柄}_{Id}并挂载WndProc钩子组装NOTIFYICONDATA结构uFlags NIF.MESSAGEuCallbackMessage WM.TRAYMOUSEMESSAGE同时按需设置NIF.TIPToolTip与NIF.ICON图标调用Shell_NotifyIcon(NIM.ADD, ...)完成注册并把实例加入TrayData.NotifyIcons静态集合。其中WM.TRAYMOUSEMESSAGE 0x800即WM_USER 1024定义于 src/Wpf.Ui.Tray/Interop/User32.cs是硬编码的、与 WinFormsShell_NotifyIcon兼容的私有消息号——托盘上的鼠标事件会以该消息的形式投递到隐藏窗口。消息分发WndProcTrayHandler继承自HwndSourcesrc/Wpf.Ui.Tray/TrayHandler.cs以零大小、透明、无尺寸的窗口样式创建仅作为托盘消息的接收者存在。所有鼠标消息在InternalNotifyIconManager.WndProcsrc/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs中分发Shell 消息lParam触发动作WM.LBUTTONDOWN触发OnLeftClick若FocusOnLeftClick为真则调用FocusApp()恢复并聚焦主窗口WM.LBUTTONDBLCLK触发OnLeftDoubleClickWM.RBUTTONDOWN触发OnRightClick若MenuOnRightClick为真则调用OpenMenu()WM.RBUTTONDBLCLK触发OnRightDoubleClickWM.MBUTTONDOWN/WM.MBUTTONDBLCLK触发中键单击/双击WM.DESTROY自动Dispose并注销图标FocusApp()src/Wpf.Ui.Tray/Internal/InternalNotifyIconManager.cs会先恢复最小化窗口再通过置顶标志翻转确保窗口被带到前台并Focus()。OpenMenu()同文件 L167-L188在弹出菜单前先调用SetForegroundWindow把钩子窗口置前避免菜单出现在任务栏后方。修改与删除动态修改图标或 ToolTip 分别走ModifyIcon()/ModifyToolTip()内部以NIM.MODIFY重发NOTIFYICONDATAsrc/Wpf.Ui.Tray/TrayManager.cs卸载图标调用Shell_NotifyIcon(NIM.DELETE, ...)同文件 L141-L153。图标句柄HICON转换NOTIFYICONDATA.hIcon需要的是 Win32HICON句柄由内部工具类Hiconsrc/Wpf.Ui.Tray/Hicon.cs负责转换FromSource(ImageSource)把BitmapSource的像素复制到托管内存按Format32bppPArgb包装成System.Drawing.Bitmap后调用GetHicon()取得句柄多帧图像如 ICO 解码帧默认取第一帧FromApp()通过Icon.ExtractAssociatedIcon提取进程主模块关联的图标作为兜底TrayManager.ReloadHiconsrc/Wpf.Ui.Tray/TrayManager.cs在替换图标前会先DestroyIcon释放旧句柄避免 GDI 资源泄漏。与主题系统的联动InternalNotifyIconManager构造时订阅了ApplicationThemeManager.Changedsrc/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 通知区域 APIshell32.dll、user32.dll见 src/Wpf.Ui.Tray/Interop/Libraries.cs因此仅适用于 Windows 平台工程目标框架均带-windows后缀或为 .NET Framework工程除net462外均引用System.Drawing.Commonsrc/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),仅供参考