摘要:目前的输入法大多采用输入法管理器-输入法编辑器(IMM-IME)进行开发,对于微软发布的新型输入法技术―文本服务框架(TSF)的研究一直比较滞后,该文论述了 TSF 的基本构成、主要接口、输入法的具体实现方法以及后续的改进,并使用该技术实现一款基本 TSF 输入法,供相关领域的研究人员参考。 
  关键词:拼音输入法;文本服务框架;动态链接库;文本服务;输入法安装 
  中图分类号:TP311 文献标识码:A 文章编号:1009-3044(2016)11-0206-03 
  Abstract:Most of the current input method are developed by the method of the input method manager-input method editor(IMM-IME), the research of the new IME technology―text service framework(TSF)that Microsoft released has been a lag. This paper discusses the basic composition of TSF, main interface and the concrete realization way of input method and subsequent improvements, and use this technology to achieve a basic TSF input, provides the reference for researchers in related fields. 
  Key words:pinyin input method; TSF; DLL; text service;installation of input method 
  1 背景 
  输入法是指为将各种按键序列转化为字符,输入计算机或其他设备(如手机、平板)而采用的编码方案,因此输入法的研究是信息处理的一个重要课题[1]。Windows 提供了两套输入法框架:在 Windows XP及之前,是 IMM(Input Method Manager),即输入法引擎,基于纯函数 API;Windows XP 及以后,提供了新的输入法框架 TSF(Text Service Framework),是基于 COM(组件对象模型)的。目前现有的各个版本输入法大多采用输入法管理器―输入法生成器(IMM-IME)框架进行开发,然而在 Windows 8系统中 Metro 风格的应用是不支持该框架的,在这些应用下进行输入,需要使用文本服务框架(TSF)开发的输入法。本文将主要介绍TSF框架的基本构成,输入法的设计以及安装要点。 
  2 TSF的构成和基本工作过程 
  2.1 基本概念 
  1)什么是 TSF 
  TSF 为高级文本和自然语言输入技术提供了一个简单、可扩展的框架,是一项从 Windows XP 开始提供的设备无关、语言无关的系统服务。TSF与传统 IME 相比虽然对键盘输入法的作用没有太大区别,但是支持 TSF 的应用程序可以从任何支持TSF的文本服务中接收文本输入(例如手写、语音输入),而不用考虑有关文本来源的具体细节。 
  2)TSF 体系结构[2] 
  TSF 主要由应用程序、文本服务和TSF管理器三个部分组成,其结构如图1所示: 
  应用程序(Applications):应用程序的任务一般包括显示、直接编辑以及文本存储,通过实现 COM 服务来提供文本访问的能力。 
  文本服务(Text Services):向应用程序提供文本,同样用 COM 实现,内置了注册为 TSF 的服务过程。多个文本服务允许同时被注册,可以包含文本的输入与输出,还可以作为一段文本的数据和属性的关联体。 
  TSF管理器(TSF Manager):作为文本服务与应用程序之间中间层,TSF 管理器支持一个应用程序同时建立多个与文本服务之间的联系,共享文本内容。其功能是由操作系统实现的。 
  2.2 与应用程序的交互 
  TSF 的优点在于其设备无关,语言无关,并且可扩展的特性,同时能够给用户提供一致的输入体验。任何 TSF-enabled 的应用程序都能从任何 Text Service 接收文本,同时可以输出文本,而不需要知道文本来源的详细信息。同时,文本服务也不需要考虑各种不同应用之间的差别。 

  TSF 是应用与 IME 之间的中介,TSF 将输入事件传递给 IME 并在用户选择字符后接收从 IME返回的输入字符。 

这篇从界面和系统的角度,介绍 Meow 输入法的整体构架。

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

界面构成
一个常见输入法由以下界面构成。
// FIXME: 换掉这张美哭了的图。

image.png


不同输入法在细节的选择上会不一样,比有些输入法没有编辑窗口,而有些输入法没有状态栏。
Meow 选择的界面元素
编辑窗口:包含编辑信息和候选数据。
语言栏:仅含输入法图标。
状态栏:由于控制了语言栏功能,还是需要一个语言栏辅助。
关于语言栏
个人觉得,界面上语言栏要谨慎使用。
一是因为语言栏涉及一个宽度的问题,不少应用也会去主动修改语言栏宽度,XP上语言栏也有宽度刷新 BUG。
二是不同操作系统语言栏有所差异。
三是因为任务栏本来就挺多东西的。。。

系统构架
由于缺乏经验,我只能大概设计结构如下,具体还要在实践编程中调整。
// FIXME: 这张图已经改了
image.png

总体来说,输入法窗口和输入候选引擎,应该独立于 TextService,这样方便开发测试,也方便皮肤和候选引擎自由扩展。
为避免不必要的复杂服,Meow 中避免创建线程,因此需要严格控制每个方法反应时间,这对候选引擎是一个挑战。

TextService 文字服务
TextService 是输入法的基础入口,维护输入法自身的状态。
重要接口有 ITfTextInputProcessorExITfThreadMgrEventSinkITfThreadFocusSink

ConfigurationManager 配置管理器
配置管理器是个贯穿全局的管理器,任何模块都可以直接调用。

WindowManager 窗口管理器
实现和管理输入法的窗口和皮肤。

CompositionManager 编辑管理器
CompositionManager 通过处理键盘事件,配合 TextService 提供的状态信息,以及自身的状态机,处理按键输入的逻辑过程。
重要接口有 ITfKeyEventSinkITfTextEditSinkITfCompositionSinkITfEditSession
//FIXME: 是否需要处理 ITfDisplayAttributeProvider、IEnumTfDisplayAttributeInfo、ITfDisplayAttributeInfo 还有待观察。

CandidateManager 候选管理器
CandidateManager 有两个任务。
一个当然是跟候选引擎配合,响应 CompositionManager,生成 CandidateList。
另一方面,CandidateManager 也负责将 CandidateList 的情况推送给其他模块,包括 WindowManager、CompositionManager 和生成 UIElement。

逻辑要点说明
ConfigurationManager 只是管理配置,并不维护当前输入法的状态。
CompositionManager,CandidateManager 的关系更像是流程的两个步骤,即:按键事件->CompositionManager->CandidateManager->UI
在 UILess 模式下,CandidateManager 是 UIElement 的提供者,主要是因为 CandidateList 跟 ITfCandidateListUIElement 显然非常接近。
TextService,CandidateManager 均会对 WindowManager 操作,在一些问题(如是否显示)上,前者优先级高于后者。
WindowManager 主要任务是皮肤相关工作,跟输入法整体上处于松耦合。
Engine 的主要任务是候选算法和词库处理,跟输入法整体上也处于松耦合。

CandidateManager 和 CompositionManager
从常理上说,CandidateList 的生成应该在 Composition 内部。但 CandidateManager 之所以独立于 CompositionManager,原因有以下几个:
1、让 CompositionManager 单纯安心地去维护 Composition 状态。(这个任务已经够艰巨了)
2、在 TSF 的概念里,Composition 本身就是跟 UI 和 CandidateList 无关的,TSF 里就没有 Composition Window 这个概念。
3、UILess 模式的存在,使得 ITfCandidateListUIElement、WindowManager 需要一定的配合,而这个“配合”恰好需要发生在他们跟 CandidateList 交互的时候,而 CandidateList 的生成需要用 CompositionManager 的字符串,以及候选引擎。因此用一个模块将 UIElement、WindowManager、CandidateList 联系起来,作为一个整体来跟 CompositionManager、候选引擎 交互,代码会更简洁好懂。

入口
DLL 入口也就是那几个 DLL 函数,DLL 入口需要完成注册表注册工作,并且向外界提供 ClassFactory,通过 ClassFactory 可以获取 TextService。

TextService
输入法初始化和释放

12345
  ActivateExITfThreadMgr ptim, TfClientId tid,  dwFlags
  ActivateITfThreadMgr ptim, TfClientId tid
  Deactivate

TextService 被激活时,ActivateEx、Activate 会被调用,此时的工作便是初始化。
TextService 被释放时,Deactivate 会被调用,此时的工作便是清理工作。
可能会有多个 TextService 存在,但一个 TextService 只会被 Activate 一次,因此 Activate 之后,便可以使用类成员来存储当前输入法实例的状态。
但 TextService 和被输入程序不是一对一关系,这取决于操作系统配置,有可能多个程序共用一个 TextService。
输入法当前活动状态管理

123456789
  OnSetThreadFocus
  OnKillThreadFocus
  OnInitDocumentMgrITfDocumentMgr pdim
  OnUninitDocumentMgrITfDocumentMgr pdim
  OnSetFocusITfDocumentMgr pdimFocus, ITfDocumentMgr pdimPrevFocus
  OnPushContextITfContext pic
  OnPopContextITfContext pic

ITfThreadFocusSink:在 Windows 里,Focus 的定义就是开始接受输入。这里利用 ITfThreadFocusSink 决定输入法状态栏是否显示。
ITfThreadMgrEventSink:输入法当前工作的目标只有可能是一个 ITfDocumentMgr,但这个 ITfDocumentMgr 可能是多个程序共用,也有可能一个程序拥有多个 ITfDocumentMgr,但必须只有一个是 Focus 的。如果 ITfThreadMgrEventSink::OnSetFocus 发生,则应该根据情况清理和初始化当前存储的所有 ITfContext 和 ITfDocumentMgr 相关的目标。OnPushContext、OnInitDocumentMgr 什么的,一般捕获不到,因为通常在输入法没有载入的时候,APP 已经完成了这些操作,所以输入法需要主动 GetFocus,取到 ITfDocumentMgr,再 GetTop,取 ITfContext。
接口提供
TextService 的 QueryInterface 为整个输入法提供接口查询,同时,各个 Manager 也是在 TextService 中初始化和保持,一般所有 Manager 都有一个指针找到自己所在的 TextService。

ConfigurationManager、WindowManager
目前还没有完整实现。

CompositionManager
CompositionManager 尝试从 TextService 分离出编辑相关的逻辑,从而方便和安全地管理编辑操作。

12345678910111213
  OnSetFocus fForeground
  OnTestKeyDownITfContext pic,  wParam,  lParam,  pfEaten
  OnTestKeyUpITfContext pic,  wParam,  lParam,  pfEaten
  OnKeyDownITfContext pic,  wParam,  lParam,  pfEaten
  OnKeyUpITfContext pic,  wParam,  lParam,  pfEaten
  OnPreservedKeyITfContext pic,  rguid,  pfEaten
  OnEndEditITfContext pic, TfEditCookie ecReadOnly, ITfEditRecord pEditRecord
  OnCompositionTerminatedTfEditCookie ecWrite, ITfComposition pComposition
  DoEditSessionTfEditCookie ec
Composition 的过程被分离成 KeyEvent 和 EditSession 两个部分。
KeyEvent:处理发生在 ITfContext 上的键盘事件,正常情况下,一般只需要处理 KeyDown 事件。
EditSession:则是用来修改 ITfContext 上的文字(包括正在 composite 的文字)。因为 APP 和 IME 同时拥有对 ITfContext 写权限, TSF Manager 便使用 EditSession 来协调。因此想要更新文字,就必须调用一次 EditSession。
正常情况下,Composition 的创建和销毁都是 IME 在 EditSession 中操作,但是在意外情况下,APP 会主动终止 Composition(比如 APP 关闭),此时 OnCompositionTerminated 会被调用。此外,EditSession 是可以异步执行的,ITfTextEditSink::OnEndEdit() 发生在每次 EditSession 终止。

CandidateManager
CompositionManager 会尝试拦截按键,并管理当前 composite 的状态,CandidateManager 则利用 CompositionManager 的 composite 结果,调用输入引擎生成 CandidateList,并调用对应 UI 模块(Window 或者 UIElement)。
暂时无法提供更详细的资料因为我忒么还没写完。




  3 输入法的设计与具体实现 

  3.1 主要接口函数的实现 
  与传统 IME 必须要实现的 ImeInquire,ImeConfigure,ImeProcessKey,ImeToAsciiEx等接口函数[3]不同,文本服务框架包含了一组新的接口函数,具体实现方法也有所不同。其中一些重要的接口如下[4]: 
  文本输入处理(ITfTextInputProcessor):ITfTextInputProcessor是创建文本服务需要实现的第一个接口,继承自 IUnknown 接口,由 TSF 管理器调用来实现文本服务的激活与停用。 
  线程管理器事件接收器(ITfThreadMgrEventSink):该接口允许文本服务来接收与响应事件焦点的变化。在 TSF 中,事件通知由被称之为事件接收器的 COM 对象收取,因此,客户端需要实现一个 ITfThreadMgrEventSink 对象,并安装事件接收器,从而获得线程管理器发送的事件通知。在TSF中,应用程序和文本服务被定义为客户端。   文档管理器(ITfDocumentMgr):文档管理器的作用是管理编辑的内容,开发者可通过ITfDocumentMgr 接口创建。每个文档管理器都维护着一个后进先出的缓冲区,通常称之为内容栈,用来存储对应的文档管理器所管理的编辑内容列表。 
  语言栏按钮项目信息(ITfLangBarItemButton):该接口也继承自 IUnknown 接口,实现一些语言栏上按钮项的信息,例如图标、文本、点击弹出的菜单项等。 
  编辑会话(ITfEditSession):编辑会话由文本服务实现并由TSF管理器调用来读取或者修改文本和属性的上下文。 
  输入组合(ITfComposition):输入组合接口由 TSF 管理器实现,同样继承自 IUnknown 接口。应用程序显示什么样的文本,以及是否显示文本,需要获取输入组合的显示属性信息,通过判断输入组合是否存在,将其状态显示给用户。 
  编辑内容查看对象(ITfContextView):文本服务为候选列表创建新的内容后,ITfContextView接口的 GetTextExt 方法可返回文本边界框的屏幕坐标。 
  除了以上接口,TSF 还有线程管理器(ITfThreadMgr)、客户端标识符(ITfClientId)、键盘事件接收器(ITfKeyEventSink)、属性设置(ITfProperty)等一些重要接口需要实现,此处不再一一赘述。 
  3.2 输入法的基本实现步骤 
  3.2.1 创建空白的动态链接库项目 
  输入法程序实际上就是一个动态链接库程序【5】,只是这个动态链接库较特殊,文件名的后缀是 .ime 而不是 .dll。 
  1)在 DLL_PROCESS_ATTACH 事件中,使用 RegisterClass 注册用户界面窗口类。可根据个人喜好设计的状态窗口、编码窗口以及候选窗口的属性。 
  2)在 DLL_PROCESS_DETACH 事件中,注销上述与注册的窗口对象并释放该对象使用的所有系统资源。 
  3.2.2 文本服务模块的设计 
  用户可使用语言栏或键盘来与文本服务进行交互,因此首先要创建一个文本服务并将其注册。要使文本服务被应用程序所使用,需要将其注册为标准 COM 嵌入进程服务项,即注册到文本服务框架中。TSF 通过 ITfInputProcessorProfiles 与 ITfCategoryMgr 两个接口来提供简单的注册过程支持。 
  线程管理器(ITfThreadMgr)是TSF Manager的基本组成部分,完成应用程序与客户端之间进行联系的公共任务,包括跟踪输入焦点的改变。同时线程管理器还负责向客户端发送事件通知,客户端通过实现 ITfThreadMgrEventSink 对象,并使用ITfSource::AdviseSink方法安装事件接收器,获得事件通知。 
  文本服务使用文档管理器获取编辑内容,ITfTextEditSink 接口允许文本服务接收与响应焦点变化事件,对于一个文本服务或者应用程序来说,这个接口的实现是可选的。 
  需要注意的是,IME 必须与系统任务栏兼容[6]。任务栏仅为兼容的 IME 显示其图标,对于不兼容的则无法显示。我们需要将 IME 图标存储在 DLL 或 EXE 文件中,而不是独立的 .ico 文件中。 
  3.2.3 完成按键的映射 
  除了语言、手写识别,最常用的仍然是键盘的识别。按键的映射是输入法设计的一个重要部分,顾名思义,也是我们最熟悉的一个部分,这里通过虚键实现对一般按键与功能按键的响应,来完成输入过程。 
  首先需要使用 Windows 宏 MAKELANGID 创建语言标识符,它包含一个主要语言标识符与一个从语言标识符,返回值同样也是语言标识符,通过 ITfInputProcessorProfileMgr 的 RegisterProfile 方法来实现注册。对于拼音输入法,使用MAKELANGID(LANG_CHINESE,SUBLANG_CHINESE_SIMPLIFIED) 即可。 
  按键事件的处理受到几个因素的影响:键盘可用状态、键盘开启状态、输入状态、空闲状态、中英文状态等等,在 TSF 中,公共缓冲池为数据共享提供了数据存储和消息处理的机制,以支持客户端程序之间的数据共享。对于键盘来说,公共缓冲池 GUID_COMPARTMENT_KEYBOARD_DISABLED 针对的是编辑内容,是预定义的,如果它的值为非零值,那么键盘不可用;而GUID_COMPARTMENT_KEYBOARD_OPENCLOSE针对的是线程管理器,如果它为非零值,那么键盘处于开启状态。我们通过 ITfCompartmentMgr 接口的 GetCompartment 方法来检查键盘是否可用。关于按键的处理如图 2 所示: 
  接下来实现 ITfKeyEventSink 接口来处理击键事件,该接口包含了 OnKeyDowm、OnKeyUp、OnSetFocus 等方法分别处理按键按下、按键弹起与一个 TSF 文本服务接收或者失去键盘焦点时的事件。同时, ITfKeystrokeMgr 接口也同样重要,它允许文本服务与键盘管理器的交互。 
  3.2.4 输入组合与候选列表的处理 
  文本服务通过调用 ITfContextComposition::StartComposition 方法创建输入组合,并通过创建 ITfCompositionSink 对象接收输入组合的事件消息,使用 ITfContextComposition::EndComposition 方法来结束输入组合。 
  在创建输入组合的同时,文本服务需要提供在应用程序中区别组合输入文本与常规文本的显示属性支持,通过在 TF_DISPLAYATTRIBUTE 结构中定义文本前景色、背景色,下划线的样式、色彩、粗细等,来实现显示属性的提供。首先需要调用 ITfCategoryMgr::RegisterCategory 方法,把文本服务注册为服务提供者,然后实现 ITfDisplayAttributeProvider 与 IEumTfDisplayAttributeInfo 接口并使它们可用,最后为文本服务提供的每种显示属性实现一个 ITfDisplayAttributeInfo 对象。   接下来是关于候选列表的处理,用户输入字符后,输入法需要提供一个合适的候选列表以便用户从中选择结果串。创建一个候选列表首先要实现候选窗口的创建与注册,然后完成事件的处理部分,如翻页、选择等,最后实现窗口的销毁和隐藏。需要通过ITfTextLayoutSink、ITfIntegratableCandidtateListUIElement等接口一一实现。 
  3.2.5 把文本服务注册为标准 COM 进程服务项 
  文本服务是作为一个 COM 来实现的,所有进程内 COM 服务器(In-Process COM Server)输出四个标准函数:DllRegisterServer、DllUnRegisterServer、DllGetClassObject和DllCanUnloadNow。我们需要在模块定义文件(.def)中导出这四个接口函数,这样才能够将输入法在系统中注册。 
  DllRegisterServer 用 Windows 注册表来注册 COM 对象,而 DllUnRegisterServer 与 DllRegisterServer 的作用正好相反, DllUnRegisterServer负责移除 DllRegisterServer 注册在 Windows 注册表中的所有项。 
  DllGetClassObject 负责提供给 COM 一个类厂,该类厂用于创建一个 COM 对象。而 COM 负责调用 DllCanUnloadNow 来看是否可以从内存中卸载 COM 服务器。 
  4 输入法的安装要点 
  关于输入法 ime 有两种安装方式: 
  1)使用第三方安装程序,如 Flexera Software提供的 InstallShield 来创建 IME 安装体验。使用这种方法导入自己的词库与所生成的 ime 文件,创建一个 Setup.exe 文件,从而可以让用户安装自己编写的 IME。具体步骤可参考 MSDN 支持文档。 

  2)使用 Regsvr32 命令。Regsvr32 命令用于注册动态链接库文件,是 Windows 系统提供的用来向系统注册或者卸载控件的命令,以命令行方式运行。具体步骤是将所生成的输入法 .ime 文件拷贝到系统 System 文件夹下,然后在 cmd 下运行 Regsvr32 输入法 .ime 即可。但是这种方式会有一些问题,输入法的图标无法使用,不过不影响测试。 

   5 如何注册安装使用

本文档仅对想重定义注册信息的人有用,否则使用输入法提供的安装文件即可。 
  
1 文件说明: 
tsf-reg.exe 是32位注册程序,yong.dll是32位内置模块 
tsf-reg64.exe是64位注册程序,yong.dll是64位内置模块 
  
对64位系统来说,需要同时注册32位和64位模块。 
  
2 参数说明 
-n 你的输入法名字(在-i参数之前使用) 
-i 执行安装 
-u 执行卸载 
-c 复制文件到system32目录下 
-d 删除system32目录下的输入法 
-l 指定安装输入法的语言 
-ll 允许安装的输入法语言列表 
  
3 已知问题 
使用了-c参数,会引发Win8的一个bug,64位系统中执行32位METRO程序时无法正确加载32位tsf模块


class CTSFWrapper : public ITfThreadMgrEventSink, public ITfUIElementSink, public ITfInputProcessorProfileActivationSinkbool CTSFWrapper::Initialize(){ ITfThreadMgr* threadMgr = nullptr; HRESULT hr; hr = CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, CLSCTX_INPROC_SERVER, IID_ITfThreadMgr, (void**)&threadMgr); if (threadMgr == nullptr) { LOG_NORM(IME, "CTSFWrapper::Initilize -- ERROR! threadMgr is NULL"); return false; } if (FAILED(hr)) { LOG_NORM(IME, "CTSFWrapper::Initilize -- ERROR! FAILED CoCreate TheadMgr"); threadMgr->Release(); return false; } m_threadMgr = threadMgr; hr = m_threadMgr->QueryInterface(IID_ITfThreadMgrEx, (void**)&m_threadMgrEx); if (FAILED(hr)) { m_threadMgr->Release(); LOG_NORM(IME, "CTSFWrapper::Initilize -- ERROR! Query ThreadMgrEx Interface"); return false; } hr = m_threadMgrEx->ActivateEx(&m_tsfClientId, TF_TMAE_UIELEMENTENABLEDONLY); if (FAILED(hr)) { LOG_NORM(IME, "CTSFWrapper::Initilize -- WARNING! Failed to activate UI-less mode"); } if (InitThreadMgrSink() != true) { LOG_NORM(IME, "CTSFWrapper::Initilize -- ERROR! Failed to initialize thread manager sink"); return false; } if (InitUIElementSink() != true) { LOG_NORM(IME, "CTSFWrapper::Initilize -- ERROR! Failed to initialize UI Element sink"); return false; }  return true;}bool CTSFWrapper::InitThreadMgrSink(){ ITfSource* source = nullptr; if (FAILED(m_threadMgr->QueryInterface(IID_ITfSource, (void **)&source))) { LOG_NORM(IME, "CTSFWrapper::InitThreadMgrSink -- ERROR! ThreadMgr FAILED Query Source Interface"); if(source != nullptr) source->Release(); return false; } if (FAILED(source->AdviseSink(IID_ITfThreadMgrEventSink, static_cast<ITfThreadMgrEventSink*>(this), &m_cookieThreadMgrEventSink))) { LOG_NORM(IME, "CTSFWrapper::InitThreadMgrSink -- ERROR! Source FAILED Advise ThreadMgrEventSink"); m_cookieThreadMgrEventSink = TF_INVALID_COOKIE; source->Release(); return false; } source->Release(); return true;}bool CTSFWrapper::InitUIElementSink(){ ITfSource* source = nullptr; HRESULT hr = m_threadMgr->QueryInterface(IID_ITfSource, (void **)&source); if (FAILED(hr)) { LOG_NORM(IME, "CTSFWrapper::InitUIElementSink -- ERROR! ThreadMgr FAILED Query Source Interface"); if(source != nullptr) source->Release(); return false; } hr = source->AdviseSink(IID_ITfUIElementSink, static_cast<ITfUIElementSink*>(this), &m_cookieUIElementSink); if (FAILED(hr)) { LOG_NORM(IME, "CTSFWrapper::InitUIElementSink -- ERROR! Source FAILED Advise ElementSink"); m_cookieUIElementSink = TF_INVALID_COOKIE; source->Release(); return false; } hr = source->AdviseSink(IID_ITfInputProcessorProfileActivationSink, static_cast<ITfInputProcessorProfileActivationSink*>(this), &m_cookieProfileActivationSink); if (FAILED(hr)) { LOG_NORM(IME, "CTSFWrapper::InitUIElementSink -- WARNING! Source FAILED Advise ProfileActivationSink"); m_cookieProfileActivationSink = TF_INVALID_COOKIE; source->Release(); return false; } source->Release(); return true;}STDAPI CTSFWrapper::QueryInterface(REFIID interfaceId, void **outObject){ if (outObject == nullptr) return E_INVALIDARG; *outObject = nullptr; if (IsEqualIID(interfaceId, IID_IUnknown)) { LOG_NORM(IME, "CTSFWrapper::QueryInterface -- IID_IUnknown"); *outObject = static_cast<ITfThreadMgrEventSink*>(this); //All our (current) interfaces derive from IUnknown } else if (IsEqualIID(interfaceId, __uuidof(ITfThreadMgrEventSink))) { LOG_NORM(IME, "CTSFWrapper::QueryInterface -- IID_ITfThreadMgrEventSink"); *outObject = static_cast<ITfThreadMgrEventSink*>(this); }else if (IsEqualIID(interfaceId, __uuidof(ITfUIElementSink))){ LOG_NORM(IME, "CTSFWrapper::QueryInterface -- IID_ITfUIElementSink"); *outObject = static_cast<ITfUIElementSink*>(this); } else if (IsEqualIID(interfaceId, __uuidof(ITfInputProcessorProfileActivationSink))) { LOG_NORM(IME, "CTSFWrapper::QueryInterface -- IID_ITfInputProcessorProfileActivationSink"); *outObject = static_cast<ITfInputProcessorProfileActivationSink*>(this); } if (*outObject != nullptr) { AddRef(); return S_OK; } return E_NOINTERFACE;}



Windows输入法技术TSF理论摘抄和源代码剖析摘抄

自己线索总结


CCompositionProcessorEngine和码表有关,拼音转换结果有关 
_pTableDictionaryEngine码表相关 
KeyHandler.cpp是处理加入的键 
CCandidateWindow候选窗口 
以前的状态栏变成什么了?好像和语言栏有关系 
InitializeSampleIMECompartment,初始化com组件 
SetupConfiguration();
TF_PRESERVEDKEY预留键 
pKS = pKeystroke->Append();
添加一个并返回添加的指针

数字签名

Requirements for Windows 8 IMEs

A third-party IME must meet these requirements:

  • Must be digitally signed.

  • Must be Text Services Framework (TSF) aware, and proper IME flags must be set to run properly in Windows 8.

  • Must follow UX guidelines for Metro style apps and be compatible with Metro style apps.

A third-party IME that doesn't meet these requirements is blocked from running in the Metro environment, but it can still run on the desktop.
Also, Windows Defender removes malicious IMEs from the system. Because of this, it's important that you familiarize yourself with the IME coding requirements for Windows 8. For more info, see Guidelines and checklist for IME development.
---------------------------------------------------------------
是不是一个TextService.dll没有数字签名时,metro下就不允许调用该输入法呢?还有,个人数字证书(非受信任的)对其进行签名有用吗? 
你的所有以来的可执行代码需要签名。 
你无法确保所有的用户都会手动将你的证书加为受信用户,所以要使用受信机构颁发的证书。

架构理解

不过根据我的了解,TFS分为两部分,一部分是接受文字输入的地方,另一部分就是接受文字输出的地方。一般认为如果你非要这么做的话,你可能需要伪造一个看不见的输入框,然后强制打开某个输入法来输入文字,然后你再设置sink监听回来。

通用元素

接口或界面

问题总结

正常情况下,使用ITfKeystrokeMgr::PreserveKey(...)注册的组合键事件,应该在ITfKeyEventSink::OnPreservedKey()中获得响应。 
但在某些程序中,虽然ITfKeystrokeMgr::PreserveKey(...)成功,但是当按下组合键时,ITfKeyEventSink::OnPreservedKey()却无法响应。

已经发现的程序有:
1)IE在打开google.com.hk之后,页面内的搜索框内无法响应 
2)QQ聊天窗口中,无法响应 
附:在windows 8.1其它所有程序中可正常使用的输入法程序,下载页面http://chinput.com/thread-1768-1-1.html,可在上述程序及其它程序中尝试使用SHIFT+SPACE切换半角/全角,即可发现问题。

win7下的微软拼音,以及win8下的搜狗或QQ输入法等,都不再使用旧的IMM了,我用
https://social.technet.microsoft.com/Forums/office/zh-CN/002efcfc-8d21-4674-b93b-53c8424d448e/vista-api-immgetdescription?forum=2087
这个帖子里的代码来获取当前激活的TSF输入法,以及强行激活为别的输入法,都可以成功,但仅限于当前程序自己创建的窗口,我现在想用自己的后台程序,获取和设置任意前台窗口的TSF输入法,总是无效(函数返回都是S_OK)
我试过用ITfThreadMgr的AssociateFocus(GetForegroundWindow(), pDocMgr, &pPrevDocMgr)
还有AttachThreadInput(GetWindowThreadProcessId(GetForegroundWindow(), NULL), GetCurrentThreadId(), TRUE); 依然没用,用IMM相关函数都可以设置的。。。请问下大神我这个需求在TSF上可以实现吗?

Win8支持

的确是这样,输入法直接在Win8style程序是无法使用的。需要进行声明:
To declare an IME as compatible with Metro style apps, set the dwCaps field withTF_IPP_CAPS_IMMERSIVESUPPORT.

怎么安装

微软网站上也没说编程安装的方法,不过说了用installshield可以安装

例子

例程
链接:http://pan.baidu.com/s/1kTxmnnp 密码:mxjb
求帮忙调试一下Tibetan_input下的那个源码,第二个可以作为参考。(其实两个都有很多问题)

安装

CTF框架

CTF框架下,一个输入法为一个TIP(Text Input Processor),其首先必须注册为一个COM组件。通过ITfInputProcessorProfileMgr::RegisterProfile()接口注册TIP的CLSID和ProfileID。这等价于下面写注册表的方式: 
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{CLSID}\LanguageProfile\[langid]\{guidProfile}
Description=SZ:
IconFile=SZ:
IconIndex=DWORD:
Enable=DWORD:[0|1]
SubstituteLayout=SZ:
CLSID 代表TIP,同时指容纳TIP的COM的GUID,ProfileID是指具体某个输入法的ID,一个COM可以包含多个输入法ProfileID。譬如,微软拼音2010就在一个COM中实现了两个输入法:新体验和简捷,以满足不同用户需求。 
或者使用老接口来注册 
1) 通过ITfInputProcessorProfiles::Register()注册CLSID
2) 通过ITfInputProcessorProfiles::AddLanguageProfile()添加language profile
-可以添加不同语言的多种的profile
这等价于: 
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{CLSID}\LanguageProfile\[langid]\{guidProfile}
Description=SZ:
IconFile=SZ:
IconIndex=DWORD:
3) 通过ITfInputProcesorProfiles::EnableLanguageProfileByDefault()来缺省Enable或disable 某profile.
- 这个设置是系统级别,即应用于不同系统中的不同用户.
- 如果没有调用此接口,默认是enable
- 可以在HKCU中覆盖此设置 
这等价于: 
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{CLSID}\LanguageProfile\[langid]\{guidProfile}
Enable=DWORD:[0|1]
4) 设置profile的名字:调用ITfInputProcessorProfilesEx::SetLanguageProfileDisplayName().
- 可选步骤. 注意设置不同语言的名字。 
这等价于: 
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{CLSID}\LanguageProfile\[langid]\{guidProfile}
Display Description=SZ:
5) 设置可替换的keyboard layout (仅使用键盘TIP)
- ITfInputProcessorProfiles::SubstituteKeyboardLayout() 为profile设置可替换的hkl。 
当焦点从Cicero aware 的控件切换到non-Cicero aware的控件上时,这个hkl会被用到。这等价于: 
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{CLSID}\LanguageProfile\[langid]\{guidProfile}
SubstituteLayout=SZ:
可选 – 在控制面板输入法对话框中隐藏profile
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP\{CLSID}\LanguageProfile\[langid]\{guidProfile}
HiddenInSettingUI=DWORD:[0|1]
如果此键值不存在,则默认为0,即此profile显示在控制面板输入法对话框中 
顺便提一下,在当前用户下设置默认输入法: 
ITfInputProcessorProfile::SetDefaultLanguageProfile()
这只影响到新创建线程,而不会对已经运行的线程产生影响。当然,重启后,在所有线程都会生效。次接口只会影响当前用户,对系统中其他用户无影响 
这等价于: 
HKEY_CURRENT_USER\SOFTWARE\Microsoft\CTF\Assembly\[langid]\{TIP’sCategory}
Default=SZ:TIP’sCLSID
KayboardLayout=DWORD:
Profile=SZ:TIP’s guidProfile
从上面可以看到,无论哪种框架,都需要向注册表HKEY_LOCAL_MACHINE路径写入输入法信息,另外不同输入法也可能注册自己的组件到操作系统中,所以安装时: 
第一,需要administrator权限;要求所用户必须属于administrators组的成员; 
第二,如果系统中安装了某些安全软件,其可能阻挡写入注册表系统路径(如HKEY_LOCAL_MACHINE),这时候安装就不能成功。要么暂时关闭其功能,要么在其提示是选择“允许写入”,要么卸载它后再安装。

重要引导

在了解了TSF的强大之后,很容易产生一个疑问,TSF是如何将应用程序和 Text Service 隔离开的呢?这里简单介绍下TSF 的工作原理。 
首先需要知道,基于TSF 框架的输入法 实际上是一个COM程序。也就是说,微软为我们提供了很多的虚基类,然后我们需要实现一个COM 程序。 
(1)首先要确认,在应用程序和 Text Service 之间进行传递的是一个 text stream(文本流),既然是 text stream, 肯定要有text(可以理解成 text stream 的载体),比如说notepad,word,各种输入框,都可以理解成是一个text。TSF 的处理是首先由应用程序创建一个 Thread Manager,创建方法是通过 CoCreateInstance 创建一个组件对象,对应的,微软提供的接口是 ITfThreadMgr。 
(2)创建好Thread Manager 之后,用 Thread Manager 来创建一个 Document Manager(文档管理器),方法是 ITfThreadMgr::CreateDocumentMgr。应用程序会为每一个不同的Document 创建一个 Document Manager
(3)创建 Document Manager 后,用 ITfDocumentMgr 来创建一个 edit context,方法是 ITfDocumentMgr::CreateContext。 
实际上,Thread Manager 为每个 Document Manager 都维护了一个 context stack,新创建的context 被压入到了栈中。 
那么,Text Service 是如何往context 中写入text stream 的呢?对于这个问题,首先Text Service 要获得一个context。 
当 Text Service 获取一个context 时,很容易想到,此时可能有很多个Document Manager,可能有更多的context,到底获取哪一个呢? 
(1)首先获取当前处于焦点的Document Manager,采用的方法是ITfThreadMgr::GetFocus,得到一个Document Manager 对象 
(2)获取之前得到的 Document Manager 的 context stack 中的栈顶 context,方法是 ITfDocumentMgr::GetTop
至此,应用程序和 Text Service 通过 Thread Manager 创建的某个 context 产生了连接。 
那么,TSF 是如何进行 Text Stores(文本存储)呢?这个问题,微软也为我们提供了相应的接口。 
比如说,我们可以实现一个TTsfTextStore,可以继承 ITextStoreAcp,这个接口中有一些函数,可以在TTsfTextStore 中实现这些函数,这些函数中就有传递 text stream 的实现。给出一个ITextStoreAcp 中的函数列表: 
/* ITextStoreACP Interfaces */ 
HRESULT       STDMETHODCALLTYPE     AdviseSink(REFIID riid, IUnknown* punk, DWORD dwMask);
HRESULT    STDMETHODCALLTYPE      UnadviseSink(IUnknown* punk);
HRESULT    STDMETHODCALLTYPE      RequestLock(DWORD dwLockFlags, HRESULT* phrSession);
HRESULT    STDMETHODCALLTYPE      GetStatus(TS_STATUS* pdcs);
HRESULT    STDMETHODCALLTYPE      QueryInsert(LONG acpTestStart, LONG acpTestEnd, ULONG cch, LONG* pacpResultStart, LONG* pacpResultEnd);
HRESULT    STDMETHODCALLTYPE      GetSelection(ULONG ulIndex, ULONG ulCount, TS_SELECTION_ACP* pSelection, ULONG* pcFetched);
HRESULT    STDMETHODCALLTYPE      SetSelection(ULONG ulCount, const TS_SELECTION_ACP* pSelection);
HRESULT    STDMETHODCALLTYPE      GetText(LONG acpStart, LONG acpEnd, WCHAR* pchPlain, ULONG cchPlainReq, ULONG* pcchPlainOut, TS_RUNINFO* prgRunInfo, ULONG ulRunInfoReq, ULONG* pulRunInfoOut, LONG* pacpNext);
HRESULT    STDMETHODCALLTYPE      SetText(DWORD dwFlags, LONG acpStart, LONG acpEnd, const WCHAR* pchText, ULONG cch, TS_TEXTCHANGE* pChange);
HRESULT    STDMETHODCALLTYPE      GetFormattedText(LONG acpStart, LONG acpEnd, IDataObject* *ppDataObject);
HRESULT    STDMETHODCALLTYPE      GetEmbedded(LONG acpPos, REFGUID rguidService, REFIID riid, IUnknown* *ppunk);
HRESULT    STDMETHODCALLTYPE      QueryInsertEmbedded(const GUID* pguidService, const FORMATETC* pFormatEtc, BOOL* pfInsertable);
HRESULT    STDMETHODCALLTYPE      InsertEmbedded(DWORD dwFlags, LONG acpStart, LONG acpEnd, IDataObject* pDataObject, TS_TEXTCHANGE* pChange);
HRESULT    STDMETHODCALLTYPE      RequestSupportedAttrs(DWORD dwFlags, ULONG cFilterAttrs, const TS_ATTRID* paFilterAttrs);
HRESULT    STDMETHODCALLTYPE      RequestAttrsAtPosition(LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID* paFilterAttrs, DWORD dwFlags);
HRESULT    STDMETHODCALLTYPE         RequestAttrsTransitioningAtPosition(LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID* paFilterAttrs, DWORD dwFlags);
HRESULT    STDMETHODCALLTYPE      FindNextAttrTransition(LONG acpStart, LONG acpHalt, ULONG cFilterAttrs, const TS_ATTRID* paFilterAttrs, DWORD dwFlags, LONG* pacpNext, BOOL* pfFound, LONG* plFoundOffset);
HRESULT    STDMETHODCALLTYPE      RetrieveRequestedAttrs(ULONG ulCount, TS_ATTRVAL* paAttrVals, ULONG* pcFetched);
HRESULT    STDMETHODCALLTYPE      GetEndACP(LONG* pacp);
HRESULT    STDMETHODCALLTYPE      GetActiveView(TsViewCookie* pvcView);
HRESULT    STDMETHODCALLTYPE      GetACPFromPoint(TsViewCookie vcView, const POINT* pt, DWORD dwFlags, LONG* pacp);
HRESULT    STDMETHODCALLTYPE      GetTextExt(TsViewCookie vcView, LONG acpStart, LONG acpEnd, RECT* prc, BOOL* pfClipped);
HRESULT    STDMETHODCALLTYPE      GetScreenExt(TsViewCookie vcView, RECT* prc);
HRESULT    STDMETHODCALLTYPE      GetWnd(TsViewCookie vcView, HWND* phwnd);
HRESULT    STDMETHODCALLTYPE      InsertTextAtSelection(DWORD dwFlags, const WCHAR* pchText, ULONG cch, LONG* pacpStart, LONG* pacpEnd, TS_TEXTCHANGE* pChange);
HRESULT    STDMETHODCALLTYPE      InsertEmbeddedAtSelection(DWORD dwFlags, IDataObject* pDataObject, LONG* pacpStart, LONG* pacpEnd, TS_TEXTCHANGE* pChange);
当然,通常情况下,我们不需要实现全部的函数,只需要根据自己的需求,实现部分函数即可。

状态栏

有个问题困惑我好几天了,不知大家可否帮我解惑,以google输入法为例

把输入光标放到一个文本文件中,并选择google输入法后,会出现一个状态栏。如果把光标移到别的程序中,google输入法被切换掉后,该状态栏消失

现在问题是,我根据tsf写的输入法,用什么事件接收器能感知到当前context失去输入焦点了?
我现在是在()中把状态栏显示出来,然后在ITfTextInputProcessor:: Deactive()中销毁,现在对现象是打开文本选择输入法,状态栏出现,直到文本关闭后状态栏才能销毁。找了好久也没找到正确对方法,还望大家不吝赐教!

UINT WMAPP_FOCUS = RegisterWindowMessage(L"TargetAppFocus");

STDAPI CTextService::OnSetFocus(ITfDocumentMgr *pDocMgrFocus, ITfDocumentMgr *pDocMgrPrevFocus) 
{
PostMessage(_hStatusWnd, WMAPP_FOCUS, NULL, 0);

_InitTextEditSink(pDocMgrFocus);

return S_OK;
}


LRESULT WINAPI StatusWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//----处理 目标窗口 焦点 切换----//
if(message==WMAPP_FOCUS)
{
if(GetFocus()==NULL)
ShowWindow(hWnd, SW_HIDE);
else
ShowWindow(hWnd, SW_SHOWNOACTIVATE);
return 0L;
}

问题flip selection,和flip doc不起作用。

输入法注册:
编译生成tsfcase.dll文件到指定路径,如:x:\tsfcase.dll,然后用Regsvr32.exe x:\tsfcase.dll注册输入法(vista下需要写成bat文件,然后以管理员身份执行)。然后,打开一个文本文件选择英文输入法,再选择该输入法,它的名字是Case Text Service,该输入法会在语言栏上额外加一个图标,点击它弹出操作菜单。包括show snoop wnd(显示监视窗口),hello world(插入字符串hello world!),flip selcetion(转换选中字符串的大小写),flip doc(转换整个文档的大小写),flip keystrokes(转换键盘输入大小写)

调试:
与一般dll调试类似,在调试命令中加入x:\windows\nodepad.exe
然后调试启动nodepad,选择该输入法,即可进入断点。

问题:
现在问题是flip selection,和flip doc不起作用。
跟踪至函数ToggleCase发现,下面这句有问题:
if (pRange->GetText(ec, dwFlags, achText, ARRAYSIZE(achText), &cch) != S_OK)
cch始终是0,致使该循环break跳出。
该函数如下:
void ToggleCase(TfEditCookie ec, ITfRange *pRange, BOOL fIgnoreRangeEnd)
{
ITfRange *pRangeToggle;
ULONG cch;
ULONG i;
DWORD dwFlags;
WCHAR achText[64];

// backup the current range
if (pRange->Clone(&pRangeToggle) != S_OK)
return;

dwFlags = TF_TF_MOVESTART | (fIgnoreRangeEnd ? TF_TF_IGNOREEND : 0);

while (TRUE)
{
// grab the next block of chars
if (pRange->GetText(ec, dwFlags, achText, ARRAYSIZE(achText), &cch) != S_OK)
break;

// out of text?
if (cch == 0)
{
break;
}

// toggle the case
for (i=0; i<cch; i++)
{
achText[i] = ToggleChar(achText[i]);
}

// shift pRangeToggle so it covers just the text we read
if (pRangeToggle->ShiftEndToRange(ec, pRange, TF_ANCHOR_START) != S_OK)
break;

// replace the text
pRangeToggle->SetText(ec, 0, achText, cch);

// prepare for next iteration
pRangeToggle->Collapse(ec, TF_ANCHOR_END);
}

pRangeToggle->Release();
}

是因为 notepad 的 tsf 支持是通过 CUAS实现的. 所以並不完全支持 TSF 的 text store界面来对文字内容存取.




VwTextStore is the point of interaction between Fieldworks and Text Services. VwTxtSrc designates VwTextStore as a friend class.

Events:

Amongst other things, it receives messages from Text Services, listens for mouse events, manipulates the document and responds to internal Fieldworks events, such as document changes and lazy box changes.

The messages from text services arrive through the interface methods provided by ITfContextOwnerCompositionSink and ITextStoreACP.

Storage:

What kind of changes can happen to the document that do not come from text services is not known (clipboard, formatting etc. possibly)

The section of the document which is being edited is stored as a DocMgr. This interface looks pretty simple. It is not clear how much of the document is stored at one time (assumedly the amount which is seen on the screen, which would be dictated by the lazy box mechanism).

Porting:

This class will be replaced in Linux Fieldworks by VwTextInputManager.

Contents

 [hide

Public Methods

ITextStoreACP methods.

  • STDMETHOD AdviseSink (REFIID riid, IUnknown * punk, DWORD dwMask);

  • STDMETHOD UnadviseSink (IUnknown * punk);

  • STDMETHOD RequestLock (DWORD dwLockFlags, HRESULT * phrSession);

  • STDMETHOD GetStatus (TS_STATUS * pdcs);

  • STDMETHOD QueryInsert (LONG acpTestStart, LONG acpTestEnd, ULONG cch, LONG * pacpResultStart, LONG * pacpResultEnd);

  • STDMETHOD GetSelection (ULONG ulIndex, ULONG ulCount, TS_SELECTION_ACP * pSelection, ULONG * pcFetched);

  • STDMETHOD SetSelection (ULONG ulCount, const TS_SELECTION_ACP * pSelection);

  • STDMETHOD GetText (LONG acpStart, LONG acpEnd, WCHAR * pchPlain, ULONG cchPlainReq, ULONG * pcchPlainOut, TS_RUNINFO * prgRunInfo, ULONG ulRunInfoReq, ULONG * pulRunInfoOut, LONG * pacpNext);

  • STDMETHOD SetText (DWORD dwFlags, LONG acpStart, LONG acpEnd, const WCHAR * pchText, ULONG cch, TS_TEXTCHANGE * pChange);

These look fairly GtkIM like.


  • STDMETHOD GetFormattedText (LONG acpStart, LONG acpEnd, IDataObject ** ppDataObject);

Embedding not supported

  • STDMETHOD GetEmbedded (LONG acpPos, REFGUID rguidService, REFIID riid, IUnknown ** ppunk);

  • STDMETHOD QueryInsertEmbedded (const GUID * pguidService, const FORMATETC * pFormatEtc, BOOL * pfInsertable);

  • STDMETHOD InsertEmbedded (DWORD dwFlags, LONG acpStart, LONG acpEnd, IDataObject * pDataObject, TS_TEXTCHANGE * pChange);


  • STDMETHOD RequestSupportedAttrs (DWORD dwFlags, ULONG cFilterAttrs, const TS_ATTRID * paFilterAttrs);

  • STDMETHOD RequestAttrsAtPosition (LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID * paFilterAttrs, DWORD dwFlags);

  • STDMETHOD RequestAttrsTransitioningAtPosition (LONG acpPos, ULONG cFilterAttrs, const TS_ATTRID * paFilterAttrs, DWORD dwFlags);

  • STDMETHOD FindNextAttrTransition (LONG acpStart, LONG acpHalt, ULONG cFilterAttrs, const TS_ATTRID * paFilterAttrs, DWORD dwFlags, LONG * pacpNext, BOOL * pfFound, LONG * plFoundOffset);

  • STDMETHOD RetrieveRequestedAttrs (ULONG ulCount, TS_ATTRVAL * paAttrVals, ULONG * pcFetched);

  • STDMETHOD GetEndACP (LONG * pacp);

  • STDMETHOD GetActiveView (TsViewCookie * pvcView);

  • STDMETHOD GetACPFromPoint (TsViewCookie vcView, const POINT * pt, DWORD dwFlags, LONG * pacp);


  • STDMETHOD GetTextExt (TsViewCookie vcView, LONG acpStart, LONG acpEnd, RECT * prc, BOOL * pfClipped);

  • STDMETHOD GetScreenExt (TsViewCookie vcView, RECT * prc);

  • STDMETHOD GetWnd (TsViewCookie vcView, HWND * phwnd);

  • STDMETHOD InsertTextAtSelection (DWORD dwFlags, const WCHAR * pchText, ULONG cch, LONG * pacpStart, LONG * pacpEnd, TS_TEXTCHANGE * pChange);

Equivalent to IM commit callback and keyboard event handler?

  • STDMETHOD InsertEmbeddedAtSelection (DWORD dwFlags, IDataObject * pDataObject, LONG * pacpStart, LONG * pacpEnd, TS_TEXTCHANGE * pChange);



ITfContextOwnerCompositionSink methods

These methods appear to have a close correlation with the GtkIMContext signals.

  • STDMETHOD OnStartComposition ITfCompositionView *pComposition, BOOL *pfOk);

    • Eqivalent to preedit-start

  • STDMETHOD OnUpdateComposition ITfCompositionView *pComposition, ITfRange *pRangeNew);

    • Eqivalent to preedit-changed

  • STDMETHOD OnEndComposition ITfCompositionView *pComposition);

    • Eqivalent to preedit-end

ITfMouseTrackerACP

  • STDMETHOD AdviseMouseSink (ITfRangeACP * range, ITfMouseSink* pSink, DWORD* pdwCookie);

    • Asks to receive mouse events affecting a particular range.

    • Probably causes MouseEvent() to be called

    • Does not handle multiple paragraphs

  • STDMETHOD UnadviseMouseSink (DWORD dwCookie);

    • Asks to stop receiving mouse events as asked for by AdviseMouseSink()

Other Public Methods.

  • void OnDocChange ();

    • Mainly just talks to Windows

    • Also calls OnLayoutChange ()

  • void OnSelChange (int nHow);

    • Seems to just call methods of AdviseSink

  • void OnLayoutChange ();

    • Mainly calls AdviseSink methods

    • Also calls DoDisplayAttrs ()

  • void SetFocus ();

  • void Init ();

  • void Close ();

    • Just clears memory

  • void AddToKeepList (LazinessIncreaser *pli);

    • Calls one method on pli using what looks like FW classes

  • bool MouseEvent (int xd, int yd, RECT rcSrc1, RECT rcDst1, VwMouseEvent me);

    • Long method (>100 lines) which seems to be interested in finding where a click landed in a root box

    • Most execution paths just end the composition (Reset the input method in GTK speak)

    • Some windows, some FW



  6 结束语 
  作为新一代输入法框架,TSF 是一个允许进行高级的、来源无关的文本输入的应用编程接口,它为高级文本和自然语言输入技术提供了一个简单和可扩展的框架。本文主要讨论了 TSF 的基本概念以及注意事项,并且使用 TSF 实现了一款简单的输入法软件。关于图标不能显示的问题还有待解决,另外,对于一个完整的输入法来说还有软键盘、鼠标输入、系统图标、菜单设置、输入法皮肤等方面需要一一实现[7],同时,输入效率也是一个不容忽视的部分,有关输入转换算法还需要进一步的研究。 
  参考文献: 
  [1] 李培峰, 朱巧明. 析 Windows 95/98/NT 平台多文种 IME 的设计技术[J]. 计算机工程与科学, 2000, 22(4): 67-70. 
  [2] 王世元. 基于文本服务框架的拼音输入法客户端设计与实现[D]. 哈尔滨: 哈尔滨工业大学, 2013. 
  [3] 胡宇晓, 马少平, 夏莹. 基于 IMM-IME 输入法接口的实现方法[J]. 计算机工程与应用, 2002(1): 117-124. 
  [4] Microsoft Corporation Company. Text Services Framework[EB/OL]. http://msdn.microsoft.com/zh-cn/library/windows/apps/ms629032.aspx. 
  [5] 刘政怡, 李炜, 吴建国. 基于IMM-IME的汉字键盘输入法编程技术研究[J]. 计算机技术与发展, 2006, 16(12): 43-48. 
  [6] Microsoft Corporation Company. Requirements for IME development[EB/OL]. http://msdn.microsoft.com/en-us/library/windows/apps/hh967425.aspx. 
  [7] 焦翠珍, 戴文华. 输入法程序设计技术初探[N]. 咸宁师专学报, 200, 21(3): 73-77.

相关推荐

输入法tsf框架候选栏定位研究

在开发输入法应用的时候,我们需要让候选栏时刻跟踪输入光标的位置,来进行输入。但候选栏定位不准,一直会困扰输入法的开发者。windows老的输入法框架imm在部分场景下比如chrome应用中会定位不准,

[输入法]tsf框架中预先上屏字符管理和控制

在TSF框架中有时候我们需要通过框架上屏一些占位字符来进行定位,这时候我们就需要对预先上屏的字符进行管理,以微软的输入法为例图中红框框选出来的部分就是预先上屏的字符。在TSF框架中通过ITfRange

[输入法]Qt实现软键盘

在使用输入法的时候,为了输入一些特殊字符我们往往会用到软键盘和符号大全。符号大全和软件盘是一类程序,主要实现的就是通过界面点击向目标程序输出对应的符号。这里介绍一下如何通过QT编写软键盘类程序,实现软

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

为什么叫 Meow?因为这是给猫用的输入法。。。目录(一):基本情况(二):基础概念和常用接口(三):整体构架(四):编辑和候选(五):界面管理和无界面模式(六):词库和候选算法(七):皮肤的实现(八

tsf输入法

TSF 即文本服务框架的英文缩写微软官方文档(文本服务框架) https://learn.microsoft.com/zh-cn/windows/win32/tsf/text-services-fra

微软输入法tsf sampleIME 源码分析(转载)

类:CCandidateWindow,候选字窗口CCompositionPricessorEngine,拼写引擎CSampleIME,主程序CStringRange, 一个特殊的字符串类 见于 Sam

64位输入法DLL注 (tsf)方案

微软两种输入法方案XP和XP以前是imm(Input Method Manager)输入方案.xp之后的系统是imm和TSF(Text Service Framework)输入方案.共存. win8

tsftext services framework)和输入法

1.TSF输入接口IMM-IME架构成熟,稳定,易于实现,在Windows中被广泛使用,甚至在Linux曾大量使用的中文输入接口SCIM中也可以看到IMM-IME的影子。但是由于IMM-IME在操作权

获取输入法坐标

以下是微软TSF输入法取坐标的方法,从里面扒出来的int 取坐标(ITfContext *pContext, TfEditCookie ec, ITfComposition* pRangeCompos

输入法的注册、安装和卸载

注册输入法输入法的安装和普通应用程序有一个大的区别是,除了复制文件到安装目录、做一些必要的设置外,还需要向Windows系统注册这个输入法。我们前期一篇博文 TSF(Text Service Fram