WPF布局

Posted jack_孟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WPF布局相关的知识,希望对你有一定的参考价值。

概述

学习了下WPF里面的布局,参考书是《WPF揭秘》,以下是笔记

WPF布局

布局是WPF界面开发中一个很重要的环节。所谓布局,即确定所有控件的大小和位置,是一种递归进行的父元素(Panel)和子元素交互的过程,为了同时满足父元素和子元素的需要,WPF采用了一种包含测量(Measure)和排列(Arrange)两个步骤的解决方案。子元素最终所占用的空间和位置是由父元素确定的(RenderSize),但是父元素会先参考子元素的意见(DesiredSize)。下面来看看子元素怎样给出意见(控制尺寸、控制位置、变换)以及父元素怎样做决定

控制尺寸

1. 高度和宽度

FrameworkElement元素会根据内容大小调整尺寸(这里有一个例外,如果Window不设置SizeToContent的话,会根据屏幕分辨率设置自己的大小),它同时有Width(默认值Double.NaN,XAML里可以指定为Auto,意思就是和内容一样大)、Height(默认值同Width)、MinWidth(默认值0)、MinHeight(默认值0)、MaxWidth(默认值Double.PositiveInfinity,XAML里面可以写Infinity)、MaxHeight(默认值同MaxWidth)控制宽高,显然如果Width和Height在Min*和Max*范围内的时候,它们的优先级要比Min*以及Max*高

FrameworkElement还有一些与尺寸有关的只读属性:DesiredSize、ActualWidth和ActualHeight、RenderSize;DesiredSize是基于以上属性计算出来的,由父元素(Panel)在布局过程中使用的;RenderSize则是布局结束后元素的尺寸,ActualWidth和ActualHeight与之相同。由于布局操作是异步的,RenderSize的值会晚于Height、Width等基本属性的值,所以依赖RenderSize是不可靠的;UIElement中有一个强制完成布局的方法UpdateLayout(),但由于它会影响性能,而且不能保证正在使用的元素会被正常渲染,所以一般不用

2. Margin和Padding

FrameworkElement.Margin:控制元素边界外的空间

Control.Padding:控制元素边界内的空间

 

3. Visibility

Visible:元素可见,并参与布局

Collapsed:元素不可见并且不参与布局

Hidden:元素不可见但是参与布局

控制位置

 不同父元素(Panel)有不同的方法确定子元素的位置,但是有一些方法是子元素共有的

1. Alignment

子元素(FrameworkElement)可以通过设置Alignment(默认值Stretch)控制怎样使用父元素分配给它的多余的空间;“多余的空间”很重要,因为如果父元素按照子元素的大小给它分配空间的话,这两个属性就不起作用了

比如Canvas就没有给它的子元素分配多余的空间,所以设置HorizontalAlignment和VerticalAlignment不起作用

再比如StackPanel(Orientation属性值这里默认是Vertical,表示子元素垂直排列)只为子元素在水平方向上分配了多余空间,垂直方向上根据尺寸分配,所以设置HorizontalAlignment可以起作用,而设置VerticalAlignment不起作用

2. Content Alignment

Control元素还可以通过设置HorizontalContentAlignment和VerticalContentAlignment控制自己的内容元素怎样对齐

3. FlowDirection

FrameworkElement可以通过设置此属性改变此元素的内容流动的方向(LeftToRight和RightToLeft),可以作用在面板(Panel)或者拥有子元素的控件上

变换(Transform)

 WPF元素还可以通过变换来改变尺寸和位置,有两种变换,RenderTransform和LayoutTransform

RenderTransform(继承自UIElement):在布局结束之后应用

LayoutTransform:在布局前应用

UIElement还有一个属性RenderTransformOrigin表示变换的原点,使用相对定位,(0,0)表示左上角,(1,1)表示右下角,显然RenderTransformOrigin只用于RenderTransform;LayoutTransform没有原点的概念是因为它要参与布局,被变换元素的位置由父元素的布局规则控制

1. RotateTransform

控制变换的属性:Angle(旋转角度)、CenterX和CenterY(旋转中心点);CenterX和CenterY使用的是绝对定位(像素无关单位),可以与RenderTransformOrigin组合起来使用,在缩放变换(ScaleTransform)和倾斜变换(SkewTransform)中都是这样

2. ScaleTransform

控制变换的属性:ScaleX(水平方向的缩放因子)、ScaleY(垂直方向的缩放因子)、CenterX和CenterY(缩放的中心点)

3. SkewTransform

控制变换的属性:AngleX(水平倾斜的角度)、AngleY(垂直倾斜的角度)、CenterX和CenterY(倾斜的中心点)

4. TranslateTransform

控制变换的属性:X(水平偏移量)、Y(垂直偏移量);与上面三种变换不同的是,TranslateTransform作为LayoutTransform应用时不起作用

5. MatrixTransform

控制变换的属性:Matrix(3×3仿射变换矩阵),上面的4种变换都可以通过定义Matrix实现,并且可以直接在XAML里用一个字符串设置,比如下图的变换实现的是水平和垂直方向上放大两倍的效果

6. TransformGroup

可以组合多个变换

 

Panel(面板)

Panel有一个ZIndex附加属性,ZIndex值大的元素会呈现在ZIndex值小的元素上方

WPF内置的常用面板有:Canvas、StackPanel、WrapPanel、DockPanel、Grid,还有一些大多数时候在控件内部使用的轻量级面板

1. 常用面板

常用面板里只记录一下GridSplitter(实际不是Panel类),Grid中可以通过GridSplitter交互改变行列尺寸,哪个单元格尺寸会被影响取决于GridSplitter的对齐值HorizontalAlignment(默认是Right)和VerticalAlignment(默认是Stretch),《WPF揭秘》里有张图,贴在这里,另外ResizeDirection和ResizeBehavior属性也会影响GridSplitter改变单元格尺寸的行为

 

2. TabPanel

TabControl的默认样式用它来处理TabItem的布局;TabPanel仅支持从左往右的排列,从上往下的换行,当换行发生时它会平均拉伸元素,使所有的行占据面板的全部宽度

3. ToolBarOverflowPanel

仅支持从左往右的排列、从上往下的换行,默认样式的ToolBar就是用它来显示无法在主区域显示的元素,有一个WrapWidth属性

4. ToolBarTray

仅支持ToolBar子元素,它会以水平的方式排列ToolBar,并且可以拖动ToolBar生成其他行,或者压缩或扩展相邻的ToolBar

5. UniformGrid

子元素按先行后列的顺序添加,并且行列的大小都是*(平均大小)

6. VirtualizingStackPanel

不同于以上的轻量级面板,当绑定大量数据的时候,VirtualizingStackPanel是首选,因为它会临时抛弃显示范围之外的元素以提高性能,ListBox的默认样式使用的就是这个面板

处理内容溢出

当父元素不能满足子元素尺寸需求的时候,子元素可能会拒绝在过小的空间呈现,这种情况下就会发生内容溢出

父元素(Panel)在处理内容溢出的时候,有以下几种策略:

1. Clipping(剪辑)

UIElement用ClipToBounds属性控制自己是否剪辑超出边界的内容,但是WPF内置面板中只有Canvas支持这个属性,其他诸如Grid等面板设置这个属性也没有用

另外Grid等面板中的子元素通过变换(Transform)超出边界的部分也会被剪辑

想要不被剪辑,看这里

再看这里

还有

不过貌似也没啥用

2. Scrolling(滚屏)

把需要滚屏的元素作为ScrollViewer的子元素即可实现滚屏,但是不要为该元素设置宽度或高度,因为ScollViewer需要根据子元素的内容大小设置合适的水平和垂直滚动范围

3. Scaling(缩放)

为了在给定空间中缩放任意元素(ScaleTransform搞不定),可以使用Viewbox,有两个重要属性:Stretch(控制子元素怎样在Viewbox的边界内缩放)、StretchDirection(控制是需要缩小还是放大子元素)。需要注意的是,Viewbox的缩放是在布局之后发生的

4. 其他

还有两种策略是换行(Wrapping)和截断(Trimming),换行是WrapPanel用的策略,截断则是TextBlock和AccessText中内联文本使用的策略

布局实例

《WPF揭秘》里一个布局实例,自己实现了一下,主要利用Grid的共享尺寸属性SharedSizeGroup,需要注意一点,只有将父级Grid的Grid.IsSharedSizeScope设置为True,它的范围内的尺寸共享才能生效

效果如下:

XAML代码如下:

<Window x:Class="VSUIDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow">

    <DockPanel>

        <Menu DockPanel.Dock="Top">
            <MenuItem Header="File" />
            <MenuItem Header="Edit" />
            <MenuItem Header="View" />
            <MenuItem Header="Project" />
            <MenuItem Header="Build" />
            <MenuItem Header="Debug" />
            <MenuItem Header="Team" />
            <MenuItem Header="Tool" />
            <MenuItem Header="Test" />
            <MenuItem Header="Structure" />
            <MenuItem Header="Analysis" />
            <MenuItem Header="Window" />
            <MenuItem Header="Help" />
        </Menu>

        <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
            <StackPanel.LayoutTransform>
                <RotateTransform Angle="90" />
            </StackPanel.LayoutTransform>
            <Button x:Name="toolboxButton"
                    Content="Toolbox"
                    MouseEnter="toolboxButton_MouseEnter" />
            <Button x:Name="solutionButton"
                    Margin="2,0"
                    Content="Solution Explorer"
                    MouseEnter="solutionButton_MouseEnter" />
        </StackPanel>

        <Grid Grid.IsSharedSizeScope="True">

            <Grid x:Name="layer0Grid"
                  Panel.ZIndex="0"
                  MouseEnter="layer0Grid_MouseEnter">
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>

                <Border Grid.ColumnSpan="2" Background="BlueViolet">
                    <TextBlock HorizontalAlignment="Center"
                               VerticalAlignment="Center"
                               FontSize="36"
                               Text="Start Page" />
                </Border>

                <GroupBox Grid.Row="1"
                          Margin="2"
                          BorderThickness="2"
                          Header="Recent Projects">
                    ...
                </GroupBox>
                <GroupBox Grid.Row="1"
                          Grid.RowSpan="3"
                          Grid.Column="1"
                          Margin="2"
                          BorderThickness="2"
                          Header="Online Articles">
                    <ListBox>
                        <ListBoxItem Content="Article #1" />
                        <ListBoxItem Content="Article #2" />
                        <ListBoxItem Content="Article #3" />
                        <ListBoxItem Content="Article #4" />
                    </ListBox>
                </GroupBox>

                <GroupBox Grid.Row="2"
                          Margin="2"
                          BorderThickness="2"
                          Header="Getting Started">
                    ...
                </GroupBox>
                <GroupBox Grid.Row="3"
                          Margin="2"
                          BorderThickness="2"
                          Header="Headlines">
                    ...
                </GroupBox>

            </Grid>

            <Grid x:Name="toolboxLayerGrid" Visibility="Collapsed">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" SharedSizeGroup="ToolboxGroup" />
                </Grid.ColumnDefinitions>

                <GridSplitter Grid.Column="1"
                              Width="3"
                              HorizontalAlignment="Left" />

                <Grid x:Name="toolboxGrid"
                      Grid.Column="1"
                      Margin="3,0,0,0">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Grid Background="LightBlue">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="35" />
                        </Grid.ColumnDefinitions>
                        <TextBlock VerticalAlignment="Center"
                                   FontSize="18"
                                   Text="Toolbox"
                                   TextTrimming="CharacterEllipsis" />
                        <Button x:Name="toolboxLayerPinButton"
                                Grid.Column="1"
                                Click="toolboxLayerPinButton_Click">
                            <Image x:Name="toolboxImage"
                                   Width="24"
                                   Height="24"
                                   Source="Resource/Image/pin_float.png" />
                        </Button>
                    </Grid>
                    <ListBox Grid.Row="1" FontSize="16">
                        <ListBoxItem Content="Button" />
                        <ListBoxItem Content="CheckBox" />
                        <ListBoxItem Content="Label" />
                        <ListBoxItem Content="ComboBox" />
                        <ListBoxItem Content="ListBox" />
                    </ListBox>
                </Grid>

            </Grid>

            <Grid x:Name="solutionLayerGrid" Visibility="Collapsed">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" SharedSizeGroup="SolutionGroup" />
                </Grid.ColumnDefinitions>

                <GridSplitter Grid.Column="1"
                              Width="3"
                              HorizontalAlignment="Left" />

                <Grid x:Name="solutionGrid"
                      Grid.Column="1"
                      Margin="3,0,0,0">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition />
                    </Grid.RowDefinitions>

                    <Grid Background="LightBlue">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="35" />
                        </Grid.ColumnDefinitions>
                        <TextBlock VerticalAlignment="Center"
                                   FontSize="18"
                                   Text="Solution Explorer"
                                   TextTrimming="CharacterEllipsis" />
                        <Button x:Name="solutionLayerPinButton"
                                Grid.Column="1"
                                Click="solutionLayerPinButton_Click">
                            <Image x:Name="solutionImage"
                                   Width="24"
                                   Height="24"
                                   Source="Resource/Image/pin_float.png" />
                        </Button>
                    </Grid>

                    <Border Grid.Row="1" Background="White">
                        <ToolBar>
                            <Image Source="Resource/Image/copy.png" />
                            <Image Margin="2,0" Source="Resource/Image/paste.png" />
                            <Image Margin="2,0" Source="Resource/Image/refresh.png" />
                        </ToolBar>
                    </Border>


                    <TreeView Grid.Row="2">
                        <TreeViewItem Header="My Solution">
                            <TreeViewItem Header="Project #1" />
                            <TreeViewItem Header="Project #2" />
                            <TreeViewItem Header="Project #3" />
                        </TreeViewItem>
                    </TreeView>

                </Grid>

            </Grid>

        </Grid>

    </DockPanel>

</Window>
View Code

后台代码如下:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;

namespace VSUIDemo
{
    public partial class MainWindow : Window
    {
        private ColumnDefinition _cloneToolboxGrid;
        private ColumnDefinition _cloneSolutionGrid;
        private ColumnDefinition _cloneToToolboxLayerGrid;

        public MainWindow()
        {
            InitializeComponent();

            _cloneToolboxGrid = new ColumnDefinition { SharedSizeGroup = "ToolboxGroup" };
            _cloneSolutionGrid = new ColumnDefinition { SharedSizeGroup = "SolutionGroup" };
            _cloneToToolboxLayerGrid = new ColumnDefinition { SharedSizeGroup = "SolutionGroup" };
        }

        private void toolboxButton_MouseEnter(object sender, MouseEventArgs e)
        {
            toolboxLayerGrid.Visibility = System.Windows.Visibility.Visible;
            toolboxLayerGrid.SetValue(Grid.ZIndexProperty, 2);

            if (solutionButton.Visibility == System.Windows.Visibility.Visible)
                solutionLayerGrid.Visibility = System.Windows.Visibility.Collapsed;
            else
                solutionLayerGrid.SetValue(Grid.ZIndexProperty, 1);
        }

        private void solutionButton_MouseEnter(object sender, MouseEventArgs e)
        {
            solutionLayerGrid.Visibility = System.Windows.Visibility.Visible;
            solutionLayerGrid.SetValue(Grid.ZIndexProperty, 2);

            if (toolboxButton.Visibility == System.Windows.Visibility.Visible)
                toolboxLayerGrid.Visibility = System.Windows.Visibility.Collapsed;
            else
                toolboxLayerGrid.SetValue(Grid.ZIndexProperty, 1);
        }

        private void layer0Grid_MouseEnter(object sender, MouseEventArgs e)
        {
            if (toolboxButton.Visibility == System.Windows.Visibility.Visible)
                toolboxLayerGrid.Visibility = System.Windows.Visibility.Collapsed;

            if (solutionButton.Visibility == System.Windows.Visibility.Visible)
                solutionLayerGrid.Visibility = System.Windows.Visibility.Collapsed;
        }

        private void toolboxLayerPinButton_Click(object sender, RoutedEventArgs e)
        {
            if (toolboxButton.Visibility == System.Windows.Visibility.Visible)
            {
                toolboxImage.Source = new BitmapImage(new Uri("Resource/Image/pin_fix.png", UriKind.Relative));
                toolboxButton.Visibility = System.Windows.Visibility.Collapsed;
                layer0Grid.ColumnDefinitions.Add(_cloneToolboxGrid);

                if (solutionButton.Visibility == System.Windows.Visibility.Collapsed)
                    toolboxLayerGrid.ColumnDefinitions.Add(_cloneToToolboxLayerGrid);
            }
            else
            {
                toolboxImage.Source = new BitmapImage(new Uri("Resource/Image/pin_float.png", UriKind.Relative));
                toolboxButton.Visibility = System.Windows.Visibility.Visible;
                toolboxLayerGrid.Visibility = System.Windows.Visibility.Collapsed;
                layer0Grid.ColumnDefinitions.Remove(_cloneToolboxGrid);

                if (solutionButton.Visibility == System.Windows.Visibility.Collapsed)
                    toolboxLayerGrid.ColumnDefinitions.Remove(_cloneToToolboxLayerGrid);
            }
        }

        private void solutionLayerPinButton_Click(object sender, RoutedEventArgs e)
        {
            if (solutionButton.Visibility == System.Windows.Visibility.Visible)
            {
                solutionImage.Source = new BitmapImage(new Uri("Resource/Image/pin_fix.png", UriKind.Relative));
                solutionButton.Visibility = System.Windows.Visibility.Collapsed;
                layer0Grid.ColumnDefinitions.Add(_cloneSolutionGrid);

                if (toolboxButton.Visibility == System.Windows.Visibility.Collapsed)
                    toolboxLayerGrid.ColumnDefinitions.Add(_cloneToToolboxLayerGrid);
            }
            else
            {
                solutionImage.Source = new BitmapImage(new Uri("Resource/Image/pin_float.png", UriKind.Relative));
                solutionButton.Visibility = System.Windows.Visibility.Visible;
                solutionLayerGrid.Visibility = System.Windows.Visibility.Collapsed;
                layer0Grid.ColumnDefinitions.Remove(_cloneSolutionGrid);

                if (toolboxButton.Visibility == System.Windows.Visibility.Collapsed)
                    toolboxLayerGrid.ColumnDefinitions.Remove(_cloneToToolboxLayerGrid);
            }
        }
    }
}
View Code

两步布局过程

1. 测量阶段

该阶段决定子元素希望占用多大的尺寸。可以通过重写MeasureOverride()来实现自己的逻辑,重写MeasureOverride()方法时,必须调用每个子元素的Measure()方法,传入边界值作为参数,可以传入一个无限大的边界( uiElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); ),这样子元素就会根据所有内容大小确定DesiredSize;另外需要注意的是MeasureOverride()返回父元素自身所占用的尺寸,可以根据所有子元素的占用尺寸计算得到,不能返回一个无限大的尺寸

2. 排列阶段

该阶段为每一个控件指定边界。可以通过重写ArrangeOverride()实现自己的逻辑。重写ArrangeOverride()方法时,必须调用每个子元素的Arrange()方法,传入一个定义尺寸和位置的Rect对象作为参数

3. 自定义面板

主要是重写以上两个阶段的逻辑,下面是《WPF编程宝典》的一个例子,自己实现了一下

效果如下:

后台代码也贴在这里:

// 自定义一个从左至右排列、从上往下换行的面板,并且提供一个附加属性可以指示在哪个子元素前换行
public class MyWrapPanel : Panel
{
    // 定义一个指示在哪个子元素前换行的附加属性
    public static readonly DependencyProperty LineBreakBeforeProperty;

    static以上是关于WPF布局的主要内容,如果未能解决你的问题,请参考以下文章

如何通过单击片段内的线性布局从片段类开始新活动?下面是我的代码,但这不起作用

Wordpress - 将代码片段包含到布局的选定部分的插件

WPF Grid布局 实现DataGrid控件宽充满布局

有没有更聪明的方法将布局绑定到片段?

android片段表格布局

重新创建片段布局