ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

屏幕软键盘.zip:用SendInput实现跨进程输入的原理与常见坑

屏幕软键盘.zip:用SendInput实现跨进程输入的原理与常见坑 简介屏幕软键盘是一款基于图形界面的虚拟输入工具面向触摸屏设备、公共计算机以及不便使用实体键盘的辅助输入场景既能解决无物理键盘时的字符录入问题也能在一定程度上规避键盘记录器的风险。这份压缩包提供的是一个完整的Visual C 6.0工程属于MFC对话框程序包含可编译源码与工程文件适合学习Windows桌面应用开发、键盘事件映射和消息传递机制的初中级开发者。压缩包共20个文件核心为5个头文件与4个C源文件配套项目工作区、资源脚本、图标、使用说明及文本笔记等整体仅32KB小巧清晰。已有163人学习下载。源码中涉及软键盘的界面布局、按键点击检测、字符编码输出以及系统托盘集成等关键实现读者可直接编译运行或在此基础上改造出适合自己项目的屏幕键盘对研究虚拟输入技术颇有参考价值。1. 屏幕软键盘.zip一个能跨进程输入字符的桌面工具到底在解决什么问题屏幕软键盘.zip 这类压缩包拆开之后通常就是一个能悬浮在其他窗口之上、把鼠标点按转成字符输入的小工具。它的价值不在“把键盘画在屏幕上”而在“向其他程序中输入字符”这条跨进程通道。很多人解压 zip 后第一次双击运行都会遇到同一个困惑键盘弹出来了点下去的字却全落在软键盘自己的输入框里目标记事本一个字符没收到。这个工具真正要解决的是两件事一是让软键盘点击时不抢焦点二是把点击准确注入到系统输入流。适合谁给工控机、收银台配触摸屏的人给无人值守设备写虚拟键盘的现场工程师以及想在自己 C# 项目里嵌入一个“能向外部程序打字”控件的开发者。2. 屏幕软键盘的核心原理与选型为什么“向其他程序输入”才是技术难点把一个键盘画出来只需要几十行代码真正的技术难点在输入通道。你点的不是目标程序窗口里的按钮而是软键盘自己的按钮这句点击要怎么变成目标程序收到的按键事件下面把原理拆开先说结构再说选型最后给一个 30 行就能跑通的验证程序。2.1 软键盘的三层结构界面层、输入引擎、焦点管理一套能用的屏幕软键盘我习惯拆成三层看。界面层负责“画得出来”。无边框窗口、置顶显示、不在任务栏出现这是基本要求。触摸场景下按钮尺寸要加大鼠标场景下要考虑误触这些都属于界面层的活。界面层本身不复杂WinForms、WPF、Electron 都能做选型只看你目标环境装了什么运行时。输入引擎负责“打得进去”。这是软键盘和普通键盘控件的本质区别——普通键盘控件把按键事件发给自己的文本框软键盘要把事件送进系统输入流让前台窗口以为收到了真实键盘输入。这条通道做得好不好直接决定工具是能用还是只能看。焦点管理负责“不抢焦点”。普通窗口点击时会激活自身软键盘一旦激活前台窗口就变成了软键盘自己输入引擎再努力也打不进目标程序。所以软键盘必须做到“可见但不可激活”。这属于新手最容易翻车的一层后面 3.4 和 5.1 会专门展开。三层里界面最简单焦点管理最容易被忽略输入引擎决定技术路线。2.2 跨进程输入的两条路线SendInput 与 UI Automation常见做法是两条路线选错一条后面全是坑。第一条是 SendInput 路线。它把按键事件送进系统输入队列目标程序接收到的和真实键盘按下没有任何区别。优点是兼容面广记事本、浏览器、自绘软件、老旧的 MFC 程序都能收缺点是受权限隔离限制低权限进程往高权限窗口注入会被系统拒绝全屏独占游戏需要额外走扫描码。第二条是 UI Automation 路线。它通过调用目标程序暴露的可访问性接口直接告诉控件“你的文本值应该是 xxx”。优点是语义精确能定位到具体输入框缺点是目标程序不实现 UIA 模式就完全失效而且速度慢适合做表单自动化脚本不适合做“键盘替身”。还有一条驱动级路线需要内核驱动签名企业级远程控制软件才用普通 zip 工具没必要碰。对比下来屏幕软键盘选型基本没有悬念你要假装一把真实键盘就必须走系统输入流SendInput 是这条路上最直接、最不依赖目标程序实现的选择。2.3 为什么选 SendInput 这条路线附一个 30 行验证程序我一般会先写一个最小验证程序确认 SendInput 在自己目标环境里能通再开始做界面。验证逻辑很简单获取当前前台窗口标题发送一个字母“A”看前台窗口是否收到。using System; using System.Runtime.InteropServices; using System.Text; public class SendInputDemo { [DllImport(user32.dll, SetLastError true)] static extern uint SendInput(uint nInputs, INPUT[] inputs, int cbSize); [DllImport(user32.dll)] static extern IntPtr GetForegroundWindow(); [DllImport(user32.dll)] static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); [StructLayout(LayoutKind.Sequential)] struct INPUT { public uint type; public KEYBDINPUT ki; } [StructLayout(LayoutKind.Sequential)] struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } static INPUT MakeKey(ushort vk, bool keyUp) { return new INPUT { type 1, // INPUT_KEYBOARD ki new KEYBDINPUT { wVk vk, dwFlags keyUp ? 2u : 0u } }; } public static void SendA() { IntPtr fg GetForegroundWindow(); var title new StringBuilder(256); GetWindowText(fg, title, title.Capacity); Console.WriteLine(当前前台窗口: title); INPUT[] inputs new INPUT[2]; inputs[0] MakeKey(0x41, false); // A 键按下 inputs[1] MakeKey(0x41, true); // A 键抬起 uint sent SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))); if (sent ! 2) Console.WriteLine(SendInput 失败错误码: Marshal.GetLastWin32Error()); } }逻辑说明写在两处先打印前台窗口标题是为了确认点击发出时目标确实是目标程序而不是软键盘自己然后按下和抬起必须成对发送只发按下不发抬起目标程序会认为键一直没松开。参数说明0x41 是字母 A 的虚拟键码windows 里查虚拟键码有现成表格记不住就查 MSDN 或直接用VkKeyScan(A)反查dwFlags 2是 KEYEVENTF_KEYUP 标志按下时是 0。错误码 5 代表拒绝访问后面第 5 章会专门讲权限问题。3. 把屏幕软键盘 zip 变成能用的桌面工具启动顺序与第一次跨进程输入拿到一个 zip 包别急着双击主程序。先确认里面是什么形态再按顺序启动最后用一个最小例子验证跨进程输入通了再去改布局和参数。3.1 解压 zip 之后先确认这些文件zip 解压之后屏幕软键盘这类工具常见做法是包含四类东西主程序 exe、依赖的运行时库dll、布局与按键映射的配置文件通常是 json 或 ini、以及图标和提示音等资源文件。如果拿到的是源码包还会有工程文件和第三方依赖目录。先用命令看一眼目录结构别凭感觉猜Get-ChildItem -Recurse | Select-Object FullName, Length逻辑说明这一步是为了确认主程序是不是绿色版有没有带齐依赖。很多软键盘工具发布时会把 .NET Framework 或 VC 运行库看成“用户机器上都有”的假设结果换一台干净的工控机就闪退。参数说明如果目录里只有 exe 和少量配置多半是绿色版直接跑如果有一堆 dll 且带vcredist或dotnet字样先装对应运行库。判断方法很简单右键 exe 看属性里的“目标框架”或者直接用dumpbin查依赖现场没工具就下一个依赖查看器也行。3.2 最小可启动骨架无边框置顶窗口 按钮事件先搭一个能弹出来的窗口骨架再往里面加输入逻辑。用 WinForms 做最轻量一个 Form 就能跑。using System; using System.Drawing; using System.Windows.Forms; public class KeyboardForm : Form { public KeyboardForm() { TopMost true; FormBorderStyle FormBorderStyle.None; ShowInTaskbar false; Size new Size(520, 200); StartPosition FormStartPosition.Manual; } protected override CreateParams CreateParams { get { CreateParams cp base.CreateParams; cp.ExStyle | 0x08000000; // WS_EX_NOACTIVATE点击时不激活本窗口 return cp; } } }逻辑说明TopMost保证软键盘漂浮在目标程序之上FormBorderStyle.None去掉标题栏因为软键盘不需要被拖拽关闭ShowInTaskbar false避免任务栏出现第二个图标造成混乱。参数说明最关键的其实是0x08000000这是 WS_EX_NOACTIVATE 扩展样式。加了这一行点击软键盘上的按钮时窗口不会获得焦点前台窗口始终保持在目标程序那里。这一步是整套方案的基石漏掉它后面所有 SendInput 都会打在软键盘自己身上。触摸屏使用时建议把按钮最小高度做到 48px 以上鼠标场景 32px 够用。3.3 向记事本输入 hello第一个可复现的 SendInput 调用骨架有了现在把输入引擎接进来。完整跑通“向记事本输 hello”这个场景需要把字符串拆成按键事件序列逐个发送。using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Threading; public class CrossProcessKeyboard { [DllImport(user32.dll, SetLastError true)] static extern uint SendInput(uint nInputs, INPUT[] inputs, int cbSize); [DllImport(user32.dll)] static extern short VkKeyScan(char ch); [DllImport(user32.dll)] static extern IntPtr GetForegroundWindow(); [DllImport(user32.dll)] static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); [StructLayout(LayoutKind.Sequential)] struct INPUT { public uint type; public KEYBDINPUT ki; } [StructLayout(LayoutKind.Sequential)] struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } static INPUT MakeKey(ushort vk, bool keyUp) { return new INPUT { type 1, ki new KEYBDINPUT { wVk vk, dwFlags keyUp ? 2u : 0u } }; } public static void SendStringToForeground(string text) { IntPtr hWnd GetForegroundWindow(); var title new StringBuilder(256); GetWindowText(hWnd, title, title.Capacity); var events new ListINPUT(); foreach (char c in text) { short vkResult VkKeyScan(c); ushort vk (ushort)(vkResult 0xFF); bool needShift (vkResult 0x100) ! 0; if (needShift) events.Add(MakeKey(0x10, false)); // Shift 按下 events.Add(MakeKey(vk, false)); // 字符键按下 events.Add(MakeKey(vk, true)); // 字符键抬起 if (needShift) events.Add(MakeKey(0x10, true)); // Shift 抬起 } uint sent SendInput((uint)events.Count, events.ToArray(), Marshal.SizeOf(typeof(INPUT))); if (sent ! events.Count) throw new InvalidOperationException(SendInput 失败错误码: Marshal.GetLastWin32Error()); Thread.Sleep(30); } }逻辑说明VkKeyScan返回一个 short低字节是虚拟键码高字节的标志位表示需不需要 Shift。比如小写 h 返回 0x48大写 H 返回 0x1848。这样就能针对每个字符决定是否补发 Shift 事件。发送完加一个Thread.Sleep(30)是给目标程序一个处理时间连续发字符串时没有这个间隔个别控件会丢字符。参数说明30ms 不是固定值老旧的文本控件或远程桌面环境建议调大到 60~80ms反过来如果只是发快捷键组合可以缩到 10ms。Marshal.GetLastWin32Error()只在SetLastError true时才有意义这行属性千万别省。返回值sent不等于发送数量时优先查错误码而不是重发因为重发通常只会让问题更乱。3.4 输入目标怎么定前台窗口、焦点与“抢焦点”问题很多人在这一卡就是半天明明 SendInput 返回成功目标就是收不到。原因一句话发送那一刻前台窗口已经不是目标程序了。软键盘点击按钮时如果窗口没有 WS_EX_NOACTIVATE系统会把前台窗口切换成软键盘自己SendInput 的按键全部进了软键盘的输入队列而软键盘又不处理这些键字符就凭空消失。解决思路是让软键盘从不参与焦点竞争同时在发送入口做一层防御。IntPtr GetCurrentTarget() { IntPtr fg GetForegroundWindow(); // 排除软键盘自身窗口如果前台是自己说明焦点被抢了 if (fg this.Handle) return lastTarget; // 用之前记住的目标窗口 lastTarget fg; return fg; }逻辑说明lastTarget是软键盘首次显示时记录的前台窗口句柄。点击发送时先判断当前前台是不是软键盘自己如果是就退回上一次记住的目标。这是一种防御式写法比单纯依赖 WS_EX_NOACTIVATE 更稳。参数说明如果发现lastTarget已经失效窗口被关掉了要允许重新获取否则发给一个不存在句柄的事件会被系统丢弃。这个防御逻辑只解决“焦点被抢”这一个问题权限、输入法、扫描码这些坑在第 5 章逐条说。4. 配置软键盘的关键参数按键映射表、布局切换与四个必调项输入通路通了之后剩下的工作都是配置层面的把按键映射表做好让不同现场能快速切换布局再调整几个决定手感的关键参数。4.1 按键映射表软键盘的配置文件怎么写一个可维护的软键盘按键布局绝对不能写死在代码里。常见做法是外置一份 JSON 映射表程序启动时加载。{ name: english_full, rows: [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], [q, w, e, r, t, y, u, i, o, p], [a, s, d, f, g, h, j, k, l], [z, x, c, v, b, n, m] ], keymap: { a: { label: a, default: a, shifted: A, vk: 65 }, 1: { label: 1, default: 1, shifted: !, vk: 49 } } }逻辑说明rows决定界面上的排布程序按二维数组画按钮keymap里每个键的default是普通输入值shifted是 Shift 组合后的值vk是虚拟键码。加载时遍历rows从keymap里取对应的键定义生成按钮。参数说明vk字段其实可以省略程序里用VkKeyScan(default[0])反查也行。我一般会显式写出来因为 Enter、Backspace、Tab 这类功能键没有字符可以反查必须固定虚拟键码。“不写死”只针对布局虚拟键码这种直接对应系统 API 的东西写清楚反而少踩坑。4.2 三种常用布局全键盘、数字键盘、功能键面板不同场景需要不同布局配置文件的优势这时候就体现出来了。改一行name就能切换整套键盘。布局适用场景按键密度注意点英文全键盘普通 PC 输入、工控机高需要 Shift 状态处理数字小键盘收银、扫码枪输入、进制输入低按钮做大触摸友好功能键面板快捷键、远程协助场景低配合 Ctrl/Alt/Win 组合数字小键盘的映射表最省事每个键都是单个字符不需要 Shift 组合功能键面板则要在keymap里加一类type: special比如{label: Enter, type: special, vk: 13}。布局切换时最常见的坑是rows和keymap的 key 对不上程序跑起来发现某个按钮点了没反应。排查方式很简单加载时遍历一遍看有没有keymap里缺失的键名直接在启动日志里打警告。4.3 四个必调参数延迟、按住时长、重复触发、DPI 感知参数调得好不好直接决定软键盘是“能用”还是“难用”。以下四个参数我每次到现场都会重新过一遍。参数常用初值作用调参依据按键间延迟30~80ms目标程序处理单次按键的时间目标越卡延迟调越大按住时长50ms长按、组合键识别游戏类场景要求精确适当调小重复触发阈值400ms连续点击同一键的判定间隔触摸屏场景调小避免误触发DPI 感知true按钮坐标与系统坐标对齐系统缩放 125% 以上必须开延迟和按住时长的区别很多人搞混。延迟是两个按键事件序列之间的间隔按住时长是同一个键 down 和 up 之间的间隔。发送一个“a”down 之后等 50ms 再 up再等 30ms 发送下一个键。组合键比如 CtrlC需要把按住时长拉长到 80ms 左右保证目标程序识别到同帧的按下状态。DPI 感知这个参数最容易暗算人。不开它系统把软键盘窗口按 125% 缩放绘制但鼠标点击坐标还按逻辑像素算现象就是点“A”却触发到“S”附近。WinForms 程序在 app.manifest 里加一段声明就能解决application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware /windowsSettings /application逻辑说明这段声明告诉系统“我自己处理 DPI 缩放”系统就不会再对窗口做位图拉伸按钮坐标和系统坐标就一一对应了。参数说明如果目标程序本身是老式 DPI 缩放可能需要改成PerMonitorV2数值为 2但那个要求每个窗口都响应WM_DPICHANGED复杂度高不少。普通内部工具先把上面这个true加上能解决九成问题。5. 屏幕软键盘常见输入失效问题5 个踩坑现象与排查路径这章是血泪经验集。输入失效类问题有个共同特点程序不报错、日志也不报错就是目标收不到。以下五条按发生率排序现场排查时从第一条开始验。5.1 字符打不进去且无报错现象软键盘能弹出来点按钮也有反应SendInput 返回值正确但目标程序一个字符都没收到。原因目标窗口不在前台。最常见的是软键盘点击时把焦点吸走了第 3.4 章的防御逻辑没做或者做了但没覆盖“目标窗口被最小化”的情况。另外最小化的窗口即使在前台列表里SendInput 也不会自动帮你还原窗口再输入。解决发送前检查前台窗口句柄和软键盘自身句柄是否相同相同就先用SetForegroundWindow(lastTarget)把焦点还回去再发送。如果直接调用SetForegroundWindow无效系统前台锁限制用AttachThreadInput绑到目标线程再设前台发送完立刻分离AttachThreadInput(GetCurrentThreadId(), GetWindowThreadProcessId(hWnd, out _), true); SetForegroundWindow(hWnd); AttachThreadInput(GetCurrentThreadId(), GetWindowThreadProcessId(hWnd, out _), false);逻辑说明AttachThreadInput让两个线程共享输入状态绕过前台锁的约束。这个技巧适用于软键盘这种“用户明显在操作自己”的场景正常内部工具不会触发安全拦截但如果目标窗口属于高完整性进程还是会失败那就是权限问题走 5.2。参数说明这段代码里的“分离”必须执行哪怕SetForegroundWindow失败了也要分离否则线程输入状态会一直绑定影响后续所有键盘事件。5.2 管理员权限的目标程序直接无视输入现象往普通记事本输入一切正常但往“以管理员身份运行”的编辑器或安装程序输入时SendInput 返回 0 或错误码 5。原因UIPI 隔离。低完整性进程不能向高完整性进程注入输入事件这是系统安全机制不是软键盘的 bug。错误码 5 就是 ERROR_ACCESS_DENIED。解决最省事的方式是让软键盘进程也以管理员身份运行。公司内部工具直接改 manifestrequestedExecutionLevel levelrequireAdministrator uiAccessfalse/逻辑说明requireAdministrator会让 UAC 弹一次确认之后进程就是高完整性向同样高完整性的目标程序注入输入没有问题。参数说明提权之后要留意配置文件写路径。如果映射表放在 Program Files 目录普通用户权限下可能写不进去建议把配置文件放到%APPDATA%下面程序第一次启动时自动迁移。还有uiAccess这个属性只能为 false除非做了数字签名和 UI 自动化权限申请否则设 true 会导致程序根本起不来。5.3 大小写与 Shift 状态错乱键盘和输出对不上现象软键盘上点“”目标输出的是“1”点大写字母目标输出的是小写。原因发送字符时没有检查目标窗口当前的大小写状态和 Shift 状态。VkKeyScan(!)返回的低字节是 0x31数字 1 的虚拟键高字节标志位要求按 Shift但如果你发送时目标恰好处于大写锁定的状态系统会把两个修饰条件叠加结果就乱了。解决最简单可靠的路径是绕开虚拟键直接发 Unicode 字符事件INPUT unicodeInput new INPUT(); unicodeInput.type 1; unicodeInput.ki.dwFlags 4; // KEYEVENTF_UNICODE unicodeInput.ki.wVk 0; unicodeInput.ki.wScan (ushort); SendInput(1, new[] { unicodeInput }, Marshal.SizeOf(typeof(INPUT)));逻辑说明KEYEVENTF_UNICODE 事件不经过键盘布局翻译直接把字符送进输入流Shift 和 CapsLock 状态都不会干扰它。这个方法对中文、日文、特殊符号尤其友好。参数说明缺点是老式程序或部分游戏不认 Unicode 事件。遇到这种目标就只能退回虚拟键方案并在发送前读取目标线程的 CapsLock 状态如果状态和要发送的字符不一致先补发一次VK_CAPITAL切换再发送字符。5.4 全屏游戏或无边框窗口里输入丢失现象普通 Windows 程序下一切正常切到全屏游戏或某些自绘 DirectUI 程序就丢键或者按下之后只是光标动一下。原因全屏独占模式下软键盘窗口根本没有被渲染用户看到的可能是假的画面层另外不少引擎用 Raw Input 或 DirectInput 读取输入它们直接绑定硬件扫描码SendInput 默认发虚拟键码引擎不认识。解决把发送事件从虚拟键模式切换成扫描码模式。用MapVirtualKey把虚拟键转成扫描码再带上 KEYEVENTF_SCANCODE 标志ushort scan (ushort)MapVirtualKey(vk, 0); ki.wVk 0; ki.wScan scan; ki.dwFlags KEYEVENTF_SCANCODE | (keyUp ? KEYEVENTF_KEYUP : 0);逻辑说明扫描码模式更接近物理键盘上报的数据能兼容一批只用 Raw Input 的程序。注意wVk要置 0否则部分程序会因为同时收到虚拟键和扫描码而触发两次事件。参数说明这只是提高兼容性的手段不是绕过规则的工具。在正常业务软件、远程桌面、自研的触摸输入终端上使用没有问题游戏环境里请遵守平台输入约定软键盘只适合给无障碍输入或设备配套使用。扫描码方案在 64 位系统上注意MapVirtualKey的返回类型它返回的是 UINT转 ushort 时要防止溢出。5.5 中文输入法状态下英文按键变成拼音上屏现象目标窗口开着中文输入法软键盘点“hello”结果上屏的是一串拼音字母或者弹出候选条。原因SendInput 把按键送进系统输入流后输入法先截获了字母进入组词流程。软键盘发的“h”被输入法吃掉而不是直接上屏。解决如果目标窗口是自家程序主动把 IME 状态关掉IntPtr hImc ImmGetContext(targetWindow); if (hImc ! IntPtr.Zero) { ImmSetOpenStatus(hImc, false); ImmReleaseContext(targetWindow, hImc); }逻辑说明ImmGetContext拿到目标窗口的输入法上下文句柄ImmSetOpenStatus(hImc, false)把它切到关闭状态。这个过程必须在发送按键之前完成发送完如果需要恢复再把 true 传回去。参数说明对第三方窗口你没有权限改它的 IME 状态常见规避方案是在软键盘界面上放一个“发送前关闭中文输入法”的开关配置里记录当前输入法发送前临时切换发送后恢复。这个操作涉及PostMessage模拟切换输入法快捷键不同输入法表现差异很大能适配自家产品就优先适配自家产品。指望一套代码通吃所有输入法是玄学别在这上面死磕把边界写在配置文档里才是正路。6. 进阶给软键盘加按键发送日志验证每一次输入都真正落地输入丢了不可怕可怕的是不知道丢在哪一步。我建议所有屏幕软键盘工具一上来就内置发送日志它能把“玄学问题”瞬间变成“一眼看懂的问题”。6.1 一个简单的按键发送日志在发送函数外面包一层每次发送完成写一行 CSV时间、发送内容、目标窗口句柄、目标窗口标题、SendInput 返回值、错误码。public static void SendWithLog(string text, IntPtr target, string title, uint sent, int errorCode) { string line string.Format({0:HH:mm:ss.fff},{1},0x{2:X},{3},{4},{5}, DateTime.Now, text, target.ToInt64(), title, sent, errorCode); File.AppendAllText(key_log.csv, line Environment.NewLine); }逻辑说明调用点在每次 SendInput 之后立刻执行。sent是实际发出去的按键数量errorCode是Marshal.GetLastWin32Error()的值。这条日志写下来输入链路里任何一个环节出问题都能从日志里定位。参数说明CSV 文件要注意吞吐量。如果软键盘用于连续输入每条按键都写磁盘会造成 IO 抖动建议先在内存缓冲区里累积满 50 条再批量刷盘。日志保留最近 7 天启动时检查文件大小超过 5MB 就滚动覆盖不然工控机上磁盘撑爆是个隐患。6.2 用日志说话常见日志形态定位三实例第一类日志里目标窗口标题是KeyboardForm软键盘自己说明焦点被抢了走第 5.1 条的AttachThreadInput方案。第二类日志里目标窗口标题正确但errorCode是 5说明权限不够进程需要提权走 5.2。第三类日志里sent正确且errorCode为 0但目标确实少字符说明发送节奏太快把按键间延迟从 30ms 调到 60ms 再试。日志最大的价值就在这里它先帮你把“问题出在发送前、发送中还是发送后”这一步判断做完剩下的只是对症下药。我最早做软键盘时没有日志目标窗口收不到字符就盯着屏幕干瞪眼一会儿怀疑是系统问题一会儿怀疑是目标程序太老折腾了半下午。后来在发送入口加了一行简单日志五分钟就定位到是抢焦点。从那以后凡是涉及系统级输入的工具我都先把日志功能做成默认配置再谈其他。这个习惯帮我避开了很多“看起来毫无规律”的翻车也希望帮到你。本文还有配套的精品资源点击获取
返回列表