模拟键鼠输入 - Python
模拟键鼠输入在Python中多种不同的库,这里就用pynput
库。
安装(pynput · PyPI):
1 | pip install pynput |
模拟键盘输入:
1 | from pynput.keyboard import Controller |
模拟鼠标输入:
1 | from pynput import mouse |
监听键盘输入:
1 | from pynput.keyboard import Key, Listener, Controller |
如果是ascii字符,则会直接输出一个字符;如果是shift、ctrl、pause这种特殊按键,则会输出一个Key.xxx
有了这些之后就可以结合手动信号量来写一个自动按键脚本了,比如下面的就是一个在原神中自动按f并且鼠标自动点对话按钮的脚本:
1 | import time |
关于pynput
如果你试过用C#或者js的事件监听,你会发现如果当前页面失去焦点,那么键鼠的事件监听将会失效。但是python的pynput库则可以在python脚本程序最小化时也能成功监听,这是因为它在windows下是直接调用了win32 api的钩子函数监听了低级的键盘事件(SetWindowsHookExA function (winuser.h) - Win32 apps | Microsoft Learn)。
同理,它的模拟输入也是调用了SendInput
(SendInput function (winuser.h) - Win32 apps | Microsoft Learn)。
另外,pynput实现了多个操作系统的键鼠监听与模拟输入,因此并不仅局限于windows平台。(其他主流的python键鼠库也基本是这样的)
键鼠监听和模拟键鼠输入 - C#(user32.dll)
知道以上信息后,可以用C#调用user32.dll来做到键鼠输入的监听和模拟输出。
注意,SetWindowsHookEx
监听只能在C#窗体应用中才能用,控制台应用不可使用。
监听步骤:
声明钩子函数
SetWindowsHookEx
和其需要的回调函数(SetWindowsHookExA function (winuser.h) - Win32 apps | Microsoft Learn)1
2
3
4
5
6// 定义名为LowLevelKeyboardProc的委托用于回调处理
private delegate IntPtr LowLevelKeyboardProc
LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc callback, IntPtr hInstance, uint threadId);在该函数中有4个参数:
idHook
:决定要监听的事件类型,这里以键盘输入事件为例,也就是WH_KEYBOARD_LL
,值为13。更多事件在上面的链接中可以看到。callback
:回调函数,当对应的事件触发时,系统会调用执行这个callback。这个callback请参考:HOOKPROC (winuser.h) - Win32 apps | Microsoft Learn。根据这个链接里的内容可以发现,它的入参的值与钩子函数监听的事件类型有关。这里我们监听的是键盘输入事件,那么它的回调参数含义可以参考:LowLevelKeyboardProc callback function - Win32 apps | Microsoft Learn。如果你想处理其他类型的事件,那么就参考idHook表格中的Proc说明链接。在键盘事件回调函数中的3个入参为:nCode
:用来告知钩子函数该如何处理信息,在这里不需要管它。wParam
:事件类型,这里以WM_KEYUP
为例。(WM_KEYUP message (Winuser.h) - Win32 apps | Microsoft Learn)lParam
:是一个KBDLLHOOKSTRUCT
类型的结构体,可以根据文档(KBDLLHOOKSTRUCT (winuser.h) - Win32 apps | Microsoft Learn)按需取自己需要的信息。
hInstance
:当前实例句柄threadId
:与钩子相关的线程
关于hInstance
和threadId
我也不太能理解(SetWindowsHookEx钩子详解_setwindowshook 可以不指定线程吗-CSDN博客),对于键盘事件这种全局事件来说,这两个都是可以设为0的。但是看其他的实例中,还是会把当前实例句柄传入hInstance
,因此本文也会这样做。
用系统api
GetModuleHandle
获取当前进程实例句柄1
2
3
4
5[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
// 使用方法
// using System.Diagnostics;
// GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName)声明
CallNextHookEx
函数
这个函数需要在回调函数中调用以将键盘事件传递下去。(CallNextHookEx function (winuser.h) - Win32 apps | Microsoft Learn)1
2[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);其中:
hhk
可以忽略,这里的hhk
原本指的是钩子的句柄,也就是SetWindowsHookEx
的返回值。你可以直接传入一个空指针,也可以就按原意传入hook句柄。其他的参数只要把hook回到函数中的入参原原本本传进去即可。使用
MapVirtualKeyA
把VkCode
转为char。(MapVirtualKeyA function (winuser.h) - Win32 apps | Microsoft Learn)1
2[DllImport("user32.dll")]
public static extern IntPtr MapVirtualKeyA(IntPtr uCode, uint uMapType);这里的
uMapType
取2就是MAPVK_VK_TO_CHAR
了。编写回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP)
{
// 只响应监控WM_KEYUP事件
int vkCode = Marshal.ReadInt32(lParam);
// 根据文档可以知道,这个结构体第一个32位即虚拟码vkCode
// 第二个32位即扫描码scanCode
//int scanCode = Marshal.ReadInt32(lParam, 4); // 4表示偏移4个字节
// 如果你想用扫描码,可以参考:https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-6.0/aa299374(v=vs.60)?redirectedfrom=MSDN
Trace.WriteLine(vkCode);
// vkCode转char
char c = (char)MapVirtualKeyA(vkCode, 2);
Trace.WriteLine(c);
// 如果不是字符,那么MapVirtualKeyA会返回0
}
// 这里的_hookID: IntPtr _hookID = SetWindowsHookEx(...)
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}取消钩子
1
2
3[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);hhk
即上文提到的钩子句柄。完整代码(忽略类)
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
50
51
52
53
54
55
56
57
58using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private static LowLevelKeyboardProc lowLevelKeyboardProc = mycallback; // 回调
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYUP = 0x0101;
private static IntPtr _hookID = SetHook(mycallback); // 钩子句柄, SetHook定义在下面
// 声明dll函数
[DllImport("user32.dll")]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc callback, IntPtr hInstance, uint threadId);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
public static extern IntPtr PostMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern IntPtr MapVirtualKeyA(IntPtr uCode, uint uMapType);
// 定义回调
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP)
{
// 只响应监控WM_KEYUP事件
int vkCode = Marshal.ReadInt32(lParam);
Trace.WriteLine(vkCode);
// vkCode转char
char c = (char)MapVirtualKeyA(vkCode, 2);
Trace.WriteLine(c);
// 如果不是字符,那么MapVirtualKeyA会返回0
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
// 注册钩子
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
return SetWindowsHookEx(WH_KEYBOARD_LL,
proc,
GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
0);
}
模拟输出步骤:
- 定义
SendInput
函数 - 将待模拟的输入转换为VkKey
转VkKey有两种方式,一种是直接查表(Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn),另一种是调用VkKeyScanA
(VkKeyScanA function (winuser.h) - Win32 apps | Microsoft Learn)
代码参考(C# - SendInput(Windows API)によるキー入力のサンプル(32/64bit対応) #Win32API - Qiita)
主要注意input结构体的构造。
使用PostMessageW模拟输入
PostMessage
函数可以通过句柄直接把消息发到对应的窗口中,这样的好处是当你切屏时键盘输入不会乱按,而且对于有些程序来说,它在后台时也会处理PostMessage
发送的消息,这样你就不需要一直让目标程序处于前台了。
但是对于有些游戏来说,即使你用了PostMessage
,它在后台时也不会处理输入的信息,甚至对于某些游戏即使它获得焦点的情况下仍然不会处理PostMessage
发送的信息(比如星穹铁道)。
使用PostMessage
会比SendInput
更复杂一些。PostMessageW function (winuser.h) - Win32 apps | Microsoft Learn
定义
PostMessageW
函数找到目标窗口的句柄(EnumWindows function (winuser.h) - Win32 apps | Microsoft Learn)
这里我的实现比较繁琐一些。在这里定义了一个Params结构体,它会被传入EnumWindows的迭代器中,我用Params来保存handler和title的array。其中的array_length存储两个array的长度,用Append函数添加句柄和标题。只有数组能传到dll中,所以这里没用List,而是手动扩容和维护数组有效长度。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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78delegate bool EnumWindowsHandler(IntPtr HWND,ref Params Lparam);
static EnumWindowsHandler lpEnumFunc = EnumWindowsProc;
private struct Params
{
public string window_title;
public IntPtr[] target_handler_array = new IntPtr[2];
public String[] title_array = new String[2];
public int array_length = 0;
public void Append(IntPtr handler, string title)
{
if (array_length < target_handler_array.Length)
{
target_handler_array[array_length] = handler;
title_array[array_length] = title;
}
else
{
IntPtr[] new_handler_list = new IntPtr[target_handler_array.Length * 2];
target_handler_array.CopyTo(new_handler_list, 0);
target_handler_array = new_handler_list;
target_handler_array[array_length] = handler;
string[] new_title_array = new string[title_array.Length * 2];
title_array.CopyTo(new_title_array, 0);
title_array = new_title_array;
title_array[array_length] = title;
}
array_length++;
}
public Params(string window_title)
{
this.window_title = window_title;
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool EnumWindows(EnumWindowsHandler handler, ref Params lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int GetWindowTextW(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
private static bool EnumWindowsProc(IntPtr HWND, ref Params LPARAM)
{
// 每次迭代都会调用proc处理,当return false时停止迭代
StringBuilder window_name = new StringBuilder(100);
// getwindowtextw会把HWND句柄的title名复制到window_name参数中
int flag = GetWindowTextW(HWND, window_name, 100);
if (LPARAM.window_title != "")
{
if (flag != 0 && window_name.ToString() == LPARAM.window_title)
{
LPARAM.Append(HWND, window_name.ToString());
// 找到第一个就退出
return false;
}
}
else
{
// 如果window_title为"",说明要遍历所有进程
if (flag != 0)
{
LPARAM.Append(HWND, window_name.ToString());
}
}
return true;
}
// 之后调用这个函数即可获取句柄+对应窗口名的数组
static public void GetWindowsHandlerByTitle(string window_title, out IntPtr[] handler_array, out string[] title_array) {
Params lParam = new Params(window_title);
EnumWindows(lpEnumFunc, ref lParam);
handler_array = lParam.target_handler_array.Take(lParam.array_length).ToArray();
title_array = lParam.title_array.Take(lParam.array_length).ToArray();
}决定输入事件(Msg)
将待模拟的输入转换为VkKey(wParam)
构造lparam(lParam)
lParam对于不同的事件来说是不一样的。在这里用的是WM_KEYDOWN
(WM_KEYDOWN message (Winuser.h) - Win32 apps | Microsoft Learn)和WM_KEYUP
(WM_KEYUP message (Winuser.h) - Win32 apps | Microsoft Learn)事件。
如果你想用Python实现PostMessage
,请参考Python开发游戏自动化脚本(四)后台键鼠操作 - 知乎 (zhihu.com)
如果你想用C#实现,请参考以下步骤:
1 | [DllImport("user32.dll")] |