实现CEGUI中文汉字输入法光标跟随(C/C++源码)

来源:互联网 发布:山东大学网络教育 编辑:程序博客网 时间:2024/06/05 14:59

作者:庄晓立(liigo)

日期:2011年7月20日

原创链接:http://blog.csdn.net/liigo/article/details/6621104

转载请注明出处:http://blog.csdn.net/liigo


  最新0.7.5版本的CEGUI是直接支持中文输入的,只要正常设置中文字体就行了。不过也仅限于“支持”、“够用”而已,有不足之处:在窗口模式(非全屏)下,汉字输入提示框显示在窗口左下角,不支持光标跟随功能,使用不方便;在全屏模式下,更是连汉字输入提示框都不显示了,只能盲打输入汉字。本文主要解决CEGUI中文汉字输入法“光标跟随”功能中最核心的地方,获取CEGUI编辑框(Editbox, MultiLineEditbox)中当前光标(Caret)的屏幕坐标。

  目前CEGUI0.7.5自身并不支持返回光标位置。两年前有人在CEGUI官方论坛提问如何得到编辑框光标位置,CEGUI老大CrazyEddie亲自回复给出了详细的办法,不过他的办法只是针对“多行编辑框MultiLineEditbox”(对单行编辑框Editbox无效),而且没有考虑多行编辑框有纵向滚动条的情况。

我(liigo)的解决方案是在研究和修改CEGUI源代码之后得到的。


  先说多行编辑框MultiLineEditbox的情况吧,相对简单点。思路就是CrazyEddie老大的思路:通过光标序号索引(MultiLineEditbox::getCaratIndex())得到光标所在行号(MultiLineEditbox::getLineNumberFromIndex),然后想办法得到最顶行的行号,两行号之差乘以行高(getFont()->getLineSpacing()),得纵向高度值Y;根据各文本行信息(MultiLineEditbox::getFormattedLines()),得到光标所在行中光标之前的文本,计算其横向宽度值X;多行编辑框左上角的屏幕坐标很容易取得,加上前面的X、Y值,就得到了光标的屏幕坐标。其中,“取最顶行行号”的办法是我(liigo)从CEGUI源码里翻出来的代码:

//取第一个可见行索引, 代码来自 FalagardMultiLineEditbox::cacheTextLines()int topLineNo = static_cast<size_t>(pMultiLine->getVertScrollbar()->getScrollPosition() / pMultiLine->getFont()->getLineSpacing());

下面是多行编辑框(CEGUI::MultiLineEditbox)取光标位置的核心代码:

if(activeWindow->testClassName("MultiLineEditbox")){CEGUI::MultiLineEditbox* pMultiLine = static_cast<CEGUI::MultiLineEditbox*>(activeWindow);CEGUI::Rect textRenderArea = CEGUI::CoordConverter::windowToScreen(*pMultiLine, pMultiLine->getTextRenderArea());x = textRenderArea.d_left;y = textRenderArea.d_top;int lineNo = pMultiLine->getLineNumberFromIndex(pMultiLine->getCaratIndex());int topLineNo = static_cast<size_t>(pMultiLine->getVertScrollbar()->getScrollPosition() / pMultiLine->getFont()->getLineSpacing()); //取第一个可见行索引, 代码来自 FalagardMultiLineEditbox::cacheTextLines()y += (lineNo - topLineNo) * (pMultiLine->getFont()->getLineSpacing());const CEGUI::MultiLineEditbox::LineList& lineList = pMultiLine->getFormattedLines();CEGUI::String lineBeforeCaret = pMultiLine->getTextVisual().substr(lineList[lineNo].d_startIdx, pMultiLine->getCaratIndex() - lineList[lineNo].d_startIdx);x += pMultiLine->getFont()->getTextExtent(lineBeforeCaret);}

  由于多行编辑框的滚动条是以像素为单位滚动,而不是以行为单位滚动,所以上面的代码计算出的纵坐标y会有一些偏差,但最多偏差一行文本的高度,问题应该不大。如果要想完美解决,恐怕还得修改CEGUI源码才行。


  单行编辑框(Editbox)的情况更复杂。在文本长度大于编辑框宽度时,编辑框的显示出来的文本,可能仅仅是文本中间的某一段,第一个可见字符是哪一个是不知道的(至少无法利用现有的接口获取)。必须通过修改CEGUI源码,提取相关信息。我(liigo)通过研究CEGUI源码,发现FalagardEditbox类里的私有变量d_lastTextOffset是一个很有用的数据,只要把它公开出来,就足够我们计算光标横向位置了。光标纵向位置比较容易计算,因为文本总是纵向居中显示的。最后加上编辑框左上角的屏幕坐标,就得到编辑框光标的屏幕坐标了。不过我(liigo)发现CEGUI::CoordConverter::windowToScreen()似乎有BUG,要想办法绕过才行。另外,在编辑框的密码输入模式下,其中显示的全是*号而不是真正的文本,应该特殊处理。

下面是单行编辑框(CEGUI::Editbox)取光标位置的核心代码:

if(activeWindow->testClassName("Editbox")){CEGUI::Editbox * pEditbox = static_cast<CEGUI::Editbox*>(activeWindow);CEGUI::String textBeforeCaret;if(pEditbox->isTextMasked())textBeforeCaret = CEGUI::String(pEditbox->getCaratIndex(), pEditbox->getMaskCodePoint());elsetextBeforeCaret = pEditbox->getTextVisual().substr(0, pEditbox->getCaratIndex());CEGUI::Rect screenArea = CEGUI::CoordConverter::windowToScreen(*pEditbox->getParent(), pEditbox->getArea());//下面这个判断用于临时绕过CoordConverter::windowToScreen的一个BUG//详见: http://www.cegui.org.uk/phpBB2/viewtopic.php?f=10&t=5728if(pEditbox->getParent()->testClassName("FrameWindow")){CEGUI::FrameWindow* pFrame = (CEGUI::FrameWindow*) pEditbox->getParent();screenArea.offset(CEGUI::Point(8, 35)); //横向加上FrameWindow边框宽度,纵向加上FrameWindow标题栏的高度}x = screenArea.d_left;x += pEditbox->getFont()->getTextExtent(textBeforeCaret);x += pEditbox->getTextOffset(); //需修改CEGUI源码y = screenArea.d_top;y += (screenArea.getHeight() - pEditbox->getFont()->getFontHeight()) / 2; //文字垂直居中显示}

需要修改CEGUI源码之处,说明如下:

/*//Need modify CEGUI's source code://CEGUIEditbox.h, class EditboxWindowRenderer, add a virtua method, //and overrides it in FalEditbox.h, class FalagardEditbox: //    float getTextOffset() const { return d_lastTextOffset; }//and add the same name method to class CEGUI::Editbox, just like Editbox::getTextIndexFromPosition()需修改CEGUI源码文件CEGUIEditbox.h里面:class EditboxWindowRenderer加一个虚函数:    virtual float getTextOffset() const = 0;class Editbox加一个成员函数定义:    float getTextOffset() const;文件FalEditbox.h里面:class FalagardEditbox加一个函数:    virtual float getTextOffset() const override { return d_lastTextOffset; }文件CEGUIEditbox.cpp:float Editbox::getTextOffset() const{    if (d_windowRenderer != 0)    {        EditboxWindowRenderer* wr = (EditboxWindowRenderer*)d_windowRenderer;        return wr->getTextOffset();    }    else    {        CEGUI_THROW(InvalidRequestException("Editbox::getTextOffset: "            "This function must be implemented by the window renderer"));    }}*/

  前面代码中出现的变量 activeWindow,是getGUISheet()->getActiveChild()返回的当前激活的控件。但是还要考虑特殊情况,在鼠标点击多行编辑框(MultiEditbox)纵向滚动条的情况下,当前拥有焦点(focus)的仍是多行编辑框,光标(caret)也仍在多行编辑框内,可是,但是,可但是,activeWindow已经不是多行编辑框了,它成了滚动条控件或滚动条的子控件(视鼠标点击位置而定)。所以我们的代码要处理这种情况:

CEGUI::Window* activeWindow = CEGUI::System::getSingleton().getGUISheet()->getActiveChild();if(activeWindow == NULL) return FALSE;//处理activeWindow是多行编辑框的滚动条控件,或滚动条子控件的情况if(activeWindow->testClassName("Scrollbar")){activeWindow = activeWindow->getParent();}else{//Thumb or PushButton of ScrollbarCEGUI::Window* parentWindow = activeWindow->getParent();if(parentWindow && parentWindow->testClassName("Scrollbar"))activeWindow = parentWindow->getParent();}if(activeWindow == NULL) return FALSE;

  好了,有了光标的屏幕坐标,再把编辑框内文字的高度(getFont()->getFontHeight())考虑进去,定位输入法提示框是没什么难度了。至于输入法提示框内要显示的内容(输入码、候选字词),则超出了本文范畴,那属于IME编程领域,网上有很多公开的资料可供参考。

还有其它一些额外内容,我(liigo)也简单提一提,只盼对读者有用。

//注册"窗口创建"事件处理函数CEGUI::WindowManager::getSingleton().subscribeEvent(CEGUI::WindowManager::EventWindowCreated, _internal_OnCreateWindow);//处理编辑框创建事件, 注册其"焦点得失"事件处理函数static bool _internal_OnCreateWindow(const CEGUI::EventArgs& args){const CEGUI::WindowEventArgs& winEventArgs = static_cast<const CEGUI::WindowEventArgs&>(args);//对于编辑框(Editbox,MultiLineEditbox),注册焦点得失事件处理函数,以便关联输入法if(winEventArgs.window->testClassName("Editbox") || winEventArgs.window->testClassName("MultiLineEditbox")){winEventArgs.window->subscribeEvent(CEGUI::Window::EventActivated, _internal_OnEditboxSetFocus);winEventArgs.window->subscribeEvent(CEGUI::Window::EventDeactivated, _internal_OnEditboxKillFocus);}return true;}

全文完。谢谢。liigo 2011/7/20 夜,于大连。

原创粉丝点击