通用GUI编程技术——Win32 原生编程实战(五十四)——Hook 机制
通用GUI编程技术——Win32 原生编程实战五十四——Hook 机制仓库已经开源喜欢的话点个⭐仓库Win32和Win32图形栈的部分目前已完成教程力争做一个完备的GUI教程欢迎各位大佬前来参观https://github.com/Charliechen114514/anatomy_gui上一篇文章我们学会了子类化——修改单个控件的消息处理。但子类化有一个局限它只能拦截发往特定窗口的消息。如果你想在消息到达任何窗口之前就拦截它呢比如监听全局键盘输入、拦截所有鼠标点击、或者监控系统级别的窗口事件这就是 Hook 机制的用武之地。今天我们就来聊聊 Win32 中最强大也最容易用错的机制——Windows Hook。为什么需要 Hook子类化的作用范围是一个窗口。如果你想让程序对某种输入做出全局响应——比如按 Print Screen 时自动保存截图、按某个快捷键时全局响应、或者记录用户的所有键盘输入在授权范围内——子类化就不够用了。Hook 给你提供了在消息到达目标窗口之前拦截它的能力。你可以在消息流中插入一个检查站检查每一条消息决定是放行还是拦截。但能力越大责任越大。Hook 是 Win32 中最容易出问题的机制之一——处理不当会导致系统卡顿、程序崩溃、甚至安全漏洞。所以这篇文章不仅教你怎么用更重要的是教你什么时候该用和怎么用才安全。环境说明在我们正式开始之前先明确一下我们这次动手的环境平台Windows 10/11开发工具Visual Studio 2019 或更高版本Community 版本就行编程语言CC17 或更新项目类型桌面应用程序Win32 项目代码假设你已经熟悉前面文章的内容——至少知道消息机制怎么工作、子类化的概念、基本的 Win32 API 使用。⚠️ 注意Hook 机制涉及系统级别的消息拦截。在实际项目中使用时请注意用户隐私和系统安全。本文所有示例仅用于教学目的。第一步——Hook 类型速查Windows 提供了很多种 Hook 类型每种拦截不同的消息类别Hook 类型常量范围说明键盘低级钩子WH_KEYBOARD_LL全局拦截所有键盘输入无需 DLL鼠标低级钩子WH_MOUSE_LL全局拦截所有鼠标输入无需 DLL键盘钩子WH_KEYBOARD进程/全局拦截键盘消息全局需 DLL鼠标钩子WH_MOUSE进程/全局拦截鼠标消息全局需 DLL消息钩子WH_GETMESSAGE进程/全局拦截 PostMessage 投递的消息窗口过程钩子WH_CALLWNDPROC进程/全局拦截 SendMessage 发送的消息CBT 钩子WH_CBT进程/全局监控窗口创建/销毁/激活等Shell 钩子WH_SHELL全局监控 Shell 事件窗口激活等最重要的区分低级钩子_LL后缀vs 非低级钩子。低级钩子WH_KEYBOARD_LL、WH_MOUSE_LL在消息处理的最早阶段拦截不需要 DLL 注入。这是大多数应用程序应该使用的类型。非低级钩子需要在目标进程中执行代码全局使用时需要 DLL 注入。这个教程里不涉及 DLL 注入部分只讲低级钩子和进程内钩子。第二步——安装和移除 HookSetWindowsHookExHHOOKSetWindowsHookEx(intidHook,// Hook 类型HOOKPROC lpfn,// Hook 回调函数HINSTANCE hMod,// DLL 实例句柄低级钩子用本模块DWORD dwThreadId// 线程 ID0 全局);UnhookWindowsHookExBOOLUnhookWindowsHookEx(HHOOK hhk);参数说明idHookHook 类型比如 WH_KEYBOARD_LL。lpfn你的 Hook 回调函数。签名取决于 Hook 类型。hMod对于低级钩子传GetModuleHandle(NULL)当前 exe 的模块句柄。对于需要 DLL 注入的全局钩子传 DLL 的句柄。dwThreadId0全局 Hook拦截所有线程的消息。非 0 值只拦截指定线程的消息进程内 Hook。Hook 回调的通用规则必须调用 CallNextHookEx——除非你想完全拦截这条消息。Hook 是一个链表多个程序可以同时安装 Hook。你不调用 CallNextHookEx后续的 Hook 就收不到这条消息可能导致其他程序异常。LRESULT CALLBACKHookCallback(intnCode,WPARAM wParam,LPARAM lParam){if(nCode0)// nCode 0 时必须直接传递{// 处理消息// 如果想拦截返回非零值// 如果想放行调用 CallNextHookEx}// 放行传递给下一个 HookreturnCallNextHookEx(g_hHook,nCode,wParam,lParam);}⚠️ 注意当nCode 0时Windows 文档要求你直接调用CallNextHookEx并返回其结果不要做任何处理。这是为了保持向前兼容。第三步——WH_KEYBOARD_LL全局键盘低级钩子这是最常用的 Hook 类型之一。它可以拦截系统级别的键盘输入包括所有应用程序的键盘事件。回调函数签名LRESULT CALLBACKLowLevelKeyboardProc(intnCode,// HC_ACTION (0) 表示可以处理WPARAM wParam,// 消息类型WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUPLPARAM lParam// 指向 KBDLLHOOKSTRUCT 结构);KBDLLHOOKSTRUCT 结构typedefstructtagKBDLLHOOKSTRUCT{DWORD vkCode;// 虚拟键码DWORD scanCode;// 扫描码DWORD flags;// 标志位DWORD time;// 时间戳ULONG_PTR dwExtraInfo;// 额外信息}KBDLLHOOKSTRUCT;flags 的常用位位含义LLKHF_EXTENDED扩展键右侧 Alt/Ctrl 等LLKHF_INJECTED由程序模拟的按键SendInput 等LLKHF_ALTDOWNAlt 键被按下LLKHF_UP键被释放没有此位表示按下完整示例监听 Print Screen 键这个示例程序会在后台监听 Print Screen 键按下时弹出一个通知而不是让系统截图。#includewindows.h#includestdio.hHHOOK g_hHookNULL;HWND g_hWndNULL;BOOL g_runningTRUE;// 低级键盘钩子回调LRESULT CALLBACKLowLevelKeyboardProc(intnCode,WPARAM wParam,LPARAM lParam){if(nCodeHC_ACTION){KBDLLHOOKSTRUCT*pKbd(KBDLLHOOKSTRUCT*)lParam;// 检测 Print Screen 键按下if(pKbd-vkCodeVK_SNAPSHOT(wParamWM_KEYDOWN||wParamWM_SYSKEYDOWN)){// 通知主窗口PostMessage(g_hWnd,WM_USER1,0,0);// 拦截此按键不让系统截图return1;}}returnCallNextHookEx(g_hHook,nCode,wParam,lParam);}// Hook 线程低级钩子需要消息循环DWORD WINAPIHookThread(LPVOID lpParam){// 安装低级键盘钩子g_hHookSetWindowsHookEx(WH_KEYBOARD_LL,LowLevelKeyboardProc,GetModuleHandle(NULL),0// 0 全局);if(!g_hHook){MessageBox(NULL,L安装 Hook 失败,L错误,MB_OK|MB_ICONERROR);return1;}// 低级钩子需要消息循环才能工作MSG msg{};while(g_runningGetMessage(msg,NULL,0,0)){TranslateMessage(msg);DispatchMessage(msg);}// 移除 HookUnhookWindowsHookEx(g_hHook);g_hHookNULL;return0;}LRESULT CALLBACKWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){caseWM_USER1:MessageBox(hwnd,L检测到 Print Screen 按键已被拦截。\nL此示例仅用于教学目的。,LHook 通知,MB_OK|MB_ICONINFORMATION);return0;caseWM_DESTROY:g_runningFALSE;// 向 Hook 线程发送消息以退出消息循环PostThreadMessage(GetWindowThreadProcessId(hwnd,NULL),WM_QUIT,0,0);PostQuitMessage(0);return0;}returnDefWindowProc(hwnd,uMsg,wParam,lParam);}intWINAPIwWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,PWSTR pCmdLine,intnCmdShow){WNDCLASS wc{};wc.lpfnWndProcWndProc;wc.hInstancehInstance;wc.lpszClassNameLHookDemoClass;wc.hbrBackground(HBRUSH)(COLOR_WINDOW1);wc.hCursorLoadCursor(NULL,IDC_ARROW);RegisterClass(wc);g_hWndCreateWindowEx(0,LHookDemoClass,LHook 示例 - Print Screen 监听,WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU,CW_USEDEFAULT,CW_USEDEFAULT,400,200,NULL,NULL,hInstance,NULL);if(!g_hWnd)return0;// 显示提示信息CreateWindowEx(0,LSTATIC,L程序正在监听 Print Screen 键。\r\n\r\nL按下 Print Screen 后按键会被拦截\r\nL同时弹出通知对话框。\r\n\r\nL关闭窗口退出程序。,WS_CHILD|WS_VISIBLE|SS_LEFT,20,20,350,140,g_hWnd,NULL,hInstance,NULL);ShowWindow(g_hWnd,nCmdShow);UpdateWindow(g_hWnd);// 启动 Hook 线程CreateThread(NULL,0,HookThread,NULL,0,NULL);MSG msg{};while(GetMessage(msg,NULL,0,0)){TranslateMessage(msg);DispatchMessage(msg);}return0;}代码要点解析独立的 Hook 线程低级钩子的回调需要在安装它的线程的消息循环中被调用。所以我们在一个独立线程中安装 Hook 并运行消息循环。GetModuleHandle(NULL)获取当前 exe 的模块句柄。低级钩子不需要 DLL直接用 exe 模块即可。PostMessage 通知主窗口Hook 回调在工作线程中执行不应直接操作 UI。通过 PostMessage 通知主窗口。返回 1 拦截按键Hook 回调返回非零值可以阻止消息继续传递。这里拦截 Print Screen系统不会截图。WM_DESTROY 中的清理设置标志位g_running FALSE然后向 Hook 线程发送 WM_QUIT 使其退出消息循环。Hook 线程在退出前调用UnhookWindowsHookEx。第四步——WH_MOUSE_LL全局鼠标低级钩子鼠标低级钩子的结构和键盘类似。回调函数签名LRESULT CALLBACKLowLevelMouseProc(intnCode,WPARAM wParam,// WM_MOUSEMOVE, WM_LBUTTONDOWN 等LPARAM lParam// 指向 MSLLHOOKSTRUCT 结构);MSLLHOOKSTRUCT 结构typedefstructtagMSLLHOOKSTRUCT{POINT pt;// 鼠标屏幕坐标DWORD mouseData;// 滚轮 delta 或 X 按钮编号DWORD flags;// 标志LLMHF_INJECTED 等DWORD time;// 时间戳ULONG_PTR dwExtraInfo;}MSLLHOOKSTRUCT;简要示例记录鼠标点击位置LRESULT CALLBACKLowLevelMouseProc(intnCode,WPARAM wParam,LPARAM lParam){if(nCodeHC_ACTION){MSLLHOOKSTRUCT*pMouse(MSLLHOOKSTRUCT*)lParam;if(wParamWM_LBUTTONDOWN){wchar_tbuf[128];swprintf_s(buf,L鼠标左键点击( %d, %d ),pMouse-pt.x,pMouse-pt.y);// 可以通过 PostMessage 通知主窗口显示}}returnCallNextHookEx(g_hMouseHook,nCode,wParam,lParam);}// 安装HHOOK hMouseHookSetWindowsHookEx(WH_MOUSE_LL,LowLevelMouseProc,GetModuleHandle(NULL),0);第五步——进程内 HookWH_GETMESSAGE低级钩子拦截所有线程的消息但有时候你只想拦截自己进程内的消息。这时候可以用 WH_GETMESSAGE 或 WH_CALLWNDPROC指定线程 ID。WH_GETMESSAGE 示例WH_GETMESSAGE 可以拦截通过 PostMessage 投递的消息以及 WM_PAINT、WM_TIMER 等系统消息// 全局 Hook 句柄HHOOK g_hMsgHookNULL;// 消息钩子回调LRESULT CALLBACKGetMsgProc(intnCode,WPARAM wParam,LPARAM lParam){if(nCode0){MSG*pMsg(MSG*)lParam;// 拦截主窗口的 WM_ERASEBKGNDif(pMsg-messageWM_ERASEBKGNDpMsg-hwndg_hWnd){pMsg-messageWM_NULL;// 替换为空消息等于丢弃}}returnCallNextHookEx(g_hMsgHook,nCode,wParam,lParam);}// 安装——只拦截当前线程的消息g_hMsgHookSetWindowsHookEx(WH_GETMESSAGE,GetMsgProc,NULL,// 进程内 Hook传 NULLGetCurrentThreadId()// 只拦截当前线程);// 移除UnhookWindowsHookEx(g_hMsgHook);注意进程内 Hook 的hMod参数传 NULL因为是同一个模块dwThreadId传具体的线程 ID。第六步——性能与安全注意事项时间限制低级钩子回调在系统关键路径上执行。如果回调耗时过长会影响整个系统的输入响应。Windows 有一条硬性规则低级钩子回调执行时间不能超过约 300ms具体值取决于系统版本。超过这个时间Windows 会自动移除你的 Hook你的程序就收不到后续消息了。所以不要在 Hook 回调中做耗时操作不要在 Hook 回调中调用 Sleep不要在 Hook 回调中做网络请求或文件 I/O如果需要做复杂处理用 PostMessage 转发给工作线程安全与隐私Hook 机制可以监听所有键盘和鼠标输入这既是强大的工具也是安全风险不要用 Hook 做恶意监听——未经用户同意记录键盘输入是违法的注意防病毒软件——某些安全软件会拦截或标记安装了全局 Hook 的程序只 Hook 你需要的——安装了 Hook 就要尽快卸载不要一直挂着使用 LLKHF_INJECTED 检测——可以区分真实用户输入和程序模拟的输入Hook vs 子类化场景推荐修改单个控件的行为子类化只拦截自己进程的消息进程内 Hook 或子类化需要拦截所有键盘/鼠标输入低级 Hook全局快捷键RegisterHotKey优先考虑需要修改消息参数子类化或进程内 Hook⚠️ 注意如果你只是想实现全局快捷键优先使用RegisterHotKey而不是 Hook。RegisterHotKey 是专门为这个场景设计的 API更轻量、更安全、不需要消息循环。// 注册全局快捷键 CtrlShiftKRegisterHotKey(hwnd,1,MOD_CONTROL|MOD_SHIFT,K);// 在 WndProc 中处理caseWM_HOTKEY:if(wParam1)// ID 1{// CtrlShiftK 被按下}break;// 取消注册UnregisterHotKey(hwnd,1);常见陷阱陷阱一低级钩子回调耗时过长// 错误在回调中做耗时操作LRESULT CALLBACKBadHookProc(intnCode,WPARAM wParam,LPARAM lParam){Sleep(500);// 系统 300ms 后自动移除你的 HookreturnCallNextHookEx(g_hHook,nCode,wParam,lParam);}陷阱二忘记调用 CallNextHookEx// 错误拦截了所有消息但不传递LRESULT CALLBACKBadHookProc(intnCode,WPARAM wParam,LPARAM lParam){// 什么都不做也不调用 CallNextHookExreturn0;// 这会阻止所有后续 Hook 和目标窗口}陷阱三在错误的线程安装 Hook低级钩子必须在安装它的线程中有消息循环才能工作。如果你在主线程安装 Hook 但主线程在忙别的事情比如在等待信号量Hook 回调就不会被调用。陷阱四忘记 UnhookWindowsHookEx程序退出前必须卸载 Hook。否则系统会保留一个指向已卸载模块的函数指针下次触发 Hook 时可能导致崩溃。后续可以做什么到这里Windows Hook 机制就讲完了。你现在应该理解了 Hook 的类型低级 vs 非低级全局 vs 进程内、安装和移除的标准流程、低级键盘和鼠标钩子的具体用法、进程内消息钩子的使用以及性能和安全方面的注意事项。下一篇文章我们会聊一个更实用的话题——系统托盘。你将学会如何让你的程序最小化到系统托盘、显示托盘图标和右键菜单、处理气球通知。在此之前建议你做一些练习巩固今天的知识基础练习修改 Print Screen 监听示例改为监听 WinE 组合键提示检查 vkCode 和 LLKHF_ALTDOWN 等标志进阶练习使用 WH_MOUSE_LL 实现一个简单的鼠标轨迹记录器——记录所有左键点击的坐标在主窗口中绘制轨迹线挑战练习分别用 Hook 和子类化两种方案实现按键记录器只记录自身窗口的输入比较两种方案的优缺点相关资源SetWindowsHookEx function - Microsoft LearnUnhookWindowsHookEx function - Microsoft LearnCallNextHookEx function - Microsoft LearnLowLevelKeyboardProc callback - Microsoft LearnKBDLLHOOKSTRUCT structure - Microsoft LearnUsing Hooks - Microsoft LearnRegisterHotKey function - Microsoft Learn相关阅读现代Qt开发教程新手篇1.15——正则与文本处理 - 相似度 100%现代Qt开发教程新手篇2.1——QPainter 绘图基础 - 相似度 71%