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

C#中SetWinEventHook触发时如何识别外部应用点击的菜单项?

问题:如何识别Windows菜单点击事件中的具体菜单项?

我正在构建一个POC(概念验证)项目,目标是向任意Windows应用注入自定义主菜单项,并让这些菜单项触发自定义逻辑。目前已成功注入带子项的自定义菜单,且能通过SetWinEventHook捕获EVENT_SYSTEM_MENUEND事件,在回调中执行自定义代码,但无法识别具体点击了哪个菜单项,无法针对特定菜单项执行对应逻辑。

相关代码片段

Win32 API声明与委托

public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwflags);

private static readonly WinEventDelegate callback = new WinEventDelegate(EventCallback);

添加自定义菜单代码

var newMenu = CreateMenu();
InsertMenu(newMenu, 0, MF_BYPOSITION, 10001, "Item 1");
InsertMenu(newMenu, 1, MF_BYPOSITION, 10002, "Item 2");
AppendMenu(menu, MF_POPUP, newMenu, "My Menu Section");

设置钩子代码

public void SetEventHook()
{
    var result = SetWinEventHook(EVENT_SYSTEM_MENUEND, EVENT_SYSTEM_MENUEND, IntPtr.Zero, callback, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD);            

    Console.WriteLine($"SetWinEventHook returned {result}");
}

菜单点击回调

public static void EventCallback(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    Process.Start("mspaint");
}

当前问题

EventCallback触发时,没有可用信息识别选中的菜单项。原本认为idObject参数会返回选中菜单项的ID,但无论点击自定义还是原生菜单项,它始终返回-3。


解决方案

方法1:监听EVENT_OBJECT_INVOKED事件

EVENT_SYSTEM_MENUEND仅标记菜单关闭事件,不会传递菜单项的具体标识。需要监听EVENT_OBJECT_INVOKED事件(值为0x800C),该事件在菜单项被点击激活时触发,idObject和idChild会携带有效信息。

修改钩子设置

public void SetEventHook()
{
    const uint EVENT_OBJECT_INVOKED = 0x800C;
    var result = SetWinEventHook(EVENT_OBJECT_INVOKED, EVENT_OBJECT_INVOKED, IntPtr.Zero, callback, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD);            

    Console.WriteLine($"SetWinEventHook returned {result}");
}

在回调中获取菜单项ID

通过GetMenuItemID函数,结合窗口菜单句柄和idChild(菜单项索引),获取自定义菜单项的ID:

[DllImport("user32.dll")]
private static extern uint GetMenuItemID(IntPtr hMenu, int nPos);
[DllImport("user32.dll")]
private static extern IntPtr GetMenu(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos);

public static void EventCallback(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    // 确认是菜单项触发的事件(OBJID_MENUITEM = 0xFFFFFFFD)
    if (idObject == 0xFFFFFFFD)
    {
        IntPtr mainMenu = GetMenu(hwnd);
        // 替换为自定义菜单在主菜单中的索引,这里假设是第一个子菜单
        IntPtr customSubMenu = GetSubMenu(mainMenu, 0);
        uint menuItemId = GetMenuItemID(customSubMenu, idChild);
        
        switch(menuItemId)
        {
            case 10001:
                Process.Start("mspaint");
                break;
            case 10002:
                Process.Start("notepad");
                break;
            default:
                // 处理原生菜单项或其他情况
                break;
        }
    }
}

方法2:拦截WM_COMMAND全局消息

设置全局窗口消息钩子,拦截目标窗口的WM_COMMAND消息(值为0x0111)。菜单项点击时,窗口会收到该消息,wParam的低16位即为菜单项ID,无需额外查询菜单结构。

实现代码框架

private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
private static HookProc commandHookProc = new HookProc(CommandHookCallback);
private static IntPtr commandHook = IntPtr.Zero;

[DllImport("user32.dll")]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

private const int WH_CALLWNDPROC = 4;
private const int WM_COMMAND = 0x0111;

public void SetCommandHook()
{
    commandHook = SetWindowsHookEx(WH_CALLWNDPROC, commandHookProc, IntPtr.Zero, 0);
}

private static IntPtr CommandHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0)
    {
        var msg = (CWPSTRUCT)Marshal.PtrToStructure(lParam, typeof(CWPSTRUCT));
        if (msg.message == WM_COMMAND)
        {
            uint menuItemId = (uint)msg.wParam & 0xFFFF; // 提取低16位的菜单项ID
            switch(menuItemId)
            {
                case 10001:
                    Process.Start("mspaint");
                    break;
                case 10002:
                    Process.Start("notepad");
                    break;
            }
        }
    }
    return CallNextHookEx(commandHook, nCode, wParam, lParam);
}

[StructLayout(LayoutKind.Sequential)]
private struct CWPSTRUCT
{
    public IntPtr lParam;
    public IntPtr wParam;
    public uint message;
    public IntPtr hwnd;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 00:50:56