在上一节《深度学习训练图片收集器——C++截图程序的实现1(需求分析篇)》中,我们分析了设计一个独立的截图程序的必要性。
在本节中,我们将给出VisualStudio C++的键鼠钩子设置代码,以使程序能够监听用户按下Alt+A组合键的事件,以及鼠标拖拽截图矩形的事件。
本程序的运行需要搭配DebugView工具以查看日志输出。DebugView是一个轻量级的日志查看工具,免安装,使用方便,可以在百度搜索下载源。
我使用的VS版本是VS2013。你也可以使用其它版本的VS。
现在开始演示实现步骤。
首先打开VS,点“新建项目...”,在弹出的界面中选择“MFC”→“MFC应用程序”,在名称中输入“ScreenshotForML”,如下图所示:
点击“确定”,再选“下一步”,在弹出的选项页中勾选“基于对话框”:
点击“下一步”,在弹出的界面中取消“关于框”,选中“最小化框”,再点击“下一步”,直至完成。
设置完成后,VS跳转到代码编辑器界面。我们需要在编辑器界面的菜单工具栏中将debug改为release,如下图:这时候按F7,或者点击菜单栏中的“生成---生成解决方案”,再打开硬盘的C:\ScreenshotForML\ScreenshotForML\Release目录,会看到生成的exe文件,打开exe,界面如下:
界面上有多余的按钮和静态文本。目前我们暂时不需要这些控件,可以进入VS资源视图,把这些按钮和文本删掉,再把整个对话框拉小一点,如下图所示:
按F7重新生成,启动后界面如下:
看起来干净清爽很多。当后面有需要时,我们会加入一些控件。
接下来我们需要加入用于安装键鼠钩子和监听键鼠事件的dll工程。
右键解决方案“ScreenshotForML”,选择“添加---新建项目”,在弹出窗口中选择“MFC---MFC DLL”,名称输入“kb_mouse_listener”,如下图所示:然后点击“确定”——“下一步”——“完成”。
现在可以看到解决方案视图中多出了一个项目kb_mouse_listener:双击kb_mouse_listener.def文件,在最后一行输入“Init_KeyboardListening”,如下图:
然后双击kb_mouse_listener.cpp文件,在最后面加上以下代码:
#define WM_START_SCREENSHOT WM_USER + 111
#define WM_MOUSE_LEFT_DOWN WM_USER + 113
#define WM_MOUSE_LEFT_UP WM_USER + 114
#define WM_MOUSE_MOVE WM_USER + 115
#define WM_MOUSE_DBCLICK WM_USER + 116
#define WM_ESC_PRESSED WM_USER + 117
#define DOUBLE_CLICK_INTERVAL 350 // 鼠标左键双击时间间隔阈值
HHOOK hHook_kb = NULL;
HHOOK hHook_mouse = NULL;
bool g_bIsComboKey_AltA_pressed = false; // 记录Alt+a组合键是否被按下
bool g_bStartDrawingRect = false; // 记录是否开始截图
bool g_bHasDrawnValidRect = false; // 记录是否已绘制有效矩形
int g_nLastMouseUpTime = 0;
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
MOUSEHOOKSTRUCT mouse_info = *((PMOUSEHOOKSTRUCT)lParam); // 将lParam指向的内容拷贝到mouse_info中
char msg[200] = "11111 ";
HWND hwnd = NULL;
switch (wParam)
{
case WM_LBUTTONDOWN:
if (g_bIsComboKey_AltA_pressed) {
g_bStartDrawingRect = true;
OutputDebugStringA("11111 鼠标左键被按下");
// 鼠标左键被按下,发送绘制截图区域的消息
hwnd = ::FindWindowA(NULL, "ScreenShot for Machine Learning (full screen)");
int ret = PostMessage(hwnd, WM_MOUSE_LEFT_DOWN, NULL, NULL);
}
break;
case WM_LBUTTONUP:
if (g_bStartDrawingRect) {
hwnd = ::FindWindowA(NULL, "ScreenShot for Machine Learning (full screen)");
// 如果两次左键弹起的时间间隔小于阈值,就视为发生了双击事件
int nClickInterval = GetTickCount() - g_nLastMouseUpTime;
if ((nClickInterval <= DOUBLE_CLICK_INTERVAL) && (g_bHasDrawnValidRect)) {
PostMessage(hwnd, WM_MOUSE_DBCLICK, NULL, NULL);
g_bIsComboKey_AltA_pressed = false;
g_bStartDrawingRect = false;
g_bHasDrawnValidRect = false;
g_nLastMouseUpTime = 0;
OutputDebugStringA("11111 发生了双击事件");
// 这里sleep 100毫秒是为了防止本次鼠标点击事件传递到截图之前的前景窗口,但后来加了return 1似乎就不会再传递
Sleep(100);
return 1;
}
g_nLastMouseUpTime = GetTickCount();
// 鼠标左键弹起,结束当前截图区域的绘制,截图区域的左上顶点和右下顶点由CScreenShotView类负责保存
g_bStartDrawingRect = false;
PostMessage(hwnd, WM_MOUSE_LEFT_UP, NULL, NULL);
}
break;
case WM_MOUSEMOVE:
if (g_bStartDrawingRect) {
g_bHasDrawnValidRect = true;
OutputDebugStringA("11111 鼠标移动");
// 截图区域随着鼠标移动而变化
hwnd = ::FindWindowA(NULL, "ScreenShot for Machine Learning (full screen)");
PostMessage(hwnd, WM_MOUSE_MOVE, NULL, NULL);
}
break;
default:
break;
}
return CallNextHookEx(0, nCode, wParam, lParam);
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szKey[2] = "";
char msg[200] = "11111 ";
int nProcID = 0;
HWND hwnd = NULL;
KBDLLHOOKSTRUCT *pkbhs = (KBDLLHOOKSTRUCT*)lParam;
// alt + a 组合键按下,代表开始截图
if ((pkbhs->vkCode == 65) && ((pkbhs->flags == 32) || (pkbhs->flags == 160))) {
if (!g_bIsComboKey_AltA_pressed) {
g_bIsComboKey_AltA_pressed = true;
OutputDebugStringA("11111 alt + a 组合键按下");
// 发送截图请求给主程序
hwnd = ::FindWindowA(NULL, "ScreenShot for Machine Learning");
PostMessage(hwnd, WM_START_SCREENSHOT, NULL, NULL);
}
}
// esc键按下,代表截图取消
if ((pkbhs->vkCode == 27) && (pkbhs->flags == 0)) {
if (g_bIsComboKey_AltA_pressed) {
g_bIsComboKey_AltA_pressed = false;
OutputDebugStringA("11111 esc键按下");
// 发送该事件给主程序
hwnd = ::FindWindowA(NULL, "ScreenShot for Machine Learning (full screen)");
PostMessage(hwnd, WM_ESC_PRESSED, NULL, NULL);
return 1;
}
}
return CallNextHookEx(0, nCode, wParam, lParam);
}
void Init_KeyboardListening()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
// 安装底层键盘钩子
hHook_kb = ::SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, GetModuleHandleA("mouse_kb_listener.dll"), 0);
hHook_mouse = ::SetWindowsHookEx(WH_MOUSE_LL, MouseProc, GetModuleHandleA("mouse_kb_listener.dll"), 0);
}
其中,各段代码的作用已经在注释中进行了说明。
代码的整体思路是先调用SetWindowsHookEx()函数安装低级键鼠钩子,设置回调函数,当发生键鼠事件时,回调函数KeyboardProc()和MouseProc()会被执行。在KeyboardProc()中,根据KBDLLHOOKSTRUCT结构体的vkCode和flags判断Alt+a组合键和esc键是否被按下。在MouseProc()中,根据wParam判断鼠标事件类型,监听鼠标的单击、双击、以及移动事件。
在kb_mouse_listener.cpp文件中添加以上代码后,按F7,VS会在C:\ScreenshotForML\ScreenshotForML\Release目录生成kb_mouse_listener项目的lib和dll文件,如下图:现在将kb_mouse_listener.lib文件拷贝到C:\ScreenshotForML\ScreenshotForML\ScreenshotForML目录下,以便ScreenshotForML项目可以调用kb_mouse_listener项目的函数Init_KeyboardListening()。
然后,右键ScreenshotForML项目,选择“属性”,在弹出的属性窗口中依次选择“链接器”→“输入”→“附加依赖项”,在附加依赖项中输入“kb_mouse_listener.lib”,如下图:点确定,再点确定。
然后打开ScreenshotForMLDlg.cpp文件,在头部的空白处加入以下引用声明:
_declspec (dllimport) void Init_KeyboardListening();
并在OnInitDialog()函数中加入以下代码:
// 注册键盘鼠标消息钩子
Init_KeyboardListening();
现在,按F7,重新生成exe。启动exe,程序开始进入监听键鼠事件的状态,这时候如果打开DebugView,并且按alt+A,再移动鼠标,会看到以下日志输出:
至此,我们实现了截图操作相关键盘鼠标事件的监听。下一节将介绍主程序ScreenshotForML如何响应键鼠事件进行截图操作,详见《深度学习训练图片收集器——C++截图程序的实现3(主对话框响应键鼠消息进行截图) 》。