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
相关产品推荐
相关产品推荐

