UI研究

DinS          Written on 2018/1/26

UI是任何游戏都不可或缺的部分,其与场景的区别在于UI都是2D的,且用于与场景的交互,本专题让我们深入研究UI。

一、回顾之前制作的一个UI

代码上还是很直接的,加载一个资源,然后建立图片对象并设置属性,然后加入UI子系统的根节点。

虽然可以做到布局UI,但是有几个严重的问题:
第一,对于每一个UI资源都需要加载。你可能会问:不加载资源干什么呢?实际上对于UI有优化方法。这是因为UI一般都比较小,每次从硬盘读入一张图片花费的代价相对高,因此通常的做法是把所有UI拼到一张大图上,然后获取。
第二,通过代码布局UI显得很繁琐,并且灵活性不高。如果之后要改布局,必须重新编译,显然不太可取。
第三,我们把所有UI都堆到了根节点,不利于系统化组织。场景还有各个父节点和子节点,UI系统也一样。

为了解决这些问题,让我们探究一下专业的UI布局方式。

二、xml工作原理与UI大图

业界普遍采用xml的方式来布局UI。这里不打算深入讨论xml,本着会用即可的原则来解释。
首先xml是一种标记数据格式的约定,或者说是语法也可以。记录数据时按照这种语法进行,解析xml文件时也按照语法进行,这样就实现了数据的通讯。
那么xml长什么样子?Urho3D里提供了样本,让我们先打开Data\UI\EditorIcons.xml看一看:

大图点这里

节选一小部分,还是很有规律的,因为这是一个非常简单的xml。
从语义上来看,第二个出现的<element>到</element>定义了一个叫做Scene的东西,其有两个属性:Texture和Image Rect,这两个属性的值分别是后面那个value里面的东西。
xml就是这么简单,定义一个对象,然后确定属性和值。
那么这跟UI有什么关系呢?注意Texture的值里面有一个路径,指向了一张图,打开看看:

是这样的一张图,每一块区域很明显是一个UI小图标。
然后看Image Rect的值,从名字可以看出是图片的一块矩形区域,用像素来衡量。比如那个Node覆盖的区域就是(0,0)左上角到(14,14)右下角的矩形区域。
这个区域是什么?
我自己制作一个图标,如下:

         

大小为14×14像素,跟那个矩形区域一致。
然后把这个图标覆盖到原图对应位置。

完美的覆盖!于是我们可以猜测出这个xml文件是干什么用的了。
其定义了每一个对象对应的UI图片,然后可以使用这些对象代表UI。
为了证实我们的推理,运行bin里面的Editor.bat,这是urho3d提供的编辑器,就是用来自动生成xml用的:

注意红圈,其图片确实变成了我们刚刚覆盖的图片。

这就很方便了,意味着我们可以把所有UI拼成一张大图,然后利用xml定义对应的对象和图片,以后需要更改随时替换。

为了加深理解,再看一个更复杂的。打开Data\UI\DefaultStyle.xml。这是Urho3D提供的默认UI定义,包括了常见的各种UI:

大图点这里

这个明显要复杂的多。
type后面还跟有style,各个属性也明显更多。不过xml的原理都是一样的。
第一个BorderImage指出了一张UI图片,如下:

后面出现的所有对象都没有路径了,但是从根本上style都能够回溯到BorderImage,因此可以把style认为追加了该style定义的属性。
然后剩下那些属性按语义理解即可。

但是这个只是从现象来理解,为什么定义了这些属性就能够在程序中产生效果?更进一步地,到底有那些属性可以定义?
为了回答这个问题,我们需要看源代码,即解析xml那部分的代码。代码在Urho3D-1.7\Source\Urho3D\UI的各个cpp中,这里以Button为案例,看cpp:

大图点这里

在这里出现了刚刚在xml里出现的属性。
第一行还有一个COPY_BASE_ATTRIBUTES,从语义上看是把BorderImage的属性拷贝过来,继续看BorderImage.cpp:

大图点这里

又多了一些属性,并且出现了UIElement,继续深入:

大图点这里

看来UIElement是所有UI的根属性。
现在可以来一次全面的总结了。
我们在xml中定义对象,写属性和值,然后程序拿到xml后会解析该xml,依据就是cpp中出现的各个属性定义,比如有Image Rect属性,然后读取值,然后赋值,类型就是IntRect。一直解析,直到xml解析完成,UI的默认定义就有了,然后我们可以用这个定义直接设置UI,简化代码,增加灵活性。

至于Urho3D具体是如何解析xml的,我们大可不必关注,这是底层的问题,相信队友即可。cpp中提供的属性,绝对是足够用的,如果有需求可以直接查cpp。当然没有说明文档,所以可以从语义来判断,再尝试尝试应该能够理解。

三、使用xml设置UI风格并加入代码

让我们尝试使用xml定义的UI在代码中添加UI。
虽然直接在代码中布局UI不是个好方法,但是是一个不错的起点,可以帮助我们进一步理解UI。

大图点这里

推荐做法是把UI子系统的根节点作为成员变量保留下来,这样不用每次都调两个函数了。
然后在Start()里增加如下代码:

大图点这里

大图点这里

增加了3个UI。注意一开始如何读取xml以及之后如何给控件设置风格。
到底SetStyle干了什么呢?结合Text的xml来看,可以理解为调用SetStyle本质上就是设置了控件的3个属性,一句代码等效于3句代码,本质就是这样。
其他的无非是多一些设置不同属性而已。

剩下一个看点是child之下的child。像场景模型一样,UI系统也是有层级的,这样方便组织。对于UI控件而言,其基类都是UIElement,可以在UIElement之下插入UIElement。这里展示了在window之下建立一个按钮,一旦有层级关系,position就是相对parent的位置。
可以看一下效果:

按钮的(0,0)位置位于父控件window的左上角。

我们给按钮绑定了一个事件,点击后改变文字显示:

大图点这里

这里展示了如何获取根节点下的某个控件。
UI一般不会太多,所以用名称来获取即可。另外貌似也没找到哪里设置UI的ID。注意这里用的是StaticCast,因为控件都属于UIElement,调用者才知道具体的类型。
运行看效果:

四、使用xml布局UI

虽然代码能够直接布局UI,但很快你就会发现这是一个非常枯燥的活,更好的方法是使用xml来布局。
Urho3D提供了一个例子,让我们看一个xml,该xml位于bin\Data\UI\UILoadExample.xml,内容很简单,如下:

大图点这里

可以直观地看出,有一个window控件,window内有两个button,每个button有个text显示文字。就是这样,对于各个控件的定义则出现在上面那个风格设置xml中,这里出现的实际上代表布局方式。
有了这样的xml,我们可以直接在代码中读入并显示:

大图点这里

如此简单的代码就实现了复杂的UI布局。确实是方便。运行一下:

完美。
使用xml布局不仅能够让代码异常简洁,而且还相当灵活,让我们在刚才的xml中加入一点内容:

大图点这里

增加一个lineedit,然后不改动代码,运行程序:

果然多出了一个lineedit。

但是在xml中写UI毕竟还是累,最好是所见即所得。
能够实现吗?可以,使用urho3d自带的editor。

五、使用Editor可视化布局UI

如果你build了例子,会找到这样一个东西:

这个就是Editor,不过是.bat文件,没关系,点击打开即可。
可以看到层级和属性:

使用起来还算流畅,选中层级中的一个节点,然后用menu->Create:

然后就会在选中节点下新建一个控件,然后可以在右侧设置属性
等设置满意了,选中一个节点,然后menu->UI-Layout

就可以把UI布局保存成xml了。
注意,选中哪个节点就会以该节点为根节点制作xml。但是如果你选中根节点,即UI,点保存不起作用。
引申意义就是xml布局实际上只能有一个根控件,控件之下可以有若干控件。换言之,程序中一般会读入若干xml布局才能真正完成UI布局,除非你的根节点下就一个控件。

题外话,Editor不仅仅可以用来布局UI,还可以布局Scene,实际上大部分功能是针对Scene的。本专题只讲解UI,所以不涉及Scene的xml。


UI研究完成后,再看一个必然会遇到的问题:切换场景,见《场景模型进阶-切换场景》。