如何在游戏中获取输入法候选列表
本文讲述在windows操作系统中如何通过系统提供的输入法接口获取当前输入法的候选列表信息。在全屏游戏或需要自绘输入法候选列表的软件中均需使用此技术。
在阅读之前,请务必了解windows之中包含“Input Method Editor (IME)”和“Text Services Framework (TSF)”两套输入法接口。对于使用不同框架的输入法应采用不同方式去获取候选列表。本文重点讲述TSF框架下的输入法后续列表获取。TSF框架的输入法实际是一个COM程序,所以在继续阅读之前,请务必了解COM的工作机制。
1 不得不说的IME候选列表获取方式
在网络上应该能搜出一大片此类文章,在此仅做简要说明。 IME的所有函数都在imm32.dll中实现,函数原型可在中查看。
1.1 在什么时候获取候选列表信息
如需在程序中自绘候选列表,可在程序中响应如下几个重要消息:
switch( uMsg )
{
case WM_IME_STARTCOMPOSITION: // 开始编码
case WM_IME_COMPOSITION: // 编码串已更新(显示串更新、上屏串更新、光标位置更改等)
case WM_IME_ENDCOMPOSITION: // 输入结束
case WM_IME_NOTIFY: // 这个消息应根据 wParam 的值做如下处理
switch (wParam)
{
case IMN_OPENCANDIDATE: // 打开候选列表
case IMN_CHANGECANDIDATE: // 更新候选列表
case IMN_CLOSECANDIDATE: // 关闭候选列表
}
break;
}
1.2 在 WM_IME_COMPOSITION 处获取显示编码串信息, 关键代码如下:
case WM_IME_COMPOSITION:
{
LONG lRet;
HIMC hIMC;
if(hIMC = ImmGetContext(hWnd))
{
TCHAR szCompStr[256];
DWORD dwCursorPos;
//获取显示字符串
if ( lParam & GCS_COMPSTR )
{
lRet = ImmGetCompositionString( hIMC, GCS_COMPSTR, szCompStr, ARRAYSIZE( szCompStr ) ) / sizeof(TCHAR);
szCompStr[lRet] = 0;
}
//获取显示字符串的属性标记
if ( lParam & GCS_COMPSTR )
{
lRet = ImmGetCompositionString( hIMC, GCS_COMPATTR, szCompStr, ARRAYSIZE( szCompStr ) ) / sizeof(TCHAR);
szCompStr[lRet] = 0;
}
//获取显示字符串的光标位置
dwCursorPos = ImmGetCompositionString(hIMC, GCS_CURSORPOS, NULL, 0);
//获取上屏字符串
if ( lParam & GCS_RESULTSTR )
{
lRet = ImmGetCompositionString( hIMC, GCS_RESULTSTR, szCompStr, ARRAYSIZE( szCompStr ) ) / sizeof(TCHAR);
szCompStr[lRet] = 0;
}
ImmReleaseContext(hWnd, hIMC);
}
}
1.3 应在 CHANGECANDIDATE 处获取候选列表信息, 关键代码如下:
case WM_IME_NOTIFY:
switch (wParam)
{
case IMN_OPENCANDIDATE: // 打开候选列表
case IMN_CHANGECANDIDATE: // 更新候选列表
{
HIMC hIMC;
if (hIMC = ImmGetContext(hWnd))
{
LPCANDIDATELIST lpCandList = NULL;
DWORD dwIndex = 0;
DWORD dwBufLen = ImmGetCandidateList(hIMC, dwIndex, NULL, 0 );
if ( dwBufLen )
{
lpCandList = (LPCANDIDATELIST)GlobalAlloc(GPTR,dwBufLen);
dwBufLen = ImmGetCandidateList(hIMC, dwIndex, &lpCandList, dwBufLen );
}
if ( lpCandList )
{
DWORD dwSelection = lpCandList->dwSelection; //处于选中状态的候选序号
DWORD dwCount = lpCandList->dwCount; //当前页候选数
DWORD dwPageStart = lpCandList->dwPageStart; //当前页起始序号
DWORD dwPageSize = lpCandList->dwPageSize; //当前页容量
DWORD i;
for (i = 0; i < dwCount; i++)
{
LPTSTR lpCandiString = (LPTSTR)((DWORD)lpCandList + lpCandList->dwOffset[i]); //候选字符串
}
GlobalFree(lpCandList);
}
ImmReleaseContext(hWnd,hIMC);
}
}
}
2 TSF候选列表获取方式
TSF框架的候选列表的获取,要求输入法程序必须已经实现UILess模式。
输入法程序在候选列表发生改变时使用ITfUIElementMgr::BeginUIElement、ITfUIElementMgr::UpdateUIElement、ITfUIElementMgr::EndUIElement等三个接口函数通知TSF服务程序,TSF服务程序再将此消息转发给所有的UIElement Sink。这期间的过程十分复杂,而且大部分步骤都是由输入法程序和操作系统自己完成的。因此本文不做详细描述,仅对如何获取候选列表做详细说明。
2.1 实现一个Skin用于接收 BeginUIElement UpdateUIElement EndUIElement消息。
在微软DirectX的官方示例代码 \Samples\C++\DXUT\Optional\ImeUi.cpp 中,有一个类名称为 CTsfUiLessMode 可作为参考。下文中的主要代码也是复制于此源代码文件中。
在程序初始时调用 BOOL CTsfUiLessMode::SetupSinks() ,以创建ITfUIElementSink ,关键代码如下:
BOOL CTsfUiLessMode::SetupSinks()
{
// ITfThreadMgrEx is available on Vista or later.
HRESULT hr;
hr = CoCreateInstance(CLSID_TF_ThreadMgr,NULL, CLSCTX_INPROC_SERVER, __uuidof(ITfThreadMgrEx), (void**)&m_tm);
if (hr != S_OK)
{
return FALSE;
}
// ready to start interacting
TfClientId cid;// not used
if (FAILED(m_tm->ActivateEx(&cid, TF_TMAE_UIELEMENTENABLEDONLY)))
{
return FALSE;
}
// Setup sinks
BOOL bRc = FALSE;
m_TsfSink = new CUIElementSink();
if (m_TsfSink)
{
ITfSource *srcTm;
if (SUCCEEDED(hr = m_tm->QueryInterface(__uuidof(ITfSource), (void **)&srcTm)))
{
// Sink for reading window change
if (SUCCEEDED(hr = srcTm->AdviseSink(__uuidof(ITfUIElementSink), (ITfUIElementSink*)m_TsfSink, &m_dwUIElementSinkCookie)))
{
// Sink for input locale change
if (SUCCEEDED(hr = srcTm->AdviseSink(__uuidof(ITfInputProcessorProfileActivationSink), (ITfInputProcessorProfileActivationSink*)m_TsfSink, &m_dwAlpnSinkCookie)))
{
if (SetupCompartmentSinks())// Setup compartment sinks for the first time
{
bRc = TRUE;
}
}
}
srcTm->Release();
}
}
return bRc;
}
2.2 在 CUIElementSink::BeginUIElement 和 CUIElementSink::UpdateUIElement 中响应来自输入法的候选列表更新操作
在DX官方代码中,会首先尝试获取ITfReadingInformationUIElement接口,再尝试获取ITfCandidateListUIElement接口。但一般情况下, TSF输入法中会主要仅实现ITfCandidateListUIElement接口。
在BeginUIElement()或UpdateUIElement ()中获取ITfCandidateListUIElement接口,并使用此接口获取候选列表信息, 关键代码如下:
if (SUCCEEDED(pElement->QueryInterface(__uuidof(ITfCandidateListUIElement), (void **)&pcandidate)))
{
UINT uIndex = 0;
UINT uCount = 0;
UINT uCurrentPage = 0; //当前页序号
UINT *IndexList = NULL; //候选列表页索引
UINT uPageCnt = 0;
DWORD dwPageStart = 0;
DWORD dwPageSize = 0;
BSTR bstr;
pcandidate->GetSelection(&uIndex); //获取当前选中状态的候选序号(可设置高亮显示,一般为第一候选)
pcandidate->GetCount(&uCount); //当前候选列表总数
pcandidate->GetCurrentPage(&uCurrentPage); //当前候选列表所在的页
g_dwSelection = (DWORD)uIndex;
g_dwCount = (DWORD)uCount;
g_bCandList = true;
g_bReadingWindow = false;
pcandidate->GetPageIndex(NULL, 0, &uPageCnt); //获取候选列表页每一页对应的起始序号
if(uPageCnt > 0)
{
IndexList = (UINT *)ImeUiCallback_Malloc(sizeof(UINT)*uPageCnt);
if(IndexList)
{
pcandidate->GetPageIndex(IndexList, uPageCnt, &uPageCnt);
dwPageStart = IndexList[uCurrentPage];
dwPageSize = (uCurrentPage < uPageCnt-1) ?
min(uCount, IndexList[uCurrentPage+1]) - dwPageStart:
uCount - dwPageStart;
}
}
g_uCandPageSize = min(dwPageSize, MAX_CANDLIST);
g_dwSelection = g_dwSelection - dwPageStart;
memset(&g_szCandidate, 0, sizeof(g_szCandidate));
for (UINT i = dwPageStart, j = 0; (DWORD)i < g_dwCount && j < g_uCandPageSize; i++, j++)
{
if (SUCCEEDED(pcandidate->GetString( i, &bstr ))) //获取候选列表的第i个候选串
{
if(bstr)
{
#ifndef UNICODE
char szStr[COUNTOF(g_szCandidate[0])*2];
szStr[0] = 0;
int iRc = WideCharToMultiByte(CP_ACP, 0, bstr, -1, szStr, sizeof(szStr), NULL, NULL);
if (iRc >= sizeof(szStr))
{
szStr[sizeof(szStr)-1] = 0;
}
ComposeCandidateLine( j, szStr );
#else
ComposeCandidateLine( j, bstr );
#endif
// 应注意TSF框架要求输入内部必须使用SysAlloc() 分配候选列表字符串保存空间
//在调用pcandidate->GetString 之后,必须使用SysFreeString(bstr)释放
SysFreeString(bstr);
}
}
}
if (GETPRIMLANG() == LANG_KOREAN)
{
g_dwSelection = (DWORD)-1;
}
if(IndexList)
{
ImeUiCallback_Free(IndexList);
}
}
在获取候选列表字符串之前必须首先获取当前页序号(uCurrentPage)和候选列表页索引(PageIndex),由于微软一直没有公布TSF框架的详细说明文档, 大家都只能从微软的类成员命名中猜测候选列表页索引(PageIndex)的数据结构以及使用方式。所以pcandidate->GetPageIndex()引起很多程序编写者的误解。在此对处做详细说明:
1. 使用空指针参数调用pcandidate->GetPageIndex(NULL, 0, &uPageCnt);以获取当前候选列表页数, 候选列表页数会被写入到uPageCnt中
2. 初始化一个内存块用于保存候选列表每一页的起始序号,内存快大小应为页数*sizof(UINT) IndexList = (UINT *)ImeUiCallback_Malloc(sizeof(UINT)*uPageCnt);
3. 再次调用pcandidate->GetPageIndex(IndexList, uPageCnt, &uPageCnt);
4. 调用pcandidate->GetString( i, &bstr )) 获取候选列表字符串.此处应注意i的取值,在上面的代码中
如果调用成功,在IndexLis会保存后续列表每一页的序号。本文假设候选列表有16个候选,每页显示5个候选,函数执行完毕时, 各变量会被赋值如下:
uPageCnt = 4 //候选列表有4页
IndexList[0] = 0 //候选列表第一页的起始序号为0
IndexList[1] = 5 //候选列表第二页的起始序号为5
IndexList[2] = 10
IndexList[3] = 15
好了,总算写完了。做个小结吧,我写这篇文章的初衷是希望能够为游戏编写者自绘输入法候选列表提供一个指导性帮助。写文章的过程中有如下感受:
1. 微软总是在技术说明文档上省工减料
2. 很多软件设计者、开发者都不会为追究细节深入研究
3. 现在想找点技术文档真的是太难了