You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import ctypes
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-keybdinput
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-hardwareinput
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input
# https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
# ScanCode
# https://github.com/Lateralus138/Key-ScanCode/releases/tag/1.9.30.18
class MouseInput(ctypes.Structure):
_fields_ = [("dx", ctypes.c_long), # 水平方向的绝对位置/相对移动量(像素), dwFlags 中包含 MOUSEEVENTF_ABSOLUTE 标识就是绝对移动, 否则是相对移动
("dy", ctypes.c_long), # 垂直方向的绝对位置/相对移动量(像素)
("mouseData", ctypes.c_ulong), # 某些事件的额外参数, 如: MOUSEEVENTF_WHEEL(中键滚动), 可填正负值, 一个滚动单位是120(像素?); 还有 MOUSEEVENTF_XDOWN/MOUSEEVENTF_XUP
("dwFlags", ctypes.c_ulong), # 事件标识集, 可以是移动或点击事件的合理组合, 即可以一个命令实现移动且点击
("time", ctypes.c_ulong), # 事件发生的时间戳, 可以指定发生的时间? 传入0则使用系统提供的时间戳
("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))] # 应用可通过 GetMessageExtraInfo 来接收通过此参数传递的额外消息
class KeyboardInput(ctypes.Structure):
_fields_ = [("wVk", ctypes.c_ushort), # 虚拟键码, 范围在[1,254], 如果dwFlags指定了KEYEVENTF_UNICODE, 则wVk必须是0, https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
("wScan", ctypes.c_ushort), # 键的硬件扫描码, 如果dwFlags指定了KEYEVENTF_UNICODE, 则wScan需为一个Unicode字符
("dwFlags", ctypes.c_ulong), # 事件标识集, 可合理组合
("time", ctypes.c_ulong),
("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))]
class HardwareInput(ctypes.Structure):
_fields_ = [("uMsg", ctypes.c_ulong),
("wParamL", ctypes.c_short),
("wParamH", ctypes.c_ushort)]
class Inner(ctypes.Union): # 共用体, 和结构体类似, 但是各成员属性共用同一块内存空间, 实例化时的空间大小就是成员属性中最大的那个的空间大小, 实例化时只能赋值ki/mi/hi中的一个
_fields_ = [("ki", KeyboardInput),
("mi", MouseInput),
("hi", HardwareInput)]
class Input(ctypes.Structure):
_fields_ = [("type", ctypes.c_ulong), # 输入事件类型
("ii", Inner)]
"""
MouseInput
"""
MOUSEEVENTF_MOVE = 0x0001 # 移动
MOUSEEVENTF_LEFTDOWN = 0x0002 # 左键按下
MOUSEEVENTF_LEFTUP = 0x0004 # 左键释放
MOUSEEVENTF_RIGHTDOWN = 0x0008
MOUSEEVENTF_RIGHTUP = 0x0010
MOUSEEVENTF_MIDDLEDOWN = 0x0020
MOUSEEVENTF_MIDDLEUP = 0x0040
MOUSEEVENTF_XDOWN = 0x0080 # 侧键按下
MOUSEEVENTF_XUP = 0x0100 # 侧键释放
MOUSEEVENTF_WHEEL = 0x0800 # 滚轮垂直滚动, mouseData需传入滚动值, 一个滚动单位是120(像素?)
MOUSEEVENTF_HWHEEL = 0x1000 # 滚轮水平滚动
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000 # 移动消息不会被合并
MOUSEEVENTF_VIRTUALDESK = 0x4000 # Maps coordinates to the entire desktop. Must be used with MOUSEEVENTF_ABSOLUTE.
MOUSEEVENTF_ABSOLUTE = 0x8000 # 鼠标移动事件, 如果设置此标记就是绝对移动, 否则就是相对移动. 相对鼠标运动受鼠标速度(控制面板中的指针移动速度)和两个鼠标阈值(?和速度在同一个地方设置)的影响, 绝对值移动时范围是[0,65535]
XBUTTON1 = 0x0001 # 侧下键, dwFlags为MOUSEEVENTF_XDOWN/MOUSEEVENTF_XUP时, mouseData需传入x键值
XBUTTON2 = 0x0002 # 侧上键
WHEEL_DELTA = 120 # 鼠标滚轮滚动一个单位的最小值
# dwFlags 不能同时包含 MOUSEEVENTF_WHEEL/MOUSEEVENTF_XDOWN/MOUSEEVENTF_XUP, 因为它们都需要使用mouseData字段. 如果不包含这3个字段, 则mouseData应该传入0
# dwFlags 仅仅能表示状态的变化, 而不能表示状态的持续, 如它能表示按下左键, 但按下左键不代表持续按住左键, 但我测下来好像可以(只发送down不发送up则相当于一直按住)?
# If dwFlags contains MOUSEEVENTF_WHEEL, then mouseData specifies the amount of wheel movement. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120.
"""
KeyboardInput
"""
KEYEVENTF_EXTENDEDKEY = 0x0001 # If specified, the scan code was preceded by a prefix byte that has the value 0xE0 (224).
KEYEVENTF_KEYUP = 0x0002 # 指定该标记, 就是按键弹起, 否则就是按键按下
KEYEVENTF_SCANCODE = 0x0008 # 指定该标记, 就是使用硬件扫描码来发送按键按下事件, wVk参数将被忽略
KEYEVENTF_UNICODE = 0x0004 # 发送Unicode字符消息(按下), 可以与KEYEVENTF_KEYUP组合使用
# 按键的虚拟键码和扫描代码是两套码, 虚拟键码可能因键盘/布局不同而不同, 但扫描代码是键盘无关的, 所以推荐使用扫描码
# dwFlags 仅仅能表示状态的变化, 而不能表示状态的持续, 如按下w键, 物理键盘会持续输入w, 而模拟键盘则仅能输入一个w
"""
Input
"""
INPUT_MOUSE = 0 # 鼠标输入事件
INPUT_KEYBOARD = 1 # 键盘输入事件
INPUT_HARDWARE = 2 # 硬件输入事件
"""
SendInput
cInputs: pInputs 数组的个数, 即SendInput可以一下发好几个鼠标事件/键盘事件, 这些事件存储在一个连续的数组空间里
pInputs: 输入事件的实例的指针
cbSize: 每一个INPUT事件的结构体空间, 鼠标事件和键盘事件应该不能同时放到pInputs数组中, 因为他们的size不同
该函数返回成功插入键盘或鼠标输入流的事件数。如果函数返回零则输入已被另一个线程阻塞。要获取扩展错误信息请调用GetLastError.
"""
def SendInput(*inputs): # 接收任意个参数, 将其打包成为元组形参, 双*是打包成为字典形参
nInputs = len(inputs)
pointer = Input * nInputs
pInputs = pointer(*inputs)
# 创建一个指定类型的C数组并返回首指针, 格式如下
# pointer = (Type * 数组长度)(填充该数组的实例,用逗号分隔开,个数不超过数组长度)
# 1. pointer = (Input * 3)(); 创建一个空间为3个Input的C数组, 其中的每个位置的Input已经按默认值初始化
# 2. pointer = (Input * 3)(input); 创建一个空间为3个Input的C数组, 并将input赋值给第一个位置
# 3. pointer = (Input * 3)(input, input2, input2), 创建一个空间为3个Input的C数组, 并将input赋值给第一个位置, 将input2赋值给第二第三个位置
# 4. pointer = (Input * 3)(input, input2, input3, input4), 创建一个空间为3个Input的C数组, 赋值时报错, 因为没有放input4的空间
# 5. pointer = (Input * 3)(*inputs); 创建一个空间为3个Input的C数组, 并将集合类型的inputs解包并赋值给数组的对应位置
# 6. 也可以如上分开两行写
cbSize = ctypes.sizeof(Input)
return ctypes.windll.user32.SendInput(nInputs, pInputs, cbSize)
class Keyboard:
@staticmethod
def press(wVk): # 十六进制虚拟键码, 范围在[1,254], 如果dwFlags指定了KEYEVENTF_UNICODE, 则wVk必须是0, https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(wVk, 0, 0, 0, None))))
@staticmethod
def release(wVk):
return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(wVk, 0, KEYEVENTF_KEYUP, 0, None))))
@staticmethod
def pressByScanCode(wScan): # 十六进制键扫描码, 可通过该工具获得, https://github.com/Lateralus138/Key-ScanCode/releases/tag/1.9.30.18
return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_SCANCODE, 0, None))))
@staticmethod
def releaseByScanCode(wScan):
return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP, 0, None))))
@staticmethod
def pressByUnicode(wScan): # 十六进制Unicode编码, 比如 汉字 "一" 的十六进制UTF8编码为 0x4e00, 则可通过发送该编码直接输入汉字一
return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_UNICODE, 0, None))))
@staticmethod
def releaseByUnicode(wScan):
return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, None))))
class Mouse:
@staticmethod
def leftDown():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN, 0, None))))
@staticmethod
def leftUp():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_LEFTUP, 0, None))))
@staticmethod
def leftClick():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, None))))
@staticmethod
def leftDoubleClick():
event = Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, None)))
return SendInput(event, event)
@staticmethod
def rightDown():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_RIGHTDOWN, 0, None))))
@staticmethod
def rightUp():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_RIGHTUP, 0, None))))
@staticmethod
def rightClick():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, None))))
@staticmethod
def middleDown():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_MIDDLEDOWN, 0, None))))
@staticmethod
def middleUp():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_MIDDLEUP, 0, None))))
@staticmethod
def middleClick():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, 0, MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, None))))
@staticmethod
def x1Down(): # 侧下键
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, XBUTTON1, MOUSEEVENTF_XDOWN, 0, None))))
@staticmethod
def x1Up():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, XBUTTON1, MOUSEEVENTF_XUP, 0, None))))
@staticmethod
def x1Click():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, XBUTTON1, MOUSEEVENTF_XDOWN | MOUSEEVENTF_XUP, 0, None))))
@staticmethod
def x2Down(): # 侧上键
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, XBUTTON2, MOUSEEVENTF_XDOWN, 0, None))))
@staticmethod
def x2Up():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, XBUTTON2, MOUSEEVENTF_XUP, 0, None))))
@staticmethod
def x2Click():
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, XBUTTON2, MOUSEEVENTF_XDOWN | MOUSEEVENTF_XUP, 0, None))))
@staticmethod
def move(x, y, absolute=False):
if not absolute:
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(x, y, 0, MOUSEEVENTF_MOVE, 0, None))))
else:
# 绝对值移动时, 屏幕宽高的取值范围都是[0, 65535], 需要自行换算一下
# 获取显示分辨率(非物理分辨率)
w, h = ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1)
rx, ry = int(x * 65535 / w), int(y * 65535 / h)
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(rx, ry, 0, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, 0, None))))
@staticmethod
def scroll(delta, vertical=True): # 滚动delta个单位, 一个单位是120(像素?), 区分正负值, 正值:向下转/向右转, 负值:向上移/向左转
dwFlags = MOUSEEVENTF_WHEEL if vertical else MOUSEEVENTF_HWHEEL
return SendInput(Input(INPUT_MOUSE, Inner(mi=MouseInput(0, 0, delta * WHEEL_DELTA, dwFlags, 0, None))))