浅谈 WPF布局
我们首先来了解一下图形化用户界面(Graphic User Interface)也就是我们常常听到的GUI。举个简单的例子,同样是数据,我们可以用控制台程序加格式控制符等输出,但是这些都不如GUI来的友好和方便。
WPF相对于其它只能使用编程语言进行UI设计,具有专门用于UI设计的XAML,并且能够确保界面布局能恰倒好处的适应不同的窗口尺寸。
我们来查看Window和Page的源码,发现Window的间接基类ContentControl和Page类都使用了一个object类型的Content属性。所以它只能包含一个元素,但是我们想放置多个元素怎么办呢?这个时候我们就需要使用布局容器。
为什么布局容器又可以添加多个元素呢?所有的WPF布局容器都派生自System.Windows.Controls.Panel这个抽象类。我们从类层次结构图中就不难发现它所包含的一系列布局容器。我们再来看看这个Panel类的源代码,就会发现该类有一个ContentProperty("Children")特性,所以我们会在该类中找到一个UIElementCollection类型(该类实现了IList接口)的Children属性。只要是继承自UIElement的类,都可以添加到布局容器中。
下面让我们来看看其中的一些布局容器以及与之相关联的控件属性。
StackPanel
它是最简单的布局容器,允许子元素在单行或者单列以堆栈的形式放置。
<StackPanel Orientation="Vertical" Background="LightCyan"> <Label Background="LemonChiffon">A Button Stack</Label> <Button>Button A</Button> <Button>Button B</Button> <Button>Button C</Button> </StackPanel>
我们运行会发现,StackPanel容器会充满整个窗口,因为它的HorizontalAlignment、VerticalAlignment属性值都是Stretch。
子元素是按照自上而下的方式排列的,因为我们设置了Orientation属性(默认值就是Vertical,所以这里我们可以不设置该属性);如果设置成Horizontal,就会从左到右排列。在默认情况下,每个子元素的高度都适合与它们自己内容的高度。如果是Horizontal,每个子元素的宽度都适合它们自己内容的长度。
我们拉伸运行的WPF程序,会发现StackPanel始终会充满整个窗口,子元素也会根据设置做相应的变化。
我们可以设置子元素的一些属性配合布局容器的属性,来决定布局。常见的属性有:HorizontalAlignment、VerticalAlignment、Margin、MinWidth、MinHeight、MaxWidth、MaxHeight、Height、Width。
<Window x:Class="WPFDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="200" Height="350"> <StackPanel Background="LightCyan"> <Label Background="LemonChiffon">A Button Stack</Label> <Button HorizontalAlignment="Left">Button A</Button> <Button HorizontalAlignment="Center">Button B</Button> <Button HorizontalAlignment="Right">Button C</Button> <Button Margin="5" Height="40" Width="100" HorizontalAlignment="Right">Button D</Button> <Button MinWidth="320">Button E</Button> </StackPanel> </Window>
运行这个程序,我们就会发现A、B、C三个Button分别位于水平方向上的不同位置;Button D距离四周的元素有5个单位的距离,并且指定了该按钮的高度和宽度;Button E指定了MinWidth,远大于Window的200,所以刚运行的时候左边被截断,需要我们通过拉伸才可完全看见。
Grid
顾名思义,Grid会以网格的形式对内容元素们进行布局。它具有如下的特点:
· 可以通过RowDefinitions和ColumnDefinitions属性,它们分别是RowDefinition和ColumnDefinition集合。定义任意多的行和列。
· 行的高度,列的宽度可以使用绝对数值(double数值加单位后缀px、in、cm、pt,不过在设置的时候最好不要加单位),比例值(double数值后加一个*号),自动值(字符串Auto)来灵活设置。
· 内部元素可以使用Grid的附加属性Grid.Row、Grid.Column、Grid.RowSpan、Grid.ColumnSpan设置自己所在的行、列、纵向跨几行、横向跨几列。
· 可以设置子元素的对齐方向。
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" ShowGridLines="True" Width="250" Height="100"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*" /> <RowDefinition Height="1.5*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock FontSize="20" FontWeight="Bold" Grid.ColumnSpan="3" Grid.Row="0">2013 Products Shipped</TextBlock> <TextBlock FontSize="12" FontWeight="Bold" Grid.Row="1" Grid.Column="0" HorizontalAlignment="Center">Quarter 1</TextBlock> <TextBlock FontSize="12" FontWeight="Bold" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center">Quarter 2</TextBlock> <TextBlock FontSize="12" FontWeight="Bold" Grid.Row="1" Grid.Column="2" HorizontalAlignment="Center">Quarter 3</TextBlock> <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center">50000</TextBlock> <TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">100000</TextBlock> <TextBlock Grid.Row="2" Grid.Column="2" VerticalAlignment="Center">150000</TextBlock> <TextBlock FontSize="16" FontWeight="Bold" Grid.ColumnSpan="3" Grid.Row="3">Total Units: 300000</TextBlock> </Grid>
在这个示例中,我们还设置了ShowGridLines属性,它会生成虚线的边框。我们可以修改该边框,具体可以查看陈希章老师的这篇文章:为WPF和Silverlight的Grid添加边框。
我们还可以使用UseLayoutRounding属性来控制布局舍入。
我们还可以使用GridSplitter来拖动分割窗口。
<Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="200"></ColumnDefinition> </Grid.ColumnDefinitions> <Button>1</Button> <Button Grid.Column="2">2</Button> <Button Grid.Row="2">3</Button> <Button Grid.Column="2" Grid.Row="2">4</Button> <GridSplitter Grid.Column="1" Grid.Row="0" Grid.RowSpan="3" Width="2" VerticalAlignment="Stretch" HorizontalAlignment="Center" ShowsPreview="True"></GridSplitter> </Grid>
这是一个左右拖动的分割窗口,所以我们把GridSplitter竖立放置铺满(VerticalAlignment="Stretch"),并跨行(Grid.RowSpan="3"),还要设置它的宽度Width(这样才可见);最后还要设置列的宽度为自动值Auto。我们还看到ShowsPreview属性为True,这个是当我们拖动分割条时,会有一个会射的阴影跟随鼠标运行,以显示预览。同理,我可以设置上下拖动的分割线,这里就不过多叙述了。
还有一个比较有意思的是SharedSizeGroup特性。
<Grid Margin="3"> <StackPanel> <StackPanel Grid.IsSharedSizeScope="True"> <Grid Grid.Row="0" Margin="3" Background="LightYellow" ShowGridLines="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="TextLabel"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Label Margin="5">A very long bit of text</Label> <Label Grid.Column="1" Margin="5">More text</Label> <TextBox Grid.Column="2" Margin="5">A text box</TextBox> </Grid> <Label Grid.Row="1" >Some text in between the two grids...</Label> <Grid Grid.Row="2" Margin="3" Background="LightYellow" ShowGridLines="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="TextLabel"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Label Margin="5">Short</Label> <TextBox Grid.Column="1" Margin="5">A text box</TextBox> </Grid> </StackPanel> </StackPanel> </Grid>
我们可以设置需要共享尺寸的列ColumnDefinition或者行RowDefinition的SharedSizeGroup属性,把他们都设置成同一个值;然后再把Grid的IsSharedSizeScope附加属性设置到父容器上,并设置为True。(本例嵌套了2个StackPanel就是为了说明IsSharedSizeScope的设置方式,其实完全没有必要嵌套2个StackPanel,直接就可以设置在Grid上)
还有一个更简单的网格布局容器,UniformGrid,它可以直接设置Rows、Columns属性,设置行列数;它没有Rows、Columns这些附加属性,是按照子元素的添加的顺序由左到右,由上到下排列的。
Canvas
直译就是画布的意思。它通常用于一些设计基本不会再有改动的小型布局。
Canvas布局容器是最轻量级的布局容器,它没有包含负责的布局逻辑,以改变其子元素的首选尺寸。
我们可以设置Canvas.Left、Canvas.Top(或者Canvas.Right、Canvas.Buttom)附加属性来定位子元素的位置。
<Canvas> <Button Canvas.Left="10" Canvas.Top="10">(10,10)</Button> <Button Canvas.Left="120" Canvas.Top="30">(120,30)</Button> <Button Canvas.Left="60" Canvas.Top="80" Width="50" Height="50" Canvas.ZIndex="1">(60,80)</Button> <Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50">(70,120)</Button> <Button Canvas.Left="80" Canvas.Top="150" Width="100" Height="50">(60,80)</Button> </Canvas>
默认情况下,所有子元素的都具有相同的ZIndex属性值0;如果子元素的ZIndex值相同,就按照他们在Canvas中添加的先后顺序进行显示。所以上一个示例中,最后一个Button会显示在倒数第二个Button上面;第三个因为我们显示设置了ZIndex的值为1,大于默认值0,所以第三个在第四个上面。
这一节我们讲了3个非常具有代表性的布局容器,当然需要做好布局是一件非常困难的事情,需要考虑全面。所以还需要我们真正动手做的时候,不断的积累经验。这里只是一个简单的介绍布局容器。下一节,我们会来介绍一下WPF的控件。