Ogre 1.7 这个版本比以前有了很多的变化,其中之一就是在Ogre官方不再包含CEGUI的组件,但是尽管没有使用CEGUI,在官方的demo中仍然可以看见有gui的功能,那么Ogre又是怎么实现的呢?
根据官网的说明,新的gui系统使用的是一个叫“tray”的系统,这个系统的主要代码在SdkTray.h这个文件之中。sdktray使用的Ogre overlay来实现gui,并且Ogre 1.7比以前版本似乎也多了一些gui相关的组件,这大概预示,Ogre即将开发自己的gui组件,而cegui也许将被抛弃。
不管未来如何,研究下sdktray还是很有意义的一件事情,使用overlay来实现gui,最大的特点在于可以灵活利用Ogre强大的material系统,实现很多不可思议的效果。比起使用cegui来,减少了学习周期,但是比较遗憾的是,目前Ogre 1.7的sdktray还不能作为一个完整的gui组件,有些功能还很不完善。
现在来研究以下sdktray.h文件的源代码.
一、分析SdkTays.h这个头文件
(1)这个头文件命名空间为:OgreBits
(2)此命名空间下首先定义(包含)了一些枚举TrayLocation(位置)、ButtonState(ButtonState就如同其名字一样,指的是鼠标按下、悬浮以及释放的情况下的gui对应3种状态)
(3)接着定义了一个sdkTrayListener类,它常被用做基类,从接口来看,应该是实现gui对应的功能,包括点击(含有Hit单词的函数)、选择(Select)、移动(Move)等等【本头文件中定义的sdkTrayListener类里面很简单,全是virtual的诸如Hit或Select类的函数,sdkTrayListener是个纯虚类】。
(4)接着定义了一个Widget类(组件类)。
Widget是所有窗口组件的基类。一个Widget包含一个OverlayElement和一个sdktraylistener,分别实现外观和对应的事件响应。这个基类还包括几个static的工具函数。nukeOverlayElement()的作用是删除掉OverlayElement(包括子element),isCursorOver ()用来判断鼠标是否悬浮在gui的上方。需要注意的是这个函数判定包括了边框的判断。并且仅仅实现了矩形的判断,这个大概是因为目前所有的OverlayElment都是矩形的缘故。cursorOffset()获取鼠标离OverlayElement的中心有多少像素远。getCaptionWidth()用来获取一个textAreaOverlayElement中的caption的长度。caption应该指的是textAreaOverlayElment保存的文本。fitCaptionToArea()这个函数用来对具体的caption实现简单的格式化,包括空格的长度以及根据gui长度实现裁剪,但是这个函数没有实现string太长时候的自动换行处理。接下来的4个virtual函数是回调函数, 用来实现鼠标对应事件响应处理。有意思的是这里没有keyboard对应的callback 这大概意味着目前的sdktray还没有实现响应键盘的功能。
(5)继承子Widget(组件)的一些具体控件
继承自Widget的Button类。如同Ogre许多的部件,Ogre要求使用sdkmanager来创建具体的button。在button的构造函数中可以看到widget的OverlayElement 是使用OverlayManager::createOverlayElementFromTemplate()函数创建的,这个函数的参数是硬编码,这意味着如果我们要使用自己定义的外观,需要更改button的源代码,也许copy一个多出2个参数的构造函数是个不错的选择。button使用了一个有限状态机来实现了基类的4个callback。注意_cursorRelease()中调用了listener的buttonHit()函数,说明button响应点击的逻辑判断在鼠标释放的时候,并且没有对鼠标按下进行逻辑事件的处理,这也是sdktray不够完善的地方。
【Widget只有三个成员变量,mTrayLoc位置,mElement以及mListener,这三个变量都被具体的如button或textbox继承,并在这些子类中具体赋值,注意,子类中调用诸如mElement = Ogre::OverlayManager::getSingleton().createOverlayElementFromTemplate("SdkTrays/Button", "BorderPanel", name);这样的语句来实例化mElement成员。Widget类中也有_assignListener(SdkTrayListener* listener),这个函数用来设定使用的监听器,当然监听器继承自纯虚类SdkTrayListener,并且这个指定监听器函数也很简单,里面就一句话,mListener = listener;就是把参数listener赋给成员监听器变量。现在的问题是listener是哪传来的呢?或者说在哪调用了assignListener函数呢?其实是在SdkTrayManager里头,比如在这个管理器里头创建button,则会在createButton函数里头先new一个button,然后调用assignListener并把SdkTrayManager的成员变量mListener赋给button的监听器(其实是赋给button继承的widget的监听器),当然SdkTrayManager的构造函数接收一个监听器并赋给它的成员监听器。】
TextBox继承自Widget,ogre中说明了是"Scrollable Text box",这个类实现了string超出gui宽度时候的换行处理,并且实现了一个下拉条。为了实现这些功能,这个类包含了一个显示标题的borderPanel,包含标题的textArea,包含caption的textarea,2个实现下拉条效果的Borderpanel和panel。TextBox的构造函数主要就是读取各个子OverlayElement并设置对应的的属性。同button一样,许多属性的值都是硬编码。最后构造函数调用了一个refitContents的子函数.,这个函数的功能是根据新设置的字体属性调用setText()重新排列文本。要复用的话,setText()是很好用的一个对外接口,这个函数设置需要显示的文本,并且将文本自动换行。setTextAlignment()实现文本对齐,appentText()实现字体追加。如果要自己实现打字输入,这个函数估计会有很大用途。但是这个函数每次都调用setext(),每次都对所有string重新计算位置,用来实现输入的话应该不是很有效率。setScrollPercentage()根据下拉的百分比来设置显示的字体。函数调用了filterLines(),这个子函数的功能是决定下拉的时候,哪些行的字体应该被显示。_cursorPressed()响应了对下拉条点击的处理。需要注意的是,函数使用基类cursorOffset()来进行范围判定,当鼠标在设定的范围内的时候,下拉条才开始响应对应的事件。从demo来看,下拉条是一个圆形,我估计这个圆形的半径是9像素,所以函数中判断使用的数值是81(9的平方)。
SelectMenu实现了下拉菜单的功能。getItems()、setItems()、clearItems()是对所有下拉的组操作,addItem(),removeItem(),selectitem(),getSelectedItem()实现子操作。其中selectItem调用了listener的itemSelected()函数,
label大概是最简单的一个组件。需要注意的这个label在鼠标pressed的时候调用了listener的labelHit()函数。
separator没有响应任何逻辑事件和鼠标事件。也许是用来显示一些把什么东西分开的东西。
slider实现基本的滑块功能,这个用来实现在一定范围内值的选取。setRange() 确定值的上下限,getvalue() 、setValue()获取设置value,注意value的类型是Real。从setValue()函数实现来看,设置的是滑块的left,也就是说slider类只能实现横向的滑块移动。
paramsPanel的功能是显示一个基于string类型的<key,value>。从实现上看,没有响应逻辑,也没有响应基本的鼠标事件,他的唯一功能就是显示key的value。程序中可以使用setvalue()来改变key对应的值。
checkBox选择框。类似于单选框,一个checkBox框对应一个条目。有多个条目要实现多个checkBox。toggle()是一个对外接口,这个函数调用了listener的checkBoxToggled()函数。
DecorWidget注释说明是用来实现用户自己的widget。这个DecorWidget什么都不做。如果要实现自己的widget,自己写一个继承自widget的类大概比较好。
ProgressBar进度条。包含3个部分:标题、正在读取的内容、以及显示进度用的显示条。显示条使用了2个overlayelement,其中一个根据百分比改变宽度,从而实现进度条效果。进度条没有响应任何事件。
(6)sdkTrayManager管理器类
sdkTrayManager是widget的管理器。不过看上去为了实现示例的轮盘效果这个manager多了许多的额外功能,如果不是使用示例的那个轮盘的话,很多功能似乎可以去掉。
构造函数中sdktrayManager实现了4个显示层:backdrop、trays、priority以及cursor层,并且它们的zorder依次从低到高。这里讲述一下sdktrymanager是如何显示鼠标的,在一个单独的overlay层中使用一个overlayElement跟踪鼠标的位置,并实现对应显示。priority层的用途是显示一个dialogShade的OverlayElement,这个看上去是个对话框,但是dialogShade不是一个widget,不明白为什么不单独把这个功能分离出来,要整合到manager中去。接下来的代码设置了tray层,应该对应的是sample demo中对应的那个轮盘gui。出于复用的目的,不讨论tray相关的内容。
sdktrayManager类中几个静态的工具函数有:
Ogre::Ray screneToscrene()和Ogre::Vector2 screneToScrene()实现2D坐标和基于相机视口的3D射线之间的互换。
refreshCursor()这个函数实现鼠标的更新,并且更新是基于ois unbuffered模式。我个人认为与鼠标有关的都应该独立出来作为一个类。
getCursorRay()是screentoScreen()的再封装。
接下来是各种widget对应的create函数。需要注意的是create函数将一个自己的mListener成员赋予了widget,这意味着响应事件的逻辑可以集中到manager的listener中,而不需要针对每个widget都写一个listener。
showFrameStates()这个函数的功能替代了以前debugOverlay的功能。
showLoadingBar()显示一个资源读取进度的进度条。
showOKDialog() 显示一个有OK button的对话框。
showYesNodialog()显示一个有yes no的对话框。这些功能也许都应该独立出来作为一个工具类。
frameRedneringQueued()响应了每帧循环,主要功能是清除不需要的widget以及更新framestates。
injectMouseDown()、injectMouseUp()和injectMouseMove()用于处理鼠标事件。overlay zorder靠前的overlayElement先获得处理。
总的来说,各种widget复用程度还是很高的,但是如果需要改变他们的显示的话,需要更改构造函数。sdkmanager的功能比较杂乱,如果需要实现自己的gui系统,应该将一些功能分拆出来。
【最后,补充一下,以前一直以为button和listener之间是观察者模式,因为listener负责记录鼠标单击或键盘按下等状态,并把这些状态发生或改变及时通知所有的观察者button。listener是典型的被观察的对象(记录了很多状态),而button是典型的观察者,时刻准备接收listener通知的状态改变。一定要注意虽然listener字面意思是监听器,好像是主动观察的意思,但这个观察是指观察鼠标或键盘的改变,如果把listener看成静态的容器,那么listener里面就是一个包含了很多状态的容器,就像一个电视机一样,里面包含了很多节目。而button就像看电视的人一样,就盯着电视容器里面的节目,也即盯着listener容器里面的鼠标或键盘状态。现在明白了button是观察者而listener这个复杂记录鼠标键盘状态的容器是被观察者,这点毋庸置疑了。但奇怪的是ogre在这里并没有安装典型的观察者模式来运作,如果安装典型的观察者模式则应该在listener这个被观察者里面维护一个所有观察者的列表,当某个状态改变后,迭代调用每个观察者的update函数。ogre中没这样做,他是使用了类似策略模式,但不完全是,它是这样做的:当listener知道状态改变时比如鼠标单击了,他应该通知所有的button或textbox等让他们执行鼠标单击的对应函数,这里鼠标单击就相当于update,但ogre为了更灵活,比如listener判断出来单击只是在button上面,和textbox没任何鸟关系,则listener只通知button调用鼠标单击函数,实际上就是buttonHit函数,并把这个具体的按钮作为参数传递给buttonHit的形参。也就是说listener通过给update传递一个具体的观察者作为参数,想让哪个观察者update就让哪个观察者update。实际上ogre中listener这个被观察者里面也没有维护观察者的列表了,而是随时想通知哪个观察者就把这个观察者作为参数调用update,在这个update函数中让具体的观察者执行一些更新行为。但这样有个问题,就是每个update,即鼠标单击或键盘按下要执行的函数,由于参数不同,需要写很多update函数,如果有10个控件,就要写10个update函数。实际上可以认为ogre中listener和所有的观察者之间没有任何直接联系】
下一步要研究一下ogre是怎么知道在哪个按钮上面单击的?