标签:gui
作者:i_dovelemon
日期:2014 / 12 / 18
来源:CSDN
主题:GUI
今天,博主学习了第五个教程。这个教程讲解了如何使用Irrlicht内置的一个基础模块,GUI模块,来开发一些GUI程序。作为Irrlicht的重要基础模块,博主有必要对此进行一些代码跟踪和深入的了解。详细的教程过程请看官网。
以前在使用Ogre的时候,它的程序运行之初都会问你需要使用的是哪一种API。那么在Irrlicht引擎中是否有同样的功能了?答案是肯定,在Irrlicht的driverChoice.h这个头文件中包含了一个函数,driverChoiceConsole(),这个函数就会打开一个控制台窗口,显示出可以选择的设备程序,如下图所示:
这个控制台窗口列举了很多可以使用的API程序,用户只要选择相应的API即可。博主安装的是Direct3D 9.0c所有选择了b。
在使用这个功能的时候,读者需要注意,只要当我们的程序类型是控制台类型的时候,这个函数才会起作用,否则这个函数不会弹出窗口,并且只会返回一个EDT_COUNT表示没有有效的API。这一点,只要我们在程序的开头加上这样的宜居宏命令即可:
#ifdef _IRR_WINDOWS_ #pragma comment(lib,"irrlicht.lib") #pragma comment(linker,"/subsystem:console /ENTRY:mainCRTStartup") #endif
在这个教程中,使用了前面教程4中介绍的事件处理机制。教程中创建了一个事件接收器,并且处理了GUIEvent。还记得的博主前面的文章中讲述过,在Irrlicht中的事件大致的分为6种类型,分别是GUI事件,Mouse事件,Keyboard事件,Joystick事件,Log事件以及用户自定义的事件。这6个基础事件最终导致了SEvent这种结构体,使用了共用体技术,将这6中基本类型都包含在了SEvent这个结构中了。前面我们只是对这个结构体有大概的了解,并且博主说过,等到以后遇到某种消息的时候,我们在详细的来讲解对应这种消息类型的明确结构。
在教程5中,使用的事件类型是GUI事件。也就是说,现在SEvent里面包含的是一个SGUIEvent的结构。我们打开GUIEvent结构的定义,如下所示:
//! Any kind of GUI event. struct SGUIEvent { //! IGUIElement who called the event gui::IGUIElement* Caller; //! If the event has something to do with another element, it will be held here. gui::IGUIElement* Element; //! Type of GUI Event gui::EGUI_EVENT_TYPE EventType; };
成员: IGUIElement* Caller -- 发生GUI事件的GUI元素对象
成员: IGUIElement* Element -- 与发生GUI事件的GUI元素可能发生交互的另外一个GUI元素
成员: EGUI_EVENT_TYPE EventType -- 确切的GUI事件的类型。
在Irrlicht引擎中,将事件分为了6大基础的事件类型,所有当某个事件发生的时候,事件接受器接受到一个SEvent结构,这个结构中的EventType表示的就是这个事件是6大基础事件类型中的哪一种,然后具体的事件类型在交由具体的事件结构体来保存。比如这里的基础事件类型是EET_GUI_EVENT,然后具体的事件类型保存在SGUIEvent结构体里面的EventType中了。
在博主编写这个教程程序的时候,对这个教程里面的GUISkin的概念不是很理解。所以,有必要探究一下这个GUISkin到底是个什么东东。那么,最容易的方法先看下官方文档对这个GUISkin的概括性解释,我将描述摘录下来:
“A skin modifies the look of the GUI elements.”
官网上的描述就是这么的简单粗暴,这个描述只是告诉了我Skin和元素的外表有关系,但这么简单的描述并不能满足我。所以,我们直接到GUISkin的定义出去看源代码,套用侯捷大师的一句话,“源码面前,了无秘密“,以下就是IGUISkin的定义:
//! A skin modifies the look of the GUI elements. class IGUISkin : public virtual io::IAttributeExchangingObject { public: //! returns default color virtual video::SColor getColor(EGUI_DEFAULT_COLOR color) const = 0; //! sets a default color virtual void setColor(EGUI_DEFAULT_COLOR which, video::SColor newColor) = 0; //! returns size for the given size type virtual s32 getSize(EGUI_DEFAULT_SIZE size) const = 0; //! Returns a default text. /** For example for Message box button captions: "OK", "Cancel", "Yes", "No" and so on. */ virtual const wchar_t* getDefaultText(EGUI_DEFAULT_TEXT text) const = 0; //! Sets a default text. /** For example for Message box button captions: "OK", "Cancel", "Yes", "No" and so on. */ virtual void setDefaultText(EGUI_DEFAULT_TEXT which, const wchar_t* newText) = 0; //! sets a default size virtual void setSize(EGUI_DEFAULT_SIZE which, s32 size) = 0; //! returns the default font virtual IGUIFont* getFont(EGUI_DEFAULT_FONT which=EGDF_DEFAULT) const = 0; //! sets a default font virtual void setFont(IGUIFont* font, EGUI_DEFAULT_FONT which=EGDF_DEFAULT) = 0; //! returns the sprite bank virtual IGUISpriteBank* getSpriteBank() const = 0; //! sets the sprite bank virtual void setSpriteBank(IGUISpriteBank* bank) = 0; //! Returns a default icon /** Returns the sprite index within the sprite bank */ virtual u32 getIcon(EGUI_DEFAULT_ICON icon) const = 0; //! Sets a default icon /** Sets the sprite index used for drawing icons like arrows, close buttons and ticks in checkboxes \param icon: Enum specifying which icon to change \param index: The sprite index used to draw this icon */ virtual void setIcon(EGUI_DEFAULT_ICON icon, u32 index) = 0; //! draws a standard 3d button pane /** Used for drawing for example buttons in normal state. It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param rect: Defining area where to draw. \param clip: Clip area. */ virtual void draw3DButtonPaneStandard(IGUIElement* element, const core::rect<s32>& rect, const core::rect<s32>* clip=0) = 0; //! draws a pressed 3d button pane /** Used for drawing for example buttons in pressed state. It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param rect: Defining area where to draw. \param clip: Clip area. */ virtual void draw3DButtonPanePressed(IGUIElement* element, const core::rect<s32>& rect, const core::rect<s32>* clip=0) = 0; //! draws a sunken 3d pane /** Used for drawing the background of edit, combo or check boxes. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param bgcolor: Background color. \param flat: Specifies if the sunken pane should be flat or displayed as sunken deep into the ground. \param fillBackGround: Specifies if the background should be filled with the background color or not be drawn at all. \param rect: Defining area where to draw. \param clip: Clip area. */ virtual void draw3DSunkenPane(IGUIElement* element, video::SColor bgcolor, bool flat, bool fillBackGround, const core::rect<s32>& rect, const core::rect<s32>* clip=0) = 0; //! draws a window background /** Used for drawing the background of dialogs and windows. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param titleBarColor: Title color. \param drawTitleBar: True to enable title drawing. \param rect: Defining area where to draw. \param clip: Clip area. \param checkClientArea: When set to non-null the function will not draw anything, but will instead return the clientArea which can be used for drawing by the calling window. That is the area without borders and without titlebar. \return Returns rect where it would be good to draw title bar text. This will work even when checkClientArea is set to a non-null value.*/ virtual core::rect<s32> draw3DWindowBackground(IGUIElement* element, bool drawTitleBar, video::SColor titleBarColor, const core::rect<s32>& rect, const core::rect<s32>* clip=0, core::rect<s32>* checkClientArea=0) = 0; //! draws a standard 3d menu pane /** Used for drawing for menus and context menus. It uses the colors EGDC_3D_DARK_SHADOW, EGDC_3D_HIGH_LIGHT, EGDC_3D_SHADOW and EGDC_3D_FACE for this. See EGUI_DEFAULT_COLOR for details. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param rect: Defining area where to draw. \param clip: Clip area. */ virtual void draw3DMenuPane(IGUIElement* element, const core::rect<s32>& rect, const core::rect<s32>* clip=0) = 0; //! draws a standard 3d tool bar /** Used for drawing for toolbars and menus. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param rect: Defining area where to draw. \param clip: Clip area. */ virtual void draw3DToolBar(IGUIElement* element, const core::rect<s32>& rect, const core::rect<s32>* clip=0) = 0; //! draws a tab button /** Used for drawing for tab buttons on top of tabs. \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param active: Specifies if the tab is currently active. \param rect: Defining area where to draw. \param clip: Clip area. \param alignment Alignment of GUI element. */ virtual void draw3DTabButton(IGUIElement* element, bool active, const core::rect<s32>& rect, const core::rect<s32>* clip=0, gui::EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT) = 0; //! draws a tab control body /** \param element: Pointer to the element which wishes to draw this. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param border: Specifies if the border should be drawn. \param background: Specifies if the background should be drawn. \param rect: Defining area where to draw. \param clip: Clip area. \param tabHeight Height of tab. \param alignment Alignment of GUI element. */ virtual void draw3DTabBody(IGUIElement* element, bool border, bool background, const core::rect<s32>& rect, const core::rect<s32>* clip=0, s32 tabHeight=-1, gui::EGUI_ALIGNMENT alignment=EGUIA_UPPERLEFT ) = 0; //! draws an icon, usually from the skin's sprite bank /** \param element: Pointer to the element which wishes to draw this icon. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param icon: Specifies the icon to be drawn. \param position: The position to draw the icon \param starttime: The time at the start of the animation \param currenttime: The present time, used to calculate the frame number \param loop: Whether the animation should loop or not \param clip: Clip area. */ virtual void drawIcon(IGUIElement* element, EGUI_DEFAULT_ICON icon, const core::position2di position, u32 starttime=0, u32 currenttime=0, bool loop=false, const core::rect<s32>* clip=0) = 0; //! draws a 2d rectangle. /** \param element: Pointer to the element which wishes to draw this icon. This parameter is usually not used by IGUISkin, but can be used for example by more complex implementations to find out how to draw the part exactly. \param color: Color of the rectangle to draw. The alpha component specifies how transparent the rectangle will be. \param pos: Position of the rectangle. \param clip: Pointer to rectangle against which the rectangle will be clipped. If the pointer is null, no clipping will be performed. */ virtual void draw2DRectangle(IGUIElement* element, const video::SColor &color, const core::rect<s32>& pos, const core::rect<s32>* clip = 0) = 0; //! get the type of this skin virtual EGUI_SKIN_TYPE getType() const { return EGST_UNKNOWN; } };
在使用Irrlicht引擎的GUI模块的时候,总是会和一个名为IGUIEnvironment的接口打交道,这个似乎就是整个GUI模块向我们打开的大门,所以我们就从这里下手来一窥GUI模块的整体结构。
首先第一步,看官网上对这个接口的描述,如下摘录:
"GUI Environment. Used as factory and manager of all other GUI elements"
这个接口作为GUI元素的工厂和管理器。嗯!从这个结构似乎我们就能够窥视整个GUI模块的结构。好了,接下来老样子,直接看源码,看下这个接口的定义:
class IGUIEnvironment : public virtual IReferenceCounted { public: //! Draws all gui elements by traversing the GUI environment starting at the root node. virtual void drawAll() = 0; ...........由于这个接口的定义实在太长,如果全部贴上来的话,会造成篇幅过大,这里只进行摘录,用来点出接口类提供的主要接口等。
从这个类的申明可以发现,这个接口是继承至IReferenceCounter的。还记得前面我们详细的讲解过Irrlicht引擎关于对象管理的方式吗?采用的就是这里的IReferenceCounted引用计数方法。这里的IGUIEnvironment接口也同样使用这个方法来进行管理。
接下来的是一个非常重要的函数:drawAll()。这个函数会依次的遍历GUI环境中保存的所有的GUI元素,然后绘制它们。这也是我们在进行程序主循环的时候调用的方法。
除了这个功能之外剩下的函数分别是进行如下的工作:
上面这些就是这个接口大致提供的接口操作。
使用接口的方法好处就是将内部的管理方式对用户隔离,这样用户就不需要知道内部的管理方式,而很容易的通过暴露出来的接口对各种模块进行操作了。但是博主的目的是为了学习这种管理的方式,所以还需要在此基础上,更加深入的了解。
接口中不可能含有对内部对象管理方式的任何描述,唯一的方法是要找到实际继承这个接口,并且被引擎实例化的对象。所以我们需要重新回到最开始引擎初始化的函数当中来找寻实际的进行GUI元素管理的类。
对于Irrlicht引擎来说,整个引擎的启动就是通过一个createDevice函数来启动的,所以,我们就从这个函数开始,一直路由下去,直到找到最终创建GUI环境的那个代码块处,最终路由到如下的函数中:
void CIrrDeviceStub::createGUIAndScene() { #ifdef _IRR_COMPILE_WITH_GUI_ // create gui environment GUIEnvironment = gui::createGUIEnvironment(FileSystem, VideoDriver, Operator); #endif // create Scene manager SceneManager = scene::createSceneManager(VideoDriver, FileSystem, CursorControl, GUIEnvironment); setEventReceiver(UserReceiver); }
//! creates an GUI Environment IGUIEnvironment* createGUIEnvironment(io::IFileSystem* fs, video::IVideoDriver* Driver, IOSOperator* op) { return new CGUIEnvironment(fs, Driver, op); }
在源代码中找到这个类的定义,下面是这个类的定义:
class CGUIEnvironment : public IGUIEnvironment, public IGUIElement { public: <span style="font-family:Microsoft YaHei;"> //All method from Super-class ...... </span> private: IGUIElement* getNextElement(bool reverse=false, bool group=false); void updateHoveredElement(core::position2d<s32> mousePos); void loadBuiltInFont(); struct SFont { io::SNamedPath NamedPath; IGUIFont* Font; bool operator < (const SFont& other) const { return (NamedPath < other.NamedPath); } }; struct SSpriteBank { io::SNamedPath NamedPath; IGUISpriteBank* Bank; bool operator < (const SSpriteBank& other) const { return (NamedPath < other.NamedPath); } }; struct SToolTip { IGUIStaticText* Element; u32 LastTime; u32 EnterTime; u32 LaunchTime; u32 RelaunchTime; }; SToolTip ToolTip; core::array<IGUIElementFactory*> GUIElementFactoryList; core::array<SFont> Fonts; core::array<SSpriteBank> Banks; video::IVideoDriver* Driver; IGUIElement* Hovered; IGUIElement* HoveredNoSubelement; // subelements replaced by their parent, so you only have 'real' elements here IGUIElement* Focus; core::position2d<s32> LastHoveredMousePos; IGUISkin* CurrentSkin; io::IFileSystem* FileSystem; IEventReceiver* UserReceiver; IOSOperator* Operator; static const io::path DefaultFontName; };
GUI元素创建工厂列表
字体数组
精灵组数组
悬浮元素对象的指针
悬浮对象的父元素指针
焦点元素指针
上一次鼠标悬浮位置
当前GUI元素的Skin对象指针
。。。
这里需要注意一个元素的两种状态,分别是Hovered和Focus状态。Hovered状态即为悬浮状态,这种状态是只在有鼠标设备的机器上才会有用,即当鼠标移动到某个GUI元素上时但是没有按下的这种“悬而未决”的状态,称之为悬浮状态。另外一个Focus状态,有过编程经验的都知道,当点击某个GUI元素的时候,这个元素就具有了焦点。
但是读者啊,你们有诶有发现,这个类里面好像没有直接保存那些被创建的GUI元素啊?这些GUI元素到底被放在了什么地方了?在这个类里面好像也只有GUI元素创建工厂列表这个属性可能会保存。所以,依据这个思路,我们来看下GUI元素创建工厂的内容。
首先,一看到这个类前面有个“I”,我们就应该知道这个类是一个接口,里面只会含有一些操作。那么我们就先来了解下一个GUI元素工厂有哪些操作吧!
同样的方法,先来看下官网上关于这个接口的描述,摘录如下:
“Interface making it possible to dynamically create GUI elements”
用于动态创建GUI元素的接口。好了,依然是那么的简单粗暴。打开源代码,看下接口的定义吧:
namespace gui { class IGUIElement; //! Interface making it possible to dynamically create GUI elements /** To be able to add custom elements to Irrlicht and to make it possible for the scene manager to save and load them, simply implement this interface and register it in your gui environment via IGUIEnvironment::registerGUIElementFactory. Note: When implementing your own element factory, don't call IGUIEnvironment::grab() to increase the reference counter of the environment. This is not necessary because the it will grab() the factory anyway, and otherwise cyclic references will be created. */ class IGUIElementFactory : public virtual IReferenceCounted { public: //! adds an element to the gui environment based on its type id /** \param type: Type of the element to add. \param parent: Parent scene node of the new element, can be null to add to the root. \return Pointer to the new element or null if not successful. */ virtual IGUIElement* addGUIElement(EGUI_ELEMENT_TYPE type, IGUIElement* parent=0) = 0; //! adds a GUI element to the GUI Environment based on its type name /** \param typeName: Type name of the element to add. \param parent: Parent scene node of the new element, can be null to add it to the root. \return Pointer to the new element or null if not successful. */ virtual IGUIElement* addGUIElement(const c8* typeName, IGUIElement* parent=0) = 0; //! Get amount of GUI element types this factory is able to create virtual s32 getCreatableGUIElementTypeCount() const = 0; //! Get type of a createable element type /** \param idx: Index of the element type in this factory. Must be a value between 0 and getCreatableGUIElementTypeCount() */ virtual EGUI_ELEMENT_TYPE getCreateableGUIElementType(s32 idx) const = 0; //! Get type name of a createable GUI element type by index /** \param idx: Index of the type in this factory. Must be a value between 0 and getCreatableGUIElementTypeCount() */ virtual const c8* getCreateableGUIElementTypeName(s32 idx) const = 0; //! returns type name of a createable GUI element /** \param type: Type of GUI element. \return Name of the type if this factory can create the type, otherwise 0. */ virtual const c8* getCreateableGUIElementTypeName(EGUI_ELEMENT_TYPE type) const = 0; }; } // end namespace gui
addGUIElement -- 增加特定类型的GUI元素
getCreatableGUIElementTypeCount -- 获取当前工厂能够创建的元素类型的数目
getCreatableGUIElementType -- 获取指定下标对应的元素类型
getCreatableGUIElementTypeName -- 获取指定下标对应的元素类型名称
getCreatableGUIElementTypeName -- 获取指定元素类型的元素类型名称
唔!!!好像和GUI元素的对象管理没有半毛钱关系啊。在博主找到这里的时候,心都凉了!忙了半天,却发现还没有找到对象的管理模式。这里博主突然想到,它对对象的管理方式可能不是通过一个数组或者某个具体的容器来包含所有的已经被创建的GUI元素,可能类似于我们在编写Octree场景管理的时候,只在管理器里面保存一个根节点的指针,然后通过这个指针来进行一次的遍历,也就是说可能是使用树状的结构来进行保存的。
想到这里,博主兴奋不已。因为博主突然发现在CGUIEnvironment接口中有一个成员函数,名为getRootElement。这个函数明显就是对管理器中包含元素进行遍历的一个函数嘛!!!我们可以从这个函数下手,来窥探它到底是如何组织和管理GUI元素的。下面先来看下这个函数的实现:
//! Returns the root gui element. IGUIElement* CGUIEnvironment::getRootGUIElement() { return this; }这个函数,十分的简洁,直接返回的是当前管理器对象的指针。这很奇怪不是吗?但是,我们在来看下CGUIEnvironment的定义就会发现,这个类实际上也是继承至IGUIElement这个类的。也就是说,这个管理器本身就作为了整个管理结构的根节点。那么到底是如何通过这个根节点来进行GUI元素的访问的了?
哎,写到这里的时候,博主突然发现在探究的过程中少了很重要的一个元素,那就是对象管理器所管理的对象本身。博主一直专注于管理器,却忽视了管理器所管理的对象。接下来,我们就来看下这个管理对象到底是什么。从上面的种种迹象可以推断出,管理器管理的实际上就是IGUIElement这个接口。也就是说,所有的GUI元素都将从这个接口继承而来。好了,下面就来一窥这个对象的究竟吧!!!
老规矩,先来看下官网对它的描述,摘录如下:
"Base class of all GUI elements"
所有GUI元素的基类。
在来看下它的申明吧:
//! Base class of all GUI elements. class IGUIElement : public virtual io::IAttributeExchangingObject, public IEventReceiver { public: <span style="font-family:Microsoft YaHei;">...................................</span> protected: //! List of all children of this element core::list<IGUIElement*> Children; //! Pointer to the parent IGUIElement* Parent; //! relative rect of element core::rect<s32> RelativeRect; //! absolute rect of element core::rect<s32> AbsoluteRect; //! absolute clipping rect of element core::rect<s32> AbsoluteClippingRect; //! the rectangle the element would prefer to be, //! if it was not constrained by parent or max/min size core::rect<s32> DesiredRect; //! for calculating the difference when resizing parent core::rect<s32> LastParentRect; //! relative scale of the element inside its parent core::rect<f32> ScaleRect; //! maximum and minimum size of the element core::dimension2du MaxSize, MinSize; //! is visible? bool IsVisible; //! is enabled? bool IsEnabled; //! is a part of a larger whole and should not be serialized? bool IsSubElement; //! does this element ignore its parent's clipping rectangle? bool NoClip; //! caption core::stringw Text; //! tooltip core::stringw ToolTipText; //! users can set this for identificating the element by string core::stringc Name; //! users can set this for identificating the element by integer s32 ID; //! tab stop like in windows bool IsTabStop; //! tab order s32 TabOrder; //! tab groups are containers like windows, use ctrl+tab to navigate bool IsTabGroup; //! tells the element how to act when its parent is resized EGUI_ALIGNMENT AlignLeft, AlignRight, AlignTop, AlignBottom; //! GUI Environment IGUIEnvironment* Environment; //! type of element EGUI_ELEMENT_TYPE Type; };老规矩,先看下这个类的成员,将操作函数省去。一眼看过去,wow!成员属性非常的多。所以下面就简单的做下归纳好了:
子元素的列表
父元素指针
尺寸位置相关的属性
是否可视
是否启用
是否是子元素
是否忽视父节点的裁剪
文字显示相关属性
ID
Tab Group相关属性
从这个属性列表中对于对象管理来说最重要的就是前面两个属性了:
子元素列表和父元素指针。
也就是说,在Irrlicht的GUI模块中,对GUI元素的管理是采用这种多叉树的树状结构进行的。类似于3D引擎中进行场景管理的场景图方法。
今天,我们对Irrlicht引擎的GUI模块整体结构进行了了解。总的来说,所有的GUI元素都继承至IGUIElement这个接口。而我们是通过IGUIEnvironment这个接口来操作整体的GUI。对于GUI元素的管理方式,Irrlicht采用的是多叉树的方式进行管理,所以对GUI元素的操作就变成了对一颗多叉树进行的操作,这个树的根节点就是GUI管理器本身,可以通过getRootElement来得到这个Element。剩下的内容就是Irrlicht引擎的GUI模块支持了哪些控件,以及对元素的访问有哪些功能,哪些属性了,这些东西都和具体的控件相关。等到我们需要实际的编写一个控件的时候,我们在来深入的了解下Irrlicht中每个控件是如何实现的。
好了,今天到这里就结束了,感谢大家的一直关注!!!
Irrlicht 3D Engine 笔记系列 之 教程5- User Interface
标签:gui
原文地址:http://blog.csdn.net/i_dovelemon/article/details/42005387