Silverlight 继承了 WPF 最重要的组成部分:极其灵活的布局模式。你可以将内容组织到不同的容器中。每个容器有其本身的布局逻辑,一种用来摆放元素;另一种用来将元素排列在不可见的单元格中;最后一种使用一个硬编码的坐标系。
最顶层的 UserControl 定义了一个 Silverlight 页面,仅能容纳一个元素。要想构建一个强大的用户界面,需要将容器放在网页上,然后将其他的元素添加到这个容器中。
Silverlight 提供了 3 个 Panel 的派生类来排列布局:StackPanel、Canvas、Grid。StackPanel 在系列文章 Part.3 中已经介绍过,它将一组元素从上到下或从左到右排列(取决于 Orientation 属性的值)。本文接着会介绍另 2 种。
Canvas
Canvas(画布)是 Silverlight 的 3 个布局容器中最简单的一个。它允许你用精确的坐标来放置元素!对于设计丰富的数据驱动窗体和标准对话框,它不是个好选择,但如果要创建一些略有不同的东西,比如绘图工具的绘画界面,它就是一个很好的工具。Canvas 是最轻量级的布局容器,它不需要设置众多的子参数,也就没有包含任何复杂的布局逻辑。相反,它只需要你设定它们确切的位置以及大小。
为了在 Canvas 中定位一个元素,需要使用附加属性。这是来自 WPF 的一个概念。从本质上说,附加属性是由一个类所定义但却由其他类来使用的属性。
Canvas 提供了一个很好的例子。要在 Canvas 中定位元素,需要设置 3 个属性:Left、Top、ZIndex。最简单的设计(并未被 Silverlight 使用)是在 FrameworkElement 基类中定义这 3 个属性,因为所有的元素都继承于 FrameworkElement,这可以保证所有元素都拥有一样的布局属性,而这些属性都能满足 Canvas的需要。然而,这种最简单的设计方式却会导致极为严重的问题。首先,数十种属性可能将 FrameworkElement 弄得很乱,因为不同的布局容器需要跟踪不同的细节。一旦开始混乱,除了正在被特定容器使用的属性,其他的属性都将失去效力。而且,你想用不同的布局机制来设计一个新的布局容器,这将变得不可能,因为你不可能去修改 FrameworkElement 类。
附加属性提供了一个方案来解决上述的问题。使用附加属性来为 Canvas 中的元素定义 Left、Top、ZIndex 属性。Canvas 中的这些元素可以“借用”这些已定义的属性进行自我定位。因为 Canvas 是拿到这些设置然后在这些设置的基础上定位,而不是对那些包含在 Canvas 中的元素进行设置。
要在 XAML 中设置附加属性,要使用一种两段式的语法。这两部分用句点隔开,据点左边是要定义属性的类的名称,右边是该属性的名称:
<Canvas>
<TextBlock x:Name="lblMessage" Text="Hello world."
Canvas.Top="30" Canvas.Left="30" ></TextBlock>
</Canvas>
Canvas 使用绝对定位,所以没必要使用会影响到其他布局容器的属性,如 Margin、Padding、HorizontalAlignment、VerticalAligment,这些都不会影响到 Canvas 使用的布局逻辑。
如果要使用编程的方式来更改附加属性,语法稍微复杂一些。必须调用形如 ClassName.SetPropertyName() 这样的方法,还必须传入两个参数:你更改的元素及要为该元素设置的新值。要获取值可用相应的 ClassName.GetPropertyName() 。
Canvas.SetTop(lblMessage, 100);
容易混淆的是,即便这个元素不在正确的容器中,你也可以在该元素上设置附加属性,但即便被设置,也不会起到任何效果。
1. 在 Canvas 中分层元素
Canvas.ZIndex 控制元素的分层布局。一般,你添加的所有元素都有相同的 ZIndex,即 0。相同的 ZIndex 使元素按照在 XAML 标记中被声明的顺序排列,越晚声明的元素排的越靠上。
不过,通过增加 ZIndex 值可将任何一个元素提到更高的位置,排列的优先权将高于在 XAML 中声明的顺序。
<Canvas>
<Rectangle Canvas.Left="60" Canvas.Top="80" Canvas.ZIndex="1" Fill="Blue"
Width="50" Height="50"></Rectangle>
<Rectangle Canvas.Left="70" Canvas.Top="120" Fill="Yellow"
Width="100" Height="50"></Rectangle>
</Canvas>
现在,蓝色矩形将出现在黄色矩形的上方,即使它是先在 XAML 标记中声明的:
如果要在程序代码中更改一个元素的位置,只需要调用 Canvas.SetZIndex() 方法即可。但是,你只能自行追踪最高或最低的 ZIndex 值。
2. 拖拉椭圆
我们通过一个简单的例子来理解这些概念。
每次单击画布,一个红色圆圈出现。单击一个圆圈,它变为绿色。释放圆圈,它变为橙色。此圆圈可以拖动位置。画布中出现多少圆圈以及可以拖动多少次,本例中不作限制。
每一个圆圈都是 Ellipse 元素的一个实例,它只是一个有颜色的形状,是二维绘图的一个基础成分。所有的布局容器都有一个 Children 属性用来承载子元素集合。这使你很容易能将一个元素放入一个容器中。
以代码来说明是最容易让你理解的。首先声明了 2 个实例变量来记录拖动状态和点击时鼠标的初始位置。与 ASP.NET 不同,在 Silverlight 程序中使用实例变量记录状态完全可以。因为它在 Silverlight 程序整个生命周期中都将驻留在内存中。而 Web 程序的实例变量是不可靠的,每次回发后再呈现网页,它的值都会被删除。
/// <summary>
/// Keep track of when an ellipse is being dragged
/// </summary>
private bool isDragging = false;
/// <summary>
/// When an ellipse is clicked, record the exact position where the click is made.
/// </summary>
private Point mouseOffset;
以下是完整的代码:
private void canvas_Click(object sender, MouseButtonEventArgs e)
{
// Create an ellipse (unless the user is in process of dragging another one).
if (!isDragging)
{
Ellipse ellipse = new Ellipse();
ellipse.Fill = new SolidColorBrush(Colors.Red);
ellipse.Width = 50;
ellipse.Height = 50;
// Use the current mouse position for the center of the ellipse.
Point point = e.GetPosition(this);
ellipse.SetValue(Canvas.TopProperty, point.Y - ellipse.Height / 2);
ellipse.SetValue(Canvas.LeftProperty, point.X - ellipse.Width / 2);
// Watch for left-button clicks.
ellipse.MouseLeftButtonDown += ellipse_MouseDown;
// Add the ellipse to the Canvas.
parentCanvas.Children.Add(ellipse);
}
}
private void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Dragging mode begins.
isDragging = true;
Ellipse ellipse = sender as Ellipse;
// Get the position of the click relative to the ellipse
// so the top-left conner of the ellipse is (0,0)
mouseOffset = e.GetPosition(ellipse);
// Change the ellipse color
ellipse.Fill = new SolidColorBrush(Colors.Green);
// Watch this ellipse for more mouse events
ellipse.MouseMove += ellipse_MouseMove;
ellipse.MouseLeftButtonUp += ellipse_MouseUp;
// Capture the mouse. This way you'll keep receiveing the MouseMove
// event even if the user jerks the mouse off the ellipse
ellipse.CaptureMouse();
}
private void ellipse_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Ellipse ellipse = sender as Ellipse;
// Get the position of the ellipse relative to the Canvas.
Point point = e.GetPosition(this);
// Move the ellipse
ellipse.SetValue(Canvas.TopProperty, point.Y - mouseOffset.Y);
ellipse.SetValue(Canvas.LeftProperty, point.X - mouseOffset.X);
}
}
private void ellipse_MouseUp(object sender, MouseEventArgs e)
{
if (isDragging)
{
Ellipse ellipse = sender as Ellipse;
// Change the ellipse color
ellipse.Fill = new SolidColorBrush(Colors.Orange);
// Don't watch the mouse events any longer.
ellipse.MouseLeftButtonUp -= ellipse_MouseUp;
ellipse.MouseMove -= ellipse_MouseMove;
ellipse.ReleaseMouseCapture();
isDragging = false;
}
}
网格
网格 Grid 是 Silverlight 中最强大的布局容器。当你在 VS 中添加新的 XAML 文件时,它会自动添加 Grid 标签作为第一层的容器,该容器嵌套在 UserControl 元素中。
Grid 将元素放置到多行和多列组成的看不见的网格中。尽管多个元素可以重叠的放置在一个单元格内,但通常一个单元格放置一个元素更为合理。当然,这个元素本身可能是另一个布局容器,用于组织属于它自己的一组控件。
尽管网格被设计为看不见的,你仍然可以将 Grid.ShowGridLines 设为 true 来显现,这非常方便与调试,让你了解网格如何分成细小的区域的。这个功能非常重要,因为可以让你精确控制网格所选择的列宽和行高。
使用对象来填充 Grid.ColumnDefinitions 和 Grid.RowDefinitions 集合,从而创建列和行。如下标记可创建两行三列的网格:
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
</Grid>
切换至设计视图,你会发现所有的行与列均匀共享该空间,每个单元格的大小完全相同,它们的大小取决于整个页面的大小。
要把单独的元素放入一个单元格,需要使用 Row 和 Column 这两个附加属性。这两个属性采用默认值为 0 的索引数字(放在第一行第一列的元素可以不指定这两个属性,偷偷懒~)。见下面的示例:
<Grid x:Name="LayoutRoot" Background="White" ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="Top Left"></Button>
<Button Grid.Row="0" Grid.Column="1" Content="Middle Left"></Button>
<Button Grid.Row="1" Grid.Column="2" Content="Bottom Right"></Button>
<Button Grid.Row="1" Grid.Column="1" Content="Bottom Middle"></Button>
</Grid>
可以看出,网格在 Web 页面中可以自适应大小。要使用这种设计,必须移除页面最上方 UserControl 标签的 Height 和 Width 属性。
1. 调整行和列
网格可以创建相应大小的行与列的集合。不过你仍可以改变每一行和每一列尺寸的定义:
- 绝对大小:使用像素单位定义精确的大小。这是最精细,也是用处最小的策略。对于处理不断变化的内容大小,容器大小,本地化策略等,它不够灵活。
- 自动调整大小:每行或每列都会被赋予精确的空间总量,没有多余的空间。这是最有用的尺寸模式之一。
- 按比例调整大小:上图显示的就是。
混合使用这些策略,才能达到最大限度的灵活性。
<ColumnDefinition Width="100"></ColumnDefinition> // 使用绝对大小策略将绝对宽度设置为 100 像素:
<ColumnDefinition Width="Auto"></ColumnDefinition> // 自动调整大小模式
<ColumnDefinition Width="*"></ColumnDefinition> // 按比例调整大小
比例语法来自 Web 世界,它在 HTML 帧页面中被使用。例如下面的语法可使第一行只有第二行高度的一半:
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="2*"></RowDefinition>
2. 嵌套布局容器
网格的作用的确让人印象深刻。不过真实的用户界面都会结合多个布局容器。它们可能会排列多个网格,或者将网格和其他布局容器混合使用。
下面的标记创建右下角有 OK 和 Cancel 按钮的基础对话框,然后是一个 TextBlock 大区域,大小可根据文字内容自适应。设置 Alignment 属性,页面上所有内容会居中显示:
<Grid x:Name="LayoutRoot" Background="SteelBlue" ShowGridLines="True"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Margin="10" Grid.Row="0" Foreground="White"
Text="This is a simply test of nested containers."></TextBlock>
<StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
<Button Margin="10,10,2,10" Padding="3" Content="OK"></Button>
<Button Margin="2,10,10,10" Padding="3" Content="Cancel"></Button>
</StackPanel>
</Grid>
乍一看,嵌套布局容器似乎比使用坐标将控件精确定位在精确的位置上花费稍多一些的工作。但用户界面的可扩展性却可以补偿这里付出的代价。如果想把 2 个按钮放在页面底部居中显示,只需要改变 StackPanel 的 Alignment 属性即可:
<StackPanel Grid.Row="1" HorizontalAlignment="Center" Orientation="Horizontal">
3. 跨行和跨列
另外两个附加属性可以使一个元素跨越多个单元格:RowSpan 和 ColumnSpan 。
<Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Content="Span Button"></Button> // 占用2行 在第1列
<Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2"
Content="Span Button"></Button> // 2行 2列
当需要将元素排列在一个由分割线或者大段内容隔开的表格式结构中的时候,这个功能非常方便。
你可以重写上例的对话框,将网格设计为2行3列,文本框跨越所有3列,第二行的后2列摆放两个按钮。这里就不再重写了。这个代码会让人觉得布局不清晰,也不合理。它使你很难在这个基础上再向已有的 Grid 结构里添加新的内容。那怕要做细小的添加,你也不得不创建一组新列。
当你为一个页面选择布局容器时,你不仅要关注得到正确的布局,而且要建立一个易于维护且有可扩展性的布局结构。一个好的经验法则是为一次性的布局任务(例如,布置一组按钮)使用一些较小的布局容器,例如 StackPanel。另一方面,如果需要在页面的多个区域采取统一的结构,Grid 将是一种标准化布局不可或缺的工具。