WPF之控件布局
控件概述
程序的本质是“数据+算法”——用户输入原始数据,算法处理原始数据并得到结果数据。程序可以使用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,如Electron、NanUI等。
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是构建在.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的“器”。