WinUI 3中使用Shell_NotifyIcon实现托盘图标及回调处理问题咨询
解决方案
核心思路
WinUI 3 桌面应用基于原生Win32 HWND实现,没有内置类似Windows Forms的WndProc方法,你可以通过子类化你的父窗口HWND来拦截处理Shell_NotifyIcon发送的自定义回调消息。
实现步骤
1. 补充必要的P/Invoke声明
using System.Runtime.InteropServices; using WinRT.Interop; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls.Primitives; // 窗口过程委托,注意保持实例引用避免GC回收 private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); private const int GWL_WNDPROC = -4; private const uint WM_RBUTTONUP = 0x0205; private const uint MY_TRAY_CALLBACK = 0x5700; // 你定义的回调消息ID
2. 在窗口类中实现消息回调
在你的WinUI 3窗口类(比如MainWindow.xaml.cs)中添加以下代码,注意要把旧窗口过程的指针和委托实例存为类成员,避免被GC回收导致程序崩溃:
public sealed partial class MainWindow : Window { // 存储旧窗口过程指针和当前委托实例,避免GC回收 private IntPtr _oldWndProc; private WndProcDelegate _wndProcDelegate; private IntPtr _windowHwnd; public MainWindow() { this.InitializeComponent(); // 拿到当前窗口的HWND _windowHwnd = WindowNative.GetWindowHandle(this); // 子类化窗口 _wndProcDelegate = new WndProcDelegate(WndProc); _oldWndProc = SetWindowLongPtr(_windowHwnd, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProcDelegate)); // 初始化你的托盘图标 NotifyIcon(_windowHwnd); } private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { // 处理托盘回调消息 if (msg == MY_TRAY_CALLBACK) { uint mouseMsg = (uint)lParam; // 右键抬起时弹出上下文菜单 if (mouseMsg == WM_RBUTTONUP) { ShowTrayContextMenu(); return IntPtr.Zero; } } // 其余消息交给原有窗口过程处理 return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam); } private void ShowTrayContextMenu() { // 必须先激活窗口,否则菜单点击后不会自动消失 SetForegroundWindow(_windowHwnd); // 可以选择用Win32的TrackPopupMenu,或者直接用WinUI的MenuFlyout var flyout = new MenuFlyout(); flyout.Items.Add(new MenuFlyoutItem { Text = "打开主窗口" }); flyout.Items.Add(new MenuFlyoutItem { Text = "退出" }); // 显示在鼠标当前位置 flyout.ShowAt(null, new FlyoutShowOptions { Position = new Point( Microsoft.UI.Input.InputPointer.GetCurrentPoint(null).Position.X, Microsoft.UI.Input.InputPointer.GetCurrentPoint(null).Position.Y ) }); } // 你原来的NotifyIcon方法和NotifyIconData结构放在这里 }
3. 注意事项
- 务必将WndProc委托和旧窗口过程指针存为类的实例成员,不能作为局部变量,否则GC回收委托后窗口收到消息会直接崩溃
- 弹出菜单前必须调用SetForegroundWindow,这是Shell_NotifyIcon的已知限制,否则菜单失去焦点后不会自动关闭
- 窗口关闭时可以调用Shell_NotifyIcon的NIM_DELETE命令移除托盘图标,避免系统托盘残留图标
内容的提问来源于stack exchange,提问作者Nicolai
相关产品推荐
相关产品推荐

