这一篇先讲排版的一部分。文章的顺序是按照理解GacUI的顺序来编写的,所以那些难的、不常用的东西都会放到最后。显然一个GUI库,用户最关心的就是三个部分:排版、换肤和绑定。但是由于篇幅限制,排版也要分开几篇来写。
GacUI 排版的基本用法
如果不首先了解GacUI的排版机制的话,直接硬上通常都会遇到,怎么我创建出来的控件的大小是0这样的事情。GacUI不鼓励你hard code大小,所以所有控件都没有尺寸属性。不过我还是留了一个小后门,当你就是想要这么做的时候,还是可以做。而且我的设计刚好使得,如果你大规模hard code大小,那用起来就会很苦逼。
GuiGraphicsComposition
之前我们已经了解到,GacUI的图形都是由Composition和Element组成的。Composition的基类是 GuiGraphicsComposition 。在这里有几个重要的属性:
MinSizeLimitation 属性
首先要提出的是,虽然在XML上面可以直接使用这个属性,但是当你使用C++的时候,这个属性是通过 GetMinSizeLimitation 和 SetMinSizeLimitation 来访问的。MinSizeLimitation属性控制的是,这个Composition的大小如何被他的Children所影响。属性的类型是一个枚举类型:
enum MinSizeLimitation
{
NoLimit,
LimitToElement,
LimitToElementAndChildren,
};
其中:
NoLimit:Composition的大小跟他的Children完全没有关系。
LimitToElement:Composition的大小仅跟绑定在Composition上面的这个Element有关系。举个例子,如果你的Composition使用了 GuiSolidLabelElement ,那么,Composition最小的大小就是Element里面的文字所决定的大小。当然这个Element里面也有一些设置,譬如说能否换行啊,能否显示省略号等等,这些都会影响最小大小的计算。
LimitToElementAndChildren:Composition的大小同时受到Element和Children的影响。
那么Children是如何影响Composition的大小的呢?
Margin 和 InternalMargin 属性
使用CSS的朋友们应该都很熟悉,Margin和InternalMargin分别类似于margin和padding的概念。在这里需要特别指出,Children使用的坐标空间是由InternalMargin所决定的,原点刚好是InternalMargin划出来的矩形的左上角。这首先就限制了所有的Children都必须显示在InternalMargin的范围内,超出的部分都会被直接剪裁掉,哪怕他实际上并没有超出Composition的范围。
而Margin属性则是一个建议,他会告诉上一级的Composition在计算Children的大小的时候,要把Margin考虑进去。
PreferredMinSize 属性
PreferredMinSize属性跟上面的属性可以同时使用。你可以通过给一个值,告诉一个Composition,不管计算出来的最小大小是多少,总之不能小于PreferredMinSize的大小。
GuiBoundsComposition
GuiGraphicsComposition是个抽象类,他是不能直接使用的。如果你需要放一个Composition进你的树里面,通常你会选择 GuiBoundsComposition 或者他的子类。如果一个Composition不继承自GuiBoundsComposition,那么通常意味着,他的尺寸是完全受到上一级Composition的控制的,譬如Table控制Cell,Stack控制StackItem,Flow控制FlowItem,Document控制DocumentItem这样。
GuiBoundsComposition多出了两个属性,分别是:
AlignmentToParent 属性
AlignmentToParent属性分别强行设置了一个Composition的四个方向,Margin的外面跟上一级Composition的InternalMargin的里面的距离是多少。设置为-1就意味着不规定这个距离。只要你设置了AlignmentToParent,那么这将直接影响到上一级Composition的尺寸的计算。在这里我举个例子:
假设A包含B,然后分别有下面的属性:
A.InternalMargin = {left:1 top:2 right:3 bottom:4}
B.Margin = {left:5 top:5 right:5 bottom:5}
B.PreferredMinSize = {x:100 y:50}
B.AlignmentToParent = {left:-1 top:-1 right:10 bottom:20}
首先我们可以看出,由于B设置了这一个AlignmentToParent的值,所以无论A的尺寸如何改变,B永远都位于A的左下角,而且B的Margin和A的InternalMargin在右下角的距离分别是10和20。我们可以很清楚的知道,B的最小尺寸就是100和50,那么A的最小尺寸是多少呢?
首先,A的InternalMargin决定了,A的横向大小不小于left+right=4,纵向大小不小于top+bottom=6;
其次,B的Margin和PreferredMinSize决定了,B占用的A的空间的横向大小不小于left+right+x=110,纵向大小不小于top+bottom+y=60;
再者,B的AlignmentToParent规定了,B的Margin外面和A的InternalMargin里面还有一个10和20的距离;
因此:A的最小尺寸就是4+110+10=124,和6+60+20=86,然后B永远处于A右下角的指定位置。
那么这有什么用呢?如果A是 GuiWindow 的 ContainerComposition 属性,然后你放一个B进去,那么当你拖动窗口的大小的时候,你会发现当窗口的客户区小于124和86的时候,窗口就会限制你不能把他拖得更小。事实上,你整颗Composition树的根节点的大小最后会反映到窗口上面去。
Bounds 属性
这是一个喜闻乐见的属性,因为透过这个属性,你就可以强行hard code一个Composition的位置了。你可以通过修改控件的 BoundsComposition 属性的 Bounds 属性,从而强行指定一个控件的位置。然而需要注意的是,Bounds的优先级是最低的,也就是说,如果它的值跟别的属性冲突了(譬如说Size比最小尺寸小,位置跟那些Margin和Alignment有冲突)的话,那么控件的位置和尺寸将不严格按照Bounds的设置摆放。控件会先参考别的属性的值,实在找不到约束了,最后去看Bounds。所以有时候会发现一个控件的位置跟你强行指定的位置不一样,就是这个原因。
我就是不喜欢你们hard code尺寸,哈哈哈哈哈哈。
GuiSharedSize(Root|Item)Composition
除此之外,GacUI还支持很多原生的排版功能。在这些排版功能里面,最有趣的就是SharedSize了。这是什么样的排版呢?假设你们在设计菜单。菜单的文字跟快捷键部分在垂直的方向上是不重叠的。然而他们的父子结构决定了,一个菜单首先按行切割,其次才按列切割。而且你还不能用表格做,因为每一个菜单是一个独立的控件,而且菜单里面还可以放别的东西啊。
那如何让一个菜单里面的所有菜单项,会在排版的时候,先互相交换文字和快捷键部分的长度,从而一个显示成这样的菜单:
TEXT1 [CTRL+SHIT+DEL+Z]
A-VERY-LONG-TEXT [CTRL+Z]
不会被显示成
TEXT1 [CTRL+SHIT+DEL+Z]
A-VERY-LONG-TEXT [CTRL+Z]
呢?这就靠SharedSize了。SharedSize由 GuiSharedSizeRootComposition 和 GuiSharedSizeItemComposition 组成。其中Item并不需要——而且几乎也不可能——是Root的直接的Children。Root会在排版的时候去寻找(当然算法不可能这么写了这样太慢了)他子树里面的所有Item,然后根据每个Item设置的Group去分组,最后按照要求统一他们的最小尺寸。
因此在上面的菜单的例子中,我们可以在整个菜单的外围放一个Root,而每一个菜单项控件里面,分别用两个Item放文字跟快捷键,然后设置好分组的名字,文字分为一组,快捷键分为一组。最后要求每一个组的宽度都要统一(高度不需要)。最后的结果就是,所有的文字的最小尺寸都由最长的那个决定,所有的快捷键的最小尺寸也都由最长的那个决定,于是排版的结果就相当正确了。
尾声
今天讲的几个属性就是GacUI处理排版的时候的重要内容。当然GacUI不可能只支持这种排版,原生支持的排版功能还有Stack、Table、Flow、SharedSize和Document等。他们的具体内容可以参考 GacUI_Layout 这个demo。细节将在下一篇文章中讲述。除了这五种以外,GacUI还有一些奇形怪状的排版功能,这些基本只在制作控件皮肤的时候用到。
P.S.
控制相对位置的功能实在是太重要了,每次在CSS里面搞这些的时候都觉得好蛋疼,明明这些功能在GacUI和WPF里面设置出来如此简单,结果CSS连让一个div的大小贝设置成position: relative的child div的大小约束到这么常见、直接、简单的功能,都不提供。