屏幕软键盘跨进程输入:SendInput、PostMessage与剪贴板方案选型
发布时间:2026/10/22 13:48:12 锦皓数字建站

简介这是一份面向Windows平台开发者的屏幕软键盘虚拟键盘源码工程适合需要实现无实体键盘输入、触摸屏交互或安全密码输入场景的开发者参考。资源以VC工程形式组织包含对话框界面、系统托盘、资源脚本与图标等模块可直接编译运行并集成到其他程序中帮助理解按键事件处理、字符编码转换与应用程序间输入注入的实现思路。压缩包共20个文件约32KB以h头文件与cpp源文件为核心辅以dsw/dsp工程文件、rc/rc2资源脚本、ico图标及txt使用说明结构紧凑、便于快速上手。目前已有163人学习下载。通过该工程读者可掌握屏幕键盘的界面布局设计、点击事件响应、字符发送机制以及系统托盘集成等关键环节并在此基础上扩展自定义布局、辅助输入或安全输入方案是学习GUI编程与输入法交互的实用参考案例。1. 屏幕软键盘到底解决什么问题从物理键盘失灵到跨进程注入屏幕软键盘这个词很多人第一反应是手机上的输入法面板或者 Windows 辅助功能里那个能用鼠标点的虚拟键盘。但标题里这串关键词指向的是另一类东西一个能脱离物理键盘、把字符送进其他程序输入框的软件键盘。注意后半句——“可通过软键盘向其他程序中输入字符”这才是核心。它要解决的不是“我自己有个界面能打字”而是“我打出来的字怎么进到记事本、浏览器、工控组态软件、收银系统这些我改不了源码的程序里去”。我最早接触这个需求是在一台触屏一体机上跑一套老旧的检测软件。那软件只认物理键盘的按键消息触摸屏自带输入法弹不出来现场又没有键盘。当时试过系统自带屏幕键盘结果在部分程序里点了没反应——字符发出去了目标窗口收不到。后来才明白屏幕软键盘的难点从来不是画一个键盘界面而是跨进程的字符投递。这篇文章就围绕这个方向把选型、实现、参数和踩坑讲清楚适合做上位机、工控 HMI、自助终端、无障碍辅助工具的开发者。2. 三种字符投递路线SendInput、PostMessage 与剪贴板粘贴怎么选要让软键盘的字符进到别的程序主流就三条路。选错路线后面全是玄学问题。这一章先把原理和选型理由讲透再给可跑的代码。2.1 路线一SendInput 模拟真实按键SendInput 是 Windows 提供的、把输入事件注入到系统输入流里的 API。它的特点是事件进入系统队列后由系统分发给当前前台窗口。也就是说它模拟的是“真的有人在敲键盘”。// SendInput 发送一个字符以 A 为例实际需处理大小写与 Shift #include windows.h void SendCharA() { INPUT inputs[2] {0}; // 按下 Shift因为 A 是大写 inputs[0].type INPUT_KEYBOARD; inputs[0].ki.wVk VK_SHIFT; // 按下 A 键 inputs[1].type INPUT_KEYBOARD; inputs[1].ki.wVk A; SendInput(2, inputs, sizeof(INPUT)); // 释放顺序反过来 INPUT ups[2] {0}; ups[0].type INPUT_KEYBOARD; ups[0].ki.wVk A; ups[0].ki.dwFlags KEYEVENTF_KEYUP; ups[1].type INPUT_KEYBOARD; ups[1].ki.wVk VK_SHIFT; ups[1].ki.dwFlags KEYEVENTF_KEYUP; SendInput(2, ups, sizeof(INPUT)); }逻辑说明SendInput 的第二个参数是 INPUT 数组一次调用可以塞多个事件系统按顺序处理。参数上最关键的是ki.wVk虚拟键码和ki.dwFlags是否 KEYEVENTF_KEYUP。字符和虚拟键码不是一一对应比如a和A都是 VK A靠 Shift 状态区分中文、特殊符号则要走 Unicode 路线用KEYEVENTF_UNICODE配合ki.wScan填字符编码这时wVk必须为 0。SendInput 的优点是兼容性最好绝大多数程序都认。缺点是它只发给前台窗口你的软键盘一弹出来抢了焦点字符就发给自己了。所以用这条路通常要配合“不抢焦点”的窗口样式或者发送前先把焦点还给目标窗口。2.2 路线二PostMessage 直接投递到目标窗口PostMessage 是往指定窗口的消息队列里塞一条消息不经过系统输入流。常见做法是发WM_CHAR或WM_KEYDOWN/WM_KEYUP。// 向指定窗口句柄发送一个字符 #include windows.h void PostCharToWindow(HWND hTarget, wchar_t ch) { // WM_CHAR 的 wParam 就是字符编码 PostMessageW(hTarget, WM_CHAR, (WPARAM)ch, 0); }逻辑说明hTarget是目标输入框所在窗口的句柄一般用FindWindow或WindowFromPoint拿到。WM_CHAR的wParam直接放字符lParam可以填 1 表示这是正常按键。这条路的好处是不需要目标窗口在前台你的软键盘可以一直显示字符照样进得去。但它的坑也最集中很多现代程序尤其是用了自绘控件、浏览器内核、Qt/Electron 的程序根本不处理WM_CHAR消息发过去石沉大海。所以 PostMessage 只适合目标程序是标准 Win32 编辑框的情况通用性远不如 SendInput。2.3 路线三剪贴板 粘贴这条路最“土”但最稳把字符写进剪贴板然后模拟 CtrlV。// 写剪贴板并触发粘贴简化版未含剪贴板打开失败重试 #include windows.h #include string void PasteText(const std::wstring text) { if (!OpenClipboard(nullptr)) return; EmptyClipboard(); size_t bytes (text.size() 1) * sizeof(wchar_t); HGLOBAL hMem GlobalAlloc(GMEM_MOVEABLE, bytes); if (hMem) { memcpy(GlobalLock(hMem), text.c_str(), bytes); GlobalUnlock(hMem); SetClipboardData(CF_UNICODETEXT, hMem); // 成功后内存归系统管 } CloseClipboard(); // 模拟 CtrlV keybd_event(VK_CONTROL, 0, 0, 0); keybd_event(V, 0, 0, 0); keybd_event(V, 0, KEYEVENTF_KEYUP, 0); keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0); }逻辑说明OpenClipboard可能因为别的程序占用而失败生产代码要加重试。SetClipboardData成功后hMem的所有权交给系统不能再自己GlobalFree。粘贴用keybd_event或 SendInput 都行本质还是模拟按键所以同样受前台焦点限制。三条路线的对比路线兼容性是否需前台典型适用主要风险SendInput高是通用输入抢焦点、UAC 提权窗口拦截PostMessage低否标准 Win32 控件自绘控件不响应剪贴板粘贴高是长文本、特殊符号污染用户剪贴板、焦点问题我的常规做法是默认走 SendInput遇到不响应的目标再降级到 PostMessage 试探长文本或含大量特殊符号时用剪贴板。三条路做成可切换的策略比死磕一条稳得多。3. 用 Win32 写一个最小可用的屏幕软键盘这一章给一个能跑起来的最小实现。目标一个不抢焦点的悬浮键盘窗口点按钮后字符进到记事本。3.1 窗口样式WS_EX_NOACTIVATE 是灵魂软键盘最大的矛盾是它要接收鼠标点击但又不能抢走目标窗口的键盘焦点。解决办法是给窗口加WS_EX_NOACTIVATE扩展样式。// 创建软键盘主窗口时的关键样式 HWND hKbd CreateWindowExW( WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW, // 关键扩展样式 LSoftKbdClass, L屏幕软键盘, WS_POPUP, // 无边框弹出窗 100, 100, 480, 200, nullptr, nullptr, hInstance, nullptr); // 点击按钮时不要用 SetFocus直接处理鼠标消息逻辑说明WS_EX_NOACTIVATE让窗口被点击时不激活、不抢焦点目标程序的前台状态得以保留。WS_EX_TOPMOST保证键盘浮在最上层。WS_EX_TOOLWINDOW让它不出现在 AltTab 列表里。注意加了 NOACTIVATE 后窗口收不到正常的键盘输入但鼠标点击照常这正是我们要的——用鼠标点按钮字符发给别人。3.2 按钮点击到字符投递的完整链路按钮用自绘或标准 BUTTON 都行核心是在点击处理里调用投递函数。// 按钮点击处理把字符发给当前前台窗口 void OnKeyButtonClick(wchar_t ch) { // 记录点击前的目标窗口软键盘自己不激活前台就是目标 HWND hTarget GetForegroundWindow(); if (!hTarget || hTarget g_hKbd) return; // 优先 SendInputUnicode 方式兼容中英文 INPUT in[2] {0}; in[0].type INPUT_KEYBOARD; in[0].ki.wVk 0; in[0].ki.wScan ch; in[0].ki.dwFlags KEYEVENTF_UNICODE; in[1] in[0]; in[1].ki.dwFlags KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; SendInput(2, in, sizeof(INPUT)); }逻辑说明用KEYEVENTF_UNICODE时wVk必须为 0字符放在wScan。这样中文、emoji 之外的常见字符都能发。GetForegroundWindow拿到的就是目标窗口因为软键盘不激活前台始终是用户正在操作的程序。如果发现某些程序收不到再补一条 PostMessage 的降级分支。3.3 参数怎么调重复延迟、大小写与修饰键软键盘要处理 Shift、Ctrl、CapsLock 这些状态否则打不出大写和符号。// 维护修饰键状态 bool g_shift false; bool g_caps false; wchar_t ResolveChar(wchar_t base, wchar_t shifted) { bool upper g_shift ^ g_caps; // 异或Shift 和 Caps 同时开等于没开 return upper ? shifted : base; } // 发送修饰键本身如用户点了 Shift 按钮 void ToggleShift() { g_shift !g_shift; }逻辑说明Shift 和 CapsLock 对字母的作用是异或关系两个都开反而变小写这是很多人写软键盘时翻车的地方。符号键则只受 Shift 影响不受 Caps 影响要分开处理。重复延迟方面如果做长按连续输入建议首次延迟 400ms、重复间隔 50ms和系统默认接近手感不会突兀。4. 避坑与排查字符发不出去时先看这五条这一章全是血泪经验。软键盘“点了没反应”是最常见的报障按下面顺序排查能省掉大量瞎试的时间。4.1 现象记事本能输入浏览器和某工控软件没反应原因这些程序用的是自绘控件或非标准输入模型不处理WM_CHAR而如果你当时走的正是 PostMessage 路线自然没反应。SendInput 一般能救但如果程序内部自己做了输入过滤也可能拦掉。解决先切到 SendInput 的 Unicode 模式试仍不行用剪贴板粘贴兜底。判断目标是不是自绘控件可以用 Spy 这类工具看窗口类名类名不是Edit、RichEdit之类的基本就是自绘。4.2 现象字符发到了软键盘自己身上原因软键盘窗口被激活抢了前台焦点SendInput 就把字符发给自己了。解决确认窗口带WS_EX_NOACTIVATE检查按钮点击处理里有没有误调SetFocus如果用了模态对话框模态本身会抢焦点软键盘不要用模态。4.3 现象目标程序以管理员权限运行时完全无效原因Windows 的 UIPI用户界面特权隔离机制低完整性级别的进程不能向高完整性级别的进程发送输入。你的软键盘是普通权限目标程序是管理员权限SendInput 会被系统静默丢弃。解决软键盘自身也以管理员权限运行。注意这会让它无法向普通权限程序发送——所以要么统一权限级别要么做两个版本。这是权限模型决定的没有绕过办法。4.4 现象中文输入时字符乱码或丢失原因用WM_CHAR发中文时wParam是 UTF-16 编码但有些老程序按 ANSI 处理或者一次只收一个字节。另外 SendInput 的 Unicode 模式对某些输入法框架不友好。解决中文优先走剪贴板粘贴最稳。如果必须用 SendInput确认wScan填的是 UTF-16 码元且wVk为 0。遇到代理对如部分生僻字要分两次发高低代理。4.5 现象连续快速点击时丢字符原因SendInput 的事件进入系统队列后需要时间处理点太快时后一个事件可能在前一个还没被消费时就发出目标程序来不及处理。PostMessage 同理消息队列积压会乱序。解决在每次投递后加一个短延时一般 10~30ms 就够。不要用Sleep阻塞 UI 线程放到独立发送线程里做队列。这个延时值可以做成可配置不同目标程序容忍度不一样。5. 进阶把软键盘做成可配置的输入策略引擎走到这里一个能用的软键盘已经成型了。但真正让它从“能用”到“好用”的是把投递策略做成可配置、可探测、可回退的引擎。我现在的习惯是软键盘启动时先对目标窗口做一次能力探测根据结果自动选路线而不是写死一条。探测思路很简单向目标窗口发一条无害的探测消息看它是否响应。更实用的做法是维护一张“程序-策略”映射表第一次遇到某个程序时人工确认一次之后记住。表可以存在本地配置文件里字段包括进程名、窗口类名、推荐策略、发送延时。# 策略配置示例伪代码实际用 JSON 或 ini 存 STRATEGY_TABLE { notepad.exe: {mode: sendinput, delay_ms: 10}, legacy_hmi.exe:{mode: postmessage, delay_ms: 30}, browser.exe: {mode: clipboard, delay_ms: 50}, }参数上delay_ms是最需要按程序调的。工控类程序普遍反应慢30ms 起步浏览器和现代应用 10~20ms 即可。mode的回退顺序建议是 sendinput → postmessage → clipboard逐级降级。验证方法写一个简单的测试窗口把收到的每条WM_CHAR和按键消息打日志然后用软键盘对着它发一串包含大小写、数字、符号、中文的字符串比对日志和预期。这个自测窗口我每次改投递逻辑都会跑一遍比在真实程序里试快得多。最后说个我踩过的坑早期我图省事把发送逻辑直接写在按钮的点击回调里结果长按连续输入时 UI 卡死。后来改成生产者-消费者队列按钮只负责入队独立线程负责按延时出队发送问题消失。这个结构改动不大但稳定性提升明显。做这类工具把“界面”和“投递”彻底解耦是我最想强调的一条习惯。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。