☰
屏幕软键盘实战:消息注入、焦点抢占与输入法兼容全攻略
2026/10/6 8:11:16 网站建设 项目流程

简介:输入模拟是Windows自动化与辅助工具的基础能力,涉及键盘消息注入、焦点管理、输入法兼容等关键技术。理解底层原理,才能应对不同程序的输入策略。在触屏一体机、展厅签到、工业手持终端等无键盘场景下,软键盘作为输入替代方案,需要解决“模拟按键如何被目标窗口正确识别”的核心问题。围绕屏幕软键盘的工程实现路径,从窗口样式、键位映射到SendInput调用,代码级讲解;针对焦点抢占、UIPI限制、组合键时序等实测踩坑逐一攻破。适合有意开发虚拟键盘或需要后台模拟输入的程序员参考。

1. 屏幕软键盘.zip:一个置顶小窗口,解决向其他程序输入字符的问题

拿到“屏幕软键盘.zip”这类包,解压后你会发现它往往不是网页,也不是小程序工程,而是一个独立的桌面小工具:悬浮在屏幕最上层,点击它的键帽,它把鼠标点击翻译成标准键盘消息,送进当前前台窗口或其他指定程序的输入框。这个需求常出现在触屏一体机、展台签到、仓库手持终端等场景——没有实体键盘,又要操作另一个程序录入内容。这篇文章就围绕“自己动手实现一个屏幕软键盘”展开,从消息通道选型、键位映射表、焦点策略到输入法兼容,按一条可复现的 Win32 实现路径讲透,适合做触摸屏配套工具、自助终端和演示机的人照着落地。

2. 输入消息的三个通道:SendMessage、SendInput 与低层钩子的适用边界

屏幕软键盘的本质,是把鼠标点击转译成键盘消息,再注入目标程序。这里最大的坑在于:同样一个“字符 A”,可以通过完全不相同的路径送达,而目标程序对不同路径的“信任度”完全不同。选错通道,记事本能打字,游戏里却没反应,或者反过来。

2.1 PostMessage 定向投递:适合后台窗口,但对游戏与安全输入失效

PostMessage 和 SendMessage 直接把 WM_KEYDOWN、WM_CHAR、WM_KEYUP 消息塞进目标窗口的消息队列或窗口过程。它的最大优点是“后台输入”:目标窗口不需要获得焦点,软键盘可以一边保持自己的置顶窗口,一边往另一个窗口发消息。很多截屏工具、按键精灵脚本就是靠这一招。

但问题也很明显。这类消息没有经过系统输入线程,GetAsyncKeyState 和 GetKeyboardState 这类状态查询接口完全读不到,DirectInput、Raw Input 以及大量自绘控件的输入逻辑也不认它。也就是说,PostMessage 适合的目标窗口是标准的 Win32 编辑框、RichEdit、部分 MFC 控件,以及没有做键盘状态自检的普通程序。游戏、安全输入框、带加密键盘的网银控件,基本都会无视。

在我自己项目里的经验是:PostMessage 更适合做“后台快捷键”,而不是做通用输入。原因是它构造消息序列时很容易出错——只发 WM_CHAR 不发 WM_KEYDOWN,很多控件会丢字符;发了 WM_KEYDOWN 但 dwlParam 里的扫描码、重复计数不对,某些控件也会拒绝处理。真要在后台往目标窗口打字,至少要构造完整的 WM_KEYDOWN → WM_CHAR → WM_KEYUP 序列,并且把 lParam 里的 repeat count、scan code、extended key 标志都填对,这比按一下键码要累得多。

2.2 SendInput 全局模拟:最接近真实按键,但要盯住焦点

SendInput 把 KEYBDINPUT 结构注入系统输入队列,系统会把它当作物理键盘产生的事件同等处理。这意味着:GetAsyncKeyState 能读到状态,DirectInput 能读到,Raw Input 也能读到,几乎所有按“真实键盘”逻辑运行的程序都能识别。因此,当前前台窗口是谁,字符就进谁;软键盘自己绝不能抢焦点。

这是绝大多数屏幕软键盘的正路,也符合标题里“向其他程序中输入字符”的诉求。缺点也很直观:目标窗口必须处于激活状态。如果用户在触摸屏上点了软键盘,软键盘的窗口却抢走了焦点,那字符就会打到软键盘自己的窗口里,这在后面会专门讲怎么规避。

SendInput 另一个隐蔽限制是 UIPI(用户界面特权隔离)。如果目标窗口以管理员权限运行,而你的软键盘是普通权限,那么 SendInput 会静默失败——字符进不去,也不报错。这是很多人在排查时最容易忽略的一点:明明代码没问题,换一个窗口就能打字,唯独目标程序没反应。

2.3 低层键盘钩子:适合拦截与改键,不适合当注入主力

WH_KEYBOARD_LL 低层键盘钩子经常被用来做全局热键、按键拦截、按键映射。它也能改键:钩子回调里把按键消息吞掉,再向目标窗口注入另一个键。但把这个机制当作屏幕软键盘的主输入通道,属于吃力不讨好。

原因有三。第一,低层钩子回调运行在安装钩子的线程上下文中,任何阻塞都会拖慢整个系统的键盘响应;第二,如果钩子回调里调用 SendInput 模拟按键,极易形成递归——SendInput 产出的按键事件同样会经过低层钩子,你不加递归标志,就会把自己模拟的按键再拦一遍;第三,钩子回调里做窗口查找、命中测试这类耗时操作,会让系统键盘出现肉眼可见的卡顿。

所以我的结论很明确:低层钩子在软键盘项目里只适合做“开关”或“屏蔽物理按键”,不适合做输入主力。要是看到有人推荐用低层钩子实现软键盘输入,多半只是炫技,不是最优方案。

2.4 三个通道的选型对照表

通道是否更新 GetAsyncKeyStateDirectInput/游戏是否识别是否需要目标窗口焦点典型适用场景
PostMessage / SendMessage否否否后台操作标准 Win32 控件
SendInput是是是屏幕软键盘首选,触摸屏录入
低层键盘钩子注入是是是改键工具、按键拦截,不建议注入

选型结论:你的屏幕软键盘一定以 SendInput 为主。只有当明确遇到“目标程序是标准控件且不希望它抢焦点”这种特殊约束时,才考虑加一条 PostMessage 的定向投递作为辅助,这就是后面第 4 章要展开的“混合模式”。

3. 搭建第一个可用屏幕软键盘:窗口样式、键位映射与点击输入

通道选好之后,就直接进入实现。我以 Win32 API 为例,不引入 MFC、Qt 或 Electron,原因是软键盘本身就是一个“小、快、不抢焦点”的组件,用原生 API 做出来的成品只有几十 KB,依赖少,部署时打个 zip 解压就能跑,这正好对应你在搜索引擎里见到“屏幕软键盘.zip”这种包的感觉。

3.1 窗口样式与创建参数:置顶、不抢焦点、可调透明度

软键盘窗口不能是普通主窗口,否则它会出现在任务栏、点击时会抢焦点、背景不透明还会挡住后面内容。创建一个适合软键盘的窗口,扩展样式是关键:

#include <windows.h> const wchar_t* kClassName = L"ScreenSoftKey"; const int kWindowW = 640; const int kWindowH = 220; const int kAlpha = 220; // 0 ~ 255,220 表示轻度半透明 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); HWND CreateSoftKeyWindow(HINSTANCE inst) { WNDCLASSEXW wc = { sizeof(wc) }; wc.lpfnWndProc = WndProc; wc.hInstance = inst; wc.hCursor = LoadCursorW(NULL, IDC_HAND); wc.lpszClassName = kClassName; RegisterClassExW(&wc); HWND hwnd = CreateWindowExW( WS_EX_NOACTIVATE | // 点击时不激活本窗口,焦点不被抢走 WS_EX_TOOLWINDOW | // 不在任务栏显示图标 WS_EX_TOPMOST | // 置顶显示 WS_EX_LAYERED, // 允许设置透明度 kClassName, L"ScreenSoftKey", WS_POPUP, 0, 0, kWindowW, kWindowH, NULL, NULL, inst, NULL); SetLayeredWindowAttributes(hwnd, 0, kAlpha, LWA_ALPHA); return hwnd; }

四个扩展样式缺一不可。WS_EX_NOACTIVATE 保证用户点击软键盘时,当前前台窗口不会失焦;WS_EX_TOOLWINDOW 让软键盘不出现在 Alt+Tab 切换列表和任务栏;WS_EX_TOPMOST 保证软键盘永远覆盖在目标程序上方;WS_EX_LAYERED 配合 SetLayeredWindowAttributes 做半透明,透明度值一般取 200~240,太透明了键帽看不清,太实了又挡内容。

注意:WS_EX_NOACTIVATE 只是“不激活”,但鼠标点击消息仍可能被某些窗口管理器逻辑处理掉,所以还要在窗口过程里显式拦截 WM_MOUSEACTIVATE 返回 MA_NOACTIVATE,双重保险。

3.2 按键映射表:用坐标网格描述键位

软键盘的键位不能写死在绘制代码里,否则改一个键帽位置就要重编译。常见的做法是定义一张键位结构体数组,每个键包含位置、尺寸、显示文本、虚拟键码和修饰键标志:

struct KeyDef { int x, y, w, h; // 在软键盘窗口内的像素位置 const wchar_t* label; // 键帽上显示的文字 WORD vk; // 虚拟键码,如 'A'、VK_SHIFT WORD scan; // 扫描码,0 表示交给系统从 vk 映射 BYTE modMask; // 组合键标志:1=Shift,2=Ctrl,4=Alt }; static KeyDef g_keys[] = { {0, 0, 70, 50, L"Esc", VK_ESCAPE, 0, 0}, {75, 0, 70, 50, L"Tab", VK_TAB, 0, 0}, {150, 0, 70, 50, L"Caps", VK_CAPITAL,0, 0}, {225, 0, 70, 50, L"Shift", VK_SHIFT, 0, 1}, {0, 55, 90, 50, L"Ctrl", VK_CONTROL,0, 2}, {100, 55, 60, 50, L"A", 'A', 0, 0}, // ... 字母、数字、方向键、空格键按同样格式继续铺 {540, 170, 100, 50, L"Space", VK_SPACE, 0, 0}, };

这里有几个注间点为什么不写死。label 与 vk 是两回事:Caps Lock 键帽上写着“Caps”,但虚拟键码是 VK_CAPITAL;A 键帽上写“A”,虚拟键码就是 0x41。scan 字段一般填 0,SendInput 时会用 MapVirtualKey 自动生成扫描码;但如果你服务的设备键盘布局特殊(比如日语键盘),就得在表里硬编码扫描码,否则会出现“按软键盘 A 键,目标程序收到的是别的字符”这种奇怪问题。

modMask 是处理 Shift+字母这类组合键的关键。单独点 A 键,modMask 为 0;要做一个“Shift+A”的键帽,就把 modMask 置 1,点击时先按下 Shift,再按 A,然后按相反顺序松开。这个顺序放后面代码里说明。

3.3 命中测试与 SendInput 调用:点击键帽发出键盘事件

窗口过程收到 WM_LBUTTONDOWN 时,根据鼠标坐标遍历上面的 g_keys 数组,用 PtInRect 做命中测试,命中后就调用 SendInput 发送键盘事件:

void SendKey(WORD vk, BOOL down) { INPUT in = { 0 }; in.type = INPUT_KEYBOARD; in.ki.wVk = vk; in.ki.wScan = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC); in.ki.dwFlags = down ? 0 : KEYEVENTF_KEYUP; in.ki.time = 0; in.ki.dwExtraInfo = 0; SendInput(1, &in, sizeof(INPUT)); } void ClickKey(const KeyDef* key) { if (key->modMask & 1) SendKey(VK_SHIFT, TRUE); if (key->modMask & 2) SendKey(VK_CONTROL, TRUE); if (key->modMask & 4) SendKey(VK_MENU, TRUE); SendKey(key->vk, TRUE); Sleep(2); // 极小延迟,防止目标程序把按下抬起识别为抖动 SendKey(key->vk, FALSE); if (key->modMask & 1) SendKey(VK_SHIFT, FALSE); if (key->modMask & 2) SendKey(VK_CONTROL, FALSE); if (key->modMask & 4) SendKey(VK_MENU, FALSE); } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_LBUTTONDOWN: { int px = GET_X_LPARAM(lp); int py = GET_Y_LPARAM(lp); for (size_t i = 0; i < _countof(g_keys); i++) { RECT rc = { g_keys[i].x, g_keys[i].y, g_keys[i].x + g_keys[i].w, g_keys[i].y + g_keys[i].h }; POINT pt = { px, py }; if (PtInRect(&rc, pt)) { ClickKey(&g_keys[i]); break; } } return 0; } case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProcW(hwnd, msg, wp, lp); }

这段代码里,Sleep(2) 是刻意加的。注入的按键不像物理键盘有硬件层面的按下时长,如果按下和抬起间隔太短,有些程序会当成无效抖动丢弃;2 毫秒只是底线,如果目标程序对时序敏感,比如某些游戏需要 10~20 毫秒才能识别,就应该把 Sleep 值提到配置里。

组合键的按下顺序是“修饰键先按,主键后按,抬起时镜像”。这套逻辑对 Shift+字母、Ctrl+C、Alt+Tab 都成立。要注意的是:VK_MENU 对应键盘上的 Alt 键,如果只按 Alt 不按其他键,很多程序会弹出菜单栏焦点,所以 Alt 组合键要格外确认按键序列完整。

3.4 长按连续输出与组合键:把“按住”效果补回来

真实键盘按住方向键不放,光标会连续移动;但 SendInput 每调用一次只产生一次按键事件,不会自动重复。要让软键盘支持“长按”,必须自己实现 autorepeat:

#define TIMER_REPEAT 1001 #define REPEAT_DELAY_MS 45 // 重复间隔,真实键盘系统默认约 250ms 后开始 static const KeyDef* g_pressedKey = nullptr; static HWND g_hwndOwner = nullptr; case WM_LBUTTONDOWN: { // 找到命中的 key 后 g_pressedKey = key; g_hwndOwner = hwnd; SendKey(key->vk, TRUE); SetTimer(hwnd, TIMER_REPEAT, REPEAT_DELAY_MS, NULL); return 0; } case WM_TIMER: if (wp == TIMER_REPEAT && g_pressedKey) { // 只发按下,不发抬起,Windows 会自动把它当作连续按键处理 SendKey(g_pressedKey->vk, TRUE); } return 0; case WM_LBUTTONUP: if (g_pressedKey) { KillTimer(hwnd, TIMER_REPEAT); SendKey(g_pressedKey->vk, FALSE); g_pressedKey = nullptr; } return 0;

这里有个细节值得强调:WM_LBUTTONUP 里必须发一次 keyup,否则如果用户只点击了一下,目标程序会认为这个键一直被按住。自动重复的间隔建议 45~60 毫秒,比系统默认的重复延迟快得多,这是为了照顾触摸屏上“点到为止”的操作习惯;如果做的是模拟实体键盘的工具,可以改成 250 毫秒起步的经典节奏。

4. 把键盘接进目标程序:焦点策略、输入法兼容与三种对接方式

窗口能弹出来了,按键也能发出去了,但字符能不能落到目标程序里,是另一回事。这一章处理的都是实际使用中让软键盘“不好用”的细节:焦点被抢、输入法弹拼音、后台窗口收不到。

4.1 焦点策略:WS_EX_NOACTIVATE 与 WM_MOUSEACTIVATE 缺一不可

只设置 WS_EX_NOACTIVATE 还不够。窗口样式只是告诉系统“不要因为点击而激活”,但鼠标点击消息本身仍可能触发默认激活逻辑。正确的做法是在窗口过程里显式拦截:

case WM_MOUSEACTIVATE: return MA_NOACTIVATE;

MA_NOACTIVATE 告诉系统:这次鼠标点击不要激活这个窗口,也不要取消当前前台窗口的激活状态。两个机制配合后,软键盘点击键帽时,焦点始终留在目标程序里,SendInput 才能准确地把字符送进目标窗口。

这条经验是从一个实际项目里踩出来的:早期版本只设置了 WS_EX_NOACTIVATE,发现点软键盘上的“A”,字符偶尔会跑到软键盘自己的窗口里,或者干脆丢进任务栏。后来加上 WM_MOUSEACTIVATE 返回 MA_NOACTIVATE,问题立刻消失。这个现象在做触摸屏程序时尤其明显,因为触摸点击和鼠标点击的激活行为不完全一致。

4.2 输入法兼容:KEYEVENTF_UNICODE 怎么发中文与特殊符号

屏幕软键盘如果只面向 ASCII 字符,直接用虚拟键码就够了。但要往目标程序输入中文、日文或特殊符号,问题就复杂了。

常见做法有两种。第一种是切换目标输入法到英文模式,然后发字母;第二种是剪贴板粘贴。前者会干扰用户的输入法状态,后者会被很多安全控件拦截。我一般用 KEYEVENTF_UNICODE 标志,它告诉系统“直接把这个 Unicode 字符交给当前窗口”,绕开键盘布局和输入法状态:

void SendUnicode(wchar_t ch) { INPUT in = { 0 }; in.type = INPUT_KEYBOARD; in.ki.wScan = ch; in.ki.dwFlags = KEYEVENTF_UNICODE; SendInput(1, &in, sizeof(INPUT)); in.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; SendInput(1, &in, sizeof(INPUT)); }

KEYEVENTF_UNICODE 模式下 wVk 必须为 0,字符码放在 wScan 字段。这段代码能把“中”“日”“€”这类字符直接送进标准输入框,而且不依赖目标程序的键盘布局。

但要注意边界:KEYEVENTF_UNICODE 对老式 Win32 控件、某些游戏自绘输入框支持不好,它们只认 WM_CHAR 消息携带的字符,不认这个注入通道。所以实际项目里我会把“Unicode 模式”做成开关,默认是虚拟键码模式,只有目标程序确认支持 Unicode 注入了才开。

4.3 三种对接模式:前台自动、后台定向、混合切换

不同场景下,“向其他程序输入字符”的诉求不一样,我一般把对接方式做成可配置的:

模式目标窗口获取方式按键投递方式适用场景
前台自动实时调用 GetForegroundWindowSendInput,直接落进当前前台窗口触摸屏一体机、自助终端,焦点始终在目标程序
后台定向FindWindow / 用户拖拽取窗口句柄SendMessage / PostMessage 构造完整消息序列目标窗口不能抢焦点,或需要同时操作多个窗口
混合模式两种方式并存前台窗口变化时自动切换投递路径复杂演示系统,多个程序间来回切换

前台自动是主力,也是第 3 章代码默认的行为。后台定向需要多一步:提供一个“选择目标窗口”按钮,点击后鼠标变成十字准星,用户在目标窗口上点击,系统用 WindowFromPoint 取到窗口句柄并保存。之后点击软键盘键帽时,不再用 SendInput,而是把 WM_KEYDOWN、WM_CHAR、WM_KEYUP 序列 PostMessage 给保存的那个窗口句柄。

这里有个容易翻车的点:PostMessage 构造的消息序列必须包含足够的 lParam 信息。常见的错误是只发 WM_CHAR 带一个字符,结果标准编辑框能显示,但游戏或自绘控件直接忽略。正确序列里,WM_KEYDOWN 的 lParam 要带上重复计数 1、扫描码、是否扩展键等信息;WM_CHAR 的 wParam 放字符;WM_KEYUP 也要按规范填。所以后台定向模式只建议在“目标窗口是标准输入控件”时使用,不要奢望它通吃所有程序。

4.4 全局参数清单:把键位尺寸、延迟和模式做成配置文件

软键盘做到能跑以后,下一步就是参数化。项目里我个人会留出一份全局配置,避免每次调参都改代码重编译:

参数默认值建议范围作用
RepeatDelayMs4515~120长按自动重复间隔,游戏要求快则调小
KeyPressDurationMs20~50单次点击按下与抬起间隔
Alpha220160~255窗口透明度
StickyMods10~1Shift/Ctrl/Alt 是否支持粘滞(点一下保持,再点字母)
ImeSafeMode10~11 时用 KEYEVENTF_UNICODE,0 时用标准虚拟键码
LayoutFilelayout.ini文件名键位布局文件路径
TargetMode00=前台,1=后台,2=混合对接目标程序的方式

StickyMods 这个参数值得单独解释一下。软键盘上没有物理键盘那种“按住 Shift 再按字母”的动作,所以要么点击字母键时自动补 Shift,要么让 Shift 键变成“点一下保持、再点一下取消”的粘滞模式。我一般默认开粘滞,因为它在触摸屏上最顺手,而且不需要为每个组合键单独设计键帽。

5. 屏幕软键盘常见问题排查:6 条让字符进不了窗的踩坑记录

这一章是实战记录。以下每一条都是我在做软键盘过程中真实遇到过、并且花过不少时间才找到原因的坑,按“现象 → 原因 → 解决”的格式写,方便你直接对照排查。

5.1 管理员权限窗口收不到输入:UIPI 把消息挡在门外

现象:软键盘对普通记事本打字正常,切到某台机器上向一个以管理员身份运行的程序输入时,字符完全进不去,SendInput 不报错,目标程序也没有任何反应。期间试过换窗口、换延迟、甚至换电脑,问题依旧。

原因:Windows 的用户界面特权隔离(UIPI)会阻止较低完整性级别的进程向较高完整性级别窗口注入输入或发送消息。你的软键盘是普通权限,目标程序是管理员权限,SendInput 的键盘事件在系统层就被拦掉了。

解决:给软键盘自身添加 requireAdministrator 的清单(manifest),让它也以管理员权限运行。这是最直接的方案。另外一个备选是让用户手动以管理员身份启动软键盘,但体验很差。这条排查经验后来成了我的固定动作:遇到“某个程序打不进字”,第一件事不是看代码,而是看目标进程的权限级别。

5.2 游戏或自绘控件无视 WM_CHAR:要模拟设备而非投递消息

现象:软键盘用 PostMessage 向一个游戏内置聊天框发送 WM_CHAR,字符不上屏;改成 SendMessage 也一样,但切到记事本就能正常输入。

原因:这类程序不走标准 Windows 消息循环,而是用 DirectInput 或 Raw Input 直接轮询键盘设备状态。PostMessage 构造的消息根本没有进入系统硬件输入队列,程序自然读不到。

解决:切换到 SendInput 通道,让注入的按键事件被系统当作真实设备输入处理。如果还是不行,检查按下与抬起之间的延迟是否太短,某些游戏会把 1 毫秒内的按下抬起当成信号抖动,需要把 KeyPressDurationMs 调到 10~20 毫秒。这个坑的教训是:游戏类目标程序永远优先选 SendInput,不要为了“后台输入”而坚持 PostMessage。

5.3 点击软键盘后目标窗口失焦:NOACTIVATE 没写全

现象:软键盘和正常程序同时开着,点软键盘的键帽,字符没有进目标输入框,反而把软键盘自己变成了活动窗口。

原因:窗口样式设置了 WS_EX_NOACTIVATE,但窗口过程没有处理 WM_MOUSEACTIVATE;或者两者都做了,但某个键帽点击时调用了 SetFocus 之类会改变焦点的 API。

解决:窗口过程里显式返回 MA_NOACTIVATE,同时检查代码里有没有隐藏的 SetFocus 调用。触摸屏环境下尤其要注意:触摸点击有时会走不同的激活路径,建议用真实触摸设备测试,而不是只拿鼠标点两下就收工。

5.4 长按方向键只走一步:autorepeat 需要自己实现

现象:按住软键盘上的右方向键,目标输入框里光标只移动一格就停了,必须松手再按才能继续移动。

原因:SendInput 每次调用只产生一次按键事件,不会像物理键盘那样触发系统自动重复。系统重复机制只服务于真实硬件中断,对注入事件不生效。

解决:用 SetTimer 实现自动重复。按下方向键时发第一次 keydown,随后每 45 毫秒再发一次 keydown(不发 keyup),直到鼠标松开才发 keyup。重复间隔可在配置里调整,想慢就调大到 80~120 毫秒。

5.5 组合键顺序不对,Shift+字母变成别的符号

现象:软键盘上做了一个“Shift+A”的键帽,点击后目标程序打出的是“*”或其他符号,而不是大写 A。

原因:组合键的按下与抬起顺序出错。先松开了 Shift 再松开 A,或者按下顺序变成了“先 A 后 Shift”,字符结果就会随着键盘布局和当前状态产生变化。

解决:严格按照“修饰键先按下 → 主键按下 → 主键抬起 → 修饰键抬起”的顺序。代码里先发 Shift 的 keydown,再发 A 的 keydown;抬起时必须先发 A 的 keyup,再发 Shift 的 keyup。顺序错了,轻则字符不对,重则触发系统快捷键。

5.6 中文输入法开着,字母上屏变成拼音

现象:目标程序的输入法处于中文模式,点软键盘上的字母键,上屏的不是字母,而是拼音或汉字候选框。

原因:标准虚拟键码注入不会切换输入法状态,目标窗口当前是什么输入法,注入的字母就会走什么输入法。

解决:两种做法。一是点击软键盘前把目标线程的输入法状态切到英文模式,调用 ImmSetConversionStatus 关闭输入法;二是启用字符直通,用 KEYEVENTF_UNICODE 发送字符,绕开输入法。如果目标程序是密码框或扫码录入这类要求绝对准确的场景,建议直接用 KEYEVENTF_UNICODE。

6. 让它像真键盘一样耐操:布局外置、热键开关与消息可视化验证

平面功能做齐以后,再往前一步就是工程化收尾。

6.1 布局外置:让键盘布局从配置文件加载

键位映射表写死在代码里,会导致“加一个键帽、改一个位置都要重编译”。我通常把布局文件做成纯文本,每行描述一个键帽:

# layout.ini: x,y,w,h,label,vk,scan,modifier 0,0,70,50,Esc,27,0,0 75,0,70,50,Tab,9,0,0 150,0,70,50,Caps,20,0,0 225,0,70,50,Shift,16,0,1 0,55,90,50,Ctrl,17,0,2 100,55,60,50,A,65,0,0 540,170,100,50,Space,32,0,0

程序启动时逐行解析,把结果填进 g_keys 数组。vk 列直接用十进制虚拟键码,modifier 列沿用 1=Shift、2=Ctrl、4=Alt 的位标志。这样运维人员调整键帽位置和大小,只需要改文本文件,不需要碰代码。

6.2 全局热键开关:不需要时一键隐藏

软键盘不能一直霸占屏幕。我习惯注册一个全局热键,用来快速显示或隐藏软键盘:

RegisterHotKey(hwnd, 0x01, MOD_ALT, 'K'); case WM_HOTKEY: if (wp == 0x01) { BOOL visible = IsWindowVisible(hwnd); ShowWindow(hwnd, visible ? SW_HIDE : SW_SHOW); } break;

热键建议选“不常用且组合键不易误触”的搭配,比如 Alt+K。同时要处理注册失败的情况:如果另一个程序已经占用了这个热键,RegisterHotKey 返回 FALSE,这时应该把错误打印出来并换一个热键,而不是静默忽略。

6.3 验证消息序列:用 Spy++ 检查目标窗口收到的键盘消息

写完软键盘,怎么确认它真的做到了“向其他程序输入字符”?我的固定做法是用 Spy++ 附加到目标窗口,观察按下键帽后窗口过程收到的消息序列。一个健康的序列应该是:

WM_KEYDOWN(wParam 为虚拟键码)→ WM_CHAR(wParam 为字符)→ WM_KEYUP(wParam 为虚拟键码)。

如果只看到 WM_KEYDOWN 没有 WM_CHAR,说明目标控件不认为这是一个完整按键;如果 WM_KEYDOWN 的 wParam 与键帽显示文字不符,检查扫描码映射和键盘布局;如果消息序列完全看不到,说明按键没进目标窗口,优先查焦点和 UIPI 权限。

还有个更直观的验证办法:自己写一个极简的调试目标程序,里面只放一个编辑框和一个 WM_KEYDOWN 日志,软键盘点一个键,日志记录一条。这样能一眼看出是“消息没到”还是“到了但被目标程序忽略”。

做这类工具做得多了,我个人最大的教训是:别在功能没跑通前急着调样式。先把最简窗口建出来,确认 SendInput 能把 A 打进去,再慢慢加透明度、粘滞键、布局文件。这样每一步出问题都知道是刚改的那一处。希望帮到你。

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

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

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

立即咨询