TSF输入法框架开发全指南:从COM组件到拼音输入法落地(C++/VS2022)
引言
TSF(Text Services Framework)是微软从Windows XP开始推出的现代文本输入服务框架,旨在替代传统IMM框架,通过COM组件化设计实现应用程序与文本服务的解耦,支持键盘、手写、语音等多源输入,是开发Windows平台输入法的首选方案。
本文基于Windows SDK官方规范+实际开发实践,以“基础组件→核心接口→业务逻辑→UI实现→测试落地”为脉络,拆解TSF输入法开发的每一个关键函数,提供可直接复用的C++代码框架、避坑要点和调试步骤,适合有C++/COM基础,想入门输入法开发的开发者。
开发环境:VS2022 + Windows 10/11 SDK(语言:C++,Unicode字符集)
一、前置准备:工程初始化(必做)
1. 工程配置
创建“Win32 DLL”空项目,完成以下配置:
- 包含头文件:
#include <tsf.h>、#include <<tspub.h> - 链接库:工程属性→链接器→输入→附加依赖项添加
tsf.lib、ole32.lib(COM依赖) - 字符集:工程属性→配置属性→常规→字符集→选择“使用Unicode字符集”(TSF接口均为Unicode)
2. 自定义GUID(避免冲突)
TSF服务需通过唯一GUID标识,使用VS生成自定义GUID(工具→创建GUID→选择“GUID_STR”格式),替换以下代码中的示例值:
// 文本服务CLSID(自定义,替换为自己生成的GUID)constCLSID CLSID_MyTSFInputMethod={0x12345678,0x1234,0x1234,{0x12,0x34,0x56,0x78,0x90,0xab,0xcd,0xef}};// 文本输入处理器IID(复用微软官方定义,无需修改)constIID IID_ITfTextInputProcessor={0x529A9E6B,0x6587,0x4F23,{0xAB,0x9E,0x17,0x9D,0x41,0x36,0x2F,0xB0}};二、阶段1:COM组件核心函数(系统加载DLL的基础)
TSF文本服务本质是COM DLL,需先实现以下5个核心函数,否则系统无法识别和加载。
1. DLL入口:DllMain
核心作用:初始化/释放COM环境,禁用不必要的线程通知,确保TSF多线程模型兼容。
BOOL APIENTRYDllMain(HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved){switch(ul_reason_for_call){caseDLL_PROCESS_ATTACH:// TSF必须使用多线程COM模型CoInitializeEx(NULL,COINIT_MULTITHREADED);// 禁用线程通知,提升性能DisableThreadLibraryCalls(hModule);break;caseDLL_PROCESS_DETACH:// 释放COM资源CoUninitialize();break;}returnTRUE;}避坑要点:不可省略COINIT_MULTITHREADED,否则会导致TSF接口调用失败。
2. 服务注册:DllRegisterServer
核心作用:向注册表写入COM标识、TSF服务信息和支持语言,让系统识别输入法。
STDAPIDllRegisterServer(void){WCHAR szCLSID[40]={0};StringFromGUID2(CLSID_MyTSFInputMethod,szCLSID,_countof(szCLSID));WCHAR szKey[256]={0};// 1. 注册COM CLSID(系统识别组件的基础)wsprintf(szKey,L"CLSID\\%s",szCLSID);HKEY hKey=NULL;RegCreateKeyEx(HKEY_CLASSES_ROOT,szKey,0,NULL,REG_OPTION_NON_VOLATILE,KEY_WRITE,NULL,&hKey,NULL);RegSetValueEx(hKey,NULL,0,REG_SZ,(BYTE*)L"My TSF Input Method",sizeof(L"My TSF Input Method"));RegCloseKey(hKey);// 2. 注册TSF文本输入处理器(TSF管理器识别关键)wsprintf(szKey,L"CLSID\\%s\\TextServices",szCLSID);RegCreateKeyEx(HKEY_CLASSES_ROOT,szKey,0,NULL,REG_OPTION_NON_VOLATILE,KEY_WRITE,NULL,&hKey,NULL);RegCloseKey(hKey);// 3. 注册支持语言(0804=简体中文,0409=英文,参考微软语言代码)wsprintf(szKey,L"CLSID\\%s\\Language",szCLSID);RegCreateKeyEx(HKEY_CLASSES_ROOT,szKey,0,NULL,REG_OPTION_NON_VOLATILE,KEY_WRITE,NULL,&hKey,NULL);RegSetValueEx(hKey,NULL,0,REG_SZ,(BYTE*)L"0804",sizeof(L"0804"));RegCloseKey(hKey);returnS_OK;}使用方式:管理员权限打开命令行,输入regsvr32 你的DLL路径(如regsvr32 D:\TSFInput\Debug\MyTSFInput.dll)完成注册。
避坑要点:必须包含TextServices和Language子键,否则TSF管理器无法识别。
4. 服务注销:DllUnregisterServer
核心作用:删除注册表中输入法的所有项,避免残留。
STDAPIDllUnregisterServer(void){WCHAR szCLSID[40]={0};StringFromGUID2(CLSID_MyTSFInputMethod,szCLSID,_countof(szCLSID));WCHAR szKey[256]={0};wsprintf(szKey,L"CLSID\\%s",szCLSID);// 递归删除整个CLSID子键(包含所有子项)SHDeleteKey(HKEY_CLASSES_ROOT,szKey);returnS_OK;}避坑要点:使用SHDeleteKey而非RegDeleteKey,确保子键完全删除。
4. 类工厂:IClassFactory::CreateInstance
核心作用:COM类工厂的核心方法,为TSF管理器创建文本服务实例。
先定义类工厂类:
classCMyTSFClassFactory:publicIClassFactory{public:// 创建文本服务实例STDMETHOD(CreateInstance)(IUnknown*pUnkOuter,REFIID riid,void**ppvObj)override{// TSF不支持组件聚合,禁止pUnkOuter非空if(pUnkOuter!=NULL)returnCLASS_E_NOAGGREGATION;// 创建文本服务核心类实例(后续定义)CMyTSFInputMethod*pIM=newCMyTSFInputMethod();if(pIM==NULL)returnE_OUTOFMEMORY;// 接口查询,返回请求的接口returnpIM->QueryInterface(riid,ppvObj);}// 简化实现:锁定组件(无需复杂处理)STDMETHOD(LockServer)(BOOL fLock)override{returnS_OK;}// COM基础方法:QueryInterface(接口路由)STDMETHOD(QueryInterface)(REFIID riid,void**ppvObj)override{if(riid==IID_IUnknown||riid==IID_IClassFactory){*ppvObj=(IClassFactory*)this;AddRef();returnS_OK;}*ppvObj=NULL;returnE_NOINTERFACE;}// COM引用计数ULONGAddRef()override{returnInterlockedIncrement(&m_cRef);}ULONGRelease()override{ULONG cRef=InterlockedDecrement(&m_cRef);if(cRef==0)deletethis;returncRef;}private:LONG m_cRef=1;// 引用计数初始化为1};5. 暴露类工厂:DllGetClassObject
核心作用:让系统获取类工厂实例,进而创建文本服务对象。
STDAPIDllGetClassObject(REFCLSID rclsid,REFIID riid,void**ppvObj){// 仅响应自定义CLSID的请求if(rclsid!=CLSID_MyTSFInputMethod)returnCLASS_E_CLASSNOTAVAILABLE;CMyTSFClassFactory*pFactory=newCMyTSFClassFactory();if(pFactory==NULL)returnE_OUTOFMEMORY;returnpFactory->QueryInterface(riid,ppvObj);}三、阶段2:定义TSF文本服务类(核心业务载体)
文本服务类是所有TSF接口的实现容器,整合输入逻辑、上下文管理、业务资源,需先定义类结构再实现接口方法。
1. 类结构定义:CMyTSFInputMethod
classCMyTSFInputMethod:publicITfTextInputProcessor,// TSF入口接口(必须继承)publicITfThreadMgrEventSink,// 上下文事件监听接口publicITfEditSession,// 文本编辑会话接口publicITfContextOwnerCompositionSink// 组合输入事件接口{public:// 构造/析构函数CMyTSFInputMethod():m_cRef(1),m_pThreadMgr(NULL),m_pCurrentContext(NULL),m_dwEventSinkCookie(0){}~CMyTSFInputMethod(){// 析构时释放资源if(m_pThreadMgr)m_pThreadMgr->Release();if(m_pCurrentContext)m_pCurrentContext->Release();}// -------------------------- COM基础方法(必须实现)--------------------------STDMETHOD(QueryInterface)(REFIID riid,void**ppvObj)override;STDMETHOD_(ULONG,AddRef)()override{returnInterlockedIncrement(&m_cRef);}STDMETHOD_(ULONG,Release)()override;// -------------------------- ITfTextInputProcessor接口(TSF入口)--------------------------STDMETHOD(Activate)(ITfThreadMgr*pThreadMgr,TfClientId tid,DWORD dwFlags)override;STDMETHOD(Deactivate)()override;STDMETHOD(GetInfo)(TF_TEXTINPUTPROCESSORINFO*pInfo)override;STDMETHOD(GetStatus)(DWORD*pdwFlags)override{*pdwFlags=0;returnS_OK;}// 简化实现// -------------------------- ITfThreadMgrEventSink接口(上下文事件)--------------------------STDMETHOD(OnContextCreated)(ITfThreadMgr*pThreadMgr,ITfContext*pContext)override;STDMETHOD(OnContextDestroyed)(ITfThreadMgr*pThreadMgr,ITfContext*pContext)override;STDMETHOD(OnSetFocus)(ITfThreadMgr*pThreadMgr,ITfContext*pContextFocus,ITfContext*pContextPrevFocus)override;// 其他默认实现(无需修改)STDMETHOD(OnPushContext)(ITfThreadMgr*pThreadMgr,ITfContext*pContext)override{returnS_OK;}STDMETHOD(OnPopContext)(ITfThreadMgr*pThreadMgr,ITfContext*pContext)override{returnS_OK;}// -------------------------- ITfEditSession接口(文本编辑)--------------------------STDMETHOD(DoEditSession)(ITfContext*pContext,TfEditCookie ec)override;// -------------------------- ITfContextOwnerCompositionSink接口(组合输入)--------------------------STDMETHOD(OnCompositionTerminated)(TfEditCookie ecWrite,ITfComposition*pComposition)override{returnS_OK;}// -------------------------- 自定义业务方法(后续实现)--------------------------voidInitPinyinDict();// 初始化拼音词库std::vector<CStringW>PinyinParser(constWCHAR*pchPinyin);// 拼音解析voidCommitText(constWCHAR*pchText);// 提交文本HRESULTStartComposition();// 开始组合输入HRESULTEndComposition(ITfComposition*pComposition);// 结束组合输入private:// 成员变量(核心状态管理)LONG m_cRef;// COM引用计数ITfThreadMgr*m_pThreadMgr;// TSF线程管理器(核心交互对象)ITfContext*m_pCurrentContext;// 当前活跃的编辑上下文(用户正在输入的区域)TfClientId m_tid;// TSF客户端ID(标识当前文本服务)DWORD m_dwEventSinkCookie;// 事件接收器Cookie(用于注销监听)std::map<CStringW,std::vector<CStringW>>m_mapPinyinToHanzi;// 拼音-汉字映射表(业务数据)};核心说明:必须继承ITfTextInputProcessor(TSF管理器交互入口),成员变量m_pThreadMgr和m_pCurrentContext是后续所有文本操作的基础。
2. COM基础方法:QueryInterface
核心作用:COM组件的“接口路由”,让外部通过IID获取当前类实现的接口。
STDMETHODIMPCMyTSFInputMethod::QueryInterface(REFIID riid,void**ppvObj){*ppvObj=NULL;// 匹配所有继承的接口IIDif(riid==IID_IUnknown)*ppvObj=(IUnknown*)this;elseif(riid==IID_ITfTextInputProcessor)*ppvObj=(ITfTextInputProcessor*)this;elseif(riid==IID_ITfThreadMgrEventSink)*ppvObj=(ITfThreadMgrEventSink*)this;elseif(riid==IID_ITfEditSession)*ppvObj=(ITfEditSession*)this;elseif(riid==IID_ITfContextOwnerCompositionSink)*ppvObj=(ITfContextOwnerCompositionSink*)this;elsereturnE_NOINTERFACE;// 不支持的接口((IUnknown*)*ppvObj)->AddRef();// 接口返回前必须增加引用计数returnS_OK;}避坑要点:必须包含所有继承的接口IID,遗漏会导致外部无法调用对应接口方法。
3. COM基础方法:Release
核心作用:引用计数为0时,删除对象并释放所有资源(避免内存泄漏)。
STDMETHODIMP_(ULONG)CMyTSFInputMethod::Release(){ULONG cRef=InterlockedDecrement(&m_cRef);if(cRef==0){// 1. 注销事件接收器if(m_pThreadMgr&&m_dwEventSinkCookie!=0)m_pThreadMgr->UnadviseSink(m_dwEventSinkCookie);// 2. 释放核心对象引用if(m_pThreadMgr){m_pThreadMgr->Release();m_pThreadMgr=NULL;}if(m_pCurrentContext){m_pCurrentContext->Release();m_pCurrentContext=NULL;}// 3. 释放业务资源m_mapPinyinToHanzi.clear();// 4. 删除对象本身deletethis;}returncRef;}四、阶段3:实现TSF入口接口(ITfTextInputProcessor)
这是TSF管理器与文本服务的核心交互入口,必须优先实现,否则输入法无法被系统激活。
1.GetInfo:返回输入法基本信息
核心作用:向TSF管理器返回输入法名称、CLSID等信息,用于系统显示。
STDMETHODIMPCMyTSFInputMethod::GetInfo(TF_TEXTINPUTPROCESSORINFO*pInfo){if(pInfo==NULL)returnE_INVALIDARG;// 参数校验// 填充输入法信息pInfo->clsid=CLSID_MyTSFInputMethod;// 与自定义CLSID一致pInfo->tid=m_tid;// 客户端ID(Activate时传入)wcscpy_s(pInfo->szDescription,_countof(pInfo->szDescription),L"TSF拼音输入法");// 系统显示名称pInfo->dwFlags=0;returnS_OK;}说明:szDescription是输入法在系统语言栏中的显示名称,建议简洁明了。
2.Activate:激活文本服务(核心)
核心作用:输入法被启用时的初始化逻辑,注册事件监听、加载业务资源。
STDMETHODIMPCMyTSFInputMethod::Activate(ITfThreadMgr*pThreadMgr,TfClientId tid,DWORD dwFlags){// 1. 保存核心对象引用(后续所有操作依赖)m_pThreadMgr=pThreadMgr;m_pThreadMgr->AddRef();// COM对象必须持有引用m_tid=tid;// 2. 注册事件接收器(监听上下文创建、焦点切换等事件)IID iid=IID_ITfThreadMgrEventSink;HRESULT hr=m_pThreadMgr->AdviseSink(iid,(IUnknown*)this,&m_dwEventSinkCookie);if(FAILED(hr))returnhr;// 注册失败则激活失败// 3. 初始化业务资源(如拼音词库)InitPinyinDict();returnS_OK;}避坑要点:必须调用AdviseSink注册事件接收器,否则无法感知输入焦点变化。
3.Deactivate:停用文本服务
核心作用:输入法被关闭时,释放所有资源,避免内存泄漏。
STDMETHODIMPCMyTSFInputMethod::Deactivate(){// 1. 注销事件接收器if(m_pThreadMgr&&m_dwEventSinkCookie!=0){m_pThreadMgr->UnadviseSink(m_dwEventSinkCookie);m_dwEventSinkCookie=0;}// 2. 释放核心对象引用if(m_pThreadMgr){m_pThreadMgr->Release();m_pThreadMgr=NULL;}if(m_pCurrentContext){m_pCurrentContext->Release();m_pCurrentContext=NULL;}// 3. 释放业务资源m_mapPinyinToHanzi.clear();returnS_OK;}避坑要点:Deactivate可能被多次调用,需确保资源释放逻辑幂等(多次调用不报错)。
五、阶段4:实现上下文事件监听(定位输入区域)
通过监听上下文事件,找到用户当前正在编辑的输入框(活跃上下文),为文本输入做准备。
1.OnContextCreated:上下文创建时触发
核心作用:新输入框(如记事本、Word文档)创建时,注册组合输入事件监听。
STDMETHODIMPCMyTSFInputMethod::OnContextCreated(ITfThreadMgr*pThreadMgr,ITfContext*pContext){// 为新上下文注册组合输入事件接收器IID iid=IID_ITfContextOwnerCompositionSink;DWORD dwCookie=0;pContext->AdviseSink(m_tid,iid,(IUnknown*)this,&dwCookie);returnS_OK;}说明:每个新上下文都需单独注册事件,否则无法在该输入框中进行组合输入(如拼音候选)。
2.OnSetFocus:焦点切换时触发
核心作用:输入焦点切换到新输入框时,更新当前活跃上下文。
STDMETHODIMPCMyTSFInputMethod::OnSetFocus(ITfThreadMgr*pThreadMgr,ITfContext*pContextFocus,ITfContext*pContextPrevFocus){// 释放之前的活跃上下文引用if(m_pCurrentContext)m_pCurrentContext->Release();// 更新为当前焦点上下文m_pCurrentContext=pContextFocus;if(m_pCurrentContext)m_pCurrentContext->AddRef();// 持有新上下文引用returnS_OK;}核心说明:m_pCurrentContext是后续文本输入的目标,必须实时更新。
3.OnContextDestroyed:上下文销毁时触发
核心作用:输入框关闭时,释放对应的上下文引用,避免野指针。
STDMETHODIMPCMyTSFInputMethod::OnContextDestroyed(ITfThreadMgr*pThreadMgr,ITfContext*pContext){// 如果销毁的是当前活跃上下文,释放引用if(m_pCurrentContext==pContext){m_pCurrentContext->Release();m_pCurrentContext=NULL;}returnS_OK;}六、阶段5:实现文本存储接口(ITextStoreAcp)
文本存储是TSF文本流传递的核心,负责文本的读写、插入、状态管理,需单独定义类实现。
1. 文本存储类定义:CMyTextStoreAcp
classCMyTextStoreAcp:publicITextStoreAcp{public:// 构造函数:关联到具体上下文CMyTextStoreAcp(ITfContext*pContext):m_cRef(1),m_pContext(pContext),m_pSink(NULL){m_pContext->AddRef();// 持有上下文引用m_strTextBuffer=L"";// 初始化文本缓冲区}// 析构函数:释放资源~CMyTextStoreAcp(){if(m_pContext)m_pContext->Release();if(m_pSink)m_pSink->Release();}// -------------------------- COM基础方法 --------------------------STDMETHOD(QueryInterface)(REFIID riid,void**ppvObj)override;STDMETHOD_(ULONG,AddRef)()override{returnInterlockedIncrement(&m_cRef);}STDMETHOD_(ULONG,Release)()override;// -------------------------- ITextStoreAcp核心方法 --------------------------STDMETHOD(AdviseSink)(REFIID riid,IUnknown*punk,DWORD dwMask)override;STDMETHOD(UnadviseSink)(IUnknown*punk)override;STDMETHOD(RequestLock)(DWORD dwLockFlags,HRESULT*phrSession)override;STDMETHOD(GetStatus)(TS_STATUS*pdcs)override;STDMETHOD(QueryInsert)(LONG acpTestStart,LONG acpTestEnd,LONG cchNew,LONG*pacpResultStart,LONG*pacpResultEnd)override;STDMETHOD(InsertTextAt)(TfEditCookie ec,LONG acpStart,constWCHAR*pchText,LONG cch,TS_TEXTCHANGE*pChange)override;STDMETHOD(GetText)(TfEditCookie ec,LONG acpStart,LONG acpEnd,WCHAR*pchText,LONG cchReq,LONG*pcchOut)override;// -------------------------- ITextStoreAcp默认实现(无需修改)--------------------------STDMETHOD(GetSelection)(...)override{returnE_NOTIMPL;}STDMETHOD(SetSelection)(...)override{returnE_NOTIMPL;}STDMETHOD(GetActiveView)(...)override{returnE_NOTIMPL;}STDMETHOD(GetDocMgr)(...)override{returnE_NOTIMPL;}STDMETHOD(GetEndACP)(LONG*pacp)override{*pacp=m_strTextBuffer.GetLength();returnS_OK;}STDMETHOD(GetStartACP)(LONG*pacp)override{*pacp=0;returnS_OK;}STDMETHOD(QueryText)(...)override{returnE_NOTIMPL;}STDMETHOD(ReplaceTextAt)(...)override{returnE_NOTIMPL;}STDMETHOD(DeleteTextAt)(...)override{returnE_NOTIMPL;}STDMETHOD(GetFormattedText)(...)override{returnE_NOTIMPL;}private:LONG m_cRef;// COM引用计数ITfContext*m_pContext;// 关联的编辑上下文CStringW m_strTextBuffer;// 文本缓冲区(存储输入文本)IUnknown*m_pSink;// 文本变化事件接收器};核心说明:必实现RequestLock(锁机制)、InsertTextAt(插入文本)、GetText(读取文本),其他方法可默认返回E_NOTIMPL。
2. COM基础方法实现
// QueryInterfaceSTDMETHODIMPCMyTextStoreAcp::QueryInterface(REFIID riid,void**ppvObj){*ppvObj=NULL;if(riid==IID_IUnknown||riid==IID_ITextStoreAcp){*ppvObj=(ITextStoreAcp*)this;AddRef();returnS_OK;}returnE_NOINTERFACE;}// ReleaseSTDMETHODIMP_(ULONG)CMyTextStoreAcp::Release(){ULONG cRef=InterlockedDecrement(&m_cRef);if(cRef==0){if(m_pContext)m_pContext->Release();if(m_pSink)m_pSink->Release();deletethis;}returncRef;}3. 核心方法:RequestLock(申请编辑锁)
核心作用:避免多线程同时修改文本缓冲区,保证操作原子性。
STDMETHODIMPCMyTextStoreAcp::RequestLock(DWORD dwLockFlags,HRESULT*phrSession){if(phrSession==NULL)returnE_INVALIDARG;*phrSession=S_OK;// 简化实现:直接允许锁请求(复杂场景需处理锁冲突)returnS_OK;}4. 核心方法:GetStatus(返回文本状态)
核心作用:向TSF管理器声明文本区域可读写。
STDMETHODIMPCMyTextStoreAcp::GetStatus(TS_STATUS*pdcs){if(pdcs==NULL)returnE_INVALIDARG;ZeroMemory(pdcs,sizeof(TS_STATUS));pdcs->dwDynamicFlags=TS_STATUS_READWRITE;// 标记为可读写(输入法核心需求)pdcs->dwStaticFlags=0;returnS_OK;}避坑要点:必须设置TS_STATUS_READWRITE,否则无法插入文本。
5. 核心方法:InsertTextAt(插入文本)
核心作用:将输入法生成的文本(如汉字)插入到缓冲区,并通知应用。
STDMETHODIMPCMyTextStoreAcp::InsertTextAt(TfEditCookie ec,LONG acpStart,constWCHAR*pchText,LONG cch,TS_TEXTCHANGE*pChange){// 参数校验if(pchText==NULL||cch<=0)returnE_INVALIDARG;// 1. 插入文本到缓冲区m_strTextBuffer.Insert(acpStart,pchText,cch);// 2. 填充文本变化信息(通知应用)if(pChange!=NULL){pChange->acpStart=acpStart;pChange->acpOldEnd=acpStart;// 插入前结束索引pChange->acpNewEnd=acpStart+cch;// 插入后结束索引}// 3. 通知事件接收器(如有注册)if(m_pSink){ITextStoreACPSink*pSink=NULL;m_pSink->QueryInterface(IID_ITextStoreACPSink,(void**)&pSink);if(pSink){pSink->OnTextChange(ec,pChange);pSink->Release();}}returnS_OK;}6. 核心方法:GetText(读取文本)
核心作用:读取缓冲区中指定范围的文本。
STDMETHODIMPCMyTextStoreAcp::GetText(TfEditCookie ec,LONG acpStart,LONG acpEnd,WCHAR*pchText,LONG cchReq,LONG*pcchOut){// 参数校验if(pchText==NULL||pcchOut==NULL)returnE_INVALIDARG;// 计算可读取的字符数(避免越界)LONG cchTextLen=m_strTextBuffer.GetLength();LONG cchToRead=min(cchReq,acpEnd-acpStart);cchToRead=min(cchToRead,cchTextLen-acpStart);if(cchToRead<0)cchToRead=0;// 复制文本到输出缓冲区wcscpy_s(pchText,cchReq,m_strTextBuffer.Mid(acpStart,cchToRead));*pcchOut=cchToRead;// 返回实际读取长度returnS_OK;}7. 事件注册:AdviseSink/UnadviseSink
// 注册事件接收器STDMETHODIMPCMyTextStoreAcp::AdviseSink(REFIID riid,IUnknown*punk,DWORD dwMask){if(punk==NULL)returnE_INVALIDARG;if(m_pSink)m_pSink->Release();m_pSink=punk;m_pSink->AddRef();returnS_OK;}// 注销事件接收器STDMETHODIMPCMyTextStoreAcp::UnadviseSink(IUnknown*punk){if(m_pSink==punk){m_pSink->Release();m_pSink=NULL;}returnS_OK;}七、阶段6:实现编辑会话与组合输入(业务核心)
编辑会话确保文本操作原子性,组合输入管理“拼音→候选词→确认输入”的核心流程。
1.DoEditSession:执行原子性文本操作
核心作用:所有文本修改(插入/删除)必须通过该方法执行,避免线程冲突。
STDMETHODIMPCMyTSFInputMethod::DoEditSession(ITfContext*pContext,TfEditCookie ec){if(pContext==NULL)returnE_INVALIDARG;// 1. 获取文本存储对象ITextStoreAcp*pTextStore=NULL;HRESULT hr=pContext->QueryInterface(IID_ITextStoreAcp,(void**)&pTextStore);if(FAILED(hr))returnhr;// 2. 插入文本(实际开发替换为拼音解析结果)WCHAR szTargetText[]=L"你好";TS_TEXTCHANGE textChange={0};hr=pTextStore->InsertTextAt(ec,0,szTargetText,_countof(szTargetText)-1,&textChange);// 3. 释放资源pTextStore->Release();returnhr;}避坑要点:不可直接调用ITextStoreAcp::InsertTextAt,必须通过编辑会话执行。
2. 自定义方法:CommitText(触发编辑会话)
核心作用:拼音解析完成后,调用该方法提交文本。
voidCMyTSFInputMethod::CommitText(constWCHAR*pchText){if(m_pCurrentContext==NULL)return;// 触发编辑会话(异步执行,不阻塞主线程)HRESULT hr=m_pCurrentContext->RequestEditSession(m_tid,// 客户端ID(ITfEditSession*)this,// 编辑会话对象TF_ES_ASYNCDONTCARE,// 异步标志NULL// 无需回调);}3. 组合输入:StartComposition(开始未确认输入)
核心作用:用户输入拼音但未选字时,启动组合输入模式。
HRESULTCMyTSFInputMethod::StartComposition(){if(m_pCurrentContext==NULL)returnE_INVALIDARG;ITfComposition*pComposition=NULL;// 创建组合输入对象HRESULT hr=m_pCurrentContext->StartComposition(m_tid,&pComposition);if(SUCCEEDED(hr)){// 设置组合文本范围TS_RANGE compositionRange={0,0};hr=pComposition->SetCompositionRange(m_tid,&compositionRange);// 添加拼音文本(如用户输入的“nihao”)hr=pComposition->AddText(m_tid,L"nihao",_countof(L"nihao")-1);pComposition->Release();}returnhr;}4. 组合输入:EndComposition(确认输入)
核心作用:用户选择候选词后,结束组合模式并提交最终文本。
HRESULTCMyTSFInputMethod::EndComposition(ITfComposition*pComposition){if(pComposition==NULL)returnE_INVALIDARG;// 1. 结束组合模式HRESULT hr=pComposition->EndComposition(m_tid);// 2. 提交最终文本CommitText(L"你好");returnhr;}八、阶段7:UI实现(语言栏+候选框)
1. 语言栏按钮类:CMyLangBarItem
classCMyLangBarItem:publicITfLangBarItemButton{public:CMyLangBarItem():m_cRef(1){ZeroMemory(&m_itemInfo,sizeof(m_itemInfo));m_itemInfo.clsidService=CLSID_MyTSFInputMethod;m_itemInfo.guidItem=GUID_NULL;// 自定义GUIDm_itemInfo.dwStyle=TF_LBI_STYLE_BTN_BUTTON;wcscpy_s(m_itemInfo.szDescription,L"TSF拼音输入法");}// COM基础方法(QueryInterface/AddRef/Release)参考之前实现STDMETHOD(QueryInterface)(REFIID riid,void**ppvObj)override{*ppvObj=NULL;if(riid==IID_IUnknown||riid==IID_ITfLangBarItemButton){*ppvObj=(ITfLangBarItemButton*)this;AddRef();returnS_OK;}returnE_NOINTERFACE;}// 按钮信息STDMETHOD(GetInfo)(TF_LANGBARITEMINFO*pInfo)override{if(pInfo==NULL)returnE_INVALIDARG;*pInfo=m_itemInfo;returnS_OK;}// 按钮提示STDMETHOD(GetTooltipString)(BSTR*pbstrToolTip)override{*pbstrToolTip=SysAllocString(L"TSF拼音输入法");returnS_OK;}// 点击事件(切换中英文)STDMETHOD(OnClick)(TfLBIClick click,POINT pt,constRECT*prcArea)override{m_bChineseMode=!m_bChineseMode;returnS_OK;}STDMETHOD(Show)(BOOL fShow)override{returnS_OK;}STDMETHOD(Hide)()override{returnS_OK;}private:LONG m_cRef;TF_LANGBARITEMINFO m_itemInfo;BOOL m_bChineseMode=TRUE;// 默认中文模式};2. 候选框:ShowCandidateUI(显示候选词)
voidShowCandidateUI(conststd::vector<CStringW>&vecCandidates,POINT ptInput){// 创建无边框候选框窗口HWND hCandidateWnd=CreateWindowEx(0,L"STATIC",L"候选词",WS_POPUP|WS_VISIBLE,ptInput.x,ptInput.y+20,200,300,NULL,NULL,GetModuleHandle(NULL),NULL);// 绘制候选词HDC hdc=GetDC(hCandidateWnd);for(inti=0;i<vecCandidates.size();i++){TextOut(hdc,10,10+i*20,vecCandidates[i],vecCandidates[i].GetLength());}ReleaseDC(hCandidateWnd,hdc);}九、阶段8:业务逻辑(拼音解析+词库)
1. 初始化拼音词库:InitPinyinDict
voidCMyTSFInputMethod::InitPinyinDict(){// 简化:硬编码拼音-汉字映射(实际开发读词库文件)m_mapPinyinToHanzi[L"nihao"]={L"你好",L"泥壕",L"倪浩"};m_mapPinyinToHanzi[L"zhongguo"]={L"中国",L"忠国",L"钟国"};}2. 拼音解析:PinyinParser
std::vector<CStringW>CMyTSFInputMethod::PinyinParser(constWCHAR*pchPinyin){if(pchPinyin==NULL)return{};// 查找拼音对应的候选词autoit=m_mapPinyinToHanzi.find(pchPinyin);if(it!=m_mapPinyinToHanzi.end())returnit->second;return{};}3. 键盘事件处理:InputWndProc
// 全局变量:文本服务实例(简化实现,实际需优化)CMyTSFInputMethod*g_pIM=NULL;// 获取输入框位置(简化:取鼠标位置)POINTGetInputPos(){POINT pt;GetCursorPos(&pt);returnpt;}// 窗口过程:处理键盘输入LRESULT CALLBACKInputWndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){caseWM_KEYDOWN:{// 收集拼音字符WCHAR chInput=(WCHAR)wParam;staticCStringW strPinyinBuffer;strPinyinBuffer+=chInput;// 解析拼音,生成候选词std::vector<CStringW>vecCandidates=g_pIM->PinyinParser(strPinyinBuffer);// 显示候选框ShowCandidateUI(vecCandidates,GetInputPos());return0;}default:returnDefWindowProc(hWnd,uMsg,wParam,lParam);}}十、阶段9:测试与调试(落地关键)
1. 注册DLL
- 管理员权限打开命令行,输入
regsvr32 你的DLL路径 - 注册成功会弹出提示,失败需检查注册表写入逻辑
2. 验证注册
- 工具:Windows SDK的
tsfscanner.exe(路径:C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64) - 操作:运行
tsfscanner.exe,在“Text Services”中查找自定义CLSID
3. 调试代码
- VS中设置断点(
Activate、DoEditSession、InsertTextAt) - 启动记事本(notepad.exe)
- VS→调试→附加到进程→选择“notepad.exe”
- 切换到自定义输入法,输入文字触发断点
4. 启用输入法
- Windows设置→时间和语言→输入→高级键盘设置→添加输入法
- 选择“TSF拼音输入法”,切换后在记事本中测试
十一、核心避坑指南(开发必看)
- COM引用计数严格匹配:每个
AddRef对应一个Release,遗漏会导致内存泄漏,多调用会导致野指针。 - 文本操作必须在EditSession中:直接调用
ITextStoreAcp方法会被TSF拒绝,引发线程冲突。 - GUID必须自定义:不可复用系统或其他输入法的GUID,否则注册失败。
- 优先测试记事本:记事本是纯TSF应用,无额外兼容问题,调试通过后再测试Word、浏览器。
- 管理员权限注册:注册DLL必须用管理员权限,否则注册表写入失败。
- 避免阻塞主线程:
Activate、DoEditSession中不可执行耗时操作(如词库加载),需异步处理。
参考资料
- 微软官方TSF文档:Text Services Framework
- Windows SDK TSF示例:
C:\Program Files (x86)\Windows Kits\10\Samples\winui\input\TSF
如果本文对你有帮助,欢迎点赞、收藏、留言交流!后续会更新“TSF手写输入”“多语言支持”等进阶内容~