目录:点击这里
上一篇:【翻译】Pro.Silverlight.5.in.CSharp.4th.Edition - 第二章 XAML 03
第三章 布局
在用户界面设计这档子事上,有一半的工作是要将内容通过一种吸引人、使用并且灵活的方式呈现出来。在寄宿于浏览器的应用程序中,这种工作尤其棘手,因为应用程序可能在不同的电脑和设备上运行,这些设备的显示分辨率和显示尺寸五花八门,而我们又无法控制Silverlight内容所在的浏览器窗口的尺寸。
幸运的是,Silverlight继承了WPF的极其灵活的布局模型的最重要的部分。使用这种布局模型,我们可以将内容放在各种布局容器(container)中。每个容器都有它自己的布局逻辑——有的是叠放元素,有的是将元素排列在网格的不可见的单元格里,还有的是使用硬编码的坐标来布局。如果激情澎湃的话,我们还可以自己创建一种自定义布局逻辑的容器。
本章中,我们会了解到如何使用布局容器来创建Silverlight页面的可视结构。我们会把大部分精力用来学习Silverlight的各种核心布局容器,比如StackPanel、Grid、和Canvas。在掌握了这些基本的内容之后,我们便可以发挥潜力来创建带有自定义布局逻辑的布局容器。另外,本章中我们还会介绍如何创建一个浏览器外运行并且全屏显示的应用程序。
布局容器
一个Silverlight窗口只能容纳一个元素。为了让多个元素能容纳进去并且创建更实用的用户界面,我们需要在页面中放置一个容器然后将其它元素放置于这个容器中。最终布局的效果则决定于我们所使用的容器。
所有的Silverlight布局容器都是继承于抽象类System.Windows.Controls.Panel的面板(如图3-1所示)
图3-1 Panel类的继承关系
Panel类有两个公共的属性:Background和Children。Background是用于填充面板背景的笔刷。Children则是panel中存储的元素的集合。(这是元素的第一级——换句话说,这些元素可能自身也容纳了更多的元素。)Panel类也包含一些内在的特性,我们可以利用这些特性来创建自己的布局容器(本章后续内容会有讲解)。
本身Panel这个基类只是其它更多专门的类的一个起点。Silverlight提供了三个继承于Panel的类用于布局,另外Silverlight Toolkit额外增加了两个。这些布局容器都在表3-1中有所说明(按照本章中介绍的顺序进行排列)。与其他Silverlight控件和大多数可视的元素一样,这些类都在System.Windows.Controls这个命名空间下。
表3-1 核心布局面板
名称 | 说明 |
StackPanel | 让元素按照水平或者垂直的方式来排列。这种布局容器在页面逻辑复杂的较大的页面中的小区域布局使用比较广泛。 |
WrapPanel | 将元素按照行或者列依次排放。具体的说则是:在水平方向上,WrapPanel将元素从左至右排列,超出行的长度后换行继续排列剩下的元素;在垂直方向上,WrapPanel将元素从上至下排列,超出列的高度后换列继续排列剩下的元素。这个布局容器在Silverlight Toolkit中。 |
DockPanel | 沿着容器的边缘排列元素。这个容器也在Silverlight Toolkit中。 |
Grid | 通过一个不可见的网格将元素按照行、列来排列。这是最复杂也是最常用的布局容器之一。 |
Canvas | 通过绝对坐标来设置元素的位置。这个布局容器是最简单的,但是也最不灵活。 |
布局容器也可以相互之间嵌套。一个典型的用户界面就是从Grid(Silverlight最扛把子的容器)开始,其中在包含了其他布局容器用来排列各个小区域的各组元素,比如文本框、列表的各个项、工具栏的图标、一列按钮等等。
备注:有一个特殊的布局容器没有放在表3-1中:VirtualizingStackPanel。它的布局方式和StackPanel一样,不过他使用了一种叫做virtualization的存储优化(memory-optimization)技术。VirtualizingStackPanel可以让ListBox这样的列表控件在性能下降的程度可以忽略的情况下显示成千上万的条目,因为VirtualizingStackPanel只针对当前可见的条目创建对象。但是尽管我们会使用VirtualizingStackPanel来创建自定义模板和控件(详见第15章),我们一般也不会选择用它来给页面的元素做布局排列,因此本章中暂时不介绍这个控件。
面板的背景
所有的面板元素都用Background这个属性来设置元素的背景。我们很自然会认为Background这个属性是一些color对象;但是实际上这个属性所使用的是种更强大的对象:Brush。这样的设计方式可以让我们选择使用纯色(来自SolidColorBrush类)或者其他更绚的效果(比如LinearGradientBrush类的渐变或者ImageBrush类的位图)来填充背景和前景,非常灵活。在本章节,我们先简单学习一下SolidColorBrush,稍后在第9章我们再深入研究。
备注:Silverlight中的所有Brush类都在System.Windows.Media命名空间下。
比如说,如果我们要将整个页面设置为淡蓝色的北京,我们只需要调整根面板的背景。代码如下:
layoutRoot.Background = new SolidColorBrush(Colors.AliceBlue);
从技术的角度来说,每个Color对象都是System.Windows.Media命名空间下的Color结构体的一个实例。在Colors这个类中有大量的线程的颜色对象可以供我们使用,这些颜色对象都是静态属性。(这些属性的命名是依据颜色本身在实际中的叫法。)本例中便是使用这里面的其中一个(Colors.AliceBlue)来创建一个SolidColorBrush对象,然后将这个SolidColorBrush对象设置为根面板的背景笔刷——最终的效果就是背景给刷成了淡蓝色。
提示:Silverlight也包含SystemColors类(这个类提供的Color对象与Windows系统相匹配)。比如说,SystemColors.ActiveColorBorder对应的就是用于填充前端窗口的边框的颜色。在某些情况下,我们可能要使用这个类来保证我们的应用程序的配色方案的协调一致,尤其是当我们的应用程序是浏览器外运行模式的时候(第18章中将会有所探讨)。
Colors和SystemColors这两个类用起来很方便顺手,当然还有其他方式来设置颜色。我们还可以通过设置Red、Green和Blue(即RGB)的值以及相应的用来标识透明度的Alpha值来创建一个Color对象。这四个值都是0到255之间的一个数值:
int red = 0; int green = 255; int blue = 0; layoutRoot.Background = new SolidColorBrush(Color.FromArgb(255, red, green, blue));
在调用Color.FromArgb()方法的时候我们可以通过设置Alpha的值来得到一个半透明效果的颜色。Alpha值为255的时候标识完全不透明,为0则是完全透明。
当然,大多数情况下我们都是在XAML中设置颜色,而且设置的方式比在代码中简便很多:不需要创建Brush对象,只需要设置相应的颜色名称(比如Blue)或者颜色的值(比如#FFAABBCC)。Background属性对应的类型转换器会自动将它转换成颜色对应的SolidColorBrush对象。下面是一个示例的XAML标记内容:
<Grid x:Name="layoutRoot" Background="Red">
它对应的完整语法形式如下:
<Grid x:Name="layoutRoot"> <Grid.Background> <SolidColorBrush Color="Red"></SolidColorBrush> </Grid.Background> </Grid>
当我们要创建其他类型的笔刷(比如LinearGradientBrush)来填充背景的时候就需要用这种较长的语法格式。
如果我们要使用颜色代码,则需要将R、G和B的值转为十六进制。具体有两种格式——#rrggbb或者#aarrggbb(不同之处是后者包含alpha值)。由于数值是十六进制,因此A、R、G和B值都是两位数字。下面的这个示例代码就是使用#aarrggbb这样的标记创建了和上面的示例相同颜色的背景:
<Grid x:Name="layoutRoot" Background="#FFFF0000">
其中alpha值是FF(255),red值是FF(255),green和blue值都是0。
默认情况下,布局面板的Background属性被设置为一个空引用,效果和下面的标记等效:
<Grid x:Name="layoutRoot" Background="{x:Null}">
当面板的背景设置为null的时候,这个面板下方的所有内容都会透过显示出来,这个效果就像将背景设置为了透明(Colors. Transparent)。不过请务必注意,这两者有一个非常重要的不同点:位于下方的布局容器无法收到鼠标事件。
备注:笔刷支持变更后自动通知机制。换句话说,如果我们将和一个笔刷应用到一个控件上然后修改这个笔刷,那么这个控件会自己相应地更新。
边框
布局容器支持填充背景,但是不能填充边框的外边线。不过有种元素——Brush,弥补了这个不足。
Border类比较简单:它内部只能嵌套一个元素(一般是个布局面板);然后就是自身的背景或者边框——仅此而己。表3-2的内容即是我们要掌握Border这个知识点所需要的全部内容。
表3-2 Border类的属性
名称 | 说明 |
Background | 使用Brush对象设置边框中所有元素下方的背景。可以使用纯色或者更独特的东西。 |
BorderBrush | 使用Brush对象设置Border对象的边框的填充内容。最直接的方式用SolidColorBrush来创建一个纯色边框。 |
BorderThickness | 设置边框的每个边的宽度。BorderThickness属性对应的是System.Windows.Thickness结构体(由上、下、左、右四部分组成)的实例。 |
CornerRadius | 可将边框设置为圆角。值越大则边框越圆滑。 |
Padding | 设置边框和边框内部内容之间的距离大小——内边距。(作为对比顺便一提:Margin若用于Border,则设置边框和边框外的距离——外边距) |
下面这个示例代码的最终效果是在一个圆角边框中有一个按钮:
<Border Margin="25" Background="LightYellow" BorderBrush="SteelBlue" BorderThickness="8" CornerRadius="15"> <Button Margin="10 Content="Click Me"></Button> </Border>
这个示例中边框和按钮之间有一些距离,是由Margin来实现的(这个特性将会下面的小节中讲到)。最终运行效果如图3-2所示。
图3-2 一个基本的边框示例
用StackPanel布局
StackPanel是最简单的布局容器之一。它将内部的子元素用单行或者单列的方式摆放开来,摆放的排列顺序根据他们在StackPanel中的先后顺序来定。
以下面这个包含一个TextBlock和四个Button的布局来举例说明:
<UserControl x:Class="Layout.SimpleStack" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel Background="White"> <TextBlock Text="A Button Stack"></TextBlock> <Button Content="Button 1"></Button> <Button Content="Button 2"></Button> <Button Content="Button 3"></Button> <Button Content="Button 4"></Button> </StackPanel> </UserControl>
运行后的效果如图3-3所示。
图3-3 StackPanel运行后的效果
默认情况下,StackPanel是从上到下排列元素,并且让每个元素的高度按照其显示的内容所需要的高度来自动调整。这意味着在本例中,TextBlock和四个Button都设置成能够合理地容纳其自身显示的文字信息的高度了。另外,所有的元素的宽度都拉伸到StackPanel的完整宽度,本例中也相当于页面的宽度。
可以看出本例中页面的Height和Width这两个属性并没有设置。这样的话,页面为了适应整个Silverlight内容区域会自动扩大自己的尺寸(本例中是占据整个浏览器窗口)。本章的大部分示例都是采用这个处理方式,因为这种方式在对面各种不同的布局容器时更易于调整和测试。我们可以通过调整浏览器窗口的尺寸来看布局容器是如何调整自己的尺寸大小来适应不同的页面尺寸的。
备注:在学完所有的布局容器后,我们会深入研究页面尺寸方面的内容,并且会学习当所布局的内容不适应浏览器窗口的情况下的各种处理方式。
StackPanel通过设置Orientation属性还可以让元素横向排列:
<StackPanel Orientation="Horizontal" Background="White">
这样,元素会按照最小宽度(当然前提是能足够显示出其文本内容的最小宽度)并且拉伸至容器的完整高度(参见图3-4)
图3-4 横向布局的StackPanel
很明显,要达到真实应用程序所需要灵活布局这远远不够。不过幸运的是,我们可以使用各种布局属性来给StackPanel以及其它布局容器的运作进行微调,接下来的内容即将介绍布局属性。
布局属性
尽管布局效果是由布局容器所决定,不过子元素也有一定的话语权。事实上,通过各种布局属性(Layout Properties)的协调,容器中的子元素得以和布局容器一起合作实现最终的布局效果。表3-3介绍了各种布局属性。
名称 | 说明 |
HorizontalAlignment | 这个属性决定了当布局容器横向有额外的可用空间时的子元素的摆放方式。可以选择Center(居中),Left(靠左摆放),Right(靠右摆放)或者Stretch(拉伸) |
VerticalAlignment | 与上面对应,这个属性决定了当布局容器纵向有额外的可用空间时的子元素的摆放方式。可以选择Center(居中),Top(靠上摆放),Botton(靠下摆放)或者Stretch(拉伸) |
Margin | 形象地说,这个属性的作用是让元素的外围有一些空间(即设置外边距)。Margin属性对应的是System.Windows.Thickness这一结构体的实例,由上、下、左、右四部分组成 |
MinWidth和 MinHeight | 这两个属性用于设置元素的最小尺寸。如果这个最小尺寸比容纳它的布局容器的尺寸还要大,那么元素会相应的裁剪以适应容器。 |
MaxWidth和MaxHeight | 这两个属性用于设置元素的最大尺寸。如果布局容器有更多的可用空间,元素也不会增大到超出这两个最大尺寸,就算是HorizontalAlignment和VerticalAlignment这两个属性设置为Stretch |
Width和Height | 这两个属性适用于精确地设置元素的尺寸。如果元素的HorizontalAlignment和VerticalAlignment属性设置的是Stretch,那么Width和Height属性的设置会覆盖掉它们。不过,需要注意的是,如果这两个属性的值超出了MinWidth、MinHeight、MaxWidth和MaxHeight这四个属性的原则(比如宽度设置成小于最小宽度、高度设置成大于最大宽度等这样不合实际的情况),那么这两个属性将不起作用。 |
所有这些属性都继承于FrameworkElement这个基类,因此它们适用于所有Silverlight页面中能用到的所有元素。
备注:在第2章中我们已经了解到,不同的布局容器可以给其子元素提供不同的附加属性。比如说,Grid对象中的所有子元素都可以通过设置Row和Column属性来决定它在Grid中的位置。我们可以通过附加属性给特定的布局容器设置一些特定的信息内容。另一方面,表3-3中的布局属性对大多数布局面板基本上已经足够了;因此,这些属性就被定义为FrameworkElement这个基类的一部分。
排列方式(Alignment)
为了了解这些属性是如何工作的,我们从另外一个角度来看看图3-3所显示StackPanel布局效果。这个例子展现了这样的信息:一个纵向排列的StackPanel;VerticalAlignment这个属性的设置无效(因为每个元素的高度都和容器最大高度对应)。不过,HorizontalAlignment这个属性在这里起了关键作用,因为它决定了每个元素在行这一方面的摆法位置。
通常来说,对于文本块TextBlock来说,HorizontalAlignment默认是靠左摆放;对于按钮,HorizontalAlignment默认则是拉伸。这就是途中每个按钮都占绝了整个列宽的原因。现在我们用按照下面的方式来修改这些标记细节:
<StackPanel Background="White"> <TextBlock HorizontalAlignment="Center" Text="A Button Stack"></TextBlock> <Button HorizontalAlignment="Left" Content="Button 1"></Button> <Button HorizontalAlignment="Right" Content="Button 2"></Button> <Button Content="Button 3"></Button> <Button Content="Button 4"></Button> </StackPanel>
图3-5显示了最终的运行效果。前两个Button所分配的宽度为能显示出完整文本信息的最小宽度,而且按照标记中摆放类型分别靠左和靠右摆放;后两个按钮则被拉伸至整个StackPanel的宽度。如果我们调整页面的尺寸,我们会发现TextBlock标签会一直保持在页面的中间,而前两个按钮则会保持停靠在页面的两侧。
图3-5 StackPanel中各种对齐方式的Button
备注:StackPanel自己也有HorizontalAlignment和VerticalAlignment属性。默认情况下,这两个属性值都设置为Stretch,因此StackPanel默认是充满它的容器的。本例中StackPanel就是充满整个页面(因为本例中StackPanel的容器就是整个页面)。如果我们将VerticalAlignment设置为另一个不是Stretch的值,那么StackPanel在纵向上的高度会被设置成可以满足其内部包含的所有元素所需要的最小高度。
外边距(Margin)
目前所看到的StackPanel示例很明显存在设计缺陷。一个设计良好的页面不应该只是仅仅包含元素——还应该包括元素之间的间距空间的处理。我们可以用Margin来实现这个间距,这样可以让页面元素显得不是那么拥挤。
首先我们需要了解到:设置单个宽度值会应用于四条边,就象这样:
<Button Margin="5" Content="Button 3"></Button>
或者,我们可以按照左、上、右、下的顺序给控件的每条边单独设置间距宽度:
<Button Margin="5,10,5,10" Content="Button 3"></Button>
如果是在代码中处理,那需要使用结构体Thickness:
cmd.Margin = new Thickness(5);
要想给控件设置合适的边距没那么简单,因为需要考虑到设置边距后会给相邻的控件之间带来什么影响。比如说现在有两个纵向排列的按钮,其中上面的按钮设置了5像素的下边距,下面的按钮设置了5像素的下边距,那么实际上这两个按钮的真实间距是10像素。
理想情况下,我们可以尽量让不同的间距设置保持一致,并且避免给不同的边设置不同的间距。比如在上面的StackPanel示例中,我们可以将文本块、四个按钮和面板自身设置相同的边距,如下所示:
<StackPanel Margin="3" Background="White"> <TextBlock Margin="3" HorizontalAlignment="Center" Text="A Button Stack"></TextBlock> <Button Margin="3" HorizontalAlignment="Left" Content="Button1"></Button> <Button Margin="3" HorizontalAlignment="Right" Content="Button 2"></Button> <Button Margin="3" Content="Button 3"></Button> <Button Margin="3" Content="Button 4"></Button> </StackPanel>
这样处理后,上下两个按钮之间的间距(即两个按钮的边距值的和)与页面边缘的按钮到StackPanel这个容器的相印边框的间距相同(按钮的边距值和StackPanel的边距值的和)。图3-6显示了运行后的效果,图3-7则逐项解释了margin设置的原理。
图3-6 给元素设置margins属性
图3-7 margins相互之间组合的原理
最小值、最大值以及明确尺寸
所有的元素都包含Height和Width这两个属性用来设置明确的尺寸。不过,可以设置明确的尺寸不代表就一定要这么处理。大多数情况下还是让元素根据实际情况去自适应更好。以按钮为例,如果按钮显示的文本变多的时候,按钮的长度应该相应的变长;如果有必要,我们可以通过设置最大值和最小值将元素限制在一个可接受的尺寸范围内。而如果我们设置了具体的尺寸信息,则可能使得布局容易收到破坏:比如无法适应尺寸的变化或者(更糟糕情况)将不适应其中的内容信息截去无法显示在页面中。
比如说,我们认为StackPanel中的按钮应该需要根据StackPanel的尺寸来进行拉伸,但是按钮的长度不能超过200像素,而且也不能小于100像素。(按钮默认的最小宽度是75像素。)那么我需要按照下面这样来编写XAML:
<StackPanel Margin="3"> <TextBlock Margin="3" HorizontalAlignment="Center" Text="A Button Stack"></TextBlock> <Button Margin="3" MaxWidth="300" MinWidth="200" Content="Button 1"></Button> <Button Margin="3" MaxWidth="300" MinWidth="200" Content="Button 2"></Button> <Button Margin="3" MaxWidth="300" MinWidth="200" Content="Button 3"></Button> <Button Margin="3" MaxWidth="300" MinWidth="200" Content="Button 4"></Button> </StackPanel>
提示:此刻,你可能会在想:有没有其它更简便的方式来处理这样不同的元素的一系列属性设置都一样的情况(比如上面的几个按钮的Margin="3" MaxWidth="300" MinWidth="200"设置就像是个标准一样)。答案就是样式(styles)——这是一种更可以实现属性设置复用的特性。在第14章中我们会详细介绍。
针对想上面这样的没有设置具体的Height和Width的按钮,StackPanel处理这个按钮的布局的时候会参照如下这些信息:
- 最小尺寸:每个按钮的尺寸保证不会小于最小尺寸。
- 最大尺寸:每个按钮的尺寸保证不会超过最大尺寸。(除非是误将最大尺寸的值设置的比最小尺寸的值还小)。
- 按钮显示内容:如果按钮显示的内容需要更大的宽度,那么StackPanel就会尝试将按钮的尺寸增大。
- 容器的尺寸:如果按钮的最小宽度设置得超过了StackPanel的宽度,那么按钮会的超出尺寸部分被剪掉无法显示。但是如果按钮没有设置最小宽度或者最小宽度小于StackPanel的宽度,那么按钮的宽度就不会增大到超出StackPanel,即使不能显示出按钮上的所有内容。
- 横向对齐:默认情况下,按钮的HorizontalAlignment属性值是Stretch,因此StackPanel会尝试将按钮的宽度拉伸到整个面板的宽度。(在没有最大尺寸这种属性控制的情况下)
要理解这个处理方式,我们就得先认识到最小尺寸和最大尺寸是给按钮设置了绝对的尺寸限制。当有这些限制的时候,StackPanel会以按钮自身所需要的尺寸(用于适应其显示内容)以及对齐方式的设置为优先原则。
图3-8阐明了StackPanel的这种处理方式。左边的图是页面尺寸为最小值的情况:每个按钮都是200像素的宽。如果这个时候我们再将页面的宽度调小,那么页面中的每个按钮的右边超出部分将会被剪掉而显示不出来。(本章的后续内容将讲到可以使用滚动条来处理这种情况。)
当我们放大页面的尺寸的时候,按钮的宽度也会随之变大,直到达到300像素这个最大值的限制。如果此时继续将页面拉伸,那么超出的空白空间会逐渐均分在按钮的两侧(如图3-8右边部分所示)
图3-8 调整页面尺寸,相应按钮尺寸也调整
备注:在某些情况下,我们可能需要用代码来检查元素在页面中的真实尺寸。这时候Height和Width这两个属性不起作用,因为这两个属性标记的是最初预期的尺寸设置,这个可能和实际上已经在页面中渲染出来的尺寸不一致。在理想情况下,我们会让元素的尺寸自动适应它在页面运行时所显示的内容,这样Height和Width属性就根本不需要设置。不过我们可以用ActualHeight和ActualWidth这两个属性来获取元素在页面中渲染的真实尺寸。另外要记住的是,当页面大小或者元素发生调整的时候,这两个属性的值也可能随之变化。