Windows 钩子很奇怪
Source: Dev.to
(请提供需要翻译的正文内容,我将为您翻译成简体中文。)
钩子程序
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && wParam == WM_KEYDOWN)
{
KBDLLHOOKSTRUCT *kb = (KBDLLHOOKSTRUCT*)lParam;
unsigned char keyboard_state[256];
wchar_t unicodeData[5] = {0};
GetKeyboardState(keyboard_state);
int unicodeKey = ToUnicodeEx(kb->vkCode,
kb->scanCode,
keyboard_state,
unicodeData,
sizeof(unicodeData) / sizeof(wchar_t),
0,
NULL);
if (unicodeKey > 0)
{
printf("%ls", unicodeData);
}
if (kb->vkCode == VK_RETURN)
{
printf("\n");
}
}
return CallNextHookEx(hook, nCode, wParam, lParam);
}
int main()
{
hook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
/* message loop intentionally empty */
}
}
我觉得还不错。
问题
在玩得开心的时候,我发现 Caps Lock 或 Shift 并没有在打印的键日志中被记录。对此我感到十分困惑,于是像所有自尊心强的程序员一样——Google。我找到了这篇 Stack Overflow 提问:
一条评论给出了以下修复方案:
short caps_held = GetKeyState(VK_CAPITAL) & 0x0001;
short shift_held = GetKeyState(VK_SHIFT) & 0x8000;
if (caps_held) keyboard_state[VK_CAPITAL] |= 0x01;
else keyboard_state[VK_CAPITAL] &= ~0x01;
if (shift_held) keyboard_state[VK_SHIFT] |= 0x80;
else keyboard_state[VK_SHIFT] &= ~0x80;
注意: 我知道应该使用
GetAsyncKeyState,但我这个叛逆者用了GetKeyState因为它能工作,那为什么不呢?以后记住这一点。
Caps 和 Shift 键的问题得到了解决,但出现了新的问题:Ctrl。
当我按下 Ctrl + 任意键 时,会得到奇怪的字符。尤其是 Ctrl + C,我得到的是 ♥。之前没有出现过这种情况,于是我开始调查。
在 keyboard_state 中,Ctrl + C 在 unicodeData[0] 的值为 3,这是一个控制字符,会被渲染为 ♥。我尝试清除高位:
keyboard_state[VK_CONTROL] &= ~0x80;
问题似乎解决了……但真的解决了吗?
为什么 GetKeyboardState 能工作?
GetKeyboardState() 在不泵送消息循环的应用中是无法工作 的,那到底发生了什么?答案是 GetKeyState。
出于某种原因,如果在钩子回调 内部 调用 GetKeyState,它会返回键的先前状态——即在调用你的函数之前的状态。在此过程中,它会做一些事情,使得 GetKeyboardState 实际上得以更新。
我发现仅仅调用 GetKeyState(0) 就消除了上面所示的 caps‑and‑shift 条件判断。而 GetAsyncKeyState(0) 则没有这种副作用。这是有意为之吗?我不禁这样想。
Raymond Chen on GetAsyncKeyState / GetKeyState
在阅读时,我看到 Raymond Chen(传奇人物)在 2004 年写的一篇文章:
Excerpt
[excerpt omitted for brevity]
不过,我对这段解释有一些疑问。
-
当他说函数返回的键状态是“基于你从输入队列中检索到的消息”时,这里的 输入队列 到底指的是什么?
-
MSDN 文档中写道:
“从此函数返回的键状态会随着线程从其 消息队列 中读取键消息而改变。”
因此我推测 输入队列 与 消息队列 是同一个东西,但这并不合逻辑,因为 GetKeyboardState 是基于消息队列的状态获取键值,而在我的低级钩子中并没有对该消息队列进行泵送(pump)。这到底是怎么回事?如果两个队列是相同的,GetKeyboardState 为什么一开始就不起作用?这些队列是不同的还是相同的?文档说得不清楚。
我的猜想:GetKeyboardState 和 GetKeyState 读取的是 不同 的队列或状态,而 GetKeyState 以某种方式同步了 GetKeyboardState 所读取的状态。
Source: …
简单测试程序
我决定自己测试一下 GetKeyState:
int main()
{
while (true)
{
short ctrlPressed = GetKeyState('A');
printf("%d\n", ctrlPressed & 0x8000);
}
}
当我按下 A 键时,它会打印 32768,即使这个程序根本没有与消息队列交互。如果我们字面上接受陈的说法和 MSDN 的描述,这意味着 每个 Windows 应用都有一个消息队列(这可能是对的),但 GetKeyState 到底是从哪里读取的?
MSDN 对 GetKeyState 的条目写道:
“从此函数返回的键状态会随着线程从其消息队列读取键消息而改变。”
我的简单程序从未读取过它的消息队列,我的钩子程序(据我所知)也没有读取,所以这里显然有些不对劲。
GetKeyboardState 文档 (MSDN)
“应用程序可以调用此函数检索所有虚拟键的当前状态。状态会随着线程从其消息队列中移除键盘消息而改变。状态不会因为键盘消息被发布到线程的消息队列而改变,也不会因为键盘消息被发布到或从其他线程的消息队列中检索而改变。”
如果我们按字面理解 MSDN 的表述,那么逻辑上这意味着 GetKeyState 在获取键状态之前移除键盘消息并处理队列,这可以解释为什么我的简单程序能够工作。这是一个带有偏见的假设,我并不认为情况是这样。
未解答问题概览
-
GetKeyState从哪个队列读取?- 是线程自己的消息队列、系统范围的队列,还是其他什么队列?
-
为什么在低级钩子中调用
GetKeyState会“触发”GetKeyboardState?- 是否存在未公开的副作用,使键盘状态被强制刷新?
-
GetKeyState与GetKeyboardState是否查看键盘的不同内部表示?- 如果是,它们是如何保持同步的?
-
Raymond Chen 所说的“输入队列”指的是什么,它与每线程的消息队列有什么关联?
这些是我仍在努力调和的点。任何澄清(或指向更深入文档的引用)都将不胜感激。
我唯一能解释这种不一致的方式是:Chen 的帖子和 MSDN 的文档要么过于简化,要么已经过时,亦或是这属于未文档化的功能。考虑到 Chen 的文章写于 20 多年前,三者都有可能,但我实在不清楚。
在我深入研究后可以肯定的是,GetKeyState(0) 会刷新 GetKeyboardState 所读取的内容,至于 为什么 仍是未知。这在我使用的 Windows 10 与 Windows 11 版本上有效,但我没有在 Windows 7 上进行测试。以后是否会改变我也不敢保证,所以 请谨慎。
当然,你也可以直接在钩子里自行维护键的状态:
else if (nCode == HC_ACTION && wParam == WM_KEYUP) {
KBDLLHOOKSTRUCT *kb = (KBDLLHOOKSTRUCT*)lParam;
if (kb->vkCode == VK_LSHIFT || kb->vkCode == VK_RSHIFT) {
shiftHeld = true;
printf("%s\n", "Shift released");
}
}
这同样是可行的,而且更为细致。
希望本文能帮助使用这些钩子的人。如果我有任何错误,或者你了解背后的原因,请留下评论或联系我。我随时乐意交流,也想弄清楚到底发生了什么。
下次再说…