
简介这是一份面向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发送 CtrlC却在无焦点窗口里毫无反应这不是代码写错了——是绝大多数“模拟输入”方案根本没触达 Windows 输入子系统的底层。Windows Input Simulator这个 C# 库干了一件很实在的事它不走消息队列不依赖窗口句柄也不管当前焦点在哪直接调用SendInputAPI 向系统输入缓冲区注入原始硬件级输入数据。这意味着它能稳定触发快捷键、穿透 UAC 提权窗口、在锁屏界面下需配合服务进程完成预设操作甚至能绕过某些游戏反作弊对PostMessage的拦截。它适合自动化测试工程师做 UI 回归验证、RPA 开发者构建跨应用流程、以及需要无人值守批量操作的运维场景。如果你的项目要求“按下去就真按下去”而不是“发个消息假装按了”那这个库不是可选项而是必选项。2. 为什么选 SendInput 而不是 keybd_event 或 SendMessage2.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 上易受 UIPIUser 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虚拟键码、dwFlagsKEYUP/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)确保多线程并发调用时输入事件顺序不乱——这点在自动化测试中极其关键否则 CtrlA 后紧跟 Delete 可能变成 ADelete。下面这段代码演示了如何模拟一个标准的“全选 → 复制 → 切换窗口 → 粘贴”流程var simulator new InputSimulator(); // 全选CtrlA simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_A); // 复制CtrlC simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_C); // 等待剪贴板就绪实际项目中建议加超时重试 Thread.Sleep(100); // AltTab 切换到上一个窗口 simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.TAB); // 粘贴CtrlV simulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_V);这段代码的关键不在语法糖而在于ModifiedKeyStroke的实现逻辑它先KeyDown修饰键Ctrl再KeyDown功能键A然后KeyUp功能键最后KeyUp修饰键。顺序错误比如先松 Ctrl 再松 A会导致系统认为“Ctrl 已释放”从而触发单字母 A 输入而非 CtrlA。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 IncludeWindowsInput Version10.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); // 模拟 AltF4 关闭当前窗口 sim.Keyboard.ModifiedKeyStroke(VirtualKeyCode.MENU, VirtualKeyCode.F4); } }这段代码之所以“可靠”是因为它避开了两个经典陷阱①F5是功能键其虚拟键码为VK_F50x74而非字符 F 的 ASCII 码SendKeys.Send({F5})在某些环境下会失效②AltF4是组合键必须用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参数// CtrlShiftEsc打开任务管理器比 CtrlAltDel 更稳定 sim.Keyboard.ModifiedKeyStroke( VirtualKeyCode.CONTROL, VirtualKeyCode.SHIFT, VirtualKeyCode.ESCAPE); // WinR打开运行对话框 sim.Keyboard.ModifiedKeyStroke( VirtualKeyCode.LWIN, VirtualKeyCode.VK_R); // CtrlAltDelete此组合被系统保留SendInput 无法模拟安全机制 // ❌ 下面这行无效会静默失败 sim.Keyboard.ModifiedKeyStroke( VirtualKeyCode.CONTROL, VirtualKeyCode.MENU, VirtualKeyCode.DELETE);注意CtrlAltDelete是 Secure Attention SequenceSAS由 Winlogon 专属捕获任何用户态 API包括SendInput均无法触发。这是 Windows 安全基石不可绕过。若你的自动化流程依赖此组合请改用ShutdownBlockReasonCreateExitWindowsEx等替代方案。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; // 将像素坐标 (x500, y300) 转为 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 levelrequireAdministrator uiAccessfalse /。注意uiAccesstrue需程序签名且安装到C:\Windows\System32普通项目无需启用。4.2 现象鼠标点击位置严重偏移如想点 (100,100)实际点到 (200,200)原因未考虑 DPI 缩放。当系统 DPI 设置为 125% 或 150% 时Screen.PrimaryScreen.Bounds返回的是逻辑像素而SendInput的绝对坐标需物理像素。解决改用GetDpiForWindowAPI 获取当前 DPI 缩放比或更简单——放弃绝对坐标改用MoveMouseBy() 图像识别定位如 OpenCV 找按钮模板或使用FindWindowClientToScreen获取窗口内控件真实坐标。4.3 现象ModifiedKeyStroke(VK_CONTROL, VK_C)触发了浏览器快捷键如 CtrlC 复制网页文本而非目标应用的复制原因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 响应变慢的早期信号。从那以后我每次写自动化脚本都强制走一遍“输入 → 验证 → 日志 → 回滚”四步闭环哪怕只是临时调试。因为键盘鼠标模拟不是魔法它是精密的系统交互少一步验证就多一分玄学翻车的风险。希望帮到你。本文还有配套的精品资源点击获取