Windows-classic-samples 之 SampleIME:基于 TSF 构建 Windows 8.1 输入法(IME)的完整示例详解
Windows-classic-samples 之 SampleIME:基于 TSF 构建 Windows 8.1 输入法(IME)的完整示例详解
导读
本文围绕 Windows-classic-samples 仓库中的 Samples/IME/README.md 展开,深入剖析一个可直接编译运行的输入法示例 SampleIME。它基于 Windows 的文本服务框架(Text Services Framework,TSF)实现,同时兼容 Windows Store 应用与 Windows 8.1 桌面应用。读完本文,你将掌握 TSF 输入法的核心接口体系、候选窗口父窗口获取的关键坑点与修复方案、完整的构建流程,以及基于 InstallShield 制作 IME 安装包的实战步骤,并能结合仓库源码快速定位到每个功能的实现文件。
示例概述:一个完整的 TSF 输入法长什么样
SampleIME 是一个真实可运行的输入法(IME)示例,其说明文档位于 Samples/IME/README.md,全部实现代码位于 Samples/IME/cpp 目录下。它的目标是演示如何创建一个既能用于 Windows Store 应用,也能用于 Windows 8.1 桌面应用的输入法——这正是 Windows 8/8.1 时代输入法开发者面临的最核心挑战:桌面时代基于消息钩子的传统 IME 架构在沉浸式(immersive)环境中不再适用,必须迁移到 TSF 模型。
从项目结构看,示例包含两个顶层文件:解决方案 SampleIME.sln 和唯一的工程文件 SampleIME.vcxproj(支持 Debug/Release × Win32/x64 四种配置)。编译产物是一个名为 SampleIME.dll 的 COM 服务器——这是所有 TSF 输入法的标准形态:输入法以 DLL 形式注册为 COM 组件,由系统按需加载进目标进程。
值得注意的是,这个示例自带一份简体中文全拼词典 SampleIMESimplifiedQuanPin.txt,词典采用 "拼音"="汉字" 的键值对格式(UTF-16LE 编码),例如 "A"="啊"、"AI"="爱"。也就是说,示例本身就是一个具备拼音输入能力的迷你输入法,而非空壳框架。
功能特性:README 声明的七项能力
README 明确列出了该示例输入法具备的七项特性,每一项目前都能在源码中找到对应实现:
| 特性 | 说明 | 对应源码 |
|---|---|---|
| 使用 Text Services Framework(TSF) | 整个输入法建立在 TSF COM 接口之上 | SampleIME.h |
| 运行在 base trust | 以低权限、标准用户态运行,不依赖提权 | 整个 DLL 架构 |
| 兼容 Windows Store 应用 | 通过沉浸式模式标志(TF_TMF_IMMERSIVEMODE)适配 Store 应用 | SampleIME.h 中 _IsStoreAppMode() |
| 兼容 Systray 与桌面 | 注册 GUID_TFCAT_TIPCAP_SYSTRAYSUPPORT 类别,支持语言栏/系统托盘 |
Register.cpp |
| 与触摸键盘交互 | 实现 ITfFnGetPreferredTouchKeyboardLayout 提供首选触摸键盘布局 |
SampleIME.h |
| 集成 Search 合约 | 实现 ITfFnSearchCandidateProvider 供搜索框调用 |
SearchCandidateProvider.cpp |
| 与 light-dismiss 交互 | 候选窗口支持轻关闭(点击外部自动关闭)的 UI 行为 | CandidateWindow.cpp 等 UI 代码 |
其中“兼容 Windows Store 应用”是贯穿全文的主线。从 SampleIME.h 可以看到,示例类 CSampleIME 同时实现了 ITfTextInputProcessorEx 与 ITfTextInputProcessor,其中 ActivateEx 接收的 dwFlags 被用于判断运行模式:TF_TMAE_SECUREMODE(安全模式)、TF_TMAE_COMLESS(无 COM 模式)、TF_TMF_IMMERSIVEMODE(沉浸式/Store 应用模式)。正是这些标志让同一份 DLL 能在桌面与 Store 应用两种环境中自适应运行。
深入架构:CSampleIME 的 TSF 接口实现
从 SampleIME.h 和 SampleIME.cpp 可以看到,CSampleIME 是示例输入法的核心类,它继承了多达十个 TSF 接口,完整勾勒出一个 TSF 输入法需要承担的全部职责:
- ITfTextInputProcessorEx / ITfTextInputProcessor:输入法在目标线程被激活(
Activate/ActivateEx)与停用(Deactivate)的入口; - ITfThreadMgrEventSink:监听线程管理器事件,如文档管理器创建/销毁、焦点切换(
OnSetFocus、OnPushContext等); - ITfTextEditSink:文本编辑完成后的通知(
OnEndEdit),用于跟踪文档变化; - ITfKeyEventSink:按键事件处理(
OnTestKeyDown、OnKeyDown、OnTestKeyUp、OnKeyUp),输入法的按键吞噬(key eaten)逻辑就在这里; - ITfCompositionSink:当其他组件强行终止组合时收到
OnCompositionTerminated回调; - ITfDisplayAttributeProvider:为组合串提供显示属性(如输入态/转换态的底色与下划线样式);
- ITfActiveLanguageProfileNotifySink:语言配置激活变化通知;
- ITfThreadFocusSink:线程焦点获得/丢失通知;
- ITfFunctionProvider:对外暴露扩展功能接口;
- ITfFnGetPreferredTouchKeyboardLayout:声明该 IME 偏好的触摸键盘布局类型。
输入法主体之外,示例还精心拆分了大量支撑模块,均在 Samples/IME/cpp/SampleIME 目录下:
- CCompositionProcessorEngine(CompositionProcessorEngine.cpp):组合处理引擎,负责语言配置加载、击键缓冲管理、语言栏按钮与 compartment 状态协调;
- CTableDictionaryEngine / CBaseDictionaryEngine / CDictionarySearch / CDictionaryParser:词典引擎与查询/解析,支撑拼音到汉字的转换与通配符检索;
- CCandidateListUIPresenter / CCandidateWindow / CShadowWindow / CScrollBarWindow / CButtonWindow:候选列表 UI 体系(含阴影窗口、滚动条、按钮等自绘控件);
- CLangBarItemButton(LanguageBar.cpp):语言栏按钮,提供中英文切换、全半角切换、标点切换三个开关;
- CCompartment(Compartment.cpp):TSF compartment 封装,用于与系统及输入法自身交换状态;
- CEditSession 系列(EditSession.cpp、KeyHandlerEditSession.cpp):TSF 要求所有文本修改必须在
ITfEditSession中通过ITfContext::RequestEditSession异步执行,这些类封装了该机制。
在 COM 注册层面,SampleIME.def 只导出四个经典函数:DllGetClassObject、DllCanUnloadNow、DllRegisterServer、DllUnregisterServer——这是进程内 COM 服务器的标准形态。DllMain.cpp 在 DLL_PROCESS_ATTACH 时初始化全局临界区并注册自定义窗口类。
关键代码剖析:候选窗口父窗口的获取与修复
README 花费最大篇幅强调了一个实战中极易踩坑的细节——IME 候选窗口的父窗口获取方式,这也是本文最值得精读的部分。
原文指出,示例代码原本使用如下方式获取父窗口:
pView->GetWnd(&parentWndHandle);
这段代码的实际出处是 CandidateListUIPresenter.cpp 的 MakeCandidateWindow 方法:通过 pContextDocument->GetActiveView(&pView) 拿到活动视图后,调用 pView->GetWnd(&parentWndHandle) 取得宿主窗口句柄,再传给候选窗口的 _Create 用于设置 owner 窗口。
README 明确指出这种实现的适用边界与缺陷:
- 仅对使用系统内置编辑控件(built-in edit controls)的 Windows Store 应用有效;
- 对使用 Windows Runtime 中 TSF API 子集提供的自定义编辑控件的应用无效——这类应用返回的父窗口句柄可能为
nullptr或无效值。
为确保 IME 在桌面应用与 Store 应用中都拿到正确的父窗口(从而让 owned 窗口正确设置),README 给出的修复代码是:
if (FAILED(pView->GetWnd(&parentWndHandle)) || (parentWndHandle == nullptr))
{
parentWndHandle = GetFocus();
}
这段修复的逻辑值得仔细解读:
FAILED(pView->GetWnd(&parentWndHandle)):调用失败时回退;parentWndHandle == nullptr:调用“成功”但未返回有效窗口时回退(这正是自定义编辑控件场景下常见的返回结果);- 回退方案
GetFocus():直接取当前拥有键盘焦点的窗口作为父窗口。由于 IME 必然服务于当前聚焦的输入区域,GetFocus()在绝大多数场景下能拿到正确的宿主窗口。
结合 BaseWindow.cpp 中候选窗口创建逻辑(_pParentWnd ? _pParentWnd->_GetWnd() : parentWndHandle)可以看出,这个句柄最终决定了候选列表、阴影窗口等 UI 元素的挂靠关系,一旦拿到空句柄,owned 窗口将无法正确建立层级关系,导致候选框定位异常或根本不显示。
实践建议:任何基于本示例二次开发的 IME,都应采用 README 给出的修复版写法,并且建议在 GetFocus() 仍返回空时再做一次 GetForegroundWindow() 兜底(此为防御性工程实践,原示例未包含)。
输入处理链路:从按键到候选词的完整流程
虽然 README 未展开描述输入流程,但仓库源码提供了完整的实现细节,这里作补充说明,便于读者理解 TSF 输入法的运行机制。
按键拦截阶段:用户的击键首先进入 CSampleIME::OnTestKeyDown / OnKeyDown(见 KeyHandler.cpp),输入法通过 _IsKeyEaten 判断是否需要吞噬该按键;若需要处理,则通过 _InvokeKeyHandler 发起一次 ITfEditSession 请求,把按键投递到编辑会话中执行。
组合编辑阶段:编辑会话内部根据击键状态(组合输入/候选选择/短语选择)分派到对应处理器,例如:
_HandleCompositionInput:向组合串追加拼音字符,若尚无组合则调用_StartComposition开启组合;_HandleCompositionConvert:触发词典查询并启动候选列表;_HandleCandidateSelectByNumber:按数字键选择候选词。
这些逻辑的核心在 Composition.cpp,其中值得关注的是 _FindComposingRange——它通过 TSF 的 GUID_PROP_COMPOSING 属性枚举组合范围,_SetCompositionLanguage 则通过 GUID_PROP_LANGID 属性为组合串标记语言 ID,_SetCompositionDisplayAttributes 为组合串设置输入态显示属性。这三者共同保证了组合串在文档中正确标记、正确渲染。
候选呈现阶段:当组合引擎完成词典检索后,CCandidateListUIPresenter 负责创建候选窗口(CandidateListUIPresenter.cpp),候选窗口配合阴影窗口、滚动条组成完整的 UI 体系。词典匹配支持通配符(* 与 ?),其递归比较实现在 SampleIMEBaseStructure.cpp 的 CStringRange::WildcardCompare 中,且对代理对(surrogate pair)字符做了正确处理。
Search 合约集成:示例通过 CSearchCandidateProvider(SearchCandidateProvider.cpp)实现 ITfFnSearchCandidateProvider,允许 Windows 搜索体验直接向输入法请求候选词,无需经过完整的键盘输入流程——这正是 README 中“Integrates with Search contract”的底层实现。
注册机制:让系统认识你的输入法
一个 TSF 输入法要能被系统加载,必须完成两件事:注册 COM 服务器与注册 TSF 类别/配置,二者都由 Register.cpp 承担:
-
COM 服务器注册(
RegisterServer):在HKEY_CLASSES_ROOT\CLSID\{...}下写入描述信息与InProcServer32子键,其中ThreadingModel的值来自 Define.h 中的TEXTSERVICE_MODEL = L"Apartment"(即公寓线程模型,TSF 输入法的标准选择)。 -
TSF 类别注册(
RegisterCategories):通过ITfCategoryMgr::RegisterCategory将示例的 CLSID 注册到SupportCategories数组中列出的八类 TSF 类别,其中包括:
static const GUID SupportCategories[] = {
GUID_TFCAT_TIP_KEYBOARD, // 键盘类文本输入处理器
GUID_TFCAT_DISPLAYATTRIBUTEPROVIDER, // 显示属性提供者
GUID_TFCAT_TIPCAP_UIELEMENTENABLED, // 支持 UI 元素(候选窗)
GUID_TFCAT_TIPCAP_SECUREMODE, // 支持安全模式
GUID_TFCAT_TIPCAP_COMLESS, // 支持无 COM 模式
GUID_TFCAT_TIPCAP_INPUTMODECOMPARTMENT, // 支持输入模式 compartment
GUID_TFCAT_TIPCAP_IMMERSIVESUPPORT, // 支持沉浸式(Store)应用
GUID_TFCAT_TIPCAP_SYSTRAYSUPPORT, // 支持系统托盘
};
其中 GUID_TFCAT_TIPCAP_IMMERSIVESUPPORT 与 GUID_TFCAT_TIPCAP_SYSTRAYSUPPORT 正是 README 所宣称的“兼容 Windows Store 应用”与“兼容 Systray 和桌面”在注册层面的直接证据。同一文件中还包含 UnregisterServer、UnregisterProfiles、UnregisterCategories 等对称的卸载逻辑,以及一个用于递归删除注册表子键的 RecurseDeleteKey 工具函数(因为 NT 上 RegDeleteKey 无法直接删除含子键的键)。
语言配置方面,Define.h 将 TEXTSERVICE_LANGID 定义为简体中文(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)),并指定了输入法图标、语言栏图标(中英文切换、全半角、标点三个开关)、默认字体 Microsoft YaHei UI 以及词典文件名 SampleIMESimplifiedQuanPin.txt。
构建示例:从源码到 SampleIME.dll
README 给出了标准的 Visual Studio 构建流程,适用于 Visual Studio 2013 及后续支持该工程的版本:
- 启动 Visual Studio,选择 File > Open > Project/Solution;
- 进入示例所在目录,双击解决方案文件
SampleIME.sln(即 SampleIME.sln); - 按 F7(Visual Studio 2013 中为 F6),或使用 Build > Build Solution 构建示例。
构建成功后,产物为 SampleIME.dll。需要特别说明的是:构建只是第一步,IME 必须经过注册才能被 Windows 识别和加载。工程通过 DllRegisterServer / DllUnregisterServer 导出(见 SampleIME.def),实践中通常使用 regsvr32 SampleIME.dll 完成注册、regsvr32 /u SampleIME.dll 完成反注册(README 未明示该命令,但这是 COM 服务器的标准操作)。
工程文件中还将词典文件 Dictionary\SampleIMESimplifiedQuanPin.txt 与全部语言栏图标(image\*.ico)作为项目内容(None 类型)包含在内(见 SampleIME.vcxproj),意味着这些资源需要在安装时一并部署。
运行与安装:用 InstallShield 制作 IME 安装包
README 明确指出:如果使用 Visual Studio 2013 构建,必须借助第三方安装器(如 Flexera 的 InstallShield)为 IME 制作 Windows 8.1 下的安装体验。这是因为 IME 是系统级组件,需要在安装时完成 DLL 部署与 COM/TSF 注册,普通的应用安装包无法胜任。
README 给出的 InstallShield Limited Edition 完整操作步骤如下:
- 安装 Visual Studio 2013;
- 启动 Visual Studio 2013;
- 在 File 菜单指向 New 并选择 Project,打开 New Project 对话框;
- 在左窗格导航至 Templates > Other Project Types > Setup and Deployment,点击 Enable InstallShield Limited Edition 并点击 OK,按提示完成安装;
- 重启 Visual Studio 2013;
- 打开 IME 解决方案(.sln)文件;
- 按 F6 构建解决方案;
- 在解决方案资源管理器中右键解决方案,指向 Add,选择 New Project,打开 Add New Project 对话框;
- 在左侧树控件中导航至 Templates > Other Project Types > InstallShield Limited Edition;
- 在中间窗口点击 InstallShield Limited Edition Project;
- 在 Name 文本框中输入
SetupIME并点击 OK; - 在 Project Assistant 对话框中点击 Application Information;
- 填写公司名称及其他字段;
- 点击 Application Files;
- 在左窗格右键 [INSTALLDIR] 文件夹,选择 New Folder,命名为
Plugins; - 点击 Add Files,导航到
C++\Debug目录下的SampleIME.dll,将其添加到Plugins文件夹;对 IME 词典重复此步骤(位于C++\SampleIME\Dictionary文件夹); - 右键 IME DLL 并选择 Properties,打开 Properties 对话框;
- 在 Properties 对话框中点击 COM & .NET Settings 选项卡;
- 在 Registration Type 下选择 Self-registration 并点击 OK;
- 构建解决方案,InstallShield 会生成 setup.exe 安装程序,使用户可以在 Windows 8.1 上安装你的 IME。
完成后的安装程序位于 SetupIME\SetupIME\Express\DVD-5\DiskImages\DISK1 文件夹。
对上述流程做几点技术补充(来源于源码分析,README 未展开):
- 第 15 步把 DLL 放入
Plugins子文件夹并非随意为之——IME DLL 通常不与主程序混放,独立插件目录便于版本管理与卸载清理; - 第 19 步选择 Self-registration 是关键:安装器会在安装时调用 DLL 的
DllRegisterServer导出函数(即 Register.cpp 中实现的注册逻辑),从而完成 COM 与 TSF 双重注册;若选择手动注册,则需要在安装脚本中显式执行regsvr32; - 词典文件必须与 DLL 一起部署,因为引擎在运行时按相对路径查找 SampleIMESimplifiedQuanPin.txt(其文件名常量定义于 Define.h 的
TEXTSERVICE_DIC)。
操作系统要求
README 明确给出的系统要求如下:
| 类别 | 版本 |
|---|---|
| Client(客户端) | Windows 8.1 |
| Server(服务器端) | Windows Server 2012 R2 |
需要说明的是,这是示例创建时的官方目标环境。从源码使用的 TSF API(如 ITfInputProcessorProfileMgr、ITfFnGetPreferredTouchKeyboardLayout、沉浸式模式标志)来看,示例的架构面向 Windows 8 时代引入的 TSF 增强能力。在更新的 Windows 版本上,其 TSF 实现思路依然适用,但具体行为应以目标系统实测为准。
总结与二次开发指引
SampleIME 是学习 Windows TSF 输入法开发的教科书级示例,它以约 90 个源文件(含 46 个 .cpp 与 37 个 .h,见 Samples/IME/cpp 目录)完整覆盖了 TSF 输入法的全部关键环节:接口实现、编辑会话、组合处理、词典引擎、候选 UI、语言栏、compartment 状态管理、COM/TSF 注册与安装部署。
对希望基于它做二次开发的读者,建议按以下顺序阅读源码:
- 从 SampleIME.h 建立整体接口认知;
- 读 Register.cpp 理解注册机制;
- 沿 KeyHandler.cpp → CompositionProcessorEngine.cpp → Composition.cpp 追踪输入主链路;
- 最后读 UI 层(CandidateWindow.cpp 等)与 SearchCandidateProvider.cpp。
其中,README 强调的候选窗口父窗口修复方案(pView->GetWnd 失败或为空时回退 GetFocus())是所有 IME 开发者都应铭记的实战经验——它直接决定了输入法在 Windows Store 应用与桌面应用中能否正确弹出候选框。