C++ 键盘钩子

Posted

技术标签:

【中文标题】C++ 键盘钩子【英文标题】:C++ KeyBoard hook 【发布时间】:2014-10-21 18:11:43 【问题描述】:

主题:将某些键替换为另一个键值。

例如如果我按 P 应该是 F24。

当我尝试从 .ini 文件中加载键值时,钩子不再是全局的。仅当 winapi 表单处于焦点时才有效。

我的 DLL 代码:

    extern "C" __declspec(dllexport) LRESULT CALLBACK KeyboardHook(int, WPARAM, LPARAM);
    extern "C" __declspec(dllexport) void loadSettings(LPSTR);
    bool shouldUpdateKey = false;

    int ArcherKey;

    LRESULT CALLBACK KeyboardHook(int code, WPARAM wParam, LPARAM lParam)
        

            if ((lParam >> 20))
            
                if (wParam == ArcherKey) 
                
                shouldUpdateKey = shouldUpdateKey ? false : true;
                if (shouldUpdateKey) 
                
                MessageBox(NULL, L"ArcherKey", L"", MB_OK);
                keybd_event(0x87, 45, 1, 0); //press F24
                return 1; 
                
                
            


            return CallNextHookEx(NULL, code, wParam, lParam);
        

      LPSTR GetValueFromINI(LPSTR FileName, LPSTR Section, LPSTR Key)
        
            char *key;
            key = (char *)malloc(256);
            GetPrivateProfileStringA(Section, Key, NULL, key, 256, FileName);
            return key;
            free(key);
        

      void loadSettings(LPSTR FileName) 
        
            ArcherKey = atoi(GetValueFromINI(FileName, "HotKey", "Archer key"));
        

我使用 shouldUpdateKey 来避免 x2 回调(按下和按下按键时)调用。我也尝试添加这个语句 if (lParam >>31) ^ 1,但是这个语句总是错误的。

.exe代码:

LRESULT(*pKeybHook)(int, WPARAM, LPARAM);
HHOOK hhookMsg;
void(*loadSettings)(LPSTR);

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPTSTR    lpCmdLine,
                     _In_ int       nCmdShow)

/* default code */

HMODULE dll = LoadLibrary(_T("MainHookDLL.dll"));
    if (dll)
    

        pKeybHook = (LRESULT(*)(int, WPARAM, LPARAM)) GetProcAddress(dll, "_KeyboardHook@12");

        loadSettings = (void(*)(LPSTR)) GetProcAddress(dll, "loadSettings");

        loadSettings("C:\\Settings.ini");

        hhookMsg = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)(pKeybHook), dll, 0);

    

/* defult code */
    UnhookWindowsHookEx(hhookMsg); // unhook
    FreeLibrary(dll);
    return (int) msg.wParam;
       

Settings.ini 结构:

[HotKey]
Archer key=80

所以我的问题: 如果尝试从文件加载设置,则挂钩仅在活动的 winapi 窗口中有效。它显示 MessageBox\etc,但仅以活动的 winapi 形式显示。 如果将 wParam == ArcherKey 替换为 wParam == 80,它将在所有应用程序中全局工作。 我调试我的应用程序,从 .ini 文件加载后,我的 ArcherKey = 80。所以我真的不明白我到底犯了什么错误。

【问题讨论】:

【参考方案1】:

据我回忆,如果钩子是全局的,则包含 HOOKPROC 的 DLL 会在所有其他进程中加载​​。这意味着您在内存中有多个 DLL 实例。由于您从应用程序中调用 loadSettings(..),因此 ArcherKey 的值仅针对该进程进行初始化。这会导致您观察到的行为。

要改变这一点,您应该将 DllMain(..) 函数修改为如下所示:

BOOL WINAPI DllMain(HINSTANCE hinstDLL,  // handle to DLL module
                    DWORD fdwReason,     // reason for calling function
                    LPVOID lpReserved )  // reserved


   switch( fdwReason ) 
    
      case DLL_PROCESS_ATTACH:
         loadSettings("C:\\Settings.ini");
         break;

      case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
         break;

      case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
         break;

      case DLL_PROCESS_DETACH:
         // Perform any necessary cleanup.
         break;
   

   return TRUE;

这会为所有正在安装挂钩的进程初始化 ArcherKey 的值,因为 Windows 在加载 DLL 时会调用 DllMain。出于测试目的,您可以添加 MessageBeep(0);在调用 loadSettings(..) 以验证该部分代码是否已执行之前。

快速浏览SetWindowsHookEx(..) 的文档让我的恐惧成真:如果您正在编译一个 32 位 DLL,您将能够挂钩 64 位进程,反之亦然。为此,您必须使用不同名称的 HOOKPROC 实现 64 位版本的 dll。

【讨论】:

哦,我应该阅读更多文档 -.- 我只是在 DLL 条目 func 中添加 MessageBox 时理解这一点,每次我在新窗口中执行键盘操作时,dll 都会创建新实例(如果这是正确的含义)。谢谢,现在我知道我该怎么做了。【参考方案2】:

钩子被“注入”到其他进程中,这意味着您的整个 DLL 将被加载到所有相关进程中,就好像进程本身(例如 Notepad.exe)调用了 LoadLibrary()。所以在这种情况下(在其他进程中,例如 Notepad.exe),你的设置不会被加载,所以 ArcherKey 不会被初始化,所以消息框不会出现。

所以你必须让你的 DLL 来做初始化,而不是一个单独的 .exe。您可以通过 DLL_PROCESS_ATTACH 上的 DllMain 初始化 ArcherKey(加载您的设置)(尽管此时需要注意哪些 API 是安全的 - 大多数会导致加载其他 DLL 的任何调用都是禁止的),或者您可以大致添加代码:

static DWORD initialized = 0;
static int ArcherKey;

LRESULT CALLBACK KeyboardHook(int code, WPARAM wParam, LPARAM lParam)

    if (!initialized)
    
        loadSettings();
    

    ...

尽管这段代码根本不可取,因为长时间运行的钩子至少是相当糟糕的形式,并且可能会导致问题(例如,停止该进程)。或者,您可以将数据放在已知的共享位置。编辑:在the accepted answer to a similar question 中有一些关于共享值方式的好建议。

【讨论】:

谢谢,我试试这个。【参考方案3】:
LPSTR GetValueFromINI(LPSTR FileName, LPSTR Section, LPSTR Key)

    char *key;
    key = (char *)malloc(256);
    GetPrivateProfileStringA(Section, Key, NULL, key, 256, FileName);
    return key;
    free(key); // <- will never happen


ArcherKey = atoi(GetValueFromINI(...)); // <- does not clean up

内存泄漏

【讨论】:

以上是关于C++ 键盘钩子的主要内容,如果未能解决你的问题,请参考以下文章

C++ 键盘钩子 - 参数 nCode 是啥意思?

C++ 键盘钩子

C++ 键盘钩子 CTRL 键卡住

键盘钩子怎么 使用

delphi键盘钩子没效果

C# WinForm键盘钩子