WinUI3系统托盘图标菜单无响应问题咨询
纯Win32 API实现WinUI3系统托盘完整方案
核心思路
WinUI3主窗口底层基于Win32窗口,我们通过获取主窗口的Win32句柄,结合Win32 Shell API实现托盘图标、右键菜单,并通过自定义窗口消息处理解决菜单无响应、菜单不消失、图标为空等问题。
步骤1:获取WinUI3主窗口的Win32句柄
在MainWindow类中,通过WinRT.Interop.WindowNative获取底层窗口句柄,这是后续所有Win32操作的基础:
using WinRT.Interop; using System.Runtime.InteropServices; public partial class MainWindow : Window { private IntPtr _hwnd; private IntPtr _trayIconHandle; private IntPtr _oldWndProc; private readonly WndProcDelegate _wndProcDelegate; // 消息与菜单项ID定义 private const int WM_COMMAND = 0x0111; private const int WM_DESTROY = 0x0002; private const int WM_USER = 0x0400; private const int WM_TRAY_MESSAGE = WM_USER + 1; private const int ID_OPEN = 1001; private const int ID_EXIT = 1002; public MainWindow() { InitializeComponent(); // 获取Win32窗口句柄 _hwnd = WindowNative.GetWindowHandle(this); // 注册自定义窗口过程 _wndProcDelegate = new WndProcDelegate(CustomWndProc); _oldWndProc = SetWindowLongPtr(_hwnd, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProcDelegate)); // 初始化托盘图标 InitializeTrayIcon(); // 拦截窗口关闭事件,改为隐藏窗口 Closing += MainWindow_Closing; } }
步骤2:正确初始化托盘图标(解决图标为空问题)
使用Shell_NotifyIcon注册托盘图标,确保加载有效图标资源:
private void InitializeTrayIcon() { var notifyIconData = new NOTIFYICONDATA(); notifyIconData.cbSize = Marshal.SizeOf(notifyIconData); notifyIconData.hWnd = _hwnd; notifyIconData.uID = 1; notifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; notifyIconData.uCallbackMessage = WM_TRAY_MESSAGE; // 加载系统默认应用图标,可替换为自定义图标路径 notifyIconData.hIcon = LoadIcon(IntPtr.Zero, IDI_APPLICATION); notifyIconData.szTip = "WinUI3托盘应用"; Shell_NotifyIcon(NIM_ADD, ref notifyIconData); _trayIconHandle = notifyIconData.hIcon; } // Win32结构体与API声明 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct NOTIFYICONDATA { public int cbSize; public IntPtr hWnd; public uint uID; public uint uFlags; public uint uCallbackMessage; public IntPtr hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string szTip; } private const int NIF_ICON = 0x00000002; private const int NIF_MESSAGE = 0x00000001; private const int NIF_TIP = 0x00000004; private const int NIM_ADD = 0x00000000; private const int NIM_DELETE = 0x00000002; private const int IDI_APPLICATION = 32512; [DllImport("shell32.dll", CharSet = CharSet.Auto)] private static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpData); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr LoadIcon(IntPtr hInstance, int lpIconName);
步骤3:自定义窗口过程,处理托盘与菜单消息(解决菜单点击无响应)
替换主窗口的WndProc,处理托盘鼠标事件和菜单点击命令:
private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { switch (msg) { case WM_TRAY_MESSAGE: // 处理托盘右键点击 if ((int)lParam == WM_RBUTTONUP) { ShowTrayMenu(); } break; case WM_COMMAND: // 处理菜单项点击 int menuId = LOWORD(wParam.ToInt32()); switch (menuId) { case ID_OPEN: Activate(); break; case ID_EXIT: DestroyTrayIcon(); Application.Current.Exit(); break; } break; case WM_DESTROY: DestroyTrayIcon(); break; } // 调用原窗口过程处理其他消息 return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam); } // 辅助函数:获取命令ID的低16位 private int LOWORD(int value) => value & 0xFFFF; // Win32 API声明 [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private const int GWLP_WNDPROC = -4; private const int WM_RBUTTONUP = 0x0205;
步骤4:正确弹出右键菜单(解决点击别处菜单不消失)
弹出菜单时需绑定主窗口句柄,并发送WM_NULL消息处理焦点变化:
private void ShowTrayMenu() { IntPtr hMenu = CreatePopupMenu(); AppendMenu(hMenu, MF_STRING, ID_OPEN, "Open"); AppendMenu(hMenu, MF_STRING, ID_EXIT, "Exit"); // 获取当前鼠标位置 POINT cursorPos; GetCursorPos(out cursorPos); // 设置窗口为前台,确保消息能被接收 SetForegroundWindow(_hwnd); // 弹出菜单 TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, cursorPos.x, cursorPos.y, 0, _hwnd, IntPtr.Zero); // 发送WM_NULL消息,解决菜单点击别处不消失问题 SendMessage(_hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero); DestroyMenu(hMenu); } // 辅助结构体与API声明 [StructLayout(LayoutKind.Sequential)] private struct POINT { public int x; public int y; } private const int MF_STRING = 0x00000000; private const int TPM_BOTTOMALIGN = 0x00000020; private const int TPM_LEFTALIGN = 0x00000000; private const int WM_NULL = 0x0000; [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr CreatePopupMenu(); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, string lpNewItem); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool GetCursorPos(out POINT lpPoint); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool DestroyMenu(IntPtr hMenu); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
步骤5:处理窗口关闭事件,驻留后台
拦截窗口Closing事件,取消默认关闭行为改为隐藏窗口:
private void MainWindow_Closing(object sender, WindowEventArgs args) { args.Cancel = true; Hide(); }
步骤6:清理资源
退出时销毁托盘图标,避免系统残留:
private void DestroyTrayIcon() { if (_trayIconHandle != IntPtr.Zero) { var notifyIconData = new NOTIFYICONDATA(); notifyIconData.cbSize = Marshal.SizeOf(notifyIconData); notifyIconData.hWnd = _hwnd; notifyIconData.uID = 1; Shell_NotifyIcon(NIM_DELETE, ref notifyIconData); DestroyIcon(_trayIconHandle); _trayIconHandle = IntPtr.Zero; } } [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool DestroyIcon(IntPtr hIcon);
关键问题解决说明
- 菜单点击无响应:替换窗口过程时必须保留并调用原窗口过程,同时弹出菜单前调用
SetForegroundWindow,确保消息能正确路由到主窗口。 - 图标为空:确保
hIcon是有效句柄,可通过LoadImage加载自定义图标文件,避免使用空句柄。 - 点击别处菜单不消失:弹出菜单后发送
WM_NULL消息,强制系统处理菜单焦点变化,同时TrackPopupMenu需绑定主窗口句柄。
内容的提问来源于stack exchange,提问作者Mahfuz
相关产品推荐
相关产品推荐

