今天要讲的是GacUI里面的三个基础的排版功能。这些功能都可以在GacUI_Layout示例代码里面找到。
基本概念
大部分排版的概念都是成对出现的,譬如说
熟悉地球上最先进的GUI:WPF的朋友们可能会立刻反应过来,在WPF里面,“右边”的这三个类都是通过DependencyProperty来做的,所以实际上WPF只有“左边”的这三个类。不过GacUI之所以不这么做有,自然是因为C++的限制,不可能做出好的DependencyProperty。事实上GacUI已经是我制作的第八个GUI类库了,之前的七个都因为不同的原因而失败了,其中就有一个是用来模拟DependencyProperty的。
DependencyProperty的原理就是在类里面放一张表,用来动态的查询一个属性的值,就跟Javascript等语言的做法一样。但是这里的key其实并不是属性的名字,而是用来定义DependencyProperty的那个类的名字。所以你在WPF里面才会用到什么Grid.Row啊,Grid.Column这样的名字。
为什么要使用这样的名字呢?这一点就是WPF比Windows Forms先进的地方。我们知道Windows Forms的Font属性都是继承的,但是这个所谓的继承,实际上是实现在了所有定义或者覆盖了这个Font属性的类型里面,这是一种非常粗暴而且很容易出错的实现方法。DependencyProperty就没有这个问题,因为当你的属性需要模拟这一行为的时候,你只要看一眼发现对象里面并没有cache你的这个key和对应的值,那就直接往父对象里面找。更改的时候,还可以随便广播一个事件(当然我并没有看WPF的代码,所以我说的只是可能的一种方法)。于是所有跟这个属性相关的逻辑就全部集中到了一起。
但是在C++里面没法做,原因只有一个,就是因为太慢了。不过现在GacUI其实并没有什么立场来说WPF这一点不好,因为我现在初始化窗口的时候还是在用反射。不过我跟WPF有一点本质的区别,就是我的脚本都是静态类型的,所以过不久我就可以把脚本转C++的功能上线了,到那个时候运行时就再也没有什么反射了。WPF其实也注意到了这样的一个问题,所以他在新的UWP里面(不要问我为什么WPF没有,哈哈哈哈),实现了一个叫做x:Bind的绑定,做的事情跟GacUI其实一摸一样:把data binding的逻辑全部转换成代码,而不是在运行时去hook这些对象。
英雄所见略同。
既然不能用DependencyProperty,那GacUI要怎么办呢?鉴于layout其实是一个combinator,那我就本来在WPF写成
<Button Grid.Row="0" Grid.Column="1">Click Me!</Button>
的东西,在GacUI改成
<Cell Site="row:0 column:0">
<Button Text="Click Me!"/>
</Cell>
就好了。Combinator就是这么用的,哈哈哈哈哈。
排版
上一篇文章 介绍了基础的排版功能和 GuiSharedSizeRootComposition、GuiSharedSizeItemComposition 这一租排版对象。不过这里介绍的这三组跟SharedSize不同的是,“右边”的对象必须是“左边”的对象的直接子对象。不过在运行的时候我并没有检查,因为只要你不这么放,我实际上只会简单的忽略这些属性。下面介绍的所有的排版对象都有基础的排版功能所需要的那些属性,譬如AlignmentToParent、Margin、InternalMargin、PreferredMinSize、MinSizeLimitation等这些属性,所以在这里我只会讲每个对象独有的功能。
GuiStackComposition 的属性
Direction
Direction的意义很简单。作为一个Stack,自然会有生长的方向。因此分别提供从左到右、从右到左、从上到下、从下到上的生长方向也是很自然的,参考 GuiStackComposition::Direction 。
Padding
Padding指的是每一个StackItem之间的间距。其实我们总是可以使用StackItem的InternalMargin,或者调节StackItem里面的对象的Margin或者AlignmentToParent来模拟这一属性。但是当你需要在每一个StackItem之间都插入相同的空间的时候,这样做无疑是很浪费时间的,因此Stack就提供了这样的一个属性。
ExtraMargin
ExtraMargin跟InternalMargin很接近,但是他只对StackItem起作用(也就是说,你可以把不是StackItem的东西放进Stack,那么这个时候,相对于这个对象,Stack就变成了一个普通的GuiBoundsComposition)。当确定了所有的StackItem需要的空间之后,ExtraMargin会在所有StackItem的总体的周围留下这么大的空间,让Stack本身变大。
IsStackItemClipped 和 EnsureVisible函数
在Stack并没有要求要把自己变大到足够放下所有子对象的前提下,StackItem是有可能会因为Stack不够大而看不见的。因此IsStackItemClipped函数会告诉你这种情况到底发生了没有。而且就算发生了也没关系,因为EnsureVisible函数可以让所有的StackItem根据Direction的要求进行滑动,使得你喜爱的其中一个StackItem被显示出来。说到这里可能很多人都不明白为什么要有这样的功能 —— 其实很简单,Tab控件就有这个要求。
GuiStackItemComposition 的属性
ExtraMargin
StackItem也有一个ExtraMargin属性。但是这个属性跟Margin不一样的地方在于,Stack并不会去理会StackItem的ExtraMargin属性的值。Stack会先告诉每一个StackItem他们应该被放到哪里,然后最终处理StackItem的位置的时候,会根据ExtraMargin变大一点。当然当你的ExtraMargin比Stack的Padding还要大的时候,你的StackItem就会跟别的StackItem有交叉。配合 GuiGraphicsComposition::MoveChild函数,那么Tab控件的需求就被完全满足了 —— 点中的TabHeader不仅会变大,而且还会总是在最上面,挡住旁边的两个TabHeader。
GuiFlowComposition 的属性
Axis
这个属性的值是一个 GuiAxis 对象。虽然这个对象看起来很复杂,但是我们在使用的时候只需要关心它的构造函数。Stack是一维的,但是Flow是二维的,因此生长方向自然就会有8个,所以GuiAxis构造函数就需要你填入一个 AxisDirection 枚举结构的值。
RowPadding
RowPadding是虚拟行的行距。不过这里的行是Axis属性规定的Y轴方向的间距。根据设置的不同,所以这个虚拟的行也可能是现实中的列。
ColumnPadding
ColumnPadding是虚拟列的列距。不过这里的行是Axis属性规定的X轴方向的间距。根据设置的不同,所以这个虚拟的列也可能是现实中的行。
ExtraMargin
ExtraMargin跟Stack的ExtraMargin意思完全一致,在此不再赘述。
Alignment
Alignment指的是当一个虚拟行已经放不下更多的FlowItem,但是他还有空间的时候,要怎么处理。当然我们会有(虚拟的)左对齐、居中和扩展这三种方法,所以我提供了 FlowAlignment 这一个枚举类型。
GuiFlowItemComposition 的属性
ExtraMargin
ExtraMargin跟StackItem的ExtraMargin意思完全一致,在此不再赘述。
FlowOption
FlowOption指的是计算基线的方法。不同的FlowItem可以使用不同的基线,从而使得内容在逻辑上被真正的对齐。举个例子,你需要放很多按钮,但是其中一个按钮可能下面会有一点装饰,那么这个时候你就需要抬高一下相应的FlowItem的基线,使得对齐的是按钮,从而装饰就显示在正一行的下面。
在这里需要指出,当FlowOption::baseLine的值是Percentage的时候,基线是是从上往下计算的。因此你要底部对齐,percentage属性就要写1.0。
GuiTableComposition 的属性
Rows和Columns
Table的Rows和Columns属性都是 GuiCellOption 的值的列表。当你使用C++设置这个值的时候,你需要首先调用 GuiTableComposition::SetRowsAndColumns 函数告诉Table一共有多少行多少列,然后调用 GuiTableComposition::SetRowOption 和 GuiTableComposition::SetColumnOption 去指定具体的值。
GuiCellOption的composeType属性可以设置,这一行或者列要使用内容的最小值、固定的大小或者是占用Table空间的百分比来构成。这里需要注意的是,Table会首先排除composeType是MinSize和Absolute的那些行和列占用的空间,剩下的部分才分配给composeType是Percentage的行和列。
因此如果你需要讲一个按钮居中在窗口的中间的话,你就可以简单地通过设置这些属性来完成:
<Table AlignmentToParent="left:0 top:0 right:0 bottom:0">
<att.Rows>
<CellOption>composeType:Percentage percentage:0.5</CellOption>
<CellOption>composeType:MinSize</CellOption>
<CellOption>composeType:Percentage percentage:0.5</CellOption>
</att.Rows>
<att.Columns>
<CellOption>composeType:Percentage percentage:0.5</CellOption>
<CellOption>composeType:MinSize</CellOption>
<CellOption>composeType:Percentage percentage:0.5</CellOption>
</att.Columns>
<Cell Site="row:1 column:1>
<Button Text="Click Me!"/>
</Cell>
</Table>
所有percentage加起来并没有要求一定要是1,所以你就算写是三个2,那也跟三个0.1是一样的 —— 每一个占用1/3的空间。
CellPadding
CellPadding指的是Table的外边框和内边框的大小。也就是说除了行距和列距以外,Table本身还会放大CellPadding这么大的地方,让Cell和Table本身也有一个距离。
GuiCellComposition 的属性
Site
Site属性分别有四个值:row、column、rowSpan和columnSpan,用来指定一个Cell在Table中到底占用了哪些格子。不同的Cell之间不能重叠,但是一个Cell可以占用多个格子。举个例子:如果你需要在一个窗口里面放一个文本框,然后右下角有OK和Cancel两个按钮的话,我们自然可以想到需要使用2×3的表格来做。使用GacUI当然可以简单地做到:
<Table AlignmentToParent=left:0 top:0 right:0 bottom:0" CellPadding="5">
<att.Rows>
<CellOption>composeType:Percentage percentage:0.5</CellOption>
<CellOption>composeType:MinSize</CellOption>
</att.Rows>
<att.Columns>
<CellOption>composeType:Percentage percentage:0.5</CellOption>
<CellOption>composeType:MinSize</CellOption>
<CellOption>composeType:MinSize</CellOption>
</att.Columns>
<Cell Site="row:0 column:0 columnSpan:3">
<MultilineTextBox>
<att.BoundsComposition-set AlignmentToParent="left:0 top:0 right:0 bottom:0"/>
</MultilineTextBox>
</Cell>
<Cell Site="row:2 column:1">
<Button Text="OK">
<!-- 当文字变得很多的时候,按钮会自动变大,但是最小会保持30×100的大小 -->
<att.BoundsComposition-set PreferredMinSize="x:100 y:30"/>
</Button>
</Cell>
<Cell Site="row:2 column:1">
<Button Text="Cancel">
<att.BoundsComposition-set PreferredMinSize="x:100 y:30"/>
</Button>
</Cell>
</Table>
尾声
GacUI其实还可以通过把Control和Composition放进一个富文本文档里面来进行排版。不过由于篇幅限制,这个内容我将在介绍富文本文档的时候一并说明。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。