UnderStanding Layout in WPF
布局理念
WPF程序的布局是由所选择的布局容器决定的。"理想"的WPF窗口满足以下原则:
- Elements(比如控件)的大小不应该显示地设定具体值,而应该随着其内容的大小而增大或者缩小。例如如果按钮上面的文本变多,按钮应该自动增大以装下这些文字
- Elements的位置不应该是通过指定其相对屏幕的位置而确定的,而是应该由其容器(container)结合控件的大小、顺序等属性来确定
- 布局容器(Layout Container)将自己的"可用空间"分享给其Children
- 布局容器可以嵌套。一般典型的WPF布局有一个Grid控件开始,内部再叠加其他的控件。
布局过程
WPF布局分两步进行:测量步骤和分排步骤。测量过程中,WPF Container 遍历其每一个子Element,获得其每个控件理想的大小,分排步骤中,WPF Container将每个子Element放到适当的位置上。所以,最后并不一定所有的控件都能获得其理想大小大小的空间(如容器的总大小不够大的情况,这个时候Container会把超出总空间的Elment截断,可以通过设定最小窗口大小来避免Element被截断)
布局容器
前面说到,WPF中的布局是通过布局容器实现的。那么布局容器是什么样子的呢?WPF中的所有布局容器都是从System.Controls.Panel这个抽象类派生而来的
Panel类的Public Properties | ||||||||
|
Panel是所有布局容器的起点,但是它本身不能作为容器使用。实际使用的是更具体的派生类。下表列出了常用的一些核心布局容器
核心布局Panel | ||||||||||||
|
除这些核心布局Panel之外,有时候还会用到一些用于布局特殊控件的一些Container,例如TabPanel、ToolbarPanel、ToolbarOverflowPanel,还有VisualizingStackPanel、InkCanvas等
Simple Layout With the StackPanel
StackPanel 是最简单的布局容器之一,例如以下代码:
<Window x:Class="Layout.MainWindow" xmlns= xmlns:x= Title="MainWindow" Height="223" Width="354"> <StackPanel> <Label>A Button Stack</Label> <Button>Button 1</Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> </StackPanel></Window>
默认情况下,StackPanel将其children从上到下排列,每个child的高度以能装下这个child内容的最小高度确定,而宽度则被拉伸到整个StackPanel的宽度。所以上面代码的运行结果如下图:
StackPanel也可以是横向排布的,例如,如果将上面的<StackPanel>换成:
则结果如下图所示:
这个时候,每个child的宽度由能显示其内容的最小宽度决定,而高度则被拉伸到整个StackPanel。
布局属性——"被布局者"的发言权
虽然排布是由Container决定的,但是child本身也有一定的发言权。实际上,layout panel在进行排布的时候会将children的以下属性也加入到综合考虑的范围之内:
Layout Properties | ||||||||||||
|
不同的布局Container还会赋予其Chidren不同的附加属性,例如Grid可以让其Chidren设定Grid.Row/Grid.Column,这些附加属性用来帮助child告诉Container自己对于排布的信息,但是上表中的属性是共有的。
HorizontalAlignment 与 VerticalAlignment
将StackPanel的第一个例子稍作修改,就可以看到布局属性是如何工作的,例如:
结果如下:
Margin
上例中一个明显的问题是element一个挨着一个,显得拥挤。好的布局还必须在element之间保留一定的空隙——Margin就是干这个的!
设置Margin属性的时候,可以用一个值设定上下左右四边都留出同样多的空隙,如:
也可以四个方向分别设定,如:
在代码里面,通过Thickness结构体设定Margin属性,如:
cmd.Margin = new Thickness(5, 10, 5, 10);
对上图做出如下修改:
结果如下:
Minimum/Maximum/and Explicit sizes
所有的element都包含Height/Width属性,用来显示的指定其尺寸。但是,一般来说都不需要,在好的布局系统中,也不应该这样做。当我们想这样做的时候,可以设定Mininum或者Maximum代替,这样的布局系统更稳健、智能。
当StackPanel决定一个按钮的大小的时候,会考虑这样几个方面的因素:
- 最小值:每个按钮必须最小跟这个值一样大;
- 最大值:每个按钮都必须小于或者等于这个值;
- 内容:如果按钮的内容要求更大的尺寸的话,那么StackPanel会尽量满足,但是要在最大值的限制之下
- 容器的大小:如果按钮设定的最小宽度大于StackPanel的宽度,那么这个按钮会被截断而只显示出一部分;其他情况下,按钮宽度不可能超出StackPanel
- HorizontalAlignment:如果使用的是Stretch,那么会尽量增大宽度
例如下面的代码:
效果如下,左图是减小窗口大小之后的结果
上例中,我们还是将最上层的Window的尺寸直接设定了具体的值,这样做是可以接受的。如果要将最上层的窗体的大小也变成自动,并且可以根据内容的多少动态调整,那么可以将Windows的Height/Width值删掉,然后设定Window.SizeToContent属性为 WidthAndHeight,或者设置为Height或Width
Border
Border并不是布局Panel之一。Border只能有一个内容(通常是一个布局Panel),并且在这个element的周围加上背景或者边框。掌握Border,只需要下表中的几个Border的属性就够了:
Background | 用一个Brush对象设置一个出现在整个Content的背景,可以使用单一颜色也可以使用其他东西 |
BorderBrush/BorderThickness | 设置出现在边框边缘的背景颜色,要使之可见,必须同时设置这两个值 |
CornerRadius | 圆角边框,double数值,值越大,圆角越明显 |
Padding | 边框和内容之间的空隙,与Margin不同的是,Margin在外部留空隙,而Padding在内部留空隙 |
Border实际上是一个Decorator,见以后的具体分析。
The WrapPanel and DockPanel
the WrapPanel
WrapPanel将控件一行一行排列,WrapPanel.Orientation默认设置为Horizontal,如果设置为Vertical的话,将一列一列的排列,看例子:
效果如下:
the DockPanel
DockPanel很有意思,它将控件在某个方向拉伸,而另一个方向上维持原有的尺寸。例如,如果将一个按钮Dock到DockPanel的上方,那么这个按钮将被拉伸到跟DockPanel中剩余空间一样宽,然后移动到剩余空间的顶部
看例子:
效果:
其中LastChildFill表示将最后的剩余空间赋给最后一个Child
嵌套布局控件
上述StackPanel/DockPanel/WrapPanel很少单独使用,一般会多层嵌套,例如以下是一个对话框的例子:
text to show
效果:
当我们的窗口里面有很多层嵌套的时候,可能会比较Confusing,这个时候可以使用视图->其他视图->文档大纲来浏览嵌套布局
The Grid
Grid是WPF中最强大的布局容器。
Grid将Elements放置到不可见的格子中,通常一个格子中放置一个元素(如果有多个的话就会重叠),看下面的例子:
效果:
Fine-tuning rows and columns
上面的例子中没有具体指定不同行或者列的大小,默认都是等比例的。实际上,Grid支持三种形式的尺寸值:
- 绝对值:绝对大小,例如100
- 自动值:显示该行或者该列的内容所需,方法是设置行高或者列宽为auto
- 比例值:所有设置为比例值的行或者列,按照设定比例分配剩余空间,例如1*、2*等
layout rounding
由于WPF使用的是独立于分辨率的单位,所以有时候会出现非整数像素位置的情况,例如整个Grid的宽度为200,分成两列,宽度比例为1:2,这个时候就得分出66.6像素,可能会导致屏幕显示模糊。
解决办法是设置Grid的UseLayoutRounding="True"。
spanning rows and columns
有时候需要设置某些元素横跨(或纵跨)多个列(行),这个时候可以设置Grid.ColumnSpan或者Grid.RowSpan属性
Splitting windows
所有的Windows用户都见过分隔条:将窗口分成几个区块,并且可以通过拖动分隔条改变区块的大小。
在WPF中,分隔条是Grid的功能,通过GridSpliter类实现。使用方法是:
- GridSpliter必须被放置在Grid的其中一个格子中。这个格子中可以已经有其他的Element,这个时候需要设置好Margin,使得这两者不会重叠。更好的做法是在Grid中单独预留出一个格子(更多的时候是预留出一行或者一列)来放置GridSpliter
- GridSpliter改变的是所有行或者所有列的大小,而不是单个Cell。
- 初始情况下,GridSpliter的尺寸非常小,所以如果想要使其可见,需要设置一个最小尺寸,并且如果想要横向拖动,需要设置VerticalAlignment为Stretch,如果想要纵向拖动,需要设置HorizontalAlignment为Stretch
- GridSpliter的Alignment同时也决定了它是横向的还是纵向的,如果希望是横向的(可纵向拖动),那么设置VerticalAlignment为Center,如果希望是纵向的(可横向拖动),那么需要设置HorizontalAlignment为Center
一个Grid中通常最多只可以有一个GridSpliter,但是可以在Grid的某个Cell中嵌套另一个Grid,然后在嵌套Grid中加入GridSpliter,但是这个时候GridSpliter改变的只是其直接所属的Grid的行或列尺寸。
shared size groups
如前面所属,Grid中的行高列宽可以通过三种不同的方式赋值:绝对值、比例值、自动值。还有另一种方法来设定尺寸:与另一个行或者列匹配。这是通过Shared-size Group功能来实现的。方法是对需要共享大小的行或者列同时设置SharedSizeGroup属性,并且使用匹配的字符串作为组名。
另外还有一个细节上的问题:Shared-size group的名称可见范围。可以认为Shared-size Group是在单个Window中可见的,而且WPF还额外要求使用Shared-size group的Grid必须将其 Grid.IsSharedSizeGroupScope为True。
the UniformGrid
UniformGrid打破了目前位置我们对Grid的所有固有知识:不需要对行列进行分别设置,只需要设置行数或者列数,所有的行都是等高的,所有列都是等宽的
Coordinate-Based Layout with the Canvas
Cavas是WPF中最轻量级的布局容器,因为它没有复杂的布局逻辑,只是按照指定的坐标放置elements,方法是设置附加属性Cavas.Top或者Canvas.Left
Z-Order
如果有超过一个会发生重叠的元素,可以设置附加属性Grid.Zindex,控制其重叠方法。默认情况下所有的element都属于同一层(Grid.Zindex=0),设置ZIndex之后,高ZIndex值的元素总是在低ZIndex元素的上方
The InkCanvas
InkCanvas的主要目的是支持触控