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

Cannot exit message loop from thread using Windows API and C++

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

要求

编写一个 C++ 程序来捕获 Windows OS 上的所有键盘输入。程序应该开始捕获击键,大约 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;
}

实施细节

主要要求是在一定时间内捕获击键。在那之后,我们不应该退出主程序。我认为在这种情况下适合的是创建一个单独的线程,负责捕获过程并使用事件向线程发出信号。为了更接近目标平台,我使用了 windows 个线程,而不是 c++0x 线程。

主函数首先创建事件,然后创建负责捕获击键的线程。为了满足时间要求,我能想到的最懒惰的实现是将主线程停止一定时间,然后发出辅助线程退出的信号。之后我们清理处理程序并继续进行任何所需的计算。

在辅助线程中,我们首先创建一个低级全局键盘挂钩。回调是一个 lambda 函数,它负责捕获实际的击键。我们还想调用 CallNextHookEx,这样我们就可以将消息提升到链上的下一个钩子,并且不会正确地中断来自 运行 的任何其他程序。挂钩初始化后,我们使用 Windows API 提供的 GetMessage 函数消费任何全局消息。重复此过程,直到发出停止线程的信号。在退出线程之前,我们解开回调。

我们还在整个程序执行过程中输出某些调试消息。

预期行为

运行 上面的代码,应该输出类似于下面的消息:

start capturing keystrokes
key was pressed 
key was pressed 
key was pressed 
key was pressed 
signal thread to terminate gracefully
thread is about to exit
from this point onwards, we should not capture any keystrokes
exit main thread

您的输出可能会有所不同,具体取决于捕获的击键次数。

实际行为

这是我得到的输出:

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

乍一看输出显示:

我从消息队列中读取消息的方式有问题,但经过几个小时的不同方法,我找不到任何具体实现的解决方案。我处理终止信号的方式也可能有问题。

备注

如有任何问题,请随时发表评论!预先感谢您花时间阅读这个问题并可能 post 一个答案(这将是惊人的!)。

我认为这是你的问题:

  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 只有 returns posted消息循环。如果找到已发送的消息,它会从 GetMessage 内部调用处理程序,并继续寻找 posted 消息。由于您还没有创建任何可以接收消息的 windows,并且您不会调用 PostThreadMessage1GetMessage 永远不会 returns.

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 你有 post WM_QUIT 的逻辑,但它的条件是在低级键盘钩子中接收 WM_DESTROY , WM_DESTROY 不是键盘消息。某些挂钩类型可以看到 WM_DESTROYWH_KEYBOARD_LL 不能。

What I thought would be suitable in this case, is to create a separate thread that will be responsible for the capturing procedure

如果另一个线程一直等待这个线程而一直无事可做,则没有必要这样做

你可以使用这样的代码。

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 和单独的消息循环。如果你的程序在这之前和之后无论如何 运行 消息循环 - 可能所有这些都在公共消息循环中完成,