Files
qingjian/apps/windows
chleo ff843799fc feat(windows): 小键盘数字支持选词与直输配置 (#354)
中文组句中小键盘数字以前只能选词,打编号、型号时得切英文;加 [general] numpad_digit,缺省 select 照旧选词,direct 让数字进组句一起上屏。

Closes #217
2026-10-26 11:28:47 +08:00
..
2026-10-22 18:43:31 +08:00

青简 Windows 输入法

Windows 端是一个产品、两个产物,各自一个 package,同放本目录:

目录 package 产物 职责
server/ qingjian-windows-server qingjian-server.exe 持有唯一的输入内核 qingjian-core::Engine,跑在所有应用进程之外
tsf/ qingjian-windows-tsf qingjian_tsf.dll TSF 文本服务,被加载进每个应用进程,只做按键转发与文档写入(候选窗口由 Server 自绘)
应用进程 A ── qingjian_tsf.dll ─┐
应用进程 B ── qingjian_tsf.dll ─┼─ 命名管道 \\.\pipe\qingjian ─▶ qingjian-server(唯一的 Engine)
应用进程 C ── qingjian_tsf.dll ─┘

为什么核心逻辑要在进程外

Windows 的文本服务(TSF,Text Services Framework)是一个 COM DLL(ITfTextInputProcessor), 系统会把它加载进每一个接受文本输入的应用进程。因此输入内核不能待在 DLL 里(会被复制进几十个进程、 状态无法共享、崩溃会连累宿主应用)。青简照 Weasel(WeaselServer + WeaselTSF)的做法: Engine 只此一份,跑在独立的 Server 进程;每个应用进程里的 TSF DLL 只做两件事——把系统按键翻成协议消息发来、 把 Server 回的候选画到候选窗口。

为什么是两个 package 而不是一个

两个产物的依赖集合刻意不同:DLL 只依赖 qingjian-core、qingjian-platform 与官方 windows crate(COM implement 宏),Server 才依赖词库 / 学习 / 翻译 / 云联想(含 tokio)整棵树。合成一个 package 后,DLL 的 编译单元会拉进 Server 的依赖;用 feature 区分也不行,workspace 一起构建时 feature 会统一。crate 边界就是 「DLL 不含 Engine」这条约束的强制手段。判断标准与 macOS 的 IMK 壳一致:换掉平台适配层,不应该需要改 Core 的任何一行。

三个部分

  • Server 进程(server/):装配并持有 Engine(词库 / 语言模型 / 翻译 / 学习),按 SessionId 为每个 应用会话维护各自的组句状态,处理按键、产出候选与上屏文本,把云联想 / 本地整句模型的异步结果主动推给 对应会话。
  • IPC 协议(qingjian-platform::protocol,两端共用):ClientMessage(DLL → Server:开 / 关会话(带宿主 exe 名, [apps] 按应用设置据此查)、按键、上屏、回上下文)与 ServerMessage(Server → DLL:按键结果、上屏结果、异步重绘、请求上下文),一次要绘制的状态是 Frame(preedit 分段 + 候选页)。长度前缀 JSON 帧的编解码与缺省管道名也在这里,DLL 不必依赖整个 Server 库。
  • TSF DLL(tsf/):分「引擎层」client(平台无关的管道客户端 EngineClient,泛型在任意 Read + Write 上,本机就能接真 Server 端到端测)与「COM 层」com(cfg(windows):DllGetClassObject → IClassFactory → #[implement(ITfTextInputProcessor, ITfKeyEventSink)],编辑会话上屏,把组句位置报给 Server 摆候选窗口, DllRegisterServer 注册文本服务)。

设计细节见 docs/design/architecture.md「Windows:TSF」。

构建

本机(macOS / Linux)只做交叉 check,产出不了可用二进制,但协议层的端到端测试能跑:

cargo check --target x86_64-pc-windows-gnu -p qingjian-windows-server -p qingjian-windows-tsf
cargo test -p qingjian-windows-server -p qingjian-windows-tsf

真正编译与试用都在 Windows 机器上(MSVC 工具链)。

本地联调:scripts/test-local.ps1

改了 Server 或 DLL 之后要试手,用这个脚本,别手敲:

pwsh -File apps\windows\scripts\test-local.ps1            # 编译 + 换 Server + 起,DLL 那步要管理员
pwsh -File apps\windows\scripts\test-local.ps1 -SkipBuild -SkipRegister

它做四件事:编 Server 与 TSF DLL(带 QINGJIAN_UIACCESS=0,没签名的 exe 带 uiAccess 起不来)、 停掉在跑的 Server、把已安装目录整份拷到 ~\qingjian-devtest 再换上新的 Server 与随包数据、 起 Server 并打印剩下要手动做的事(注册 DLL、设方案、敲哪几组键)。

脚本存成带 BOM 的 UTF-8(sign-local.ps1 也是)。Windows PowerShell 5.1 对没有 BOM 的 .ps1 按系统 ANSI 码页解析,中文会变成乱码、引号配对跟着崩,报出来的却是「字符串缺少终止符」这种语法错。 编辑时别把 BOM 去掉。PS7 两种都读得对,所以只用 pwsh 验会漏掉这个问题。

为什么不在仓库里直接跑:bundled_root() 按 exe 位置找数据,target\debug\ 下会落到仓库根, 而 data\generated\ 是 gitignore 的、本机多半没有,于是退回 assets\sample\ 样例——形码候选照样出得来 (码表独立),但译文几乎全空,会让人误以为释义那条设计没生效。

为什么两边要一起换:qingjian_core::Candidate 是线上格式的一部分(见 protocol/mod.rs)。 Core 加一个 CandidateKind 变体,老 DLL 就解不出整条帧、把按键原样放行——表现是「输入法突然只出英文」, 日志里只有一句 unknown variant。所以 Server 与 DLL 必须同一份源码编出来的;协议版本号对不上时 Server 会记警告,但它只警告、不拒绝,别指望它兜住。

手工步骤(脚本里也在做,供对照)

:: 1) 编译出 DLL 与 Server
cargo build -p qingjian-windows-tsf -p qingjian-windows-server

:: 2) 注册文本服务(改 HKEY_CLASSES_ROOT,图标写到 %ProgramData%\Qingjian\qingjian.ico,要管理员)
regsvr32 target\debug\qingjian_tsf.dll

:: 3) 起 Server(引擎在这里;没起时 DLL 吃掉字母键但没有候选,起来后下一键 / 下次聚焦自动重连)
cargo run -p qingjian-windows-server

:: 4) 在系统「语言 / 输入法」里应能看到「青简」,切到它,在任意输入框敲字
::    日志都在 %LOCALAPPDATA%\Qingjian\logs\:server.<日期>.log / tsf.<日期>.log / settings.<日期>.log(按天,留 7 天)

:: 反注册
regsvr32 /u target\debug\qingjian_tsf.dll

旧版设置程序与 Windows App Runtime

设置程序是三平台共用的 apps/settings(qingjian-settings.exe),状态条的齿轮打开它。 settings/(qingjian-settings-legacy.exe,开始菜单「青简旧版设置」)是旧版,留一个版本作回退;它用 Windows Reactor(WinUI 3)画界面,是唯一依赖 Windows App Runtime 的产物。 它的部署方式是自包含:build.rs 让 windows-reactor-setup 把 Microsoft.WindowsAppSDK.Runtime 的 MSIX 解到 target\release\ 并按自包含标记嵌清单,安装包把这些文件装到 exe 同级——不依赖机器上装没装框架包。 Windows 10 上框架依赖的引导走不通(它要先调 Windows 11 才有的 TryCreatePackageDependency), 同一个 build.rs 还把这两个 API 改成延迟加载:否则它们会进 exe 的导入表,Windows 10 在加载期就起不来。 定位过程、上游 issue / PR 与取舍见 docs/notes/windows-win10.md;打包侧见 apps/windows/installer/README.md。

版本与发布

各平台壳版本号独立(见 docs/notes/release.md):两个 package 的 version 各自写在自己的 Cargo.toml, 不跟 workspace 走;发布标签用 windows-v<版本>。