Windows消息机制深度解析:PostMessage与SendMessage模拟输入的实战指南 1. 项目概述为什么模拟按键总出岔子模拟键盘鼠标操作听起来是个挺基础的需求无论是做自动化测试、游戏辅助还是开发一些需要后台操作的效率工具都绕不开它。很多开发者尤其是刚接触Windows API的朋友第一反应就是去搜SendMessage或者PostMessage。网上的代码片段一抓一大把复制粘贴跑起来好像也“能用”。但真到了实际项目里问题就来了按键怎么死活发不出去焦点明明在目标窗口却一点反应都没有更诡异的是有时候一个按键会莫名其妙地重复触发好几次或者目标程序直接崩溃了。如果你也踩过这些坑那太正常了。PostMessage和SendMessage这两个API用起来门槛低但想用好、用对里头的门道可不少。它们不是简单的“发送按键信号”而是向窗口发送特定的Windows消息。这就像是你想控制一台机器不是直接去按它的按钮而是给它发了一封操作指令邮件。邮件能不能被正确接收、解读和执行取决于收件人目标窗口的“邮箱地址”窗口句柄、它是否愿意处理这类邮件消息处理函数以及邮件格式是否正确。网上很多教程只给了“发邮件”的代码却没告诉你邮件地址可能找错、收件人可能拒收、或者你的邮件格式根本不对。这就是为什么照着抄代码却无法成功的原因。本文将彻底拆解使用PostMessage和SendMessage模拟输入时从原理到实操的所有核心细节特别是那些导致“发送不成功”和“重复按键”的隐蔽陷阱并提供经过实战检验的解决方案。无论你是用Delphi、VB.NET、C还是其他语言调用Windows API这里的思路都是相通的。2. 核心原理消息机制与模拟输入的本质在深入代码之前我们必须先理解Windows应用程序是如何与键盘鼠标交互的。这绝不是简单的“按下A键程序收到一个‘A’”而是一个由驱动、系统、消息队列和窗口过程共同参与的精密流程。2.1 Windows消息循环事件的传送带每个带有用户界面的Windows程序其主线程都运行着一个消息循环。你可以把它想象成一个不断运转的传送带。系统或其它程序将发生的事件如鼠标移动、按键按下、窗口重绘等打包成一个个标准的“消息包裹”放到这个程序的传送带消息队列上。程序的消息循环则不断地从传送带上取下包裹并根据包裹上标注的“收货地址”窗口句柄HWND分发给对应的窗口过程函数去处理。PostMessage和SendMessage的作用就是允许一个程序向另一个程序或自己的某个窗口“投递”或“发送”一个自定义的“消息包裹”。PostMessage异步投递。它把消息包裹放到目标窗口所在线程的消息队列末尾然后立即返回不等待包裹被处理。就像你把信扔进邮筒不管对方何时收到、是否回复。SendMessage同步发送。它直接调用目标窗口的窗口过程函数并等待该函数处理完毕返回后自己才返回。这好比你把信直接交到对方手上并站在旁边等他看完、给出答复。对于模拟输入我们通常使用PostMessage因为它更接近真实用户操作发生时系统的行为——事件被放入队列等待程序依次处理不会阻塞发送方。2.2 键盘消息WM_KEYDOWN, WM_KEYUP, WM_CHAR模拟一个按键比如按下并释放‘A’键最少需要发送两条消息WM_KEYDOWN表示键被按下。消息的wParam参数是虚拟键码VK_AlParam则包含重复计数、扫描码等复杂信息。WM_KEYUP表示键被释放。参数与WM_KEYDOWN类似但状态位不同。很多程序尤其是编辑控件不仅关心WM_KEYDOWN/UP更关心WM_CHAR消息。WM_CHAR是在WM_KEYDOWN之后产生的它传递的是经过键盘布局和转换后的字符代码。你按ShiftAWM_KEYDOWN收到的是VK_A而WM_CHAR收到的则是大写‘A’的ASCII码65。如果你只发送了WM_KEYDOWN/UP而没有触发WM_CHAR那么记事本里可能就不会出现字符。关键陷阱1消息顺序与附加消息真实的按键过程远不止两条消息。在WM_KEYDOWN之后系统可能会自动生成WM_CHAR甚至WM_DEADCHAR等消息。某些应用程序特别是那些自己处理输入法的依赖于这些消息的完整序列。简单地PostMessage(WM_KEYDOWN); PostMessage(WM_KEYUP);可能无法满足它们导致模拟失败。2.3 鼠标消息WM_LBUTTONDOWN, WM_MOUSEMOVE等鼠标消息相对直接但坐标计算是坑点。WM_LBUTTONDOWN等消息的lParam参数包含了鼠标相对于窗口客户区左上角的坐标。低16位是X坐标高16位是Y坐标。这意味着你不能直接使用屏幕坐标。必须通过ScreenToClient这个API函数将屏幕坐标转换到目标窗口的客户区坐标再填入消息参数。2.4 与其他模拟方式的本质区别搜索热词里提到了keybd_event和SendInput这里必须厘清它们的层级关系keybd_event/mouse_event(旧API)在系统层面模拟物理输入。它们直接“欺骗”系统让系统以为物理键盘或鼠标真的产生了信号。影响的是全局焦点窗口不关心具体是哪个应用程序。这是比较“底层”和“粗暴”的方式但兼容性好。SendInput(新API)keybd_event的升级版功能更强大可以组合键盘鼠标事件一次性发送同样是在系统输入层面模拟。PostMessage/SendMessage在应用层面模拟逻辑事件。它们不模拟硬件输入而是直接向特定窗口发送“你已经收到输入了”的通知。这要求你对目标窗口有精确的控制正确的句柄并且该窗口愿意处理你发送的消息。核心区别SendInput是“扮演用户”系统会把输入事件分发给当前焦点窗口。PostMessage是“扮演系统”直接通知某个窗口“你收到事件了”即使这个窗口没有焦点。因此PostMessage可以实现后台发送但前提是目标窗口必须能处理这种“突如其来”的消息。3. 实操详解从获取句柄到发送消息理解了原理我们来看具体怎么做。整个过程可以分解为定位目标 - 准备消息 - 发送消息 - 后续处理。3.1 精确获取目标窗口句柄这是成功的第一步也是最容易出错的一步。句柄不对一切白费。1. 使用FindWindow和FindWindowEx这是最常用的方法。FindWindow通过类名和窗口标题查找顶层窗口。// 查找标题为“无标题 - 记事本”的窗口 HWND hwndNotepad FindWindow(NULL, L无标题 - 记事本); // 通过类名查找更精确记事本的类名是“Notepad” HWND hwndNotepad FindWindow(LNotepad, NULL);但很多现代程序如Chrome每个标签页都是一个独立窗口或者窗口标题动态变化。这时需要FindWindowEx来遍历子窗口。2. 使用EnumWindows进行高级遍历当窗口条件复杂时需要自己写回调函数枚举所有顶层窗口根据进程名、标题关键字、样式等条件进行过滤。BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { DWORD dwProcessId; GetWindowThreadProcessId(hwnd, dwProcessId); // 根据进程ID或窗口标题进一步判断 TCHAR szTitle[256]; GetWindowText(hwnd, szTitle, 256); if (wcsstr(szTitle, L目标关键词) ! NULL) { *(HWND*)lParam hwnd; // 找到目标传出句柄 return FALSE; // 停止枚举 } return TRUE; // 继续枚举 } HWND hTarget NULL; EnumWindows(EnumWindowsProc, (LPARAM)hTarget);3. 获取控件句柄如文本框要向记事本的编辑区发送文字需要找到它的编辑控件句柄。记事本的编辑控件类名通常是“Edit”。HWND hwndEdit FindWindowEx(hwndNotepad, NULL, LEdit, NULL);对于更复杂的界面如对话框中的按钮可能需要多层FindWindowEx来定位。实操心得句柄验证与动态更新永远不要假设一次找到的句柄永远有效。窗口可能被关闭、重建如浏览器刷新页面。在关键的发送操作前特别是循环发送时建议重新获取或验证句柄是否依然有效IsWindow函数。对于长时间运行的自动化脚本实现一个稳健的句柄查找和重试机制是必要的。3.2 构造并发送键盘消息假设我们已经获得了目标编辑框的句柄hwndEdit。基本发送// 发送一个‘A’键 UINT vkCode ‘A‘; // 虚拟键码 // 按下 PostMessage(hwndEdit, WM_KEYDOWN, vkCode, 0); // 释放 PostMessage(hwndEdit, WM_KEYUP, vkCode, 0);这段代码大概率没用。因为lParam参数是0这不符合系统生成的真实消息结构。正确构造lParamlParam是一个32位值其结构如下0-15位重复计数通常为1。16-23位扫描码OEM依赖通常可设为0系统会转换。24位扩展键标志如Alt键、小键盘键为1。29位上下文代码Alt键按下时为1。30位前一个键状态用于判断是首次按下还是重复。31位转换状态KEYDOWN为0KEYUP为1。手动构造非常复杂。更实用的方法是借用系统生成的消息或者使用MapVirtualKey和keybd_event的副作用来生成一个合法的lParam。但更常见的做法是直接发送WM_CHAR消息这对于输入文本来说往往更有效。推荐方案直接发送WM_CHAR对于简单的文本输入许多窗口过程更“认”WM_CHAR消息。// 发送字符‘A‘ PostMessage(hwndEdit, WM_CHAR, ‘A‘, 0); // 发送字符串需要循环 const char* szText Hello; for (int i 0; szText[i] ! ‘\0‘; i) { PostMessage(hwndEdit, WM_CHAR, szText[i], 0); Sleep(10); // 关键稍作延迟模拟人的输入节奏 }这里的Sleep(10)至关重要它模拟了人工按键之间的间隔。没有延迟消息可能会被合并或丢失导致乱码或程序无响应。发送组合键如CtrlV组合键需要按下多个键并注意状态位。// 发送 CtrlV (粘贴) // 1. 按下 Ctrl PostMessage(hwndEdit, WM_KEYDOWN, VK_CONTROL, 0); // 2. 按下 V PostMessage(hwndEdit, WM_KEYDOWN, ‘V‘, 0); PostMessage(hwndEdit, WM_CHAR, ‘v‘, 0); // 有时也需要CHAR消息 // 3. 释放 V PostMessage(hwndEdit, WM_KEYUP, ‘V‘, 0); // 4. 释放 Ctrl PostMessage(hwndEdit, WM_KEYUP, VK_CONTROL, 0);3.3 构造并发送鼠标消息鼠标消息需要客户区坐标。1. 获取目标窗口位置并转换坐标HWND hwndTarget ...; // 目标窗口句柄 POINT ptScreen {100, 200}; // 假设你想点击的屏幕坐标 POINT ptClient ptScreen; // 复制一份 ScreenToClient(hwndTarget, ptClient); // 关键转换为客户区坐标2. 发送鼠标移动和点击消息// 将坐标编码到lParam中X在低字Y在高字 LPARAM lParam MAKELPARAM(ptClient.x, ptClient.y); // 移动鼠标到指定位置 PostMessage(hwndTarget, WM_MOUSEMOVE, 0, lParam); // wParam通常为0 // 发送左键按下和释放一次点击 PostMessage(hwndTarget, WM_LBUTTONDOWN, MK_LBUTTON, lParam); // wParam指示按下的键 Sleep(50); // 点击需要一点按下时间 PostMessage(hwndTarget, WM_LBUTTONUP, 0, lParam);WM_LBUTTONDOWN的wParam参数需要包含MK_LBUTTON标志表示左键处于按下状态。而WM_LBUTTONUP的wParam通常为0。3. 发送双击消息双击是两次快速点击但系统通常将其识别为WM_LBUTTONDBLCLK消息。PostMessage(hwndTarget, WM_LBUTTONDBLCLK, MK_LBUTTON, lParam); PostMessage(hwndTarget, WM_LBUTTONUP, 0, lParam);注意有些窗口可能需要在发送WM_LBUTTONDBLCLK之前先发送一次WM_LBUTTONDOWN和WM_LBUTTONUP来模拟第一次点击这取决于目标程序的消息处理逻辑。4. 深度排查发送不成功的八大原因及对策当你按照上述步骤操作消息却石沉大海时请按以下清单逐一排查。4.1 句柄问题你找对“门”了吗这是最常见的问题。PostMessage成功返回TRUE只代表消息被成功放入队列不代表目标窗口处理了它。症状PostMessage返回成功但目标程序毫无反应。排查验证句柄有效性使用IsWindow(hWnd)检查句柄是否代表一个存在的窗口。检查窗口状态窗口是否被禁用IsWindowEnabled是否最小化最小化的窗口可能不处理某些消息。确认目标层级你真的找到了接收输入消息的那个子窗口吗用SpyVisual Studio工具或开源工具WinSpy等查看目标程序的窗口层次结构找到正确的编辑框、按钮句柄。权限问题跨进程发送消息尤其是向以更高权限如管理员身份运行的进程发送消息时低权限进程可能受限。尝试以管理员身份运行你的发送程序。4.2 消息类型问题它“听”得懂这种“语言”吗你发送的消息类型可能不是目标窗口期望处理的。症状发送键盘消息无效但发送鼠标消息可能有效或反之。排查与对策尝试WM_CHAR代替WM_KEYDOWN/UP对于文本输入这是最常成功的捷径。发送消息序列尝试发送更完整的序列。例如对于键盘输入按顺序发送WM_KEYDOWN-WM_CHAR-WM_KEYUP。可以尝试从Spy中捕获一次真实按键的消息流并模仿。使用SendMessageTimeout如果怀疑消息被阻塞可以用这个API。它允许你设置一个超时时间如果在指定时间内目标窗口没有处理完消息它就返回。LRESULT lResult; DWORD_PTR dwResult; if (SendMessageTimeout(hwndTarget, WM_CHAR, ‘A‘, 0, SMTO_ABORTIFHUNG, 1000, dwResult)) { // 消息在1秒内被处理 } else { // 目标窗口可能“挂起”或无响应 }4.3 焦点与前台窗口问题它在“专心”等你输入吗有些程序尤其是游戏或安全软件只响应具有输入焦点的窗口的消息或者会检查消息是否来自“前台”状态。症状只有当目标窗口是活动窗口时模拟才成功后台发送则失败。对策尝试附加线程输入这是一个高级技巧。使用AttachThreadInput函数将你的发送线程的输入队列附加到目标窗口线程的输入队列上。这样你的线程发送的消息在目标线程看来就像是“自己人”发的一样可能会被更友好地处理。DWORD dwTargetThreadId GetWindowThreadProcessId(hwndTarget, NULL); DWORD dwCurrentThreadId GetCurrentThreadId(); AttachThreadInput(dwCurrentThreadId, dwTargetThreadId, TRUE); // ... 在这里发送消息 ... AttachThreadInput(dwCurrentThreadId, dwTargetThreadId, FALSE); // 完成后分离警告AttachThreadInput使用不当可能导致线程死锁或输入混乱务必在try...finally或RAII机制中确保分离。结合SetForegroundWindow在发送前先将目标窗口切换到前台。但这会干扰用户且在某些系统如Windows 10/11上有限制。终极方案评估SendInput如果目标程序顽固地只接受来自系统输入层的消息那么PostMessage这条路可能走不通。此时应认真考虑切换到SendInput或keybd_event它们模拟的是物理输入不受窗口焦点和线程附加的限制。4.4 消息队列与状态问题它的“信箱”满了吗目标程序的消息队列可能被塞满或者它处于一个不处理消息的状态如模态对话框阻塞。症状间歇性失败或者程序在繁忙时模拟失效。对策增加延迟在连续发送多条消息如输入一段话之间插入Sleep(10-50)毫秒的延迟给目标程序处理消息的时间。检查窗口是否可响应使用IsHungAppWindow或SendMessageTimeout检测窗口是否“挂起”。避免在目标程序繁忙时发送例如避免在其进行大量计算或磁盘IO时模拟输入。5. 深度排查重复按键与消息风暴“重复按键”问题通常比“发送失败”更隐蔽也更令人头疼。5.1 真实重复与感知重复首先区分两种情况真实重复目标程序确实收到了多次相同的消息。这通常由发送方代码逻辑错误引起。感知重复发送方只发了一次但目标程序如游戏、编辑器的“按键重复”功能被触发产生了多个字符。这属于目标程序的行为不是模拟的问题。5.2 发送方导致的重复1. 循环或定时器失控这是新手最常见的错误。例如在一个按钮的Click事件里写了发送消息的代码但没有防止重复点击的机制用户快速点击两次就发送了两次。// Delphi示例错误的按钮事件处理 procedure TForm1.Button1Click(Sender: TObject); begin PostMessage(hEdit, WM_CHAR, Ord(‘A‘), 0); end;对策在操作期间禁用按钮或设置一个“正在发送”的标志位。procedure TForm1.Button1Click(Sender: TObject); begin if FIsSending then Exit; // 防止重复进入 FIsSending : True; try Button1.Enabled : False; PostMessage(hEdit, WM_CHAR, Ord(‘A‘), 0); Sleep(100); // 等待操作完成 finally Button1.Enabled : True; FIsSending : False; end; end;2.WM_KEYDOWN的重复计数当你发送WM_KEYDOWN时如果lParam中的“前一个键状态”位被错误地设置为1表示键之前已经是按下状态而“重复计数”又大于1系统或目标程序可能会将其解释为“自动重复”从而生成多个字符。对策确保你构造的lParam中第30位前一个键状态在发送WM_KEYDOWN时为0表示首次按下在发送WM_KEYUP后也为0。对于简单的WM_CHAR发送可以避免直接构造lParam的麻烦。3. 消息被意外投递多次在复杂的UI框架如.NET WinForms、WPF中一个用户事件可能会触发多个消息处理路径如果不小心可能导致发送代码被执行多次。对策仔细检查代码逻辑确保发送消息的函数或代码块在预期条件下只执行一次。使用调试器设置断点观察是否被意外命中多次。5.3 接收方目标程序导致的“重复”1. 目标程序的消息处理缺陷有些程序特别是那些自己处理消息循环或使用了特殊UI库的程序对消息的处理可能有bug。它们可能对WM_KEYDOWN和WM_CHAR都做出响应导致一个物理按键产生两次字符输入。当你用PostMessage同时发送了这两条消息就可能触发这个bug。对策尝试只发送WM_CHAR。如果必须发送WM_KEYDOWN/UP确保不要额外发送WM_CHAR。用Spy监视目标程序在真实按键时接收到的消息序列并严格模仿那个序列。2. 输入法编辑器IME的干扰在中文、日文等需要IME的输入环境下PostMessage发送的WM_CHAR消息可能会被IME拦截并转换产生不可预知的结果包括重复或错误的字符。对策如果可能在模拟输入前通过PostMessage发送WM_IME_SETCONTEXT消息暂时禁用IME。或者更简单粗暴的方法是在模拟输入时确保系统输入法已切换到英文状态。对于需要输入非英文字符的复杂情况PostMessage可能不是最佳选择应考虑SendInput。5.4 排查重复问题的步骤简化测试创建一个最简单的目标程序如一个标准Windows编辑框的记事本用你的代码向其发送单个字符。如果仍有重复问题肯定在发送方。使用消息监视工具在发送消息的同时用Spy监视目标窗口收到的消息。确认是否收到了重复的WM_CHAR或WM_KEYDOWN消息。检查发送代码的调用栈在发送函数入口处打日志或断点确认是否被意外调用多次。隔离线程问题如果你的发送代码在子线程或定时器中检查线程同步和定时器间隔是否设置合理。6. 进阶技巧与替代方案当PostMessage/SendMessage之路走到尽头时我们还有其他选择。6.1 何时选择SendInput在以下场景SendInput是比PostMessage更可靠的选择目标程序进行低级键盘钩子或输入验证一些游戏或安全软件会检查输入是否来自真实的硬件事件。SendInput在驱动层面模拟更难被检测为“软件模拟”。需要模拟全局快捷键PostMessage需要精确的窗口句柄来发送WM_KEYDOWN等消息模拟CtrlC而SendInput可以直接向系统注入这些按键无论焦点在哪。跨权限级别操作SendInput以当前输入桌面的焦点窗口为目标避免了跨进程权限的一些问题。需要模拟绝对鼠标移动或滚轮SendInput的MOUSEINPUT结构支持绝对坐标和滚轮刻度更强大。SendInput的基本用法INPUT inputs[2] {}; // 按下‘A‘键 inputs[0].type INPUT_KEYBOARD; inputs[0].ki.wVk ‘A‘; // 释放‘A‘键 inputs[1].type INPUT_KEYBOARD; inputs[1].ki.wVk ‘A‘; inputs[1].ki.dwFlags KEYEVENTF_KEYUP; UINT uSent SendInput(2, inputs, sizeof(INPUT)); if (uSent ! 2) { // 处理错误 }SendInput是一次性提交一个INPUT结构数组原子性更好更接近真实输入。6.2 驱动级模拟的考量keybd_event和mouse_event是旧的API虽然简单但在多线程发送或需要组合复杂事件时不如SendInput。SendInput是其现代替代品。对于极少数需要超高级别模拟或绕过所有防护的场景可能需要考虑内核模式的驱动但这涉及巨大的复杂性和安全风险远超一般应用需求不推荐普通开发者尝试。6.3 针对特定框架的优化Windows Forms / WPF (.NET)这些框架有自己封装的消息循环。虽然可以P/Invoke调用PostMessage但有时直接调用控件的SendKeys.Send或SendKeys.SendWait方法模拟系统输入更简单可靠。对于UI自动化微软的UI Automation框架是更现代、更强大的选择。Qt / Electron等跨平台框架它们的窗口消息处理可能与标准Win32窗口不同。PostMessage可能无效。需要研究该框架提供的特定自动化接口或模拟输入方法。7. 实战案例向后台记事本自动输入文本让我们用一个完整的、健壮的例子将上述所有知识点串联起来。目标向一个已存在的、可能处于后台的记事本窗口的编辑区输入字符串“Hello, PostMessage!”。#include windows.h #include string bool SendStringToNotepadEdit(const std::wstring targetWindowTitle, const std::string textToSend) { // 1. 查找记事本窗口 HWND hwndNotepad FindWindow(LNotepad, targetWindowTitle.c_str()); if (hwndNotepad NULL) { // 尝试用通用窗口标题查找 hwndNotepad FindWindow(NULL, targetWindowTitle.c_str()); if (hwndNotepad NULL) { OutputDebugString(L找不到记事本窗口。\n); return false; } } // 2. 查找编辑控件 HWND hwndEdit FindWindowEx(hwndNotepad, NULL, LEdit, NULL); if (hwndEdit NULL) { OutputDebugString(L找不到记事本编辑框。\n); return false; } // 3. 可选附加输入线程提高后台发送成功率 DWORD dwNotepadThreadId GetWindowThreadProcessId(hwndEdit, NULL); DWORD dwOurThreadId GetCurrentThreadId(); bool bAttached false; if (AttachThreadInput(dwOurThreadId, dwNotepadThreadId, TRUE)) { bAttached true; } else { OutputDebugString(L警告附加线程输入失败后台发送可能失效。\n); } // 4. 确保窗口至少是可见的非最小化状态更好 if (IsIconic(hwndNotepad)) { ShowWindow(hwndNotepad, SW_RESTORE); } // 5. 循环发送每个字符 for (char c : textToSend) { // 发送WM_CHAR消息这是最可靠的方式 if (!PostMessage(hwndEdit, WM_CHAR, (WPARAM)c, 0)) { DWORD err GetLastError(); // 可以记录错误 break; } // 关键延迟模拟人工输入速度避免消息被吞 Sleep(15); } // 6. 发送一个回车键如果需要 // PostMessage(hwndEdit, WM_KEYDOWN, VK_RETURN, 0); // Sleep(10); // PostMessage(hwndEdit, WM_KEYUP, VK_RETURN, 0); // 或者更简单 // PostMessage(hwndEdit, WM_CHAR, L‘\r‘, 0); // 7. 清理分离线程输入 if (bAttached) { AttachThreadInput(dwOurThreadId, dwNotepadThreadId, FALSE); } return true; }这个例子包含了错误处理、线程附加、状态恢复和关键延迟。在实际使用中你可能需要根据目标窗口的状态调整ShowWindow的逻辑或者处理发送失败的重试。8. 总结与最终建议模拟键盘鼠标输入PostMessage/SendMessage是一把精准但挑剔的手术刀。它轻量、高效、可后台操作但要求你对Windows消息机制和目标程序的行为有深入理解。我的最终建议是首选WM_CHAR对于文本输入优先尝试PostMessage发送WM_CHAR消息这是兼容性最好的方式。延迟是朋友在连续消息间加入Sleep(10-30ms)这是稳定性的关键。句柄是基石花时间用Spy等工具确认你获取的句柄是正确的并且窗口处于可接收消息的状态。从简到繁测试先在记事本、标准Edit控件上测试通你的逻辑再应用到复杂目标上。准备好备选方案当PostMessage在特定程序上无论如何都不奏效时不要死磕。SendInput作为系统级模拟的备选方案成功率要高得多尽管它会干扰前台用户操作。理解目标最后也是最根本的尝试去理解你的目标程序。它是一个标准Win32程序还是用了DirectX/OpenGL的游戏或是基于Chrome的Electron应用不同的技术栈对输入的处理方式天差地别。没有一种模拟方法能通吃所有场景掌握原理灵活选择才是解决问题的正道。