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>
后台代码如下:
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); } } } }
两步布局过程
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 MyWrapPanel() { var metadata = new FrameworkPropertyMetadata { AffectsMeasure = true, AffectsArrange = true }; LineBreakBeforeProperty = DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(MyWrapPanel), metadata); } public static void SetLineBreakBefore(UIElement element, bool value) { element.SetValue(LineBreakBeforeProperty, value); } public static bool GetLineBreakBefore(UIElement element) { return (bool)element.GetValue(LineBreakBeforeProperty); } protected override Size MeasureOverride(Size availableSize) { var totalWidth = 0.0; var totalHeight = 0.0; var rowHeight = 0.0; var rowWidth = 0.0; foreach (UIElement uiElement in this.Children) { uiElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); // 当行宽超过了可用空间的宽度或者子元素设置了换行的附加属性时换行 if (rowWidth + uiElement.DesiredSize.Width >= availableSize.Width || GetLineBreakBefore(uiElement)) { // 面板的总宽度是所有行的最大宽度 totalWidth = Math.Max(totalWidth, rowWidth); // 面板的高度是所有行的高度之和 totalHeight += rowHeight; // 换行后重置行高和行宽 rowHeight = 0.0; rowWidth = 0.0; } else { // 每一行的宽度是所有子元素宽度之和 rowWidth += uiElement.DesiredSize.Width; // 每一行的高度都是这一行中所有子元素的最大高度 rowHeight = Math.Max(rowHeight, uiElement.DesiredSize.Height); } } // 加上最后一行的高度 totalHeight += rowHeight; return new Size(totalWidth, totalHeight); } protected override Size ArrangeOverride(Size finalSize) { var x = 0.0; var y = 0.0; var rowHeight = 0.0; foreach (UIElement uie in this.Children) { // 如果该子元素将要超出边界或者设置了换行,则换一行从头显示 if (x + uie.DesiredSize.Width >= finalSize.Width || GetLineBreakBefore(uie)) { x = 0.0; y += rowHeight; // 重置行高 rowHeight = 0.0; } uie.Arrange(new Rect(x, y, uie.DesiredSize.Width, uie.DesiredSize.Height)); rowHeight = Math.Max(rowHeight, uie.DesiredSize.Height); x = x + uie.DesiredSize.Width; } return finalSize; } }