选自:《CEGUI深入解析》
第13章 中文输入
CEGUI可以显示中文,前文已经简单的介绍过。哪么如何在CEGUI中输入中文呢?计算机原生支持英文的输入,但要输入其他的文字则需要输入法IME(Input Method Editor)的支持。我们前文已经介绍过CEGUI的String类其实保存的是Unicode字符串。所以CEGUI其实是可以支持任何字符的显示的,只要有对应的字体支持就可以。既然CEGUI支持中文的显示,哪么其实输入中文已经支持了。只不过我们需要输入Unicode字符到CEGUI系统中而已。CEGUI注入的字符本来都是Unicode的字符。为什么英文字符可以直接注入也能显示呢?英文的本地编码(ASCII编码)字符与它在Unicode中的编码完全相同。所以我们注入ASCII编码的英文也可以正常的显示。但是我们的中文编码GBK等是多字节的编码,它和Unicode编码是不同的,所以就需要获取多字节编码的对应Unicode字符然后在注入到CEGUI中。当然还要有对应的中文字体支持才能正确的显示。我们的中文字体采用隶书字体,笔者是从Window系统的字体文件夹中拷贝出来的。
在介绍CEGUI中文输入之前我们首先简单介绍IME。以便读者可以理解Window是如何支持非英文的输入的。
13.1 IME简介
什么是IME呢?IME全称Input Method Editor,输入法编辑器的意思。其实就是读者熟悉的输入。现在比较流行的有搜狗输入法,紫光,五笔,智能ABC等。作者以前使用微软拼音输入法比较多,但最近发现搜狗输入法非常智能。输入常常只需要输入想输入的词的第一个字母就可以第一个输入这个词。
相信读者对输入法应该非常的熟悉了。中文的输入有几个步骤,首先启用一种支持中文的输入法。然后就可以通过这种输入法支持的输入方法通过输入英文或者数字来最终输入中文了。输入法涉及到2个窗口,一个是输入法窗口,它包含了输入的状态,名称,设置等等功能。另一个是单词选择窗口,当用户输入的拼音(这里以拼音编码的中文输入法为例)对应多个字或者词的时候需要有个字符选则窗口来供用户选择。
游戏里往往都需要输入法的编程,当然不是需要游戏自己编写一个输入法编辑器。而是一般游戏都需要自己实现选择文本的窗口,这样才能符合游戏的风格。这就要涉及到一小部分的IME的编程了。本节主要介绍,如果希望自己实现一个输入法的选择窗口需要做些什么。第14章还要实现一个输入法的选择窗口的控件,并且完整的实现一个输入法的界面。
13.1.1 输入法的Window消息介绍
输入法有很多消息,下面分别介绍。
消息WM_IME_CHAR,当输入法混合字符串完成后,就是说输入法已经确定用户已经输入一些字符的时候会发送这个消息给窗口。这个消息wParam参数包含的输入字符的编码,如果窗口是Unicode窗口则发送的是Unicode编码的字符,如果不是则发送的是本地代码页的字符编码。(中文就是GBK编码)。这个消息和WM_CHAR消息比较类似,如果窗口不是Unicode窗口默认处理函数DefWindowProc会将这个字符的两个字节分成两个字符生成
两个WM_CHAR消息,每个消息对应字符的一个字节。
消息WM_IME_COMPOSITION,就是我们说的混合字符的消息。这个消息的lParam参数值代表许多种状态。其中比较重要的有GCS_RESULTSTR,表示一次输入已经完成,可以获取结果字符串了,这时候可以调用Imm函数来获取这个字符串。GCS_CURSORPOS代表当前输入法光标的位置。GCS_COMPSTR代表目前用户输入的混合字符串。比如输入
"ni"希望得到"你"这里的ni就是GCS_COMPSTR状态可以获取的字符。而GCS_RESULTSTR状态获取的字符则是"你"。
消息WM_IME_ENDCOMPOSITION会在一次混合结束后发送的,可以用来确定隐藏混合字符的窗口。
消息WM_IME_NOTIFY会在IME的各种状态改变的时候发送,比如输入法的全角和半角状态,进入选字状态,退出选字状态,关闭选字表等。它的wParam参数的值代表着这些不同的状态。
消息WM_IME_STARTCOMPOSITION会在混合开始的时候被发送。收到这个消息时可以准备显示选词窗口了。
这里只介绍了一部分我们会用到的IME消息,其他用不到的我们就不在介绍,有兴趣的读者可以详细阅读MSDN的相关部分。
还有一个不是IME消息,但它和IME有关,这个消息就是WM_INPUTLANGCHANGE输入法改变的消息。要处理这个消息获取新的输入法的信息,并且需要阻止默认窗口过程处理这个函数。
如果窗口不处理这些消息,Window默认窗口过程DefWindowProc函数会处理他们。他会负责调用输入法的默认选字窗口,显示用户的选字并最终生成一些WM_IME_CHAR消息。我们要做的就是截获其中一些消息,然后获取其中的选字列表和当前的输入编码(比如前面说过的"ni")。
总结,我们需要处理的消息有WM_INPUTLANGCHANGE,WM_IME_STARTCOMPOSITION,WM_IME_ENDCOMPOSITION,WM_IME_NOTIFY和WM_IME_COMPOSITION这5个消息,因为这些消息没有传递给默认窗口过程因此并不会产生WM_IME_CHAR消息。如果用户不需要实现选字窗口,可以通过截获WM_CHAR或者WM_IME_CHAR消息,然后找到对应的Unicode编码并注入到CEGUI系统也可以实现中文输入的支持。但似乎这两个函数并不好实现,更好的方法是截获WM_IME_COMPOSITION消息,然后获取最终的混合后的Unicode编码的字符串,然后一个字符一个字符的注入到CEGUI系统中。典型的代码如下所示:
static wchar_t buf[1024];
if (lParam & GCS_RESULTSTR)
{
//获取IME句柄
HIMC hIMC = ImmGetContext(g_mainWnd);
//获取Unicode结果字符串的长度,这个长度怎么也不会比1024还长
LONG buflen = ImmGetCompositionStringW(hIMC,GCS_RESULTSTR,NULL,0);
//重置字符串长度的缓冲区为0,否则会出现先前的字符
memset(buf,0, buflen*sizeof(wchar_t));
//获取Unicode结果字符串
ImmGetCompositionStringW(hIMC,GCS_RESULTSTR,buf,buflen);
//逐个字符注入到CEGUI系统中
for (int i=0; i<buflen; i++)
{
System::getSingleton().injectChar((CEGUI::utf32)buf[i]);
}
//释放IME句柄
ImmReleaseContext(g_mainWnd, hIMC);
}
上面的代码通过两个Unicode函数就获得了Unicode的字符串。然后逐个注入到系统中。
下一节详细介绍需要用到的输入法相关的函数。
13.1.2 输入法函数介绍
在截获了IME的消息后可以通过一些以Imm开头的函数来获取当前窗口的输入法的各种信息。
Ø ImmGetContext函数,这个函数获取指定窗口的IME的句柄(或者叫环境)。其他的Imm函数都需要通过这个环境来获取输入法在当前窗口的信息。
Ø GetKeyboardLayout函数,获取某个线程当前的激活的键盘布局。传递0作为参数是获取当前线程激活的句柄。
Ø ActivateKeyboardLayout函数,激活某一个键盘布局作为当前线程的键盘布局。
Ø ImmIsIME函数,判断一个键盘布局是不是具有输入法。
Ø ImmEscape函数,获取输入法的各种信息,可以通过这个函数获取输入法的名称。
Ø ImmGetConversionStatus函数,获取混合的各种字符串。
Ø ImmReleaseContext函数,释放指定的IME环境。与ImmGetContext配对使用。
Ø ImmGetConversionStatus函数,获取某个输入法的一些状态信息,比如半角,全角信息。
Ø ImmGetCandidateList函数,获取混合的字符列表。
我们用到的函数大概就有这些。这些函数的详细使用方法读者可以查阅MSDN或者查看我们的源代码。
这些函数的使用一般是在截获对应的消息后,调用他们获取或者设置输入法的字符串或者状态等。
13.2 CEGUI中文输入支持
前文已经说过,如果只是希望支持CEGUI的中文输入其实非常简单,只要在游戏窗口过程中加入第13.1.1节中指定的代码就可以实现。(本章就是暂时这样实现的)但是我们希望自己定义一个选词窗口,因此必须做一些其他的处理工作。
要显示选词列表最重要的是获取选词列表,然后显示它。另外一般选词窗口需要显示当前的输入名称以及各种种状态。本章代码中定义了一个新类IME,它提供了对选词列表的
支持。下面我们着重介绍这个类,首先还是介绍它的成员变量。
wchar_t d_imeName[64]; //输入法的名称
BOOL d_bAlphaNumeric; //英文模式
BOOL d_bSharp; //全角标志
BOOL d_bSymbol; //中文标点标志;
BYTE* d_bufCandidate ; //选字缓冲
DWORD d_candidateLength; //缓冲区大小
LONG d_cursorPos; //当前光标的位置
wchar_t *d_pResultStr; //结果字符串缓冲
LONG d_ResultStrBuflen; //这个缓冲的长度
wchar_t *d_pCompStr; //编码字符串缓冲
LONG d_CompStrBuflen; //这个缓冲的大小
HWND d_hWnd; //对应的窗口
std::vector<STRING> d_candidateArray; //选词列表
本节的字符串变量都是wchar_t类型的,就是说他们保存的都是Unicode数据,这是为了方便注入到CEGUI而设计的。选字缓冲是一个结构体,所以这里设计成了BYTE类型的,但在获取选词列表的时候做了wchar_t类型的转化。STRING类定义为std::wstring它保存的是Unicode字符。
成员函数有两个比较重要,他们是MessageProc用来处理游戏窗口过程中消息处理,另一个其实也是处理消息的只不过它处理的消息WM_IME_NOTIFY,这个消息的处理比较复杂所以单独写了一个函数OnNotify函数来处理。下面着重介绍这两个函数。MessageProc函数我们按照每个消息的处理单独讲解。第一个消息输入法改变的处理。
case WM_INPUTLANGCHANGE:
{
//获取当前激活的键盘布局
HKL hKL = GetKeyboardLayout( 0 );
//当前激活的键盘布局是否有输入法窗口
if(ImmIsIME(hKL))
{
//获取环境
HIMC hIMC = ImmGetContext(d_hWnd);
//获取输入法的名称
if(!ImmEscapeW(hKL,hIMC,IME_ESC_IME_NAME,d_imeName))
{
//出错的话激活下一个键盘布局
ActivateKeyboardLayout((HKL)HKL_NEXT,0);
break;
}
//更新各个输入法内部状态
DWORD dwConversion, dwSentence;
ImmGetConversionStatus(hIMC,&dwConversion,&dwSentence);
if(dwConversion & IME_CMODE_NATIVE) d_bAlphaNumeric = FALSE;
else 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);
}
else
{
d_imeName[0] = d_imeName[1] =0;
}
//激发输入法改变的事件
onInputMethodChange();
}
break;
下面两个消息,只是简单的激发了对应的事件。
case WM_IME_STARTCOMPOSITION:
onCompositionBegin();
break;
case WM_IME_ENDCOMPOSITION:
onCompositionEnd();
break;
使用这个类的其他模块可能希望截获这两个事件,因此我们提供了响应函数。
消息WM_IME_COMPOSITION包含许多子状态,我们分别介绍他们,下面是这个消息处理的公共部分获取当前游戏窗口的输入法环境。
case WM_IME_COMPOSITION:
{
HIMC hIMC = ImmGetContext(d_hWnd);
...
ImmReleaseContext(d_hWnd,hIMC);
onCompositionStr((LONG)lParam);
}
break;
第一个子状态,获取输入的编码的字符串获取。使用Unicode格式的函数获取的结果就是Unicode的字符串结果。
if(lParam & GCS_COMPSTR)
{
LONG buflen = ImmGetCompositionStringW(hIMC,GCS_COMPSTR,NULL,0);
if(buflen > 0)
{
buflen += sizeof(WCHAR);
if(buflen > d_CompStrBuflen)
{
if(d_pCompStr)
{
delete d_pCompStr;
}
d_pCompStr = new wchar_t [buflen];
d_CompStrBuflen = buflen;
}
memset(d_pCompStr,0,buflen);
ImmGetCompositionStringW(hIMC,GCS_COMPSTR,d_pCompStr,buflen);
}
else if(d_pCompStr)
{
d_pCompStr[0] = d_pCompStr[1]=0;
}
}
第二个子状态,获取结果字符串的状态。同理这个函数获取的也是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章介绍的一样这里就不在介绍了。
14.2 选词控件的渲染
这个控件由于使用了三个子窗口来实现功能,所以它的渲染窗口实现非常简单,只是负责描绘背景。代码如下。
void FalgardIMEShowWindow::render()
{
IMEShowWindow* w = (IMEShowWindow*)d_window;
const WidgetLookFeel& wlf = getLookNFeel();
wlf.getImagerySection("background").render(*w, 0);
}
读者可能非常奇怪,为什么就这么简单的描绘背景,就会描绘出这么复杂的选词窗口呢。因为我们这里只是负责渲染自己,子窗口的渲染在他们自己的对应渲染窗口中进行。他们的已经被CEGUI实现了,他们是比较复杂的。有兴趣的读者可以自己阅读相关的代码。
下面简单介绍选词控件的外观定义。(第14.1节已经介绍了一部分)。
我们的控件使用了属性的链接的定义。由于我们的需要通过设置控件的属性就能够设置子窗口的属性,所以我们需要定义属性链接。定义如下。
<PropertyLinkDefinition name="IMENameColors" widget="__auto_imename__" targetProperty="TextColours" initialValue="tl:FFFFFF00 tr:FFFFFF00 bl:FF00FF00 br:FF00FF00" redrawOnWrite="true" />
<PropertyLinkDefinition name="InputNameColors" widget="__auto_inputcode__" targetProperty="TextColours" initialValue="tl:FFFFFF00 tr:FFFFFF00 bl:FF00FFFF br:FF00FF00" redrawOnWrite="true" />
这是两个颜色的属性链接。控件中定义了一个名为IMENameColors的属性,它指向子窗口后缀名为"__auto_imename__"的子窗口中的TextColours属性。也就是说如果操作了控件的IMENameColors属性实际上是在操作这个子窗口中的TextColours属性。而这个属性正式子窗口文本的颜色矩形。我们说过CEGUI支持文本的过渡色。只要设置控件的这个属性就可以实现不同的过渡色。读者可以自己试验。tl表示左上,t表示top,l表示left,其他类似。指定redrawOnWrite为真,当这个值被修改后重绘控件。另外一个属性和我们介绍的非常类似,这里就不在介绍了。
背景图片的外观的定义是从外观Vanilla/Shared中的BackgroundOnly图像段中拷贝过来的。它定义了一个图像的框架作为背景。这个框架支持各向的拉伸操作。因此非常时候我们的控件,因为我们需要随时的调整控件的大小。
14.3 使用选词控件
控件设计好了,哪么如何使用他呢。我们说过选词控件需要响应第13章实现的IME消息。把IME类获取的各种文本添加到控件中。
首先,我们要定义IME的实例,并且处理各种IME消息。我们定义了一个名为g_IME的全局变量,并且在Windows窗口过程中调用这个实例的MessageProc函数来处理IME消息。定义和消息处理如下所示。
//全局变量的声明
IME g_IME;
//处理消息,如果处理的话就返回,不在被其他函数处理
if (g_IME.MessageProc(hwnd, uMsg, wParam, lParam))
{
return 0;
}
其次,创建我们的选词控件,我们在程序里创建。代码如下。
//创建一个选词窗口
Window* pIME = winMgr.createWindow("Vanilla/ImeWindow", "IMEShow");
if (pIME)
{
//设置其实位置,其实这一步也没什么用,控件或自己调整位置和大小
pIME->setArea(UVector2(UDim(0, 150), UDim(1, -400)),
UVector2(UDim(0,200), Dim(0,260)));
//添加到背景窗口
background->addChildWindow(pIME);
//默认隐藏窗口
pIME->hide();
//保存到全局变量中,在IME类中使用
g_IMEShower = (IMEShowWindow*)pIME;
}
再次,我们需要修改IME类,响应IME类提供的各种函数来控制选词窗口。
当开始文本混合的时候我们显示选词窗口。
void IME::onCompositionBegin()
{
if (g_IMEShower == NULL)
{
return;
}
//显示窗口
g_IMEShower->show();
//获取当前的激活窗口
CEGUI::Window* pWnd = CEGUI::System::getSingleton().getGUISheet()-> getActiveChild();
//设置这个窗口为跟踪窗口
g_IMEShower->TraceWindow(pWnd);
}
当混合结束后我们隐藏选词窗口。
void IME::onCompositionEnd()
{
if (g_IMEShower == NULL)
{
return;
}
g_IMEShower->hide();
}
当在混合过程中的时候,我们响应各种操作。
void IME::onCompositionStr(LONG flag)
{
if (g_IMEShower == NULL)
{
return;
}
//如果混合结束后,我们注入结果字符串到CEGUI中
if (flag & GCS_RESULTSTR)
{
for (int i=0; i<wcslen(d_pResultStr); i++)
{
CEGUI::System::getSingleton().injectChar(d_pResultStr[i]);
}
}
//如果混合编码变化我们修改混合编码
if (flag&GCS_COMPSTR )
{
if (wcslen(d_pCompStr) <= 0)
{
return;
}
g_IMEShower->setInputCode(ucs_to_utf8(d_pCompStr));
}
//暂不处理这个消息
if (flag&GCS_CURSORPOS )
{
}
获取激活的窗口
CEGUI::Window* pWnd = CEGUI::System::getSingleton().getGUISheet()-> getActiveChild();
//追踪这个窗口
g_IMEShower->TraceWindow(pWnd);
}
当输入法的状态发生变化的时候。修改输入法的名称和全角半角状态。
void IME::onIMEStatusChanged()
{
if (g_IMEShower == NULL)
{
return;
}
STRING str = d_imeName ;
str += L"(" ;
str += (d_bSharp ? L"全角" : L"半角") ;
str += L")";
g_IMEShower->setInputName(ucs_to_utf8(str.c_str()));
g_IMEShower->hide();
}
当选词列表发生变化的时候。
void IME::onCandidateListChanged()
{
if (g_IMEShower == NULL)
{
return;
}
//先清空列表
g_IMEShower->clearWordList();
//然后获取选词窗口的文本,然后注入到CEGUI系统中
for (int i=0; i<d_candidateArray.size(); i++)
{
g_IMEShower->addWord(ucs_to_utf8(d_candidateArray[i].c_str()));
}
}
读者可能不明白ucs_to_utf8函数的作用。它将Unicode的字符串转化为utf8的字符串。我们说过String类接受的Unicode编码格式是UTF8格式。原始的Unicode编码不直接支持。读者可以写一个支持Unicode格式的构造函数,哪么这里就不需要这一步转化了。转化函数如下所示。
const CEGUI::utf8* ucs_to_utf8(const wchar_t* pucs)
{
static char tmpbuf[1024];
memset(tmpbuf, 0, sizeof(tmpbuf));
WideCharToMultiByte(CP_UTF8, 0, pucs, (int)wcslen(pucs), tmpbuf, 1023,0,0);
return (CEGUI::utf8*)tmpbuf;
}
这个函数调用了Window提供的转化函数将Unicode编码转化为UTF8编码。静态缓冲tmpbuf有1024个字节一般来说不可能超过它,所以不必检查缓冲区的溢出。本书的很多代码都是为例子程序而写,可能不是很注重效率以及安全性。读者在使用的时候需要自己考虑。
14.4 本章小结
本章介绍了选词控件的实现,并且介绍了如何使用这个控件。结合第13章介绍的输入法的支持,本章完整的实现了一个输入法的选词控件。读者可以打开例子程序体验,感觉一定很不错哦。