第二个子状态,获取结果字符串的状态。同理这个函数获取的也是Unicode字符串。
if(lParam & GCS_RESULTSTR)
{
LONG buflen = ImmGetCompositionStringW(hIMC,GCS_RESULTSTR,NULL,0);
if(buflen > 0)
{
buflen += sizeof(wchar_t);
if(buflen > d_ResultStrBuflen)
{
if(d_pResultStr)
{
delete[] d_pResultStr;
}
d_pResultStr = new wchar_t [buflen];
d_ResultStrBuflen = buflen;
}
memset(d_pResultStr,0, buflen);
ImmGetCompositionStringW(hIMC,GCS_RESULTSTR,d_pResultStr,buflen);
}
}
第三个状态,获取当前光标的位置。
if(lParam & GCS_CURSORPOS)
{
d_cursorPos = ImmGetCompositionStringW(hIMC,GCS_CURSORPOS,NULL,0);
}
下面介绍消息WM_IME_NOTIFY这个消息的处理比较复杂我们专门提供了一个函数来处理。
case WM_IME_NOTIFY:
{
return OnNotify(wParam,lParam);
}
break;
下面介绍OnNotify函数。它获取选择文本,修改输入法的状态。这个函数的介绍也分为两部分,一部分是状态改变,另一部分是选词。首先介绍OnNotify的结构,主要功能都省略了,下面单独介绍。
BOOL IME::OnNotify(WPARAM wParam, LPARAM lParam)
{
switch(wParam)
{
//全角/半角,中/英文标点改变
case IMN_SETCONVERSIONMODE:
break;
//进入选字状态
case IMN_OPENCANDIDATE:
//选字表翻页
case IMN_CHANGECANDIDATE:
break;
//关闭选字表,清理选词的向量
case IMN_CLOSECANDIDATE:
{
ClearCandidateString();
}
break;
case IMN_PRIVATE:
{
if(lParam == 193) return FALSE;
}
break;
}
return TRUE;
}
下面这段代码获取当前输入法的各种状态。
case IMN_SETCONVERSIONMODE:
{
HIMC hIMC = ImmGetContext(d_hWnd);
DWORD dwConversion, dwSentence;
ImmGetConversionStatus(hIMC,&dwConversion,&dwSentence);
//英文模式的标志
if(dwConversion == IME_CMODE_ALPHANUMERIC)
{
d_bAlphaNumeric = TRUE;
}
//全角半角标志
if(dwConversion & IME_CMODE_FULLSHAPE)
{
d_bSharp = TRUE;
}
else d_bSharp = FALSE;
//中文标点标志
if(dwConversion & IME_CMODE_SYMBOL)
{
d_bSymbol = TRUE;
}
else d_bSymbol = FALSE;
ImmReleaseContext(d_hWnd,hIMC);
//激发状态改变的消息
onIMEStatusChanged();
}
break;
下面这段代码获取选词列表。
case IMN_OPENCANDIDATE:
case IMN_CHANGECANDIDATE:
{
//清理上次的选词列表
ClearCandidateString();
HIMC hIMC = ImmGetContext(d_hWnd);
//获取需要的缓冲区大小
DWORD buflen = ImmGetCandidateListW(hIMC,0,NULL,0) * sizeof(wchar_t);
if(buflen)
{
//修正缓冲区到合适的大小
if(buflen > d_candidateLength)
{
if(d_bufCandidate)
{
delete[] d_bufCandidate;
}
d_bufCandidate = new BYTE [buflen];
d_candidateLength = buflen;
}
//获取缓冲区的数据 ImmGetCandidateListW(hIMC,0,(LPCANDIDATELIST)d_bufCandidate,buflen);
CANDIDATELIST *pList = (CANDIDATELIST*)d_bufCandidate;
STRING wstr = L"";
//循环生成每个选词行
for(DWORD i = 0; (i < pList->dwCount - pList->dwSelection) && (i < pList-> dwPageSize); i ++)
{
wstr = L"";
if(i >9) wstr = L"\0";
//数字0
else if(i == 9) wstr = (wchar_t)(0x31);
//只是数字1到9的Unicode编码生成
else wstr = (wchar_t)(0x31 + i);
wstr += L":";
//这一句非常重要,注意指针的转换
wstr += (wchar_t*)((char*)pList+pList->dwOffset[pList->dwSelection+i]);
//添加到选词列表中
d_candidateArray.push_back(wstr);
}
}
ImmReleaseContext(d_hWnd,hIMC);
//激发选词列表改变的事件
onCandidateListChanged();
}
break;
选词列表的获取根据MSDN介绍CANDIDATELIST 结构的各个成员的含义编写。选词列表的每行词的格式是行号:这一行的词。比如0:你好。
有关Window的输入法消息的函数就介绍完了,这个类还有一些以on开头的事件函数,这些函数目前什么都不干,他们是为使用这个类提供的事件函数。使用者可以在这些函数中做一些处理来更新选词窗口的状态和选词列表等。这些工作将在下一章进行。
13.3 本章小结
第14章 IME选词控件
在游戏中经常可以看到输入法的选词界面。我想读者应该非常想知道如何实现它。这一章我们结合第13章实现一个输入法选词控件。他的行为和普通的输入法提供的界面非常类似。
第13章实现了输入法的各种事件的处理并激发对应的输入法事件。我们这一章主要是响应这些事件,并将对应的内容输入到我们这一章实现的选词控件中。本章先介绍输入法控件的实现,然后介绍如何使用这个控件。读者可以先运行这章的例子,以便更好的学习本章。
14.1 选词控件
选词控件由三部分构成,第一部分是读者输入的编码,第二部分是选词列表,第三部分是输入法的信息。第一和第三部分使用CEGUI的静态文本来实现。第二部分使用CEGUI的ListBox控件来实现。本书所有的例子都是基于Vanilla外观的,它定义在Vanilla.looknfeel和VanillaSkin.scheme等文件中。
这三部分的控件以子窗口的形式定义在外观文件中,在控件中需要获取这些子窗口,并且调用这些子窗口的函数来实现控件的功能。我们介绍一个子窗口的外观定义。本书例子的外观定义都在example.looknfeel文件中,本章控件的外观名称是Vanilla/IMEWindow。下面是外观中定义的第一部分的子窗口。
<Child type="Vanilla/StaticText" nameSuffix="__auto_inputcode__">
<Area>
<Dim type="LeftEdge"><AbsoluteDim value="5" /></Dim>
<Dim type="TopEdge"><AbsoluteDim value="3" /></Dim>
<Dim type="Width"><UnifiedDim scale="1" offset="-10" type="Width" /></Dim>
<Dim type="Height"><AbsoluteDim value="25" /></Dim>
</Area>
<VertFormatProperty name="VertLabelFormatting" />
<HorzFormatProperty name="HorzLabelFormatting" />
<Property name="Font" value="FZYT" />
</Child>
这个子窗口类型是静态文本框,名称后缀是__auto_inputcode__,位置在父窗口的左边5像素,上边3像素,宽度是父窗口的宽度减去10像素(两边边框各5像素),高度是25像素。指定的字体是方正姚体,这个字体是本章新加的一个字体。增加字体的方法读者应该知道了,如果还不知道,请参考datafiles/fonts目录下的字体文件定义,以及datafiles/schemes中VanillaSkin.scheme的定义。其他两个子窗口的外观定义类似。
哪么如何获取外观中定义的子窗口的指针呢?(获取指针后就可以操作这个控件了)在CEGUI中获取窗口的方法是通过窗口管理器通过窗口的名称来获取这个窗口。获取子窗口的函数必须在Window基类提供的一个虚函数initialiseComponents中。
d_wordList = (Listbox*)WindowManager::getSingleton().getWindow(getName() + "__auto_woldlist__");
d_inputCodeWindow = WindowManager::getSingleton().getWindow(getName() + "__auto_inputcode__");
d_imeNameWindow = WindowManager::getSingleton().getWindow(getName() + "__auto_imename__");
这段代码分别获取了第二部分,第一部分以及第三部分的子窗口。这个获取的顺序是无关紧要的。除了定义子窗口外观中还定义了几个属性,我们这里获取控件关心的三个属性,他们分别是选词ListBox控件的文本颜色,控件的默认宽度,控件的边框宽度。边框宽度是外观中计算出来的,这里获取后在控件中会用到。
d_wordTextColor = PropertyHelper::stringToColour(getProperty("WordListColor"));
d_maxWidth = PropertyHelper::stringToFloat(getProperty("DefaultWidth"));
d_borderWidth = PropertyHelper::stringToFloat(getProperty("BorderWidth"));
下面分别介绍控件提供的函数。对于玩家输入的英文编码和输入法的名称以及状态只需提供设置和获取函数就可以了。
//设置输入法名称
void IMEShowWindow::setInputName(const String& name)
{
//保存在控件的变量中
d_inputName = name;
//设置对应子窗口的文本
d_imeNameWindow->setText(name);
//计算设置文本的宽度,如果宽度大于目前的宽度则调整控件的宽度
if(d_imeNameWindow->getFont())
{
//这里通过字体获取设置文本的宽度,然后加上控件的边框宽度
float w = d_imeNameWindow->getFont()->getTextExtent(name) + d_borderWidth;
//如果这个值大于目前最大的控件宽度则设置控件宽度为这个值
if (w > d_maxWidth)
{
d_maxWidth = w;
setWidth(cegui_absdim(d_maxWidth));
}
}
//文本改变了请求重绘
requestRedraw();
}
//设置输入的英文编码字符,处理类似setInputName
void IMEShowWindow::setInputCode(const String& code)
{
d_inputCode = code;
d_inputCodeWindow->setText(code);
if(d_inputCodeWindow->getFont())
{
float w = d_inputCodeWindow->getFont()->getTextExtent(code) + d_borderWidth;
if (w > d_maxWidth)
{
d_maxWidth = w;
setWidth(cegui_absdim(d_maxWidth));
}
}
requestRedraw();
}
选词窗口的文本清除和设置。清除使用Listbox函数的清除函数,添加一个新词将创建一个文本ListBoxItem然后添加到Listbox子控件中。
//清除所有的列表子项
void IMEShowWindow::clearWordList()
{
d_wordList->resetList();
requestRedraw();
}
//添加一个列表项
void IMEShowWindow::addWord(const String& word)
{
//创建一个文本子项
ListboxTextItem* pItem = new ListboxTextItem(word);
//设置文本子项的文本颜色
pItem->setTextColours(d_wordTextColor);
//添加到列表框中
d_wordList->addItem(pItem);
//计算文本宽度,并适时调整控件的宽度
if(pItem->getFont())
{
float w = pItem->getFont()->getTextExtent(word) + d_borderWidth;
if (w > d_maxWidth)
{
d_maxWidth = w;
setWidth(cegui_absdim(d_maxWidth));
}
}
//请求重绘
requestRedraw();
}
我们的控件会根据文本的宽度自己调整控件的宽度以适用窗口的宽度。除了这项功能外,选词窗口还需要根据输入框的位置来确定自己的位置,它的基本原理是获取屏幕区域,控件自己的区域以及目标输入框的区域,然后计算合适的区域保证整个控件都会被显示在屏幕上。下面介绍位置的控制函数。
void IMEShowWindow::TraceWindow(Window* inputWindow)
{
//如果传入的窗口是输入框
if (inputWindow && inputWindow->testClassName("Editbox"))
{
d_traceWindow = inputWindow;
//获取输入框在屏幕坐标系下的区域
Rect rect = inputWindow->getPixelRect();
//获取屏幕的区域大小
Rect scrRect = System::getSingleton().getRenderer()->getRect();
//获取控件自己在屏幕坐标系下的区域,这个区域没有经过父窗口的裁剪
Rect selfRect = getUnclippedPixelRect();
Rect resultRect ;
//首先计算输入框左边加上控件的宽度,在加上控件留下的空白(10)
resultRect.d_left = rect.d_left + selfRect.getWidth() + 10;
//其次计算输入框的高度减去控件的高度
resultRect.d_top = rect.d_top - selfRect.getHeight();
//如果紧靠着输入框无法完全的显示控件则控件向输入法的左边移动
if (resultRect.d_left >= scrRect.d_right)
{
resultRect.d_left = scrRect.d_right - selfRect.getWidth() -10;
}
//否则计算真正的输入法的控件位置
else
{
resultRect.d_left = rect.d_left -10;
}
//如果控件无法在在输入框的上边完全显示则显示在输入框的下边
if (resultRect.d_top < 0)
{
resultRect.d_top = rect.d_bottom;
}
//设置最终控件的高度和宽度
resultRect.setWidth(selfRect.getWidth());
resultRect.setHeight(selfRect.getHeight());
//设置控件的区域,这个控件的父窗口占据这个屏幕,所以这个控件使用屏幕坐标
setArea(URect(cegui_absdim(resultRect.d_left), cegui_absdim(resultRect.d_top), cegui_absdim(resultRect.d_right), cegui_absdim(resultRect.d_bottom)));
}
}
这个函数的算法比较复杂,读者细细体会应该会明白。我们希望在输入法下次显示的时候,控件的宽度是默认的宽度。因为上次的输入过程可能导致控件的宽度过宽。所以我们设置输入法在显示的时候为默认宽度。在显示的时候CEGUI窗口基类会调用一个名叫onShown的虚函数,我们重载它实现我们的功能。
void IMEShowWindow::onShown(WindowEventArgs& e)
{
//首先基类处理
Window::onShown(e);
//然后获取默认宽度值作为最大宽度
d_maxWidth = PropertyHelper::stringToFloat(getProperty("DefaultWidth"));
//最后设置控件的宽度为默认宽度
setWidth(cegui_absdim(d_maxWidth));
}
读者可能对cegui_absdim宏不太熟悉,它设置一个绝对值作为一个UDim变量。还有一个类似的是cegui_reldim宏,它设置一个相对值作为UDim量。
控件的注册以及导出和第12章介绍的一样这里就不在介绍了。