[深入浅出Windows 10]布局原理
5.2 布局原理
很多时候在编写程序界面的时候都会忽略了应用布局的重要性,仅仅只是把布局看作是对UI元素的排列,只要能实现布局的效果就可以了,但是在实际的产品开发中这是远远不够的,你可能面临要实现的布局效果要比常规布局更加复杂,这就需要对布局的技术知识有深入的理解和掌握才能够实现。要实现一个布局的效果,可能会有很多总布局方案,我们该怎么去选择实现的方法?如果要实现的一个布局效果是比较复杂的,我们该怎么去对这种布局规律进行封装?要解决这些问题,首要的问题就是需要我们对程序的布局原理有着深入的理解。
5.2.1 布局的意义
布局是页面编程的第一步,是从总体的方向去把握页面上UI元素的显示。布局其实一个一个应用程序开发里面非常重要的一部分,这块的知识也是往往会被开发者所忽视。随着Windows 10支持这非常多的设备和屏幕分辨率,布局显得原来越重要,也将会变得更加复杂,我们非常有必要去了解做程序的界面布局有什么样的意义,这不仅仅是界面显示可以就完事了。下面来看一下应用程序的布局有什么样的意义:
(1)代码逻辑
良好的布局会使得代码逻辑非常清晰,差的布局方案会让页面代码逻辑很混乱。如果你只是靠拖拉控件来做Windows 10的布局,这个程序的界面布局肯定会变成很糟糕,所以好的布局方案,一定要基于对各种布局控件的理解,然后充分地它们的特性去实现布局的效果。
(2)效率性能
布局不仅仅是界面UI的事情,它甚至会影响到程序的运行效率。当然简单的几个控件的页面布局,对程序效率性能的影响是微乎其微的,但是如果你的界面要展示大量的控件的时候,这时候布局的好坏就会直接影响到程序的效率。好的布局实现逻辑会让程序即使在有大量控件的页面也能流程的运行。
(3)动态适配
动态适配包括两个方面,一个是Windows 10的多种分辨率的界面的适配,另外一个是页面的控件是不确定的也会产生动态适配的问题。好的布局方案,可以使应用程序可以兼容各种分辨率的设别,受到不同分辨率所影响的页面,也是可以通过布局的技巧来解决的,保证不同的分辨率下面都是符合产品的显示效果。还有一个就是动态产生的控件,这个是指你的页面上会根据不同的情况显示不同的内容,这时候在做布局的时候就要思考如何对付这些会变化的页面。
(4)实现复杂的布局
有时候页面需要实现一些复杂的布局效果,比如像圆圈一定排列控件,Windows 10里面是没有这样的布局控件支持这种复杂的布局效果的,这时候就需要去自定义布局的规律来解决这样的问题。能否自定义布局控件去实现复杂的布局效果,这就要看你对Windows 10的布局技术的掌握程度了。
5.2.2 布局系统
布局系统是指对Windows 10的布局面板所进行的布局过程的运作原理的统称。布局其实是一个在 Windows 10应用中调整对象大小和定位对象的过程。要定位可视化对象,必须将它们放置于 Panel 或其他布局面板中。Panel类是所有布局面板的父类,系统的布局面板Canvas、StackPanel、Grid和RelativePanel都是Panel类的子类,继承了所有Panel类的特性。Panel类定义了在屏幕上绘制所有的面板里面的成员(Children属性)的布局行为。这是一个计算密集型过程,即 Children 集合越大,执行的计算次数就越多,也就是面板里面的元素越多,布局系统布局的整个过程的时间就会越长。所有的布局面板类都是在Panel的基础上添加了相应的布局规律的,在Panel类的基础上继续封装的布局面板是为了更好地解决一些布局规律的问题,实际上这样的封装是增加复杂性,对性能造成一定的损失的。所以如果不需要较为复杂的布局面板(如 Grid),则可以使用构造相对简单的布局(如 Canvas),这种布局可产生更佳的性能。
简单地说,布局是一个递归系统,实现在屏幕上对元素进行大小调整、定位和绘制。在整个布局的过程中,布局系统对布局面板成员的处理分为两个过程:第一个是测量处理过程,第二个是排列处理过程。测量处理过程是确定每个子元素所需大小的过程。排列处理过程是最终确定每个子元素的大小和位置的过程。每当面板里面的成员改变其位置时,布局系统就可能触发一个新的处理过程,重新处理上面所说的两个过程。不论何时调用布局系统,都会发生以下一系列的操作:
1、第一次递归遍历测量每个布局面板子元素(UIElement类的子类控件)的大小。
2、计算在 FrameworkElement类的子类控件元素上定义的大小调整属性,例如 Width、Height 和 Margin。
3、应用布局面板特定的逻辑,例如StackPanel面板的水平布局。
4、第二次递归遍历负责把子元素排到对于自己的相对的位置。
5、把所有的子元素绘制到屏幕上。
如果其他子元素添加到了集合中、子元素的布局属性(如 Width 和 Height)发生了改变或调用了 UpdateLayout 方法,均会再次调用该过程。因此,了解布局系统的特性就很重要,因为不必要的调用可能导致应用性能变差。下面的内容将会详细地演示这个布局的过程。
5.2.3 布局系统的重要方法和属性
在Windows 10中,布局不仅仅是布局面板的要做的事情,布局面板是在负责把这个布局的过程组织起来,而在整个布局的过程中对于布局面板里面的元素都要经过一个从最外面到最里面的一个递归的测量和排列的过程。在研究这个递归的排列和测试的过程,先来了解一下,基本的控件上关于布局的一些重要的方法和属性。
Windows 10的UI元素有两个非常重要的基类UIElement类和FrameworkElement类,他们的继承层次结构如下:
Windows.UI.Xaml.DependencyObject
Windows.UI.Xaml.UIElement
Windows.UI.Xaml.FrameworkElement
(1)UIElement类
UIElement类是具有可视外观并可以处理基本输入的大多数对象的基类。关于布局,UIElement类有两个非常重要的属性——DesiredSize和RenderSize属性和两个非常重要的方法——Measure 方法和Arrange方法。
DesiredSize属性:这是一个只读的属性,类型是Size类,表示在布局过程的测量处理过程中计算的大小。
RenderSize属性:这是一个只读的属性,类型是Size类,表示UI元素最终呈现大小,RenderSize和DesiredSize并不一定是相等的。RenderSize就是其ArrangeOverride方法的返回值。
public void Measure(Size availableSize)方法:Measure方法所做的事情是更新 UIElement 的 DesiredSize属性,测量出UI元素的大小。如果在该UI元素上实现了FrameworkElement.MeasureOverride(System.Windows.Size)方法,将会用此方法以形成递归布局更新。参数availableSize表示:父对象可以为子对象分配的可用空间。子对象可以请求大于可用空间的空间,如果该特定面板中允许滚动或其他调整大小行为,则提供的大小可以适应此空间。
public void Arrange(Rect finalRect)方法:Arrange方法所做的事情是定位子对象并确定 UIElement 的大小,也就是DesiredSize属性的值。如果在该UI元素上实现了FrameworkElement. ArrangeOverride(System.Windows.Size)方法,将会用此方法以形成递归布局更新。参数finalRect表示:布局中父对象为子对象计算的最终大小,作为 System.Windows.Rect 值提供。
(2)FrameworkElement类
FrameworkElement类是UIElement类的子类,为 Windows 10布局中涉及的对象提供公共 API 的框架。FrameworkElement类有两个和布局相关的虚方法MeasureOverride 方法和ArrangeOverride方法。如果已经存在的布局面板无法满足特殊的布局需求,你可能需要自定义布局面板,就需要重写MeasureOverride和ArrangeOverride两个方法,而这两个方法是Windows 10的布局系统提供给用户的自定义接口,下面来看下这两个方法的含义。
protected virtual Size MeasureOverride(Size availableSize):提供 Windows 10布局的度量处理过程的行为,可以重写该方法来定义其自己的度量处理过程行为。参数availableSize表示对象可以赋给子对象的可用大小,可以指定无穷大值 (System.Double.PositiveInfinity),以指示对象的大小将调整为可用内容的大小,如果子对象所计算出来的大小比availableSize大,那么将会被截取出availableSize大小的部分。返回结果表示此对象在布局过程中基于其对子对象分配大小的计算或者基于固定面板大小等其他因素而确定的它所需的大小。
protected virtual Size ArrangeOverride(Size finalSize):提供 Windows 10布局的排列处理过程的行为,可以重写该方法来定义其自己的排列处理过程行为。参数finalSize表示父级中此对象应用来排列自身及其子元素的最终区域。返回结果表示元素在布局中排列后使用的实际大小。
5.2.4 测量和排列的过程
Windows 10的布局系统是一个递归系统,它总是以Measure方法开始,最后以Ararnge方法结束。假设在整个布局系统里面只有一个对象,这个对象在加载到界面之前会先调用Measure方法来测量对象的大小,最后再调用Ararnge方法来安排对象的位置完成了整个过程的布局。但是现实中往往是一个对象里面包含了多个子对象,子对象里面也包含着子对象,如此递归下去,直到最底下的对象。布局的过程就是从最顶层的对象开始测量,最顶层的对象的测量过程又会调用它的子对象的测量方法,如此递归直到最底下的对象。测量的过程完成之后,则开始排列的过程,排列的过程也是和测量的过程的原理一样,一直递归下去直到最底下的对象。下面通过一个示例来模拟这个过程。
示例里面创建了两个类,TestPanel类用来模拟最外面的布局面板,作为父对象的角色;TestUIElement类用来模拟作为布局面板的元素,作为最底下子对象的角色。TestUIElement类和TestPanel类都继承了Panel类,实现了MeasureOverride和ArrangeOverride方法,并打印出相关的日志用于跟踪布局的详细情况。
代码清单5-8:模拟测量和排列的过程(源代码:第5章\Examples_5_8)
TestUIElement.cs文件主要代码 ------------------------------------------------------------------------------------------------------------------ public class TestUIElement : Panel { protected override Size MeasureOverride(Size availableSize) { Debug.WriteLine("进入子对象" + this.Name + "的MeasureOverride方法测量大小"); return availableSize; } protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) { Debug.WriteLine("进入子对象" + this.Name + "的ArrangeOverride方法进行排列"); return finalSize; } }
TestPanel.cs文件主要代码 ----------------------------------------------------------------------------------------------------------------- public class TestPanel : Panel { protected override Size MeasureOverride(Size availableSize) { Debug.WriteLine("进入父对象" + this.Name + "的MeasureOverride方法测量大小"); foreach (UIElement item in this.Children) { item.Measure(new Size(120, 120));//这里是入口 Debug.WriteLine("子对象的DesiredSize值 Width:" + item.DesiredSize.Width + " Height:" + item.DesiredSize.Height); Debug.WriteLine("子对象的RenderSize值 Width:" + item.RenderSize.Width + " Height:" + item.RenderSize.Height); } Debug.WriteLine("父对象的DesiredSize值 Width:" + this.DesiredSize.Width + " Height:" + this.DesiredSize.Height); Debug.WriteLine("父对象的RenderSize值 Width:" + this.RenderSize.Width + " Height:" + this.RenderSize.Height); return availableSize; } protected override Size ArrangeOverride(Size finalSize) { Debug.WriteLine("进入父对象" + this.Name + "的ArrangeOverride方法进行排列"); double x = 0; foreach (UIElement item in this.Children) { //排列子对象 item.Arrange(new Rect(x, 0, item.DesiredSize.Width, item.DesiredSize.Height)); x += item.DesiredSize.Width; Debug.WriteLine("子对象的DesiredSize值 Width:" + item.DesiredSize.Width + " Height:" + item.DesiredSize.Height); Debug.WriteLine("子对象的RenderSize值 Width:" + item.RenderSize.Width + " Height:" + item.RenderSize.Height); } Debug.WriteLine("父对象的DesiredSize值 Width:" + this.DesiredSize.Width + " Height:" + this.DesiredSize.Height); Debug.WriteLine("父对象的RenderSize值 Width:" + this.RenderSize.Width + " Height:" + this.RenderSize.Height); return finalSize; } }
创建了TestUIElement类和TestPanel类之后,接下来要在UI上使用这两个类,把TestPanel看作是布局面板控件来使用,把TestPanel看作是普通控件来使用,然后观察打印出来的运行日志。要添加这两个控件需要现在xaml页面上把这两个空间所在的空间引入进去再进行调用,如下面的代码所示:
MainPage.xaml文件主要代码 ----------------------------------------------------------------------------------------------------------------- ……省略若干代码 下面引入控件空间 xmlns:local="using:MeasureArrangeDemo" ……省略若干代码 下面调用控件布局 <StackPanel> <Button Content="改变高度" Click="Button_Click_1"></Button> <local:TestPanel x:Name="panel" Height="400" Width="400" Background="White" > <local:TestUIElement x:Name="element1" Width="60" Height="60" Background="Red" Margin="10"/> <local:TestUIElement x:Name="element2" Width="60" Height="60" Background="Red" /> </local:TestPanel> </StackPanel>
程序在Debug的状态下运行之后,在Visual Studio的输出窗口可以看到打印出来的日志。日志的详细情况如下:
/*日志开始*/
进入父对象panel的MeasureOverride方法测量大小
进入子对象element1的MeasureOverride方法测量大小
子对象的DesiredSize值 Width:80 Height:80
子对象的RenderSize值 Width:0 Height:0
进入子对象element2的MeasureOverride方法测量大小
子对象的DesiredSize值 Width:60 Height:60
子对象的RenderSize值 Width:0 Height:0
父对象的DesiredSize值 Width:0 Height:0
父对象的RenderSize值 Width:0 Height:0
进入父对象panel的ArrangeOverride方法进行排列
进入子对象element1的ArrangeOverride方法进行排列
子对象的DesiredSize值 Width:80 Height:80
子对象的RenderSize值 Width:60 Height:60
进入子对象element2的ArrangeOverride方法进行排列
子对象的DesiredSize值 Width:60 Height:60
子对象的RenderSize值 Width:60 Height:60
父对象的DesiredSize值 Width:400 Height:400
父对象的RenderSize值 Width:0 Height:0
/*日志结束*/
从打印出来的日志可以很清楚地看到整个布局过程的步骤(如图5.25所示),以及布局过程中DesiredSize值和RenderSize值的变化情况。
从布局的过程中可以总结出下面的结论:
1. 测量的过程是为了确认DesiredSize的值,最终是要提供给排列的过程去使用。
2. DesiredSize是根据Margin,,Width,Height等属性来决定。
3. 排列的过程确定RenderSize,以及最终子对象被安置的空间。RenderSize就是ArrangeOverride的返回值,没还有被裁剪过的值。
4. Margin,,Width,Height等属性只是控件表面上的属性,而实际掌控住这些效果的是布局的测量排列过程。
5. Margin,,Width,Height等属性的改变会重新触发布局的过程。
源代码下载:http://vdisk.weibo.com/u/2186322691
目录:http://www.cnblogs.com/linzheng/p/5021428.html
欢迎关注我的微博@WP林政 微信公众号:wp开发(号:wpkaifa)
Windows10/WP技术交流群:284783431