全局钩子必须用SetWindowsHookEx配合WH_KEYBOARD_LL/WH_MOUSE_LL,回调需静态且StdCall,结构体须显式对齐,句柄依赖创建线程生命周期,x64下注意KBDLLHOOKSTRUCT字段大小。

全局钩子必须用 SetWindowsHookEx,别试 KeyDown 事件
窗体级的 KeyDown 或 PreviewKeyDown 只能捕获自己窗口的输入,切到别的程序就完全失效。真要监听全局键盘鼠标,绕不开 Windows API 的钩子机制。核心是 SetWindowsHookEx,它把你的回调函数“挂”进系统消息流里,不管当前焦点在哪,只要底层有硬件事件,你的函数就有机会被调用。
常见错误是直接在 WinForms 里写个 Application.AddMessageFilter 就以为能抓全局按键——它只对本进程消息有效,且不处理原始硬件事件(比如 Alt+Tab、Win 键、锁屏前的 Ctrl+Alt+Del),这些都会被系统提前截走。
- 必须用
WH_KEYBOARD_LL(低级键盘)或WH_MOUSE_LL(低级鼠标),不能用带_LL后缀的老式钩子(如WH_KEYBOARD),后者需要注入 DLL,权限高、兼容差、Win10+ 基本不可靠 - 回调函数必须是静态方法,且标记
[UnmanagedFunctionPointer(CallingConvention.StdCall)],否则系统调用时会崩溃 - 钩子回调里别做耗时操作(比如弹窗、文件读写),否则会拖慢整个系统输入响应,用户会觉得键盘卡顿
LowLevelKeyboardProc 回调里怎么判断是否拦截按键
系统每次发键盘事件,都会调用你注册的 LowLevelKeyboardProc 函数,传入 nCode、wParam、lParam。关键逻辑全在 wParam:它是消息类型,WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN(带 Alt 的组合键)都要分别处理;而是否“吃掉”这个键,取决于你 return 的值——返回非零表示拦截,不往下传;返回 CallNextHookEx 的结果表示放行。
容易踩的坑是误判 lParam。它是指向 KBDLLHOOKSTRUCT 的指针,需用 Marshal.PtrToStructure 解析。其中 vkCode 是虚拟键码(如 VK_F12 = 123),flags 里的 LLKHF_ALTDOWN 表示 Alt 被按下,不是简单看 vkCode == 18 就认为按了 Alt。
- 想屏蔽 Ctrl+Q,得同时检查
wParam == WM_KEYDOWN、vkCode == 81(Q)、且flags包含LLKHF_CONTROL - Win 键(
VK_LWIN/VK_RWIN)默认无法拦截,微软从 Win8 起做了限制,除非关闭快速启动或改组策略,否则 return 非零也无效 - 不要在回调里直接调用
SendKeys.Send或InputSimulator,容易引发递归调用或死锁
为什么调试时钩子突然不触发?检查这三处
最常遇到的现象是:Release 模式能用,Debug 下断点一打就失灵;或者程序最小化后钩子失效;又或者热更新/重载程序后钩子没注销干净,导致后续注册失败。根本原因不在代码逻辑,而在生命周期和线程模型。
-
SetWindowsHookEx必须在有消息循环的线程上调用(比如 WinForms 主线程),如果在后台线程(Task.Run或新Thread)里注册,钩子会立即失效 - 钩子句柄(
HHOOK)是进程级资源,但依赖创建它的线程存活。如果主线程退出(比如窗体关闭),钩子自动销毁,不会等你手动UnhookWindowsHookEx - 多次调用
SetWindowsHookEx而没先UnhookWindowsHookEx,会导致句柄泄漏,系统最多允许每种钩子 10 个左右,超了就静默失败,GetLastError返回 1428(ERROR_HOOK_NOT_INSTALLED)
64 位程序跑在 64 位系统上,lParam 解析要特别注意
结构体大小在 32/64 位下不同,KBDLLHOOKSTRUCT 里有个 uintptr_t 类型的 dwExtraInfo 字段,在 x64 下占 8 字节,但很多人按 4 字节解析,导致后面字段全部错位,vkCode 读成乱码,条件判断永远不成立。
正确做法是用 Marshal.SizeOf<kbdllhookstruct>()</kbdllhookstruct> 动态获取,并确保结构体用 [StructLayout(LayoutKind.Sequential, Pack = 1)] 显式对齐。别信网上抄来的“固定偏移取字节”写法,那在 AnyCPU 或 x64 下必崩。
- VS 里项目属性设为
x64或x86,别用AnyCPU+ “首选 32 位”,否则在 64 位系统上运行可能意外降为 32 位上下文,钩子行为不一致 - 鼠标钩子同理,
MSLLHOOKSTRUCT的pt(POINT)字段在 x64 下也是 8 字节对齐,Marshal.PtrToStructure不指定泛型类型会出错
钩子本身不难写,难的是它活在系统底层和 .NET 托管环境的夹缝里——稍不注意,内存、线程、位数、权限任何一个环节松动,表现就是“明明代码一样,有时行有时不行”。盯住句柄生命周期、结构体大小、线程上下文这三点,比纠结回调逻辑更关键。


















