周银辉
WPF拥有很多布局面板,比如Grid, StackPanel等,这让我们的界面布局更加的灵活,但也许这些面板并不能完全满足你的需求,这时你需要自定义面板来满足你的布局要求.
1,面板布局是如何实现的
WPF布局引擎采用了一种递归的方式来实现控件及其子控件的布局,大致过程是这样的:要实现控件A的布局,那么先要实现A的子控件a1,a2,a3...的布局,要实现a1的布局,那么得实现a1的子控件a11,a12,a13...的布局,如此循环,然后但子控件的布局完成后,在完成父控件的布局,最后递归回去,递归结束,布局才算完成.
控件的最终大小和位置是由该控件和父控件相磋商来完成的,父控件先给出其能给予子控件的力所能及的布局空间,子控件在反馈给父控件一个自己的期望值,父控件最后根据自己所拥有的空间大小与子控件的期望值分配一定的空间给子控件并返回自己的大小.这一系列过程是通过重写面板的MeasureOverride和ArrangeOverride方法来完成的.
2,Size MeasureOverride(Size availableSize)方法
该方法为布局中控件所需要的空间大小进行评估.
参考如下代码:
protected override Size MeasureOverride(Size availableSize)
{
Size childrenSize = new Size(0, 0);
foreach (UIElement child in this.Children)
{
child.Measure(new Size(Double.PositiveInfinity,Double.PositiveInfinity));
childrenSize.Width += child.DesiredSize.Width;
childrenSize.Height += child.DesiredSize.Height;
}
return childrenSize;
}
在MeasureOverride阶段我们对面板中的每个Child调用了其Measure(Size sz)方法,该方法的作用是父控件告诉子控件其预计要分配给子控件的空间大小,对Child调用了该Measure方法后,子控件会在其内部给父控件一个回应以便告诉父控件它所期望的大小,而子控件的该期望值便保存在其child.DesiredSize中.如果Measure方法中传入的是正无穷大(new Size(Double.PositiveInfinity,Double.PositiveInfinity))时,相当于父控件在对子控件说"尽管说出你所需要的大小吧,如果可以的话全世界都可以给你".{
Size childrenSize = new Size(0, 0);
foreach (UIElement child in this.Children)
{
child.Measure(new Size(Double.PositiveInfinity,Double.PositiveInfinity));
childrenSize.Width += child.DesiredSize.Width;
childrenSize.Height += child.DesiredSize.Height;
}
return childrenSize;
}
MeasureOverrice方法中的availableSize参数正是其父控件(你编写的面板控件以后在实际使用时其父控件)对其调用Measure方法时传入的值(减去一些边界值,比如要减掉Margin等)
MeasureOverride方法的返回值正是其告诉其父控件的期望值,即是其父控件(你编写的面板控件以后在实际使用时其父控件)对其调用Measure方法后,其DesiredSize值
注意:作为父控件,你可以很慈爱地仅可能地满足子控件的需要(尽管不一定有这能力),所以可以在对子控件调用Measure方法时传入一个正无穷大的尺寸,但作为子控件,你不能贪婪地向父控件索要正无穷大的空间,所以我们不能将正无穷大作为MeasureOverride方法的返回值,也不能直接将该方法的availableSize参数作为返回值(因为你的父控件有可能将正无穷大作为该参数传递给你)
3,Size ArrangeOverride(Size finalSize)方法
该方法作用在于为面板子控件提供布局空间即排列子控件并返回自身大小
参考下面的代码:
protected override Size ArrangeOverride(Size finalSize)
{
Point childPos = new Point(0, 0);
foreach (UIElement child in this.Children)
{
child.Arrange(new Rect(childPos, new Size(child.DesiredSize.Width, finalSize.Height)));
childPos.X += child.RenderSize.Width;
}
return finalSize;
}
该方法对每个Child调用Arrange方法,Arrange方法中传入的Rect结构告诉子控件其被安排在那个空间内进行布局,然后子元素会根据自己得到的空间以及自己的对齐属性(XXXAlignment)进行放置,放置完成后子控件的大小等便得到了确定,而该值便存放在子控件的RenderSize中(即是ActualWidth与ActualHeight).{
Point childPos = new Point(0, 0);
foreach (UIElement child in this.Children)
{
child.Arrange(new Rect(childPos, new Size(child.DesiredSize.Width, finalSize.Height)));
childPos.X += child.RenderSize.Width;
}
return finalSize;
}
控件本身也可以根据子控件占用的实际空间大小来决定自己的大小,也可以直接将得到的空间大小(即是ArrangeOverride方法中的finalSize参数)作为自己的最终大小返回
ArrangeOverride方法中的finalSize参数是该控件的父控件(你编写的面板控件以后在实际使用时其父控件)在对该控件调用Arrange方法时传入的Rect的大小,正如该控件对其子控件调用Arrange方法一样.
注意:Child的Arrange方法只是给定一个空间让子控件在此空间内进行摆放,该空间并没有决定子控件的具体位置和大小,这还得取决于子控件的其它属性,比如HorizontalAlignment与VerticalAlignment等. 在Arrange期间,系统会自动进行对齐(Alignment)操作.
下图是自定义的面板MyStackPanel,其比WPF内置的StackPanel多了两个方向上的布局("从右到左"与"从下到上")
下载DEMO