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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 00:36:04