前言
该系列文章将记录我在学习CEGUI(0.6.0)时碰到的一些问题和得出的心得体会,适合没有任何CEGUI基础的朋友看。我会不定期的将自己的学习整理成笔记,同时由于我是本月刚接触CEGUI,也是正在学习的阶段,所以热烈欢迎各路朋友和我探讨、交流、指教。最终目的是想在基于OGRE图像库的项目中应用CEGUI,希望我这些文字对你有所帮助。
CEGUI学习笔记一
CEGUI 自带例子,所以我的学习切入点就从这些例子开始。
如果你还没有成功编译CEGUI自带的例子,请仔细阅读《vs2005环境下编译CEGUI 0.6.0 》的第4部分。
另外提一句,Lua已经升级到5.1.3了```
OK,现在我假设你对编译例子时各个文档是干什么用的有一定了解了。并能根据自己机器上的SDK来选择你想要的渲染层模块。
实际上,Common就是把渲染层和CEGUI控件分成了两件事,让你能专心关注CEGUI的实际使用,而不是过多的关系如何实现一个CEGUI用的渲染层。
关于渲染层的问题,将在未来与OGRE合并的时候来讨论,现在我们只需要关注使用CEGUI的套路就好。
首先来看看CEGUISaple_FirstWindow这个例子
class FirstWindowSample : public CEGuiSample
... {
public:
// method to initialse the samples windows and events.
// 初始化函数
bool initialiseSample();
// method to perform any required cleanup operations.
// 清理函数
void cleanupSample(void);
} ;
这里没什么特别的,去看CPP文件吧
#include " Sample_FirstWindow.h "
#include " CEGUI.h "
int main( int argc, char * argv[])
... {
// 所有例子的函数入口点都是 run(),暂时不要关注CEGUI框架下的事件(消息)是如何传递和被处理的,
// 作为使用者,我们往应用角度看。
FirstWindowSample app;
return app.run();
}
/**/ /*************************************************************************
Sample specific initialisation goes here.
*************************************************************************/
bool FirstWindowSample::initialiseSample()
... {
// 命名空间,凡是需要使用CEGUI的函数,都应该声明这个命名空间。
using namespace CEGUI;
// 加载图片集
// 你一定会问什么是图片集,对于这些需要详细解释的问题,我会在展示完代码之后一起讨论。
Imageset* taharezImages = ImagesetManager::getSingleton().createImageset("TaharezLook.imageset");
// 设置鼠标图标,如果不设置鼠标图标,就看不见鼠标了....
System::getSingleton().setDefaultMouseCursor(&taharezImages->getImage("MouseArrow"));
// 设置字体。必须设置字体,否则将没办法显示文字。
FontManager::getSingleton().createFont("Commonwealth-10.font");
// 设置视感
WidgetLookManager::getSingleton().parseLookNFeelSpecification("TaharezLook.looknfeel");
// 设置大纲,或者叫风格吧。CEGUI使用falagard皮肤系统,
// 不过不只是简单的换一个表面,而是可以更换布局的级别。
SchemeManager::getSingleton().loadScheme("TaharezLookWidgets.scheme");
// 获取CEGUI的窗口管理器对象(设计模式-单件,
// 它提供了更加安全的访问机制,暂时就当获取全局变量好了。)
WindowManager& winMgr = WindowManager::getSingleton();
// 创建一个默认样式的窗口。默认样式不是通过大纲文件配置的,而是CEGUI系统默认的,因此它总是可用。
DefaultWindow* root = (DefaultWindow*)winMgr.createWindow("DefaultWindow", "Root");
// 还是单件,你可以当作(System::getSingleton().)是一个System类型的全局变量,我们调用它的
// 成员方法来设置GUISheet 为我们刚才创建的 名为 Root的窗口root。
System::getSingleton().setGUISheet(root);
// 创建FrameWindow。所谓FrameWindow就是一个带有标题栏和工作区的窗口。
FrameWindow* wnd = (FrameWindow*)winMgr.createWindow("TaharezLook/FrameWindow", "Demo Window");
// 现在将创建的FrameWindow和rootWindow关联起来,这样才能显示出来。
root->addChildWindow(wnd);
// 一组设置窗口坐标、大小、拉伸上下限的操作。
wnd->setPosition(UVector2(cegui_reldim(0.25f), cegui_reldim( 0.25f)));
wnd->setSize(UVector2(cegui_reldim(0.5f), cegui_reldim( 0.5f)));
wnd->setMaxSize(UVector2(cegui_reldim(1.0f), cegui_reldim( 1.0f)));
wnd->setMinSize(UVector2(cegui_reldim(0.1f), cegui_reldim( 0.1f)));
// 设置窗体的标题栏
wnd->setText("Hello World!");
// 返回true,这样CEGUI就知道初始化成功了,否则CEGUI就要抛出异常。
return true;
}
// 专门释放资源的地方,在这个例子里,什么都不干。
void FirstWindowSample::cleanupSample()
... {
// nothing to do here!
}
1、Imageset、looknfell、scheme、layout都是些什么?
他们都是CEGUI的窗体样式配置文件。你可以参考powerkoria的专栏,关于falagard皮肤系统的翻译手稿(2篇)。http://blog.csdn.net/powerkoria/archive/2008/03/31/2232283.aspx。
Imageset
Imageset是CEGUI 用来绘制窗口控件的图片集坐标。如果你不知道我在说什么,
首先下载CEImagesetEditor-0.6.0,
然后自己打开一个imageset文件自己看看吧。
注意设置相应的tag文件的路径。
再用VS2005打开imageset文件,明白了吧?
looknfell
looknfell 就是我们写控件的地方了,这个文件描述了控件长什么样子,包括大小,颜色,子控件的布局等等。
scheme
该文件说老实话以我现在对CEGUI的了解,还不知道如何描述它是什么。去我刚才给出的连接看看吧,别说是我盗连的哦。
layout
记录窗体布局的文件,可以通过该文件直接创建一个带各种控件的完整窗口。该文件可以用CELayoutEditor来生成。自己用的看看吧。
2、关于 imageset,默认字体,looknfell这些设置,实际上都可以在scheme文件里设置,而不需要显式的在程序中写代码来做。
仔细比较WindowsLook.scheme和WindowsLookWidgets.scheme这两个文件。
在开头部分,WindowsLook.scheme 多出两行。
< LookNFeel Filename ="WindowsLook.looknfeel" />
在这里我使用WindowsLook样式来举例子,是因为我将CPP文件里关于样式的读取文件全部替换成了WindowsLook风格。这个没有什么困难的,你可以自己动手做做看。
唯一需要注意的就是WindowsLookWidgets.scheme缺少systemButton的声明,
自己从WindowsLook.scheme中拷过去就好了。
OK,那如何直接使用WindowsLook.scheme把全部的配置都给做了呢?显然在这个例子里是没有范例代码的。
不过先别急,在完全阅读了FirstWindow这个例子,并成功的显示出不同于Taharez风格的窗口之前,我在这等你。
很好~我们现在都已经看到了WindowsLook风格的窗口,就和想象中的一样呆头呆脑。
现在再来看看第2个例子FalagardDemo1。
class DemoConsole
... {
public:
DemoConsole(const CEGUI::String& id_name, CEGUI::Window* parent = 0);
~DemoConsole();
void toggleVisibility();
bool isVisible() const;
private:
// these must match the IDs assigned in the layout
static const unsigned int SubmitButtonID;
static const unsigned int EntryBoxID;
static const unsigned int HistoryID;
// 回调函数,用于响应事件时的调用。
bool handleSubmit(const CEGUI::EventArgs& args);
bool handleKeyDown(const CEGUI::EventArgs& args);
CEGUI::Window* d_root; // 窗口对象,一定要注意对它的初始化。
int d_historyPos;
std::vector<CEGUI::String> d_history;
} ;
class FalagardDemo1Sample : public CEGuiSample
... {
public:
// 初始化函数,跟上个例子稍微不一样,注意它直接加载了Imageset,font,和looknfeel。
bool initialiseSample();
// 清除函数
void cleanupSample(void);
protected:
// 这也是一个回调函数。
bool handleRootKeyDown(const CEGUI::EventArgs& args);
// 前面声明的类的指针, 在初始化的时候会用来指向我们定制的窗口。
DemoConsole* d_console;
} ;
解释事件机制是一个很麻烦的事情,我不知道能否将它讲述清楚,尽力而为。
讲抽象的东西 我不在行,就以鼠标点击事件来作为例子吧。
当你初始化CEGUI的时候,会在输入层的系统(比如OIS或者DXInput之类)上注册监听,表示如果发生什么事情的话,告诉它一声。
这个时候人点击鼠标左键,鼠标驱动捕获该动作(发生MouseLeftDown事件),向所有监听该事件的系统发送这个事件。
CEGUI得到了通知,现在它就开始处理这个事件。
首先从事件发生的坐标开始找起,看鼠标当前悬停在哪个窗口之上。
然后看看这个窗口有没有 注册处理这个事件 的回调函数。
如果有,则调用它。
如果没有,则按照转发规则逐层转发,直到有人处理它或者 忽略本次事件。
这个规则一般来说 是向父级传递,比如如果Button没有处理点击事件,那该事件就交给Button的父级窗口来处理,在VCL里就象在Button的 OnMouseLeftDown里 向button->parent Send Message一样。
也有不向上级转发的事件,比如 MouseLeftUp或者别的什么。这个需要深入研究CEGUI源代码。
但是实际上事件处理和消息机制还是有些不同的,因为这个时候父级对象不知道该事件的通知,CEGUI系统只是按照它的规则在查找与该事件相关的对象上是否有处理该事件的函数。(我不确定有没发生事件转发通知....)
那么CEGUI如何进行查找的呢?
任何对象都有一个Event列表,每一个Event后面都对应着一个回调函数队列。如:
对象EVENT列表结构:
event1: callbak 1, callbak 2, callbak3
event2: callbak 1, callbak 2, callbak3
event3: callbak 1, callbak 2, callbak3
event4: callbak 1, callbak 2, callbak3
(你可以想象一个 Map,以事件名字符串为Key,以回调函数列表List为值 的双层容器)
首先我们写一个函数,
然后将该函数注册到Button事件列表中WindowEvent_OnMouseLeftDown这个事件所对应的回调函数列表中。
当在这个Button上发生了OnMouseLeftDown,CEGUI就会调用该Button上所有挂在这个消息上的回调函数。
OK,首先请记住上面这个注册回调流程,现在我们将在CPP文件中看到 在CEGUI中,如何在控件上注册回调函数,从而使某个事件发生时,可以执行我们想要的逻辑。
我不是很习惯摘代码来描述问题....但是为了说明这个流程,我还是先摘一下把。
首先我们写一个函数:
... {
using namespace CEGUI;
// get the text entry editbox
Editbox* editbox = static_cast<Editbox*>(d_root->getChild(EntryBoxID));
// get text out of the editbox
String edit_text(editbox->getText());
// if the string is not empty
if (!edit_text.empty())
...{
// add this entry to the command history buffer
d_history.push_back(edit_text);
// reset history position
d_historyPos = d_history.size();
// append newline to this entry
edit_text += ' ';
// get history window
MultiLineEditbox* history = static_cast<MultiLineEditbox*>(d_root->getChild(HistoryID));
// append new text to history output
history->setText(history->getText() + edit_text);
// scroll to bottom of history output
history->setCaratIndex(static_cast<size_t>(-1));
// erase text in text entry box.
editbox->setText("");
}
// re-activate the text entry box
editbox->activate();
return true;
}
然后将这个函数注册到 某个控件的事件监听列表。
以下代码分别在 一个按钮的 EventClicked事件 和 一个Editbox 的EventTextAccepted事件 上注册了刚才我们写的那个函数。
这样的话,你在这个函数里写的东西,就会在发生相对应的事件时被调用。
subscribeEvent(PushButton::EventClicked, Event::Subscriber( & DemoConsole::handleSubmit, this ));
d_root -> getChild(EntryBoxID) ->
subscribeEvent(Editbox::EventTextAccepted, Event::Subscriber( & DemoConsole::handleSubmit, this ));
这个就是注册事件监听函数的套路了。使用这个机制很简单:写函数→注册到事件,但是至于为什么要这么写....一时半会我说不清楚....这个问题等我再次对本文进行修改的时候在这个地方补充进来.....首先就这样用着吧。
好了,现在我们知道如何通过控件响应事件。再回到 窗体布局 和样式配置的问题上来。
我想说的就是:大家看着改吧,没什么特别的。
最后留个作业:自己用LayoutEditor 搞个有editbox 和 button 的窗口,然后显示它。
并且当Button按下时,用editbox里的文字 来设置窗口的标题。