模拟按键 - keybd_event Send/PostMessage的一些思考

本文最后更新于:2022年3月23日 下午

前情提要

在软件开发,特别是辅助软件开发过程中,经常需要用到键盘按键模拟来实现自动化

比如,使用虚拟键码VK_VOLUME_UP & VK_VOLUME_DOWN来实现音量调节

或者,使用VK_SNAPSHOT实现截屏

这是一种可以避开系统API复杂操作的,非常讨巧的捷径

常用函数

keybd_event

为了实现按键模拟,我们常用Windows API - keybd_event:

1
2
3
4
5
6
void keybd_event(
[in] BYTE bVk, //一个虚拟键码。代码必须是 1 到 254 范围内的值
[in] BYTE bScan, //密钥的硬件扫描码
[in] DWORD dwFlags, //控制功能操作的各个方面: KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP
[in] ULONG_PTR dwExtraInfo //与击键关联的附加值
);

一般情况下,我们只需要使用bVK & dwFlags参数即可(复杂情况,如游戏窗口,需要加上扫描码)

一个击键的过程分为两个步骤:按下 & 抬起

所以,如果我们要模拟键盘上A键的击键:

1
2
keybd_event('A', 0, 0, 0); //第三个参数0代表KeyDown
keybd_event('A', 0, KEYEVENTF_KEYUP, 0);

或者是,回车键(ENTER)

1
2
keybd_event(VK_RETURN, 0, 0, 0);
keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);

知道了这点,那么模拟组合键也不在话下了,如Ctrl+Tab

1
2
3
4
keybd_event(VK_CONTROL, 0, 0, 0);//按下Ctrl
keybd_event(VK_TAB, 0, 0, 0);//按下Tab
keybd_event(VK_TAB, 0, KEYEVENTF_KEYUP, 0);//抬起Tab
keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);//抬起Ctrl

其他按键的宏定义可以参考:虚拟键代码(Virtual-Key Codes)

注:微软官方建议 - 此功能已被取代。请改用SendInput(其实差不多)

SendMessage/PostMessage

众所周知,Windows系统是基于消息的,而这两个函数都是向窗口发送消息

他们的区别在于:

  • SendMessage是同步函数,或者说阻塞函数,调用指定窗口的窗口过程,直到窗口过程处理完消息才返回
  • PostMessage是异步函数,在与创建指定窗口的线程关联的消息队列中发布一条消息,并在不等待线程处理消息的情况下返回。

也就是说,SendMessage返回的时候,消息肯定被对方处理完了

PostMessage不管处理没处理,什么时候处理,直接扔过去就返回了

好处是,对方不回应的时候,自己不会卡死(挂起)

为什么要提这两个函数呢,因为Windows基于消息,所以按键也是消息(WM_KEYDOWN | WM_KEYUP)

那么只要向指定窗口发送消息就可以模拟按键了:

1
2
3
//模拟A键
SendMessage(hwnd, WM_KEYDOWN, 'A', 0);
SendMessage(hwnd, WM_KEYUP, 'A', 0);

keybd_event多一个参数,这里的hwnd指的是窗口句柄,类似于窗口的ID

PostMessage如果这么写的话,就会发现不对劲了,按键貌似被重复了两遍

查询资料 & 官方文档可知,我们忽略了最后一个参数:lParam

重复计数、扫描码、扩展键标志、上下文代码、前一个键状态标志和转换状态标志,如下所示

lParam

击键消息的lParam参数包含有关生成消息的击键的附加信息。该信息包括重复计数、扫描码、扩展键标志、上下文代码、前一个键状态标志和转换状态标志。下图显示了这些标志和值在lParam参数中的位置。

击键消息的 lparam 参数中的标志和值的位置

也就是在一个32位的参数中,表示了大量标志信息,这需要用到位运算

1
2
3
4
5
6
7
DWORD dwVKFkeyData;
WORD dwScanCode = MapVirtualKey('B', 0);//计算扫描码
dwVKFkeyData = 1; //00000...00001(b)
dwVKFkeyData |= dwScanCode << 16;
PostMessage(hwnd, WM_KEYDOWN, 'B', dwVKFkeyData);
dwVKFkeyData |= 3 << 30; //11(b) << 30
PostMessage(hwnd, WM_KEYUP, 'B', dwVKFkeyData);

这样才能完成一次PostMessage击键消息的正确发送

显然是SendMessage更方便,但要考虑到阻塞函数的成本

问题与思考

以上都是一些基础操作,没什么好说的,也都是大家公认的

但是呢,需求一旦复杂化,基本的信息就不够用了,问题就蜂拥而至了

差异

首先我们说一下,keybd_eventSend/PostMessage的区别吧

  • keybd_event是全局按键模拟,也就是像真实地按键一样,将消息发送给前台活动窗口,也就是焦点窗口,这是一大限制

  • Send/PostMessage可以自行指定发送的窗口对象,可以向无焦点的窗口发送按键信息,更灵活

如果世上真有这样完美的函数就好了,那另一方就会被抹杀

这两者同时存在是有原因的:

  • Send/PostMessage无法模拟组合键(ALT除外),这是一个很大的限制,因为消息只是状态的切换,不能维持按下,而程序通常是直接检测Ctrl Shift等修饰键的状态来决定组合键的
  • 模拟按键时,SendMessage有时会失效(如QQ窗口),所以建议用PostMessage原因不明

组合键处理

那怎么办,要发送组合键怎么办

如果是活动窗口,那直接keybd_event模拟,没啥好说的

如果是非活动窗口怎么办

  • 最粗暴,直接将其变为前台活动窗口,然后再模拟(设置前台窗口参见:AttachThreadInput & SetForegroundWindow
  • ↑但这是个悖论,这样就不是非活动窗口了
  • 或者用keybd_event模拟修饰键(如Ctrl) + Send/PostMessage模拟其他按键(如A),但问题在于无法很好地配合消息顺序,因为keybd_event不会等待消息处理
  • 解决办法:在keybd_event之后延时一段时间,再进行SendMessage,可实现对非活动窗口的组合键控制

往下 再往下(驱动级模拟)

一般的窗口这样就行了,但是游戏窗口可能会直接使用底层硬件输入(低延时),而不经过Windows

这时候就不能在使用Windows消息机制了,必须直接对底层端口进行操作

这时候就需要外力帮助了

WinIO程序库允许在32位的Windows应用程序中直接对I/O端口和物理内存进行存取操作。通过使用一种内核模式的设备驱动器和其它几种底层编程技巧,它绕过了Windows系统的保护机制

拓展资料:

winio下载winio的源码_GitHub_帮酷

winIO介绍 - 野蛮木头 - 博客园

Ref

键盘鼠标模拟全知道 - 菜鸟学院

keybd_event function (winuser.h) - Win32 apps | Microsoft Docs

About Keyboard Input - Win32 apps | Microsoft Docs

WM_KEYDOWN message (Winuser.h) - Win32 apps | Microsoft Docs

通过PostMessage/SendMessage实现模拟键盘鼠标按键,发送不成功或出现重复按键的可参考本文_lzl_li的博客-CSDN博客_c# sendmessage模拟键盘

向某个窗口发送按键消息(包括后台隐藏的窗口) - 代码先锋网


模拟按键 - keybd_event Send/PostMessage的一些思考
https://mrbeancpp.github.io/2022/03/23/模拟按键-keybd-event-Send-PostMessage的一些思考/
作者
MrBeanC
发布于
2022年3月23日
许可协议