使用 WH_KEYBOARD_LL 和 keybd_event (Windows) 实现全局键盘钩子

14

我想编写一个简单的全局键盘钩子程序来重定向一些按键。例如,当程序被执行时,我在键盘上按下'a'键,程序可以禁用该按键并模拟'b'键的点击。我不需要图形用户界面,只需要控制台即可(保持运行)。

我的计划是使用全局钩子来捕获键盘输入,然后使用keybd_event来模拟键盘输入。但是我遇到了一些问题。

第一个问题是,程序可以正确地阻止'A'键,但如果我在键盘上敲击'A'键一次,回调函数中的printf会执行两次,以及keybd_event也会执行两次。因此,如果我打开一个文本文件,我按一次'A'键,就会有两个'B'键被输入。这是为什么?

第二个问题是,为什么使用WH_KEYBOARD_LL的钩子可以在没有dll的情况下在其他进程中工作?我以为我们必须使用dll来创建全局钩子,直到我写了这个例子...

#include "stdafx.h"
#include <Windows.h>
#define _WIN32_WINNT 0x050

LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    BOOL fEatKeystroke = FALSE;

    if (nCode == HC_ACTION)
    {
        switch (wParam)
        {
        case WM_KEYDOWN:
        case WM_SYSKEYDOWN:
        case WM_KEYUP:
        case WM_SYSKEYUP:
            PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
            if (fEatKeystroke = (p->vkCode == 0x41)) {     //redirect a to b
            printf("Hello a\n");
            keybd_event('B', 0, 0, 0);
            keybd_event('B', 0, KEYEVENTF_KEYUP, 0);
            break;
            }
            break;
        }
    }
    return(fEatKeystroke ? 1 : CallNextHookEx(NULL, nCode, wParam, lParam));
}

int main()
{
    // Install the low-level keyboard & mouse hooks
    HHOOK hhkLowLevelKybd = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, 0, 0);

    // Keep this app running until we're told to stop
    MSG msg;
    while (!GetMessage(&msg, NULL, NULL, NULL)) {    //this while loop keeps the hook
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    UnhookWindowsHookEx(hhkLowLevelKybd);

    return(0);
}

非常感谢!


全局钩子不会阻塞输入,它们只是让你预览输入。 - Cody Gray
根据http://msdn.microsoft.com/en-us/library/windows/desktop/ms644985%28v=vs.85%29.aspx,"@CodyGray" - "...可能返回一个非零值,以防止系统将消息传递给其余的钩子链或目标窗口过程。" 对我来说,防止系统将消息传递给目标窗口过程看起来就像是阻塞。 - Ivan Danilov
1
对于那些试图让这个程序工作但是遇到了ERROR_HOOK_NEEDS_HMOD(1428)错误的人,请参考SetWindowsHookEx文档,"如果hMod参数为NULL且dwThreadId参数为零,则可能会发生错误"。因此,您必须指定hMod,但在这种情况下,您可以使用任何合法值,因为对于低级别钩子不会注入任何DLL。例如,您可以使用GetModuleHandle("kernel32.dll") - user
3个回答

11

你的回调函数会执行两次,这是因为 WM_KEYDOWNWM_KEYUP 两个消息都会调用它。 当你按下键盘上的某个键时,Windows 会通过 WM_KEYDOWN 消息调用回调函数;当你松开该键时,Windows 会通过 WM_KEYUP 消息再次调用回调函数。这就是为什么回调函数会执行两次。

你应该将 switch 语句更改为以下内容:

switch (wParam)
{
    case WM_KEYDOWN:
    case WM_SYSKEYDOWN:
    case WM_KEYUP:
    case WM_SYSKEYUP:
        PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
        if (fEatKeystroke = (p->vkCode == 0x41))  //redirect a to b
        {     
            printf("Hello a\n");

            if ( (wParam == WM_KEYDOWN) || (wParam == WM_SYSKEYDOWN) ) // Keydown
            {
                keybd_event('B', 0, 0, 0);
            }
            else if ( (wParam == WM_KEYUP) || (wParam == WM_SYSKEYUP) ) // Keyup
            {
                keybd_event('B', 0, KEYEVENTF_KEYUP, 0);
            }
            break;
        }
        break;
}
关于你的第二个问题,我认为你已经从@Ivan Danilov的回答中得到了答案。

8

第一个很简单。您会得到一个按下键和另一个松开键的键盘事件。

至于为什么它可以在没有DLL的情况下工作 - 这是因为它是一个全局钩子。与特定线程的钩子不同,它在您自己的进程中执行,而不是在发生键盘事件的进程中执行。这是通过向安装了钩子的线程发送消息来完成的 - 这正是为什么您需要在此处使用消息循环的原因。如果没有它,您的钩子就无法运行,因为没有人来侦听传入的消息。

对于特定线程的钩子,需要DLL,因为它们在另一个进程的上下文中调用。为使此功能正常工作,您的DLL应注入该进程。但这里不是这种情况。


1
这里唯一奇怪的是MSDN说:“所有全局钩子函数必须在库中。” - user
2
这完全不对。所有的全局钩子必须在 DLL 中,除非是低级别的钩子。文档在这方面缺乏说明。这里使用的语言是 C++,但是因为低级别的钩子并不需要 DLL,所以 C# 也可以使用。 - Sam Hobbs

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接