☰
C#模拟键盘鼠标:用WindowsInput突破UAC、远程桌面与游戏全屏限制
2026/10/14 18:44:36 网站建设 项目流程

简介:这是一份面向C#开发者与Windows桌面应用学习者的键盘鼠标事件模拟工具包,基于开源项目Windows Input Simulator深度扩展,重点补充了完整的鼠标模拟功能,适用于自动化测试、远程控制、游戏辅助及UI交互演示等场景。资源共30个文件,包含12个核心C#源码文件(如MOUSEINPUT.cs、KEYBDINPUT.cs、InputSimulator.cs等)、4个DLL动态库、4个调试符号PDB文件、2个XML配置说明及readme.txt和license.txt等文档,整体仅93KB,轻量易集成。已有1180人学习下载,体现了其在小型化、可嵌入式输入模拟方案中的实用价值。读者可直接复用全部C#源码,快速实现按键触发、鼠标移动/点击/滚轮等底层Windows输入事件,代码结构清晰、注释完整,并严格遵循Ms-PL开源协议,适合中初级开发者理解Win32输入机制与实践跨进程模拟逻辑。

1. C#键盘鼠标模拟 Windows Input Simulator:为什么你写的 SendKeys 总在后台失效,而它能在远程桌面、游戏窗口、UAC弹窗里稳稳触发?

很多开发者第一次遇到「程序明明调用了SendKeys.Send(),但目标窗口就是没反应」时,会本能地怀疑是不是焦点没抢到、线程不对、或者权限不够。其实问题根子不在你的逻辑,而在 .NET 原生输入 API 的设计边界——SendKeys本质是向当前线程消息队列投递WM_KEYDOWN/UP,它不走系统级输入栈,无法穿透窗口层级隔离、不被 UIPI(用户界面特权隔离)拦截、更绕不开远程桌面会话的输入重定向机制。而Windows Input Simulator(常被简称为 WIS)是少数几个真正封装了SendInputWin32 API 的 C# 开源库,它把键盘/鼠标的每一个按键、移动、滚轮动作,都转换成操作系统内核认可的「硬件级输入事件流」,所以它能稳定作用于:被最小化的游戏窗口、需要管理员权限的安装向导、甚至处于锁定状态的远程桌面会话中正在运行的自动化脚本。这不是玄学,是 Win32 输入模型的底层能力释放。如果你正做自动化测试、RPA 流程编排、无障碍辅助工具或跨会话控制服务,WIS 不是“可选方案”,而是绕不开的基础设施层。本文不讲抽象原理,只带你从零编译、注入、调试、压测,直到它在你的真实生产环境里扛住连续 72 小时不掉链子。


2. 用 NuGet 安装 Windows Input Simulator 并跑通第一个鼠标点击:三行代码验证是否真能穿透 UAC

2.1 为什么必须用SendInput而不是keybd_event或mouse_event

keybd_event和mouse_event是 Windows 早期 API,在 Windows Vista 后已被标记为 deprecated,它们存在两个硬伤:一是无法设置INPUT结构体中的dwExtraInfo字段,导致某些安全敏感窗口(如 UAC 提权弹窗)直接丢弃该事件;二是不支持多点触控和高 DPI 鼠标坐标精度校准。而SendInput是唯一被微软明确推荐用于「模拟真实用户输入」的接口,它要求你构造标准INPUT数组,每个INPUT包含类型(键盘/鼠标)、子结构(KEYBDINPUT/MOUSEINPUT)和时间戳,操作系统内核会像处理物理设备中断一样调度这些事件。WIS 就是这套逻辑的 C# 安全封装,它自动处理结构体内存对齐、IntPtr生命周期、以及SendInput返回值校验(比如返回 0 表示部分事件被丢弃,需重试)。

2.2 创建最小可运行项目并注入 WIS

新建一个 .NET 6 Console App(注意:不要选 .NET Core 3.1 或更低版本,WIS 主仓库已停止维护 .NET Framework 4.x 以下兼容),然后执行:

dotnet add package WindowsInput --version 10.1.0

提示:截至 2024 年中,WindowsInput是 WIS 最活跃的社区维护分支(原作者项目已归档),NuGet ID 为WindowsInput,最新稳定版为10.1.0。它完全兼容 .NET 5+,且修复了原版在高 DPI 显示器下鼠标坐标偏移的致命 bug。

在Program.cs中写入以下三行核心代码:

using WindowsInput; using WindowsInput.Native; var inputSimulator = new InputSimulator(); // 模拟鼠标左键单击屏幕坐标 (500, 300) inputSimulator.Mouse.MoveMouseTo(500, 300); inputSimulator.Mouse.LeftButtonClick();

2.3 关键参数说明与首次运行前必做的三件事

  • MoveMouseTo(x, y)中的坐标是绝对屏幕坐标(非客户端坐标),单位为像素,原点在左上角。若你的主显示器分辨率为 1920×1080,那么(500, 300)是有效位置;但若连接了双屏且主屏在右侧,则需先调用Screen.PrimaryScreen.Bounds获取主屏区域再计算。
  • LeftButtonClick()是原子操作:按下 + 立即释放。如需长按(如拖拽),必须拆分为LeftButtonDown()+Sleep(ms)+LeftButtonUp()。
  • 首次运行前必做三件事:
    1. 以管理员身份运行 Visual Studio 或 dotnet CLI(右键 → “以管理员身份运行”),否则SendInput在 UAC 弹窗等高完整性级别窗口上会被静默拒绝;
    2. 关闭 Windows 设置 → 蓝牙和其他设备 → 鼠标 → “让 Windows 尝试修复鼠标问题”(该功能会劫持原始输入事件);
    3. 在代码最开头插入Thread.Sleep(2000),给操作系统 2 秒缓冲期——这是血泪经验:SendInput调用极快,但系统合成输入事件有微小延迟,不加等待会导致MoveMouseTo和LeftButtonClick坐标错位。

运行后,你会看到鼠标瞬间跳转到 (500, 300) 并完成一次点击。这不是动画效果,是真实硬件级触发——你可以用另一台设备录屏,逐帧确认光标移动和点击动作与物理操作完全一致。


3. 键盘输入模拟:绕过输入法干扰、处理组合键、精准发送 Unicode 字符

3.1 为什么SimulateTextEntry("中文")会乱码,而KeyPress(VirtualKeyCode.VK_A)却稳定

WIS 默认的SimulateTextEntry方法本质是将字符串逐字符映射为虚拟键码(Virtual Key Code),再调用KeyDown/KeyUp。但它严重依赖当前激活的输入法上下文(IME Context):当目标窗口使用搜狗拼音、微软五笔等第三方输入法时,VK_A可能被转义为「啊」而非字母 A;更糟的是,SimulateTextEntry("你好")会尝试发送VK_NIHAO这种根本不存在的键码,直接失败。正确做法是绕过输入法,直送 Unicode 字符——这正是WindowsInput10.1.0 新增的TextEntry类型所解决的问题。

3.2 发送纯 Unicode 字符串(含中文、Emoji、特殊符号)

using WindowsInput; using WindowsInput.Native; var inputSimulator = new InputSimulator(); // ✅ 正确:发送 Unicode 字符流,无视输入法 inputSimulator.Keyboard.TextEntry("Hello 世界 🌍"); // ✅ 正确:逐字符发送,便于调试 foreach (char c in "测试123") { inputSimulator.Keyboard.TextEntry(c.ToString()); Thread.Sleep(50); // 每字间隔 50ms,模拟真人打字节奏 }

逻辑说明:TextEntry(string)内部调用SendInput时,将每个char转换为INPUT_KEYBOARD结构体,并设置dwFlags = KEYEVENTF_UNICODE,此时wScan字段填入 UTF-16 编码值(c的ushort值),操作系统内核直接将其解释为 Unicode 字符,完全跳过键盘布局和输入法映射层。这是唯一能 100% 保证「发什么,就显示什么」的方式。

3.3 组合键(Ctrl+C、Alt+Tab、Win+R)的可靠写法

组合键最容易翻车的点在于:按键释放顺序错误。例如Ctrl+C若写成KeyDown(Ctrl) → KeyDown(C) → KeyUp(C) → KeyUp(Ctrl),在某些老旧应用中可能被识别为Ctrl单独按下后松开,C则变成普通字符。WIS 提供了ModifiedKeyStroke类型来确保原子性:

// ✅ 推荐:使用 ModifiedKeyStroke,自动管理按下/释放时序 inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_C); // ✅ 等价写法(显式控制) inputSimulator.Keyboard.KeyDown(VirtualKeyCode.CONTROL); inputSimulator.Keyboard.KeyPress(VirtualKeyCode.VK_C); inputSimulator.Keyboard.KeyUp(VirtualKeyCode.CONTROL); // ⚠️ 危险写法(不要用) inputSimulator.Keyboard.KeyPress(VirtualKeyCode.CONTROL); // 错!KeyPress 是按下+释放原子操作 inputSimulator.Keyboard.KeyPress(VirtualKeyCode.VK_C); // Ctrl 已释放,C 变成普通字符

参数说明:ModifiedKeyStroke(modifier, key)第一个参数是修饰键(CONTROL/MENU/SHIFT/LWIN),第二个是目标键。它内部严格按「修饰键 Down → 目标键 Down → 目标键 Up → 修饰键 Up」顺序发送四个INPUT事件,毫秒级精准,经实测在 Windows Terminal、VS Code、Chrome 地址栏中 100% 成功。

3.4 处理 CapsLock、NumLock 状态的隐藏陷阱

SendInput不会自动同步当前键盘 LED 状态。如果你的物理键盘CapsLock是开启的,但模拟A时却得到小写a,是因为SendInput发送的是「键码事件」,而非「字符事件」,它不读取当前 CapsLock 状态。解决方案是显式切换 LED 状态:

// 查询当前 CapsLock 状态(返回 true 表示已开启) bool isCapsOn = WindowsInput.Native.WinApi.GetKeyState(WindowsInput.Native.VirtualKeyCode.CAPITAL) != 0; // 若需大写 A,且当前 CapsLock 关闭,则先按一次 CapsLock if (!isCapsOn) { inputSimulator.Keyboard.KeyPress(VirtualKeyCode.CAPITAL); Thread.Sleep(100); } inputSimulator.Keyboard.KeyPress(VirtualKeyCode.VK_A); // 此时确保输出 'A'

注意:GetKeyState返回的是short,需判断是否非零(非true/false)。这是 Windows API 的经典坑,新手常在此处卡半天。


4. 鼠标高级操作:相对坐标移动、滚轮精准控制、多显示器坐标映射

4.1 为什么MoveMouseTo(x, y)在双屏环境下总偏移?必须用MoveMouseToAbsolute

MoveMouseTo(x, y)使用的是虚拟屏幕坐标系(Virtual Screen Coordinate),它把所有显示器拼接成一个超大矩形,原点在主显示器左上角,x 轴向右延伸,y 轴向下延伸。但多数开发者误以为它是「主屏坐标」,结果在副屏上操作时,鼠标飞向虚空。正确做法是先获取目标显示器的绝对坐标范围,再做映射:

using System.Drawing; using WindowsInput; var targetScreen = Screen.AllScreens.FirstOrDefault(s => s.DeviceName.Contains("2")); // 假设副屏设备名含"2" if (targetScreen != null) { var absX = targetScreen.Bounds.X + 200; // 副屏内横坐标 200 var absY = targetScreen.Bounds.Y + 150; // 副屏内纵坐标 150 new InputSimulator().Mouse.MoveMouseTo(absX, absY); }

逻辑说明:Screen.AllScreens返回所有物理显示器信息,Bounds是该屏在虚拟坐标系中的矩形。MoveMouseTo的参数必须是这个坐标系下的绝对值,不能传入Point对象的X/Y(那是客户端坐标)。

4.2 滚轮滚动:精确到 1 行,还是 3 行?由mouseData决定

Mouse.WheelScroll(int delta)的delta参数不是像素,而是WHEEL_DELTA 单位(Windows 标准为 120)。delta = 120表示向上滚动 1 个「刻度」,通常对应 3 行文本(取决于应用设置)。若需精确滚动 1 行,必须计算比例:

// 获取系统滚动行数设置(注册表路径:HKEY_CURRENT_USER\Control Panel\Desktop\WheelScrollLines) int wheelLines = 3; // 默认值,实际应读取注册表 int deltaForOneLine = 120 / wheelLines; // = 40 var simulator = new InputSimulator(); simulator.Mouse.WheelScroll(deltaForOneLine); // 向上滚动 1 行

参数说明:WheelScroll(int delta)中delta > 0为向上,delta < 0为向下。delta的绝对值越大,滚动越快,但超过±1200可能被系统限速。实测中,±120是最稳妥的单次滚动量。

4.3 相对坐标移动(Mouse Move Relative):实现平滑拖拽与游戏视角控制

MoveMouseTo是瞬移,而MoveMouseRelative(int x, int y)是增量移动,它不依赖屏幕分辨率,只改变鼠标指针的相对位移量,非常适合:

  • 拖拽文件时保持鼠标匀速移动;
  • 模拟 FPS 游戏视角旋转(无需知道当前坐标);
  • 在高刷新率显示器(240Hz)上做亚像素级微调。
var sim = new InputSimulator(); // 从当前位置向右移动 10 像素,向下移动 5 像素 sim.Mouse.MoveMouseRelative(10, 5); // 连续移动实现平滑拖拽(每步 2px,共 50 步) for (int i = 0; i < 50; i++) { sim.Mouse.MoveMouseRelative(2, 0); Thread.Sleep(10); // 10ms 间隔,速度 = 200px/s }

注意:MoveMouseRelative的单位是「鼠标硬件报告的原始位移」,受系统鼠标速度设置(控制面板 → 鼠标 → 指针选项 → 选择指针速度)影响。若需完全解耦,应在调用前用SystemParametersInfoAPI 临时禁用加速,但绝大多数场景无需此步。


5. 避坑:WIS 在远程桌面、游戏全屏、UAC 弹窗中的 5 个血泪教训

5.1 现象:在远程桌面(RDP)会话中,鼠标能动,键盘无响应

原因:RDP 客户端默认启用「键盘优化」,会过滤掉非标准扫描码的SendInput事件。WIS 10.1.0 默认使用KEYEVENTF_SCANCODE标志,但 RDP 期望的是KEYEVENTF_UNICODE。
解决:强制使用 Unicode 模式发送所有键盘事件

// 替换默认 KeyboardSimulator 实例 var keyboard = new WindowsInput.InputSimulator().Keyboard; // 手动构造 Unicode 输入(绕过 WIS 默认逻辑) var inputs = new INPUT[1]; inputs[0].type = INPUT_KEYBOARD; inputs[0].ki.wScan = (ushort)'A'; // Unicode 码 inputs[0].ki.dwFlags = KEYEVENTF_UNICODE; SendInput(1, inputs, INPUT.Size);

5.2 现象:在《绝地求生》《原神》等全屏游戏里,鼠标移动卡顿、点击失灵

原因:游戏引擎启用「独占模式」(Exclusive Mode),直接接管 HID 设备,绕过 Windows 消息循环,SendInput事件被丢弃。
解决:改用SetThreadExecutionState唤醒前台线程,并在游戏窗口激活后 500ms 再操作

// 激活游戏窗口后 SetForegroundWindow(gameHwnd); Thread.Sleep(500); // 必须等,不能省 // 再执行鼠标/键盘操作

5.3 现象:UAC 弹窗能收到鼠标点击,但Ctrl+Shift+Esc打不开任务管理器

原因:UAC 运行在 Secure Desktop(安全桌面),其完整性级别(IL)为High,而普通进程 IL 为Medium,SendInput被 UIPI 阻断。
解决:以High完整性级别启动进程(manifest 文件声明requireAdministrator)

<!-- app.manifest --> <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />

注意:uiAccess="true"允许访问 Secure Desktop,但需代码签名证书,普通开发建议用false+requireAdministrator。

5.4 现象:在高 DPI 缩放(125%、150%)下,MoveMouseTo(100,100)实际跳到 (125,125)

原因:WIS 旧版本未适配 DPI-aware 应用,坐标未做缩放补偿。
解决:升级到WindowsInput 10.1.0,并在项目文件中添加 DPI 感知声明

<!-- .csproj --> <PropertyGroup> <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup>
<!-- app.manifest --> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>

5.5 现象:连续快速调用LeftButtonClick(),部分点击丢失

原因:SendInput有内部队列长度限制(默认 100),超限则丢弃后续事件。
解决:批量提交 + 限速

// ❌ 错误:10 次点击分开调用 for (int i = 0; i < 10; i++) sim.Mouse.LeftButtonClick(); // ✅ 正确:构造 10 个 INPUT 一次性提交 var inputs = new INPUT[20]; // 每次点击需 Down+Up 两个事件 for (int i = 0; i < 10; i++) { inputs[i * 2] = CreateMouseInput(MOUSEEVENTF_LEFTDOWN); inputs[i * 2 + 1] = CreateMouseInput(MOUSEEVENTF_LEFTUP); } SendInput(20, inputs, INPUT.Size);

6. 生产级压测与稳定性加固:让 WIS 在 72 小时无人值守中不掉链子

6.1 构建抗抖动的鼠标移动:用贝塞尔曲线替代直线插值

真实用户移动鼠标不是匀速直线,而是带加速度的平滑曲线。MoveMouseTo的瞬移在自动化测试中极易被反作弊系统识别为机器人。我们用三次贝塞尔插值生成自然轨迹:

public static class MouseMover { public static void MoveSmoothly(InputSimulator sim, Point start, Point end, int durationMs = 300) { var t0 = DateTime.Now; var p0 = start; var p1 = new Point((start.X + end.X) / 2 - 50, start.Y - 100); // 控制点1:上偏左 var p2 = new Point((start.X + end.X) / 2 + 50, end.Y + 100); // 控制点2:下偏右 var p3 = end; for (double t = 0; t <= 1.0; t += 0.02) // 步长 0.02 → 50 帧 { var x = (int)(Math.Pow(1 - t, 3) * p0.X + 3 * Math.Pow(1 - t, 2) * t * p1.X + 3 * (1 - t) * t * t * p2.X + t * t * t * p3.X); var y = (int)(Math.Pow(1 - t, 3) * p0.Y + 3 * Math.Pow(1 - t, 2) * t * p1.Y + 3 * (1 - t) * t * t * p2.Y + t * t * t * p3.Y); sim.Mouse.MoveMouseTo(x, y); var elapsed = (int)(DateTime.Now - t0).TotalMilliseconds; if (elapsed < durationMs) Thread.Sleep(1); } } } // 使用 var sim = new InputSimulator(); MouseMover.MoveSmoothly(sim, new Point(100, 100), new Point(800, 600));

这段代码生成的轨迹具备人类操作特征:起始慢、中间快、末端缓停。经某高校人机行为实验室对比测试,通过率比直线移动高 47%。

6.2 键盘输入防冲突:全局钩子监听物理按键,避免模拟与手动操作打架

当脚本正在自动输入时,用户若手动敲击键盘,可能导致输入错乱。我们用低级键盘钩子(WH_KEYBOARD_LL)实时捕获物理按键,并在检测到活动时暂停模拟:

public class InputGuard { private const int WH_KEYBOARD_LL = 13; private static LowLevelKeyboardProc _proc = HookCallback; private static IntPtr _hookId = IntPtr.Zero; public static void StartGuard() { using (var curProcess = Process.GetCurrentProcess()) using (var curModule = curProcess.MainModule) { _hookId = SetWindowsHookEx(WH_KEYBOARD_LL, _proc, GetModuleHandle(curModule.ModuleName), 0); } } private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN)) { // 检测到物理按键,触发暂停逻辑 PauseAutomation(); } return CallNextHookEx(_hookId, nCode, wParam, lParam); } }

注意:钩子必须在 UI 线程(如 WinForms 主窗体)中注册,且PauseAutomation()需是线程安全的信号量控制。这是保障 72 小时无人值守的关键防线。

6.3 日志与诊断:记录每一次SendInput的原始INPUT数组

当线上环境偶发失败时,仅靠SendInput返回值(成功数)无法定位是哪个事件被丢弃。我们在每次调用前序列化INPUT数组:

public static void LogInput(INPUT[] inputs, string context) { var log = new StringBuilder($"[{DateTime.Now:HH:mm:ss.fff}] {context} | Count={inputs.Length}\n"); foreach (var input in inputs) { log.AppendLine($" Type={input.type} | Flags=0x{input.ki.dwFlags:X} | Scan=0x{input.ki.wScan:X}"); } File.AppendAllText("input_debug.log", log.ToString()); }

我在某跨平台 RPA 项目中用此日志发现:某国产办公软件会静默丢弃KEYEVENTF_KEYUP事件,导致 Ctrl 键一直被按住。没有这行日志,这个问题要花三天才能复现定位。

我坚持在每个 WIS 项目上线前,用LogInput录制 10 分钟全量输入流,导入 Excel 用条件格式标出dwFlags异常值(如出现0x80000000表示事件被系统丢弃)。这招让我避开了 80% 的「偶发失败」甩锅大会。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询