WPF:新手入门指南 - 第 1 部分 (共 n 部分)






4.85/5 (782投票s)
WPF 布局系统的介绍。
引言
可能有些朋友读过我在 CodeProject 上发表的其他几篇文章。如果读过,你们可能会注意到我最近写了几篇关于 WPF/LINQ/Silverlight/WCF 的文章。我通常喜欢在写文章时找点乐子,并尽量让自己觉得有趣;有时候,这意味着文章会比较复杂,而我喜欢这样。
然而,我记得不久前,我发表了一篇我自认为属于中级水平的 WPF 文章,由于 CodeProject 的升级,它被归类为新手级。它本来就不是一篇新手文章,因此遭到了一些人的批评,认为它太复杂,不适合新手。
我并不介意受到批评,但这次是因为升级问题,而不是我自己的问题。不过,这确实让我开始思考论坛上的一个帖子:“WPF 的新手文章都去哪儿了?”
于是我思考了一下,并在 CodeProject 上搜索了一下,发现 CodeProject 上的 WPF 作者们(包括我自己)大多都在炫技。当然,这并非坏事;我从许多炫技的文章中学到了很多东西。但我同时也觉得,对于那些时间不多但又想学习 WPF 的新手来说,这里的内容并不多。所以我想,也许出一个 WPF 新手系列会比较好。我知道 Josh Smith 在他的系列文章《WPF 导览》中做得非常出色,我个人推荐大家在他还没读过的话去读一读。
当然,还有书籍,但我有三本 WPF 书籍,它们的信息各有不同。所以我想,好吧,也许这个新手系列也并非那么糟糕。我决定试一试。我不知道这个系列最终会有多少篇文章,但可能会是这样的:
- 布局 (本文)
- XAML 与代码 / 标记扩展和资源 (下一篇文章)
- 命令和事件
- 依赖属性
- DataBinding
- 样式/模板
正如我所说,我认为 Josh 在这方面做得非常出色,但我为什么还要买三本 WPF 书呢?一本不够吗?有时候,只是想换个角度看看。例如,Josh 是 WPF 大师(就像一只秃鹰……或者某种同样雄伟的鸟……在阳光下黑压压一片,翅膀展开),而我只是 WPF 世界里一只刚刚学飞(翅膀很小)的雏鹰。所以我想,从稍微不同的角度(也就是疯狂的雏鹰)听听也许能让一些人受益。
当然,我也会介绍一些可以直接从书中摘录的内容,但我希望也能有一些我自己在 WPF 之旅中学到的新东西。
由于这个系列需要很大的投入,我只请求一件事:如果您觉得本文有帮助,请在底部留下评论和投票,以便我了解是否值得继续完成这个系列。您知道,我还有数百篇待发表的文章(我有一个长长的列表),我可以随心所欲地写,但我认为这个系列可能很有用(基本上,请在文章论坛告诉我您想要更多这样的内容),如果那样的话,我很乐意投入精力,让这个系列尽我所能地有趣。
那么,言归正传,我猜我们应该开始研究本文需要掌握的内容了。作为这个(假定受欢迎的)系列的第一篇文章,它确实应该从布局开始。
布局
布局是任何 WPF 项目中最重要的部分之一;以下子节将向您介绍可用的新布局选项。
布局的重要性
在我看来,WPF 可以有两种使用方式:可以在浏览器中使用(部分信任)——这称为 XBap,或者作为功能齐全的应用程序,基本上是一个可执行文件(* .exe*)。无论选择哪种格式,对于本文的内容都不重要,因为布局对这两种格式都同样重要。
我的意思是,布局是编写任何 WPF 程序时使用的基本构建块,无论它是 XBap 还是应用程序。使用 WPF 中的布局控件可以使开发人员/设计人员创建非常复杂的页面/控件排列。没有布局,我们可能什么也做不成,只会一团糟。所以,如果您在寻找一团糟,请在此处停止阅读。但是,如果您想了解如何在 WPF 中使用新的布局选项,请继续阅读。
WPF 中的新特性
如果您是 Web 开发人员,本文将介绍的大部分内容(但不是全部)对您来说可能是新的。如果您是 WinForms 开发人员,您肯定已经遇到过 Panel
类,甚至可能使用过一些更复杂的 Panel
子类,例如 FlowLayoutPanel
和 TableLayoutPanel
。您还应该熟悉 WinForms 中的 Anchor
和 Dock
等属性。听起来耳熟吗?是的,其中一些知识仍然有用,但您仍然需要学习一些新东西。对于 Web 开发者来说,好消息是 XAML 语法与 XHTML 相当相似,所以您应该很容易掌握这种创建 UI 的新方式。
在 WPF(至少是当前版本)中,微软提供了一些布局控件供开发人员/设计人员使用,其中大多数我猜对你们大多数人来说都是陌生的。这些新的布局控件将是本文的重点。当然,如果预制的控件不符合您的需求,您可以自由创建自己的布局控件。稍后我们将对此进行更多介绍。
出于本文的目的,我们将重点关注以下内容:
画布
StackPanel
WrapPanel
DockPanel
Grid
请注意,我只会涵盖这些控件的基础知识;如果您想深入研究,还有许多其他资源可供您学习这些控件的高级用法。然而,我认为这些控件的高级用法超出了本文的范围。请记住,这是一个新手系列,所以我想将其保持在新手水平。
简要了解 Margin 的重要性
您必须了解的一件事是 Margin
属性的重要性。通过使用 Margin
,我们可以指定当前控件(为其指定 Margin
属性的控件)在其周围需要多少空间。WPF 提供了一个 ValueConverter
,它接受格式为 5,5,5,5
的字符串,但这代表什么呢?基本上,它意味着我们希望控件周围有 5 像素的 Margin
。Margin
字符串依次表示左、上、右、下,并且是 Thickness
类使用的三个重载构造函数之一,当我们在代码隐藏中设置 Margin
时会使用它。
画布
Canvas
控件是最容易使用的布局控件之一。它是一个简单的 X/Y 定位容器,其中每个包含的(子)控件都必须指定以下四个属性才能在父 Canvas
控件中定位:
Canvas.Left
Canvas.Right
Canvas.Top
Canvas.Bottom
有了这四个属性,控件就会使用这些值在父 Canvas
控件中进行定位。这些属性看起来可能有点奇怪,比如 Canvas.Left
;是的,它们确实有点奇怪。它们不是您在 .NET 2.0 环境中使用的常规属性。它们是依赖/附加属性,我暂时不去深入介绍,因为它们将是本系列后续文章的主题。
关于 Canvas
控件及其子项,我们还需要了解什么?嗯,实际上就这些了;唯一需要考虑的是,如果 Canvas
控件是一个简单的 X/Y 定位容器,那么什么可以阻止两个子控件重叠,以及哪个子控件应该在上面?嗯,这一切都由 Canvas
控件的另一个依赖/附加属性来处理。这就是 Canvas.ZIndex
属性,它指示哪个控件应该在上面。基本上,Canvas.ZIndex
值越高,声明此依赖/附加属性的控件就越靠上。如果未声明任何子控件的 Canvas.ZIndex
,则 Canvas.ZIndex
将设置为子项添加到 Canvas
控件的顺序。
让我们来看一个例子,好吗?下图显示了一个 Canvas
控件及其两个子项,一个在另一个上面。这张图来自附加演示项目中的 CanvasDEMO.xaml 文件。
那么代码看起来是怎样的呢?在 XAML 中,如下所示:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WPF_Tour_Beginners_Layout.CanvasDEMO"
x:Name="Window"
Title="CanvasDEMO"
Width="640" Height="480">
<Canvas Margin="0,0,0,0" Background="White">
<Rectangle Fill="Blue"
Stroke="Blue"
Width="145"
Height="126"
Canvas.Left="124" Canvas.Top="122"/>
<Ellipse Fill="Green"
Stroke="Green"
Width="121" Height="100"
Panel.ZIndex="1"
Canvas.Left="195" Canvas.Top="191"/>
</Canvas>
</Window>
在 C# 中,它将是这样的:
Canvas canv = new Canvas();
//add the Canvas as sole child of Window
this.Content = canv;
canv.Margin = new Thickness(0, 0, 0, 0);
canv.Background = new SolidColorBrush(Colors.White);
//The Rectangle
Rectangle r = new Rectangle();
r.Fill = new SolidColorBrush(Colors.Blue);
r.Stroke = new SolidColorBrush(Colors.Blue);
r.Width = 145;
r.Height = 126;
r.SetValue(Canvas.LeftProperty, (double)124);
r.SetValue(Canvas.TopProperty, (double)122);
canv.Children.Add(r);
//The Ellipse
Ellipse el = new Ellipse();
el.Fill = new SolidColorBrush(Colors.Green);
el.Stroke = new SolidColorBrush(Colors.Green);
el.Width = 121;
el.Height = 100;
el.SetValue(Canvas.ZIndexProperty, 1);
el.SetValue(Canvas.LeftProperty, (double)195);
el.SetValue(Canvas.TopProperty, (double)191);
canv.Children.Add(el);
而在 VB.NET 中,它将是:
Dim canv As New Canvas()
'add the Canvas as sole child of Window
Me.Content = canv
canv.Margin = New Thickness(0, 0, 0, 0)
canv.Background = New SolidColorBrush(Colors.White)
'The Rectangle
Dim r As New Rectangle()
r.Fill = New SolidColorBrush(Colors.Blue)
r.Stroke = New SolidColorBrush(Colors.Blue)
r.Width = 145
r.Height = 126
r.SetValue(Canvas.LeftProperty, CDbl(124))
r.SetValue(Canvas.TopProperty, CDbl(122))
canv.Children.Add(r)
'The Ellipse
Dim el As New Ellipse()
el.Fill = New SolidColorBrush(Colors.Green)
el.Stroke = New SolidColorBrush(Colors.Green)
el.Width = 121
el.Height = 100
el.SetValue(Canvas.ZIndexProperty, 1)
el.SetValue(Canvas.LeftProperty, CDbl(195))
el.SetValue(Canvas.TopProperty, CDbl(191))
canv.Children.Add(el)
这就是基本的 Canvas
布局的全部内容。
StackPanel
StackPanel
控件也非常易于使用。它通过一个名为 Orientation
的属性来堆叠其内容,可以是垂直或水平方向。
让我们来看一个例子,好吗?下图显示了一个 StackPanel
控件及其两个子项,一个在另一个上面。这张图来自附加演示项目中的 StackPanelDEMO.xaml 文件。
那么代码看起来是怎样的呢?在 XAML 中,如下所示:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WPF_Tour_Beginners_Layout.StackPanelDEMO"
x:Name="Window"
Title="StackPanelDEMO"
WindowStartupLocation="CenterScreen"
Width="640" Height="480">
<StackPanel Margin="0,0,0,0"
Background="White" Orientation="Vertical">
<Button Content="Im Top of Stack"/>
<Button Content="Im Bottom Of Stack"/>
</StackPanel>
</Window>
在 C# 中,它将是这样的:
StackPanel sp = new StackPanel();
//add the StackPanel as sole child of Window
this.Content = sp;
sp.Margin = new Thickness(0, 0, 0, 0);
sp.Background = new SolidColorBrush(Colors.White);
sp.Orientation = Orientation.Vertical;
//Button1
Button b1 = new Button();
b1.Content = "Im Top of Stack";
sp.Children.Add(b1);
//Button2
Button b2 = new Button();
b2.Content = "Im Bottom of Stack";
sp.Children.Add(b2);
而在 VB.NET 中,它将是:
Dim sp As New StackPanel()
'add the StackPanel as sole child of Window
Me.Content = sp
sp.Margin = New Thickness(0, 0, 0, 0)
sp.Background = New SolidColorBrush(Colors.White)
sp.Orientation = Orientation.Vertical
'Button1
Dim b1 As New Button()
b1.Content = "Im Top of Stack"
sp.Children.Add(b1)
'Button2
Dim b2 As New Button()
b2.Content = "Im Bottom of Stack"
sp.Children.Add(b2)
这就是基本的 StackPanel
布局的全部内容。
WrapPanel
WrapPanel
控件同样非常易于使用(您开始看到模式了吗?WPF 中的布局相当容易!),它只是简单地换行其内容。
让我们来看一个例子,好吗?下图显示了一个 WrapPanel
控件及其 10 个子项。这张图来自附加演示项目中的 WrapPanelDEMO.xaml 文件。
那么代码看起来是怎样的呢?在 XAML 中,如下所示:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WPF_Tour_Beginners_Layout.WrapPanelDEMO"
x:Name="Window"
Title="WrapPanelDEMO"
WindowStartupLocation="CenterScreen"
Width="640" Height="480">
<WrapPanel Margin="0,0,0,0" Background="White">
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
<Rectangle Margin="10,10,10,10"
Fill ="Blue" Width="60" Height="60"/>
</WrapPanel>
</Window>
在 C# 中,它将是这样的:
WrapPanel wp = new WrapPanel();
//add the WrapPanel as sole child of Window
this.Content = wp;
wp.Margin = new Thickness(0, 0, 0, 0);
wp.Background = new SolidColorBrush(Colors.White);
//Add Rectangles
Rectangle r;
for (int i = 0; i <= 10; i++)
{
r = new Rectangle();
r.Fill = new SolidColorBrush(Colors.Blue);
r.Margin = new Thickness(10, 10, 10, 10);
r.Width = 60;
r.Height = 60;
wp.Children.Add(r);
}
而在 VB.NET 中,它将是:
Dim wp As New WrapPanel()
'add the WrapPanel as sole child of Window
Me.Content = wp
wp.Margin = New Thickness(0, 0, 0, 0)
wp.Background = New SolidColorBrush(Colors.White)
'Add Rectangles
Dim r As Rectangle
For i As Integer = 0 To 10
r = New Rectangle()
r.Fill = New SolidColorBrush(Colors.Blue)
r.Margin = New Thickness(10, 10, 10, 10)
r.Width = 60
r.Height = 60
wp.Children.Add(r)
Next
这就是基本的 WrapPanel
布局的全部内容。
DockPanel
DockPanel
控件是我认为最有用的布局控件之一。它可能是我们用于任何新 Window
的基础布局控件。基本上,通过一个(或两个)DockPanel
控件,我们可以实现大多数应用程序一直以来都采用的布局。我们可以将菜单栏停靠在顶部,然后是左/右主内容区域,底部是状态栏。这一切都归功于 DockPanel
控件的几个属性。基本上,我们可以通过以下依赖/附加属性来控制父 DockPanel
控件中任何子控件的停靠:
DockPanel.Dock
此属性可以设置为 Left
/Right
/Top
/Bottom
。DockPanel
控件上还有一个由正常 CLR 属性公开的附加属性,称为 LastChildFill
,当设置为 true
时,它将使添加到 DockPanel
控件的最后一个子控件填充剩余的可用空间。这将覆盖子控件可能已经设置的任何 DockPanel.Dock
属性。
让我们来看一个例子,好吗?下图显示了一个 DockPanel
控件及其两个子项,一个停靠在顶部,另一个停靠以填充剩余的可用区域。这张图来自附加演示项目中的 DockPanelDEMO.xaml 文件。
那么代码看起来是怎样的呢?在 XAML 中,如下所示:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WPF_Tour_Beginners_Layout.DockPanelDEMO"
x:Name="Window"
Title="DockPanelDEMO"
WindowStartupLocation="CenterScreen"
Width="640" Height="480">
<DockPanel Width="Auto"
Height="Auto" LastChildFill="True">
<Rectangle Fill="CornflowerBlue" Stroke="CornflowerBlue"
Height="20" DockPanel.Dock="Top"/>
<Rectangle Fill="Orange" Stroke="Orange" />
</DockPanel>
</Window>
在 C# 中,它将是这样的:
DockPanel dp = new DockPanel();
dp.LastChildFill = true;
//this is the same as Width="Auto" in XAML, as long as its not applied
to a GridColumn Width/Height /GridRow Width/Height which has special classes
dp.Width = Double.NaN;
dp.Height = Double.NaN;
//add the WrapPanel as sole child of Window
this.Content = dp;
//Add Rectangles
Rectangle rTop = new Rectangle();
rTop.Fill = new SolidColorBrush(Colors.CornflowerBlue);
rTop.Stroke = new SolidColorBrush(Colors.CornflowerBlue);
rTop.Height = 20;
dp.Children.Add(rTop);
rTop.SetValue(DockPanel.DockProperty,Dock.Top);
Rectangle rFill = new Rectangle();
rFill.Fill = new SolidColorBrush(Colors.Orange);
rFill.Stroke = new SolidColorBrush(Colors.Orange);
dp.Children.Add(rFill);
而在 VB.NET 中,它将是:
Dim dp As New DockPanel()
dp.LastChildFill = True
dp.Width = [Double].NaN
'this is the same as Width="Auto" in XAML
dp.Height = [Double].NaN
'this is the same as Height="Auto" in XAML
'add the DockPanel as sole child of Window
Me.Content = dp
'Add Rectangles
Dim rTop As New Rectangle()
rTop.Fill = New SolidColorBrush(Colors.CornflowerBlue)
rTop.Stroke = New SolidColorBrush(Colors.CornflowerBlue)
rTop.Height = 20
dp.Children.Add(rTop)
rTop.SetValue(DockPanel.DockProperty, Dock.Top)
Dim rFill As New Rectangle()
rFill.Fill = New SolidColorBrush(Colors.Orange)
rFill.Stroke = New SolidColorBrush(Colors.Orange)
dp.Children.Add(rFill)
这就是基本的 DockPanel
布局的全部内容。
Grid
Grid
控件是目前为止最复杂的 WPF 布局控件(目前)。它有点像 HTML 的表格控件,您可以指定行和列,并且单元格可以跨越多行或跨越多列。还有一个奇怪的语法可能用于列和行的大小,称为星号 "*" 表示法,它通过 GridLength
类公开。可以将其视为剩余空间的百分比划分器。例如,我可能有一些标记,如:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
在这里,我声明了三个 Grid ColumnDefinition
控件,其中第一个 ColumnDefinition
获得 40 像素的固定宽度,其余空间在最后两个 ColumnDefinition
控件之间分配,最后一个获得的宽度是倒数第二个的两倍。RowDefinition
的原理也是如此。
为了使 Grid
控件的子控件能够告诉 WPF 布局系统它们属于哪个单元格,我们只需使用以下依赖/附加属性,它们使用从 0 开始的索引:
Grid.Column
Grid.Row
要指定单元格应占用多少行或列,我们只需使用以下依赖/附加属性,它们从 1 开始:
Grid.ColumnSpan
Grid.RowSpan
通过巧妙地使用 Grid
控件,您应该几乎能够模仿其他所有布局控件。这我留给读者作为练习。
让我们来看一个 Grid
控件的例子,好吗?下图显示了一个具有三列和一行的 Grid
控件,其中有两个子项。第一个子项占据列 1,第二个子项占据列 2-3,因为其 Grid.ColumnSpan
设置为 2。这张图来自附加演示项目中的 GridDEMO.xaml 文件。
那么代码看起来是怎样的呢?在 XAML 中,如下所示:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WPF_Tour_Beginners_Layout.GridDEMO"
x:Name="Window"
Title="GridDEMO"
WindowStartupLocation="CenterScreen"
Width="640" Height="480">
<Grid Width="Auto" Height="Auto" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Rectangle Fill="Aqua"
Grid.Column="0" Grid.Row="0"/>
<Rectangle Fill="Plum"
Grid.Column="1" Grid.ColumnSpan="2"/>
</Grid>
</Window>
在 C# 中,它将是这样的:
Grid grid = new Grid();
grid.Width = Double.NaN;
//this is the same as Width="Auto" in XAML
//this is the same as Height="Auto" in XAML
grid.Height = Double.NaN;
//add the Grid as sole child of Window
this.Content = grid;
//col1
ColumnDefinition cd1 = new ColumnDefinition();
cd1.Width = new GridLength(40);
grid.ColumnDefinitions.Add(cd1);
//col2
ColumnDefinition cd2 = new ColumnDefinition();
cd2.Width = new GridLength(1, GridUnitType.Star);
grid.ColumnDefinitions.Add(cd2);
//col3
ColumnDefinition cd3 = new ColumnDefinition();
cd3.Width = new GridLength(2, GridUnitType.Star);
grid.ColumnDefinitions.Add(cd3);
//Now add the cells to the grid
Rectangle r1c1 = new Rectangle();
r1c1.Fill = new SolidColorBrush(Colors.Aqua);
r1c1.SetValue(Grid.ColumnProperty, 0);
r1c1.SetValue(Grid.RowProperty, 0);
grid.Children.Add(r1c1);
Rectangle r1c23 = new Rectangle();
r1c23.Fill = new SolidColorBrush(Colors.Plum);
r1c23.SetValue(Grid.ColumnProperty, 1);
r1c23.SetValue(Grid.ColumnSpanProperty, 2);
grid.Children.Add(r1c23);
而在 VB.NET 中,它将是:
Dim grid As New Grid()
grid.Width = [Double].NaN
'this is the same as Width="Auto" in XAML
grid.Height = [Double].NaN
'this is the same as Height="Auto" in XAML
'add the Grid as sole child of Window
Me.Content = grid
'col1
Dim cd1 As New ColumnDefinition()
cd1.Width = New GridLength(40)
grid.ColumnDefinitions.Add(cd1)
'col2
Dim cd2 As New ColumnDefinition()
cd2.Width = New GridLength(1, GridUnitType.Star)
grid.ColumnDefinitions.Add(cd2)
'col3
Dim cd3 As New ColumnDefinition()
cd3.Width = New GridLength(2, GridUnitType.Star)
grid.ColumnDefinitions.Add(cd3)
'Now add the cells to the grid
Dim r1c1 As New Rectangle()
r1c1.Fill = New SolidColorBrush(Colors.Aqua)
r1c1.SetValue(Grid.ColumnProperty, 0)
r1c1.SetValue(Grid.RowProperty, 0)
grid.Children.Add(r1c1)
Dim r1c23 As New Rectangle()
r1c23.Fill = New SolidColorBrush(Colors.Plum)
r1c23.SetValue(Grid.ColumnProperty, 1)
r1c23.SetValue(Grid.ColumnSpanProperty, 2)
grid.Children.Add(r1c23)
正如我所说,Grid
控件相当复杂,所以我强烈建议您进一步探索它。您可以对 Grid
进行各种操作,例如使用 GridSplitter
控件来调整列/行大小,还可以为多个网格设置共享大小,这称为 SizeGroup
。因此,请进一步探索 Grid
控件。
整合起来
现在我们可以将所有这些知识结合起来,创造出像这样的美丽作品:
不……开玩笑的,我认为这里最好的做法是,比如说我有一个想要实现的假设布局。假设我们要实现一个多年来都非常流行的常见布局:顶部有一个菜单栏,接着是主内容区域,底部是状态栏区域。让我们看看我们想要达到的目标的模拟图(设计为一个简单的 WinForm):
我认为我已经为您提供了在 WPF 中实现此类布局所需的所有工具。您想要一个提示吗?我认为您需要使用 StackPanel
、DockPanel
和 Grid
控件来完成这项工作。
如果您好奇(或者迷失了方向,哈哈),这是我如何实现的(这次只有 XAML,抱歉了各位):
<Window x:Class="WPF_Tour_Beginners_Layout.PuttingItAllTogether"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStartupLocation="CenterScreen"
Title="PuttingItAllTogether"
Width="640" Height="480" >
<DockPanel Width="Auto"
Height="Auto" LastChildFill="True">
<!--Top Menu Area-->
<Menu Width="Auto" Height="20"
Background="#FFA9D1F4"
DockPanel.Dock="Top">
<!-- File Menu -->
<MenuItem Header="File">
<MenuItem Header="Save"/>
<Separator/>
<MenuItem Header="Exit"/>
</MenuItem>
<!-- About Menu -->
<MenuItem Header="Help">
<MenuItem Header="About"/>
</MenuItem>
</Menu>
<!--Bottom Status Bar area, declared before middle section,
as I want it to fill entire bottom of Window,
which it wouldn't if there was a Left docked panel before it -->
<StackPanel Width="Auto"
Height="31" Background="#FFCAC5C5"
Orientation="Horizontal" DockPanel.Dock="Bottom">
<Label Width="155"
Height="23" Content="Status Bar Message...."
FontFamily="Arial" FontSize="10"/>
</StackPanel>
<!--Left Main Content area-->
<StackPanel Width="136"
Height="Auto" Background="White">
<Button Margin="5,5,5,5" Width="Auto"
Height="26" Content="button1"/>
<Button Width="126" Height="26"
Content="button2" Margin="5,5,5,5"/>
<Button Width="126" Height="26"
Content="button3" Margin="5,5,5,5"/>
</StackPanel>
<!--Right Main Content area, NOTE HOW this Grid is the last child
so takes all the remaining room -->
<Grid Width="Auto" Height="Auto"
Background="#FFCC9393">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Aqua" Margin="10,10,10,10"
Grid.Row="0" Grid.Column="0"/>
<Rectangle Fill="Aqua" Margin="10,10,10,10"
Grid.Row="0" Grid.Column="1"/>
<Rectangle Fill="Aqua" Margin="10,10,10,10"
Grid.Row="1" Grid.Column="0"/>
<Rectangle Fill="Aqua" Margin="10,10,10,10"
Grid.Row="1" Grid.Column="1"/>
</Grid>
</DockPanel>
</Window>
这将导致以下 Window
:
这里有几个技巧,即:
- 右侧内容区域的
Grid
必须是最后声明的子项,以便它能够占据父DockPanel
由于LastChildFill="True"
而需要填充的剩余空间。 - 用于状态栏的
StackPanel
必须在任何其他声明为DockPanel.Dock="Left"
或DockPanel.Dock="Right"
的子项之前。因为,如果状态栏StackPanel
之前还有其他元素,状态栏StackPanel
将无法跨越整个可用宽度,因为该空间已被任何DockPanel.Dock="Left"
或DockPanel.Dock="Right"
的子项占据。试试看,您就会明白我的意思。只需将状态栏的 XAML 移到 XAML 文件中的更下方,例如放到最后。
性能考量
由于某些面板可以绑定到项(这将在 DataBinding
文章中进一步讨论),有时面板中显示的子元素数量会非常多。例如,如果一个 StackPanel
包含一个绑定到大型数据库查询的 ListBox
,那么会有很多项。在这种情况下,ListBox
将会有很多子项。然而,在内部,ListBox
控件默认使用一个垂直 StackPanel
来渲染其项。嗯,这不太好。
然而,事情并非如此糟糕。WPF 还有另一个绝招来帮助解决这种情况。我们可以在 ListBox
上使用依赖/附加属性 VirtualizingStackPanel.IsVirtualizing
,这意味着 ListBox
控件内部渲染项的 StackPanel
现在将是虚拟化的。但 VirtualizingStackPanel
到底是什么?
当一个面板被虚拟化时,意味着只有可见的元素会被创建。其余的则不会显示。例如,创建一个显示绑定到包含 100,000 条记录的数据库的图片的 ListBox
,ListBox
加载将花费很长时间。如果使用虚拟化面板,则只有可见的图像将在 UI 中创建。当您向下滚动时,当前可见的项将被销毁,新的可见项将被加载到 UI 中。只有一个面板支持虚拟化,那就是 VirtualizingStackPanel
。如果您需要创建任何新的虚拟化面板,您将不得不自己编写。
自定义布局
您还可以创建自己的自定义面板,以执行各种自定义布局。
现在我可以尝试耍点聪明,想出一些新的方法来做到这一点,但 Paul Tallet 在他出色的 CodeProject 文章《FishEye panel》中已经做得非常好了。所以接下来的这一小部分内容要归功于 Paul Tallet。
要启动您自己的自定义面板,您需要继承自
System.Windows.Controls.Panel
并实现两个重写:MeasureOverride
和LayoutOverride
。它们实现了两阶段布局系统,在测量阶段,您的父项会调用您来了解您需要多少空间。您通常会询问您的子项它们需要多少空间,然后将结果返回给父项。在第二阶段,有人会决定所有元素的大小,并将最终大小传递给您的ArrangeOverride
方法,您在该方法中告诉子项它们的大小并进行布局。请注意,每次执行影响布局的操作时(例如,调整窗口大小),都会使用新尺寸重新执行所有这些操作。
Rob Relyea 的博客 这里 有大量自定义面板链接。
我们完成了
呼,我想这确实是很多内容,但我希望到目前为止您已经掌握了 WPF 布局的基础知识。
但是……正如我所说,如果您想要更多……
正如我之前提到的,这个系列文章对我来说是一个很大的投入,所以如果您想要更多,请投票并留言,以便我了解是否值得我以这个级别完成整个系列。
历史
- 17/01/08:首次发布。