在Microsoft®平台软件开发包(SDK)的 samples 文件夹的 winui 子文件夹下提供了一组可供编译的TSF应用程序和文本服务的范例源代码。
• 公共组成部分
• 应用程序
• 文本服务
下列软件组件被用于支持TSF的应用程序和文本服务中或者由它们来实现。
• 线程管理器
• 客户端标识符
• 文档管理器
• 编辑内容
• 片段
• 属性
• 共享包
• 输入组合
线程管理器是TSF管理器的基本组成部分,由它完成有关应用程序和文本服务(客户端)之间进行联系的公共任务。这些任务包括(但不限于)激活或挂起TSF文本服务、文档管理器的创建和维护文档与输入焦点之间的正确关联。线程管理器可以通过ITfThreadMgr接口来定义。
TSF管理器的多数接口和对象都可以使用线程管理器接口提供的方法来获得。
应用程序
应用程序可以通过带CLSID_TFThreadMgr参数的CoCreateInstance函数来创建一个线程管理器对象。
文本服务
文本服务在其ITfTextInputProcessor::Activate服务方法中包含了一个线程管理器对象。
事件通知
线程管理器还负责向客户端发送事件通知。在TSF中,事件通知是通过一个被称之为事件接收器的COM对象来收取的。因此要想获得线程管理器发送的事件通知的话,客户端就应该实现一个ITfThreadMgrEventSink对象并安装事件接收器。事件接收器的安装方法是向线程管理器查询IID_ITfSource并调用带IID_ITfThreadMgrEventSink参数的ITfSource::AdviseSink方法。
在文本服务框架中,应用程序和文本服务被定义为客户端。所有客户端都会接收到并必须要维护一个由线程管理器分配的客户端标识符,客户端在调用各种TSF方法的时候通常都需要出示自己的客户端标识符。
应用程序
应用程序获取客户端标识符的方法是调用 ITfThreadMgr::Activate 。
文本服务
文本服务在调用 ITfTextInputProcessor::Activate 方法的时候就会得到它的客户端标识符了。
TSF定义了一个被称之为“编辑内容”的基本文本输入模型,它可以理解为一个通过 ITfContext 接口创建的连续的文本流,通常是由应用程序创建编辑内容提供给文本服务使用,不过并不是说文本服务就不可以创建编辑内容了。不管是哪种类型的程序创建了编辑内容,在TSF框架中都被理解为 编辑内容所有者 。
应用程序
应用程序通过 ITfDocumentMgr::CreateContext 方法来创建编辑内容。
文本服务
文本服务通常是使用当前活动的编辑内容,也就是位于活动文档管理器堆栈最上面的编辑内容。获得当前活动内容的方法是先通过 ITfThreadMgr::GetFocus 文本服务得到活动文档管理器,然后再用 ITfDocumentMgr::GetTop 得到堆栈最上面的编辑内容。
有时,文本服务也需要创建自己的编辑内容,这应该使用 ITfDocumentMgr::CreateContext 方法来完成。
编辑信息片段
很多文本服务方法(比如 ITfRange::SetText )需要某种标识一个编辑内容的方法来实现只读或读/写 文档锁 。文档锁由TSF管理器和应用程序通过协商获得,文本服务不能直接实现这种协商。文本服务只能通过 编辑会话 请求对特定内容进行只读或读/写访问来获得文档锁。开通编辑会话后,文本服务需要提供一个用于标识被请求访问的编辑内容的编辑信息片段。这个信息片段用来出示给文本服务方法以标识实际需要访问的编辑内容。
ITfDocumentMgr::CreateContext 方法也会提供给编辑内容所有者一个编辑信息片段,这个信息片段具备只读访问级别且不能改变其此访问级别。实际上TSF管理器并没有实现这个信息片段的文档锁,也就是说该信息片段虽然内部标记为只读,但文档并未真正锁定。例如编辑内容所有者用 ITfDocumentMgr::CreateContext 提供的编辑信息片段来调用 ITfContext::GetSelection 的结果,跟应用程序调用 ITextStoreACP::GetSelection 或 ITextStoreAnchor::GetSelection 是一样的。在获取所需的内容之前,应用程序需要确定是否存在文档锁。因为如果没有加锁的话,应用程序就会发生TS_E_NOLOCK异常。因此当应用程序用没有文档锁的信息片段来调用它的一个文本存储方法时,它应该清楚这个情况并自己处理可能发生的问题。
如果编辑内容所有者需要一个具备读/写访问级别的编辑信息片段,它就应该建立自己的编辑会话。
文档管理器是通过 ITfDocumentMgr 接口创建并用来管理编辑内容的。每个文档管理器都维护着一个被称之为内容栈的后进先出缓冲区,内容栈里面存储的就是该文档管理器所管理的编辑内容列表。通常内容栈里面只有一个编辑内容,不过根据需要,也可以把其它内容添加到栈中。每个栈最多只允许装载两个内容。
应用程序
应用程序通过调用 ITfThreadMgr::CreateDocumentMgr 方法来创建文档管理器对象。应用程序应该为它所管理的每份文档创建一个专属的文档管理器对象。应用程序使用文档管理器来创建编辑内容、把内容添加到内容栈和把内容栈中的内容移出来。
文本服务
文本服务不创建自己的文档管理器对象,而是调用 ITfThreadMgr::GetFocus 方法来获取当前活动的文档管理器对象。文本服务使用文档管理器来获取内容栈最上面的那份编辑内容。
文本服务也可以使用文档管理器来创建它自己的编辑内容,并把它添加到内容栈或从中删除。这么做的原因是通常文本服务需要显示某种形式的用户界面,比如当显示一个可供用户选择的候选文字列表时,文本服务就把它自己的编辑内容放到内容栈上;当候选列表关闭时,文本服务就把它自己的编辑内容从内容栈中移出。
在TSF里面,所有文本都存储在一个被称为片段的对象中。片段是一个描述了存在于用作读、写和操作文本的文本流里面的某一小段文本的对象。
文本片段不与应用程序中文本流上的字符位置(ACP)相关联,而是涉及并与特定的文本段联系在一起,这就使得当该段文本在文本流中的位置发生变化时,也不需要重新定义相应的片段--即片段是可游移的。
例如,在下面这个文本流中定义了一个片段“真好”:
其中<anchor>标签表示片段开始,</anchor>标签表示片段结束。因为片段在包含它的文本流中是可游移的,所以如果在“真好”的前面插入文本“~”的话,片段标签可以带着它的文本片段跟在字符“~“的后边。在上述插入过程中,如果是用ACP的方式来定义片段的话,则最终片段所包含的文本会变成“~真”。比如,在TSF中该插入操作的结果是:
假设是ACP方式定义的片段的话,结果就会变成:
而这显然不是我们想要的。
当在片段中插入或删除文本时,片段的边界就会相应地伸展或收缩。这种由于片段本身内容的更改所导致的边界伸展或收缩是必然的,但不允许从片段中剪切出文本或把文本粘贴到文本流中。
片段标签
片段是由一头一尾两个标签界定出来的,分别表示片段的开始和结束。标签本身由小于号“<”和大于号“>”及它们括起来的片段标识符组成。片段开始标签放在要定义为片段的文本段的前面,结束标签则放在文本段的后面。开始和结束标签可以一前一后紧挨着出现,此时表示一个空片段。
比如,原始文本内容为:
现在在文本的前面插入紧挨着的开始和结束两个标签,结果得到文本:
注意上面这对标签之间是没有任何空格的,它表示一个长度为0的空片段。
现在将结束标签往后移动三个字符位置,结果得到文本:
由于开始标签位于第一个字符的前面,结束标签位于第三个字符的后面,因此这对标签界定了内容为“这里是”的文本片段。
另外请注意:开始标签不能放在结束标签的后面,结束标签也不能放在开始标签的前面。
标签重心
每个片段标签都有一个重心设置用来说明当文本流在标签位置上插入了文本时该如何进行处理。当标签位置上发生插入操作时,标签的位置肯定相应地要进行调整。重心就是用来决定如何调整标签位置的。
例如:
如果在片段位置上插入文本“真的”的话,片段开始标签可能会被调整到已插入文本的前面或后面:
或:
标签重心用来说明当标签位置上插入文本时,该如何重新调整其位置。有两种重心设置可以选择:前倾或后倾。
当标签重心设为前倾时,标签相对文本插入位置向前移动,因此插入结果是文本处于标签的后面:
当标签重心设为后倾时,标签相对文本插入位置向后移动,因此插入结果是文本处于标签的前面:
复制和备份
有两种方法可以“复制”文本片段对象。第一种是使用 ITfRange::Clone 方法来克隆,第二种是使用 ITfContext::CreateRangeBackup 方法来 备份 。
克隆将创建片段的一个副本(不包括静态数据),克隆片段将复制源片段的标签并覆盖其文本内容。克隆片段实际上是源片段的全关联对象,也就是说当克隆片段或源片段中某个的文本或属性发生改变时,其它关联片段也会动态地发生同样的改变。
备份将片段当前的文本和属性作为静态数据存储起来,然后克隆并跟踪源片段的容量和位置发生的变化。也就是说备份片段的文本和属性是静态的,不会随源片段发生改变而改变。
例如,下面这个文本流中包含了一个标记为pRange的片段:
接下来分别克隆和备份这个片段:
此时,片段、克隆和备份对象的内容分别如下:
然后更改pRange片段的文本:
此时,各有关对象的内容如下:
上面这个改变文本流内容的操作同时导致了pRange和pClone片段的结束标签发生改变。现在pClone的文本内容为“中文字”,因为源片段中的文本内容发生了改变,而这些改变可以被所有克隆片段所跟踪到。当pRange和pClone片段所覆盖的文本发生改变时,pClone的文本同时也被改变了。
pBackup中的文本没有随着源片段pRange的改变而改变,因为备份片段中的数据(文本和属性)不与文本流内容相关而是单独存储的。对备份片段的内容所做的修改是静态的,也就是说不会影响到文本流和源片段的内容。
在恢复备份时,备份内容完全可以应用到源片段或另一个片段中去。应用备份内容到源片段的方法是,在调用 ITfRangeBackup::Restore 方法时传递一个空指针给 pRange 参数,比如:
此时,各有关对象的内容如下:
恢复备份到另一个片段的方法是,在调用 ITfRangeBackup::Restore 方法时传递目标片段的指针给 pRange 参数,备份文本和属性将应用到指定的目标片段中。比如,在调用 Restore 方法前先对pRange片段做出以下的一些改变:
此时,各有关对象的内容如下:
注意: 当pRange片段的结束标签向前移动两个字符位置之后,pClone片段的结束标签没有发生变化。
接下来恢复备份到pRange中:
此时,各有关对象的内容如下:
pRange覆盖的那部分文本被替换为“文字”,被pClone覆盖的那部分文本相应发生改变,而pBackup则修改为与pRange匹配。
在TSF中,属性被用来关联文本片段和元数据。这些属性包括粗体文本、文本语言标识符和文本服务提供的原始数据(比如从语音文本服务那里得到的和文本相关的音频数据)。
下面的格式文本实例演示了一个假设的文本色彩属性是如何通过红(R)、绿(G)和蓝(B)三种基本颜色参数来定义任何可能的色彩表现的。
不同类型的属性可以叠加在一起。仍以上面这段文本为例,可以为它加上色彩和加粗(B)或倾斜(I)的文本属性。
文本“this□”将会被设为加粗,“is“将会被设为加粗和红色,“□some□”将会以常规方式显示,“colored□”将会被设为绿色和倾斜,而“text“则被设为倾斜。
相同类型的属性不能叠加在一起。比如以下的情形是不允许出现的,因为文本“is“和”colored“重复定义了相同类型的属性值。
属性类型
TSF定义了三种不同的属性类型:
静态属性
静态属性对象把属性数据和文本存储在一起,另外它还存储了属性所应用到的每个文本片段的片段信息。当使用GUID_TFCAT_PROPSTYLE_STATIC参数调用属性对象的 ITfReadOnlyProperty::GetType方法返回成功时,表示该属性为静态属性。
简洁式静态属性
除了不存储文本片段信息之外,简洁式静态属性与静态属性完全一样。当需要用片段覆盖具备简洁式静态属性的文本时,TSF将会把每段属性相似的文本创建为一个分组,然后创建一个单独的片段覆盖它。对于那些基于每个字符来存储属性的要求来说,简洁式静态属性是最有效的实现方式。当使用GUID_TFCAT_PROPSTYLE_STATICCOMPACT参数调用属性对象的 ITfReadOnlyProperty::GetType方法返回成功时,表示该属性为简洁式静态属性。
定制属性
定制属性对象中存储了属性所应用到的每个文本片段的片段信息。它不存储属性的实际数据,而是存储了一个 ITfPropertyStore对象。TSF管理器使用这个对象来访问和维护属性数据。当使用GUID_TFCAT_PROPSTYLE_CUSTOM参数调用属性对象的 ITfReadOnlyProperty::GetType方法返回成功时,表示该属性为定制属性。
属性的使用
获取属性值的方法是使用 ITfReadOnlyProperty接口,而修改属性值的方法则是使用 ITfProperty接口。
当调用 ITfContext::GetProperty方法时,必须指定一个特定的属性类型。 ITfContext::GetProperty方法需要用一个 GUID来标识想要获取的属性。TSF 定义并广泛应用着一组 预设属性标识符,文本服务也可以定义自己的属性标识符。如果要使用定制属性的话,提供属性的程序必须公布属性 GUID及其数据获取格式。
比如,要获取某个文本片段所有者的 CLSID时,就应该先用 ITfContext::GetProperty方法获取属性对象,然后用 ITfProperty::FindRange方法得到完整覆盖了该属性的文本片段,最后用 ITfReadOnlyProperty::GetValue方法得到一个 TfGuidAtom类型数据,它代表拥有这个片段的文本服务的 CLSID。以下的示例代码演示这么一种应用编程情形:在给定一个编辑内容、文本片段和编辑信息片段的条件下,编写一个专门的处理函数来获取拥有这段文本内容的文本服务的 CLSID。
通过 ITfContext::EnumProperties方法返回的 IEnumTfProperties接口,可以列举出所有的属性对象。
属性的永久存储
通常,属性对于应用程序和使用它们的一个或多个文本服务来说,是临时性的。当应用中出现保存属性数据的需求时(比如保存到一个文件中),应用程序必须把属性数据串接起来(存储过程)或拆解出来(读取过程)。在这种处理模式下,应用程序关心的不是个别属性,而是要列举编辑内容中的所有属性并存储它们。
在应用程序中要完成属性数据的存储工作就应该通过如下几个步骤:
1. 调用 ITfContext::EnumProperties方法获得一个属性罗列器。
2. 通过 IEnumTfProperties::Next方法罗列每一个属性。
3. 用每个属性的 ITfReadOnlyProperty::EnumRanges方法获得它们各自的文本片段罗列器。
4. 使用 IEnumTfRanges::Next方法罗列属性中的每个文本片段。
5. 对于属性中的每个片段,以相应的属性、片段、一个 TF_PERSISTENT_PROPERTY_HEADER_ACP结构和一个由应用程序实现的流对象为参数调用 ITextStoreACPServices::Serialize方法。
6. 把 TF_PERSISTENT_PROPERTY_HEADER_ACP结构的内容写入永久性存储器。
7. 把流对象的内容写入永久性存储器。
8. 重复上述这些步骤把所有属性中的全部文本片段存储起来。
9. 应用程序需要把某些类型的终止信息写入到流中,这样当读取数据时,就可以通过这些终止信息来标识停止读取的位置。
以下的示例代码演示了一个方法,应用程序可以用它来串接编辑内容中的属性数据。
当应用程序调用 TextStoreACPServices::Serialize方法来串接定制属性的时候,TSF管理器将通过 ITfPropertyStore::Serialize方法把属性数据存储到指定的流当中 。
在应用程序中实现属性数据读取的步骤如下:
1. 设置流指针让其指向第一个 TF_PERSISTENT_PROPERTY_HEADER_ACP结构开始的地方。
2. 读取 TF_PERSISTENT_PROPERTY_HEADER_ACP结构。
3. 通过 TF_PERSISTENT_PROPERTY_HEADER_ACP结构的 guidType成员来调用 ITfContext::GetProperty方法。
4. 接下来应用程序可以进行以下两项工作之一:
a. 创建一个 ITfPersistentPropertyLoaderACP对象(由应用程序实现)的实例,然后调用 ITextStoreACPServices::Unserialize方法( pStream和 ITfPersistentPropertyLoaderACP参数设为空)。
b. 调用 ITextStoreACPServices::Unserialize(传入输入流参数, pLoader设为空)。
通常应该首先考虑使用第一个方法,因其效率较高。方法二在调用 ITextStoreACPServices::Unserialize的时候会导致读出流中的所有属性数据,而方法一则只是在最后才读出所需的属性数据。
5. 重复上述步骤直到所有的属性区块都拆解出来为止。
以下的示例代码演示了一个方法,应用程序可以用它来把存储在永久性存储器中的属性数据拆解出来。
输入组合是一个临时输入状态,它允许一个文本服务把应用程序和输入文字的用户保持在一个不断变化的状态中。应用程序可以且应该获取输入组合的显示属性信息,并使用这个信息来把输入组合状态显示给用户。
下面来看一个输入组合应用在语音输入中的例子。当用户说话的时候,语音文本服务会创建一个输入组合,该输入组合会被完整保留直到整个语音输入过程完成之后,当会话结束时,语音文本服务就会停止输入组合。
应用程序根据输入组合是否存在来决定如何显示和显示什么样的文本,并完成相应的处理工作。例如,当用户使用语音输入文字时,应用程序将不在任何输入组合文本上执行任何的拼写和文法检查,因为在输入组合结束前得到的文本被认为是不完整的。
文本服务
文本服务是通过调用 ITfContextComposition::StartComposition 方法来创建输入组合的,然后可以随时创建一个 ITfCompositionSink 对象来接收输入组合的事件消息。 StartComposition 方法返回一个 ITfComposition 对象,由文本服务维护对该对象的引用并用来更改和结束输入组合。文本服务是通过 ITfComposition::EndComposition 方法来结束输入组合的。
如果一个文本服务要创建输入组合的话,那它就应该提供在应用程序中区别组合输入文本和常规文本的显示属性支持。有关这方面的更多内容,请查阅 提供显示属性 一节。
应用程序
通过安装一个 ITfContextOwnerCompositionSink 接收器,应用程序可以跟踪输入组合的创建、更改和结束等情况。当一个输入组合启动时,就会触发 ITfContextOwnerCompositionSink::OnStartComposition 方法。同样地,当输入组合更改或结束时,就会分别触发 ITfContextOwnerCompositionSink::OnUpdateComposition 和 ITfContextOwnerCompositionSink::OnEndComposition 方法。
以下是一个使用输入组合来更改文档的典型过程:
1. 使用 ITextStoreACP::InsertTextAtSelection 或 ITextStoreAnchor::InsertTextAtSelection 方法来向输入组合中插入初始文本。
2. 通过从 InsertTextAtSelection 返回的文本片段,调用 ITfContextComposition::StartComposition 方法来启动输入组合。
3. 当接收到来自语音或键盘接口的新输入时,应用程序通过 ITextStoreACP::SetText 或 ITextStoreAnchor::SetText 方法来更改输入组合。
4. 当应用程序确定要结束输入组合时,调用 ITfComposition::EndComposition 方法就可以了。
应用程序应该使用由文本服务提供的显示属性来随时更改文本的显示,而不仅仅是在输入组合激活的时候。有关这方面的更多内容,请查阅 提供显示属性 一节。
必要时,应用程序也可以通过 ITfContextOwnerCompositionServices::TerminateComposition 方法来终止一个输入组合。
以下列出的是在 支持TSF的应用程序 中要用到或者需要它去实现的TSF编程要素。
• 文本存储
• 文档锁
• 显示属性的使用
• 嵌入对象
• 语言栏