1. Windows输入模拟基础在Windows系统中模拟键盘鼠标输入主要有两种方式通过虚拟键码(Virtual Key Codes)和扫描码(Scan Codes)。虚拟键码是与键盘布局相关的抽象键值而扫描码则是硬件层面的物理键值。举个例子字母A在美式键盘上的虚拟键码是0x41但它的扫描码是0x1E。使用ctypes调用Windows API时我们需要先定义对应的结构体。Windows SendInput API需要三个核心结构体import ctypes from ctypes import wintypes class MOUSEINPUT(ctypes.Structure): _fields_ [ (dx, wintypes.LONG), (dy, wintypes.LONG), (mouseData, wintypes.DWORD), (dwFlags, wintypes.DWORD), (time, wintypes.DWORD), (dwExtraInfo, ctypes.POINTER(wintypes.ULONG)) ] class KEYBDINPUT(ctypes.Structure): _fields_ [ (wVk, wintypes.WORD), (wScan, wintypes.WORD), (dwFlags, wintypes.DWORD), (time, wintypes.DWORD), (dwExtraInfo, ctypes.POINTER(wintypes.ULONG)) ]在实际项目中我发现扫描码的兼容性更好特别是在游戏自动化场景中。因为扫描码直接对应物理按键不受键盘布局影响。比如在法语键盘上虚拟键码可能会产生不同的字符但扫描码始终对应同一个物理按键。2. 封装SendInput函数SendInput函数是Windows输入模拟的核心API它允许我们发送键盘和鼠标事件。完整的封装需要考虑以下几点输入类型(键盘/鼠标/硬件)事件标志(按下/释放等)坐标或键值时间戳和额外信息下面是一个完整的SendInput封装实现class INPUT(ctypes.Structure): class _INPUT(ctypes.Union): _fields_ [ (mi, MOUSEINPUT), (ki, KEYBDINPUT) ] _anonymous_ (_input,) _fields_ [ (type, wintypes.DWORD), (_input, _INPUT) ] def send_input(*inputs): n_inputs len(inputs) ptr (INPUT * n_inputs)(*inputs) return ctypes.windll.user32.SendInput( n_inputs, ptr, ctypes.sizeof(INPUT) )我在实际使用中发现SendInput对连续事件的执行速度非常快可以达到毫秒级响应。但在某些游戏中过快的输入可能会被忽略这时需要适当添加延迟。3. 键盘输入实现细节键盘输入需要考虑多种场景普通按键、组合键、长按和快速连击。下面是一个完整的键盘类实现class Keyboard: staticmethod def press(key_code, use_scan_codeFalse): flags 0x0008 if use_scan_code else 0 input_struct INPUT( type1, _inputINPUT._INPUT( kiKEYBDINPUT( wVk0 if use_scan_code else key_code, wScankey_code if use_scan_code else 0, dwFlagsflags, time0, dwExtraInfoNone ) ) ) return send_input(input_struct) staticmethod def release(key_code, use_scan_codeFalse): flags 0x0002 | (0x0008 if use_scan_code else 0) input_struct INPUT( type1, _inputINPUT._INPUT( kiKEYBDINPUT( wVk0 if use_scan_code else key_code, wScankey_code if use_scan_code else 0, dwFlagsflags, time0, dwExtraInfoNone ) ) ) return send_input(input_struct) staticmethod def press_and_release(key_code, delay0.1, use_scan_codeFalse): Keyboard.press(key_code, use_scan_code) time.sleep(delay) Keyboard.release(key_code, use_scan_code)在游戏自动化中我强烈建议使用扫描码而非虚拟键码。比如在《魔兽世界》中使用扫描码可以确保技能快捷键在各种键盘布局下都能正确触发。4. 鼠标输入实现细节鼠标操作比键盘更复杂需要考虑移动、点击、滚轮等多种操作。以下是鼠标类的完整实现class Mouse: staticmethod def move(x, y, absoluteTrue): flags 0x0001 | (0x8000 if absolute else 0) if absolute: # 将屏幕坐标转换为绝对坐标(0-65535) screen_width ctypes.windll.user32.GetSystemMetrics(0) screen_height ctypes.windll.user32.GetSystemMetrics(1) x int(x * 65535 / screen_width) y int(y * 65535 / screen_height) input_struct INPUT( type0, _inputINPUT._INPUT( miMOUSEINPUT( dxx, dyy, mouseData0, dwFlagsflags, time0, dwExtraInfoNone ) ) ) return send_input(input_struct) staticmethod def click(buttonleft, doubleFalse): down_flag, up_flag { left: (0x0002, 0x0004), right: (0x0008, 0x0010), middle: (0x0020, 0x0040) }.get(button.lower(), (0x0002, 0x0004)) events [] for _ in range(2 if double else 1): events.append(INPUT( type0, _inputINPUT._INPUT( miMOUSEINPUT( dx0, dy0, mouseData0, dwFlagsdown_flag, time0, dwExtraInfoNone ) ) )) events.append(INPUT( type0, _inputINPUT._INPUT( miMOUSEINPUT( dx0, dy0, mouseData0, dwFlagsup_flag, time0, dwExtraInfoNone ) ) )) return send_input(*events)在FPS游戏自动化测试中我发现绝对坐标移动比相对移动更精确。但要注意某些游戏(如CS:GO)会禁用绝对坐标移动这时需要使用相对移动并计算合适的移动量。5. 游戏自动化实战技巧在游戏自动化中有几个常见问题需要注意输入延迟某些游戏会故意延迟处理快速输入。解决方案是调整输入间隔通常在50-100ms之间。输入丢失高频输入可能导致部分事件被丢弃。可以通过分批发送输入或使用多线程解决。反作弊检测许多游戏会检测自动化工具。应对方法包括随机化输入间隔添加微小移动偏移使用更自然的输入模式下面是一个游戏自动化的完整示例def automate_game_actions(): # 移动到指定位置并攻击 Mouse.move(800, 600) # 屏幕中央 Mouse.click(right) # 右键攻击 # 使用技能快捷键(假设1-4是技能键) for skill_key in [0x02, 0x03, 0x04, 0x05]: # 1-4的扫描码 Keyboard.press_and_release(skill_key, use_scan_codeTrue) time.sleep(0.3) # 技能冷却时间 # 移动躲避攻击 for _ in range(5): Mouse.move(700, 600, absoluteFalse) # 向左移动 time.sleep(0.1)在MMORPG自动化测试中我通常会建立一个动作序列库包含各种战斗组合和移动模式。这样可以更真实地模拟玩家行为减少被检测的风险。6. 高级应用Unicode输入和特殊操作除了基本输入SendInput还支持一些高级功能Unicode输入可以直接发送Unicode字符适用于需要输入中文等非ASCII字符的场景。def send_unicode(char): char_code ord(char) # 按下 send_input(INPUT( type1, _inputINPUT._INPUT( kiKEYBDINPUT( wVk0, wScanchar_code, dwFlags0x0004, # KEYEVENTF_UNICODE time0, dwExtraInfoNone ) ) )) # 释放 send_input(INPUT( type1, _inputINPUT._INPUT( kiKEYBDINPUT( wVk0, wScanchar_code, dwFlags0x0004 | 0x0002, # KEYEVENTF_UNICODE | KEYEVENTF_KEYUP time0, dwExtraInfoNone ) ) ))鼠标滚轮可以模拟滚轮操作适用于需要滚动的界面。def mouse_scroll(clicks1, verticalTrue): flags 0x0800 if vertical else 0x1000 return send_input(INPUT( type0, _inputINPUT._INPUT( miMOUSEINPUT( dx0, dy0, mouseDataclicks * 120, # 每个单位120 dwFlagsflags, time0, dwExtraInfoNone ) ) ))组合键通过发送多个输入事件实现组合键效果。def send_hotkey(key, modifierctrl): modifier_code { ctrl: 0x11, alt: 0x12, shift: 0x10 }.get(modifier.lower(), 0x11) # 按下修饰键 Keyboard.press(modifier_code) time.sleep(0.1) # 按下主键 Keyboard.press(key) time.sleep(0.1) # 释放主键 Keyboard.release(key) time.sleep(0.1) # 释放修饰键 Keyboard.release(modifier_code)在办公自动化项目中我经常使用这些高级功能来自动填写表格或操作复杂界面。特别是Unicode输入可以完美解决中文输入的问题。7. 性能优化和错误处理在实际使用中SendInput可能会遇到各种问题。良好的错误处理机制至关重要def safe_send_input(*inputs): try: result send_input(*inputs) if result ! len(inputs): error_code ctypes.get_last_error() if error_code ! 0: raise ctypes.WinError(error_code) return False return True except Exception as e: print(fSendInput failed: {str(e)}) return False性能方面有几点优化建议批量发送将多个输入事件打包成一次SendInput调用减少系统调用开销。减少休眠在保证可靠性的前提下尽量减少time.sleep的使用。预分配内存对于高频输入可以预分配INPUT数组内存。下面是一个优化后的连续移动示例def smooth_movement(start_x, start_y, end_x, end_y, steps10, duration1.0): inputs [] step_delay duration / steps for i in range(steps 1): x start_x (end_x - start_x) * i / steps y start_y (end_y - start_y) * i / steps inputs.append(INPUT( type0, _inputINPUT._INPUT( miMOUSEINPUT( dxint(x), dyint(y), mouseData0, dwFlags0x8001, # MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE time0, dwExtraInfoNone ) ) )) # 分批发送每批最多10个输入 for i in range(0, len(inputs), 10): batch inputs[i:i10] safe_send_input(*batch) time.sleep(step_delay * len(batch))在自动化测试框架中我通常会建立一个输入队列系统将输入请求放入队列然后由专门的线程按最优方式发送。这样可以实现高效的输入调度同时避免阻塞主线程。8. 兼容性问题和解决方案不同游戏和应用程序对输入的处理方式各不相同常见问题及解决方案如下DirectInput游戏许多游戏使用DirectInput而非标准Windows输入。解决方法使用扫描码而非虚拟键码增加输入延迟配合窗口焦点管理防作弊系统如BattlEye、EasyAntiCheat等。应对策略降低输入频率添加随机变化避免完美重复模式窗口焦点问题某些操作需要窗口处于焦点状态。解决方案使用SetForegroundWindow添加焦点检查备用输入方案下面是一个兼容性更强的输入示例def robust_key_press(key_code, window_titleNone): if window_title: # 尝试将窗口置前 hwnd ctypes.windll.user32.FindWindowW(None, window_title) if hwnd: ctypes.windll.user32.SetForegroundWindow(hwnd) time.sleep(0.1) # 尝试多种输入方式 for use_scan in [True, False]: if Keyboard.press(key_code, use_scan): time.sleep(0.05) Keyboard.release(key_code, use_scan) return True return False在商业自动化工具开发中我通常会实现多种输入策略然后根据应用程序类型自动选择最合适的方案。这种自适应机制可以显著提高工具的通用性。