布局系统

布局系统决定控件在界面上的位置和大小如何确定。Winform中我们通常用设计器把控件拖到绝对坐标上,再靠AnchorDock应付窗口缩放;WPF则采用一套容器自适应的布局模型,控件被放进各种布局面板(Panel)中,由面板负责计算每个子元素的位置和尺寸,窗口大小变化时面板自动重新排布。理解这套模型是写好WPF界面的关键。

布局原理:测量与排列

WPF的布局过程大致分为两个阶段:

Measure(测量):面板自上而下询问每个子元素“你想要多大空间”,子元素根据自己的内容和对齐方式汇报一个期望尺寸。

Arrange(排列):面板根据测量结果,给每个子元素分配最终的矩形区域,子元素在这个区域内渲染自己。

这两个阶段在窗口大小变化、内容变化时会自动重新执行,所以WPF界面天生就是自适应的,这与Winform需要手动设置AnchorDock才能勉强应付缩放有本质区别。

尺寸单位与尺寸值

WPF的尺寸使用的不是物理像素,而是设备无关单位DIU。如果你拿一把尺子去测量屏幕,你会发现不同缩放比例的屏幕下,一个DIU永远等于1/96英寸(约0.264mm,当然,现实可能会有一点误差,但这里是为了表达DIU的设计理念)。在高DPI屏幕上WPF会自动放大渲染,界面不会模糊或错位。

布局中尺寸值有三种写法:

写法 含义
数字 固定尺寸,如Width="100"
Auto 根据内容自动计算尺寸
* 按比例分配剩余空间,如*2*

其中Auto*主要用在Grid的行列定义中,是WPF布局的灵魂。

Grid网格布局

Grid是WPF中最重要、最常用的布局面板,它把区域划分成若干行和列,子元素通过附加属性指定自己落在哪一行哪一列。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="30" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="标题区" />
    <TextBlock Grid.Row="1" Grid.Column="1" Text="主内容区" />
</Grid>

上面的例子定义了3行2列,第一行高度随内容自适应,第二行吃掉所有剩余空间,第三行固定30,两列按1:2的比例分配宽度。

Grid.RowGrid.Column这类写在子元素上却属于面板的属性叫附加属性(Attached Property),它是WPF依赖属性系统的扩展能力,含义是子元素告诉Grid自己应该被放在哪里,不指定时默认为0。

跨行跨列使用Grid.RowSpanGrid.ColumnSpan,下面是一个例子。

<Button Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="横跨两列" />

此外,Grid还有一个实用特性,多个子元素放在同一行同一列时会叠在一起,后写的在上层,利用这一点可以实现层叠效果。

StackPanel堆叠面板

StackPanel把子元素沿一个方向依次排列,默认纵向排列(即Orientation="Vertical"),也可设为Horizontal横向排列。它不瓜分空间,而是每个子元素在排列方向上按内容大小排列。

<StackPanel Orientation="Vertical">
    <TextBlock Text="用户名:" />
    <TextBox />
    <TextBlock Text="密码:" />
    <PasswordBox />
</StackPanel>

StackPanel简单直接,适合表单字段、按钮组这类线性排列的场景,但它不支持按比例拉伸,复杂布局还得靠Grid

DockPanel停靠面板

DockPanel让子元素依次贴靠到某个边缘,子元素用附加属性DockPanel.Dock指定停靠方向,可取值LeftRightTopBottom,最后一个子元素默认填满剩余空间,这个行为可用LastChildFill="False"关闭。

<DockPanel>
    <Menu DockPanel.Dock="Top" Height="28" />
    <StatusBar DockPanel.Dock="Bottom" Height="24" />
    <TreeView DockPanel.Dock="Left" Width="200" />
    <TextBox AcceptsReturn="True" />
</DockPanel>

停靠的顺序就是元素书写的顺序,先写的先占边缘。DockPanel非常适合搭建经典的顶部菜单、底部状态栏、左侧导航、中间内容的主窗口骨架,相当于Winform中Dock属性的面板化实现。

WrapPanel流式面板

WrapPanelStackPanel类似,也是沿一个方向排列子元素,但一行(或一列)排满后会自动换行(或换列),类似文字的折行效果。

<WrapPanel Orientation="Horizontal">
    <Button Content="按钮1" Width="80" Margin="4" />
    <Button Content="按钮2" Width="80" Margin="4" />
    <Button Content="按钮3" Width="80" Margin="4" />
    <Button Content="按钮4" Width="80" Margin="4" />
    <Button Content="按钮5" Width="80" Margin="4" />
</WrapPanel>

我们可以将程序运行起来,当缩减窗口宽度时,放不下的按钮会自动折到下一行。WrapPanel适合工具栏按钮组、标签集合等数量不定、需要自动折行的场景。

Canvas绝对定位面板

Canvas是最接近Winform布局方式的面板,子元素用附加属性Canvas.LeftCanvas.Top(或RightBottom)指定绝对坐标,Canvas不做任何自适应计算。

<Canvas>
    <Ellipse Canvas.Left="20" Canvas.Top="20" Width="50" Height="50" Fill="Red" />
    <Rectangle Canvas.Left="80" Canvas.Top="60" Width="100" Height="60" Fill="Blue" />
</Canvas>

Canvas适合绘图、图表、游戏界面等需要精确控制坐标的场景,但它丧失了自适应能力,普通表单界面不推荐使用。

UniformGrid均分网格

UniformGrid是一个简化版的Grid,所有单元格大小完全相同,通过RowsColumns指定行列数,子元素按书写顺序依次填入单元格。

<UniformGrid Rows="2" Columns="2">
    <Button Content="A" Margin="4" />
    <Button Content="B" Margin="4" />
    <Button Content="C" Margin="4" />
    <Button Content="D" Margin="4" />
</UniformGrid>

它适合九宫格、计算器键盘这类规整的均分布局,UniformGrid中我们不需要写麻烦的RowDefinitions和附加属性,比Grid写起来简单。

对齐与边距

除了面板,控件自身的对齐和边距属性也参与布局。

属性 说明
HorizontalAlignment 水平对齐:Left/Center/Right/Stretch
VerticalAlignment 垂直对齐:Top/Center/Bottom/Stretch
Margin 外边距,可写一个值(四周相同)、两个值(左右、上下)或四个值(左、上、右、下)
Padding 内边距(内容控件才有),语法同Margin
<Button Content="居中" Width="100" Height="30"
        HorizontalAlignment="Center" VerticalAlignment="Center"
        Margin="0,10,0,0" />

HorizontalAlignmentStretch(默认值)且没有显式设置Width时,控件会拉伸填满父容器分给它的空间;Margin="10"表示四周各留10个单位的间距,Margin="10,5"表示左右10、上下5。

综合实例

下面综合运用这些面板写一个登录窗口,整体用Grid分行,表单区用一个内嵌Grid实现标签与输入框对齐,底部按钮用StackPanel横向排列。

<Window x:Class="Gacfox.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Gacfox.Demo"
        mc:Ignorable="d"
        Title="MainWindow">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock Text="用户登录" FontSize="20" FontWeight="Bold"
                   HorizontalAlignment="Center" />

        <Grid Grid.Row="1" Margin="0,16">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="用户名:" VerticalAlignment="Center" />
            <TextBox x:Name="usernameTextBox" Grid.Column="1" Height="26" />
            <TextBlock Grid.Row="1" Text="密码:" VerticalAlignment="Center" Margin="0,6,0,0" />
            <PasswordBox x:Name="passwordBox" Grid.Row="1" Grid.Column="1" Height="26" Margin="0,6,0,0" />
        </Grid>

        <StackPanel Grid.Row="2" Orientation="Horizontal"
                    HorizontalAlignment="Right">
            <Button Content="登录" Width="80" Click="loginButton_Click" />
            <Button Content="取消" Width="80" Margin="10,0,0,0"
                    Click="cancelButton_Click" />
        </StackPanel>
    </Grid>
</Window>

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。