资讯动态

C# 键盘鼠标模拟实战:Windows Input Simulator 源码拆解与避坑指南

发布时间:2026/10/23 12:12:40 来源:尧图企业网站定制
简介这份资源面向需要在 Windows 平台实现键鼠自动化操作的 C# 开发者基于开源的 Windows Input Simulator 扩展而来在原有键盘事件模拟的基础上补充了鼠标模拟事件可用于自动化测试、桌面辅助工具、批量操作脚本等场景适合具备一定 C# 基础、希望快速集成输入模拟能力的开发者参考使用。压缩包共 30 个文件约 93KB以 cs 源码文件为主配合 dll 程序集、pdb 调试符号、xml 文档及 csproj 工程文件涵盖键盘与鼠标输入结构定义、虚拟键码枚举、标志位枚举等核心模块工程结构完整可直接编译运行。目前已有 1181 人学习下载。通过阅读源码读者能够理解 Windows 底层输入事件的封装方式掌握键盘按键、鼠标移动与点击等模拟逻辑的实现思路并在此基础上按需扩展自定义输入行为同时遵循 Microsoft Public License (Ms-PL) 协议便于在合规前提下用于个人或商业项目。1. 键盘鼠标模拟的 C# 利器Windows Input Simulator 能帮你做什么如果你写过自动化测试、远程控制、游戏辅助或者批量操作工具大概率绕不开一个需求让程序代替人去敲键盘、点鼠标。用 Win32 的SendInput、keybd_event、mouse_event当然能做但 P/Invoke 的签名、结构体对齐、扫描码和虚拟键码的转换写一次错一次调试起来全是玄学。Windows Input Simulator 就是来解决这个问题的它把 Win32 底层输入 API 封装成一套干净的 C# 接口让你用SimulateKeyPress、SimulateTextEntry、SimulateMouseClick这样的方法就能完成键盘鼠标模拟。这份资源适合做桌面自动化、UI 测试、辅助工具、批量录入的开发者也适合想搞明白 Windows 输入模拟底层原理的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲一遍。2. 项目定位与技术选型为什么不用原生 SendInput2.1 它到底封装了什么Windows 的输入模拟最终都落到user32.dll的SendInput上。这个函数接收一个INPUT结构体数组每个结构体描述一次键盘或鼠标事件。问题在于INPUT是个联合体union在 C# 里要用StructLayout(LayoutKind.Explicit)配合FieldOffset手动布局键盘事件还要区分KEYBDINPUT、MOUSEINPUT、HARDWAREINPUT三种类型。Windows Input Simulator 把这些全部封装在内部对外只暴露IInputSimulator接口。它的核心能力包括键盘单键按下/释放、组合键CtrlC 这类、文本直接输入、扫描码模式鼠标移动、点击左/右/中、滚轮、按下/释放分离扩展支持传入自定义的键盘/鼠标模拟实现方便做单元测试技术栈上它是纯 C# 类库目标框架覆盖 .NET Framework 和 .NET Standard没有第三方依赖直接引用 DLL 或源码即可。这意味着你可以在 WinForms、WPF、控制台程序、甚至 Unity 的桌面构建里用它。2.2 为什么值得用而不是自己写自己写 P/Invoke 的坑在于INPUT结构体在不同位数进程下大小不同32 位是 28 字节64 位是 40 字节SendInput的cbSize参数传错就直接静默失败返回 0 你都不知道为什么。另外键盘事件里wVk虚拟键码和wScan扫描码的取舍、KEYEVENTF_UNICODE标志的使用都是血泪经验堆出来的。这个库帮你把这些细节都处理好了还提供了KeyCode枚举把VK_开头的常量变成可读的枚举值。提示如果你的目标进程以管理员权限运行而你的模拟程序不是SendInput会失败。这是 UIPI用户界面特权隔离机制不是库的问题。3. 快速上手从引用到跑通第一个模拟3.1 获取与引用这份资源是源码包拿到后有两种用法。第一种是直接把WindowsInput项目加入你的解决方案添加项目引用第二种是编译成 DLL 后引用。用 .NET CLI 的话在解决方案目录下执行# 进入源码目录编译类库 dotnet build WindowsInput/WindowsInput.csproj -c Release # 编译产物在 bin/Release 下按目标框架分目录编译完成后在你的项目里添加引用。如果是 .NET Core / .NET 5 项目直接引用 csproj 更省事!-- 在你的 csproj 里添加项目引用 -- ItemGroup ProjectReference Include..\WindowsInput\WindowsInput.csproj / /ItemGroup参数说明-c Release指定发布配置产物会做优化如果你要兼容老项目注意看WindowsInput.csproj里的TargetFramework常见做法是同时输出net45和netstandard2.0前者给 .NET Framework后者给 .NET Core 系。3.2 第一个可运行示例引用好之后写一个控制台程序模拟按下 WinR 打开运行框输入 notepad 并回车using WindowsInput; using WindowsInput.Native; class Program { static void Main() { // 创建输入模拟器实例这是整个库的入口 var sim new InputSimulator(); // 按下 Win 键 sim.Keyboard.KeyDown(VirtualKeyCode.LWIN); // 按下 R 键此时 Win 还按着形成 WinR sim.Keyboard.KeyPress(VirtualKeyCode.VK_R); // 释放 Win 键 sim.Keyboard.KeyUp(VirtualKeyCode.LWIN); // 等待运行框弹出实际项目里建议用更稳的等待方式 System.Threading.Thread.Sleep(500); // 直接输入文本内部会处理字符到按键的映射 sim.Keyboard.TextEntry(notepad); // 回车确认 sim.Keyboard.KeyPress(VirtualKeyCode.RETURN); } }逻辑说明KeyDown和KeyUp是分离的按下与释放适合组合键场景KeyPress是按下加释放的原子操作TextEntry接收字符串内部逐字符处理遇到大写字母或符号会自动配合 Shift。参数上VirtualKeyCode枚举覆盖了绝大多数键LWIN是左 Win 键VK_R是字母 RRETURN是回车。3.3 鼠标模拟示例鼠标操作同样直观下面这段模拟移动鼠标到屏幕坐标 (500, 300) 并左键单击using WindowsInput; class MouseDemo { static void Main() { var sim new InputSimulator(); // 移动到绝对坐标参数是像素值 sim.Mouse.MoveMouseTo(500, 300); // 左键单击按下释放 sim.Mouse.LeftButtonClick(); // 滚轮向下滚动参数是滚动量正数向上负数向下 sim.Mouse.Scroll(-120); } }逻辑说明MoveMouseTo内部会把像素坐标转换成SendInput要求的 0~65535 归一化坐标这一步如果自己写很容易算错。Scroll的 120 是 Windows 标准滚轮步进单位传 -120 就是向下滚一格。注意MoveMouseTo是绝对移动如果你要做相对移动用MoveMouseBy。4. 核心模块拆解一个按键事件从调用到落地的完整链路4.1 接口层与实现层的分离这个库的架构是典型的「接口 实现」分层。顶层是IInputSimulator它聚合了IKeyboardSimulator和IMouseSimulator。默认实现是InputSimulator内部持有IInputDeviceStateAdaptor用于查询按键状态和IKeyboardSimulator、IMouseSimulator的具体实例。这种设计的好处是你可以替换掉任意一层。比如写单元测试时注入一个假的IKeyboardSimulator断言「是否调用了 KeyPress」而不真的去敲键盘。常见做法是配合依赖注入容器把IInputSimulator注册为单例。// 自定义一个用于测试的键盘模拟器只记录调用不真正发送 public class FakeKeyboardSimulator : IKeyboardSimulator { public ListVirtualKeyCode PressedKeys { get; } new(); public IKeyboardSimulator KeyPress(VirtualKeyCode keyCode) { PressedKeys.Add(keyCode); // 只记录不调用 SendInput return this; } // 其余接口方法省略按需实现 public IKeyboardSimulator KeyDown(VirtualKeyCode keyCode) this; public IKeyboardSimulator KeyUp(VirtualKeyCode keyCode) this; public IKeyboardSimulator TextEntry(string text) this; // ... }参数说明IKeyboardSimulator的方法大多返回自身这是流式调用的设计可以链式写sim.Keyboard.KeyDown(A).KeyPress(B).KeyUp(A)。实现自定义模拟器时只要保证返回this就能保持链式风格。4.2 键盘模拟的内部实现键盘模拟的核心在KeyboardSimulator类。以TextEntry为例它不是简单地把字符串拆成字符逐个发送而是要处理大小写和特殊符号。内部逻辑大致是遍历每个字符判断是否需要 Shift然后发送对应的虚拟键码。// 简化后的 TextEntry 核心逻辑帮助理解实现思路 public IKeyboardSimulator TextEntry(string text) { foreach (char c in text) { // 判断字符是否需要配合 Shift 键 bool needShift char.IsUpper(c) || IsShiftRequiredSymbol(c); VirtualKeyCode key CharToVirtualKey(c); if (needShift) { // 先按下 Shift再按键最后释放 Shift _keyboard.KeyDown(VirtualKeyCode.SHIFT); _keyboard.KeyPress(key); _keyboard.KeyUp(VirtualKeyCode.SHIFT); } else { _keyboard.KeyPress(key); } } return this; }逻辑说明CharToVirtualKey负责把字符映射到虚拟键码比如 a 映射到VK_A1 映射到VK_1。IsShiftRequiredSymbol判断像!#$%这类需要 Shift 的符号。这里有个细节对于中文等非 ASCII 字符虚拟键码方式行不通需要用KEYEVENTF_UNICODE标志直接发送 Unicode 扫描码库内部对这种情况有单独分支处理。4.3 鼠标模拟的坐标转换鼠标模拟里最容易翻车的是坐标。SendInput的MOUSEINPUT结构里如果带MOUSEEVENTF_ABSOLUTE标志dx和dy是 0~65535 的归一化值映射到整个虚拟屏幕。库内部的转换逻辑是// 像素坐标转归一化坐标的核心计算 private static int ConvertToNormalized(double pixel, double screenSize) { // 65535 是 SendInput 要求的最大值加 0.5 做四舍五入 return (int)(pixel * 65535 / screenSize 0.5); } // 调用时传入屏幕宽高 int normalizedX ConvertToNormalized(x, screenWidth); int normalizedY ConvertToNormalized(y, screenHeight);参数说明screenSize要用SystemParameters.PrimaryScreenWidth或虚拟屏幕尺寸多显示器场景下要用SystemInformation.VirtualScreen的宽高否则坐标会偏。这是多屏用户最常踩的坑。4.4 扫描码模式与虚拟键码模式的区别库支持两种键盘事件发送方式虚拟键码wVk和扫描码wScan。默认用虚拟键码兼容性好大多数场景够用。但有些游戏或特殊程序只认扫描码这时需要走扫描码模式。区别在于KEYBDINPUT结构里填哪个字段以及是否设置KEYEVENTF_SCANCODE标志。模式填充字段标志位适用场景虚拟键码wVk无普通桌面程序、输入框扫描码wScanKEYEVENTF_SCANCODE部分游戏、DirectInput 程序UnicodewScanKEYEVENTF_UNICODE中文、emoji 等非 ASCII 字符注意扫描码模式需要先用MapVirtualKey把虚拟键码转成扫描码库内部封装了这个转换但如果你自己扩展别忘了这一步。5. 避坑与常见问题排查5.1 模拟没反应程序像没执行一样现象代码跑完没有任何报错但目标窗口没有任何输入。原因通常是权限不对等——目标进程以管理员身份运行而你的程序是普通权限UIPI 会拦截SendInput。解决把你的程序也以管理员身份运行或者在项目清单里加上requireAdministrator。另一个可能是目标窗口没有焦点SendInput是发给当前前台窗口的先调用SetForegroundWindow把目标窗口激活。5.2 中文输入变成乱码或没反应现象TextEntry(你好)执行后输入框里什么都没有或者出现奇怪字符。原因虚拟键码模式无法表达中文字符必须走 Unicode 模式。解决确认库版本支持 Unicode 分支或者手动构造带KEYEVENTF_UNICODE的INPUT结构。另外如果目标程序用的是 IME 输入法直接发 Unicode 可能被输入法拦截这种情况建议先切换到英文输入法状态。5.3 多显示器下鼠标点偏了现象双屏环境下MoveMouseTo(100, 100)点到了副屏而不是主屏。原因坐标归一化时用了主屏尺寸但SendInput的绝对坐标是相对整个虚拟屏幕的。解决用SystemInformation.VirtualScreen获取虚拟屏幕的宽高和偏移计算时把偏移量算进去。如果只是想在主屏操作确保传入的坐标在主屏范围内。5.4 组合键偶尔失效现象CtrlC十次里有一两次不生效。原因KeyDown(Ctrl)和KeyPress(C)之间如果间隔太短某些程序来不及响应或者KeyUp(Ctrl)发得太快目标程序还没处理完 C。解决在关键操作之间加短延时一般 10~50ms 就够。库本身不提供延时需要自己用Thread.Sleep或异步等待。血泪经验是别用 0 延时稳定性和速度要权衡。5.5 编译报错找不到 VirtualKeyCode现象引用了库但VirtualKeyCode枚举用不了。原因VirtualKeyCode在WindowsInput.Native命名空间下不在根命名空间。解决文件顶部加using WindowsInput.Native;。如果还是找不到检查引用的 DLL 版本和源码版本是否一致老版本可能枚举名不同。6. 二次开发与改造从能跑到好用6.1 封装一层自己的输入服务直接到处new InputSimulator()不是好习惯我一般会封装一个IInputService把常用操作组合成业务方法。比如「点击某个按钮」这种操作内部包含移动、等待、点击三步暴露成一个方法给上层调用。这样做的另一个好处是方便加日志和重试。public interface IInputService { void ClickAt(int x, int y, int delayMs 50); void TypeText(string text, int charDelayMs 20); } public class InputService : IInputService { private readonly IInputSimulator _sim; public InputService(IInputSimulator sim) _sim sim; public void ClickAt(int x, int y, int delayMs 50) { _sim.Mouse.MoveMouseTo(x, y); Thread.Sleep(delayMs); // 给目标程序反应时间 _sim.Mouse.LeftButtonClick(); } public void TypeText(string text, int charDelayMs 20) { foreach (char c in text) { _sim.Keyboard.TextEntry(c.ToString()); Thread.Sleep(charDelayMs); // 逐字符延时模拟真人输入 } } }参数说明delayMs是操作间隔默认 50ms 适合大多数桌面程序charDelayMs是逐字符延时20ms 在稳定性和速度之间比较平衡。如果目标程序响应慢可以调大。6.2 加入重试与异常兜底SendInput失败时返回 0但库不一定抛异常。稳妥的做法是在关键操作后检查返回值或者用「操作后验证」的方式——比如点击按钮后检查窗口标题是否变化。我习惯给每个关键操作包一层重试public static bool Retry(Action action, int maxRetry 3, int intervalMs 200) { for (int i 0; i maxRetry; i) { try { action(); return true; } catch { Thread.Sleep(intervalMs); } } return false; }逻辑说明maxRetry控制最大重试次数intervalMs是重试间隔。这个模式适合点击、输入这类幂等操作不适合「切换开关」这种有状态的操作否则会重复切换。6.3 扩展扫描码模式支持如果你的目标程序只认扫描码需要扩展库的键盘模拟部分。核心是构造KEYBDINPUT时填wScan而不是wVk并设置KEYEVENTF_SCANCODE。改造思路是继承或替换KeyboardSimulator重写发送逻辑。验证方法是拿一个 DirectInput 的游戏或工具做测试看按键是否被识别。改造点原实现改造后验证方式键码字段wVkwScan目标程序能收到按键标志位无KEYEVENTF_SCANCODE用 Spy 看消息转换不需要MapVirtualKey对比扫描码表6.4 一个我踩过的坑早期我图省事在循环里连续调用TextEntry输入几百个字符中间不加任何延时结果目标程序丢字符十次里错两三次。排查了半天以为是库的问题后来才想明白是目标程序的消息队列处理不过来。从那以后我每次做批量输入都强制走一遍「逐字符 延时」的路径宁可慢一点也要稳。希望这个经验能帮你少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑