无法从使用 Windows API 和 C++ 要求我的解决方案实施细节预期行为实际行为注意事项

问题描述

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

要求

编写一个 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);
    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,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 线程。

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

在辅助线程中,我们首先创建一个低级全局键盘钩子。回调是一个 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

乍一看输出结果:

  • 调用 unhook 函数
  • 程序不断捕获击键,这可能表明我处理消息队列的方式有问题

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

注意事项

  • 在 SO 中,我越接近找到答案,就是 this 问题。然而,该解决方案并没有像我想要的那样帮助我。
  • 所提供的实现是一个可重现的最小示例,无需导入任何外部库即可编译。
  • 建议的解决方案是将捕获击键功能实现为一个单独的子进程,我们可以在其中随时启动和停止。但是,我对使用线程找到解决方案更感兴趣。我不确定这是否可能(可能)。
  • 上面的代码不包含任何错误处理。这是为了防止可能的代码过度膨胀。

如有任何问题,请随时发表评论!提前感谢您抽出时间阅读此问题并可能发布答案(这会很棒!)。

解决方法

我认为这是你的问题:

  while (WaitForSingleObject(ghStopEvent,1) == WAIT_TIMEOUT)
  {
    // Retrieve the current messaged from message queue.
    GetMessage(&msg,NULL,0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

原因是目前您的循环可能永远停留在 GetMessage() 步骤上,并且永远不会再查看手动重置事件。

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

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

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,PM_REMOVE)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
}

1 您有发布 WM_QUIT 的逻辑,但它的条件是在低级键盘钩子中接收 WM_DESTROY,而 WM_DESTROY 不是键盘信息。某些钩子类型可以看到 WM_DESTROY,但 WH_KEYBOARD_LL 不能。

,

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

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

你可以使用这样的代码。

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,lParam);
}

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

        while ((time = GetTickCount()) < endTime)
        {
            MSG msg;
            switch (MsgWaitForMultipleObjectsEx(0,endTime - time,QS_ALLINPUT,MWMO_INPUTAVAILABLE))
            {
            case WAIT_OBJECT_0:
                while (PeekMessageW(&msg,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。如果您的程序在此之前和之后无论如何都运行消息循环 - 可能所有这些都在公共消息循环中进行,