深入解读Silverlight的布局原理
对于Silverlight学习来说,首先要面对的应该是布局:你得把元素放到你想摆放的位置,然后是考虑元素的层次以及可见性,之后可能你想让它动起来,就学习动画,最后理解更深入之后,可能会开发如Behavior之类的行为,或者设置复杂的控件状态,模板。
很多教程是从使用Grid开始,然后是Canvas,StackPanel之类的控件,他教你怎样设置元素的位置。然后就没有下文,很少会去讲解布局的原理,不过如果是看Silverlight SDK,是能看到的。其实最好的教程就是Silverlight SDK,包括像两个不同的SL插件(即使它们在不同的浏览器窗口)之间的通信这些一般人没有注意到的特性,里面都是讲得清清楚楚。所以,建议不要花钱去买Silverlight相关的书。
其实,不是能使用Grid之类的就知道了布局,Silverlight布局包含更多的东西,理解布局系统有利于更深层的理解Silverlight,从而开发更得心应手。
遗憾的事初学者理解布局也许有点复杂,可能是因为其中的递归,但是我们生活中其实有很多递归系统的。我试着学习郭欣用铁路系统描述网络传输一样,也来构造这样一个场景。
布局原理
首先,所有元素的最顶层必须是一个容器(通常如Grid,Canvas,StackPanel等),然后在容器中摆放元素,容器中也可能包含容器。这里的容器就像行政长官一样,他们负责分配元素的空间。同样,首先顶层的容器一个一个的问自己的子元素:你想要多大的空间?如果子元素也是容器,它又继续向下递归,最后又顶层开始向上汇报。这就是所谓的测量。
测量完之后就是排列,这个时候每个容器知道自己每个子元素想要的空间大小,就按自己的实际情况进行分配。一致递归到最底层。
注意上述红色字体部分,通过前面的故事,我们知道,资金的发放完全由行政长官控制,不管下面想要多少,都是他说了算,他甚至可以一分钱都不给,或者给你超多你的预期的数目。
这里的容器也一样,容器拥有完全的分配权,不过这里容器不仅仅是分配空间,还决定元素的位置,因为空间总是跟位置相关的。也就是说,容器说想给你多大空间你就只有有那么大的空间可使用,容器想让你摆在什么位置,你就得乖乖呆着什么位置。
只不过,这里的容器是遵守规则的,它遵守开发者指定的规则:
Grid的规则是:我把我这个空间分成一格一格的格子,看起来有些像Table,在我里面的元素我完全按照附加属性Grid.Row,Grid.Column,Grid.RowSpan,Grid.ColumnSpan来决定其大小和位置。
Canvas的规则是:我读取附加属性Canvas.Left,Canvas.Right,Canvas.Top,Canvas.Bottom,并以此来决定元素的位置,我通常不限制元素开用空间
StackPanel的规则是:根据附加属性,我要么让元素横着排列,要么竖着排列。
聪明的你是不是立刻想到,我可不可以定义自己的规则呢?哈哈,当然可以!
比如,你可以让Panel里面的元素随机分布,并可让它们随机旋转一定角度,这不就是现在某些很酷的相册吗;你可以让元素排成一个圆形,这不就是Blend里面的例子吗;你可以让元素根据某个Path元素排列,这不就是PathListBox吗?如下图:
所以,你现在是不是觉得布局不是那么简单并且很好玩呢?
下面我们就来看怎么实现这么酷的东西!
基础框架-FrameworkElement
为Silverlight布局中涉及的对象提供公共API的框架。FrameworkElement还定义在Silverlight中与数据绑定,对象树和对象生存期功能区域相关的API。
继承层次结构:
System.Object
System.Windows.DependencyObject
System.Windows.UIElement
System.Windows.FrameworkElement
System.Windows.Controls.Border
System.Windows.Controls.Control
System.Windows.Controls.Panel
System.Windows.Shapes.Shape
类定义:
namespace System.Windows
{
public abstract class FrameworkElement : UIElement
{
protected virtual Size ArrangeOverride(Size finalSize);
protected virtual Size MeasureOverride(Size availableSize);
......
}
}
我们看到FrameworkElement定义了一个布局的框架:其中MeasureOverride就是测量过程,ArrangeOverride就是排列过程。我们一般的控件都继承自它,所以就可能建立起了这样一个递归系统。
Silverlight要求,要定位可视元素,必须将它们放置于Panel或者其他容器对象中。因为Panel定义了确定如何在屏幕上绘制Panel元素的Children集合成员的布局行为。
自定义Panel很简单,从Panel派生并重写其MeasureOverride和ArrangeOverride方法即可。在两个方法中,都可以获取Children属性(下面列举SDK例子)。
测量:
protected override Size MeasureOverride(Size availableSize)
{
int i =0;
foreach (FrameworkElement child in Children)
{
if (i < 9) child.Measure(new Size(100, 100));
else child.Measure(new Size(0, 0));
i++;
}
return new Size(300,300);
}
在MeasureOverride中,必须调用每个子元素的Measure方法,传递该容器可以分配的空间,确定分配多少空间给子级,然后像上级返回整个容器需要的空间大小。该方法会触发子元素执行内部的MeasureOverride方法,如此递归。
其实我们想到,测量过程可以是不必要的,排列可以完全按照自己的逻辑进行,它可以不管子元素到底需要多大,但是这样肯定是不行的,除非容器额外了解很多子元素的情况,否则可能排列出的效果很差。那么测量其实是为排列提供一个重要的参考信息:
DesiredSize,DesiredSize可以理解为:行政长官给在发放通知的时候给下级一个指标,但是下级会有一个实际需求,而DesiredSize相当于指标和需求之间的最小值。具体的说,DesiredSize在Measure发放之后计算的,布局系统基于传递给Measure的availableSize和元素的固有大小去定子元素的DesiredSize,一般讲DesiredSize设置为两种的最小值。这个值可以在排列的时候作为一个参考依据。
排列:
在排列处理过程中,必须确定每个子级的布局槽的位置并设置面板的最终大小。当你计算好位置之后,调用每个元素的Arrange方法就可以了,其参数是一个Rect对象,这个对象可以表示一个元素的空间大小和位置。如下所示:
image.Arrange(new Rect(10, 10, 150, 150));
该句将元素image放在x为10,y为10的位置,所占宽度和高度都为150的大小空间。
定义一个Panel就这里简单,这里是定义Panel规则的地方,你可以设计不同的规则,根据不同的附加属性或依赖属性进行布局。Grid,Canvas,StackPanel等都是经过这样定义的。
所以,你可以在这里设计很复杂的计算,使元素按不同的方式布局排列。
神秘绝招-让Panel的子元素都具有某种行为
如果只是能摆放元素的位置和限制大小,这样未免没有多大的用途,所有元素都只是一个摆设。
但是,想到我们前面花了大量篇幅分析得Behavior,试想,我们能不能给容器设置某种行为,让它把这种行为附加到每一个元素,从而让容器还能控制元素的行为呢?哈哈,你太聪明了,没错,这是完全可以的!
由于讲过Behavior的原理,以及布局的原理,这里我就不分析代码了,附上源文件供参考,代码写得很粗糙,仅作演示用,请勿用在项目中,这里还是讲一下大概思路:
首先定义了一个派生自Panel的类PhotoWallPanel,在ArrangeOverride中将每个元素随机选择了一个位置,并随机旋转了一个角度;
然后定义一个Behavior,它作用于PhotoWallPanel,遍历其中的每一个元素,给每个元素添加一个鼠标经过和离开的效果,然后附加到PhotoWallPanel上,值得注意的是:
<i:Interaction.Behaviors>
<local:ShowPictureBehavior/>
</i:Interaction.Behaviors>
</local:PhotoWallPanel>
在XAML中,附加Behavior的代码应该位于Panel的结尾之前,或者在其他字元素之后,这样才能保证将行为添加到元素上,因为XAML是顺序解析的,在前面的元素首先实例化其对象,如果放在前面,这个时候Panel的子元素还没有初始化。
总结
写了这么多,有的人觉得烦了,确实也是这么点东西写那么多废话,但是自定义Panel以及Behavior这些都是重要的基本概念,如果你想深入Silverlight开发,或者很在意UI,这些应该是必须掌握的。希望本篇能对学习Silverlight的朋友有点启示。
值得重视的是,我们看到Silverlight中附加属性的重要性,基本上容器控件都有定义附加属性,而Behavior也是利用附加属性,很值得好好研究,并在项目中充分利用。
Blend截图:
实际效果
补充:
经@super110的提示,这里可能忽略了一点东西,而我又仔细分析了一下,可能一般人很容易在这里理解起来比较纳闷。注意,以下描述基于实验,而非基于原理:
我们再仔细分析这两个方法:
protected override Size MeasureOverride(Size availableSize)
{
foreach (var item in this.Children)
{
item.Measure(availableSize);
}
return “此返回值将作为该容器的DesiredSize”;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var item in this.Children)
{
FrameworkElement my
= item as FrameworkElement;
my.Arrange(new Rect(0, 0, my.Width, my.Height));
}
return “此返回值将作为此容器的ActualWidth和ActualHeight”;
}
在Measure每个元素的时候,这个方法执行完就会产生DesiredSize值,其实它也就是MeasureOverride()方法的返回值。因为这其实是一个递归系统。
DesiredSize的取值方法:
1,对于非容器类的控件如Image,布局系统会取其所设置的Width和Height与availablesize相比较的最小值,打个比如,这个有点像进入公司的时候老板问你期望的待遇,当然他心中有一个数字,如果你的数字大于他的数字,以他的为准;如果你的数字小于他的数字,以你的为准,我暂且称其为“老板不吃亏原则”
2,对于容器控件,由于是我们写代码控制MeasureOverride方法的返回值,所以这个返回值就是该容器的DesiredSize值。换句话说,如果这个容器处于另外一个容器当中,当父容器调用子容器的Measure方法之后,这个子容器返回的值就是它的DesiredSize值。
ActualWidth和ActualHeight的计算:
1,对于非容器控件,往往是由容器给定的值决定,即容器在调用的Arrange方法的时候,给根据自身规则情况来分配值,这个值就是非容器控件的ActualWidth和ActualHeight
2,对于容器控件,其值由ArrangeOverride方法的返回值来确定,原理同DesiredSize一样
DesiredSize,ActualWidth和ActualHeight这几个值都是不能在程序中设置的,我们看到它要么由布局系统计算,要么由容器来计算,我们在程序中一般只读取这几个值。