代码改变世界

WPF布局

2013-04-28 15:52  小汪quant  阅读(1874)  评论(0编辑  收藏  举报

UnderStanding Layout in WPF

布局理念

WPF程序的布局是由所选择的布局容器决定的。"理想"的WPF窗口满足以下原则:

  1. Elements(比如控件)的大小不应该显示地设定具体值,而应该随着其内容的大小而增大或者缩小。例如如果按钮上面的文本变多,按钮应该自动增大以装下这些文字
  2. Elements的位置不应该是通过指定其相对屏幕的位置而确定的,而是应该由其容器(container)结合控件的大小、顺序等属性来确定
  3. 布局容器(Layout Container)将自己的"可用空间"分享给其Children
  4. 布局容器可以嵌套。一般典型的WPF布局有一个Grid控件开始,内部再叠加其他的控件。

布局过程

WPF布局分两步进行:测量步骤和分排步骤。测量过程中,WPF Container 遍历其每一个子Element,获得其每个控件理想的大小,分排步骤中,WPF Container将每个子Element放到适当的位置上。所以,最后并不一定所有的控件都能获得其理想大小大小的空间(如容器的总大小不够大的情况,这个时候Container会把超出总空间的Elment截断,可以通过设定最小窗口大小来避免Element被截断)

布局容器

前面说到,WPF中的布局是通过布局容器实现的。那么布局容器是什么样子的呢?WPF中的所有布局容器都是从System.Controls.Panel这个抽象类派生而来的

Panel类的Public Properties

名称

描述

Background

The brush that is used to paint the panel background.You must set this property to a non-null value if you want to receive mouse events

Children

The collection of items that is stored in the panel.This is the first level of items,and these items may themselves contain more items

IsItemsHost

A Boolean value that is true if the panel is being used to show the items that are associated with an ItemsControl.Most of time you won't even be aware that a list control is using a behind-the-scenes panel to manage the layout of its items.However,the detail becomes more important if you want to create a customized list that lays out children in a different way.

 

Panel是所有布局容器的起点,但是它本身不能作为容器使用。实际使用的是更具体的派生类。下表列出了常用的一些核心布局容器

核心布局Panel

StackPanel

Place elements in a horzontal or vertical stack.it is typically used for small sections of a larger,more complex window

WrapPanel

Place elments in a series of wrapped lines.In horizontal orientation,the WrapPanel lays items out in a row from left to right and then onto subsequent lines.In vertical orientation,the WrapPanel lays out items in a top-to-bottom column and then uses addictional columns to fit the remaining items.

DockPanel

Aligns elements against an entire edge of the container

Grid

Place elments in a invisible table but forces all cells to have the same size.This is one of the most flexible and commonly used layout container.

UniformGrid

Grid that forces all cells to have the same size.

Canvas

Allow elements to be positioned absolutely by using fixed coordinates.

除这些核心布局Panel之外,有时候还会用到一些用于布局特殊控件的一些Container,例如TabPanel、ToolbarPanel、ToolbarOverflowPanel,还有VisualizingStackPanel、InkCanvas等

 

Simple Layout With the StackPanel

StackPanel 是最简单的布局容器之一,例如以下代码:

<Window x:Class="Layout.MainWindow"
        xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
        xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        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>换成:

<StackPanel Orientation="Horizontal">
								

则结果如下图所示:

这个时候,每个child的宽度由能显示其内容的最小宽度决定,而高度则被拉伸到整个StackPanel。

布局属性——"被布局者"的发言权

虽然排布是由Container决定的,但是child本身也有一定的发言权。实际上,layout panel在进行排布的时候会将children的以下属性也加入到综合考虑的范围之内:

Layout Properties

HorizontalAlignment

当水平方向有多余空间的时候如何排布对象,可以取四种值:靠左、靠右、居中、拉伸

VerticalAlignment

类似HorizontalAlignment,当竖直方向有剩余空间的时候如何排布

Margin

在element的四周加上一定的空白,取值为Thickness结构体

MinWidth / MinHeight

设置最小值,如果某个element对容器来说太大了,就会被裁减掉

MaxWidth / MaxHeight

设置最大值,设置此值之后,即使设置了HorizontalAlignment/VerticalAlignment为拉伸并且有多余空间的时候,element的大小也不会超过设定的值

Width / Height

显示的直接设定element的大小,这个属性会覆盖掉HorizontalAlignment或者VerticalAlignment设定的拉伸值,但是只有这个值在设定的MinWidth/MaxWidth 或者 MinHeight/MaxHeight之间的时候才有效

不同的布局Container还会赋予其Chidren不同的附加属性,例如Grid可以让其Chidren设定Grid.Row/Grid.Column,这些附加属性用来帮助child告诉Container自己对于排布的信息,但是上表中的属性是共有的。

HorizontalAlignment 与 VerticalAlignment

将StackPanel的第一个例子稍作修改,就可以看到布局属性是如何工作的,例如:

    <StackPanel>
        <Label>A Button Stack</Label>
        <Button HorizontalAlignment="Left">Button 1</Button>
        <Button HorizontalAlignment="Right">Button 2</Button>
        <Button HorizontalAlignment="Center">Button 3</Button>
        <Button>Button 4</Button>
    </StackPanel>

结果如下:

Margin

上例中一个明显的问题是element一个挨着一个,显得拥挤。好的布局还必须在element之间保留一定的空隙——Margin就是干这个的!

设置Margin属性的时候,可以用一个值设定上下左右四边都留出同样多的空隙,如:

<Button Margin="5">Button 4</Button>

 

也可以四个方向分别设定,如:

<Button Margin="5,10,5,10">Button 4</Button>

 

在代码里面,通过Thickness结构体设定Margin属性,如:

cmd.Margin = new Thickness(5, 10, 5, 10);

对上图做出如下修改:

    <StackPanel Margin="3">
        <Label Margin="3" HorizontalAlignment="Center">A Button Stack</Label>
        <Button Margin="3" HorizontalAlignment="Left">Button 1</Button>
        <Button Margin="3" HorizontalAlignment="Right">Button 2</Button>
        <Button Margin="3" HorizontalAlignment="Center">Button 3</Button>
        <Button Margin="3">Button 4</Button>
    </StackPanel>

结果如下:


				

 

Minimum/Maximum/and Explicit sizes

所有的element都包含Height/Width属性,用来显示的指定其尺寸。但是,一般来说都不需要,在好的布局系统中,也不应该这样做。当我们想这样做的时候,可以设定Mininum或者Maximum代替,这样的布局系统更稳健、智能。

当StackPanel决定一个按钮的大小的时候,会考虑这样几个方面的因素:

  1. 最小值:每个按钮必须最小跟这个值一样大;
  2. 最大值:每个按钮都必须小于或者等于这个值;
  3. 内容:如果按钮的内容要求更大的尺寸的话,那么StackPanel会尽量满足,但是要在最大值的限制之下
  4. 容器的大小:如果按钮设定的最小宽度大于StackPanel的宽度,那么这个按钮会被截断而只显示出一部分;其他情况下,按钮宽度不可能超出StackPanel
  5. HorizontalAlignment:如果使用的是Stretch,那么会尽量增大宽度

例如下面的代码:

    <StackPanel Margin="3">
        <Label Margin="3" HorizontalAlignment="Center" MinWidth="120">A Button Stack</Label>
        <Button Margin="3" MinWidth="120" MaxWidth="240" HorizontalAlignment="Center">Button 1</Button>
        <Button Margin="3" MinWidth="120" MaxWidth="240" HorizontalAlignment="Left">Button 2</Button>
        <Button Margin="3" MinWidth="120" MaxWidth="240">Button 3</Button>
        <Button Margin="3" MinWidth="120" MaxWidth="240">Button 4</Button>
    </StackPanel>

效果如下,左图是减小窗口大小之后的结果

上例中,我们还是将最上层的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 BorderBrush="Blue" BorderThickness="2" Margin="5" Padding="3" VerticalAlignment="Top">
        <StackPanel>
            <Label Margin="3" HorizontalAlignment="Center" MinWidth="120">A Button Stack</Label>
            <Button Margin="3" MinWidth="120" MaxWidth="240" HorizontalAlignment="Center">Button 1</Button>
            <Button Margin="3" MinWidth="120" MaxWidth="240" HorizontalAlignment="Left">Button 2</Button>
            <Button Margin="3" MinWidth="120" MaxWidth="240">Button 3</Button>
            <Button Margin="3" MinWidth="120" MaxWidth="240">Button 4</Button>
        </StackPanel>
    </Border>

 

Border实际上是一个Decorator,见以后的具体分析。

 

The WrapPanel and DockPanel

the WrapPanel

WrapPanel将控件一行一行排列,WrapPanel.Orientation默认设置为Horizontal,如果设置为Vertical的话,将一列一列的排列,看例子:

    <WrapPanel Margin="5">
        <Button VerticalAlignment="Top">Top Button</Button>
        <Button VerticalAlignment="Stretch" MinHeight="80">Tall Button</Button>
        <Button VerticalAlignment="Bottom">Bottom Button</Button>
        <Button>Stretch Button</Button>
        <Button VerticalAlignment="Center">Centered Button</Button>        
    </WrapPanel>

效果如下:

 

the DockPanel

DockPanel很有意思,它将控件在某个方向拉伸,而另一个方向上维持原有的尺寸。例如,如果将一个按钮Dock到DockPanel的上方,那么这个按钮将被拉伸到跟DockPanel中剩余空间一样宽,然后移动到剩余空间的顶部

看例子:

    <DockPanel LastChildFill="True">
        <Button DockPanel.Dock="Top">Top</Button>
        <Button DockPanel.Dock="Bottom">Bottom</Button>
        <Button DockPanel.Dock="Left">Left 1</Button>
        <Button DockPanel.Dock="Left">Left 2</Button>
        <Button DockPanel.Dock="Right">Right</Button>
        <Button>Remaining Space</Button>
    </DockPanel>

效果:

其中LastChildFill表示将最后的剩余空间赋给最后一个Child

嵌套布局控件

上述StackPanel/DockPanel/WrapPanel很少单独使用,一般会多层嵌套,例如以下是一个对话框的例子:

    <DockPanel LastChildFill="True">
        <StackPanel DockPanel.Dock="Bottom" HorizontalAlignment="Right" Orientation="Horizontal" Margin="5">
            <Button Margin="5">OK</Button>
            <Button Margin="5">Cancel</Button>
        </StackPanel>
        <TextBox Margin="5">text to show</TextBox>
    </DockPanel>

效果:

当我们的窗口里面有很多层嵌套的时候,可能会比较Confusing,这个时候可以使用视图->其他视图->文档大纲来浏览嵌套布局

The Grid

Grid是WPF中最强大的布局容器。

Grid将Elements放置到不可见的格子中,通常一个格子中放置一个元素(如果有多个的话就会重叠),看下面的例子:

    <Grid ShowGridLines="True">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Button>Upper Left</Button>
        <Button Grid.Column="1" Grid.Row="1">Bottom Middle</Button>
        <Button Grid.Column="2">Upper Right</Button>
    </Grid>

效果:

Fine-tuning rows and columns

上面的例子中没有具体指定不同行或者列的大小,默认都是等比例的。实际上,Grid支持三种形式的尺寸值:

  1. 绝对值:绝对大小,例如100
  2. 自动值:显示该行或者该列的内容所需,方法是设置行高或者列宽为auto
  3. 比例值:所有设置为比例值的行或者列,按照设定比例分配剩余空间,例如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类实现。使用方法是:

  1. GridSpliter必须被放置在Grid的其中一个格子中。这个格子中可以已经有其他的Element,这个时候需要设置好Margin,使得这两者不会重叠。更好的做法是在Grid中单独预留出一个格子(更多的时候是预留出一行或者一列)来放置GridSpliter
  2. GridSpliter改变的是所有行或者所有列的大小,而不是单个Cell。
  3. 初始情况下,GridSpliter的尺寸非常小,所以如果想要使其可见,需要设置一个最小尺寸,并且如果想要横向拖动,需要设置VerticalAlignment为Stretch,如果想要纵向拖动,需要设置HorizontalAlignment为Stretch
  4. GridSpliter的Alignment同时也决定了它是横向的还是纵向的,如果希望是横向的(可纵向拖动),那么设置VerticalAlignment为Center,如果希望是纵向的(可横向拖动),那么需要设置HorizontalAlignment为Center

 

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="100"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition MinWidth="50"/>
        </Grid.ColumnDefinitions>
        
        <Button>Upper Left</Button>
        <Button Grid.Column="2">Upper Right</Button>
        <Button Grid.Row="1">Bottom Left</Button>
        <Button Grid.Row="1" Grid.Column="2">Bottom Right</Button>
        
        <GridSplitter Grid.Column="1" Grid.RowSpan="2" MinWidth="3" 
                      HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
    </Grid>

 

一个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的所有固有知识:不需要对行列进行分别设置,只需要设置行数或者列数,所有的行都是等高的,所有列都是等宽的

    <UniformGrid Rows="2" Columns="2">
        <Button>Upper Left</Button>
        <Button>Upper Right</Button>
        <Button>Bottom Left</Button>
        <Button>Bottom Right</Button>
    </UniformGrid>

 

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的主要目的是支持触控

Layout Examples

A Column of Settings

Daynamic Content

A Modular User Interface

The Last Word