您的位置:首页 > 其它

让OGRE支持中文中文输入

2010-03-04 16:20 316 查看
让OGRE支持中文3
——中文输入
-1.前言
中文输入终于实现了,这并不是一个轻松的过程,足足用了几个月时间才搞定,真正负责中文输入处理的就是一个简单的类,不过为了实现这个类需要的接口对OGRE进行了大量的改动,在此期间读了相当多的OGRE源码,改写尽量保持OGRE自身的风格,零零碎碎改写的地方相当多,这篇文章难免有疏漏的地方,如果各位真的希望了解真正的改写,还是看看源码比较好,这篇文章能做一个源码导读。我相信大多数OGRE的使用者还是希望直接使用这个输入的,所以我给大家提供了一个例子和相应的代码,大家可以直接拿去用。

0.还是检讨
之前已经写了两篇关于OGRE引擎支持中文的文章,第一篇是位图字体,第二篇是TFF字体,关于位图字体,在字体管理器的管理下,同一字体之产生了一个实例,所以不会产生我在第二篇文章说的用过多字体实例导致内存被占用。在第二篇TFF字体的程序中,有几个严重错误。第一个是释放文字,在写的时候我曾认为每个实例维护一个贴图,事实上却是相同字体实例共同使用一个贴图,而且在正常使用的过程中,这个字体类型的实例也只能有一个,但是调用这个字体类的实例进行渲染的TextAreaGuiElement却有很多,把记录字体是否使用的代码改成引用记数才能正常工作,之前的代码会导致错误的释放。第二个问题是向
贴图画字的时机,如果在申请贴图时候才画字,在某些情况下会无法显示,改成在判断是否使用该字也就是调用引用记数时进行画字便解决了这个问题。第三个问题是在之前代码的顺序下,首先申请绘字空间然后判断是否在字库中有这个字,这样导致申请空间却没有画字的情况出现。在修改了顺序之后解决了这个问题。大体思路没有改变,只是进行了一些修改和优化,有兴趣的朋友可以直接看源码中的OgreFont.h、OgreFont.cpp、OgreTextAreaGuiElement.h,、OgreTextAreaGuiElement.cpp,这四个文件,并和之前的文件对比一下就能发现区别了。

1.捕捉Windows消息
在Windows环境下,如果要进行中文输入,捕捉Windows的祖字消息是必需的,但是OGRE为了其支持多平台的特性,从而拒绝将Windows消息交给用户处理,封装在相应的窗口渲染类中。

上图是处理窗口选的相关类的继承图,其中D3D7RenderWindow(DirectX7.0)、D3D9RenderWindow(DirectX9.0)、Win32Window(OpenGL)三个类是在Windows环境下处理窗口的相应类,Windows消息便封装在这三个类中,在我们所使用的抽象层RenderWindow类中已经无法得知具体的Windows消息,我们要做的就是在RenderWindow中建立一个可以监听Windows消息的接口,并且在D3D7RenderWindow(DirectX7.0)、D3D9RenderWindow(DirectX9.0)、Win32Window(OpenGL)三个类向上传递Windows消息,在Linux和Max环境中没有消息传递(这样的处理破坏了OGRE的多平台性,但是我所知道的中文输入处理只是针对Windows平台的,如果要保持多平台性应该做更多的处理,或者实现自己的输入法,不过这就是一个更大更大的工程了)。
参照OGRE的编码风格,我使用监听者模式,一个向要得知Windows消息的对象,需要具备(继承)Windows消息监听者的接口(RenderWindowListener),然后向RenderWindow实例注册自身,当有Windows消息出现时候RenderWindow实例得到windows消息,并向所有注册的实例发送消息。
我们首先实现两个基础结构和类。



struct RenderWindowEvent //这个结构用来封装windows消息
{
HWND hWnd;
UINT uMsg;
WPARAM wParam;
LPARAM lParam;
};

class _OgreExport RenderWindowListener //消息监听者的接口
{
public:
virtual bool RWUpdate(const RenderWindowEvent& evt) { return true;}
};

然后在RenderWindow增加
virtual void
addRWListener (RenderWindowListener* listener)//注册监听者

virtual void
removeAllRWListeners (void)//移除所有监听者

virtual void
removeRWListener (RenderWindowListener* listener)//移除某一监听者

virtual bool
RWUpdate (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
//系统得到消息时调用的函数,在这里向所有监听者发送消息

具体请参照本文提供的代码。

最后在Windows的回调函数中加入处理代码,这里拿DX9.0为例。在下面函数中
LRESULT D3D9RenderWindow::WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
增加
LPCREATESTRUCT lpcs;
D3D9RenderWindow* win=NULL;

// look up window instance
if( WM_CREATE != uMsg )
// Get window pointer
win = (D3D9RenderWindow*)GetWindowLong( hWnd, 0 );
// 以下三行是我们增加的
if(win)
if(!win->RWUpdate(hWnd,uMsg, wParam, lParam ))
return 0;

这样就把Windows消息导出来了,不用高兴,现在只是开胃菜而已。

2.对中文输入的预处理。
说到输入,就要有输入框,有光标。这些东西要到哪里搞呢。看看手上的资源TextBoxGuiElement类就是一个简单的输入框,不过也真够简单,只能支持英文不说,还没有光标,要做到一个类似我们在窗口中的输入框,还有很远的路要走。
我们要实现自己的TextInputGuiElement类,让其可以实现中文输入。这个类是一个TextBoxGuiElement类的子类,继承了TextBoxGuiElement类的接口,但是我们要进行大规模的手术才可以为我所用。(相关源文件清参考OgreTextInputGuiElement.h、OgreTextInputGuiElement.cpp)
首先是加入对全角字的预处理,因为如果进行中文输入的话,我们需要判定,现在所指的字节是半角字还是全角字,这样我们删除的时候才可以知道是删除一个字节还是两个字节。在这里我使用了一个
std::vector < bool > mCheck;
结构来辅助储存信息,
bool b = true;
for(i = text.begin();i != iend;++i) //检查整个字符串
{
mCheck.push_back(b);
if((unsigned char)*i >= CHINESE_FIRST) //如果是全角字
{
++i;
if(i == iend)
break;
mCheck.push_back(b); //下一位的bool和本位相等
}
b=!b;
}
上面text是String类型字符串,这种运行结果产生了一个判断全/半角的辅助结构,
比如“ab中国c人”这个字符串生成的辅助结构就是“true,false,true,true,false,false,ture,false,false”,当我们发现连续两个相等的布尔值时候,相应位置上的字符就是全角,否则就是半角。
然后我们来加入光标。
加入光标分两个步骤,第一是如何得到目前光标的位置,第二是如何显示光标。这些在WinAPI中轻而易举的事情要让我们从头做起了。
得到光标位置,我们就需要在渲染时候分析全/半角字,并储存渲染的位置,然后我们要实现这样两个查询函数,第一个是通过第几个字得到这个字渲染的位置,另外一个函数是通过坐标(鼠标点选)得到这个字的位置和这个字是字符串中的几个字。这些艰巨的任务我们交给TextAreaGuiElement类来完成。
我们首先加入这个数据
typedef std::vector<Rectangle> RectList;
RectList mRectList;

来保存相应文字的坐标,Rectangle结构是OGRE自身定义的有四个Real(float)数据定义的矩形。
然后又回到修改
void TextAreaGuiElement::updateGeometry()
这个函数上了,我们主要是在渲染字的时候把其坐标储存到Rectagle数据中,具体的代码请参看OgreTextAreaGuiElement.cpp文件中
RectList::iterator iR,iendR;
相关的部分。

然后是增加的两个函数。
第一个是从字数中得到坐标。
//get the rect form the num
00136 inline void getRect(unsigned int& num,Rectangle& rect)
00137 {
00138
00139 if(mRectList.empty())//如果没有字
00140 {
00141 num=0;
00142
00143 float left = _getDerivedLeft() * 2.0 - 1.0;//得到坐标
00144 float top = -( (_getDerivedTop() * 2.0 ) - 1.0 );
00145
00146 rect.left = left;
00147 rect.top = top;
00148 rect.bottom = top-mCharHeight * 2.0;
00149 rect.right = left;
00150
00151 return;
00152 }
00153
00154 RectList::iterator iR ,iendR;
00155
00156 //如果取一个新字符位置
00157 if(num >= mCaption.size())
00158 {
00159 iendR = mRectList.end();
00160 --iendR;//取最后一个矩形
00161 num = mCaption.size();
00162
00163 rect.left = iendR->right;
00164 rect.top = iendR->top;
00165 rect.bottom = iendR->bottom;
00166 rect.right = rect.left;
00167
00168 return;
00169 }
00170
00171 iR = mRectList.begin();
00172 iendR = mRectList.end();
00173 //寻找字符位置
00174
00175 for(unsigned int i = 0;i < num; ++i )
00176 {
00177
00178 if(unsigned char(mCaption.at(i)) >= CHINESE_FIRST) //如果是汉字
00179 ++i;
00180 ++iR;
00181 }
00182
00183
00184 if(i != num)//当目前字符为汉字的第二个字节时候产生的情况;
00185 num = i;
00186
00187
00188
00189 //当目前字符为汉字的第二个字节时
00190 //并且为最后一个字符候产生的情况;
00191 if(iR >= iendR)
00192 {
00193 --iendR;//取最后一个矩形
00194 num = mCaption.size();
00195
00196 rect.left = iendR->right;
00197 rect.top = iendR->top;
00198 rect.bottom = iendR->bottom;
00199 rect.right = rect.left;
00200
00201 return;
00202 }
00203
00204
00205
00206
00207 rect= *iR;
00208
00209 return;
00210 }

另外一个是通过坐标(鼠标点选)得到这个字的位置和这个字是字符串中的几个字。
inline unsigned int getNum(Real x, Real y ,Rectangle& rect)
00217 {
00218 x = x * 2.0 - 1.0;
00219 y = - ( ( y * 2.0 ) - 1.0);
00220 if(mRectList.empty())//如果没有字
00221 {
00222
00223 float left = _getDerivedLeft() * 2.0 - 1.0;//得到坐标
00224 float top = -( (_getDerivedTop() * 2.0 ) - 1.0 );
00225
00226 rect.left = left;
00227 rect.top = top;
00228 rect.bottom = top-mCharHeight * 2.0;
00229 rect.right = left;
00230
00231 return 0;
00232 }
00233
00234 RectList::iterator iR ,iendR;
00235
00236 iR = mRectList.begin();
00237 iendR = mRectList.end();
00238
00239
00240 unsigned int i=0;
00241
00242 for(;iR != iendR; ++iR,++i)
00243 {
00244 if(i >= mCaption.size())//未知错误
00245 i = mCaption.size() - 1;
00246
00247
00248 if(x >= iR->left && x <= iR->right && y >= iR->bottom && y <= iR->top)
00249 {
00250
00251 rect=*iR;
00252 return i;
00253 }
00254
00255 if(unsigned char(mCaption.at(i)) >= CHINESE_FIRST) //如果是汉字
00256 ++i;
00257
00258 }
00259 --iendR;//最后一个矩形
00260 rect.left = iendR->right;
00261 rect.top = iendR->top;
00262 rect.bottom = iendR->bottom;
00263 rect.right = rect.left;
00264
00265 return mCaption.size();
00266
00267 }

这样我们便有了光标的位置,但是我们更需要光标。

光标的显示。
因为我们构造的类是TextBoxGuiElement 的子类,而TextBoxGuiElement 又是GuiContainer的子类,所以我们可以向我们构造的类中添加Element元素,具体概念请参考Ogre源码和相关教程,我们这里只是按照Ogre的风格向我们的类中添加一个2D元素。
void
setCursorPanel (const String& templateName, int size)


String
getCursorPanelName () const

void
initCursorPanel ()

void
updateCursor ()


GuiContainer*
mCursorPanel


String
mCursorPanelTemplateName


CmdCursorPanel
msCmdCursorPanel


以上就是我们增加的函数和数据成员。CmdCursorPanel这个是我们为实现StringInterface实现的类,也便是为了实现脚本而作的,具体代码请参考OgreTextInputGuiElement.h和OgreTextInputGuiElement.cpp两个文件。
为了支持脚本系统,我们重载
void TextInputGuiElement::addBaseParameters(void)
并添加
dict->addParameter(ParameterDef("cursor_panel",
"The template name of the panel is the cursor in text."
, PT_STRING),
&msCmdCursorPanel);
这样我们就可以在脚本中定义光标的样式了。

然后是控制光标和相关操作。

鼠标点选,当我们鼠标点到某个文字上面时候,光标应该以东道这个文字上,并且者时候这个输入框处于激活状态,者时候可以进行键盘操作。
在事件处理函数
void TextInputGuiElement::processEvent(InputEvent* e)
中增加
case KeyEvent::KE_KEY_FOCUSOUT: // 失去焦点
if (mCursorPanel)
mCursorPanel->hide();//隐藏光标
break;
case KeyEvent::KE_KEY_FOCUSIN: //得到焦点
if (mCursorPanel)
{
initCursorPanel();//初始化光标
mCursorPanel->show();//显示光标
}
break;
case MouseEvent::ME_MOUSE_PRESSED: //鼠标点击事件
//通过鼠标位置得到 当前字数和当前光标位置
mNum = mTextArea->getNum(static_cast<MouseEvent*> (e)->getX(),static_cast<MouseEvent*> (e)->getY(),mRect);
setCaptionToTextArea();//更新
break;

键盘操纵光标
我们来给我们的输入框增加一些键盘操作,比如删除,橡皮(向后删除),左移动,右移动,home,end这些功能。
00688 bool TextInputGuiElement::key(int key)
00689 {
00690 switch(key)
00691 {
00692 case KC_LEFT: //左移动
00693
00694 if(mNum)
00695 if(mNum >= 2) //看看是否有空间移动全角
00696 { //判断是否全角
00697 if(mCheck.at(mNum-1)^mCheck.at(mNum-2))
00698 mNum -= 1;
00699 else
00700 mNum -= 2; //全角移动两个字节
00701 }
00702 else
00703 mNum -= 1; //半角一个
00704 setCaptionToTextArea();//更新
00705 return true;
00706
00707 case KC_RIGHT: //右移动
00708
00709 if(mNum < mCaption.size())
00710 if(mNum+2 <= mCaption.size())
00711 {
00712 if(mCheck.at(mNum)^mCheck.at(mNum+1))
00713 mNum += 1;
00714 else
00715 mNum += 2;
00716 }
00717 else
00718 mNum += 1;
00719 setCaptionToTextArea();
00720 return true;
00721
00722 case KC_DELETE: //删除
00723
00724 if(mNum < mCaption.size())
00725 if(mNum+2 <= mCaption.size())
00726 {
00727 if(mCheck.at(mNum)^mCheck.at(mNum+1))
00728 mCaption.erase(mNum,1);
00729 else
00730 mCaption.erase(mNum,2);
00731 }
00732 else
00733 mCaption.erase(mNum,1);
00734 setCaptionToTextArea();
00735 return true;
00736
00737 case KC_HOME: //移动到头
00738 mNum = 0;
00739 setCaptionToTextArea();
00740 return true;
00741
00742 case KC_END: //移动到尾
00743 mNum = -1; //因为mNum是非负,这里付给他一个最大值
00744 setCaptionToTextArea();
00745 return true;
00746
00747 case KC_BACK : //橡皮,向后删除
00748 if(mNum)
00749 if(mNum >= 2)
00750 {
00751 if(mCheck.at(mNum-1)^mCheck.at(mNum-2))
00752 {
00753 mNum -= 1;
00754 mCaption.erase(mNum,1);
00755 }
00756 else
00757 {
00758 mNum -= 2;
00759 mCaption.erase(mNum,2);
00760 }
00761 }
00762 else
00763 {
00764 mNum -= 1;
00765 mCaption.erase(mNum,1);
00766 }
00767 setCaptionToTextArea();
00768 return true;
00769
00770 case KC_RETURN :
00771
00772 if (mActionOnReturn)
00773 {
00774 ActionEvent* ae = new ActionEvent(this, ActionEvent::AE_ACTION_PERFORMED, 0, 0, mName);
00775 processActionEvent(ae);
00776 delete ae;
00777 }
00778 return true;
00779
00780 }
00781 return false;
00782
00783 }


这样我们也就可以调用键盘处理了,具体使用请参考相关文档和源文件OgreTextInputGuiElement.cpp。

3.中文输入
写了好久,现在才进入关键领域。

《一个中文输入法的类》,这是一篇在GameRes.com找到的教程,参照这个教程中的例子实现了Cime类,主要是把C风格字符串转换成String这样的工作之类。
具体实现请参照《一个中文输入法的类》这个教程和本文提供的源码中的Cime类,参看OgreTextInputGuiElement.h和OgreTextInputGuiElement.cpp。

然后我们让我们实现的TextInputGuiElement类分别继承RenderWindowListener和FrameListener。并注册监听windows消息和每一画面更新的消息。

下面是处理Windows消息的代码。
00503 bool TextInputGuiElement::RWUpdate(const RenderWindowEvent& evt)
00504 {
00505
00506 if(!mCursorPanel->isVisible())
00507 {
00508 if(evt.uMsg==IMN_CHANGECANDIDATE||evt.uMsg==WM_IME_COMPOSITION)
00509 return false;
00510 return true;
00511 }
00512 switch( evt.uMsg )
00513 {
00514 case WM_CHAR:
00515 if(evt.wParam==8)
00516 key(KC_BACK);
00517 else
00518 {
00519
00520
00521 if(mCTemp)
00522 {
00523 addChar( mCTemp , (char)evt.wParam);
00524 mCTemp=0;
00525 }
00526 else
00527 {
00528 if((unsigned char)evt.wParam>=(unsigned char)CHINESE_FIRST)
00529 {
00530
00531 mCTemp=(char)evt.wParam;
00532
00533 }
00534 else
00535 {
00536
00537 addChar((char)evt.wParam);
00538 }
00539
00540 }
00541 }
00542 break;
00543 case WM_KEYDOWN:
00544 switch(evt.wParam)
00545 {
00546
00547 case VK_LEFT:
00548 key(KC_LEFT);
00549 break;
00550 case VK_RIGHT:
00551 key(KC_RIGHT);
00552 break;
00553 case VK_HOME:
00554 key(KC_HOME);
00555 break;
00556 case VK_END:
00557 key(KC_END);
00558 break;
00559 case VK_DELETE:
00560 key(KC_DELETE);
00561 break;
00562 }
00563 case WM_IME_SETCONTEXT:
00564 mIme.enableIme();
00565 LogManager::getSingleton().logMessage("WM_IME_SETCONTEXT" );
00566 return !mIme.onWM_IME_SETCONTEXT();
00567
00568 break;
00569 case WM_INPUTLANGCHANGEREQUEST:
00570 LogManager::getSingleton().logMessage("WM_INPUTLANGCHANGEREQUEST" );
00571 return !mIme.onWM_INPUTLANGCHANGEREQUEST();
00572 break;
00573 case WM_INPUTLANGCHANGE:
00574 LogManager::getSingleton().logMessage("WM_INPUTLANGCHANGE" );
00575 return !mIme.onWM_INPUTLANGCHANGE( evt.hWnd );
00576 break;
00577 case WM_IME_STARTCOMPOSITION:
00578 LogManager::getSingleton().logMessage("WM_IME_STARTCOMPOSITION" );
00579 return !mIme.onWM_IME_STARTCOMPOSITION();
00580 break;
00581 case WM_IME_ENDCOMPOSITION:
00582 LogManager::getSingleton().logMessage("WM_IME_ENDCOMPOSITION" );
00583 return !mIme.onWM_IME_ENDCOMPOSITION();
00584 break;
00585 case WM_IME_NOTIFY:
00586 if(mChosePanel)
00587 switch(evt.wParam)
00588 {
00589 case IMN_OPENCANDIDATE:
00590 mChosePanel->show();
00591 break;
00592 case IMN_CLOSECANDIDATE:
00593 mChosePanel->hide();
00594 break;
00595
00596 }
00597
00598 LogManager::getSingleton().logMessage("WM_IME_NOTIFY" );
00599 return !mIme.onWM_IME_NOTIFY( evt.hWnd, evt.wParam );
00600 break;
00601 case WM_IME_COMPOSITION:
00602 LogManager::getSingleton().logMessage("WM_IME_COMPOSITION" );
00603 return !mIme.onWM_IME_COMPOSITION( evt.hWnd, evt.lParam );
00604 break;
00605 }
00606
00607 return true;
00608 }
上面主要是用来得到消息并调用Cime得到输入字串和相应组字信息的。


在每一画面渲染开始时候调用。
00651 bool TextInputGuiElement::frameStarted(const FrameEvent &evt)
00652 {
00653 String a,b,c,listString;
00654 char i=0;
00655 std::vector<String> list;
00656 c=mIme.getImeName();
00657 mIme.getImeInput(&b,&a,NULL,&list);
00658 c+=a+b;
00659 std::vector<String>::iterator it;
00660 for(it=list.begin();it!=list.end();++it)
00661 {
00662 i++;
00663 i=i%10;
00664 listString+=StringConverter::toString(i)+"."+*it+"/n";
00665
00666 }
00667
00668 if(!listString.empty())
00669 {
00670 if(mChoseArea)
00671 mChoseArea->setCaption(listString);
00672 }
00673
00674
00675 if(mCaption!=c)
00676 {
00677
00682 setCaptionToTextArea();
00683 }
00684 return true;
00685 }
以上是,用来显示文字和组字信息的,mChoseArea用来显示选择的字的信息的文本区域,增加方式和我们增加光标是一样的,在我写的这个函数里面,省略了显示组字信息和数书法名称,如果您的程序需要这些信息进行显示的话,请进行相应的改动。
4.脚本
因为我们的输入框是一个hud元素,也便可以通过通用的脚本来定义输入框的样式,在这里给大家一个简单的例子。
// Ogre overlay scripts
#include <BasicOgreGuiTemplates.inc>
//光标
template container Panel(SS/Templates/CursorPanel)
{
left 0
top 0
width 0.01
height 0.01
material Core/Edit

}
//文本区域
template element TextArea(SS/Templates/BasicSmallText)
{
font_name StarWars
char_height 0.04
colour_top 1 1 0
colour_bottom 1 0.2 0.2
left 0.03
top 0.02
width 0.12
height 0.04
}

//选字的背景
template container BorderPanel(SS/Templates/ChosePanel) : SS/Templates/BasicBorderPanel
{
left 0.1
top 0.1
width 0.6
height 0.8
material Core/StatsBlockCenter






}
//选字区域
template element TextArea(SS/Templates/ChoseArea)
{
font_name StarWars
char_height 0.04
colour_top 1 1 0
colour_bottom 1 0.2 0.2
left 0.03
top 0.02
width 0.12
height 0.04

}

SS/Setup/HostScreen/Overlay
{
zorder 490
//这里的就是我们的输入框的定义
container TextInput(SS/Setup/HostScreen/TextInput)
{
left 0.02
top 0.5
width 1.0
height 0.05
text_area SS/Templates/BasicSmallText test
back_panel SS/Templates/BasicSmallBorderPanel
cursor_panel SS/Templates/CursorPanel
chose_panel SS/Templates/ChosePanel
chose_area SS/Templates/ChoseArea ss
left 0.1
caption 请在这里输入
}

}

5.问题
在DX9窗口模式下切换过输入法,在退出是后会有非法退出,这样的话只能在Dx9全屏和OpenGL全屏/窗口中实现中文输入了。还有就是在使用这个的时候记得要键盘即时输入才可以,当键盘位缓冲时候,无法得到窗口消息。
原Ogre的GUI的例子已经被我篡改为中文输入的例子了。

6.结语
以上便是中文输入相关的主要部分,如果希望具体研究的请参看源码,虽然跌跌撞撞的OGRE中文支持三部曲基本上到此为止了,但是还是又改进的余地,比如unicode码的支持,还有本文是在ogre-win32-v0-14-0,最新版本应该是ogre-win32-v0-14-1,还有要更新的工作啊。

7.源码导航
被改写的文件
OgreFont.h
OgreFont.cpp
OgreRenderWindow.h
OgreRenderWindow.cpp
OgreTextAreaGuiElement.h
OgreTextAreaGuiElement.cpp
OgreD3D9RenderWindow.cpp
OgreD3D7RenderWindow..cpp
OgreWin32Window.cpp
OgreFontManager.cpp
新增加的文件
unicodemap.h
OgreTextInputGuiElement.h
OgreTextInputGuiElement.cpp
OgreRenderWindowListener.h


作者:免费打工仔
From:GameRes http://www.GameRes.com

资源档案下载:http://resource.gameres.com/OgreSupportCHN3.rar
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: