使用消息和简单的 CBT 挂钩来操作窗口






4.68/5 (30投票s)
演示了使用窗口消息和挂钩的技术,使我们能够自动化 Windows 属性对话框甚至自定义应用程序。
引言
Windows 本质上是一个消息驱动的操作系统,因为大多数操作都是对发送到应用程序主窗口过程的消息的响应。无论是按下键盘、移动鼠标还是拖动窗口,应用程序都会通过其消息队列接收消息并做出相应的反应。现在,这产生了一个开发人员可以利用的非常有趣的推论;通过按正确的顺序将正确的消息发送到窗口或其子窗口,我们可以实际模拟应用程序中的人类操作。这在各种场景中都有其用途。显而易见,首先想到的是自动化任务的能力,例如在 Word 中打开文档,将整个文本左对齐并打印出来。
但对我来说,这项技术更有趣的用法是利用 Windows 用户界面快速完成任务,而这些任务可能需要大量的编程调用和对未文档化信息的访问。这包括更改各种系统属性、通过控制面板小程序进行更改,甚至更改桌面的显示属性。在本文中,我将随机选择一个此类场景(一个测试场景),并查看如何使用一些简单的 Windows 技术(如向窗口发送消息、枚举子窗口和基本的 CBT 挂钩)来自动化此任务。
测试场景
我将使用 Windows XP Professional 作为我的测试平台,因此我的示例场景仅在 XP 上有意义。其他操作系统的用户可能需要对我的示例代码片段进行适当的更改才能获得与本文相同的结果。
默认情况下,XP 操作系统不会显示菜单的键盘导航快捷方式(这让许多用户在首次遇到 Windows 2000 中的此功能时感到困惑和恼火)。我已放弃 Windows 2000,不记得在 2000 中是否有记录在案的更改此设置的方法,除了编辑注册表或使用某些调整应用程序,但在 XP 中,可以通过“显示属性”控制面板小程序轻松更改此设置。您需要做的就是从“显示属性”对话框中选择“外观”选项卡,打开“效果”子窗口,然后取消选中显示“除非我按下 Alt 键,否则隐藏用于键盘导航的下划线字母”的复选框。现在我完全确定,通过修改微不足道的注册表项很可能可以更改此设置;但为了这个测试场景和本文,让我们假设我们不知道如何以编程方式实现这一点。
手动方法
让我们看看如果我们坐在机器前手动完成,我们会怎么做。我们可能会遵循以下步骤(或非常相似的步骤):-
- 右键单击桌面并打开“显示属性”控制面板小程序
- 选择“外观”选项卡
- 通过单击“效果”按钮打开“效果”子窗口
- 根据我们要执行的操作,勾选/取消勾选相应的复选框
- 单击“确定”关闭“效果”子窗口
- 单击“确定”关闭“显示属性”并全局应用我们的更改
代码中的解决方案
现在我们需要决定如何通过代码实现这一系列事件。
- 打开“显示属性”窗口并选择“外观”选项卡可以一步完成,因为我们知道“显示属性”控制面板小程序名为 desk.cpl ,并且它接受命令行参数,可以使用这些参数来指定默认显示的选项卡。事实上,我们需要像这样调用它:-
control.exe desk.cpl Display,@Appearance
Control.exe 用于打开传递给它的第一个参数的控制面板小程序,额外的参数用于强制它以选定的“外观”选项卡启动。
- 现在我们需要枚举“外观”选项卡上的子窗口(控件),直到找到“效果”按钮,这可以通过
EnumChildWindows
实现。一旦我们获得“效果”按钮的句柄,我们就可以向它发送一个按钮单击消息,并打开“效果”子窗口。 - 要定位“效果”子窗口上的所需复选框,我们首先需要获取刚刚弹出的子窗口的句柄。我们通过设置一个全局 CBT 挂钩来实现这一点(这意味着我们需要将所有代码放入 DLL),并监视所有新激活的窗口。我们知道“效果”子窗口的标题文本,因此我们在窗口激活时获取其句柄。现在我们执行与之前相同的操作,即使用
EnumChildWindows
获取复选框的句柄,然后向其发送一个按钮单击消息。 - 我们向“效果”子窗口发送一个
WM_COMMAND
消息,命令 ID 为IDOK
,这相当于通过单击 OK 按钮关闭窗口。 - 我们对主“显示属性”窗口也执行相同的操作。
实现细节
打开“显示属性”窗口
BOOL BringUpDisplayAppearance() { return reinterpret_cast<int>(ShellExecute(GetDesktopWindow(), "open","control.exe","desk.cpl Display,@Appearance", "",SW_SHOW )) > 32 ? TRUE : FALSE; }
代码非常简单明了,我们只需使用 ShellExecute
来打开“显示属性”小程序窗口,并将默认选项卡设置为“外观”选项卡。我在这里使用了 SW_SHOW
,因为使用 SW_HIDE
对显示属性窗口无效(我相信 control.exe 程序或 desk.cpl 本身稍后会在代码中的某处调用 ShowWindow(hWnd, SW_SHOW)
)。我们在挂钩过程中隐藏窗口(但这也不是完全有效的,屏幕上仍会有一闪而过),但我们的目标并不是真正向最终用户隐藏我们的操作,而是尽可能使事情更加清晰,这可以通过将窗口可见的时间减少到几毫秒来实现。
获取“效果”按钮的句柄
HWND GetEffectsButton(HWND hWndParent) { HWND hWnd = NULL; EnumChildWindows(hWndParent, EnumAppearanceChildProc, (LPARAM)&hWnd); return hWnd; } BOOL CALLBACK EnumAppearanceChildProc(HWND hwnd, LPARAM lParam) { TCHAR buff[512]; GetWindowText(hwnd,buff,512); if(_tcscmp(buff,_T("&Effects...")) == 0) { *reinterpret_cast<HWND*>(lParam) = hwnd; return FALSE; } return TRUE; }
使用 Spy++,我们提取与“效果”按钮完全相关的文本,该文本恰好是“&效果...”,我们利用此知识重复比较每个子控件的文本,直到获得我们想要的按钮控件。
获取复选框句柄
HWND GetMenuUnderlineCheck(HWND hWndParent) { HWND hWnd = NULL; EnumChildWindows(hWndParent, EnumEffectsChildProc, (LPARAM)&hWnd); return hWnd; } BOOL CALLBACK EnumEffectsChildProc(HWND hwnd, LPARAM lParam) { TCHAR buff[512]; GetWindowText(hwnd,buff,512); if(_tcsstr(buff,_T("&Hide underlined"))) { *reinterpret_cast<HWND*>(lParam) = hwnd; return FALSE; } return TRUE; }
这与我们获取“效果”按钮句柄的方式非常相似。
CBT 挂钩过程
LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam) { if(nCode == HCBT_ACTIVATE) { HWND hWnd = (HWND) wParam; TCHAR buff[512]; GetWindowText(hWnd,buff,512); if(_tcscmp(buff,_T("Effects")) == 0) { ShowWindow(hWnd,SW_HIDE); g_hWndEffects = hWnd; UnhookWindowsHookEx(g_hook); } if(_tcscmp(buff,_T("Display Properties")) == 0) { ShowWindow(hWnd,SW_HIDE); } } return 0; }
HCBT_ACTIVATE
代码表示一个窗口即将被激活。我们将此窗口的标题文本与“效果”进行比较,如果匹配,则表示我们找到了要查找的窗口。如果您想知道为什么我们必须安装 CBT 挂钩而不是使用 FindWindow
和标题文本;这是为了确保即使已经存在一个具有相同标题文本的窗口,它也不会干扰我们的搜索,因为我们只检查新激活的窗口。我们在代码的后期安装 CBT 挂钩,并在获得想要的窗口时立即卸载它。挂钩的活动时间是从我们打开“显示属性”窗口到“效果”子窗口即将被激活为止,在大多数情况下,这不应超过几毫秒。
我们还使用挂钩过程来隐藏弹出的窗口,包括主“显示属性”窗口和“效果”子窗口。微小的闪烁仍然存在,如果有人有任何关于如何进一步减少这种闪烁的想法,欢迎提出建议。
主函数(导出)
DEMODLL_API BOOL ToggleMenuUnderline(void) { HWND hWndAppearance = NULL; BOOL ret = TRUE; g_hook = SetWindowsHookEx(WH_CBT, CBTProc, g_hModule, 0); ret = BringUpDisplayAppearance(); Sleep(500);//Wait for the window to come up if(ret) { hWndAppearance = FindWindow(NULL, _T("Display Properties")); ret = hWndAppearance != NULL; if(ret) { HWND hWndEffectsButton = GetEffectsButton( hWndAppearance); ret = hWndEffectsButton != NULL; if(ret) { PostMessage(hWndEffectsButton,BM_CLICK,0,0); //Wait for the Effects window to come up while(!IsWindow(g_hWndEffects)) Sleep(100); HWND hWndCheck = GetMenuUnderlineCheck( g_hWndEffects); ret = hWndCheck != NULL; if(ret) { PostMessage(hWndCheck,BM_CLICK,0,0); PostMessage(g_hWndEffects,WM_COMMAND, IDOK,NULL); PostMessage(hWndAppearance,WM_COMMAND, IDOK,NULL); //Wait for the window to be dismissed while(IsWindow(hWndAppearance)) Sleep(100); } } } } //Final checks in case of error conditions if(IsWindow(g_hWndEffects)) PostMessage(g_hWndEffects,WM_CLOSE,0,0); if(IsWindow(hWndAppearance)) PostMessage(hWndAppearance,WM_CLOSE,0,0); return ret; }
我们首先设置我们的 CBT 挂钩,然后调用 BringUpDisplayAppearance
函数来打开“显示属性”窗口并选择“外观”选项卡。窗口打开后,我们使用 GetEffectsButton
函数获取“效果”按钮的句柄,然后使用我们刚刚获得的句柄向“效果”按钮发送一个 BM_CLICK
消息。几乎立即,“效果”子窗口弹出,我们通过 CBT 挂钩过程获取其句柄,该过程在不再需要时还会卸载挂钩。我们使用以下 while 循环等待“效果”窗口出现:-
while(!IsWindow(g_hWndEffects)) Sleep(100);
这样可以避免睡眠时间过长或过短。现在我们使用 GetMenuUnderlineCheck
函数获取复选框的句柄,并向复选框发送一个 BM_CLICK
消息,该消息有效地切换其状态,这正是我们试图做的。现在,我们只需向“效果”子窗口和“显示属性”主窗口发送 WM_COMMAND
消息,其中 wParam
设置为 IDOK
。就这样;我们现在已成功切换了“为菜单隐藏用于键盘导航的下划线快捷方式”的系统范围属性的状态。
结论
我们考虑的测试场景可能过于简单,无法揭示此技术的真正强大之处,但当您考虑到现在可以从程序中完成用户手动使用 Windows GUI 可以完成的任何事情时,您会逐渐对其强大可能性感到印象深刻。 您可以使用此技术枚举 Windows 主题、更改当前主题、更改显示设置、更改系统设置、自动化您自己的应用程序等。除了 Everett 之外,您只需要 Spy++ 或类似的应用程序。祝您的基于消息的 Windows 自动化尝试好运。
历史
- 2003 年 8 月 8 日 - 初稿