简介:这是一份面向C#开发者与Windows桌面应用学习者的键盘鼠标事件模拟工具包,基于开源项目Windows Input Simulator深度扩展,重点补充了完整的鼠标模拟功能,适用于自动化测试、远程控制、游戏辅助及UI交互演示等场景。资源共30个文件,包含12个核心C#源码文件(如MOUSEINPUT.cs、KEYBDINPUT.cs、InputSimulator.cs等)、4个动态链接库(dll)用于直接引用、4个调试符号文件(pdb)及配套的license.txt和readme.txt说明文档,整体压缩包仅93KB,轻量易集成。目前已有1180人学习下载,体现了其在中小型自动化需求中的实用价值。读者可直接编译运行示例代码,快速掌握底层Windows输入消息(INPUT结构体、VirtualKeyCode、MouseFlag等)的构造与投递机制,并复用已封装的高内聚模拟接口,避免重复实现Win32 API调用细节。
1. C#键盘鼠标模拟:不是发键事件,而是绕过UI线程的真实输入注入
你有没有试过用SendKeys.Send()控制 Excel 自动填表,结果在远程桌面断开后脚本就卡死?或者用keybd_event发送 Ctrl+C,却在无焦点窗口里毫无反应?这不是代码写错了——是绝大多数“模拟输入”方案根本没触达 Windows 输入子系统的底层。Windows Input Simulator这个 C# 库干了一件很实在的事:它不走消息队列,不依赖窗口句柄,也不管当前焦点在哪,直接调用SendInputAPI 向系统输入缓冲区注入原始硬件级输入数据。这意味着它能稳定触发快捷键、穿透 UAC 提权窗口、在锁屏界面下(需配合服务进程)完成预设操作,甚至能绕过某些游戏反作弊对PostMessage的拦截。它适合自动化测试工程师做 UI 回归验证、RPA 开发者构建跨应用流程、以及需要无人值守批量操作的运维场景。如果你的项目要求“按下去就真按下去”,而不是“发个消息假装按了”,那这个库不是可选项,而是必选项。
2. 为什么选 SendInput 而不是 keybd_event 或 SendMessage?
2.1 三类输入模拟的本质差异:从“发消息”到“造事件”
Windows 提供了至少五种输入模拟路径,但真正能被系统视为“真实用户行为”的只有SendInput。我们来拆解三类主流方案的底层行为:
SendMessage/PostMessage:向目标窗口的消息队列投递WM_KEYDOWN等消息。问题在于:它完全绕过输入栈(Raw Input Stack),不经过键盘布局转换、不触发热键注册(RegisterHotKey)、不被GetAsyncKeyState捕获,更不会点亮 CapsLock 指示灯。很多现代应用(尤其是 Electron、WPF 或启用 DWM 合成的程序)会直接忽略这类“伪消息”。keybd_event/mouse_event:这是 WinAPI 早期接口,虽比 SendMessage 更底层,但已被微软标记为deprecated。它内部仍会走部分兼容层,在 Windows 10/11 上易受 UIPI(User Interface Privilege Isolation)限制,高权限进程无法向低权限窗口注入事件,且无法设置输入设备的dwExtraInfo字段,导致某些安全软件将其识别为自动化工具特征。SendInput:它是目前唯一被微软明确推荐、且与真实键盘/鼠标驱动同级的接口。它把输入构造成INPUT结构体数组,交由win32k.sys内核模块统一调度,全程参与扫描码翻译、键盘状态维护(如 Shift 键是否按下)、焦点窗口判定、甚至触控笔压感模拟。这才是“让系统相信你真的动了手指”。
提示:
Windows Input Simulator封装的正是SendInput,而非keybd_event。它的SimulateKeyPress()方法最终调用的是SendInput(1, &input, sizeof(INPUT)),参数input.type = INPUT_KEYBOARD,结构体字段全部按微软文档填充,包括wVk(虚拟键码)、dwFlags(KEYUP/KEYDOWN/LLHOOK)、time(时间戳)和dwExtraInfo(可用于携带自定义上下文标识)。
2.2 Input Simulator 的核心设计:面向对象封装 + 线程安全抽象
该库没有采用裸 API 调用风格,而是构建了一套清晰的领域模型:
InputSimulator是根对象,持有一个InputDeviceState实例,用于跟踪当前键盘修饰键(Ctrl/Shift/Alt/Win)状态;- 所有输入操作被拆分为原子动作:
KeyDown(),KeyUp(),KeyPress(),MouseMove(),MouseClick(),每个方法都返回InputSimulator自身,支持链式调用; - 鼠标操作默认使用相对坐标(
MOUSEEVENTF_MOVE),避免因 DPI 缩放或多显示器导致的定位偏移;若需绝对坐标,必须显式调用MoveMouseTo()并启用MOUSEEVENTF_ABSOLUTE标志; - 所有方法内部加了
lock (_syncRoot),确保多线程并发调用时输入事件顺序不乱——这点在自动化测试中极其关键,否则 Ctrl+A 后紧跟 Delete 可能变成 A+Delete。
下面这段代码演示了如何模拟一个标准的“全选 → 复制 → 切换窗口 → 粘贴”流程:
var simulator = new InputSimulator(); // 全选:Ctrl+A simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_A); // 复制:Ctrl+C simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_C); // 等待剪贴板就绪(实际项目中建议加超时重试) Thread.Sleep(100); // Alt+Tab 切换到上一个窗口 simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.TAB); // 粘贴:Ctrl+V simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_V);这段代码的关键不在语法糖,而在于ModifiedKeyStroke的实现逻辑:它先KeyDown修饰键(Ctrl),再KeyDown功能键(A),然后KeyUp功能键,最后KeyUp修饰键。顺序错误(比如先松 Ctrl 再松 A)会导致系统认为“Ctrl 已释放”,从而触发单字母 A 输入而非 Ctrl+A。Input Simulator把这个易错细节封装死了,开发者无需操心键序。
2.3 与 .NET 原生方案对比:为什么不用 SendKeys 或 Windows Forms?
.NET Framework 提供了System.Windows.Forms.SendKeys,表面看更轻量。但它有硬伤:
- 仅限于 WinForms 应用内使用,无法在控制台、WPF 或 .NET Core/.NET 5+ 无 UI 环境中工作(会抛出
InvalidOperationException: SendKeys cannot run inside this application type); - 内部仍基于
keybd_event,在 Windows 10 1809+ 版本中被系统策略限制,默认禁用; - 不支持鼠标模拟,功能残缺。
而Windows Input Simulator是纯 .NET Standard 2.0 库,无 UI 依赖,可直接在dotnet new console项目中dotnet add package WindowsInput引入,编译后生成单文件可执行程序,部署零依赖。某公司曾用它构建了一个运行在 Windows Server Core 容器中的自动化报表导出服务——没有桌面会话,没有 Explorer 进程,只有后台服务持续调用 Excel COM 接口并用 Input Simulator 模拟点击“另存为”对话框按钮,稳定运行 14 个月无一次输入失败。
3. 快速上手:从 NuGet 安装到第一个可靠按键
3.1 安装与基础引用:三行代码启动真实输入
该项目已发布至 NuGet,最新稳定版为WindowsInput 10.6.0(截至 2024 年中)。安装方式极简:
# .NET CLI 方式(推荐,兼容所有 SDK 版本) dotnet add package WindowsInput --version 10.6.0 # 或在 .csproj 文件中手动添加 <PackageReference Include="WindowsInput" Version="10.6.0" />安装后无需任何初始化配置。在代码中引入命名空间并实例化即可:
using WindowsInput; using WindowsInput.Native; class Program { static void Main() { var sim = new InputSimulator(); // 模拟按 F5 刷新(常见于浏览器或文件管理器) sim.Keyboard.KeyPress(VirtualKeyCode.F5); // 等待 1.5 秒,让页面加载 Thread.Sleep(1500); // 模拟 Alt+F4 关闭当前窗口 sim.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.F4); } }这段代码之所以“可靠”,是因为它避开了两个经典陷阱:
①F5是功能键,其虚拟键码为VK_F5(0x74),而非字符 'F' 的 ASCII 码,SendKeys.Send("{F5}")在某些环境下会失效;
②Alt+F4是组合键,必须用ModifiedKeyStroke而非两次独立KeyPress,否则系统收不到“Alt 按下期间 F4 按下”的时序信号。
3.2 键盘操作详解:虚拟键码、修饰键与组合键链式调用
VirtualKeyCode枚举覆盖了全部 256 个标准虚拟键,包括VK_LWIN(左 Win 键)、VK_APPS(菜单键)、VK_BROWSER_BACK(浏览器返回)等特殊键。使用时务必注意:
- 字母键(A-Z)对应
VK_A~VK_Z,不是 ASCII 码。想输入小写 a,用VK_A即可(系统根据当前 Shift 状态自动处理大小写); - 数字键(0-9)对应
VK_0~VK_9,位于主键盘区,而非小键盘区(小键盘数字键是VK_NUMPAD0~VK_NUMPAD9); - 小键盘
Enter是VK_RETURN,主键盘Enter是VK_SEPARATOR(实际中两者常通用,但严格来说不同)。
修饰键组合推荐用ModifiedKeyStroke,它接受多个VirtualKeyCode参数:
// Ctrl+Shift+Esc:打开任务管理器(比 Ctrl+Alt+Del 更稳定) sim.Keyboard.ModifiedKeyStroke( VirtualKeyCode.CONTROL, VirtualKeyCode.SHIFT, VirtualKeyCode.ESCAPE); // Win+R:打开运行对话框 sim.Keyboard.ModifiedKeyStroke( VirtualKeyCode.LWIN, VirtualKeyCode.VK_R); // Ctrl+Alt+Delete:此组合被系统保留,SendInput 无法模拟(安全机制) // ❌ 下面这行无效,会静默失败 sim.Keyboard.ModifiedKeyStroke( VirtualKeyCode.CONTROL, VirtualKeyCode.MENU, VirtualKeyCode.DELETE);注意:
Ctrl+Alt+Delete是 Secure Attention Sequence(SAS),由 Winlogon 专属捕获,任何用户态 API(包括SendInput)均无法触发。这是 Windows 安全基石,不可绕过。若你的自动化流程依赖此组合,请改用ShutdownBlockReasonCreate+ExitWindowsEx等替代方案。
3.3 鼠标操作实战:绝对坐标、相对移动与精准点击
鼠标模拟分两类场景:
- 相对移动(Relative Move):适用于拖拽、画图、游戏视角控制,位移单位为“微移”(mickeys),与 DPI 无关;
- 绝对坐标(Absolute Move):适用于点击固定按钮(如“确定”、“下一步”),需将屏幕坐标映射为
0~65535范围。
绝对坐标点击(推荐用于 GUI 自动化)
// 获取屏幕尺寸(以像素为单位) var screenWidth = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width; var screenHeight = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height; // 将像素坐标 (x=500, y=300) 转为 SendInput 所需的 0~65535 范围 int xAbs = (int)(500.0 / screenWidth * 65535); int yAbs = (int)(300.0 / screenHeight * 65535); // 移动鼠标到绝对位置并左键单击 sim.Mouse.MoveMouseTo(xAbs, yAbs); sim.Mouse.LeftButtonClick();关键点:MoveMouseTo()内部自动设置MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE标志,并填充dx/dy字段。若忘记调用MoveMouseTo()直接LeftButtonClick(),则点击发生在当前鼠标位置——这在多步骤流程中极易出错。
相对移动(适用于游戏或绘图脚本)
// 向右移动 100 微移,向下移动 50 微移 sim.Mouse.MoveMouseBy(100, 50); // 按住左键拖拽:先按下,再移动,再释放 sim.Mouse.LeftButtonDown(); sim.Mouse.MoveMouseBy(200, 0); // 向右拖 200 微移 sim.Mouse.LeftButtonUp();MoveMouseBy()使用MOUSEEVENTF_MOVE标志,dx/dy值直接传入,无需缩放。微移单位与鼠标 DPI 设置相关,但对自动化而言,只要保持同一设备,位移比例恒定即可。
4. 避坑指南:五个血泪经验换来的输入失败排查清单
4.1 现象:按键无响应,但代码无异常
原因:目标进程运行在更高完整性级别(High Integrity Level),而当前程序是中等或低级别,受 UIPI 限制,SendInput被系统静默丢弃。
解决:以管理员身份运行你的程序(右键 → “以管理员身份运行”),或在项目属性中添加app.manifest并设置<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />。注意:uiAccess="true"需程序签名且安装到C:\Windows\System32,普通项目无需启用。
4.2 现象:鼠标点击位置严重偏移(如想点 (100,100),实际点到 (200,200))
原因:未考虑 DPI 缩放。当系统 DPI 设置为 125% 或 150% 时,Screen.PrimaryScreen.Bounds返回的是逻辑像素,而SendInput的绝对坐标需物理像素。
解决:改用GetDpiForWindowAPI 获取当前 DPI 缩放比,或更简单——放弃绝对坐标,改用MoveMouseBy()+ 图像识别定位(如 OpenCV 找按钮模板),或使用FindWindow+ClientToScreen获取窗口内控件真实坐标。
4.3 现象:ModifiedKeyStroke(VK_CONTROL, VK_C)触发了浏览器快捷键(如 Ctrl+C 复制网页文本),而非目标应用的复制
原因:SendInput注入的事件被前台窗口捕获,但若目标窗口未激活(如最小化或被遮挡),系统会将事件路由给当前活动窗口。
解决:在发送前强制激活目标窗口。使用SetForegroundWindow(hWnd)(需先通过FindWindow获取句柄),并在调用后Thread.Sleep(100)等待窗口真正获得焦点。某导师曾因此调试三天,最后发现是SetForegroundWindow返回true但窗口实际未激活,加Sleep后问题消失。
4.4 现象:连续快速按键(如模拟打字)时出现漏字或重复
原因:SendInput虽快,但仍有最小间隔要求。若两次KeyPress间隔 < 20ms,部分应用(尤其是 Java Swing)会合并事件。
解决:在链式调用中插入可控延迟。不要用Thread.Sleep(50)这种粗粒度等待,而是用InputSimulator的SleepMilliseconds属性(需继承自InputSimulator并重写ExecuteInput方法),或更稳妥地——对每个字符调用KeyPress后Sleep(30)。实测 30ms 是多数应用的稳定阈值。
4.5 现象:在远程桌面(RDP)会话中,本地运行的模拟程序完全无效
原因:RDP 会话默认禁用SendInput,因其被视为潜在安全风险。本地会话的输入事件不会转发到远程会话中。
解决:必须在远程会话内部运行模拟程序(即 RDP 登录后,在远程桌面上启动你的 .exe)。若需无人值守,可配置 Windows 服务在交互式会话中启动,或使用psexec -i命令在指定会话中执行。切记:本地机器上的SendInput对远程桌面内的进程无效,这是架构限制,非 Bug。
5. 进阶技巧:构建可验证、可回滚、带日志的生产级输入流
5.1 输入操作的原子性验证:用 GetAsyncKeyState 确认键已落下
SendInput成功返回只代表系统接收了事件,不代表目标应用已处理。为确保可靠性,可在发送后立即验证键状态:
public static bool IsKeyDown(VirtualKeyCode vk) { const int KEY_PRESSED = 0x8000; return (NativeMethods.GetAsyncKeyState((int)vk) & KEY_PRESSED) != 0; } // 使用示例:发送 Ctrl 后验证 sim.Keyboard.KeyDown(VirtualKeyCode.CONTROL); Thread.Sleep(10); // 给系统一点处理时间 if (!IsKeyDown(VirtualKeyCode.CONTROL)) { throw new InvalidOperationException("Ctrl key not detected after SendInput"); }GetAsyncKeyState读取的是内核维护的键盘状态位,比轮询GetKeyState更及时,且不受线程焦点影响。这是某跨平台系统在金融交易终端自动化中强制加入的校验环节——任何按键未确认落下,立即重试三次,否则终止流程并告警。
5.2 构建可回滚的输入事务:记录状态快照与逆向操作
真实自动化不是单向执行,而是“执行 → 验证 → 失败则回退”。我们为InputSimulator扩展一个事务管理器:
public class InputTransaction { private readonly List<(Action undo, string desc)> _undoStack = new(); private readonly InputSimulator _sim; public InputTransaction(InputSimulator sim) => _sim = sim; public void KeyDown(VirtualKeyCode vk) { _sim.Keyboard.KeyDown(vk); _undoStack.Add((() => _sim.Keyboard.KeyUp(vk), $"KeyUp {vk}")); } public void MouseClick(int x, int y) { var prevPos = GetMousePosition(); // 自定义方法,调用 GetCursorPos _sim.Mouse.MoveMouseTo(x, y); _sim.Mouse.LeftButtonClick(); _undoStack.Add((() => { _sim.Mouse.MoveMouseTo(prevPos.X, prevPos.Y); _sim.Mouse.LeftButtonClick(); }, $"Restore mouse to {prevPos}")); } public void Rollback() { foreach (var (undo, desc) in _undoStack.AsEnumerable().Reverse()) { try { undo(); } catch (Exception ex) { Console.WriteLine($"Undo failed: {desc} - {ex.Message}"); } } _undoStack.Clear(); } }使用时:
using var tx = new InputTransaction(sim); try { tx.KeyDown(VirtualKeyCode.CONTROL); tx.KeyDown(VirtualKeyCode.VK_A); tx.KeyUp(VirtualKeyCode.VK_A); tx.KeyUp(VirtualKeyCode.CONTROL); // ... 后续操作 } catch { tx.Rollback(); // 自动恢复 Ctrl 和 A 的释放状态 throw; }这种模式让输入操作具备了数据库事务的 ACID 特性(原子性、一致性),避免因异常导致键盘卡在 Ctrl 按下状态,进而影响后续所有操作。
5.3 生产环境日志:记录每次输入的上下文与耗时
在无人值守服务中,光有成功/失败不够,需知道“谁在什么时候按了什么”。我们封装一个带日志的模拟器:
public class LoggedInputSimulator : InputSimulator { private readonly ILogger _logger; public LoggedInputSimulator(ILogger logger) => _logger = logger; public override void KeyboardKeyPress(VirtualKeyCode keyCode) { var sw = Stopwatch.StartNew(); base.KeyboardKeyPress(keyCode); sw.Stop(); _logger.LogInformation("KEY_PRESS: {KeyCode} took {Ms}ms", keyCode, sw.ElapsedMilliseconds); } public void MouseClickAt(string context, int x, int y) { var sw = Stopwatch.StartNew(); Mouse.MoveMouseTo(x, y); Mouse.LeftButtonClick(); sw.Stop(); _logger.LogInformation("MOUSE_CLICK[{Context}]: ({X},{Y}) took {Ms}ms", context, x, y, sw.ElapsedMilliseconds); } }配合 Serilog 或 NLog,日志输出形如:[INF] KEY_PRESS: F5 took 2ms[INF] MOUSE_CLICK[LoginButton]: (842,511) took 8ms
这些日志可接入 ELK 或 Grafana,当某类操作平均耗时突增 50%,即刻触发告警——这往往是目标应用卡顿或 UI 响应变慢的早期信号。
从那以后我每次写自动化脚本,都强制走一遍“输入 → 验证 → 日志 → 回滚”四步闭环,哪怕只是临时调试。因为键盘鼠标模拟不是魔法,它是精密的系统交互,少一步验证,就多一分玄学翻车的风险。希望帮到你。
本文还有配套的精品资源,点击获取