一个通用的键盘钩子
一个易于使用的键盘钩子DLL,适用于大多数应用程序。
问题
虽然安装和维护键盘钩子据称很简单,因为我们只需要查看::SetWindowsHookEx
API,但有几点值得思考。一个问题是,如果我们想钩住所有正在运行的线程(这很常见),那么回调进程函数必须位于DLL中。这个规则本身不是问题,但它对DLL重用性造成的衰减确实是个问题。每个应用程序都会以自己的方式处理捕获到的键盘事件,那么为什么我们要构建一个只能用于一个程序的DLL呢?
解决方案
规则不能改变,所以函数无论如何都必须位于DLL中,但是,我们可以尝试最大化DLL的重用性,使其适用于几乎所有类型的应用程序,也就是说,将其制作成一个通用的键盘钩子DLL。
为了实现这一目标,我们考虑了一些要点:
- 同步
该DLL将由多个进程使用,其中一些数据是共享的,至关重要的是我们要确保对这些共享数据块的同步访问。在此项目中,我们使用互斥体来实现此目的。
- 特定键盘事件的定义
要捕获哪些类型的键盘事件?在什么条件下?当捕获到合适的键盘事件时,应该通知谁以及如何通知?所有这些指定的细节将帮助应用程序钩住正确的事件。本项目中定义的
KEYENTRY
struct
包含了所有详细信息,稍后将进行描述。 - 有效地通知应用程序
当一个键盘事件被确定为匹配一个应用程序定义的所有标准和条件时,它也可能匹配其他应用程序定义的所有标准和条件,因此,一个事件可能需要通知多个应用程序。通常,我们通过回调函数或通过Windows消息系统将消息发送到应用程序窗口来通知应用程序。在这里使用回调函数是不安全的,因为我们必须等到函数返回。如果一个编写不当的应用程序在函数内部有无限循环,那么我们都会陷入困境。使用Windows消息系统是一个替代方案,显然应该避免使用
::SendMessage
,因为存在同样的问题,所以最终,::PostMessage
似乎是最佳选择。尽管如此,使用
::PostMessage
也有一些缺点。首先,它不能保证是实时的,今天使用::PostMessage
发送的消息可能明天才会收到(我承认这有点夸张),但当应用程序收到通知时,系统信息(如键盘状态、前台窗口等)可能已经改变了,所以DLL必须在捕获到事件的瞬间尝试发送所有这些有价值的信息。其次,还有一个更糟糕的限制,在Win32平台上,我们一次只能向应用程序发送最多64位的数据,32位通过wParam
,32位通过lParam
。我甚至不会考虑在堆内存中分配数据并将指针发送给应用程序,消息可能永远不会收到,内存很可能会泄漏。在此项目中,wParam
被用于存储发生捕获到的键盘事件的窗口句柄,因此我们需要找到一种方法,将尽可能多的数据打包到lParam
中,同时确保原始lParam
的重要部分(如扫描码、前一个按键状态、上/下标志等)保持不变。 - 全面的键盘事件信息检索
作为一个通用的DLL,至关重要的是,应用程序应该能够检索尽可能多的关于捕获到的键盘事件的详细信息。本项目中定义的
KEYRESULT
struct
包含了所有详细信息,稍后将进行描述。
实现
定义键盘事件
KEYENTRY
struct
定义了应该捕获哪些类型的事件以及如何处理捕获到的事件。它包含以下成员:
类型 | 名称 | 注释 |
---|---|---|
UINT |
nMessage |
捕获到适当的按键事件时将发送的通知消息,不能为零。 |
HWND |
hCallWnd |
将发送通知消息的窗口句柄,不能为空。 |
HWND |
hHookWnd |
发生按键事件的窗口句柄。如果设置为null ,则捕获任何窗口中发生的按键事件。 |
BYTE |
iKeyEvent |
要捕获的按键事件类型(按下、抬起、重复)。如果设置为0,则捕获所有类型的事件。 |
BYTE |
iMinVKCode |
要捕获的最小虚拟键码,不能大于iMaxVKCode 。 |
BYTE |
iMaxVKCode |
要捕获的最大虚拟键码,不能小于iMinVKCode 。 |
BYTE |
iCombKeys |
组合键标志(alt、ctrl、shift)。如果设置为0,则组合键状态无关紧要。 |
BYTE |
iIndicators |
键盘指示灯(大写锁定、数字锁定、滚动锁定)的开关状态。如果设置为0,则指示灯状态无关紧要。 |
DWORD |
dwReserved |
保留,请勿使用。 |
iKeyEvent
的值可以是以下任意组合:
KH_KEY_DOWN
,KH_KEY_UP
,KH_KEY_REPEAT
.
iCombKeys
的值可以是以下任意组合:
KH_ALT_PRESSED
,KH_ALT_NOT_PRESSED
,KH_CTRL_PRESSED
,KH_CTRL_NOT_PRESSED
,KH_SHIFT_PRESSED
,KH_SHIFT_NOT_PRESSED
,
iIndicators
的值可以是以下任意组合:
KH_CAPSLOCK_ON
,KH_CAPSLOCK_OFF
,KH_NUMLOCK_ON
,KH_NUMLOCK_OFF
,KH_SCRLOCK_ON
,KH_SCRLOCK_OFF
,
检索捕获到的键盘事件信息
KEYRESULT
struct
用于检索捕获到的键盘事件的详细信息。它包含以下成员:
类型 | 名称 | 注释 |
---|---|---|
HWND |
hOccurredInWnd |
发生按键事件的窗口句柄。 |
BYTE |
iKeyEvent |
捕获到的按键事件类型(按下、抬起或重复)。 |
BYTE |
iVKCode |
捕获到的按键产生的虚拟键码。 |
BYTE |
iCombKeys |
捕获按键事件时组合键(alt、ctrl、shift)的标志。 |
BYTE |
iIndicators |
捕获按键事件时键盘指示灯(大写锁定、数字锁定、滚动锁定)的开关状态。 |
TCHAR |
chPrintableChar |
捕获到的按键产生的可打印字符,如果不可打印则为0。 |
TCHAR |
szKeyName[32] |
按键的名称。例如:“Shift”。 |
注意:KEYRESULT
是 tagKeyResultW
或 tagKeyResultA
的 typedef
,取决于是否定义了UNICODE
。chPrintableChar
和 szKeyName[32]
要么是 wchar_t
要么是 char
。
iKeyEvent
、iVKCode
、iCombKeys
和 iIndicators
的值与KEYENTRY
struct
的相应成员类似,只是这个struct
仅用于检索数据,因此这些值将代表实际的事件数据。例如,在KEYENTRY
struct
中,其iKeyEvent
成员可能是KH_KEY_DOWN | KH_KEY_REPEAT
,表示用户希望捕获按键按下和重复事件,而在KEYRESULT
struct
中,iKeyEvent
永远不会是这种组合,因为如果捕获到的事件是按键按下,则它不是重复按键。
DLL 导出函数
DLL导出的函数数量很少且易于使用,以下是一些重要函数的描述。有关函数和返回代码的完整列表,请下载并查看KeyHook.h。
LRESULT __declspec(dllexport) InstallKeyHook();
返回值
如果成功,函数返回KH_OK
,否则返回错误代码。
备注
将当前进程注册到钩子。应用程序在使用钩子之前必须调用此函数。
LRESULT __declspec(dllexport) UninstallKeyHook();
返回值
如果成功,函数返回KH_OK
,否则返回错误代码。
备注
注销当前进程与钩子的关联。应用程序在完成使用钩子后应尽快调用此函数,以释放所有占用的全局资源。
LRESULT __declspec(dllexport) AddKeyEntry(LPCKEYENTRY lpEntry);
参数
lpEntry
[in]
包含要捕获的按键事件信息的KEYENTRY
struct
的地址。
返回值
如果成功,函数返回KH_OK
,否则返回错误代码。
备注
将新的按键条目添加到钩子条目列表中,以便捕获符合lpEntry
中定义的条件的按键事件。
LRESULT __declspec(dllexport) RemoveKeyEntry(LPCKEYENTRY lpEntry);
参数
lpEntry
[in]
包含要从钩子条目列表中删除的按键条目信息的KEYENTRY
struct
的地址。
返回值
如果成功,函数返回KH_OK
,否则返回错误代码。
备注
从钩子条目列表中删除指定的按键条目,以便不再捕获指定的按键事件。其他进程添加的按键条目不可访问,也不会受到影响。
LRESULT __declspec(dllexport) RemoveAllKeyEntries();
返回值
如果成功,函数返回KH_OK
,否则返回错误代码。
备注
从钩子条目列表中删除当前进程添加的所有按键条目。其他进程添加的按键条目不可访问,也不会受到影响。
LRESULT __declspec(dllexport) GetKeyEventResult(WPARAM wParam, LPARAM lParam, LPKEYRESULT lpKeyResult, UINT nMask = KH_MASK_ALL);
参数
wParam
[in]
随通知消息接收到的WPARAM
。lParam
[in]
随通知消息接收到的LPARAM
。lpKeyResult
[out]
将接收捕获到的按键事件详细信息的KEYRESULT
struct
的地址。nMask
[in]
指定必须填充KEYRESULT
struct
的哪些成员。
返回值
如果成功,函数返回KH_OK
,否则返回错误代码。
备注
当捕获到预定义的按键事件时,将向目标窗口(在KEYENTRY::hCallWnd
中定义)发送在KEYENTRY::nMessage
中定义的通知消息,以及一个wParam
和一个lParam
,其中包含捕获到的按键事件的详细信息。此函数解析这些参数并提取信息。
nMask
的值可以是以下任意组合。如果您不需要所有信息,则指定尽可能少的标志以减少计算时间。
符号 | 含义 |
---|---|
KH_MASK_ALL |
所有成员都必须填充。 |
KH_MASK_OCCURREDWND |
必须填充 hOccurredInWnd 成员。 |
KH_MASK_EVENTTYPE |
必须填充 iKeyEvent 成员。 |
KH_MASK_VKCODE |
必须填充 iVKCode 成员。 |
KH_MASK_COMBKEYS |
必须填充 iCombKeys 成员。 |
KH_MASK_INDICATORS |
必须填充 iIndicators 成员。 |
KH_MASK_PRINTABLECHAR |
必须填充 chPrintableChar 成员。 |
KH_MASK_KEYNAME |
必须填充 szKeyName 成员。 |
待办事项
要使用该DLL,您可以按照以下简单步骤操作:
- 将 KeyHook.h、KeyHook.dll、Keyhook.lib 复制到您的项目目录中,通过“Project - Add to Project - Files”将 KeyHook.lib 添加到您的工作区,并在需要的地方包含 KeyHook.h。
- 调用
InstallKeyHook
来安装钩子。 - 调用
AddKeyEntry
来注册按键条目,这些条目包含有关应捕获哪些类型的键盘事件以及应通知谁的信息。 - 在您的应用程序窗口收到通知消息后,调用
GetKeyEventResult
来检索捕获到的按键事件的信息,wParam
和lParam
的值保持不变。 - 在您完成使用钩子后,调用
UninstallKeyHook
来释放资源。
就是这样!真的很简单,不是吗?现在,是时候看一些代码示例了。
代码示例
在这个示例中,我们将编写一个键盘记录器,它记录任何窗口中发生的按键,不,我绝不纵容密码窃取。
/////////////////////////////////////////////////////// // MyDlg.h /////////////////////////////////////////////////////// class CMyDlg : public CDialog { // ... ... protected: // Add a function to handle the notification message LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam); } /////////////////////////////////////////////////////// // MyDlg.cpp /////////////////////////////////////////////////////// // ID of the notification message that will be sent to us #define WM_MY_MESSAGE (WM_APP + 100) BEGIN_MESSAGE_MAP(CMyDlg, CDialog) //{{AFX_MSG_MAP(CMyDlg) // ... ... //}}AFX_MSG_MAP ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage) // Maps our message END_MESSAGE_MAP() BOOL CMyDlg::OnInitDialog() { CDialog::OnInitDialog(); // ... ... // TODO: Add extra initialization here LRESULT lRes = InstallKeyHook(); // Install the hook ASSERT(lRes == KH_OK); // Prepare the KEYENTRY struct KEYENTRY ke; ke.nMessage = WM_MY_MESSAGE; // Our message ID ke.hCallWnd = m_hWnd; // Send message to this window ke.hHookWnd = 0; // Capture key-strokes occurred in any windows ke.iCombKeys = 0; // Combination key states do not matter ke.iIndicators = 0; // Caps-lock, Num-lock, // Scroll-lock on/off states do not matter ke.iKeyEvent = KH_KEY_DOWN | KH_KEY_REPEAT; // Capture key-down and key-repeat events ke.iMinVKCode = 0; // Capture all keys regardless of their virtual key codes ke.iMaxVKCode = 255; // Add the entry to the hook lRes = AddKeyEntry(&ke); ASSERT(lRes == KH_OK); return TRUE; // return TRUE unless you set the focus to a control } void CMyDlg::OnDestroy() { CDialog::OnDestroy(); // TODO: Add your message handler code here UninstallKeyHook(); // Uninstall the hook to cleanup. } // Handler of WM_MY_MESSAGE notification LRESULT CMyDlg::OnMyMessage(WPARAM wParam, LPARAM lParam) { // In this sample we will display the event types and // characters that the captured key events produced. KEYRESULT kr; // The struct to receive information // Information that we need to extract are event type and printable character UINT nMask = KH_MASK_EVENTTYPE | KH_MASK_PRINTABLECHAR; // This function extracts event details LRESULT lRes = GetKeyEventResult(wParam, lParam, &kr, nMask); ASSERT(lRes == KH_OK); // We only display key-strokes that produce printable characters if (kr.chPrintableChar != 0) { CString sEvent; if (kr.iKeyEvent == KH_KEY_DOWN) sEvent = _T("Key Down"); else if (kr.iKeyEvent == KH_KEY_UP) sEvent = _T("Key Up"); else if (kr.iKeyEvent == KH_KEY_REPEAT) sEvent = _T("Key Repeat"); else ASSERT(FALSE); // will never happen CString sMsg; sMsg.Format(_T("Event: %s\nCharacter:%c"), sEvent, kr.chPrintableChar); MessageBox(sMsg); // display the result } return (LRESULT)0; }
限制
同时使用该钩子的最大进程数以及所有进程注册条目的最大数量是有限制的。如果达到这些限制,后续操作可能会失败。当前版本支持最多256个独立进程,总共注册最多1024个按键条目。
此DLL不是低级别键盘钩子,因此它不会拦截低级别系统按键事件,如Alt-Tab、Ctrl-Alt-Del等。要捕获这些事件,您需要指定WH_KEYBOARD_LL
而不是WH_KEYBOARD
,作为::SetWindowsHookEx
的第二个参数。有关低级别键盘钩子的详细信息,请参阅MSDN上的SetWindowsHookEx
。
历史
v1.00 -- 2004年5月6日
- 初始发布。
v1.01 -- 2004年5月14日
- 修复了组合键和键盘指示键验证中的一个错误。
- 更新了演示项目,现在它更清晰地演示了钩子的使用。
v1.02 -- 2004年5月26日
- 修复了事件翻译中的一些小问题。
- 删除了不必要的组合键标志和指示器标志。