【问题标题】:When I swap keys using SetWindowsHookEx WH_KEYBOARD_LL, why does my program get into a cycle of too many keyboard input events?当我使用 SetWindowsHookEx WH_KEYBOARD_LL 交换键时,为什么我的程序会进入一个键盘输入事件过多的循环?
【发布时间】:2014-12-04 20:54:23
【问题描述】:

我正在尝试为 Windows 系统编写一个交换 AB 键的程序,即当我按下 A 键时, B 被输入,反之亦然。

为此,我首先将 A 键映射为类似于 B 键的行为。这是我写的代码。

#include <stdio.h>
#include <windows.h>

HHOOK hook;

LRESULT CALLBACK keyboardHook(int nCode, WPARAM wParam, LPARAM lParam)
{
    KBDLLHOOKSTRUCT *p = (KBDLLHOOKSTRUCT *) lParam;
    DWORD newVkCode;
    INPUT inputs[1];
    UINT ret;

    char wParamStr[16];
    char vkStr[16] = "";

    if (wParam == WM_KEYDOWN)
        strcpy(wParamStr, "KEYDOWN");
    else if (wParam == WM_KEYUP)
        strcpy(wParamStr, "KEYUP");
    else if (wParam == WM_SYSKEYDOWN)
        strcpy(wParamStr, "SYSKEYDOWN");
    else if (wParam == WM_SYSKEYUP)
        strcpy(wParamStr, "SYSKEYUP");
    else
        strcpy(wParamStr, "UNKNOWN");

    if (p->vkCode == 10)
        strcpy(vkStr, "<LF>");
    else if (p->vkCode == 13)
        strcpy(vkStr, "<CR>");
    else
        vkStr[0] = p->vkCode;

    printf("%d - %s - %lu (%s) - %d - %lu\n",
           nCode, wParamStr, p->vkCode, vkStr, p->scanCode, p->time);

    inputs[0].type = INPUT_KEYBOARD;
    inputs[0].ki.wScan = 0;
    inputs[0].ki.dwFlags = 0;
    inputs[0].ki.time = 0;
    inputs[0].ki.dwExtraInfo = 0;

    if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
        inputs[0].ki.dwFlags = KEYEVENTF_KEYUP;
    }

    if (p->vkCode == 'A') {
        inputs[0].ki.wVk = 'B';
        ret = SendInput(1, inputs, sizeof (INPUT));
        return 1;
    }
    /*
    else if (p->vkCode == 'B') {
        inputs[0].ki.wVk = 'A';
        ret = SendInput(1, inputs, sizeof (INPUT));
        return 1;
    }
    */

    return CallNextHookEx(hook, nCode, wParam, lParam);
}

int main(int argc, char **argv)
{
    MSG messages;

    hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboardHook, NULL, 0);
    if (hook == NULL) {
        printf("Error %d\n", GetLastError());
        return 1;
    }

    printf("Waiting for messages ...\n");
    while (GetMessage (&messages, NULL, 0, 0))
    {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }
    return 0;
}

这按预期工作。我首先使用以下命令编译此代码。

"%vs80comntools%\vsvars32.bat"
cl /D_WIN32_WINNT=0x0401 /EHsc foo.c /link user32.lib

现在,我运行程序。我转到另一个程序,比如记事本,然后按 A 键并输入B。发生这种情况时,我会看到上述程序的以下输出。

C:\foo>foo.exe
Waiting for messages ...
0 - KEYUP - 13 (<CR>) - 28 - 874702239
0 - KEYDOWN - 65 (A) - 30 - 874703752
0 - KEYDOWN - 66 (B) - 0 - 874703752
0 - KEYUP - 65 (A) - 30 - 874703877
0 - KEYUP - 66 (B) - 0 - 874703877

现在,我继续下一步,将 B 键映射为类似于 A 键的行为。为此,我只需将上面注释的五行代码取消注释,编译并再次运行。

这也符合预期。当我按下 A 键时,B 被输入,当我按下 B 键时,A 被输入。但是,当我检查程序的输出时,我看到的事件太多了。

这是程序的输出,当我按下 A 键一次时。

C:\foo>foo.exe
Waiting for messages ...
0 - KEYUP - 13 (<CR>) - 28 - 874746621
0 - KEYDOWN - 65 (A) - 30 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYDOWN - 66 (B) - 0 - 874749008
0 - KEYDOWN - 65 (A) - 0 - 874749008
0 - KEYUP - 65 (A) - 30 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101
0 - KEYUP - 66 (B) - 0 - 874749101
0 - KEYUP - 65 (A) - 0 - 874749101

在此之后,当我按一次 B 时,我会看到以下输出。

0 - KEYDOWN - 66 (B) - 48 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYDOWN - 65 (A) - 0 - 874824590
0 - KEYDOWN - 66 (B) - 0 - 874824590
0 - KEYUP - 66 (B) - 48 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637
0 - KEYUP - 65 (A) - 0 - 874824637
0 - KEYUP - 66 (B) - 0 - 874824637

为什么这里有这么多活动?我猜这是因为当我按下 A 键时,

  • keyboardHook 函数通过 A 键按下的键盘输入事件调用。
  • 它处理键盘输入事件并为 B 键发送键盘输入事件。
  • 再次为 B 键调用 keyboardHook 函数。
  • 它现在处理它并为 A 发送一个事件。
  • 整个循环重复。
  • 10 个这样的循环后,Windows 系统可能会干扰并停止循环。

以上六点解释纯属想象。我对此有几个问题。

  1. 我们能否解释一下为什么使用 Microsoft 的官方文档会生成这么多事件?如果没有官方文档,我们是否可以通过编写另一个程序或进行一些实验来证明产生这么多事件的原因?
  2. 如果事件确实是由于重复同一个循环而产生的(即消耗A,生成B,消耗B,生成A,重复),那么它如何在10个循环后自动停止?这也可以使用官方文档、其他程序或任何实验来解释吗?

顺便说一句,我知道如何解决这个问题。对于keyboardHook函数中使用SendInput函数发送的假按键事件,在下面的语句中设置硬件扫描码为0

    inputs[0].ki.wScan = 0;

所以我可以利用这个事实来忽略keyboardHook函数中keyboardHook函数发送的事件,方法是修改代码的键映射部分如下。

    if (p->vkCode == 'A' && p->scanCode != 0) {
        inputs[0].ki.wVk = 'B';
        ret = SendInput(1, inputs, sizeof (INPUT));
        return 1;
    }
    else if (p->vkCode == 'B' && p->scanCode != 0) {
        inputs[0].ki.wVk = 'A';
        ret = SendInput(1, inputs, sizeof (INPUT));
        return 1;
    }

现在当我按下 A 键时,我得到了熟悉的输出。

C:\foo>foo.exe
Waiting for messages ...
0 - KEYUP - 13 (<CR>) - 28 - 875183112
0 - KEYDOWN - 65 (A) - 30 - 875186310
0 - KEYDOWN - 66 (B) - 0 - 875186310
0 - KEYUP - 65 (A) - 30 - 875186388
0 - KEYUP - 66 (B) - 0 - 875186388

有没有更好的方法来解决这个问题?

【问题讨论】:

  • 那么,您的问题是为什么循环不会无限期地继续?因为您似乎已经非常巧妙地证明了伪造的输入事件确实传递给了您的钩子,从而导致理论上的无限递归。
  • @IMSoP 我有两个问题:(1)我们能解释一下为什么使用微软的官方文档会生成这么多事件吗?如果没有官方文档,我们是否可以通过编写另一个程序或进行一些实验来证明产生如此多事件的原因? (2)如果事件确实是由于重复同一个周期(即消费A,产生B,消费B,产生A,重复)而产生的,那么它如何在10个周期后自动停止?这也可以使用官方文档、其他程序或任何实验来解释吗?
  • 好吧,问题 1 我想您已经演示了使用您的程序在一次迭代中将扫描代码设置为零,并在下一次迭代中读出;这是非常确定的证据,每次您生成虚假输入事件时,您创建的事件都会被发送到您的钩子。如果您仍然不相信,您可以修改它以将扫描码设置为 1、2、3、4 等。
  • @IMSoP 感谢您的 cmets。我同意我的第一个问题由我的程序和输出回答。该程序将“假输入”事件中的硬件扫描代码设置为零,并且输出显示程序也在接收这些“假输入”事件。输出的每一行的第四个字段显示硬件扫描码,我们可以在那里看到扫描码为 0 的事件。我还尝试将硬件扫描代码设置为其他数字,如 11、12 等,我看到这些数字也出现在输出中。所以我对此深信不疑。
  • 应用程序可能需要正确的扫描码才能正常运行。相反,检查注入标志以检测递归。

标签: c windows visual-studio keyboard-hook


【解决方案1】:

根据 cmets 中 Raymond Chen 对问题的建议,这里有一个完整的程序,它以正确的方式交换 AB,即通过检查注入的低级键盘输入事件的标志。换句话说,我执行以下测试来决定是否消费一个低级键盘输入事件并用另一个替换它。

(p->flags & LLKHF_INJECTED) == 0

这是完整的程序。

#include <stdio.h>
#include <windows.h>

HHOOK hook;

LRESULT CALLBACK keyboardHook(int nCode, WPARAM wParam, LPARAM lParam)
{
    KBDLLHOOKSTRUCT *p = (KBDLLHOOKSTRUCT *) lParam;
    DWORD newVkCode;
    INPUT inputs[1];
    UINT ret;

    char wParamStr[16];
    char vkStr[16] = "";

    if (wParam == WM_KEYDOWN)
        strcpy(wParamStr, "KEYDOWN");
    else if (wParam == WM_KEYUP)
        strcpy(wParamStr, "KEYUP");
    else if (wParam == WM_SYSKEYDOWN)
        strcpy(wParamStr, "SYSKEYDOWN");
    else if (wParam == WM_SYSKEYUP)
        strcpy(wParamStr, "SYSKEYUP");
    else
        strcpy(wParamStr, "UNKNOWN");

    if (p->vkCode == 10)
        strcpy(vkStr, "<LF>");
    else if (p->vkCode == 13)
        strcpy(vkStr, "<CR>");
    else
        vkStr[0] = p->vkCode;

    printf("%d - %s - %lu (%s) - %d - %lu\n",
           nCode, wParamStr, p->vkCode, vkStr, p->scanCode, p->time);

    inputs[0].type = INPUT_KEYBOARD;
    inputs[0].ki.wScan = 0;
    inputs[0].ki.dwFlags = 0;
    inputs[0].ki.time = 0;
    inputs[0].ki.dwExtraInfo = 0;

    if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
        inputs[0].ki.dwFlags = KEYEVENTF_KEYUP;
    }

    if (p->vkCode == 'A' && (p->flags & LLKHF_INJECTED) == 0) {
        inputs[0].ki.wVk = 'B';
        ret = SendInput(1, inputs, sizeof (INPUT));
        return 1;
    } else if (p->vkCode == 'B' && (p->flags & LLKHF_INJECTED) == 0) {
        inputs[0].ki.wVk = 'A';
        ret = SendInput(1, inputs, sizeof (INPUT));
        return 1;
    }

    return CallNextHookEx(hook, nCode, wParam, lParam);
}

int main(int argc, char **argv)
{
    MSG messages;

    hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboardHook, NULL, 0);
    if (hook == NULL) {
        printf("Error %d\n", GetLastError());
        return 1;
    }

    printf("Waiting for messages ...\n");
    while (GetMessage (&messages, NULL, 0, 0))
    {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }
    return 0;
}

这可以编译如下。

"%vs80comntools%\vsvars32.bat"
cl /D_WIN32_WINNT=0x0401 foo.c /link user32.lib

现在当我运行这个程序时,转到另一个程序,比如记事本,然后按 A 键,B 会被输入。发生这种情况时,我会看到上述程序的以下输出。

C:\lab\foo>foo.exe
Waiting for messages ...
0 - KEYUP - 13 (<CR>) - 28 - 588708476
0 - KEYDOWN - 65 (A) - 30 - 588711206
0 - KEYDOWN - 66 (B) - 0 - 588711206
0 - KEYUP - 65 (A) - 30 - 588711284
0 - KEYUP - 66 (B) - 0 - 588711284

【讨论】:

    猜你喜欢
    • 2017-03-15
    • 2011-08-23
    • 1970-01-01
    • 2021-06-17
    • 1970-01-01
    • 2020-05-09
    • 2016-05-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多