逗比的输入法实现(二):基础概念和常用接口

为什么叫 Meow?因为这是给猫用的输入法。。。

目录
(一):基本情况
(二):基础概念和常用接口
(三):整体构架
(四):编辑和候选
(五):界面管理和无界面模式
(六):词库和候选算法
(七):皮肤的实现
(八):其他杂事

TSF vs IMM32
输入法广泛使用 IMM32 接口,也发展成熟,XP 开始微软推广 TSF,Vista 操作系统默认开启了 TSF 管理器。
但 TSF 一直饱受非议,虽然设计的很通用,但使用 COM 组件、接口隐晦、操作麻烦,而且从输入法角度来说,能做的事情似乎并不比 IMM32 强多少。此外,TSF 文档不全,样例单一,个别输入法功能上(比如输入法窗口管理)甚至比 IMM32 还要弱。TSF 似乎一直不怎么受到输入法开发者待见。
直到 Win8 之后,METRO 界面只能使用 TSF 接口,于是主流输入法纷纷实现 TSF,用于处理全屏模式、游戏、以及 METRO。
TSF 和 IMM32 之间的兼容性
兼容性TSF APPIMM32 APP
TSF IME利用 msctf.dll 和 TSF Manager 交互msctfime.ime(XP 下需要开启高级文字服务)
IMM32 IMEmsctf.dll 的 IMM32 兼容层利用 imm32.dll 和操作系统交互
msctfime.ime,能够把 TSF IME 模拟成 IMM32 输入法。唯一的例外的情况,是 XP 下,如果关闭高级文字服务,msctfime.ime 将被禁用。
尽管似乎一切都没有问题,但是实际情况下,由于 IMM32 和 TSF 本身的差异,在兼容模式下,一些事件的触发是不可靠的,所以依然不能认为他们完全兼容。
TSF 特点
使用 COM 组件,扩展性非常好,但上手难度大。
本身着重文字服务,比如手写输入、语音输入,甚至纠错、翻译什么的,都是可以用 TSF 实现。
APP 和 IME 之间有更多信息可以通讯。
IME 相关功能提供不全,如只提供了输入字属性和语言栏修改,输入法窗口均需要自己实现。(因为这一点,主流的纯 TSF 输入法都放弃输入法状态栏)
大部分注册表相关任务(比如注册输入法,注册快捷键)需要自己实现。
IMM32 特点
接口直白,操作上简单。
完全为 IME 设计的接口,拥有一些 IME 特有的功能,比如输入法窗口管理,系统输入法快捷键管理。
IME 单向为应用提供输入,交互功能受限。
接口扩展性低,已停止更新。

COM 和 DLL 基础概念
输入法文件的本质是个 dll,只要 dll 编译正确,就可以注册进操作系统。
(现在的主流输入法都跑了一堆 exe,感觉主要是为了更新和偷窥。对于输入法 DLL 来说,完全可以触发自己的工作线程,没必要额外的 exe)
而 TSF 输入法,则是一个可注册的,包含 COM 接口的 dll。

COM
COM 是微软的核心接口方案,COM 在 Windows 和 Office 中使用非常广泛,比如看似笨重的 IE 其实在为整个操作系统实现了网页相关的接口。
COM 的一大特点是跨进程的,使用 COM 的时候,你不知道对面是跟进程,还是个被操作系统托管的 dll,而 COM 对象而言,也会在不同进程里被使用。

内存和引用
COM 接口都继承自 IUnknown,而 IUnknown 就干了一件事:引用管理。
当一个 COM 对象生成了之后,它可能被多个进程调用,过早释放肯定是危险的,不及时释放会出现内存泄漏。
因此,COM 接口的使用者需要严格把控引用计数操作,COM 接口自身也应该拥有自毁灭功能(既当无人引用自己时,主动销毁自己)。
实现任何 COM 接口都应该好好处理引用管理。对于一些特殊的情况,比如传递字符串 BSTR,则应该使用约定俗成的原则。

线程模型
ThreadingModel,DLL 在操作系统执行时,经常只占用一份内存,而实际每个实例又有自己的 hinstance 可以互相区分,那么这个 dll 是否多线程安全,完全取决于这个 dll 怎么写的。ThreadingModel 相当于告诉操作系统,我适用于什么线程模型。如果完全不知道自己该干嘛,应该使用 Apartment 模型,也就是支持单线程模式。 http://support.microsoft.com/kb/150777

DLL DEF
编译 dll 需要暴露外部接口,暴露的方式就是在编译的时候指定 def 文件。一个 COM 组件的 def 文件至少是这样的:
1234567
LIBRARY

EXPORTS
        DllGetClassObject               PRIVATE
        DllCanUnloadNow                 PRIVATE
        DllRegisterServer               PRIVATE
        DllUnregisterServer             PRIVATE
// FIXME: 提供一个更详细的函数调用情况说明
其中 DllRegisterServer 和 DllUnregisterServer 是被注册(安装卸载),也就是调用 regsvr32.exe 注册时触发的函数。
而组件实际被调用的时候,DllGetClassObject 会被调用,通常你需要返回一个 ClassFactory,注意引用管理。
DllCanUnloadNow 则是用来计算能否卸载,需要跟引用管理配合。

DllMain
除了那 4 个手动暴露的函数,DllMain 是个默认暴露的函数,但 DllMain 不是必须要实现的。
在 DllMain 通常发生于 Dll 载入、卸载 的时候,这时候进程的状态是非常不确定的,有可能都没有初始化完毕,因此在 DllMain 里调用的操作要非常小心。
一般来说,DllMain 里只会包含一些 HINSTANCE 相关操作,比如记录 HINSTANCE,初始化 CriticalSection 等。

引用管理
DLL 自身跟 COM 对象一样,需要实现应用管理。
DllGetClassObject 每返回一个对象(ClassFactory),引用应该 +1
当确保被引用的 ClassFactory 都已释放时,DllCanUnloadNow 才可以返回 S_OK。
监控 ClassFactory 的状态是比较麻烦的,特别是有多个不同 ClassFactory 的情况。


TSF 重要概念
这是官方的图,希望有帮助。
概要
先来直白看下 APP 和 IME 都需要干啥。(当然,APP 很多工作都由操作系统默认控件代为实现了)
APP:
12345
创建 ThreadMgr
ThreadMgr->Activate
创建 DocumentMgr,创建 Context。
ThreadMgr->SetFocus(DocumentMgr)
DocumentMgr->Push(Context)
IME:
1234
注册 TextInputProcessor
实现 TextInputProcessor->Activate
ThreadMgr->GetFocus(DocumentMgr)
DocumentMgr->GetTop(Context)

核心 TSF 接口
// FIXME: 提供一个更详细的接口及其方法说明
COM 接口的松耦合性,导致其接口通常比较隐晦,很难直观地看出该怎么使用参数,组件是如何被调用的。
http://msdn.microsoft.com/en-us/library/windows/desktop/ms629037.aspx
这个网页列举了 TSF 的相关接口,自从我打开这个网页,我就知道我进了一个深坑。有些接口给应用程序用的,有些给输入法用的,混在一起,光接口就多达142个。
很多接口的网页打开之后只有基本是一句话描述,完全没有使用场景说明和样例。我只能从不同版本的 SDK 里寻找不同样例,尝试理解接口的使用场景和用法,把重要信息列举出来。
有些接口是 Application 专用的,有些是 Text Service 专用的,而有些是公用的,一般接口的一句话描述里会提到。

★ ITfTextInputProcessorEx、ITfTextInputProcessor
大致可以理解为输入法的进程管理接口。会触发激活、释放两个事件,对应的应该做出各种初始化和释放。
这是最基础的入口接口,APP 如果需要跟输入法交互,需要主动查询和调用 ITfThreadMgrEx,对于的 IME 响应便是 ITfTextInputProcessor。

★ ITfThreadMgrEventSink
输入法线程管理,有时候每个应用都使用独立的输入法线程,有的时候是公用的,跟操作系统设定有关,必须适应多种情况,必须实现。

★ ITfKeyEventSink
处理键盘事件的接口,输入法当然要有咯。
一般来说,需要利用ITfKeyEventSink,判断按键,决定是否触发 Composition Session。
如果当前没有任何 Composition Session 那么需要根据按键触发 Composition Session。
如果当前有 Composition Session,则需要根据按键决定是否终止 Composition Session(比如常用的按空格)。
当然,个别其他事件也需要终止 Composition Session,比如焦点转移了。

★ ITfContext、ITfEditSession
当键盘事件被触发时,获得的参数是ITfContext。但是ITfContext并不能直接修改内容添加文字等,需要调用 RequestEditSession,传入一个 ITfEditSession 才能实现内容的修改。
RequestEditSession 之后,ITfEditSession 的 DoEditSession 会被(迅速)调用,可以在这个时候实现对内容的修改,也可以干别的事情。比如创建或者结束 一个ITfComposition。
为什么这么设计,我猜测是从线程管理的角度。被输入的应用程序不可能锁住UI线程等待输入,但是多线程同时修改,可能会产生线程同步问题。操作系统可能会在你触发 RequestEditSession 的时候迅速锁住 UI 线程,输入,然后释放 UI 线程。

★ ITfContextComposition、ITfComposition
Composition 实现的接口,通常需要在一定的时机,调用 ITfContextComposition,并且生成 ITfComposition。
并且在合适的时候触发 ITfComposition 的 EndComposition,在 EndComposition 中生成一个 ITfEditSession,实现输入。
什么实际算合适呢,需要小心控制。在很多实现中,都是利用 ITfEditSession 的 DoEditSession 中创建和结束 ITfComposition。
就我目前的观察而言,Composition 并非必须的,也不能利用 Composition 直接修改文字,实际上你可以完完全全吞掉输入的按键。但 Composition 实际上是 TextService 和应用程序之间共享的接口。利用 ITfComposition 应用程序可知道你正在输入,并且知道正在输入的内容。所以从兼容性角度,应该实现 ITfComposition。
通常 TextService 主动利用 ITfContextComposition 创建 ITfComposition,然后利用 ITfCompositionSink,监听 OnCompositionTerminated。
应用程序可调用 ITfContextOwnerCompositionSink 监听 Composition 的事件。

★ ITfTextEditSink、ITfCompositionSink
常用的管理输入过程的接口,如触发文字改动,触发编码等等,理论上肯定会用到。
一般来说,这些事件都用来辅助 Composition Session 的管理。

ITfThreadFocusSink
被输入线程焦点(活动窗口)管理。用 ITfThreadMgrEventSink 管理活动窗口并不靠谱,因为无法确定操作系统是如何管理输入法线程的。如果需要在每次焦点变化时候做调整(如隐藏、显示状态栏),ITfThreadFocusSink 是比较好的处理接口。

ITfDisplayAttributeProvider、ITfDisplayAttributeInfo、IEnumTfDisplayAttributeInfo
在输入过程中,如果想为输入的字符串显示特殊的效果(比如颜色、下划线),则可以使用这两个接口。
因为考虑到各种兼容性,这两个接口提供的效果非常有限。

ITfUIElement、ITfCandidateListUIElement
从名字上很容易误以为这是输入法管理 UI 的接口,实际上这个是输入法允许应用程序自己托管 UI 所需的接口。比如全屏游戏,比如命令行模式。
程序希望自己绘制输入法界面时,如果输入法提供了这些接口,程序就可以用对应的接口获取输入法的 CandidateList,并且用自己的 UI 显示,比如魔兽世界就会如此。

ITfLangBarEventSink
用于管理语言栏相关事件,如果语言栏是万年不动的,可以不用。

ITfFnConfigure
配置接口。。。控制面板的输入法里,有个配置按钮。。。用于处理那个用的。
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值