【发布时间】:2021-07-05 19:00:35
【问题描述】:
我正在尝试实现以下场景:
要求
编写一个 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;
}
实现细节
主要要求是在一定时间内捕获击键。在那之后,我们不应该退出主程序。在这种情况下,我认为合适的是创建一个单独的线程,该线程将负责捕获过程并使用事件向线程发出信号。为了更接近目标平台,我使用了 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
第一眼看到输出就会发现:
- 未调用 unhook 函数
- 程序不断捕获击键,这可能表明我处理消息队列的方式有问题
我从消息队列中读取消息的方式有问题,但经过数小时的不同方法后,我找不到任何具体实现的解决方案。我处理终止信号的方式也可能有问题。
注意事项
- 在 SO 中,我越接近找到答案,就是 this 问题。但是,该解决方案对我的帮助并没有我想要的那么大。
- 提供的实现是一个最小的可重现示例,无需导入任何外部库即可编译。
- 建议的解决方案是将捕获击键功能作为一个单独的子进程来实现,我们可以随时在其中启动和停止。但是,我对使用线程找到解决方案更感兴趣。我不确定这是否可能(可能)。
- 以上代码不包含任何错误处理。这是为了防止可能的代码过度膨胀。
如果您有任何问题,请随时发表评论!提前感谢您抽出时间阅读此问题并可能发布答案(这将是惊人的!)。
【问题讨论】:
-
问得好。我向你致敬。
-
在你当前有注释的地方放一些调试输出 "// 在退出线程之前,删除已安装的钩子。"
-
@BenVoigt 您对哪种调试输出感兴趣?只是一个静态的或特定的东西。我还想指出,即使我没有进行任何错误处理,程序也可以正常运行。我只是提交它们以防止代码过度膨胀
-
你在这里创建什么专用线程?
-
@RbMm 我想根据我的程序启动和结束捕获功能。这就是为什么我认为最好把它放在另一个线程中。我检查了你的答案和你介绍的不同观点。
标签: c++ windows multithreading winapi message-queue