当我使用 SetWindowsHookEx WH_KEYBOARD_LL 交换按键时,为什么我的程序会陷入键盘输入事件过多的循环?

Sus*_*Pal 2 c windows keyboard-hook visual-studio

我正在尝试为 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;
}
Run Code Online (Sandbox Code Playgroud)

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

"%vs80comntools%\vsvars32.bat"
cl /D_WIN32_WINNT=0x0401 /EHsc foo.c /link user32.lib
Run Code Online (Sandbox Code Playgroud)

现在,我运行该程序。我转到另一个程序,例如记事本,然后按下该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
Run Code Online (Sandbox Code Playgroud)

现在,我继续下一步,将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
Run Code Online (Sandbox Code Playgroud)

之后,当我按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
Run Code Online (Sandbox Code Playgroud)

为什么这里有这么多活动?我猜会发生这种情况是因为当我按下按键A

  • keyboardHook函数通过按键的键盘输入事件来调用A
  • 它处理键盘输入事件并发送该B键的键盘输入事件。
  • keyboardHook再次针对该键调用该函数B
  • 它会处理它并暂时发送一个事件A
  • 整个循环重复进行。
  • 经过 10 个这样的循环后,Windows 系统可能会干扰并停止该循环。

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

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

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

    inputs[0].ki.wScan = 0;
Run Code Online (Sandbox Code Playgroud)

因此我可以利用这个事实通过修改代码的键映射部分来忽略函数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;
    }
Run Code Online (Sandbox Code Playgroud)

现在,当我按下该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
Run Code Online (Sandbox Code Playgroud)

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

Sus*_*Pal 6

按照 Raymond Chen 在问题评论中的建议,这里是一个完整的程序,它以正确的方式交换AB,即通过检查低级键盘输入事件的注入标志。换句话说,我执行以下测试来决定是否使用低级键盘输入事件并将其替换为另一个事件。

(p->flags & LLKHF_INJECTED) == 0
Run Code Online (Sandbox Code Playgroud)

这是完整的程序。

#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;
}
Run Code Online (Sandbox Code Playgroud)

这可以编译如下。

"%vs80comntools%\vsvars32.bat"
cl /D_WIN32_WINNT=0x0401 foo.c /link user32.lib
Run Code Online (Sandbox Code Playgroud)

现在,当我运行该程序时,转到另一个程序(例如记事本),然后按下该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
Run Code Online (Sandbox Code Playgroud)