简介:这是一份面向Windows开发者的API Hook实战源码,聚焦屏幕取词这一典型应用场景,适合具备一定C++与Win32编程基础、希望深入理解系统级Hook机制的学习者。源码围绕低级鼠标与键盘钩子的安装、事件处理与卸载流程展开,演示了如何借助SetWindowsHookEx等接口拦截输入事件,并结合GetCursorPos、ScreenToClient、GetWindowText等API完成坐标转换与文本读取,同时涉及分词、词性标注等NLP预处理及外部翻译接口调用的思路。压缩包共31个文件,约145KB,以h头文件与cpp源文件为核心,辅以vcproj、sln工程配置、rc资源脚本、dll与lib库文件及exe可执行程序,结构完整可直接编译调试。目前已有257人学习下载,读者可从中获得Hook安装与管理的完整代码框架、屏幕取词事件处理链路以及Windows系统编程的排错参考。
1. 屏幕取词与 API Hook:从一份 VC 工程源码说起
做桌面翻译工具或辅助阅读软件时,屏幕取词几乎是绕不开的功能。用户用鼠标在任意窗口划一下,程序就得把选中的文字抓出来,再送去翻译或查询。听起来简单,真动手才发现难点不在翻译,而在“怎么拿到别的进程里的文字”。这份API-HOOK.rar就是一份用 VC 写的 API Hook 源码工程,核心目标正是实现划词与屏幕取词。它包含ApiHook.sln、ApiHook.vcproj、ApiHook.dll、ApiHook.exe、ApiHookDlg.cpp、ApiHook_dll.h等文件,是一套完整的 Windows Hook 实践骨架。适合谁?适合已经会写 Win32 窗口程序、想搞明白 Hook 到底怎么落地、又不想从零搭工程的人。下面我按“它是什么、怎么编译跑起来、Hook 怎么装、坑在哪、怎么验证”一路拆下去。
2. 工程结构与 Hook 选型:先搞清楚这份源码里有什么
2.1 文件清单与模块划分
拿到一个.rar源码包,我习惯先看目录结构再动手编译,不然很容易在缺库、缺头文件上浪费时间。这份工程的文件大致分四类:
| 类别 | 文件 | 作用 |
|---|---|---|
| 解决方案与工程 | ApiHook.sln、ApiHook.vcproj、ApiHook.suo、ApiHook.ncb | VS 解决方案、工程配置、用户选项与浏览数据库 |
| 主程序 | ApiHook.cpp、ApiHookDlg.cpp、ApiHookDlg.h、Resource.h、ApiHook.rc | 对话框主程序,负责界面与 Hook 的安装/卸载入口 |
| 动态库 | ApiHook_dll.h、ApiHook.dll、ApiHook.lib | 把 Hook 逻辑封装进 DLL,供注入或加载使用 |
| 公共头与预编译 | ApiHook.h、stdafx.h、stdafx.cpp、ReadMe.txt | 公共声明、预编译头、说明文件 |
.suo、.ncb、.aps这些是 VS 的中间文件,换机器或换 VS 版本后基本会重建,不用太在意。真正要读的是ApiHookDlg.cpp(界面与流程控制)、ApiHook.cpp(Hook 主体)和ApiHook_dll.h(DLL 导出接口)。ReadMe.txt通常写了编译顺序和依赖,先扫一眼能省不少事。
2.2 为什么屏幕取词要选 API Hook
屏幕取词的本质是:用户在其他进程的窗口里划选文字,你的程序要拿到这段文字。直接读对方窗口控件是不行的,因为跨进程内存访问受保护,GetWindowText对很多自绘控件也拿不到内容。常见做法是 Hook 与文本获取相关的 API,在目标进程调用这些函数时截获参数或返回值。
Windows 下实现 Hook 有几条路:SetWindowsHookEx适合拦截消息级事件(鼠标、键盘、窗口过程);Detour / EasyHook 这类库适合拦截任意函数调用,粒度更细。这份源码走的是SetWindowsHookEx路线,配合 DLL 注入,属于比较经典、依赖少、容易讲清楚的方案。选它的理由很直接:不需要额外第三方库,VC 工程自带,编译出来就能跑,适合拿来理解 Hook 的完整生命周期。
2.3 低级鼠标钩子与键盘钩子的取舍
屏幕取词通常要监听两类事件:鼠标按下/抬起(确定选区起止)和键盘(比如按住某键触发取词)。对应WH_MOUSE_LL和WH_KEYBOARD_LL两种低级钩子。
WH_MOUSE_LL:全局鼠标钩子,能拿到屏幕坐标,适合判断用户在哪里划词。WH_KEYBOARD_LL:全局键盘钩子,适合做快捷键触发或辅助选择。
低级钩子不需要把 DLL 注入到每个进程,系统会把事件回调到安装钩子的线程消息队列里,实现相对干净。代价是回调必须快,不能在里面做耗时操作,否则系统会把钩子摘掉。所以取词逻辑一般是:钩子里只记录坐标和状态,真正的文本提取放到主程序里异步做。
提示:低级钩子回调运行在安装它的线程上下文,若该线程阻塞,整个系统的鼠标键盘响应都会受影响。回调里只做标记,别做翻译。
3. 编译与运行:把 ApiHook.sln 跑起来
3.1 环境准备与工程打开
这份工程是 VC 工程(.vcproj格式),对应 Visual Studio 2008 到 2013 这一代。用更高版本 VS 打开会触发工程升级向导,一般能升,但字符集和 MFC 版本可能要对一下。我一般这样做:
- 安装带 MFC 的 Visual Studio(安装时勾选“MFC 和 ATL 支持”)。
- 解压
API-HOOK.rar到纯英文路径,路径里不要有中文和空格,否则链接 DLL 时容易出玄学问题。 - 双击
ApiHook.sln,若提示升级,点确定,让 VS 生成新的工程文件。
打开后先看解决方案资源管理器里是不是有两个项目:一个 exe、一个 dll。如果只有一个,说明 DLL 项目没被正确加载,需要手动添加ApiHook_dll相关文件。
3.2 编译顺序与配置
Hook 的常见结构是:DLL 里放钩子过程,exe 负责加载 DLL 并安装钩子。所以编译顺序应该是先 DLL 后 exe。在 VS 里可以设置项目依赖:
# 在解决方案上右键 -> 项目依赖项 # 让 ApiHook(exe)依赖 ApiHook_dll(dll) # 这样生成解决方案时会先编 DLL配置上注意两点:
- 平台选
Win32,这份老工程大概率没有 x64 配置,强行切 x64 会缺库。 - 字符集建议用“多字节字符集”,因为老代码里
GetWindowText、SetWindowsHookEx的调用多半按 ANSI 写的,切 Unicode 会有一堆LPCWSTR类型不匹配。
编译成功后,输出目录下会有ApiHook.exe和ApiHook.dll。如果 DLL 没生成,检查ApiHook_dll.h里的导出宏是否和工程设置里的“导出符号”一致。
3.3 运行与基本验证
直接运行ApiHook.exe,界面上一般会有“安装钩子”“卸载钩子”之类的按钮。点安装后,去记事本或浏览器里划一段文字,看程序有没有反应。
// 典型安装钩子的调用形态(示意,具体以源码为准) HHOOK hMouse = SetWindowsHookEx( WH_MOUSE_LL, // 钩子类型:低级鼠标 MouseProc, // 钩子回调函数 hInstance, // DLL 实例句柄 0 // 线程 ID,0 表示全局 ); if (hMouse == NULL) { // 安装失败,用 GetLastError 查原因 DWORD err = GetLastError(); }SetWindowsHookEx四个参数分别是钩子类型、回调地址、模块句柄、线程 ID。全局低级钩子线程 ID 传 0。返回 NULL 就是失败,常见原因是回调函数不在 DLL 里(对全局钩子而言)或权限不够。验证时如果划词没反应,先确认钩子安装返回值不是 NULL,再看回调里有没有正确判断鼠标位置。
4. 取词逻辑实现:坐标、文本与消息处理
4.1 从鼠标坐标到目标窗口
钩子回调拿到的是屏幕坐标,要取词得先知道鼠标下是哪个窗口、窗口里哪个控件。常见链路是:
// 在鼠标回调里记录坐标,主程序里处理 POINT pt = ((MSLLHOOKSTRUCT*)lParam)->pt; // 屏幕坐标 HWND hWnd = WindowFromPoint(pt); // 命中的窗口 HWND hRoot = GetAncestor(hWnd, GA_ROOT); // 取顶层窗口WindowFromPoint返回的是最内层窗口,可能是子控件;GetAncestor用来上溯到顶层窗口。拿到窗口句柄后,可以尝试GetWindowText读标题或内容,但对编辑框、浏览器渲染内容往往无效,这也是屏幕取词最头疼的地方。
4.2 文本获取的几种手段
源码里可能用到的文本获取方式包括:
GetWindowText:对标准控件有效,对自绘控件无效。SendMessage(hWnd, WM_GETTEXT, ...):跨进程发送消息取文本,比直接读内存安全。GetCursorPos+ScreenToClient:把屏幕坐标转成窗口客户区坐标,用于定位选区。- 对浏览器或 Office 这类复杂控件,往往要配合 Accessibility 接口或 UI Automation,源码未必覆盖到。
// 跨进程取文本的常见写法 int len = (int)SendMessage(hWnd, WM_GETTEXTLENGTH, 0, 0); if (len > 0) { TCHAR* buf = new TCHAR[len + 1]; SendMessage(hWnd, WM_GETTEXT, len + 1, (LPARAM)buf); // buf 里就是文本,用完 delete[] }WM_GETTEXTLENGTH先拿长度,再WM_GETTEXT取内容。注意跨进程SendMessage传指针时,系统会做一次编组,所以不能传复杂结构体,只能传基本类型或系统能识别的缓冲区。
4.3 划词与选区的判定
划词的核心是判断“用户按住鼠标拖动了一段距离”。在鼠标回调里记录按下点和抬起点,两点距离超过阈值才算划词,避免单击误触发。
// 伪代码:划词判定 static POINT g_start = {0, 0}; static bool g_dragging = false; // 鼠标按下 if (wParam == WM_LBUTTONDOWN) { g_start = pt; g_dragging = true; } // 鼠标抬起 if (wParam == WM_LBUTTONUP && g_dragging) { int dx = abs(pt.x - g_start.x); int dy = abs(pt.y - g_start.y); if (dx > 5 || dy > 5) { // 阈值可调 // 触发取词 } g_dragging = false; }阈值 5 像素是经验值,太小会把轻微抖动当划词,太大则短词选不中。实际项目里还会结合双击选词、三击选段等逻辑,源码里若没写,可以自己补。
注意:低级钩子回调里不要直接弹窗或做耗时 IO,否则系统可能因为超时把钩子移除,表现为“用一会儿就失效”。
5. 避坑与排查:Hook 失效、取不到词、程序崩溃
5.1 钩子安装返回 NULL
现象:点“安装钩子”没反应,日志里SetWindowsHookEx返回 NULL。
原因:全局钩子的回调函数必须位于 DLL 中,如果回调写在 exe 里,系统无法把回调映射到其他进程,安装就会失败。另外权限不足(比如目标进程以管理员运行而你的程序不是)也会失败。
解决:确认MouseProc、KeyboardProc这些回调在 DLL 工程里,并且 DLL 已正确加载。用GetLastError看错误码,ERROR_HOOK_NEEDS_HMOD基本就是回调不在 DLL 里。
5.2 划词没反应,但钩子装上了
现象:钩子安装成功,鼠标事件也能进回调,但划词后程序没拿到文本。
原因:多半是文本获取环节失败。GetWindowText对浏览器、PDF 阅读器、自绘控件无效;或者坐标转换错了,WindowFromPoint拿到的窗口不是真正承载文本的控件。
解决:在回调里打印hWnd和窗口类名(GetClassName),确认命中的是什么控件。对浏览器可以尝试 UI Automation;对标准编辑框检查是否用了WM_GETTEXT而不是直接读内存。
5.3 程序运行一段时间后钩子自动失效
现象:刚启动能用,过几分钟划词就没反应了。
原因:低级钩子回调有超时限制,系统默认约 300 毫秒。如果回调里做了翻译、网络请求或大量计算,超时后系统会静默移除钩子。
解决:回调里只记录坐标和事件类型,把取词和翻译放到主线程或工作线程异步处理。可以用PostMessage把事件丢回主窗口,避免在回调里做重活。
5.4 编译时报 MFC 相关链接错误
现象:nafxcwd.lib找不到,或者_afx...符号未解析。
原因:工程用了 MFC,但当前 VS 没装 MFC 组件,或者工程配置里的 MFC 使用方式(静态/动态)和实际库不匹配。
解决:在 VS 安装器里勾选“MFC 和 ATL 支持”。工程属性里检查“使用 MFC”是“在共享 DLL 中使用 MFC”还是“在静态库中使用 MFC”,两者要和你链接的库一致。
5.5 卸载钩子后目标程序异常
现象:卸载钩子后,某些程序鼠标键盘行为异常,甚至崩溃。
原因:UnhookWindowsHookEx调用时机不对,或者 DLL 在被其他进程使用时被卸载。全局钩子注入到多个进程,卸载时要确保所有进程都释放了 DLL。
解决:先UnhookWindowsHookEx,再FreeLibrary。不要在钩子回调还在执行时卸载。调试阶段可以用WH_MOUSE_LL这种不需要注入的钩子,减少对目标进程的影响。
6. 进阶技巧:用日志与最小复现验证 Hook 行为
Hook 调试最难受的地方是“看不见”——钩子到底有没有被调用、参数是什么、返回值对不对,全靠猜。我的习惯是给钩子加一层轻量日志,把关键事件写到文件里,再配合最小复现程序验证。
// 钩子回调里写日志(注意:仅调试用,正式版要去掉或异步化) void LogEvent(const char* tag, WPARAM wParam, POINT pt) { FILE* f = fopen("hook_log.txt", "a+"); if (f) { fprintf(f, "%s wParam=%u pt=(%d,%d)\n", tag, (unsigned)wParam, pt.x, pt.y); fclose(f); } }日志能回答三个问题:回调有没有进、事件类型对不对、坐标是不是预期值。如果日志里根本没有记录,说明钩子没装上或没生效;如果坐标不对,检查 DPI 缩放——高 DPI 下屏幕坐标和实际像素可能差一个缩放系数,WindowFromPoint会命中错误窗口。
验证时我一般开一个记事本做最小复现:在记事本里划词,看日志里hWnd是不是记事本的编辑框,WM_GETTEXT能不能拿到文字。记事本通过后再去浏览器、PDF 阅读器上试,逐步定位是哪类控件不支持。
还有一个容易忽略的点:32 位和 64 位进程的钩子不能互通。32 位 DLL 装的全局钩子对 64 位进程无效,反之亦然。如果目标程序是 64 位而你的工程是 Win32,划词在 64 位程序上就会失效。解决办法是分别编译 32 位和 64 位两个 DLL,按目标进程位数加载。
# 检查目标进程位数(任务管理器 -> 详细信息 -> 平台) # 32 位进程显示“32 位”,64 位显示“64 位” # 你的 DLL 位数必须和目标一致最后说个血泪经验:Hook 代码写完先别急着集成到主程序,单独写一个最小 exe + dll 跑通“安装钩子 → 划词 → 拿到文本 → 卸载钩子”全流程,再往业务里搬。我早期图省事直接在项目里改,结果钩子失效和业务 bug 混在一起,排查花了两倍时间。从那以后我每次做 Hook 都强制先走一遍最小复现,确认钩子生命周期干净了再集成。希望帮到你。
本文还有配套的精品资源,点击获取