您的位置:首页 > 编程语言 > C#

【翻译】Pro.Silverlight.5.in.CSharp.4th.Edition - 第三章 布局 03

2012-08-20 21:26 288 查看
目录:点击这里

上一篇:【翻译】Pro.Silverlight.5.in.CSharp.4th.Edition - 第三章 布局 02

使用Canvas基于坐标布局

到目前为止我们还剩下Canvas没有学习到。Canvas可以让我们使用精确坐标给元素设置位置。对于设计一个以数据为主导的窗体和标准对话框来说Canvas不是一个非常合适的选择,但是如果要创建某些不一样的内容(比如给图形控件设计界面)时则可能非常有用。Canvas也是最轻量级的布局容器,因为它在处理它的子元素的尺寸的时候不涉及到特别复杂的布局逻辑,而是按照子元素所设置的精确尺寸和位置直接展现出来。

我们需要使用Canvas.Left和Canvas.Top这两个附加属性来给元素设置在Canvas中的具体位置。Canvas.Left用于设置元素的左边缘距离Canvas的左边线的距离的像素值;Canvas.Top用于设置元素的上边缘距离Canvas的上边线的距离的像素值。

实际情况中,我们可以用Width和Height这两个属性精确地设置元素的尺寸。而在Canvas这种布局容器中处理的时候则使用得更频繁,因为相对于其他布局容器来说Canvas没有自身的布局逻辑。如果我们不设置Width和Height这两个属性,那么元素会调整自己的尺寸到合适的值——换句话说,元素的尺寸会增大到能容纳元素的所有内容。在这种情况下再调整Canvas的尺寸就对内部元素无效了。

下面这个示例的Canvas中包含了四个按钮:

<Canvas Background="White">
<Button Canvas.Left="10" Canvas.Top="10" Content="(10,10)"></Button>
<Button Canvas.Left="120" Canvas.Top="30" Content="(120,30)"></Button>
<Button Canvas.Left="60" Canvas.Top="80" Width="50" Height="50" Content="(60,80)"></Button>
<Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50" Content="(70,120)"></Button>
</Canvas>


相应的运行效果如图3-16所示:



图3-16 按钮在Canvas中通过精确的坐标来布局

和其它布局容器一样,Canvas也可以嵌套在用户界面中。这意味着我们可以在页面中的某部分区域用Canvas来绘制一些细节内容,同时其它的元素用更标准的布局容器来处理。

使用ZIndex分层

如果页面中的元素出现了重叠,那么我们可以使用附加属性Canvas.ZIndex来控制重叠元素的层级关系。

默认情况下,所有元素的ZIndex值都是0。当元素的ZIndex值一样的时候,它们在页面上显示的顺序和她们在Canvas.Children这个集合中的顺序一致——取决于她们在XAML标记内容中定义的顺序。标记中之后声明的元素(比如上个示例中的按钮(70,120))将叠放在之前声明的元素之上(比如上个示例中的按钮(60,80))。

通过增大元素的ZIndex属性值,我们可以将相应的元素显示在更外一层。这是因为页面在顺序上是先呈现ZIndex值小的元素,然后呈现ZIndex值大的元素。通过这个技术,我们可以改变前一个示例的呈现顺序:

<Button Canvas.Left="60" Canvas.Top="80" Canvas.ZIndex="1" Width="50" Height="50" Content="(60,80)"></Button>
<Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50" Content="(70,120)"</Button>


备注:Canvas.ZIndex属性的具体值没有实际意义,可以是任意的正整数和负整数;重点是元素之间这个属性值的大小的比较。

在通过后台代码来改需要变元素的位置的时候,这个ZIndex属性显得尤其重要。我们只需要调用Canvas.SetZIndex()这个方法,传入两个参数(一个是要修改的元素,一个是要设置的新的ZIndex值)即可。不过可惜的是没有类似将元素的呈现顺序往前或者往后调一个单位的方法——这就意味着需要我们自己记住目前Canvas中所用到的全部ZIndex值的最大和最小值,并根据这个范围来自己控制以便达到我们所需要的布局效果。

裁剪

Canvas有一方面和直觉相违背。大多数布局容器都会将其内容限制在容器的可用空间中。比如,我们创建了一个高度为100像素的StackPanel并且这个StackPanel放了一列按钮,这列按钮的高度超过了100像素,我们知道超出的部分会在StackPanel底部开始才剪掉。然而Canvas就没有遵循这个常识(不走寻常路);相反地,它会把所有的子元素都绘制出来,即使超出了Canvas的尺寸限制。这意味着:即使我们将之前的那个示例的Canvas的Height和Width这两个属性都设置为0,最后页面呈现的效果还是一样的。

Canvas按这个方式来工作是出于性能的原因——坦率地说,绘制出所有子元素之后再检查每个子元素是否在Canvas的尺寸范围内的效率更高。不过,这种布局行为并非一直是我们所想要的。比如在第11章中有一个动画游戏:将炸弹投掷出游戏区域(用Canvas实现)的边缘。在这种情况下,炸弹应该是只在Canvas中的时候可见——当炸弹飞出去后就应该消失掉,而不是叠放在其他元素之上。

幸运的是,Canvas支持裁剪(clipping),这可以使得在某指定区域之外的元素(或者元素的一部分)被剪掉,就像在StackPanel或者Grid中的表现形式一样。我们唯一需要额外手动处理的是给相应的裁剪区域设置Canvas.Clip属性。

从技术的角度来说,Clip属性的值是一个几何图形(Geometry)对象(在第8章绘制部分内容我们会了解到这是一个非常有用的对象)。Silverlight有许多针对不同形状的Geometry派生类,包括正方形和矩形(RectangleGeometry)、圆形和椭圆形(EllipseGeometry)和其它一些更复杂的形状(PathGeometry)。下面这个示例则是将裁剪区域设置成为一个和Canvas的尺寸一致的矩形:

<Canvas x:Name="canvasBackground" Width="200" Height="500" Background="AliceBlue">
<Canvas.Clip>
<RectangleGeometry Rect="0,0,200,500"></RectangleGeometry>
</Canvas.Clip>
...
</Canvas>


本例中,裁剪区域相当于是一个左上角的坐标为(0,0)、宽度200像素、高度500像素的矩形。左上角的坐标是相对于Canvas自身来说的,因此通常来说都是将这个值设置为(0,0),除非我们要给这个Canvas的上边或者左边区域留出一些空余区域。

某些情况下,在标记中设置裁剪区域并不是最合适的做法。比如碰到Canvas需要动态地设置尺寸以便能适应一个尺寸可变的容器或者浏览器窗体这种情况的时候,裁剪区域最好也应该通过后台代码来动态处理。还算幸运的是,我们只需要增加一个简单的事件处理程序:Canvas的尺寸变化的时候,在相应的Canvas.SizeChanged事件处理函数中实现裁剪区域的尺寸的相应的调整即可。(在Canvas首次创建的时候这个事件也会激活,因此我们必须要注意裁剪区域的初始化设置。)

private void canvasBackground_SizeChanged(object sender, SizeChangedEventArgs e)
{
RectangleGeometry rect = new RectangleGeometry();
rect.Rect = new Rect(0, 0, canvasBackground.ActualWidth,
canvasBackground.ActualHeight);
canvasBackground.Clip = rect;
}


我们可以用下面的方式来注册事件处理程序:

<Canvas x:Name="canvasBackground" SizeChanged="canvasBackground_SizeChanged"
Background="AliceBlue">


在第11章中我们会通过那个投弹游戏来实际学习这个技术。


选择合适的布局容器

按照一般的经验,Grid和StackPanel最合适用于业务类型的应用程序(比如展示数据表单或者数据文档)。它们善于处理因为窗体尺寸变化或者动态内容(比如会根据实际情况增长或者收缩的文本块)所带来的调整;另外它们也让修改、本地化、应用程序的主题设置变得比较容易,因为临近的元素在尺寸调整的时候会相互影响(翻译补充一句:调整尺寸后,临近的元素的位置也会随之发生改变,这样我们能够及时的发现调整后所连带的其它效果并检查这个效果是否是我们需要的,因此这样的形式对我们来说比较方便)。而且,Grid和StackPanel和普通的HTML页面的工作方式最接近。

Canvas则完全不同了。由于其子元素都是通过固定坐标来排列,因此我们需要做更多的工作来调整子元素的位置(如果是要针对新元素或者新格式调整优化布局,那就会耗费更多的精力)。尽管如此,在某些图形界面的富应用程序(比如游戏)中,Canvas则非常有意义。在这样的应用程序中,我们需要非常细致的控制文本、经常重叠的图形,而且还会经常通过后台代码来改变坐标。这种情况下,灵活性不是重点,实现特定的视觉效果才是,因此就必须使用Canvas。


自定义布局容器

尽管Silverlight提供了这么多可靠的布局容器,但是这些并不能满足我们的所有要求。为了让应用程序的XAP尽可能“苗条”,很多Silverlight开发者做了许多特定的布局容器。

当然,我们也可以做自己需要的布局容器。简单的说,只需要创建一个继承于Panel的类并实现相应的布局逻辑即可。如果来劲了,还可以将自定义容器的布局逻辑和其他Silverlight特性结合起来。比如,我们可以创建一个处理鼠标悬停事件的面板,并且让面板中的元素支持拖拽(类似第4章中要介绍的拖拽示例);或者创建一个用动画效果展示子元素的面板。

在本章接下来的内容中,我们会逐渐了解布局的整个过程的工作方式,然后我们会学习如何创建一个自定义的布局容器。后面将有一个名叫UniformGrid的示例——这个定制的grid控件实现了将元素铺贴在一个均分单元格大小的网格中。

布局的两步骤

每种面板都使用相同的处理方式:分别负责尺寸和子元素排列的两个过程。第一阶段是尺寸测量,关键任务是确定面板其子元素大小;第二个阶段是布局设计,关键任务是确定每个子元素的具体排列。这两个步骤是必不可少的,因为在面板决定如何分配可用空间之前需要考虑所有子元素的布局要求。

我们可以通过重写MeasureOverride()和ArrangeOverride()这两个方法来分别完成上面所述的两个步骤所需要的逻辑实现,这两个方法属于Silverlight布局机制的一部分,定义在FrameworkElement类中。这两个方法名称中含有一个单词Override,意思就是要告诉我们:这两个方法是用来代替定义在UIElement类中的MeasureCore()和 ArrangeCore()的;这两个“Core”方法不可重写。

MeasureOverride()

第一个阶段是在MeasureOverride()方法中决定每个子元素所需要的空间大小。当然,就算是在MeasureOverride()中,子元素也不能给无限制的尺寸,而是限制在面板的可用空间里。有时候我们可能需要对子元素有更严格的限制手段。比如,一个均分为两行的Grid限制其子元素的高度是Grid高度的一半;StackPanel给第一个子元素提供所有可用空间,而第二个子元素则是在第一个元素占据之后剩下的空间中进行布局,依次往后。

MeasureOverride()的具体实现是遍历所有子元素并对每个子元素调用Measure()方法。调用Measure()方法的时候会设置了一个边界框——决定子控件的最大可用空间的一个Size对象。MeasureOverride()方法最后会返回现实所有子元素所需要的空间大小。

下面的示例代码是MeasureOverride()方法的一个基本结构:

protected override Size MeasureOverride(Size panelSpace)
{
  // 遍历所有子元素
foreach (UIElement element in this.Children)
{
  // 查询每个子元素所需要的空间大小, 并给出相应的尺寸限制
  Size availableElementSize = new Size(...);
  element.Measure(availableElementSize);
  // (这里可以读取element.DesiredSize来获得上面所设置的尺寸值)
}
// 此处指明面板需要的空间大小
// 此处可设置面板的DesiredSize属性
return new Size(...);
}


Measure()方法没有返回值。对子元素调用Measure()之后,DesiredSize属性的值便是之前请求的值。这个信息可以用作子元素的尺寸分配(以及面板所需要的全部空间)的依据。

每个子元素都必须调用Measure()方法,就算我们不需要限制元素的尺寸或者使用其DesiredSize属性。许多元素都是在调用了Measure()之后才能呈现在页面中。如果要让某个子元素完全按照自己的尺寸要求来自动调整,那就将Size对象的两个参数值都设置为Double.PositiveInfinity。(滚动条就是按照这个方式来实现的,因此它能容纳的元素没有尺寸限制。)这样,子元素会返回其内容所需要的全部空间大小。否则,子元素会选择它的内容需要的空间或者全部可用空间这两者的小的一方。

在MeasureOverride()方法的最后,布局容器必须返回它所预期的尺寸。对于结构较简单的面板来说,它的预期尺寸是所有子元素的尺寸的总和。

备注:不能草率地将传给MeasureOverride()方法的Size参数作为面板的预期尺寸值。尽管这样做似乎是一种不错的设置所有可用空间的方式,但是当传入的Size对象的两个参数存在Double.PositiveInfinity值(意味着尺寸没有界限)的时候会存在问题:无限制的尺寸可以用作尺寸的限制,但是不能用作最终的尺寸值,因为Silverlight无法判断出元素应该需要多大的尺寸。而且,我们实际上也不需要超出的空间,因为这样会导致最终的布局效果有些额外的空白或者本该显示的元素被挤出窗体可见区域之外。

留心的同学可能发现在每个子元素上调用的Measure()方法和这个面板布局逻辑的第一阶段必须的MeasureOverride()方法非常相似。事实上,Measure()方法会触发MeasureOverride()方法。因此,如果我们将一个布局容器放在另一个布局容器之中,那么在我们调用Measure()方法之后,最终能得到布局容器和它的所有子元素所需要的全部尺寸。

尺寸测量这个阶段分两步(用Measure()方法触发MeasureOverride()方法)来处理的其中一个原因是要处理间距(Margin)。在调用Measure()方法的时候,传入的参数是全部可用的空间尺寸;而在Silverlight调用MeasureOverride()方法的时候,它会自动将减去间距的空间后剩余部分作为可用空间(除非是Size设置了Double.PositiveInfinity)

ArrangeOverride()

测量完所有的元素之后就该将他们布局在相应的可用空间上。布局系统会调用面板的ArrangeOverride()方法,然后在其中遍历所有的子元素调用Arrange()方法为其分配空间尺寸。(我们很明显会猜到Arrange()方法会触发ArrangeOverride(),就像之前已经了解到的Measure()会触发MeasureOverride()一样)。

在测量阶段的Measure()方法中,我们传入了一个Size类型的参数,这个参数定义了可用空间的限定范围;而在元素布局的Arrange()方法中,我们传入的参数是一个System.Windows.Rect 类型的对象,这个参数定义了元素的尺寸和位置。这个时候,相当于每个元素通过类似Canvas中的决定布局容器和元素的左边距(上边距)的X(Y)坐标概念确定了具体位置。

下面的示例代码是ArrangeOverride()方法的一个基本结构:

protected override Size ArrangeOverride(Size panelSize)
{
// 检查所有子节点.
foreach (UIElement element in this.Children)
{
// 给子元素分配尺寸和位置.
Rect elementBounds = new Rect(...);
element.Arrange(elementBounds);
// (element.ActualHeight 和 element.ActualWidth这两个属性就是
// 元素所使用的高度和宽度
}
// 此处可以用设置面板的ActualHeight和ActualWidth属性
// 设置面板占用的空间大小
return arrangeSize;
}


这个函数传入的Size参数不能是无限制的。不过,我们可以将DesiredSize属性值作为参数,这样元素在排列的时候就会按照它所期望的尺寸来布局。我们也可以让传入的Size参数比元素所需要的实际尺寸要大。实际上这种处理方式非常普遍。比如,一个垂直的StackPanel给个子元素所需要的高度以及StackPanel自身的完整宽度。类似地,Grid也可以将固定值或者比按比例的行高设置得比相应行中的元素的实际尺寸要高。而且即使我们将元素放在尺寸正好相匹配的容器中,元素的尺寸也可能因为使用了Height和Width这两个属性设置了具体的尺寸而变大。

当元素尺寸变得比其预期的尺寸要大的时候,HorizontalAlignment和VerticalAlignment属性就要起作用了。这两个属性的设置决定了元素内容的具体位置。

因为ArrangeOverride()方法收到的参数必须是有大小限制的,所以我们可以将这个Size参数作为函数的返回值,也即是面板的最终的尺寸大小。事实上,很多布局容器都是按照这个方式来占据给它分配的所有空间。我们不必担心会占据其他控件的空间,因为在测量尺寸的阶段已经保证了不会得到需求之外的其他空间,除非有多余的可用空间。

自定义容器UniformGrid

到目前为止,我们对布局系统有了一定程度的了解,现在很有必要试着做一个具有Silverlight自带的面板不支持的功能的自定义布局容器。在本小节中,我们会看到一个示例:UniformGrid,这个布局容器是WPF的一个标准控件,可以将子元素排列在自动生成的等分单元格中,现在我们用Silverlight来实现这个控件。

备注:UniformGrid作为常规Grid的一个轻量级的代替品非常有用,因为它不需要明确的行列的定义,而且也不会强制要求我们手动将子元素放在合适的单元格中。在需要展示一组平铺的图片的时候,UniformGrid显得尤其合适。事实上,在整个.NET 框架中,WPF中存在这个控件的一个稍微完善的版本。

和所有的自定义面板一样,UniformGrid的定义也是继承于基类Panel:

public class UniformGrid : System.Windows.Controls.Panel
{ ... }


备注:我们可以直接在我们的Silverlight应用程序中直接创建这个UniformGrid类。但是如果想各种不同的应用程序中重用我们的自定义布局容器,更合适的方式是将这个类放在一个新的Silverlight类库中,这样,当我们需要在另一个应用程序中使用这个自定义的布局容器的时候,只需要添加相应的类库的引用即可。

UniformGrid的概念非常简单。它会检查可用空间、计算需要的单元格的数量(以及最终单元格的大小),然后将这些子元素一个接一个地展示出来。UniformGrid可以使用Rows和Columns这两个属性来自定义控件的行为。Rows和Columns可以独立设置,也可以关联设置:

public int Columns { get; set; }
public int Rows { get; set; }


下面几条内容是Rows和Columns这两个属性的一些特性:

如果这两个属性都设置了,那么UniformGrid会知道要创建多大的网格。它只需要将可用空间等分然后找到每个单元格的尺寸。如果元素的个数超过单元格的个数,那么超出的元素就不显示。

如果只设置了一个属性,那么UniformGrid会在要显示出所有元素的前提下自动计算另一个属性值。比如说,如果我们将Columns设置为3并且要放置的元素个数是8,那么UniformGrid将把可用空间分为3行。

如果两个属性都没有设置,那么UniformGrid会在要显示出所有元素的前提下,假定行列数相同,然后将这两个值都计算出来。(不过UniformGrid不会创建一个完全空的行或者列。相反,如果不能精确地匹配上行和列的数量,UniformGrid会增加额外的一列。)

为了实现这种机制,UniformGrid需要记录下真实的行列数。如果Rows和Columns这两个属性都设置了,那么真实的行列数就是这两个属性的值;如果没有设置,那么Grid会调用一个自定义的方法CalculateColumns(),先获得子元素的数量,然后再决定网格的行列规格。这个方法要在布局的第一阶段调用。

private int realColumns;
private int realRows;
private void CalculateColumns()
{
  // 计算子元素的数量
  // 如果面板是空的,则函数直接返回
  double elementCount = this.Children.Count;
  if (elementCount == 0) return;
  realRows = Rows;
  realColumns = Columns;
  // 如果 Rows 和 Columns 属性都设置了,那么就直接使用
  if ((realRows != 0) && (realColumns != 0))
    return;
  //如果 Rows 和 Columns 属性都没有设置,那么先计算列数
  if ((realColumns == 0) && realRows == 0)
    realColumns = (int)Math.Ceiling(Math.Sqrt(elementCount));
  // 如果只设置了Rows, 那么就计算列数.
  if (realColumns == 0)
    realColumns = (int)Math.Ceiling(elementCount / realRows);
  // 如果只设置了Columns, 那么就计算列数.
  if (realRows == 0)
    realRows = (int)Math.Ceiling(elementCount / realColumns);
}


Silverlight布局系统通过调用UniformGrid中的MeasureOverride()方法开始这个布局进程。这个方法需要调用上面的那个CalculateColumns()方法(目的是要确保行数和列数都设置好),然后可用空间将会被等分成一系列单元格。

protected override Size MeasureOverride(Size constraint)
{
  CalculateColumns();
  // 将可用空间等分.
  Size childConstraint = new Size(
  constraint.Width / realColumns, constraint.Height / realRows);
  ...

  // 下面继续接.


方法执行到这一步,UniformGrid内部的元素需要开始测量尺寸。不过,这个时候有个意外的情况——元素调用了Measure()方法后可能会返回一个更大的值,这说明最小尺寸比分配的空间还要大。UniformGrid记录下了最大的请求宽度和高度值。最后,在整个测量过程完成后,UniformGrid需要计算尺寸,确保单元格的大小能足够适应最大宽度和高度。这个计算出来的尺寸则作为MeasureOverride()这个方法的返回值。


  // 接上面内容

  ...
  // 记录每个元素所需要的最大的请求尺寸.
  Size largestCell = new Size();
  // 遍历面板中的每个子元素.
  foreach (UIElement child in this.Children)
  {
    // 获取子元素的期望尺寸.
    child.Measure(childConstraint);
    // 记录下最大的请求尺寸.
    largestCell.Height = Math.Max(largestCell.Height, child.DesiredSize.Height);
    largestCell.Width = Math.Max(largestCell.Width, child.DesiredSize.Width);
  }
// 使用最大的请求尺寸的高度和宽度来计算网格所需要的最大尺寸
return new Size(largestCell.Width * realColumns, largestCell.Height * realRows);
}


ArrangeOverride()方法所处理的工作类似。不过,它不再测量子元素。相反,它记录最终测量的空间大小、计算单元格的尺寸并将每个子元素放在合适的限定区域中。如果到了网格的最后单元格还有多的子元素(只有在Columns和Rows这两个属性值设置较小的情况下才发生),那么这些额外的子元素会放在一个0×0的区域中,换句话说就是隐藏起来了。

protected override Size ArrangeOverride(Size arrangeSize)
{
  // 计算每个单元格的尺寸.
  double cellWidth = arrangeSize.Width / realColumns;
  double cellHeight = arrangeSize.Height / realRows;
  // 设置每个子元素要占据的空间.
  Rect childBounds = new Rect(0, 0, cellWidth, cellHeight);
  // 遍历面板中的子元素.
  foreach (UIElement child in this.Children)
  {
    // 给子元素布局.
    child.Arrange(childBounds);
    // 将界限移到下一个位置.
    childBounds.X += cellWidth;
    if (childBounds.X >= cellWidth * realColumns)
    {
      // 换到下一行.
      childBounds.Y += cellHeight;
      childBounds.X = 0;
      // 如果子元素个数多余单元格个数,就像额外的元素隐藏,
      if (childBounds.Y >= cellHeight * realRows)
        childBounds = new Rect(0, 0, 0, 0);
    }
  }
  // 返回面板实际占用的尺寸
  return arrangeSize;
}


UniformGrid的使用很简单。我们只需要在XAML中映射对应的命名空间,然后就像定义其它布局容器那样定义UniformGrid即可。下面这个示例是在一个带有文本块的StackPanel中放置了一个UniformGrid。这个示例可以帮助我们证实UniformGrid的尺寸的计算是正确的,以及证实UniformGrid的内容的布局形式。

<UserControl x:Class="Layout.UniformGridTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Layout" >
<StackPanel Background="White">
<TextBlock Margin="5" Text="Content above the WrapPanel."></TextBlock>
<local:UniformGrid Margin="5" Background="LawnGreen">
<Button Height="20" Content="Short Button"></Button>
<Button Width="150" Content="Wide Button"></Button>
<Button Width="80" Height="40" Content="Fixed Button"></Button>
<TextBlock Margin="5" Text="Text in the UniformGrid cell goes here"
TextWrapping="Wrap" Width="100"></TextBlock>
<Button Width="80" Height="20" Content="Short Button"></Button>
<TextBlock Margin="5" Text="More text goes in here"
VerticalAlignment="Center"></TextBlock>
<Button Content="Unsized Button"></Button>
<Button Content="Unsized Button"></Button> <!--原书多写了这一行-->
</local:UniformGrid>
<TextBlock Margin="5" Text="Content below the WrapPanel."></TextBlock>
</StackPanel>
</UserControl>


图3-17展示了这段XAML的运行效果。 从UniformGrid中的子元素各自不同的粒度,我们可以看出它实际是如何布局的。比如,第一个按钮(名叫“Short Button”)设置了一个具体的Height属性值,因此这个按钮的高度就没有匹配上单元格的高度,而宽度则占据了单元格的整个宽度。第二个按钮(Wide Button)设置了具体的Width属性值,但是按钮是UniformGrid中最宽的元素,这就意味着这个宽度决定了UniformGrid的单元格的宽度。最终这个按钮的尺寸和后面的那个没有设置Height和Width的按钮(Unsized Button)的大小完全一致——都是占据了单元格的全部空间。同样地,UniformGrid中的第四个元素TextBlock由于TextWrapping属性值为“Wrap”,根据其文本内容的真实长度以及TextBlock的Width属性,这个文本内容被分为三行,这个TextBlock就成了最高的元素,所以相应的UniformGrid的单元格的高度也因此确定下来。



图3-17 UniformGrid的元素布局

备注:如果想看其他更绚的自定义布局容器,看看http://tinyurl.com/cwk6nz这个地址中的Radial Panel 射线面板,这个布局容器将子元素沿着一个不可见的圆圈的边线等距离排列。

页面尺寸的处理

到目前为止,我们对Silverlight提供的各种布局容器以及如何使用这些容器进行元素的布局有了大致的了解。不过,我们还有一个重点没有提到——top-level页面。所有的用户界面都要基于这个top-level页面才得以呈现。

我们已经了解到:这个用于承载应用中的Silverlight页面的top-level容器是个继承于UserControl的自定义类。UserControl类中只有一个属性Content(数据类型是UIElement),是Silverlight的用户界面元素的基础结构。Content属性接受一个元素,这个元素就是用户控件的内容。

用户控件并不包含任何特定的功能——它们仅仅是用来将一系列相关联的元素组合起来。不过,用户控件尺寸的处理不同会影响到最终界面的外观,因此还是有必要研究一下的。

我们已经知道如何用各种不同的布局容器的各种布局属性,使得元素尺寸能够适应其内容、适应可用空间或者固定的尺寸。设置页面的尺寸的手段有很多种,如下所示:

固定尺寸:设置用户控件的Width和Height属性,这样页面将得到固定尺寸。如果页面中有元素的尺寸超出了页面的尺寸,那么这样的元素的超出部分会被裁剪掉。当控件尺寸是固定值的时候,我们通常将它的HorizontalAlignment 和 VerticalAlignment属性设置为Center,这样控件就会处于浏览器窗口的中间,而不是停靠在左上角。

浏览器尺寸:用户控件没有设置Width和Height属性,这种情况下应用程序会占据整个Silverlight内容区域。(顺便一提,VS生成的HTML入口页面中将Silverlight内容区域设置为占据整个浏览器窗口。)如果我们使用这种方式,元素超出展现区域的可能性仍然存在,不过用户会发现这个问题,而且可以通过调整浏览器窗口的大小从而将缺失的内容展现出来。用这种处理方式的时候,如果我们想在Silverlight页面和浏览器窗口之间有一些留白空间,只需要设置用户控件的Margin属性即可。

约束性质的尺寸设置:即用MaxWidth、MaxHeight、MinWidth和 MinHeight这四个属性来对控件的尺寸进行控制。这种情况下,用户控件会在限定的尺寸范围内进行调整以适应浏览器窗口,不会超出限定范围,这样就可以保证页面不会变乱。

无限制的尺寸:在某些情况下,我们还是需要将Silverlight内容区域的尺寸超出整个浏览器窗口的大小。这时候就需要有一个滚动条,就像在一个较长的HTML页面中那样。要达到这个效果,我们需要去掉Width和Height属性的设置,并且修改Silverlight内容的入口页面(TestPage.html):去掉<object>元素的width="100%" 和 height="100%"这两个属性设置。这样下来,Silverlight内容区域的尺寸就会增大以适应用户控件的尺寸。

备注:记住,像VS和Blend这样的设计工具会在用户控件中自动加上DesignWidth和DesignHeight这两个属性。这两个属性只在设计阶段对页面的呈现效果起作用(表现形式和Width、Height类似)。在运行时这两个属性就失效了。这两个属性的主要作用是让我们可以在设计阶段就能比较真实的预览到应用程序应该会呈现的效果。

这四个处理方式各有各的使用前提条件。根据创建的用户界面的类型的不同,我们选择合适的那个。使用非固定尺寸的优点是通过布局的自适应调整能将浏览器窗口的额外空间加以利用;缺点是如果浏览器的窗口过大或者过小,那么页面的内容就会变得比较难读懂,也不易操作。我们可以通过页面设计来解决这些问题,但是工作量较大。另一方面,固定尺寸这种设计的缺点是无论浏览器窗体是什么尺寸,我们的应用程序的尺寸永远不变,这会导致如果固定尺寸值比浏览器窗口的尺寸小,那么浏览器窗体可能有很大的空白区域被浪费掉;或者如果固定尺寸值比浏览器窗口的尺寸大,那么应用程序可能无法使用(因为需要用的界面没有显示出来)。

根据经验,尺寸可调整的页面更为灵活,而且一般作为优先选择考虑。这种方式通常是商业应用程序和那种用户界面比较传统、同时没有过多图形内容的应用程序的最佳选择。另一方面,图形富应用程序和游戏的页面通常需要尺寸更加精确的控件,因此它们倾向于使用固定尺寸的方式。

提示:如果想测试这各种途径的实际效果,建议将页面的边界弄得更明显一点。有个简单的方法是将top-level内容元素的背景设置为非白色(比如,把Grid的Background属性设置为Yellow)。我们不能设置用户控件自身的Background属性,因为UserControl这个类本身就没有提供Background属性。另外还有一个方法是使用Border作为top-level元素,这样的话,设置其 BorderBrush属性和BorderThickness属性,页面的区域就相当于有了外边线。

本章后续内容还会介绍其它一些特殊的尺寸处理方式:滚动条,伸缩组件以及全屏展示。

滚动条

在有限的空间里展示大量的内容需要一个很关键的特性:滚动。之前所讲到的容器都不支持滚动。在Silverlight中,通过ScrollViewer控件就可以实现滚动效果。

如果要实现滚动的效果,我们只需是使用ScrollViewer将需要实现滚动效果的内容包裹起来。ScrollViewer内容可以放任何类型的元素,一个典型的做法就是包裹一个布局容器。比如由一列文本框和一列按钮组成的可以滚动的Grid,这个页面是占满浏览器窗口尺寸的,另一方面页面上设置了边距(Margin=”20”)用以将滚动条和环绕着滚动条的浏览器窗体区分开。下面的标记内容展示了这个示例的基本结构,为了节省篇幅,标记语言中只写了第一行的元素,其它行的忽略了:

<UserControl x:Class="Layout.Scrolling"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="20">
<ScrollViewer Background="AliceBlue">
<Grid Margin="3,3,10,3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
...
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" Margin="3"
Height="Auto" VerticalAlignment="Center"></TextBox>
<Button Grid.Row="0" Grid.Column="1" Margin="3" Padding="2"
Content="Browse"></Button>
...
</Grid>
</ScrollViewer>
</UserControl>


图3-18为最终运行的结果。



图3-18 支持滚动条的页面

如果调整页面的大小是的页面足够能容纳Grid的所有内容,那么滚动表就会变得不可用,不过滚动条依然是可见的。我们可以通过设置VerticalScrollBarVisibility属性(其值来自ScrollBarVisibility枚举)来控制这种行为。默认值为Visible使得垂直滚动条处于一直可见的状态;如果设置为Auto,那么在滚动条需要的时候会显示出来,而不需要的时候则不可见;如果根本不需要使用垂直滚动条,那么将它设置为Disabled即可。

备注:如果设置为Hidden,效果和Disabled相似不过略有不同。首先,隐藏的ScrollViewer仍然支持滚动效果。(我们可以通过键盘上的方向键实现滚动。)其次,两种方式下,ScrollViewer内部的内容布局也不尽相同:设置为Disabled的时候,ScrollViewer中的内容的尺寸还是局限于ScrollViewer之中;如果设置为Hidden,那么ScrollViewer中的内容的尺寸就没有了限制,这意味着它可能超出滚动区域。

ScrollViewer也支持水平的滚动,不过HorizontalScrollBarVisibility属性的值默认是Hidden。将这个属性值设置为Visible或者Auto即可使用水平滚动效果。

Viewbox的伸缩功能

在本章前面的内容中,我们讨论过Grid可以实现按比例分配尺寸以保证子元素能占据所有可用空间。因此Grid是用来创建能通过伸缩来适应浏览器窗口的界面的绝佳工具。

尽管我们常常要用到这种尺寸可调整的行为,但是这并不是永远都适用的。改变控件尺寸的同时也会改变控件能容纳的子元素的数量,也会使布局产生微小的移位。图形富应用程序需要尺寸更精确的控件以保证元素之间能够完美的排列起来。不过这并不意味着我们要使用固定尺寸来处理页面。相反,我们可以使用另外一个技巧,叫做scaling(按比例伸缩)。

本质上,伸缩会调整控件的整个视觉外观,而不仅仅是它的外边界。不管伸缩的比例如何,控件所展现的内容都是一样的——只是看起来不同。

图3-19展现了这种不同。左边的是正常尺寸下的效果,中间的是窗体增加后用传统的尺寸调整方式后的效果,右边的则是窗体增加后用伸缩方式实现的效果。



图3-19 普通(左)、尺寸调整效果(中)和尺寸伸缩效果(右)的对比

要用上缩放,我们必须先使用形状变换(transform)这个概念。在第8章中我们会了解到transform是Silverlight的2D绘图框架的一个关键部分,它可以用来实现 缩放变换、倾斜变换、旋转变换以及一些改变元素外观的其他效果。本例中,我们需要使用ScaleTransform(缩放变换)来实现页面的缩放效果。

有两种方式来使用ScaleTransform。第一种方式是DIY。在代码中响应UserControl.SizeChanged事件,检查页面的当前尺寸,然后经过适当的计算后用code-behind手工创建ScaleTransform。这么做虽然干得过,但是有点儿苦逼。我们可以换第二种方式:Viewbox控件,最终能实现相同的效果,重要的是没那么多代码量。

在编码实现缩放效果的代码之前,我们需要确保XAML标记的配置是符合如下要求的:

用户控件的尺寸不能使确定值——相反,它必须能随着浏览器窗口的尺寸的变化而相应的做出自适应调整。

为了保证能缩放至准确的尺寸范围,我们需要知道其理想的尺寸值,也就是能精确地匹配上它内部的所有元素的尺寸大小。尽管这个尺寸大小不会设置在XAML标记中,但是会在代码中的缩放计算部分进行处理。

当这些细节准备就绪后我们就可以很轻松地创建一个支持缩放效果的页面了。下面这段XAML标记就是图3-19所对应的一个包含了几组【文本框+按钮】的理想尺寸为200×225像素的Grid。

<UserControl x:Class="Layout.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<!--这个Viewbox容器是实现缩放必须的 -->
<Viewbox>
<!--这个Grid则是普通用户界面的布局内容.请注意其尺寸是固定值设置的. -->
<Grid Background="White" Width="200" Height="225" Margin="3,3,10,3">
<Grid.RowDefinitions>
...
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" Margin="3"
Height="Auto" VerticalAlignment="Center" Text="Sample Text"></TextBox>
<Button Grid.Row="0" Grid.Column="1" Margin="3" Padding="2"
Content="Browse"></Button>
...
</Grid>
</Viewbox>
</UserControl>


本例中,Viewbox在缩放的过程中遵循了Grid的纵横比。换句话说,它是以适应小维度(高度或者宽度)的原则来调整尺寸,而不是为了填充所有的可用空间而破坏控件的现有尺寸比例。如果我们不需要维持这个尺寸比例,只需将Stretch属性设置为Fill即可。这种做法对页面缩放功能来说并不是十分有用,但是如果是有其他目的(比如设置按钮中的矢量图的尺寸)就另说了。

最后我们可以做个有趣的测试:将Viewbox放在ScrollViewer中,这会产生一些有意思的效果。比如说,将Viewbox的Height和Width属性设置大点让Viewbox的尺寸超过可用空间的尺寸,然后在内部的被放大的元素上实现缩放。我们可以用这个技巧来创建一个可缩放的用户界面,用户可以通过拖动滑块或者鼠标滚轮改变界面的缩放比例。第4章会介绍一个用这个技术实现的使用鼠标滚轮来调整页面显示的示例。


浏览器缩放的Silverlight支持

Silverlight运行在某些浏览器和操作系统(比如当前最近几版的Firefox和IE)的时候,Silverlight应用程序能提供一种叫做自动缩放(autozoom)的特性。这意味着用户可以通过改变缩放比例使得Silverlight应用程序变大或者变小。(在IE中,可以通过状态栏右侧来调整或者菜单栏-查看-缩放选项。)举个栗子,如果用户选择缩放比例是110%,那么整个Silverlight应用程序,包括文字、图表以及各种空间都会放大10%。

多半情况下,这种行为都有意义——而且显示的效果和预期一致。然而,如果我们想让应用程序能够提供其自身的缩放特性的话,那么浏览器的自动缩放功能就不太适合了。这种情况下,我们需要禁用浏览器的自动缩放功能,做法是:在HTML入口页面中增加一个enableAutoZoom参数并将属性值设置为false,如下所示:

<div id="silverlightControlHost">
  <object data="data:application/x-silverlight-2,"
    type="application/x-silverlight-2" width="100%" height="100%">
    <param name="enableAutoZoom" value="false" />
    ...
  </object>
  <iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe>
</div>



全屏模式

Silverlight应用程序也可以实现全屏显示,使应用能在脱离浏览器之外运行。在全屏模式下,Silverlight插件会占据整个显示区域,并且将其他所有应用程序(包括浏览器)叠放在本应用程序的下方。

全屏模式有几个严重的局限性:

只有在接收到用户输入响应后才能切换到全屏模式:换句话说,当用户点击一个按钮或者键盘上按下一个键,系统才能切换到全屏模式。系统不能在应用程序加载之后立刻切换到全屏模式。(就算是用代码实现我们期望的效果,系统也会忽略这段代码。)系统有这样的限制的目的是为了防止Silverlight应用程序被设计成一个可能误导用户、让用户以为自己进入了一个本地的应用程序或者系统的窗口中的状态。

在全屏模式下,键盘输入受到了限制:除了Tab、回车、Home、End、Page Up、Page Down、空格和方向键以外,其他的键都被屏蔽掉了。这就意味着我们可以做一个简单的全屏游戏,但是不能使用文本框或者其他支持输入的控件。设计这样一个限制的原因是为了防止不法分子用来骗取密码——比如,将系统设计成窗体对话框的样子去欺骗用户输入密码。例外情况是创建受信任的应用程序没有这样的限制(具体见第18章)

备注:全屏模式主要用于设计大屏的视频展示。对于简单图形应用程序(比如照片浏览器)和游戏只需要那几个按键能响应就足够了。要处理输入类型的控件之外的按键,只需要使用标准的KeyPress事件句柄即可(比如第4章就会介绍在应用程序的根布局容器中增加一个KeyPress事件来捕获键盘按键。)

下面这段代码实现的点击相应的按钮使应用程序切换到全屏模式:

private void Button_Click(object sender, RoutedEventArgs e)
{
  Application.Current.Host.Content.IsFullScreen = true;
}


当程序切换进入全屏模式的时候,屏幕中央会显示一个如图3-20的消息。这个消息包含了应用程序所在的站点的域名。如果是一个ASP.NET站点并且内嵌于VS中的web服务器,那么我们会看到域名是http://localhost;如果是用一个硬盘上的HTML测试页面寄宿了应用程序,那么我们会看到域名是file://。这个消息也提示用户通过按ESC键来退出全屏模式。另外,通过将IsFullScreen属性为false也可以退出全屏模式。



图3-20 全屏模式时提示的消息

如果应用程序要使用全屏模式,那么最顶层的用户控件不应该把Height和Width设置为固定值,这样就可以让用户控件能自动调整尺寸来适应可用空间。在全屏模式下,我们也可以使用前面所讨论过的缩放技术通过变换(transform)将程序中的元素的尺寸变大。

还有另外一种退出Silverlight应用程序全屏模式的方法:切换到另一个应用的窗口上。一般来讲,这种方式效果还不错。但是当你使用多个显示器的时候,这个方式所实现的效果可能与你想象的不一样了:从全屏的Silverlight应用中切换出去的时候,它不能保证Silverlight应用仍然在这个显示器中处于全屏状态,而在另一个显示器中展现你切换过去的应用程序。

如果想阻止这种行为的发生,我们可以用下面的代码将应用程序的全屏模式“固定”住,即使应用程序失去焦点,它也同样处于全屏模式下。

Application.Current.Host.Content.FullScreenOptions = FullScreenOptions.StaysFullScreenWhenUnfocused;


这段代码必须在切换到全屏模式之前使用。在这之后再设置IsFullScreen属性,用户会得到一个是否让应用程序一直处于全屏状态的确认提示(图3-21)。这个确认对话框中也包含了一个是否要记住用户的选择的复选框,勾上之后,下次用户在切换进入全屏模式的时候就不会再次弹出这个提示框了。

如果用户选择了“是”,窗口会保持全屏模式直到用户按下ESC键或者系统执行了将IsFullScreen属性设置为false的代码。如果用户选择“否”,那么应用程序会按照正常的方式进入全屏模式,也就意味着当应用程序失去焦点的时候就退出全屏状态。



图3-21 切换到全屏模式的时候弹出的是否保持全屏状态的确认对话框

本章总结

本章中,我们对Silverlight布局模型进行了深入的了解,学习了如何将元素放在StackPanel、Grid等布局容器中。我们尝试通过嵌套的各种布局容器呈现一个复杂的布局界面,另外还用GridSplitter实现了布局尺寸的可调整。然后我们还学习了如何创建一个自定义的布局容器。最后,我们还学习了通过改变尺寸、缩放和全屏等技术来控制最顶层的用户控件的展现效果。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐