选自:《CEGUI深入解析》

13 中文输入

CEGUI可以显示中文,前文已经简单的介绍过。哪么如何在CEGUI中输入中文呢?计算机原生支持英文的输入,但要输入其他的文字则需要输入法IMEInput Method Editor)的支持。我们前文已经介绍过CEGUIString类其实保存的是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_INPUTLANGCHANGEWM_IME_STARTCOMPOSITIONWM_IME_ENDCOMPOSITIONWM_IME_NOTIFYWM_IME_COMPOSITION5个消息,因为这些消息没有传递给默认窗口过程因此并不会产生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;

       }

}