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))))