You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);

关键问题解决说明

  1. 菜单点击无响应:替换窗口过程时必须保留并调用原窗口过程,同时弹出菜单前调用SetForegroundWindow,确保消息能正确路由到主窗口。
  2. 图标为空:确保hIcon是有效句柄,可通过LoadImage加载自定义图标文件,避免使用空句柄。
  3. 点击别处菜单不消失:弹出菜单后发送WM_NULL消息,强制系统处理菜单焦点变化,同时TrackPopupMenu需绑定主窗口句柄。

内容的提问来源于stack exchange,提问作者Mahfuz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 16:34:54