WPF之控件布局

  • A+
所属分类:.NET技术
摘要

程序的本质是“数据+算法”——用户输入原始数据,算法处理原始数据并得到结果数据。程序可以使用LED阵列、格式字符串、图形化用户界面(Graphic User Interface,GUI)将结果数据显示给用户,其中图形化用户界面最方便、直观。
在Windows上实现图形化的界面有多种方法,每种方法又拥有自己的一套开发理念和工具并组成一种方法论,常见的有:

控件概述

程序的本质是“数据+算法”——用户输入原始数据,算法处理原始数据并得到结果数据。程序可以使用LED阵列、格式字符串、图形化用户界面(Graphic User Interface,GUI)将结果数据显示给用户,其中图形化用户界面最方便、直观。
在Windows上实现图形化的界面有多种方法,每种方法又拥有自己的一套开发理念和工具并组成一种方法论,常见的有:

  • Windows APl(Win APl):调用Windows底层绘图函数,使用C语言,最原始也最基础。
  • Microsoft Foundation Class(MFC):使用C++语法将原始的Win32API函数封装成控件类。
  • Visual Component Library(VCL):Delphi和C++Builder使用的与MFC相近的控件类库。
  • Visual Basic+ActiveX控件(VB6):使用组件化的思想把WinAPI封装成UI控件,以期多语言共用。
  • Java Swing/AWT:Java SDK中用于跨平台开发GUI程序的控件类库。
  • Windows Form:.NET平台上进行GUl开发的老牌劲旅,完全组件化但需要.NET运行时支持。
  • Windows Presentation Foundation(WPF):后起之秀,使用全新的数据驱动UI的理念。

可以把上述这些方法论分为四代:

  • WinAPI时代:函数调用+Windows消息处理。
  • 封装时代:使用面向对象理念把WinAPI封装成类;由来自UI的消息驱动程序处理数据。
  • 组件化时代:使用面向组件理念在类的基础上封装成组件;消息被封装成事件,变成事件驱动。
  • WPF时代:在组件化的基础上,使用专门的UI设计语言并引入由数据驱动UI的理念。

注:目前流行使用前端的方式实现GUI,如ElectronNanUI等。

WPF中是数据驱动UI,数据是核心、是主动的;UI从属于数据并表达数据、是被动的。WPF把那些能够展示数据、响应用户操作的UI元素称为控件(Control),控件所展示的数据称之为控件的“数据内容”,控件在响应用户的操作后会执行自己的一些方法或以事件(Event)的形式通知应用程序称之为控件的“行为”或“算法内容”

WPF中的控件是个非常抽象的概念——Control是数据和行为的载体,而无需具有固定的形象,如只要是用来显示一个bool类型值并允许用户通过单击来切换true/false/null的UI元素就是一个CheckBox(关注抽象的数据和行为而不是控件具体的形象)。

日常工作中打交道最多的控件无外乎6类,即:

  • 布局控件:可以容纳多个控件或嵌套其他布局控件,用于在UI上组织和排列控件,如Grid、StackPanel、DockPanel 等控件(共同的父类是Panel)。
  • 内容控件:只能容纳一个其他控件或布局控件作为它的内容,如Window、Button等控件(经常需要借助布局控件来规划其内容,共同父类是ContentControl)。
  • 带标题内容控件:相当于一个内容控件,但可以加一个标题(Header),标题部分亦可容纳一个控件或布局,GroupBox、Tabltem等是这类控件的典型代表(共同父类是HeaderedContentControl)。
  • 条目控件:可以显示一列数据,一般情况下这列数据的类型相同,此类控件包括ListBox、ComboBox等(共同基类是ltemsControl,在显示集合类型数据方面功能非常强大)。
  • 带标题条目控件:相当于一个条目控件加上一个标题显示区,Tree Viewltem、Menultem都属于此类控件(往往用于显示层级关系数据,结点显示在其Header区域,子级结点则显示在其条目控件区域,共同基类是HeaderedltemsControl)。
  • 特殊内容控件:这类控件相对比较独立,比如TextBox容纳的是字符串、TextBlock可以容纳可自由控制格式的文本、Image容纳图片类型数据。

6类控件的派生关系如下图所示:
WPF之控件布局

WPF是构建在.NET Framework上的一个子系统,它也是一个用于开发应用程序的框架(Framework),FrameworkElement的Framework指的就是WPF Framework。而FrameworkElement类在UIElement类的基础上添加了很多专门用于WPF开发的API(比如SetBinding方法),所以从这个类开始才算是进入WPF开发柜架。

WPF的内容模型

根据是否可以装载内容、能够装载什么样的内容,WPF的UI元素可以分为如下类型:

名称 注释
ContentControl 单一内容控件
HeaderedContentControl 带标题的单一内容控件
ltemsControl 以条目集合为内容的控件
HeaderedltemsControl 带标题的以条目集合为内容的控件
Decorator 控件装饰元素
Panel 面板类元素
Adorner 文字点缀元素
Flow Text 流式文本元素
TextBox 文本输入框
TextBlock 静态文字
Shape 图形元素

控件的内容可以直接是数据,也可以是控件。当控件的内容还是控件的时候就形成了控件的嵌套(UI布局时尤为常见),被嵌套的控件称为子级控件,所以WPF的UI会形成一个树形结构。

  • 逻辑树(Logical Tree):不考虑控件内部的组成结构,只观察由控件组成的“树”。
  • 可视元素树(Visual Tree):WPF控件往往是由更基本的控件构成的(控件本身就是一棵树),连控件本身的树也考虑在内,比逻辑树更“繁茂”的树。

控件是内存中的对象,控件的内容也是内存中的对象。控件通过自己的某个属性引用着作为其内容的对象,这个属性称为内容属性(Content Property)。“内容属性”是个统称,具体到每种控件上,内容属性都有自己确切的名字——Content、Child、Items或Children。

控件的内容属性与XAML标签的内容存在一定的对应关系,XAML标签的内容区域专门映射了控件的内容属性,下面从语法和常理来解释一下:

  • 严格按照语法来说,控件有内容属性,在XAML里就应该能够使用Atribute=Value 或者属性标签的形式来为内容赋值,如:
<Button Content="OK"/>  <!--或--> <Button>   <Button.Content>     <sys:String>OK</sys:String>   </Button.Content> </Button> 
  • 按照常理来说,控件对应到XAML文档里就是标签,控件的内容就应该是标签的内容、子级控件就应该是标签的子级元素(简称标签的元素),标签的内容是夹在起始标签和结束标签间的代码,上面的代码可以写成:
<Button>   <sys:String>OK</sys:String> </Button> 

各类内容模型详解

把符合某类内容模型的UI元素称为一个族,每个族用它们共同基类来命名

ContentControl族

本族元素的特点如下:

  • 均派生自ContentControl类。
  • 它们都是控件(Control)。
  • 内容属性的名称为Content。
  • 只能由单一元素充当其内容

“只能由单一元素充当其内容”以Button为例,Buton只能接受一个元素作为它的Content,需要一个带图标、文字的Button时要先用一个可以包含多个元素的布局控件把图片和文字包装起来,再把这个布局控件作为Buton的内容(控件的内容也可以是控件)。

ContentControl族包含的控件:Button、ButtonBase、CheckBox、ComboBoxItem、ContentControl、Frame、GridViewColumnHeader、GropItem、Label、ListBoxItem、ListViewItem、NavigationWindow、RadioButton、RepeatButton、ScrollViewer、StatusBarItem、ToggleButton、ToolTip、UserControl、Window。

HeaderedContentControl族

本族元素的特点如下:

  • 它们都派生自HeaderedContentControl类,HeaderedContentControl是ContentControl类的派生类。
  • 它们都是控件,用于显示带标题的数据。
  • 除了用于显示主体内容的区域外,控件还具有一个显示标题(Header)的区域。
  • 内容属性为Content和Header。
  • 无论是Content 还是Header都只能容纳一个元素作为其内容。

HeaderedContentControl族包含的控件:Expender、GroupBox、HeaderedContentControl、TabItem。
下面演示一个以图标为Header、以文字为主体内容的GroupBox,代码如下:

<!--GroupBox.Content标签可以省略--> <GroupBox Margin="10" BorderBrush="SlateBlue">     <GroupBox.Header>         <Image Source="img.jpg" Width="20" Height="20"></Image>     </GroupBox.Header>     <GroupBox.Content>         <TextBlock TextWrapping="WrapWithOverflow" Margin="10" Text="测试内容"></TextBlock>     </GroupBox.Content> </GroupBox> 

ItemsControl族

本族元素的特点如下:

  • 均派生自ItemsControl类。
  • 它们都是控件,用于显示列表化的数据。
  • 内容属性为Items或ItemsSource。
  • 每种ItemsControl都对应有自己的条目容器(Item Container)

本族的包含的控件:Menue、MenuBase、ContextMenu、ComboBox、ItemsControl、ListBox、ListViewe、TabControl、TreeView、Selector、StatusBar,对应的Item Container如下:

ItemsControl名称 对应的Item container
ComboBox ComboBoxItem
ContextMenu MenuItem
ListBox ListBoxItem
ListView ListViewItem
Menu MenuItem
StatusBar StatusBarItem
TabControl TabItem
TreeView TreeViewItem

下面列出属于ItemsControl族元素和其对应的Item Container有ComboBox——ComboBoxItem,ContextMenu——MenuItem,ListBox——ListBoxItem,ListView——ListViewItem,Menu——MenuItem,StatusBar——StatusBarItem,TabControl——TabItem,TreeView——TreeViewItem.

ListBox:在XAML中添加数据

ListBox除了可以显示中规中矩的字符串条目还能够显示更多的元素,如CheckBox、RadioButton、TextBox等,例如下面这段代码:

<!--ListBoxItem标签已省略--> <ListBox x:Name="listbox" Margin="5">     <CheckBox x:Name="chb1" Content="选择1"/>     <CheckBox x:Name="chb2" Content="选择2"/>     <CheckBox x:Name="chb3" Content="选择3"/>     <CheckBox x:Name="chb4" Content="选择4"/>     <Button x:Name="btn1" Content="按钮1"/>     <Button x:Name="btn2" Content="按钮2"/>     <Button x:Name="btn3" Content="按钮3"/> </ListBox> 

表面看上去是ListBox直接包含了一些CheckBox和Buton,实际上这些CheckBox和Buton的父级容器是ListBoxItem。为按钮添加如下的事件代码查看父容器:

private void btn1_Click(object sender, RoutedEventArgs e) {                 Button btn = (sender) as Button;     DependencyObject level1 = VisualTreeHelper.GetParent(btn);     DependencyObject level2 = VisualTreeHelper.GetParent(level1);     DependencyObject level3 = VisualTreeHelper.GetParent(level2);      StringBuilder sbr = new StringBuilder();     sbr.AppendLine("level1:"+level1.GetType().ToString());     sbr.AppendLine("level2:" + level2.GetType().ToString());     sbr.AppendLine("level3:" + level3.GetType().ToString());                MessageBox.Show(sbr.ToString());     }  

ListBox:在代码中添加数据

除非列表里的元素自始至终都是固定的才使用这种直接把UI元素作为ItemsControl内容的方法,如日期等。大多数情况下,UI上的列表会用于显示动态的后台数据,此时交给ltemsControl的就是程序逻辑中的数据了。在代码中为ListBox添加数据的代码如下:

<Grid>     <ListBox x:Name="listbox" Margin="5"/> </Grid> 
List<Employee> empList = new List<Employee>() {     new Employee(){Id = 1, Name ="Tim", Age = 30},     new Employee(){Id = 2, Name="Tom",Age=26},     new Employee(){Id = 3,Name="Guo",Age=26},     new Employee(){Id = 4,Name="Yan",Age=25},     new Employee(){Id = 5,Name="Owen",Age=30},     new Employee(){Id=6,Name="Victor",Age=30 } };  this.listbox.DisplayMemberPath = "Name";  this.listbox.SelectedValuePath = "Id";  this.listbox.ItemsSource= empList;  //Employee类 public class Employee {     public int Id { get; set; }     public string Name { get; set; }     public int Age { get; set; } } 
  • DisplayMemberPath:这个属性告诉ListBox显示每条数据的哪个属性,ListBox会去调用这个属性值的ToString()方法,把得到的字符串放入一个TextBlock(最简单的文本控件),然后再按前面说的办法把TextBlock包装进一个ListBoxItem里。
  • SelectedValuePath:这个属性将与其SelectedValue属性配合使用,当调用SelectedValue属性时ListBox先找到选中的Item所对应的数据对象,然后把SelectedValuePath的值当作数据对象的属性名称并把这个属性的值取出来

DisplayMemberPath 和Selected ValuePath 是两个相当简化的属性。DisplayMemberPath只能显示简单的字符串,想用更加复杂的形式显示数据需要使用DataTemplate;SelectedValuePath也只能返回单一的值,如果想进行一些复杂的操作可直接使用ListBox的SelectedItem和SelectedItems属性,这两个属性返回的是数据集合中的对象

HeaderedItemsControl族

本族控件*除了具有ItemsControl的特性外,还具显示标题的能力**。本族元素的特点如下:

  • 均派生自HeaderedItemsControl类。
  • 它们都是控件,用于显示列表化的数据,同时可以显示一个标题。
  • 内容属性为Items、ItemsSource和Header。

本族控件只有3个:MenuItem、TreeViewItem、ToolBar

Decorator族

本族中的元素是在UI上起装饰效果的,本族元素的特点如下:

  • 均派生自Decorator类。
  • 起UI装饰作用。
  • 内容属性为Child。
  • 只能由单一元素充当内容。

本族元素有:ButtonChrome、ClassicBorderDecorator、ListBoxChrome、SystemDropShadowChrome、Border、InkPresenter、BulletDecorator、Viewbox、AdornerDecorator。

可以使用Border元素为一些组织在一起的内容加个边框,使用ViewBox元素让组织在一起的内容能够自由缩放

TextBlock和TextBox

两个控件最主要的功能是显示文本:

  • TextBlock只能显示文本,不能编辑,又称静态文本,可以使用丰富的印刷级的格式控制标记显示专业的排版效果。由于需要操纵格式它的内容属性是Inlines(印刷中的“行”),同时也保留一个名为Text的属性(当简单地显示一个字符串时可以使用)
  • TextBox则允许用户编辑其中的内容,由于不需要太多的格式显示它的内容是简单的字符串,内容属性为Text

TextBlock属于比较底层的控件,因此它的性能要比Label好一些。如果需求只是纯文本的显示,并且不提供Access key的支持,那么TextBlock是个不错的选择。

Shape族元素

Shape族元素(只是简单的视觉元素,不是控件)是专门用来在UI上绘制图形的一类元素,本族元素的特点如下:

  • 均派生自Shape类。
  • 用于2D图形绘制。
  • 无内容属性
  • 使用Fill属性设置填充,使用Stroke属性设置边线

注:该族控件一般较少使用,用于自定义控件外观。

Panel族元素

所有用于UI布局的元素都属于这一族,本族元素的特点如下:

  • 均派生自Panel抽象类
  • 主要功能是控制UI布局。
  • 内容属性为Children
  • 内容可以是多个元素,Panel元素将控制它们的布局。

ItemsControl和Panel元素内容都可以是多个元素,但ItemsControl强调以列表的形式来展现数据而Panel则强调对包含的元素进行布局,所以ItemsControl的内容属性是Items和ItemsSource而Panel的内容属性名为Children。

本族元素有:Canvas、DockPanel、Grid、TabPanel、ToolBarOverflowPanel、StackPanel、ToolBarPanel、UniformGrid、VirtualizingPanel|VirtualizingStackPanel、WrapPanel。

UI布局(Layout)

WPF的布局是依靠各种布局元素实现的。

布局元素

WPF中的布局元素有如下几个:

  • Grid:网格,可以自定义行和列并通过行列的数量、行高和列宽来调整控件的布局,近似于HTML中的Table。
  • StackPanel:栈式面板,可将包含的元素在竖直或水平方向上排成一条直线,移除元素时后面的元素会自动向前移动填充空缺
  • Canvas:画布,内部元素可以使用以像素为单位的绝对坐标进行定位,类似于Windows Form编程的布局方式。
  • DockPanel:泊靠式面板,内部元素可以选择泊靠方向,类似于在Windows Form编程中设置控件的Dock属性
  • WrapPanel:自动折行面板,内部元素在排满一行后能够自动折行,类似于HTML中的流式布局。

Grid

Grid元素会以网格的形式对内容元素们(即它的Children)进行布局。
Grid的特点如下:

  • 可以定义任意数量的行和列,非常灵活。
  • 行的高度和列的宽度可以使用绝对数值、相对比例或自动调整的方式进行精确设定,并可设置最大和最小值。
  • 内部元素可以设置自己的所在的行和列,还可以设置自己纵向跨几行、横向跨几列。
  • 可以设置Children元素的对齐方向。

Grid适用的场合有:

  • UI布局的大框架设计
  • 大量UI元素需要成行或者成列对齐的情况。
  • UI整体尺寸改变时,元素需要保持固有的高度和宽度比例
  • UI后期可能有较大变更或扩展。

定义Grid的行与列

Grid类具有ColumnDefinitions和RowDefinitions两个属性,分别是ColumnDefinition和RowDefinition的集合,表示Grid定义了多少列、多少行。
XAML代码如下:

<Grid>     <Grid.ColumnDefinitions>         <ColumnDefinition/>         <ColumnDefinition/>                 </Grid.ColumnDefinitions>     <Grid.RowDefinitions>         <RowDefinition/>         <RowDefinition/>     </Grid.RowDefinitions> </Grid> 

C#代码如下:

//添加列 grid.ColumnDefinitions.Add(new ColumnDefinition()); grid.ColumnDefinitions.Add(new ColumnDefinition()); //添加行 grid.RowDefinitions.Add(new RowDefinition()); grid.RowDefinitions.Add(new RowDefinition()); 

行高和列宽的单位

计算机图形设计的标准单位是像素(Pixel),所以Grid的宽度和高度单位就是像素。此外,Grid还接受英寸(Inch)、厘米(Centimeter)和点(Point),如下表所示:

英文名称 中文名称 简写 换算
Pixel 像素 px(默认单位,可省略) 图形基本单位
Inch 英寸 in 1inch=96pixel
Centimeter 厘米 cm 1cm=(96/2.54)pixel
Point pt 1pt=(96/72)pixel

实际使用如下所示:

<Grid>     <Grid.RowDefinitions>         <RowDefinition Height="30px"/>         <RowDefinition Height="30"/>         <RowDefinition Height="0.5in"/>         <RowDefinition Height="1cm"/>         <RowDefinition Height="30pt"/>     </Grid.RowDefinitions> </Grid> 
  • 属性的值为double类型
  • 因为像素是默认单位,所以px可以省略
  • 其他单位也会被转换成像素并显示在Grid的边缘处

行高和列宽的取值

对于Grid的行高和列宽,可以设置三类值:

  • 绝对值:double数值加单位后缀(如上例),一经设定就不会再改变,又称固定值,适用于当控件的宽度和高度不需要改变或者使用空行、空列作为控件间隔时
  • 比例值:double数值后加一个星号(“*”),比例值的最终像素数=比例值的数值/所有比例值的数值和*未被占用空间的像素数,当改变容器的尺寸时使用比例值的行高会保持固有比例,行高和列宽的默认形式就是比例值,没有显式指定行高或列宽时默认值就是1*(1*又可以简写为*)
  • 自动值:字符串Auto,行高或列宽的实际值将由行列内控件的高度和宽度决定,控件会把行列“撑”到合适的宽度和高度,行列中没有控件时行高和列宽均为0

为控件指定行和列遵循的规则

  • 行和列都是从0开始计数
  • 指定一个控件在某行,就为这个控件的标签添加Grid.Row=“行编号“这样一个Attribute,若行编号为0(即控件处于首行)则可省略这个Attribute
  • 指定一个控件在某列,就为此控件添加Grid.Column=”列编号”这样的Attribute,若列编号为0则Attribute可以者略不写
  • 控件需要跨多个行或列,请使用Grid.RowSpan=“行数“和Grid.ColumnSpan=“列数“两个Atribute

StackPanel

StackPanel可以把内部元素在纵向或横向上紧凑排列、形成栈式布局,StackPanel适合的场合有:

  • 同类元素需要紧凑排列(如制作菜单或者列表)。
  • 移除其中的元素后能够自动补缺的布局或者动画。

StackPanel使用Orientation、HorizontalAlignment和VerticalAlignment这3个属性来控制内部元素的布局,如下所示:

属性名称 可取值 描述
Orientation Horizontal
Vertical
决定内部元素是横向累积还是纵向累积
HorizontalAlignment Left
Center
Right
Stretch
决定内部元素水平方向上的对齐方式
VerticalAlignment Top
Center
Bottom
Stretch
决定内部元素竖直方向上的对齐方式

Canvas

Canvas译成中文就是“画布”,在Canvas里布局就像在画布上画控件一样。使用Canvas布局与在Windows Form窗体上布局基本上是一样的,只是WPF的控件没有Left和Top等属性,当控件被放置在Canvas里时就会被附加上Canvas.X和Canvas.Y属性

Canvas适用的场合包括:

  • 一经设计基本上不会再有改动的小型布局(如图标)。
  • 艺术性比较强的布局。
  • 需要大量使用横纵坐标进行绝对点定位的布局。
<Canvas>     <TextBlock Text="用户名:" Canvas.Left="12" Canvas.Top="12"/>     <TextBox Height="23" Width="200" BorderBrush="Black" Canvas.Left="66" Canvas.Top="19"/>     <TextBlock Text="密码:" Canvas.Left="12" Canvas.Top="40.72" Height="16" Width="36"/>     <TextBox Height="23" Width="200" BorderBrush="Black" Canvas.Left="66" Canvas.Top="38"/>     <Button Content="确定" Width="80" Height="22" Canvas.Left="100" Canvas.Top="67"/>     <Button Content="清除" Width="80" Height="22" Canvas.Left="186" Canvas.Top="67"/> </Canvas> 

除非你确定这个窗口的布局以后不会改变而且窗体尺寸固定,不然还是用Grid进行布局弹性会更好。

DockPanel

DockPanel内的元素会被附加上DockPanel.Dock这个属性,这个属性的数据类型为Dock枚举(可取Left、Top、Right和Bottom四个值)。根据Dock属性值,DockPanel内的元素会向指定方向累积、切分DockPanel内部的剩余可用空间。

DockPanel还有一个重要属性——bool类型的LastChildFill(默认值是True),当LastChildFill属性的值为True时,DockPanel内最后一个元素的DockPanel.Dock属性值会被忽略,这个元素会把 DockPanel内部所有剩余空间充满

实际使用如下:

<Grid>     <DockPanel>         <TextBox DockPanel.Dock="Top" Height="25" BorderBrush="Black" />         <TextBox DockPanel.Dock="Left" Width="150" BorderBrush="Black"/>         <TextBox BorderBrush="Black"/>     </DockPanel> </Grid> 

WrapPanel

WrapPanel内部采用的是流式布局,使用Orientation属性来控制流延伸的方向,使用HorizontalAlignment和VerticalAlignment 两个属性控制内部控件的对齐。在流延伸的方向上,WrapPanel会排列尽可能多的控件,排不下的控件将会新起一行或一列继续排列。
实际使用如下:

<WrapPanel>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/>     <Button Width="50" Height="50" Content="OK"/> </WrapPanel> 

WPF之控件布局

总结

形而上者谓之道,形而下者谓之器。WPF的内部机理可以说是WPF的“道”,动手实践写程序可以说是WPF的“器”。

参考资料

WPF控件和布局
控件与布局(WPF)
WPF学习二:TextBlock和Label的区别