Windows 输入模式切换工具:中文切换中/英,日语循环平假名/片假名/英文。
- ✅ 中文输入法:CapsLock 发送 Ctrl+Space,保持原来的中/英切换
- ✅ 日语输入法:CapsLock 循环 平假名 → 全角片假名 → 半角英文 → 平假名,每次按实际状态决定下一档
- ✅ 其它语言不接管,CapsLock 原样放行;启用映射和 Alt+CapsLock 放行开关继续有效
- ✅ 后台运行,无窗口界面
- ✅ 系统托盘图标显示运行状态
- ✅ 右键托盘图标可退出程序
- ✅ 防止程序重复运行
- ✅ 退出后CapsLock恢复原有功能
- ✅ 长时间运行自愈:键盘钩子失效后自动重装,Explorer重启后自动恢复托盘图标
- ✅ 托盘图标创建失败时自动重试,不再直接退出
- ✅ 设置页里可随时开启/关闭映射、
Alt+CapsLock放行、鼠标指针变色和开机启动;变色开着时 用滑块调浓度。改动立刻写进 exe 旁边的capslock-switcher.ini,下次启动照旧 - ✅ 每次切换在屏幕中央显示输入模式:中文红底"中文"、英文蓝底"English";日语下字符换成
A / あ / ア,其中平假名
#FFC900黄底、片假名#FF8B31橙底,都写黑字,英文那档仍是蓝底白字 - ✅ 鼠标指针也跟着变色:中文红、平假名黄、片假名橙、英文蓝,和横幅底色是同一份颜色。按 CapsLock 立刻变, 切换窗口也会跟着变(切到中文的窗口就变红,切回英文的窗口就变蓝),不按 CapsLock 直接用 Ctrl+Space 或点任务栏切换也能在半秒内跟上
- ✅ CapsLock 每按一下都往
capslock-switcher.log写一行:切换输入法、Alt+CapsLock放行 (还会记下结果是大写还是小写)、"映射已关闭"、非中/日输入法下原样放行都能看出来(设置页上的打开日志文件能直接打开看) - ✅ 双击托盘图标(或右键菜单里的"设置...")打开设置页:四个勾选项,鼠标颜色开关右边就是浓度滑块
- ✅
Alt+CapsLock触发原本的大写锁定(可以在设置页里关掉),放行时中央会闪一个紫色 横幅,白色文字写着"大写"或"小写"(小写的紫色淡一半),告诉你现在是什么状态 - ✅ 可开关开机启动(设置页里勾),以计划任务方式登录时自动运行且带管理员权限
- ✅ 程序启动时在屏幕中央显示
umbral-keys.png,随后淡出 - ✅ 出错退出时把详细错误信息写入 exe 同目录的
capslock-switcher.log
- 在Visual Studio中打开
capslock-switcher.slnx并编译 - x64 的产物在仓库根目录:调试用
x64\Debug\capslock-switcher.exe,发布用x64\Release\capslock-switcher.exe。发布就是这一个文件:静态链接 CRT、不依赖任何 运行库,拷到哪台 Win10/11 x64 机器上双击就能跑(发版前改capslock-switcher/version.h那一行) - 双击运行程序(设置页在主程序里:双击托盘图标,或右键菜单里的"设置...")
- 程序会在系统托盘右下角显示图标
- 中文输入法下按 CapsLock 发送 Ctrl+Space;日语输入法下每按一次循环 平假名 → 全角片假名 → 半角英文 → 平假名。英文仍属于日语输入法内部的模式,不切到英语布局。 横幅显示真实读到的模式:中文红、英文蓝,日语下字符是 A / あ / ア(平假名黄底、片假名橙底, 都是黑字);鼠标指针始终和横幅同色(中文红、平假名黄、片假名橙、英文蓝)。 读不到状态时不猜测、不切换日语模式;横幅不显示新状态,指针保留已有颜色。 单独英语等其它语言布局不被接管。日语切换时请松开 Ctrl/Alt/Win 等修饰键,以免触发其它 IME 命令。
- 右键点击托盘图标打开菜单;双击托盘图标直接打开设置页(菜单里也有"设置...")。
开关都挪到设置页了:四个勾选项,鼠标颜色开关右边就是浓度滑块。
勾选项和滑块改完立刻生效,也不会再弹提示气泡;失败原因只写进日志:
启用映射 (中文 / 日语):取消后 CapsLock 恢复成普通大写锁定键, 程序仍在后台运行,随时可以再勾回来Alt+CapsLock = 原来的大写锁定:勾上(默认)时Alt+CapsLock等于原来的大写锁定; 取消后 Alt 不再特殊,Alt+CapsLock和 CapsLock 一样照样切换输入法鼠标随语言变色勾选框:总开关。取消勾选立刻把你的指针方案原样装回去,旁边 的浓度滑块同时变灰、拖不动;再勾回来马上按原来的浓度重新上色,浓度值不会被清掉- 颜色浓度滑块(
鼠标随语言变色右边):100%(默认)就是完整的中文红/英文蓝,0% 相当于关掉,中间的数值 按比例把红/蓝减淡。滑块点哪跳哪,按住还能接着拖(系统的默认行为是点一下只挪一页)。 染的是箭头、文本 I 型、链接手型、十字、横向拉伸、纵向拉伸这六个 系统指针;具体边界见下面的"注意事项" 开机启动 (管理员):勾选时会弹一次 UAC 提权确认,随后创建名为CapsLock Switcher的计划任务("登录时"触发、最高权限),之后开机自启不会再弹 UAC;升级换了目录或 exe 名字之后,下次启动会把任务里的 exe 路径改成当前这份(日志里记一行计划任务的exe路径从 x 改为 y),这一步同样要提权,所以也会弹一次 UAC打开日志文件:用系统默认程序打开capslock-switcher.log- 关掉设置页(或按 Esc)只是收起来,程序继续在托盘里跑
- 右键菜单本身只有三项:当前输入法(灰显)、
设置...、Exit——开关全在设置页里,菜单不再重复一份。 - 想临时用一次大写锁定,按
Alt+CapsLock即可(等同于原来的 CapsLock);松手时屏幕中央 会闪一个紫色横幅,显示"大写"或"小写"。这一条可以在设置页里关掉,关掉之后Alt+CapsLock就和 CapsLock 一样切换输入法。另外它受"启用映射"开关控制:映射关掉时, 单独按 CapsLock 也是普通大写锁定。
启动时屏幕中央会显示一次 umbral-keys.png(键帽图),停留0.5秒后在0.3秒内淡出,
用来提示程序已经起来了。
设置存在 exe 旁边的 capslock-switcher.ini 里,绿色版,不写注册表:
[General]
MappingEnabled=1
AltCapsLockPassThrough=1
CursorTintEnabled=1
CursorTintPercent=100- 只认
0/1(三个开关)和0~100(浓度)的整数,而且整数要完整占满整行, 多一个字符就算写坏。 - 文件不存在、某一项缺失或写坏:那一项退回上面的默认值,其余项照原样保留,并立刻把整份 文件重写成合法内容(日志里记一行)。所以手改坏了不会让程序启动不了,也不会一直坏下去。
- 开机启动不在这个文件里:它本身就是系统的计划任务,状态直接查任务,不由程序另记一份。
- 文件写不进去(例如放在只读目录)只记一行日志,程序照常跑,只是设置不跨启动保留。
- Visual Studio 2026(平台工具集 v145)
- Windows SDK
- C++20标准
- 版本号是手写的:
capslock-switcher/version.h里一行#define APP_VERSION "x.y.z", C++ 代码(设置页标题栏、日志)用它;不需要 Node.js 之类的额外工具
- 代码按职责拆成几个模块,各自一对
.cpp/.h:main.cpp只管进程外壳(单实例、 隐藏主窗口、消息循环),键盘钩子在keyboard.cpp,托盘在tray.cpp,开机启动在startup.cpp,启动画面在splash.cpp,设置页在settings.cpp,日志在logging.cpp, 分层窗口和 DPI 辅助在surface.cpp,鼠标指针在cursor.cpp - 使用Windows低级键盘钩子(WH_KEYBOARD_LL)捕获CapsLock按键
- 中文继续注入
Ctrl+Space;日语通过VK_IME_ON进入平假名、Shift + VK_IME_ON进入全角片假名、VK_IME_OFF进入半角英文。两种语言共用按键注入和部分失败后的补抬逻辑,不合成 CapsLock, 不使用 F6/F7/F10 转换组合中的文字,不设置全局循环计数器,不改变罗马字/假名键盘输入偏好。 依据 Microsoft Learn 的 Keyboard Japan - ImeOn / ImeOff Implementation(见下方来源); 面向支持这些原生键的微软日语 IME,不添加旧 IME 或第三方输入法的兼容分支。 - 输入状态统一由
keyboard.cpp的GetInputMode读取:GetGUIThreadInfo找焦点控件,ImmGetDefaultIMEWnd取得默认 IME 窗口,再发带超时的WM_IME_CONTROL。 日语先读IMC_GETOPENSTATUS(0x0005),关闭就是英文;打开后读IMC_GETCONVERSIONMODE(0x0001), 用IME_CMODE_NATIVE和IME_CMODE_KATAKANA区分三态。中文沿用原来的 NATIVE 位判断。 不再枚举线程内第一个名为 IME 的窗口;链接 Windows 自带的 imm32,没有新增随包 DLL。 - 第三方输入法不再仅因名称而禁用状态横幅:每次切换前读取焦点控件的 IMM 原始开关/转换位,
切换后约 50ms 读取;未确认时再等 100ms,最多读三次(单次 IMM 查询共用 50ms 超时预算)。
只有同一输入框、同一 IME 窗口、本次中英文/假名状态确实变化才显示模式;中文也接受
“NATIVE 保留、open 改变”的开关式切换。固定 0/0、固定非零值、仅标点/全半角变化、
错误码、超时或名称缺失均不猜测,显示白字、
#00BEB6底的“切换”。 不注入 DLL、不安装新的全局钩子、不自动试按键、不跨切换缓存支持状态。窗口/布局/已知名称 改变时丢弃过期横幅。微软系继续使用原本模式读取,失败时也使用“切换”提示。 非微软输入法的鼠标仍恢复原状、不染色,这次开放的是切换横幅,不是持续光标染色。 这是基于本次状态变化的尽力探测,不保证所有第三方输入法都提供准确的 IMM 数据。 - 提示横幅在屏幕中央停留约 0.5 秒后淡出:中文红底
#FF1F45、英文蓝底#0073FF、 日语假名暖底色,上面写黑字:平假名#FFC900黄、片假名#FF8B31橙(日语的英文档仍是#0073FF白字A)、 大写紫底#9333EA、 小写用淡一半的紫#C999F4,除日语外文字都是白的;圆角和逐像素 Alpha 的做法与启动画面相同(GDI+ 画形状、GDI 画字、最后补 Alpha 蒙版) - 鼠标指针变色(
cursor.cpp)走的是SetSystemCursor:把OCR_NORMAL(32512)、OCR_IBEAM(32513)、OCR_HAND(32649)、OCR_CROSS(32515)、OCR_SIZEWE(32644)、OCR_SIZENS(32645) 六个槽换成我们自己造的 32bpp 带 Alpha 光标 (热点从原光标继承过来,I 型的热点不在角上,丢了点击就不准; 掩码按 Alpha 反推,免得在忽略 Alpha 的路径上画出一整块色块)。三个触发点:CapsLock 切换时(和横幅一样先等 50ms 让注入的模式键生效)、SetWinEventHook的EVENT_SYSTEM_FOREGROUND(切窗口,同样去抖 50ms)、以及 500ms 一次的兜底轮询 (覆盖同窗口内用 Ctrl+Space/Shift/点任务栏换输入法的情况,这种换法不产生前台事件)。 染色强度由设置页的滑块给:先按上面的公式算出满色,再和用户原来的像素按百分比线性插值 ——100% 是满色,0% 还原,中间越小红/蓝越淡。 - 原图直接读取注册表方案指向的 .cur 文件,不读取已经可能被染色的系统槽。
目标尺寸取用户的
CursorBaseSize × 鼠标所在显示器 DPI / 96:例如基础大小 48、 150% 缩放,对应 72×72 原生帧,而不是SM_CXCURSOR返回的 48×48。 显式解析多尺寸目录并用CreateIconFromResourceEx解码选中的帧,避免LoadImage自动选帧再次缩放;没有精确匹配时优先缩小较大的原生帧,只有源文件没有足够大的图像 才放大。鼠标跨显示器后,500ms 轮询会按新 DPI 重建,即使输入法状态没有变化。 - 上色只改预乘 RGB,每个像素的 Alpha、尺寸、热点原样保留:先判断亮体/暗体, 再让本体朝目标色靠拢,另一头的描边不动;半透明暗体的白边也按 Alpha 预乘,避免亮边 溢出。亮暗的判据是贴着透明的那一圈描边,不是整张图的平均亮度——I 型、十字这类 细长形状的白填充面积小,用平均亮度会被黑描边压过去,方向反过来就只剩描边变色。 整个图形看不到描边时按亮体处理,最坏只是不染,不会染错方向。无 Alpha 的彩色光标 按 AND 掩码区分透明与黑色;单色光标按上下两半 AND/XOR 掩码解码,不按系统默认尺寸 猜高度。
SetSystemCursor对普通静态光标还会做一次 DPI 归一化:本机实测原生 72px 经 72 → 48 → 72 后,即使宽高没变,像素也已经模糊。提交时使用两帧完全相同的 ANI, 视觉仍然静止,但系统保留原像素;只用一帧会再次退化为静态路径。实机集成测试必须在 写入系统槽后回读比较,不能只测试创建前的内存位图。- 正常退出或滑到 0% 时,将缓存的原始 .cur 字节同样双帧提交,保留原调色板、
掩码和热点,不重新编码原图,也不调用
SPI_SETCURSORS重载所有槽或写注册表。强杀 不能自动还原,但下次启动从磁盘重新读源,不会叠加颜色,也不需要猜测原色再“反染”。每 10 秒补刷一次 可能被 Windows 冲掉的颜色;高对比度下不染。动画 .ani、没有可读静态方案文件的槽 和自带光标的应用保持原样。 - 大写锁定现在开着还是关着,是钩子自己数出来的:系统的开关位(
GetKeyState(VK_CAPITAL)的低位)只在有焦点的线程输入队列里更新,本程序没有焦点,读到的会是过期值。所以程序在 钩子线程起来时读一次(那时队列刚建好,拿到的是系统当前状态),之后每放行一次 CapsLock (记在按键抬起时)就翻一次;别的程序注入的 CapsLock 也会跟着数,免得慢慢跑偏 - 钩子运行在独立线程,回调只在每次 CapsLock 首次按下时投递
PostMessage, 由主线程执行SendInput;长按不会反复切换,耗时操作也不会阻塞钩子线程。 如果前台窗口或键盘布局已改变,则丢弃排队的切换请求 - 进程在初始化时声明 Per-Monitor V2 DPI 感知(
SetProcessDpiAwarenessContext), 设置页是同一个进程里的 Win32 窗口,跟着进程一起走。窗口尺寸和字体都按它所在显示器的 DPI 缩放,WM_DPICHANGED里换字体并按系统给的位置重排,拖到别的显示器不会发虚。 Alt+CapsLock:以 CapsLock 首次按下时的 Alt 状态决定是否放行,并保持到该键抬起; 中途松开 Alt 或改变映射开关,都不会拆散按下/抬起事件- 语言分流直接按前台线程键盘布局的
PRIMARYLANGID判LANG_CHINESE/LANG_JAPANESE, 不依赖本地化名字。首次按下时记录目标窗口及完整 HKL,重复键和抬起保持同一决策; 主线程在 IME 查询前后复核窗口和布局,过期请求直接丢弃。 - 启动画面
umbral-keys.png以 RCDATA 资源嵌进 exe(见capslock-switcher.rc), 用 GDI+ 解码后画进一张**预乘 Alpha(PARGB)**的 DIB,再交给UpdateLayeredWindow——所以它是真正的逐像素透明,键帽边缘不会出现黑框。 淡出就是拿同一个 DIB 反复调用UpdateLayeredWindow并逐级降低SourceConstantAlpha, 不需要重新解码或重绘 - 设置的读写就在
config.cpp里,GetPrivateProfileStringW/WritePrivateProfileStringW直接操作 exe 旁边的 ini,没有额外的配置层:读取时逐项校验,坏值退默认并重写文件; 每次改动只写回那四个键。设置页改完写盘后立刻重读一遍应用下去,启动时也走同一条路径。 - 开机启动用计划任务实现(
schtasks /SC ONLOGON /RL HIGHEST),因为"登录时以管理员身份 启动且不弹 UAC"只有计划任务的"最高权限"能做到,启动文件夹的快捷方式做不到。 创建任务本身需要管理员权限,所以菜单项会用ShellExecuteW(..., "runas", ...)以--startup-enable/--startup-disable参数重新拉起自己;这个提权副本 在抢互斥体之前就完成工作并退出,不创建任何窗口,结果写进日志 - 任务名固定(不带版本号),所以每次启动都会用
schtasks /Query /TN "CapsLock Switcher" /XML看一眼任务动作里记的 exe 路径:和当前这份 exe 不一致(换过目录、改过文件名)就用/F重建同一条任务改指过来,并记下计划任务的exe路径从 x 改为 y;任务不存在、或者路径已经 一致时什么都不做 - 使用Shell_NotifyIcon创建系统托盘图标
- 使用互斥体(Mutex)防止程序重复运行
main.cpp 和 tests/regression.cpp 都保存为 UTF-8 带签名(BOM),注释是中文。
BOM 是必需的:没有 BOM 的 UTF-8 源文件会被 MSVC 按系统代码页(中文系统是936)解释,
一个多字节字符会连带吞掉字符串结尾的引号,直接编译不过(错误 C2001/C4819)。
字符串里的中文仍然写成 \uXXXX 转义(例如 \u542F\u7528 就是"启用"),
这样即使文件哪天被重新存成不带 BOM,也不会波及这些字面量;新写的代码(比如 settings.cpp)
直接写中文即可,BOM 已经保证了正确性。
- 在某些受保护的应用中可能需要管理员权限
- 退出程序后CapsLock键恢复原有大小写锁定功能
- 鼠标指针变色的边界(用的是
SetSystemCursor,改的是当前登录会话的系统光标):- 自带光标的应用盖不住:Photoshop、游戏、网页里自定义
cursor的地方仍然是原来的指针 - 程序被任务管理器强杀时来不及还原,指针颜色会保留到注销为止。下次启动本程序会 按上色公式把残留的颜色拆回灰阶原图再重新上色,不会越染越深(正常退出、滑块拖到 0% 都会立刻把你自己的原图装回去)
- 开了高对比度时不染色(那套指针是专门挑的高可见度方案,不该乱动)
- 你自己在"鼠标设置"里换指针方案或改大小,程序会重新读一遍再接着染色
- 0.0.2 及更早的版本用
SPI_SETCURSORS还原,会把"辅助功能 → 鼠标指针"调大的尺寸 退回 DPI 默认值,指针从此发虚。升级到这一版后不会再发生;如果升级前已经虚了,把 那个大小滑块动一下再调回来,系统会按你的尺寸重装一遍光标,之后就一直是清晰的
- 自带光标的应用盖不住:Photoshop、游戏、网页里自定义
- 如需开机自启动,可将程序快捷方式放入Windows启动文件夹:
C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup - 程序已在运行时再次启动,会提示"Already running",并顺带要求已在运行的实例 重新显示托盘图标(因为它可能已经因为Explorer重启而不可见)
程序长时间运行后曾出现「CapsLock没反应、托盘图标也没了」的现象。这两件事各有原因, 而且进程通常并没有真的崩溃:
- CapsLock失效:在低级键盘钩子回调里直接调用
SendInput会拖长回调耗时。一旦超过LowLevelHooksTimeout,Windows 7 以上会静默卸载该钩子,且没有任何通知或查询接口。 现在钩子使用独立消息线程,回调只投递一条消息,注入动作在主线程完成; 主线程创建计划任务时,钩子仍能处理输入。此外每60秒重新挂一次钩子 (先挂新的再摘旧的,因此不存在「没有钩子」的空档,也不会重复注入), 即使钩子仍被系统摘除也能自行恢复。 - 托盘图标消失:Explorer 重启会销毁任务栏上的所有图标。程序现在处理
TaskbarCreated消息,任务栏重建后自动把图标加回去。
另外,启动时钩子或托盘图标创建失败(例如开机自启时外壳尚未就绪)不再导致程序退出, 而是每2秒重试一次。
回归检查可在 PowerShell 中运行 ./tests/run.ps1。脚本自动加载 Visual Studio C++ 环境,
验证按键配对、长按、修饰键、注入失败、钩子线程生命周期,启动图的解码与释放,
以及指针变色的浓度公式(0%/50%/100%)、热点保留和"解码→上色→还原"整条状态机。
各模块的 .cpp 会编进同一个翻译单元,键盘 API 换成替身,所以替身对所有模块都生效。
测试使用模拟键盘 API,不会安装全局键盘钩子或向其他程序发送按键。
capslock-switcher/
├── capslock-switcher/
│ ├── main.cpp # 进程外壳:单实例、隐藏主窗口、消息循环
│ ├── app.h # 共享状态与跨模块入口
│ ├── keyboard.cpp / keyboard.h # 键盘钩子、输入状态读取与中/日模式切换
│ ├── banner.cpp / banner.h # 切换提示横幅(中/日文模式,日语假名黄底黑字)
│ ├── cursor.cpp / cursor.h # 鼠标指针跟着输入模式变色(和横幅同色)
│ ├── tray.cpp / tray.h # 托盘图标与右键菜单
│ ├── startup.cpp / startup.h # 开机启动(计划任务 + 提权副本)
│ ├── splash.cpp / splash.h # 启动画面
│ ├── settings.cpp / settings.h # 设置页(四个开关 + 浓度滑块),进程内窗口
│ ├── config.cpp / config.h # ini 读写(键名、取值范围、坏值处理)
│ ├── logging.cpp / logging.h # 诊断日志
│ ├── surface.cpp / surface.h # 分层窗口与 DPI 辅助
│ ├── resource.h # 资源ID头文件
│ ├── version.h # 手写的版本号(发版时改这一行)
│ ├── app.ico # 程序/托盘图标(16/24/32/48/256)
│ ├── umbral-keys.png # 启动画面(以 RCDATA 嵌进 exe)
│ ├── capslock-switcher.rc # 资源脚本
│ └── capslock-switcher.vcxproj # 项目文件
├── capslock-switcher.slnx # 解决方案
├── tests/
│ ├── regression.cpp # 回归检查
│ └── run.ps1 # 跑回归检查(自动加载 VS C++ 环境)
└── README.md # 本文件
普通回归(不修改系统光标):./tests/run.ps1。覆盖原生多尺寸选帧、逐像素 Alpha/
热点保真、DPI 切换、异常文件和着色生命周期。
关闭正在运行的 CapsLock Switcher 后,可执行 ./tests/run.ps1 -LiveCursor:
临时替换真实的箭头、I 型、手型、十字、横竖拉伸六个指针,检查中英文两色在 100%、60%、5% 时写入系统槽后的
像素、Alpha、尺寸、热点,以及退出还原结果;不写注册表,完成后按当前方案原图还原。
建议在 150% 等非整数缩放下运行,以抓住普通位图提交造成的二次缩放。
Microsoft Learn:Keyboard Japan - ImeOn / ImeOff Implementation
https://learn.microsoft.com/en-us/windows-hardware/design/component-guidelines/keyboard-japan-ime
普通回归覆盖语言分流、日语各状态、按键配对、布局改变、IME 超时/缺失、修饰键冲突和部分注入失败; 实机已在本机日语 IME 的独立编辑窗口中验证两轮三态循环,并另用后台进程验证跨进程读取和切换。 第三方日语 IME 尚未实测。