全局钩子必须用SetWindowsHookEx配合WH_KEYBOARD_LL/WH_MOUSE_LL,hMod需传GetModuleHandle(null)而非IntPtr.Zero;KBDLLHOOKSTRUCT须加[StructLayout(Sequential, Pack=8)];回调必须static且StdCall;务必调用UnhookWindowsHookEx释放资源。

全局钩子在 C# 中不能靠 KeyDown 或 PreviewKeyDown 实现,那些只响应当前窗体焦点;真正能跨进程监听键盘/鼠标的唯一可行路径,是调用 Windows 的 SetWindowsHookEx 配合低级钩子(WH_KEYBOARD_LL / WH_MOUSE_LL),且必须走 P/Invoke + 托管委托 + 正确生命周期管理这一套。
为什么 SetWindowsHookEx 的 hMod 参数不能传 IntPtr.Zero
这个参数实际要传的是包含回调函数的模块句柄,不是“随便填个零”。C# 托管程序没有传统 DLL 模块,所以得用 GetModuleHandle(null) 获取当前进程主模块句柄。传 IntPtr.Zero 会导致钩子安装失败,SetWindowsHookEx 返回 IntPtr.Zero,但错误码常被忽略——你只会发现“没反应”,连日志都打不出来。
-
GetModuleHandle(null)是正确写法,对应当前 EXE 的 HMODULE - 不要试图用
Assembly.GetExecutingAssembly().Location去 LoadLibrary,.NET Core/.NET 5+ 下会失败 - x64 进程中若用 AnyCPU 且未勾选“首选 32 位”,
GetModuleHandle返回值仍有效,但结构体字段对齐必须显式声明(见下一条)
KBDLLHOOKSTRUCT 在 x64 下字段偏移不对?
这是新手最常卡住的点:32 位和 64 位系统下,KBDLLHOOKSTRUCT 中的指针字段(如 dwExtraInfo)大小不同,而 .NET 默认按 32 位对齐。不显式控制布局,结构体在 x64 下读出来的 wParam、扫描码、虚拟键值全错,甚至引发 AccessViolationException。
- 必须加
[StructLayout(LayoutKind.Sequential, Pack = 8)](x64 用 8,x86 用 4) - 所有字段类型严格匹配 WinSDK 定义:
uint vkCode、uint scanCode、uint flags、UIntPtr dwExtraInfo - 别用
int接dwExtraInfo——它在 x64 下是 8 字节
回调函数为什么必须是 static 且 CallingConvention.StdCall
Windows 在分发钩子消息时,是直接通过函数指针调用你的回调,不经过 .NET 运行时栈管理。如果回调是非静态实例方法,托管对象可能被 GC 回收,而系统还在往一个悬空地址发调用,结果就是静默崩溃或偶发 AV 错误。
- 回调委托类型必须标记
[UnmanagedFunctionPointer(CallingConvention.StdCall)] - 实现方法必须是
static,且不能捕获任何实例状态(如this、控件引用) - 如需访问 UI 或业务逻辑,用线程安全方式(如
Control.Invoke或SynchronizationContext.Post)转发到主线程 - 别在回调里做耗时操作——钩子链阻塞会影响整个桌面输入响应
UnhookWindowsHookEx 失败或没调用会怎样?
钩子句柄是内核资源,不释放会导致:① 同一进程重复安装时旧钩子残留,事件重复触发;② 进程退出后钩子未卸载,系统仍尝试回调已释放的托管委托,引发蓝屏风险(尤其驱动级环境);③ 调试时看似正常,发布后在某些 Win10/Win11 版本上出现随机死锁。
- 必须在
IDisposable.Dispose()中调用UnhookWindowsHookEx,且判断返回值是否为true - 建议加
GC.SuppressFinalize(this)避免 Finalizer 再次尝试卸载 - 如果钩子用于长时服务(如后台守护进程),需监听
AppDomain.CurrentDomain.ProcessExit或Console.CancelKeyPress做兜底清理
最难缠的从来不是怎么装上钩子,而是确保它在任意异常、任意线程调度、任意系统休眠唤醒后,依然保持句柄有效、回调可入、资源干净——这些细节不抠清楚,上线后的问题基本无法复现,只能靠日志盲猜。











