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

作者:庄晓立(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());
	else
		textBeforeCaret = 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=5728
	if(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 Scrollbar
	CEGUI::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 夜,于大连。

你可能感兴趣的:(String,function,null,Class,输入法,float)