简介:这是一份面向C#开发者与自动化脚本编写者的实用工具包,专为后台模拟输入操作设计,适用于游戏辅助(如《魔兽世界》AutoFish)、RPA轻量级自动化及测试场景。资源基于.NET Core 3.1构建,提供x86/x64双架构支持,完整封装键盘单键后台触发、鼠标坐标定位点击(含定时点击功能)、鼠标移动及左右键模拟等核心能力,虽暂不支持组合键,但接口简洁、调用直接,适合中初级开发者快速集成。压缩包大小为2.98MB,内含可执行程序、配置说明文档及详细使用指南,文件总数未披露,但主体为可运行二进制文件与配套文本说明,便于开箱即用与二次开发参考。目前已有2385人学习下载,读者可直接获取稳定可用的模拟输入模块、清晰的调用示例及针对游戏自动化场景的实操适配要点,显著降低后台输入控制的技术门槛。 很多做C#上位机或者Windows桌面自动化工具的朋友,应该都遇到过这种需求:程序在后台运行,不干扰用户操作,但需要给某个窗口发送按键、移动鼠标、点击按钮。我最早接触这个场景是做产线测试工具,程序要控制一个老旧的数据采集软件去点菜单、填参数,又不能把用户的鼠标抢走,只能在后台偷偷发消息。后来陆陆续续接过好几回类似需求,踩了不少坑,这里就把整套方案和心得整理出来,给有同样需求的朋友当个参考。
这篇文章主要围绕C#环境下如何实现后台模拟键盘按键、后台模拟鼠标移动以及左右键点击,会重点讲清楚Win32消息机制、PostMessage和SendInput这两种核心API的取舍、组合键怎么处理好,以及遇到没反应、焦点丢失、窗口闪烁这些常见问题时怎么排查。适合正在做上位机、自动化测试工具、辅助操作软件,或者说想深入了解Windows消息机制的C#开发者参考。不管你是刚入门还是已经写过几个工具,里面都会有能直接用的东西。
1. 这个需求到底在解决什么问题
1.1 前台模拟和后台模拟的差别
先理清一个概念:模拟键盘鼠标,Windows下有两条完全不同的路线。
第一条是前台模拟,核心API是SendInput,它把输入事件注入到系统的输入流里,效果跟真实键鼠完全一样。目标窗口必须是当前激活窗口,也就是说发消息的时候,用户会看到窗口被激活、鼠标在屏幕上滑动,如果用户同时在操作,俩人会“打架”。它的好处是几乎所有程序都认,因为对目标程序来说,这就是真实的物理输入。
第二条是后台模拟,核心API是PostMessage,它只往目标窗口的消息队列或者窗口过程直接塞消息,不经过系统输入流。窗口不需要获得焦点,用户该干嘛干嘛,互不干扰,感觉就像有个隐形人在操作那个窗口。但代价是,目标程序能不能正确响应,完全取决于它内部是怎么处理消息的。
先说结论:标题里的“后台按键、后台鼠标”,本质上就是走PostMessage,往指定窗口句柄发WM_KEYDOWN、WM_KEYUP、WM_MOUSEMOVE、WM_LBUTTONDOWN这些消息。这条方案在Windows 10/11上稳定性还不错,但有几个前提条件,下面会挨个说。
1.2 为什么一定要用 Win32 消息,而不是别的方式
有人可能问,C#里不是有SendKeys、还有Microsoft.VisualBasic.Devices.Keyboard吗,这些不是更简单?确实,SendKeys封装得很好,几行代码就能发一个组合键,但它实现底层用的还是keybd_event或者SendInput,属于前台模拟,目标窗口必须激活。对后台场景来说,它根本不适用。
还有人说用Windows UI Automation(UIA),它走的是自动化接口,确实能做到很多后台操作,但前提是目标程序实现了UIA的Pattern,比如Button控件要有InvokePattern。很多老的工业软件、自绘界面的程序、嵌了浏览器内核的窗口,根本不暴露这些接口,UIA就抓瞎了。而Win32窗口消息是Windows界面交互的最底层通道,是个窗口就得处理消息,所以通用性最强。这也是我能给出的最稳、最普适的方案。
另外一个常见做法是用钩子(Hook),比如WH_KEYBOARD_LL,监听全局键盘事件然后拦截。但这个更适合做输入监控,而不是后台模拟,而且全局钩子容易误伤其他程序,还会被杀软拦。PostMessage是点对点发消息,只在进程间传递,风险小得多。
2. 键盘后台模拟的核心细节
2.1 WM_KEYDOWN 和 WM_KEYUP 的参数怎么构造
键盘模拟在Windows消息层面其实就两类消息:按下(WM_KEYDOWN)、抬起(WM_KEYUP)。消息本身带了两个重要参数:wParam是虚拟键码(Virtual-Key Code),lParam则打包了一大堆信息,包括重复计数、扫描码、扩展键标志、上下文码、之前的键状态等等。
很多教程里只给个简化版,把lParam直接填0或填1,短期内好像也能用,但遇到严格检查消息参数的程序就不灵了。我实际测试下来,按键消息的lParam至少要正确设置这几项:
| 位段 | 含义 | 典型值 |
|---|---|---|
| 0-15位 | 重复计数 | 通常为1 |
| 16-23位 | 扫描码 | 查表或可传0 |
| 24位 | 扩展键标志 | 如方向键、Win键为1 |
| 30位 | 之前按键状态 | 按下时为1,抬起时为0 |
| 31位 | 转换状态 | 按下时为0,抬起时为1 |
比如按下A键,lParam可以构造成1(重复计数)。但严格一点,A键的扫描码是0x1E,所以按下时lParam可以填0x001E0001,抬起时填0xC01E0001。这样拼出来的消息,跟真实键盘进去的几乎一致。不过说实话,大多数普通窗口程序只关心wParam(键码),lParam只要不是太离谱都能正常工作,只有那些做了防作弊或者特殊逻辑的程序才会仔细去读lParam,所以能填准确是最稳的。
这里给一个我常用的C#构造lParam的辅助方法,其实很简单,就是按位或操作:
private static IntPtr MakeLParam(byte scanCode, bool isExtended, bool isKeyUp, bool isRepeat) { uint lParam = 1; // 重复计数默认为1 lParam |= (uint)scanCode << 16; // 扫描码放到16-23位 if (isExtended) lParam |= 0x01000000; // 扩展键标志 if (isRepeat) lParam |= 0x40000000; // 之前的键状态为按下 if (isKeyUp) lParam |= 0xC0000000; // 抬起:位置30和31都要置1 return new IntPtr(lParam); }再说一下虚拟键码。C#里可以直接用System.Windows.Forms.Keys枚举,它跟Win32的虚拟键码完全对应。比如Keys.A就是0x41,Keys.Enter就是0x0D,用起来方便。如果你要支持像Alt、Ctrl、Shift这些修饰键,它们也都有对应的虚拟键码(0x12、0x11、0x10)。
2.2 组合键要怎么发才不会被目标程序忽略
组合键是后台模拟里最容易翻车的地方。我最早写过Ctrl+C,结果发现目标窗口根本不执行复制操作。后来排查发现,问题出在“按键顺序”和“状态保持”上。
正确的组合键流程是这样的:
- 发送Ctrl键的WM_KEYDOWN(按下不抬起)
- 发送C键的WM_KEYDOWN
- 发送C键的WM_KEYUP
- 发送Ctrl键的WM_KEYUP
注意第1步和第4步,Ctrl的按下状态必须持续到目标键发送完毕,不能按下就立刻抬起。有些实现图省事,把Ctrl和C的按下同时发出去,这样目标窗口很可能识别不到“Ctrl处于按住状态”,从而导致组合键失效。正确做法是按下修饰键之后,稍微等一个消息处理周期(哪怕Sleep(10)),再按下正常键,最后按相反顺序抬起。
另外还有一个坑:单纯用PostMessage发WM_KEYDOWN时,Windows并不会自动更新全局的键盘状态表(即GetKeyState返回的状态)。有些程序,尤其是用键盘钩子或者查询状态标志位的程序,它会检查“Ctrl是否真的被按下”,如果检测不到,组合键照样不触发。这种情况下的解决办法是:先用keybd_event或者SendInput把真正的Ctrl键按下去,再用PostMessage发业务键,最后把Ctrl抬起来。这样既保住了后台发送,又让程序能查询到Ctrl的真实状态。但这个方案的前提是系统允许当前进程注入输入事件,如果程序以管理员运行,要确保自己的程序也提权了。
还有一类特殊组合键,比如Ctrl+Alt+Z这种,Alt键的处理方式也类似,但Alt键按下后会给窗口发WM_SYSKEYDOWN而不是WM_KEYDOWN,所以如果目标窗口的菜单快捷键依赖Alt的组合,还得考虑发WM_SYSKEYDOWN。自定义的全局热键场景还不太一样,建议先用键盘钩子确认一下目标程序对Alt的处理方式。
3. 鼠标后台模拟的实现思路
3.1 用 PostMessage 发送鼠标消息
鼠标的后台模拟比键盘要复杂一些,因为鼠标消息多,而且坐标计算容易出错。后台发送鼠标点击的核心是往目标窗口发送WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTONUP这三个消息组合,分别对应移动、按下、抬起。
有一个原则你一定要记住:PostMessage发送的鼠标坐标,是相对于目标窗口客户区的坐标,不是屏幕坐标。很多人在这里翻车,把屏幕坐标当作窗口坐标发过去,目标窗口要么没反应,要么在奇怪的位置响应。换算方法也简单,用ScreenToClientAPI或者用窗口矩形和客户区矩形的差值算一下都可以。
具体消息的参数:
- wParam是鼠标按键状态,比如左键按下时传MK_LBUTTON(0x0001),右键按下时传MK_RBUTTON(0x0002)
- lParam是打包了坐标的无符号整数,低16位是X,高16位是Y
比如想在客户区坐标(100, 200)处单击左键,流程是:
PostMessage(hwnd, WM_MOUSEMOVE, 0, (IntPtr)((200 << 16) | 100)); PostMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, (IntPtr)((200 << 16) | 100)); PostMessage(hwnd, WM_LBUTTONUP, 0, (IntPtr)((200 << 16) | 100));顺序不能乱,必须先移动再按下再抬起,否则很多控件不会触发Click事件。另外,WM_LBUTTONUP的wParam通常传0,表示抬起时没有其他按钮被按住,这个细节也注意一下。
双击左键,就要发两次按下和抬起,并且第二次的wParam要带上MK_LBUTTON(表示第一次点击后按钮仍处于按下状态)。不过更稳妥的方式是发WM_LBUTTONDBLCLK,但前提是目标窗口类注册时接收了双击消息,否则还是老老实实发两次单击序列。
3.2 鼠标坐标和消息参数的封装
鼠标消息的lParam构造跟键盘的lParam不一样,比较简单,但有个细节是坐标必须是客户区坐标。对于窗口里嵌了子控件的情况,如果知道控件句柄,那最好,直接拿控件句柄发消息,坐标相对控件来算,准确度更高。
我提供一个通用的坐标转换方法:
public static Point ScreenToClient(IntPtr hwnd, Point screenPoint) { POINT pt = new POINT { X = screenPoint.X, Y = screenPoint.Y }; ScreenToClient(hwnd, ref pt); return new Point(pt.X, pt.Y); }然后是封装一个鼠标消息发送函数,方便复用:
private const uint WM_MOUSEMOVE = 0x0200; private const uint WM_LBUTTONDOWN = 0x0201; private const uint WM_LBUTTONUP = 0x0202; private const uint WM_RBUTTONDOWN = 0x0204; private const uint WM_RBUTTONUP = 0x0205; private const int MK_LBUTTON = 0x0001; private const int MK_RBUTTON = 0x0002; public static void SendMouseClick(IntPtr hwnd, int x, int y) { IntPtr lParam = new IntPtr((y << 16) | x); PostMessage(hwnd, WM_MOUSEMOVE, IntPtr.Zero, lParam); PostMessage(hwnd, WM_LBUTTONDOWN, (IntPtr)MK_LBUTTON, lParam); PostMessage(hwnd, WM_LBUTTONUP, IntPtr.Zero, lParam); }这里还有一个很多人不知道的点:如果目标窗口是DirectX游戏或者全屏独占程序,PostMessage鼠标消息是无效的,因为游戏不走普通窗口消息循环。这种情况只能退回前台SendInput方案。所以设计之前先确认目标是不是普通窗口类程序,很关键。
4. 可直接复用的 C# 辅助类
4.1 找窗口句柄,这是整个方案的地基
后台模拟的第一步就是拿到目标窗口的句柄(IntPtr)。拿不到窗口句柄,后面所有消息都无处投递。常用方法有两种:按窗口标题找,按进程枚举窗口找。
按标题找最简单,用FindWindow,比如:
IntPtr hwnd = FindWindow(null, "记事本");但实际用的时候窗口标题可能带前后缀、或者动态变化,这时可以用EnumWindows把所有顶层窗口列出来,然后挨个比对标题包含指定关键字,或者用GetWindowThreadProcessId配合进程ID来匹配。这个方式更可靠,尤其是你想确保控制的是某个特定进程的窗口,而不是同名标题的另一个窗口。
我一般会封装一个方法:输入进程名(比如“notepad”)或窗口标题关键字,返回第一个匹配的窗口句柄。这样让外部调用更简单。
另外注意,有些窗口只是主窗口,里面按钮是子控件。用Spy++或者FindWindowEx可以一层层找到子控件的句柄,拿到控件句柄后直接把鼠标消息发给控件,坐标用相对控件的(0,0)或中心点即可。很多时候后台点击按钮失败,就是因为你把坐标算到了主窗口客户区,跟按钮实际位置偏差太大。
4.2 发送键盘和鼠标控制,代码封装
这里我直接给一个我长期在用的辅助类,里面包含键盘、鼠标消息发送的核心方法。代码不长,但踩过的坑都已经处理了,直接拿去改改就能用。
public static class BackgroundInput { [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } // 键盘消息常量 private const uint WM_KEYDOWN = 0x0100; private const uint WM_KEYUP = 0x0101; public static void SendKey(IntPtr hwnd, Keys key, bool ctrl = false, bool shift = false, bool alt = false) { if (ctrl) PostMessage(hwnd, WM_KEYDOWN, (IntPtr)Keys.ControlKey, MakeLParam(0x1D, false, false, false)); if (shift) PostMessage(hwnd, WM_KEYDOWN, (IntPtr)Keys.ShiftKey, MakeLParam(0x2A, false, false, false)); if (alt) PostMessage(hwnd, WM_KEYDOWN, (IntPtr)Keys.Menu, MakeLParam(0x38, false, false, false)); Thread.Sleep(20); PostMessage(hwnd, WM_KEYDOWN, (IntPtr)key, MakeLParam(0, false, false, false)); PostMessage(hwnd, WM_KEYUP, (IntPtr)key, MakeLParam(0, false, true, false)); Thread.Sleep(20); if (alt) PostMessage(hwnd, WM_KEYUP, (IntPtr)Keys.Menu, MakeLParam(0x38, false, true, false)); if (shift) PostMessage(hwnd, WM_KEYUP, (IntPtr)Keys.ShiftKey, MakeLParam(0x2A, false, true, false)); if (ctrl) PostMessage(hwnd, WM_KEYUP, (IntPtr)Keys.ControlKey, MakeLParam(0x1D, false, true, false)); } private static IntPtr MakeLParam(byte scanCode, bool isExtended, bool isKeyUp, bool isRepeat) { uint lParam = 1; lParam |= (uint)scanCode << 16; if (isExtended) lParam |= 0x01000000; if (isRepeat) lParam |= 0x40000000; if (isKeyUp) lParam |= 0xC0000000; return new IntPtr(lParam); } // 鼠标消息常量 private const uint WM_MOUSEMOVE = 0x0200; private const uint WM_LBUTTONDOWN = 0x0201; private const uint WM_LBUTTONUP = 0x0202; private const uint WM_RBUTTONDOWN = 0x0204; private const uint WM_RBUTTONUP = 0x0205; private const uint WM_LBUTTONDBLCLK = 0x0203; private const int MK_LBUTTON = 0x0001; private const int MK_RBUTTON = 0x0002; public static void SendMouseLeftClick(IntPtr hwnd, int x, int y) { IntPtr lParam = new IntPtr((y << 16) | (x & 0xFFFF)); PostMessage(hwnd, WM_MOUSEMOVE, IntPtr.Zero, lParam); PostMessage(hwnd, WM_LBUTTONDOWN, (IntPtr)MK_LBUTTON, lParam); PostMessage(hwnd, WM_LBUTTONUP, IntPtr.Zero, lParam); } public static void SendMouseRightClick(IntPtr hwnd, int x, int y) { IntPtr lParam = new IntPtr((y << 16) | (x & 0xFFFF)); PostMessage(hwnd, WM_MOUSEMOVE, IntPtr.Zero, lParam); PostMessage(hwnd, WM_RBUTTONDOWN, (IntPtr)MK_RBUTTON, lParam); PostMessage(hwnd, WM_RBUTTONUP, IntPtr.Zero, lParam); } }这个类我用过很多次,包含了一个项目里足够用的功能集。如果你要在后台模拟滚动鼠标滚轮,还需要加上WM_MOUSEWHEEL(0x020A),wParam中是滚轮滚动量,正数向上负向下。
代码里我特意在组合键之间加了Thread.Sleep(20),主要给目标窗口留出处理消息的时间,尤其是界面卡顿或者刚打开窗口时,这个延迟让它能反应过来。具体项目里可以调整,甚至去掉。
5. 常见问题排查实录
5.1 后台按键没反应的几种原因
后台模拟失败,最常见的几个原因我一个个说:
第一,窗口句柄不对。有的人用FindWindow传空类名,或者拿到的句柄是桌面的,消息全都发到桌面去了,目标窗口自然没反应。排查时先用Spy++看一下目标窗口的类名和句柄,比对下自己代码拿到的是不是同一个。
第二,消息发错了线程。窗口消息跟线程消息不一样,PostMessage发到窗口句柄上,只要窗口存在,消息就会进入该窗口所属线程的队列。但有些自定义的窗口过程会拦截消息,消息即便到了也被吞了。这种情况通常没法从外部强制解决,除非换SendInput。
第三,程序不是从普通消息循环里获取输入,而是直接用GetAsyncKeyState轮询物理键状态。PostMessage的消息不会改变物理状态,所以这类程序就是没反应。比如很多游戏、某些老式控件都是这么设计的,解决方式是混合方案:先用SendInput或keybd_event发一次真实的按键(这会更新物理状态),再配合其他方式。不过这就不是纯后台了。
第四,管理员权限问题。如果你的目标是管理员权限运行的窗口,而你的程序是普通权限,PostMessage会直接失败,SetLastError返回5(拒绝访问)。解决方式是程序清单里加requireAdministrator,或者装成服务。实测很多客户环境,UAC是最常见的“为什么点了没反应”的元凶。
5.2 游戏或特殊窗口模拟失败的解决方法
遇到DirectX游戏、全屏独占程序或者使用原始输入(Raw Input)的窗口,PostMessage方案通常失效,因为它们接收输入的方式跟普通窗口完全不同。这时候我的建议是:
先确认目标程序是否走Raw Input。判断方法很简单:在窗口处理函数里检查WM_INPUT消息,或者用Spy++看消息流。如果程序确实不处理标准窗口消息,那就不要死磕后台方案了,考虑SendInput或者驱动级模拟。
还有一类常见拐点是Electron或者Chromium内核的窗口,比如VS Code、很多新出的桌面App。这类窗口对后台鼠标消息的支持时好时坏,因为它们自己处理坐标映射。实测发现有的支持后台点击组件,有的只支持键盘,各种都不一样。这种建议先实机验证一轮再决定方案,别一上来就全指望PostMessage。
如果退回到SendInput方案,要注意发送前先SetForegroundWindow激活窗口,否则SendInput的坐标定位和按键目标都不可控。另外SendInput模拟鼠标点击时,用户如果同时在动鼠标,也会被影响的。
5.3 组合键状态丢失、消息遗漏的处理
组合键状态丢失,前面提过,主要是PostMessage不更新全局键盘状态导致的。为了解决这个问题,我通常会在发组合键前调用keybd_event或SendInput,先真实把修饰键按下。这个方案我用了很久,基本能解决问题。示例:
private static void PressCtrlGlobally() { keybd_event(0x11, 0, 0, UIntPtr.Zero); // Ctrl按下 } private static void ReleaseCtrlGlobally() { keybd_event(0x11, 0, KEYEVENTF_KEYUP, UIntPtr.Zero); // Ctrl抬起 }这里KEYEVENTF_KEYUP = 0x0002。注意这种方式会全局触发Ctrl状态改变,所以用完后一定要立刻抬起,不然用户会发现自己的Ctrl键逻辑异常,这个坑我在自动化脚本里踩到过。
另外还有一种情况:目标窗口的消息循环繁忙,比如正在加载大量数据,PostMessage进去的消息被积压,导致动作执行得很慢或者丢失。这个在操作大型软件时很常见。解决办法是在每个消息之间加一个延迟(比如50ms),或者用SendMessageTimeOut检查目标窗口是否响应。如果窗口处于忙碌状态,可以等待GetGUIThreadInfo查看线程输入状态,再去发后续消息。实测对一个卡住的窗口疯狂发消息,只会让情况更糟,一定要做好重试和超时。
这里再补一个经验:如果你连续往一个窗口发很多条键盘消息,有些窗口会做消息合并处理,比如快速输入文字时自动合并输入法组合。为了防止模拟输入变成乱码或漏字,建议每条消息之间至少间隔30ms,不能一口气全塞进去。这是我用后台模拟往文本框填长字符串时踩过的坑。
5.4 后台模拟时怎么确认自己的消息真的发出去了
这个排查技巧比较实用。我在写工具时都会临时加一个日志,记录每次PostMessage的返回值和LastError。PostMessage失败时返回false,但不代表消息一定被接收成功,它只代表消息成功进了目标线程的消息队列。为了进一步确认,可以再给目标窗口发一个自定义消息(WM_APP + 1),然后看它是否响应。
更靠谱的验证手段是用Spy++附加到目标进程,实时看消息流。Spy++能看到窗口收到的每一条消息,如果你的PostMessage消息没出现在Spy++里,说明消息根本没到目标窗口,问题在句柄或权限;如果出现了但程序没反应,说明消息被目标窗口处理时忽略了,问题在参数或目标逻辑。这两类问题用Spy++一秒定位,非常推荐大家养成这个习惯。
另外,发送鼠标消息前,也可以先用GetCursorPos拿到目标窗口下的真实屏幕坐标,然后通过ScreenToClient换算成客户区坐标,跟自己的消息参数对比一下。很多“点了没反应”实际是坐标算错了,点到了窗口外面,只是没报错而已。
6. 从 PostMessage 到 SendInput 的兜底思路
有一段经历让我印象比较深。之前做一个制造业的上位机数据采集工具,要控制一个老牌组态软件,后台发消息发得好好的,但升级到新版本后,对方改用了自绘界面,PostMessage对它的自定义控件全部失灵。我临时加了SendInput方案做兜底,在用户不操作电脑的时候用前台模拟,才把项目撑过去。
所以我的建议是,在正式做一个自动化工具之前,先花10分钟写一个简单的验证程序,分别用PostMessage和SendInput去操作目标软件,看哪种模式有效。这个验证成本很低,但能帮你从一开始就选对方向,避免开发到后面才发现方案根本行不通。我之前就因为偷懒跳过这一步,结果白白多花了两天去适配。
如果你的目标正好是那种两种方式都有效的软件,那肯定优先后台方案,用户体验好太多。如果后台方案部分失效,可以考虑做一个模式切换:支持界面操作时用后台,需要强制注入时切前台。这种设计在实际项目中往往是最实用的。
最后再分享一个容易被忽略的小技巧:PostMessage发送键盘消息时,如果目标窗口有输入法(IME)焦点,中文输入法可能会吞掉你的英文字符消息。解决方法是先切换输入法到英文模式,或者直接发送Unicode字符消息WM_CHAR。对于需要输入中文文本的场景,建议直接发WM_CHAR带Unicode值,比模拟按键组合可靠得多,这也是我在自动化填表时兜底用的方案。
本文还有配套的精品资源,点击获取