news 2026/10/23 12:12:11

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# 键盘鼠标模拟实战:Windows Input Simulator 源码拆解与避坑指南

简介:这份资源面向需要在 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. 项目定位与技术选型:为什么不用原生 SendInput

2.1 它到底封装了什么

Windows 的输入模拟最终都落到user32.dll的SendInput上。这个函数接收一个INPUT结构体数组,每个结构体描述一次键盘或鼠标事件。问题在于INPUT是个联合体(union),在 C# 里要用StructLayout(LayoutKind.Explicit)配合FieldOffset手动布局,键盘事件还要区分KEYBDINPUT、MOUSEINPUT、HARDWAREINPUT三种类型。Windows Input Simulator 把这些全部封装在内部,对外只暴露IInputSimulator接口。

它的核心能力包括:

  • 键盘:单键按下/释放、组合键(Ctrl+C 这类)、文本直接输入、扫描码模式
  • 鼠标:移动、点击(左/右/中)、滚轮、按下/释放分离
  • 扩展:支持传入自定义的键盘/鼠标模拟实现,方便做单元测试

技术栈上,它是纯 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 第一个可运行示例

引用好之后,写一个控制台程序,模拟按下 Win+R 打开运行框,输入 notepad 并回车:

using WindowsInput; using WindowsInput.Native; class Program { static void Main() { // 创建输入模拟器实例,这是整个库的入口 var sim = new InputSimulator(); // 按下 Win 键 sim.Keyboard.KeyDown(VirtualKeyCode.LWIN); // 按下 R 键,此时 Win 还按着,形成 Win+R 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是字母 R,RETURN是回车。

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 List<VirtualKeyCode> 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_A,'1' 映射到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 组合键偶尔失效

现象:Ctrl+C十次里有一两次不生效。原因: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输入几百个字符,中间不加任何延时,结果目标程序丢字符,十次里错两三次。排查了半天以为是库的问题,后来才想明白是目标程序的消息队列处理不过来。从那以后我每次做批量输入,都强制走一遍「逐字符 + 延时」的路径,宁可慢一点也要稳。希望这个经验能帮你少走弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 12:09:53

“拔掉网线,真的能关住超级AI吗?”:物理隔离失效、侧信道逃逸与万台智能体暗流——读OpenAI核心研究员Noam Brown《Dwarkesh Podcast》深度对话

Dwarkesh Podcast 顶级前沿对话 | 访谈嘉宾&#xff1a;Noam Brown&#xff08;OpenAI 核心研究员、o1与多智能体系统核心缔造者&#xff09; | 主题&#xff1a;物理隔离&#xff08;Air-Gapping&#xff09;安全假象、侧信道渗透与智能体集群失控风险核心洞察&#xff1a;许…

作者头像 李华
网站建设 2026/10/23 11:48:05

如何在没有 Microsoft Store 的 Win10 电脑上安装 ChatGPT 桌面版

我们公司电脑上没有 Microsoft Store&#xff0c;Windows 更新也被禁用了&#xff0c;导致装不了 ChatGPT 桌面版。 但其实 OpenAI 在企业部署文档里提供了官方的离线安装包。 遂写下这篇文章&#xff0c;记录详细步骤和坑点&#xff0c;供大家&#xff08;包括我自己&#xff…

作者头像 李华
网站建设 2026/10/23 11:46:55

Mac 和 Linux 连不上 NAS 共享?客户端侧两类报错排查

文章目录 📖 介绍 📖 🏡 演示环境 🏡 📒 Finder 的失败和 mount 的报错码,各查各的 📒 🔍 Mac 侧:先让 Finder 说实话 📋 Linux 侧:报错码就是路标 🩺 挂载参数怎么给,文件属主才不乱 ✅ 连上之后,顺手做两件收尾 ⚓ 相关链接 ⚓ 📖 介绍 📖 前几天…

作者头像 李华
网站建设 2026/10/23 11:42:53

Last Meeting Theory与Soulmate的关系动力学解析

1. 这不是玄学&#xff0c;是关系动力学里的一个可观测现象“Last Meeting Theory”和“Soulmate”这两个词最近在社交平台高频共现&#xff0c;但绝大多数讨论停留在情绪共鸣或宿命论层面——比如“我们分手后三年重逢&#xff0c;那天他刚好辞职、我刚结束异地&#xff0c;连…

作者头像 李华
网站建设 2026/10/23 11:42:06

C语言贪吃蛇项目:EasyX图形库实现音乐、图片与美观界面

每次看到还有人用黑底白字的控制台写贪吃蛇&#xff0c;我都想劝一句&#xff1a;试试EasyX。这个C语言图形库装上以后&#xff0c;贪吃蛇这个题目完全可以从“交作业”变成“拿得出手”的项目。今天要聊的项目&#xff0c;标题就很直白——EasyX图形库、C语言贪吃蛇、音乐、图…

作者头像 李华
网站建设 2026/10/23 11:40:56

龙头战法:市场情绪周期与流动性溢价的交易策略

1. 龙头战法核心逻辑解析龙头战法本质上是一种基于市场情绪周期与资金流动规律的高阶交易策略。这套方法的核心在于识别并参与市场中最具人气和资金承载力的标的&#xff0c;通过把握情绪转折点获取超额收益。与传统的技术分析不同&#xff0c;龙头战法更注重对市场整体博弈生态…

作者头像 李华