无法使用Windows API和C++退出线程的消息循环

无法使用Windows API和C++退出线程的消息循环,c++,windows,multithreading,winapi,message-queue,C++,Windows,Multithreading,Winapi,Message Queue,我正在尝试实现以下场景: 要求 < >编写一个C++程序,捕获Windows操作系统上所有的键盘输入。程序应开始捕捉击键,大约3秒后,具体时间量不是很相关,可能是4/5/等,程序应停止捕捉击键并继续执行 在开始实际的实现细节之前,我想澄清一下,我更喜欢以练习的形式编写需求,而不是提供一个冗长的描述。我不是在为家庭作业收集答案。如果问题处理得当,我实际上非常支持这些问题,但这里的情况并非如此 我的解决方案 在过去的几天里,我们对不同的实现进行了研究,以下是迄今为止最完整的实现: #include

我正在尝试实现以下场景:

要求 < >编写一个C++程序,捕获Windows操作系统上所有的键盘输入。程序应开始捕捉击键,大约3秒后,具体时间量不是很相关,可能是4/5/等,程序应停止捕捉击键并继续执行

在开始实际的实现细节之前,我想澄清一下,我更喜欢以练习的形式编写需求,而不是提供一个冗长的描述。我不是在为家庭作业收集答案。如果问题处理得当,我实际上非常支持这些问题,但这里的情况并非如此

我的解决方案 在过去的几天里,我们对不同的实现进行了研究,以下是迄今为止最完整的实现:

#include <iostream>
#include <chrono>
#include <windows.h>
#include <thread>

// Event, used to signal our thread to stop executing.
HANDLE ghStopEvent;

HHOOK keyboardHook;

DWORD StaticThreadStart(void *)
{
  // Install low-level keyboard hook
  keyboardHook = SetWindowsHookEx(
      // monitor for keyboard input events about to be posted in a thread input queue.
      WH_KEYBOARD_LL,

      // Callback function.
      [](int nCode, WPARAM wparam, LPARAM lparam) -> LRESULT {
        KBDLLHOOKSTRUCT *kbs = (KBDLLHOOKSTRUCT *)lparam;

        if (wparam == WM_KEYDOWN || wparam == WM_SYSKEYDOWN)
        {
          // -- PRINT 2 --
          // print a message every time a key is pressed.
          std::cout << "key was pressed " << std::endl;
        }
        else if (wparam == WM_DESTROY)
        {
          // return from message queue???
          PostQuitMessage(0);
        }

        // Passes the keystrokes
        // hook information to the next hook procedure in the current hook chain.
        // That way we do not consume the input and prevent other threads from accessing it.
        return CallNextHookEx(keyboardHook, nCode, wparam, lparam);
      },

      // install as global hook
      GetModuleHandle(NULL), 0);

  MSG msg;
  // While thread was not signaled to temirnate...
  while (WaitForSingleObject(ghStopEvent, 1) == WAIT_TIMEOUT)
  {
    // Retrieve the current messaged from message queue.
    GetMessage(&msg, NULL, 0, 0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  // Before exit the thread, remove the installed hook.
  UnhookWindowsHookEx(keyboardHook);

  // -- PRINT 3 --
  std::cout << "thread is about to exit" << std::endl;

  return 0;
}

int main(void)
{
  // Create a signal event, used to terminate the thread responsible
  // for captuting keyboard inputs.
  ghStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

  DWORD ThreadID;
  HANDLE hThreadArray[1];

  // -- PRINT 1 --
  std::cout << "start capturing keystrokes" << std::endl;

  // Create a thread to capture keystrokes.
  hThreadArray[0] = CreateThread(
      NULL,              // default security attributes
      0,                 // use default stack size
      StaticThreadStart, // thread function name
      NULL,              // argument to thread function
      0,                 // use default creation flags
      &ThreadID);        // returns the thread identifier

  // Stop main thread for 3 seconds.
  std::this_thread::sleep_for(std::chrono::milliseconds(3000));

  // -- PRINT 4 --
  std::cout << "signal thread to terminate gracefully" << std::endl;

  // Stop gathering keystrokes after 3 seconds.
  SetEvent(ghStopEvent);

  // -- PRINT 5 --
  std::cout << "from this point onwards, we should not capture any keystrokes" << std::endl;

  // Waits until one or all of the specified objects are
  // in the signaled state or the time-out interval elapses.
  WaitForMultipleObjects(1, hThreadArray, TRUE, INFINITE);

  // Closes the open objects handle.
  CloseHandle(hThreadArray[0]);
  CloseHandle(ghStopEvent);

  // ---
  // DO OTHER CALCULATIONS
  // ---

  // -- PRINT 6 --
  std::cout << "exit main thread" << std::endl;

  return 0;
}
根据捕获的击键次数,您的输出可能会有所不同

实际行为 这是我得到的输出:

start capturing keystrokes
key was pressed 
key was pressed 
key was pressed 
key was pressed 
signal thread to terminate gracefully
from this point onwards, we should not capture any keystrokes
key was pressed 
key was pressed
key was pressed
对输出的第一眼观察显示:

未调用unhook函数 程序不断捕获击键,这可能表明我处理消息队列的方式有问题 我从消息队列中读取消息的方式有问题,但经过数小时的不同方法之后,我找不到任何具体实现的解决方案。我处理终止信号的方式也可能有问题

笔记 在这里,我越接近于找到一个答案,这就是问题。然而,这个解决方案并没有给我带来我想要的帮助。 所提供的实现是一个最小的可复制示例,无需导入任何外部库即可进行编译。 一个建议的解决方案是将捕获击键功能作为一个单独的子进程来实现,在这个子进程中,我们可以随时启动和停止击键。然而,我更感兴趣的是找到一个使用线程的解决方案。我甚至不确定这是否有可能。 上述代码不包含任何错误处理。这是为了防止代码可能过度膨胀。
如果您有任何疑问,请随时发表评论!提前感谢您抽出时间阅读这个问题,并可能发布一个答案,这将是惊人的

我想这是你的问题:

  while (WaitForSingleObject(ghStopEvent, 1) == WAIT_TIMEOUT)
  {
    // Retrieve the current messaged from message queue.
    GetMessage(&msg, NULL, 0, 0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
原因是当前您的循环可能会永远停留在GetMessage步骤上,而不再查看手动重置事件

修复方法只是将WaitForSingleObject+GetMessage组合替换为MsgWaitForMultipleObjects+PeekMessage

您犯此错误的原因是您不知道GetMessage只会将已发布的消息返回到消息循环。如果它找到已发送的消息,它将从GetMessage内部调用处理程序,并继续查找已发布的消息。由于您尚未创建任何可以接收消息的窗口,并且您没有调用PostThreadMessage1,因此GetMessage永远不会返回

while (MsgWaitForMultipleObjects(1, &ghStopEvent, FALSE, INFINITE, QS_ALLINPUT) > WAIT_OBJECT_0) {
   // check if there's a posted message
   // sent messages will be processed internally by PeekMessage and return false
   if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
}

1您有逻辑发布WM_QUIT,但前提是在低级键盘挂钩中接收WM_DESTROY,并且WM_DESTROY不是键盘消息。某些钩子类型可以看到WM_销毁,但键盘无法看到。

我认为这是您的问题:

  while (WaitForSingleObject(ghStopEvent, 1) == WAIT_TIMEOUT)
  {
    // Retrieve the current messaged from message queue.
    GetMessage(&msg, NULL, 0, 0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
原因是当前您的循环可能会永远停留在GetMessage步骤上,而不再查看手动重置事件

修复方法只是将WaitForSingleObject+GetMessage组合替换为MsgWaitForMultipleObjects+PeekMessage

您犯此错误的原因是您不知道GetMessage只会将已发布的消息返回到消息循环。如果它找到已发送的消息,它将从GetMessage内部调用处理程序,并继续查找已发布的消息。由于您尚未创建任何可以接收消息的窗口,并且您没有调用PostThreadMessage1,因此GetMessage永远不会返回

while (MsgWaitForMultipleObjects(1, &ghStopEvent, FALSE, INFINITE, QS_ALLINPUT) > WAIT_OBJECT_0) {
   // check if there's a posted message
   // sent messages will be processed internally by PeekMessage and return false
   if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
}
1您有逻辑发布WM_QUIT,但前提是在低级键盘挂钩中接收WM_DESTROY,并且WM_DESTROY不是键盘消息。一些钩子类型可以看到WM_破坏,但键盘不能

我认为在这种情况下,创建一个单独的 负责捕获过程的线程

如果另一个线程一直在等待这个线程而什么也不做,那么就没有必要这样做

您可以使用这样的代码

LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    if (HC_ACTION == code)
    {
        PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

        DbgPrint("%x %x %x %x\n", wParam, p->scanCode, p->vkCode, p->flags);
    }

    return CallNextHookEx(0, code, wParam, lParam);
}

void DoCapture(DWORD dwMilliseconds)
{
    if (HHOOK hhk = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, 0, 0))
    {
        ULONG time, endTime = GetTickCount() + dwMilliseconds;

        while ((time = GetTickCount()) < endTime)
        {
            MSG msg;
            switch (MsgWaitForMultipleObjectsEx(0, 0, endTime - time, QS_ALLINPUT, MWMO_INPUTAVAILABLE))
            {
            case WAIT_OBJECT_0:
                while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
                {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                break;

            case WAIT_FAILED:
                __debugbreak();
                goto __0;
                break;

            case WAIT_TIMEOUT:
                DbgPrint("WAIT_TIMEOUT\n");
                goto __0;
                break;
            }
        }
__0:
        UnhookWindowsHookEx(hhk);
    }
}
在实际代码中,通常不需要编写带有单独消息循环的单独文档。如果您的程序在此之前和之后运行消息循环,那么所有这些都可以在公共消息循环中执行

我认为在这种情况下,创建一个单独的 负责捕获过程的线程

如果另一个线程一直在等待这个线程而什么也不做,那么就没有必要这样做

您可以使用这样的代码

LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    if (HC_ACTION == code)
    {
        PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

        DbgPrint("%x %x %x %x\n", wParam, p->scanCode, p->vkCode, p->flags);
    }

    return CallNextHookEx(0, code, wParam, lParam);
}

void DoCapture(DWORD dwMilliseconds)
{
    if (HHOOK hhk = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, 0, 0))
    {
        ULONG time, endTime = GetTickCount() + dwMilliseconds;

        while ((time = GetTickCount()) < endTime)
        {
            MSG msg;
            switch (MsgWaitForMultipleObjectsEx(0, 0, endTime - time, QS_ALLINPUT, MWMO_INPUTAVAILABLE))
            {
            case WAIT_OBJECT_0:
                while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
                {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                break;

            case WAIT_FAILED:
                __debugbreak();
                goto __0;
                break;

            case WAIT_TIMEOUT:
                DbgPrint("WAIT_TIMEOUT\n");
                goto __0;
                break;
            }
        }
__0:
        UnhookWindowsHookEx(hhk);
    }
}
也是真实的

代码-通常不需要使用单独的消息循环编写单独的DoCapture。如果您的程序在此之前和之后运行消息循环,那么所有这些都可以在公共消息循环中执行,

提出了一个好问题。我向您致敬。将一些调试输出放在您当前有注释的位置//在退出线程之前,请删除已安装的挂钩。@BenVoigt您对哪种调试输出感兴趣?只是一个静态的或一些特殊的。我还想指出,即使我没有做任何错误处理,程序也能正确运行。我只是为了防止你在这里创建的专用线程的代码过多?@RbMm我想根据我的程序开始和结束捕获功能。这就是为什么我认为最好把它放在另一个线程中。我检查了你的答案和你介绍的不同观点。提出的问题很好。我向您致敬。将一些调试输出放在您当前有注释的位置//在退出线程之前,请删除已安装的挂钩。@BenVoigt您对哪种调试输出感兴趣?只是一个静态的或一些特殊的。我还想指出,即使我没有做任何错误处理,程序也能正确运行。我只是为了防止你在这里创建的专用线程的代码过多?@RbMm我想根据我的程序开始和结束捕获功能。这就是为什么我认为最好把它放在另一个线程中。我检查了你的答案和你介绍的不同观点。当你发布代码片段时,我正准备编辑我的答案。这个解决方案似乎是正确的。在接下来的几个小时里,我将进行更多的测试,如果发生任何异常情况,我将返回给您。无论如何,我要非常感谢你。我现在处理这个问题已经两天了,我对Windows API还不太适应。我接受这个解决方案。你添加的任何评论和参考对我也很有帮助。@GeorgeGkasdrogkas:我强烈推荐陈雷蒙的博客《旧事新事》。例如,本文非常相关:后续内容也非常相关:然而,MsgWaitFormulatPleObjects应该比WaitForSingleObject+WaitMessage+PeekMessage更有效。PeekMessage仅当MsgWaitForMultipleObjects返回WAIT\u OBJECT\u 0+nCount时才存在检测调用-所以具体来说,WAIT\u OBJECT\u 0+1case@RmMm:是的,我编写了条件as循环,虽然它不是stop事件,但当它是messagewaiting status时,您也可以将其表示为loop。当您发布代码片段时,我正准备编辑我的答案。这个解决方案似乎是正确的。在接下来的几个小时里,我将进行更多的测试,如果发生任何异常情况,我将返回给您。无论如何,我要非常感谢你。我现在处理这个问题已经两天了,我对Windows API还不太适应。我接受这个解决方案。你添加的任何评论和参考对我也很有帮助。@GeorgeGkasdrogkas:我强烈推荐陈雷蒙的博客《旧事新事》。例如,本文非常相关:后续内容也非常相关:然而,MsgWaitFormulatPleObjects应该比WaitForSingleObject+WaitMessage+PeekMessage更有效。PeekMessage仅当MsgWaitForMultipleObjects返回WAIT\u OBJECT\u 0+nCount时才存在检测调用-所以具体来说,WAIT\u OBJECT\u 0+1case@RmMm:是的,我编写了条件as循环,虽然它不是stop事件,但您也可以将其表示为loop,因为它是message waiting status,谢谢您的回答@RbMm。的确,在现实世界中,在大多数情况下,整个应用程序都存在一个消息循环。我发布这个问题的方式是为了能够提供一个最小的可复制示例。@geogegkasdrogkas-是的,在现实世界中,我假设您基于某个事件调用setWindowshookxw表单UI必须是单个公共消息循环。基于MsgWaitForMultipleObjectsEx。不需要创建单独的线程-这是100%。和单独的消息loops@GeorgeGkasdrogkas-许多解决方案都是可能的。例如,您可以在调用setWindowsHookXW后创建计时器,在WM_timer上调用unhookwindowshookX并取消计时器。。很多方法都存在,非常感谢您的回答@RbMm。的确,在现实世界中,在大多数情况下,整个应用程序都存在一个消息循环。我发布这个问题的方式是为了能够提供一个最小的可复制示例。@geogegkasdrogkas-是的,在现实世界中,我假设您基于某个事件调用setWindowshookxw表单UI必须是单个公共消息循环。基于MsgWaitForMultipleObjectsEx。不需要创建单独的线程-这是100%。和单独的消息loops@GeorgeGkasdrogkas-许多解决方案都是可能的。例如,您可以在调用setWindowsHookXW后创建计时器,在WM_timer上调用unhookwindowshookX并取消计时器。。真的有很多方法存在