news 2026/4/2 13:52:11

TSF输入法框架开发全指南:从COM组件到拼音输入法落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TSF输入法框架开发全指南:从COM组件到拼音输入法落地

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.libole32.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)完成注册。
避坑要点:必须包含TextServicesLanguage子键,否则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_pThreadMgrm_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. 调试代码

  1. VS中设置断点(ActivateDoEditSessionInsertTextAt
  2. 启动记事本(notepad.exe)
  3. VS→调试→附加到进程→选择“notepad.exe”
  4. 切换到自定义输入法,输入文字触发断点

4. 启用输入法

  • Windows设置→时间和语言→输入→高级键盘设置→添加输入法
  • 选择“TSF拼音输入法”,切换后在记事本中测试

十一、核心避坑指南(开发必看)

  1. COM引用计数严格匹配:每个AddRef对应一个Release,遗漏会导致内存泄漏,多调用会导致野指针。
  2. 文本操作必须在EditSession中:直接调用ITextStoreAcp方法会被TSF拒绝,引发线程冲突。
  3. GUID必须自定义:不可复用系统或其他输入法的GUID,否则注册失败。
  4. 优先测试记事本:记事本是纯TSF应用,无额外兼容问题,调试通过后再测试Word、浏览器。
  5. 管理员权限注册:注册DLL必须用管理员权限,否则注册表写入失败。
  6. 避免阻塞主线程ActivateDoEditSession中不可执行耗时操作(如词库加载),需异步处理。

参考资料

  • 微软官方TSF文档:Text Services Framework
  • Windows SDK TSF示例:C:\Program Files (x86)\Windows Kits\10\Samples\winui\input\TSF

如果本文对你有帮助,欢迎点赞、收藏、留言交流!后续会更新“TSF手写输入”“多语言支持”等进阶内容~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/23 16:55:22

AI微课视频:教育市场的千亿风口

AI微课视频项目的市场前景AI微课视频结合了人工智能技术与在线教育&#xff0c;市场需求持续增长。在线教育市场规模预计2025年突破5000亿元&#xff0c;AI技术可降低内容制作成本&#xff0c;提升个性化学习体验。企业培训、K12教育、职业资格认证等领域对高质量微课内容需求旺…

作者头像 李华
网站建设 2026/3/23 14:41:53

孤能子视角:“人界线““虚空背景“––普朗克常量

(注意:理论不是科学&#xff0c;是认知工具&#xff0c;用来启发思维。我一般以日常通识发问。比较长&#xff0c;姑且当科幻小说。)我的问题:1.我没看花眼吧&#xff0c;七个基本物理量&#xff0c;快有一半与普朗克常量有关&#xff1f;2.以前讨论过物理常数都有个"硬&q…

作者头像 李华
网站建设 2026/4/1 6:33:18

5个实际场景解析:用PyKalman在Python中实现卡尔曼滤波

5个实际场景解析&#xff1a;用PyKalman在Python中实现卡尔曼滤波 【免费下载链接】pykalman Kalman Filter, Smoother, and EM Algorithm for Python 项目地址: https://gitcode.com/gh_mirrors/py/pykalman 当你的传感器数据充满噪声、预测模型需要处理不确定性时&…

作者头像 李华
网站建设 2026/4/1 14:31:16

跨平台资源监控UI设计:从零构建专业级监控界面

跨平台资源监控UI设计&#xff1a;从零构建专业级监控界面 【免费下载链接】compose-multiplatform JetBrains/compose-multiplatform: 是 JetBrains 开发的一个跨平台的 UI 工具库&#xff0c;基于 Kotlin 编写&#xff0c;可以用于开发跨平台的 Android&#xff0c;iOS 和 ma…

作者头像 李华
网站建设 2026/3/18 14:32:50

人工智能学会看视频做决策!Meta推出能边思考边生成的TV2TV模型

这篇由Meta FAIR实验室的韩晓创团队发表的研究论文&#xff0c;在2025年12月刊载于arXiv预印本平台&#xff08;论文编号&#xff1a;arXiv:2512.05103v1&#xff09;&#xff0c;首次展示了一个能够"边思考边生成"的视频生成模型。有兴趣深入了解的读者可以通过该论…

作者头像 李华
网站建设 2026/3/28 12:27:44

《Python学习手册》第5章 作业

第1题 超大整数阶乘计算器 1.1 需求 编写函数 big_factorial(n),返回 n 的精确阶乘结果,要求支持 n 最大到 5000。 1.2 举例 输入 100,输出 9332621...000000(共 158 位)。 1.3 考点 Python int 无限精度;循环累积效率。 1.4 实现步骤 特判 n < 0 返回 None; …

作者头像 李华