最近支援C++兄弟的项目里面有在windows下发送模拟按键的需求,整个功能做下来发现了不少的坑,这里记录下来。
首先Windows上发送模拟按键可以用SendInput或者keybd_event去实现,而keyb_event文档里面也说更推荐用SendInput,所以我也选用了它:
Note This function has been superseded. Use SendInput instead.
SendInput的用法也很简单,下面是官方文档提供的Demo:
1 | // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput#example |
服务中无法发送按键
代码写完之后在本地调试下工作正常,但是部署到正式环境之后没有效果。经过对比发现本机调试时候是直接运行exe,而正式环境下我们的程序实际是作为Windows的一个服务在运行,在服务中调用SendInput没有作用。
原因是Windows服务运行在非交互会话中,这意味着服务所使用的窗口会话并非是当前正在登录用户的会话或默认会话。在这种情况下使用SendInput函数也会失败。因为SendInput函数必须将输入消息发送到活动窗口或当前焦点所在的窗口,而在非交互会话中默认没有活动窗口或当前焦点。
其实这个在服务开发里面还是挺经常遇到的,常规的方式就是使用CreateProcessAsUserW在当前登录用户的会话中创建子进程去调用SendInput(代码见Github,参考CreateProcessAsCurrentUser(cmd, false))。
任务管理器接收不到模拟按键
加上子进程之后正常情况功能是正常了,但是在部分场景下发送模拟按键还是没有效果,例如焦点在任务管理器的情况下。这个问题在文档里面其实也有提及:
This function is subject to UIPI. Applications are permitted to inject input only into applications that are at an equal or lesser integrity level.
只能往和自己同样或者低权限的程序发送按键。所以解决的思路就是让子进程以管理员权限运行,搜索之后发现的确是可行的,在系统服务中可以用GetTokenInformation获得一个TokenLinkedToken去启动一个管理员身份的进程:
1 | 我们用GetTokenInformation可以获得一个TokenLinkedToken,简单的说就是要获得与我们进程token关联的token。 |
PS: 代码见Github,参考CreateProcessAsCurrentUser(cmd, true)
方向键被识别成小键盘数字键
本来以为已经没有问题了,但是测试发现在某些场景下,上下作用的方向键会被识别成2468的数字键。例如开始菜单的搜索栏会概率出现,而我在debug的时候发现必现的场景是快捷方式的快捷键设置那里,而且windows自动的虚拟键盘也是有同样的问题:

会将 VK_LEFT、VK_UP、VK_RIGHT、VK_DOWN 识别成 VK_NUMPAD4、VK_NUMPAD8、VK_NUMPAD6、VK_NUMPAD2。
用键盘检测工具检测按键码,发现发送的的确是VK_LEFT、VK_UP、VK_RIGHT、VK_DOWN:

后面发现第三方的虚拟键盘没有这个问题:

用键盘检测工具去检查,发现这两者的区别在于这个第三方键盘会设置按键的扫描码,而且在发送方向键的时候还会设置KEYEVENTF_EXTENDEDKEY:

我尝试了下,实际上不需要设置扫描码,只需要把KEYEVENTF_EXTENDEDKEY这个flag加上问题也解决了:
1 | INPUT ip = {0}; |
从文档来看,像方向键和INS, DEL, HOME, END, PAGE UP, PAGE DOWN这些按键,作为拓展键,他们的扫描码会在在数据前拼上0xE0:
1 | The extended-key flag indicates whether the keystroke message originated from one of the additional keys on the Enhanced 101/102-key keyboard. The extended keys consist of the ALT and CTRL keys on the right-hand side of the keyboard; the INS, DEL, HOME, END, PAGE UP, PAGE DOWN, and arrow keys in the clusters to the left of the numeric keypad; the NUM LOCK key; the BREAK (CTRL+PAUSE) key; the PRINT SCRN key; and the divide (/) and ENTER keys in the numeric keypad. The right-hand SHIFT key is not considered an extended-key, it has a separate scan code instead. |
从扫描码对照表里面也可以看出来。
方向键的扫描码是0xE048、0xE050、0xE04B、0xE04D, 如果没有前面的0xE0,变成0x48、0x50、0x4B、0x4D就可能被识别成方向键或者小键盘的数字键:
HID Usage Page | HID Usage ID | HID Usage Name | Key Location | Scan 1 Make |
---|---|---|---|---|
07 | 52 | Keyboard UpArrow | 83 | E0 48 |
07 | 51 | Keyboard DownArrow | 84 | E0 50 |
07 | 50 | Keyboard LeftArrow | 79 | E0 4B |
07 | 4F | Keyboard RightArrow | 89 | E0 4D |
07 | 60 | Keypad 8 and Up Arrow | 96 | 48 |
07 | 5A | Keypad 2 and Down Arrow | 98 | 50 |
07 | 5C | Keypad 4 and Left Arrow | 92 | 4B |
07 | 5E | Keypad 6 and Right Arrow | 102 | 4D |
于是猜测某些程序会将虚拟按键码转换成扫描码去做处理,在没有设置KEYEVENTF_EXTENDEDKEY的时候可能转换出来就识别成了小键盘数字键。