了解win32低级鼠标和键盘钩子

15

我正在尝试捕获全局鼠标和键盘输入。

LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) {
  if (nCode >= 0) {
    if (wParam == WM_RBUTTONDOWN) printf("right mouse down\n");
    if (wParam == WM_RBUTTONUP) printf("right mouse up\n");
  }
  return CallNextHookEx(0, nCode, wParam, lParam);
}

HHOOK mousehook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, NULL, 0);
while(true) {
  MSG msg;
  if (PeekMessage(&msg,0,0,0,PM_REMOVE)) {
    printf("msg recvd\n");
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
#ifdef TEST
  Sleep(50);
#endif
}

如果我在代码中用#define TEST来加入Sleep,那么鼠标会变得非常迟缓,这是可以预料到的,因为这样只允许鼠标每秒更新20次。而如果没有Sleep,那么CPU将被占用100%。但现在还好(如果使用GetMessage,这种情况就不会出现)。

据我所知,低级钩子通过切换到安装它的进程,并发送某种消息给该进程以执行钩子回调来工作。但有一点让我困惑,为什么我的程序永远不会打印“msg recvd”,但在单击右键时会打印“right mouse down / up”。这使我得出结论,我的MouseHookProcPeekMessage调用期间被调用。它只是一些特殊消息,并且PeekMessage返回0。但我仍然需要调用PeekMessage或其等效物。

由于我的程序需要执行很多任务,我显然不能通过调用需要50毫秒才能返回的另一个函数来降低消息泵送循环(即调用PeekMessage)的效率。如何将我的程序多线程化以在同时进行繁重任务时保持鼠标的响应性?在多线程win32程序中,仍然只有一个消息队列,对吗?

更新:阅读了MS的文档后,我想我知道该怎么做了。我只需在我的应用程序中生成一个线程来调用SetWindowsHookEx以注册鼠标钩子,然后坐在自己的消息循环中,系统将负责将鼠标更新发送到此线程。它将可以在MouseHookProc中自由地执行任何操作,而我的应用程序的其余部分将独立运行。


你是否在DLL中有MouseHookProc,并且在EXE中有SetWindowsHookExPeekMessage循环? - Oleg
我想避免注入DLL。对于我的需求来说,系统切换到我的程序调用其“MouseHookProc”似乎更合适,因此我想使用低级别的鼠标钩子。据我所知,如果我想让普通鼠标钩子成为全局钩子,则必须使用DLL方法。 - Steven Lu
4个回答

14

问题出在你的消息循环上,因为你使用了PeekMessage()导致CPU占用率达到100%。即使你不轮询消息,Windows也知道如何保持钩子的活性,使用GetMessage()来解决问题。在这里使用Sleep(1)也可以解决问题,但不是必要的。

为什么必须将SetWindowsHookEx与Windows消息队列一起使用


6
我问你是否将MouseHookProc放在了DLL中,因为尝试将其放在EXE内部是一个典型的错误。我也很多年前犯过这个错误。
首先,你可以在http://msdn.microsoft.com/en-us/library/ms644990.aspx中读到:

SetWindowsHookEx可用于将DLL注入到另一个进程中。32位DLL无法注入到64位进程中,64位DLL无法注入到32位进程中。如果应用程序需要在其他进程中使用钩子,则需要32位应用程序调用SetWindowsHookEx将32位DLL注入到32位进程中,并且64位应用程序调用SetWindowsHookEx将64位DLL注入到64位进程中。32位和64位DLL必须具有不同的名称。

所以你必须放在一个DLL中。确切地说,如果你想支持32位和64位平台,你必须实现两个DLL:一个32位和一个64位的DLL。但为什么?SetWindowsHookEx是如何工作的呢?
如果你在EXE中执行以下代码:
HINSTANCE hinstDLL = LoadLibrary(TEXT("c:\\myapp\\syshook.dll"));
HOOKPROC hkprcMouse = (HOOKPROC)GetProcAddress(hinstDLL, "MouseHookProc");
HHOOK hhookMouse = SetWindowsHookEx( 
                    WH_MOUSE_LL,
                    hkprcMouse,
                    hinstDLL,
                    0); 

您想要在同一Windows站点的所有其他进程中注入您的syshook.dll,并向user32.dll发出请求。 (dll不会被注入到通过快速用户切换登录的其他用户的服务和进程中)。然后,user32.dll调用LoadLibrary到不同的进程中的syshook.dll。如果调用MouseHookProc函数,则将在处理鼠标消息的进程的上下文中调用它。如果该进程不是控制台应用程序,则执行以下代码:

printf("right mouse down\n");

无法工作。

因此,我希望现在你能理解为什么必须将MouseHookProc放置在DLL中。


2
我认为这里可能存在一些混淆。目前为止,我已经从这个主题中获得了我需要的大部分内容:https://dev59.com/NUvSa4cB1Zd3GeqPdU97感谢你抽出时间来帮助! - Steven Lu
2
这是非常有趣的信息!WH_MOUSE_LL是一般DLL注入规则的一个例外,这非常有趣。谢谢!我建议您使用http://msdn.microsoft.com/en-us/library/ms644990.aspx上的“向Microsoft发送有关此主题的评论”来建议微软根据http://msdn.microsoft.com/en-us/library/ms644986.aspx中的信息稍微更改`SetWindowsHookEx`的文档。 - Oleg
所以你的意思是使用 WH_MOUSE_LL 可以无需位于 DLL 中运行,但对于其他消息类型则必须在 DLL 中运行? - rogerdpack
2
@rogerdpack 是的。在这里的备注中(http://msdn.microsoft.com/en-us/library/ms644986.aspx),您可以阅读到:“但是,WH_MOUSE_LL钩子不会被注入到另一个进程中。相反,上下文切换回安装钩子的进程,并在其原始上下文中调用它。然后上下文切换回生成事件的应用程序。”所有其他钩子都必须将`HookProc`的代码注入(加载)到目标进程中。它只能与像我在答案中描述的DLL一起使用。 - Oleg
在我仍然拥有的旧Windows参考文献中,我没有看到WH_MOUSE_LL常量,因此我认为从当前应用程序的上下文中进行挂钩并未被发明... 我认为它已经包含在新的SDK中。我是对的吗?因为我六年前写了一个全局键盘钩子。那时我没有遇到过WH_MOUSE_LL ...?(我不是常规的win32程序员,而且WH_MOUSE_LL在全球范围内工作) - Jayapal Chandran
请参阅https://dev59.com/MmXWa4cB1Zd3GeqPMFo7,有趣的是,如果您删除对CallNextHookEx的调用并将其作为exe运行,则可以有效地“关闭”鼠标移动,因此显然它具有全局效果,并且WH_MOUSE_LL确实与WH_MOUSE不同。 - rogerdpack

3

不要这样做:

if (PeekMessage(&msg,0,0,0,PM_REMOVE)) {
    printf("msg recvd\n");
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}
Sleep(50);

将其切换为:

while (PeekMessage(&msg,0,0,0,PM_REMOVE)) {
    // Add this potentially...
    if (msg.message == WM_QUIT)
        break;
    printf("msg recvd\n");
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}
Sleep(10);

这将使你的应用程序继续处理队列中的所有消息,直到它为空(就像没有睡眠一样),然后在应用程序“空闲”时释放一些CPU时间。


3
MouseHookProc 应该放在 dll 中,否则无法捕捉“全局”输入(http://msdn.microsoft.com/en-us/library/ms997537.aspx)。
关于循环 - 您可以像这样进行修改:
while(true) {
  MSG msg;
  while (PeekMessage(&msg,0,0,0,PM_REMOVE)) {
    printf("msg recvd\n");
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
#ifdef TEST
  DoStuff();
  Sleep(50);
#endif
}

4
低级别的鼠标钩子不需要 DLL,它不像 WH_MOUSE 那样是一个全局钩子。 - Hans Passant

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