Electron中使用Node-ffi模拟键鼠操作

前言

折腾这个东西就是要实现一个很简单的功能:在我的应用中控制处于后台的PPT进行翻页。

结论:无法实现。

在我测试的过程中无论是wps还是office都无法在后台响应的事件 用Spy++查看无论是窗口句柄还是发送的消息都是完全正确的,都无法响应。处于前台时也依旧没法用PostMessageA或是SendMessageA发送消息,但使用keybd_event是可以的。

也就是说:

  • keybd_event只能在应用在前台时才有效,因为他发送的是全局事件。
  • PostMessageASendMessageA 发送的是应用的事件,但是也可能无论应用在前台或是后台都无效。

工具

Viewdll

查看DLL中的函数 支持WIN10

链接:https://pan.baidu.com/s/19emkiUdbCdaPRKrY9ahyWw
提取码:i32n

Microsoft Spy++

查看应用的按键消息等

链接:https://pan.baidu.com/s/1TV5c4cTOiG7E-OdMtHA_mA
提取码:scew

模拟单个按键

模拟单个按键,如按下键A

在一般情况下可以,即使目标程序在后台运行也可以。
但正如你等下在下面看到的文章所说,在某些程序里第四个参数需要特别注意,否则发送按键将无效。

1
PostMessageA(hWnd,WM_KEYDOWN,'A',0);

模拟ALT+A

向后台程序发送组合键ALT+按键 是可行的。记住,只可以是ALT,不能是Ctrl或Shift
操作如下:发送ALT+A

1
PostMessageA(hWnd,WM_SYSKEYDOWN,'A',1<<29);

模拟其他组合按键

我现在的做法只能是激活目标窗口使其成为前台窗口后再模拟发送组合按键,如下:

1
2
3
4
5
SetForegroundWindow(g_OperaWnd);
keybd_event(VK_CONTROL,0,0,0);
keybd_event(65,0,0,0);
keybd_event(65,0,KEYEVENTF_KEYUP,0);
keybd_event(VK_CONTROL,0,KEYEVENTF_KEYUP,0);

模拟鼠标的行为

模拟鼠标的行为最好用SendMessageA(不要用PostMessageA),这样可以把消息直接发送到目的窗口的窗口处理过程,成功率会高很多。

1
SendMessageA(GetWnd(),WM_LBUTTONDOWN,NULL,MAKELPARAM(47,11));

方法介绍

PostMessage

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea

声明PostMessage函数的时候(其实很多很多API函数都是这样),有两种版本:A结尾的是ANSI版本,W结尾的是Unicode版本。一般用A结尾的。

可见PostMessage是我们为了方便起的名字,PostMessageA才是人家的原名。

1
static extern bool PostMessageW(int hwnd, int msg, uint wParam, uint lParam);
  • 第一参数是窗口句柄

  • 第二个参数是消息windows消息,在C#中需要定义WM_CHAR或者直接填WM_CHAR的值0x0102

  • 第三个参数填键码

  • 第四个参数

    • 0-15位:指定当前消息的重复次数。其值就是用户按下该键后自动重复的次数,但是重复次数不累积
    • 16-23位:指定其扫描码,其值依赖于OEM厂商
    • 24位:指定该按键是否为扩展按键,所谓扩展按键就是Ctrl,Alt之类的,如果是扩展按键,其值为1,否则为0
    • 25-28位:保留字段,暂时不可用
    • 29位:指定按键时的上下文,其值为1时表示在按键时Alt键被按下,其值为0表示WM_SYSKEYDOWN消息因没有任何窗口有键盘焦点而被发送到当前活动窗口。
    • 30位:指定该按键之前的状态,其值为1时表示该消息发送前,该按键是被按下的,其值为0表示该消息发送前该按键是抬起的。
    • 31位:指定其转换状态,对WM_SYSKEYDOWN消息而言,其值总为0。

    请看位29的说明!!

    当值为1时表示ALT键被按下!这不正是我需要的吗?于是把29位设置为1,函数调用变成

    1
    PostMessage(hWnd,WM_SYSKEYDOWN,0x41,1<<29);

    经过测试,发现这个就是Alt+A的效果!!

那么再来看看如何确定键盘消息中的wParam 和lParam 这两个参数。

wParam 参数的含义较简单,它表示你要发送的键盘事件的按键虚拟码,比如你要对目标程序模拟按下A键,那么wParam 参数的值就设为VK_A 。

lParam 这个参数就比较复杂了,因为它包含了多个信息,一般可以把它设为0,但是如果你想要你的模拟更真实一些,那么建议你还是设置一下这个参数。那么我们就详细了解一下lParam 吧。

lParam 是一个long类型的参数,它在内存中占4个字节,写成二进制就是00000000 00000000 00000000 00000000 一共是32位,我们从右向左数,假设最右边那位为第0位(注意是从0而不是从1开始计数),最左边的就是第31位,

那么该参数的的

0-15位表示键的发送次数等扩展信息,

16-23位为按键的扫描码,

24-31位表示是按下键还是释放键。

大家一般习惯写成16进制的,那么就应该是00 00 00 00

  • 0-15位一般为0001
  • 16-23位的扫描码
  • 24-31位如果是按下键为00,释放键则为C0,

那么16-23位的扫描码怎么得呢

1
MapVirtualKeyA(0x41,0)

我们先看看VB怎样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function MakeKeyLparam(ByVal VirtualKey As Long, ByVal flag As Long) As Long
'参数VirtualKey表示按键虚拟码,flag表示是按下键还是释放键,用WM_KEYDOWN和WM_KEYUP这两个常数表示
Dim s As String
Dim Firstbyte As String 'lparam参数的24-31位
If flag = WM_KEYDOWN Then '如果是按下键
Firstbyte = "00"
Else
Firstbyte = "C0" '如果是释放键
End If
Dim Scancode As Long
'获得键的扫描码
Scancode = MapVirtualKey(VirtualKey, 0)
Dim Secondbyte As String 'lparam参数的16-23位,即虚拟键扫描码
Secondbyte = Right("00" & Hex(Scancode), 2)
s = Firstbyte & Secondbyte & "0001" '0001为lparam参数的0-15位,即发送次数和其它扩展信息
MakeKeyLparam = Val("&H" & s)
End Function

JS写法

16进制转换

1
2
3
4
5
6
//10进制字符串
console.info(0x41.toString());
//16进制字符串
console.info(0x41.toString(16));
//16进制字符串转数字
console.info(parseInt("0x41"));

上面的方法JS的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// flag 0按下 1释放
function makeKeyLparam(virtualKey, flag) {
let firstbyte = "";
let scancode = user32.MapVirtualKeyA(virtualKey, 0).toString(16);
if (flag === 0) {
firstbyte = "00"
} else {
firstbyte = "C0"
}
let keyLparamStr = "0x" + firstbyte + scancode + "0001";
console.info("keyLparamStr", keyLparamStr);
let keyLparam = parseInt(keyLparamStr);
console.info("keyLparam", keyLparam);
return keyLparam
}

PPT放映

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const ffi = require('ffi');
const ref = require('ref');
const iconv = require('iconv-lite');

const voidPtr = ref.refType(ref.types.void);
const stringPtr = ref.refType(ref.types.CString);
const user32 = ffi.Library('user32.dll', {
GetSystemMenu: ['int', ['int', 'bool']],
EnumWindows: ['bool', [voidPtr, 'int32']],
GetWindowTextA: ['long', ['long', stringPtr, 'long']],
SetForegroundWindow: ['bool', ['long']],
MapVirtualKeyA: ['long', ['long', 'long']],
PostMessageW: ['bool', ['long', 'long', 'long', ref.types.ulong]],
SendMessageA: ['bool', ['long', 'long', 'long', ref.types.ulong]],
keybd_event:['bool', ['int', 'int', 'int', 'int']]
});

// flag 0按下 1释放
function makeKeyLparam(virtualKey, flag) {
let firstbyte = "";
let scancode = user32.MapVirtualKeyA(virtualKey, 0).toString(16);
if (flag === 0) {
firstbyte = "00"
} else {
firstbyte = "C0"
}
let keyLparamStr = "0x" + firstbyte + scancode + "0001";
console.info("keyLparamStr:", keyLparamStr);
let keyLparam = parseInt(keyLparamStr);
return keyLparam
}

windowProc = ffi.Callback('bool', ['long', 'int32'], function (hwnd, lParam) {
let buf, name, ret;
buf = new Buffer(255);
ret = user32.GetWindowTextA(hwnd, buf, 255);
name = ref.readCString(buf, 0);
let text = iconv.decode(buf, 'gbk');
if (text.indexOf("ppt") !== -1) {
console.log(text);
console.info(hwnd.toString(16));
console.log("");
user32.SetForegroundWindow(hwnd);
setTimeout(function () {
user32.keybd_event(0x74,0,0,0);
}, 500);
}
return false;
});

下面的代码并不起作用 但点击F5的消息确实是发送了

1
2
3
4
5
6
let lp1 = makeKeyLparam(0x74, 0);
let lp2 = makeKeyLparam(0x74, 1);
user32.PostMessageW(hwnd, 0x0100, 0x74, lp1);
setTimeout(function () {
user32.PostMessageW(hwnd, 0x0101, 0x74, lp2);
}, 500);

Shift+F5

1
2
3
4
user32.keybd_event(0x10,0,0,0);
user32.keybd_event(0x74,0,0,0);
user32.keybd_event(0x74,0,2,0);
user32.keybd_event(0x10,0,2,0);

按键表

https://docs.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes

https://docs.microsoft.com/zh-cn/windows/win32/inputdev/wm-syskeydown

按键方式码

常用名称 十六进制值 十进制值 作用
WM_KEYDOWN 0x0100 256 表示一个普通键被按下
WM_KEYUP 0x0101 257 表示一个普通键被释放
WM_SYSKEYDOWN 0x0104 260 表示一个系统键被按下,比如Alt键
WM_SYSKEYUP 0x0105 261 表示一个系统键被释放,比如Alt键

WM_KEYDOWNWM_KEYUP之间的区别就很容易区别了,一个是键的按下,一个是键的释放。

WM_SYSKEYDOWNWM_KEYDOWN的区别在于WM_SYSKEYDOWNWM_SYSKEYUP消息经常由与Alt相组合的按键产生,这些按键启动程序菜单或者系统菜单上的选项,或者用于切换活动窗口等系统功能(Alt-Tab或者Alt-Esc),也可以用作系统菜单加速键(Alt键与一个功能键相结合,例如Alt-F4用于关闭应用程序)。程序通常忽略WM_SYSKEYUPWM_SYSKEYDOWN消息,并将它们传送到DefWindowProc。由于Windows要处理所有Alt键的功能,所以您无需拦截这些消息。您的窗口消息处理程序将最后收到关于这些按键结果(如菜单选择)的其它消息。如果您想在自己的窗口消息处理程序中加上拦截系统按键的程序码,那么在处理这些消息之后再传送到DefWindowProc,Windows就仍然可以将它们用于通常的目的。当然我们完全可以在响应WM_KEYDOWNWM_KEYUP消息的lParam参数时,判断第29位来判断Alt键是否按下,如果在按键的时候同时按下ALT键,那么该位为1, 否则为0;或者通过GetKeyState(VK_MENU)来判断ALT也是可以的。

我们开发时主要用WM_KEYDOWNWM_KEYUP

按键码

常用名称 十六进制值 十进制值 对应按键
VK_LBUTTON 0x01 1 鼠标的左键
VK_RBUTTON 0x02 2 鼠标的右键
VK-CANCEL 0x03 3 Contol-break执行
VK_MBUTTON 0x04 4 鼠标的中键(三按键鼠标)
VK_BACK 0x08 8 Backspace键
VK_TAB 0x09 9 Tab键
VK_CLEAR 0x0C 12 Clear键
VK_RETURN 0x0D 13 Enter键
VK_SHIFT 0x10 16 Shift键
VK_CONTROL 0x11 17 Ctrl键
VK_MENU 0x12 18 Alt键
VK_PAUSE 0x13 19 Pause键
VK_CAPITAL 0x14 20 Caps Lock键
VK_ESCAPE 0x1B 27 Ese键
VK_SPACE 0x20 32 Spacebar键
VK_PRIOR 0x21 33 Page Up键
VK_NEXT 0x22 34 Page Domw键
VK_END 0x23 35 End键
VK_HOME 0x24 36 Home键
VK_LEFT 0x25 37 LEFT ARROW键(←)
VK_UP 0x26 38 UP ARROW键(↑)
VK_RIGHT 0x27 39 RIGHT ARROW键(→)
VK_DOWN 0x28 40 DOWN ARROW键(↓)
VK_SELECT 0x29 41 SELECT键
VK_EXECUTE 0x2B 43 EXECUTE键
VK_SNAPSHOT 0x2C 44 Print Screen键
VK_INSERT 0x2D 45 Ins键
VK_DELETE 0x2E 46 Del键
VK_HELP 0x2F 47 Help键
VK_0 0x30 48 0键
VK_1 0x31 49 1键
VK_2 0x32 50 2键
VK_3 0x33 51 3键
VK_4 0x34 52 4键
VK_5 0x35 53 5键
VK_6 0x36 54 6键
VK_7 0x37 55 7键
VK_8 0x38 56 8键
VK_9 0x39 57 9键
VK_A 0x41 65 A键
VK_B 0x42 66 B键
VK_C 0x43 67 C键
VK_D 0x44 68 D键
VK_E 0x45 69 E键
VK_F 0x46 70 F键
VK_G 0x47 71 G键
VK_H 0x48 72 H键
VK_I 0x49 73 I键
VK_J 0x4A 74 J键
VK_K 0x4B 75 K键
VK_L 0x4C 76 L键
VK_M 0x4D 77 M键
VK_N 0x4E 78 N键
VK_O 0x4F 79 O键
VK_P 0x50 80 P键
VK_Q 0x51 81 Q键
VK_R 0x52 82 R键
VK_S 0x53 83 S键
VK_T 0x54 84 T键
VK_U 0x55 85 U键
VK_V 0x56 86 V键
VK_W 0x57 87 W键
VK_X 0x58 88 X键
VK_Y 0x59 89 Y键
VK_Z 0x5A 90 Z键
VK_NUMPAD0 0x60 96 数字键0键
VK_NUMPAD1 0x61 97 数字键1键
VK_NUMPAD2 0x62 98 数字键2键
VK_NUMPAD3 0x63 99 数字键3键
VK_NUMPAD4 0x64 100 数字键4键
VK_NUMPAD5 0x65 101 数字键5键
VK_NUMPAD6 0x66 102 数字键6键
VK_NUMPAD7 0x67 103 数字键7键
VK_NUMPAD8 0x68 104 数字键8键
VK_NUMPAD9 0x69 105 数字键9键
VK_MULTIPLY 0x6A 106 *键
VK_ADD 0x6B 107 =+键
VK_SEPARATOR 0x6C 108 Separator键
VK_SUBTRACT 0x6D 109 =-键
VK_DECIMAL 0x6E 110 .键
VK_DIVIDE 0x6F 111
VK_F1 0x70 112 F1键
VK_F2 0x71 113 F2键
VK_F3 0x72 114 F3键
VK_F4 0x73 115 F4键
VK_F5 0x74 116 F5键
VK_F6 0x75 117 F6键
VK_F7 0x76 118 F7键
VK_F8 0x77 119 F8键
VK_F9 0x78 120 F9键
VK_F10 0x79 121 F10键
VK_F11 0x7A 122 F11键
VK_F12 0x7B 123 F12键
VK_NUMLOCK 0x90 144 Num Lock键
VK_SCROLL 0x91 145 Scroll Lock键