读书笔记3:uwp布局原理与自定义布局设计

布局原理 

  布局的意义

  布局是页面编程的第一步,是总体把握页面上UI元素的显式。由于Windows10支持不同分辨率不同设备,布局显得越来越重要,也越来越复杂。。布局有着如下意义:

  1)代码逻辑:良好的布局会使代码逻辑非常清晰,不好的布局方案回事页面代码逻辑混乱。好的布局方案,要给予对各种布局控件的理解,然后充分的利用他们的特性去实现布局的效果。

  2)效率性能:布局不仅仅是界面UI的事情,他甚至会影响程序的运行效率。当界面要展示大量的控件时,布局的好坏就会直接影响到程序的效率。良好的布局实现逻辑会让程序即使在有大量控件的页面也能流畅地运行。

  3)动态适配:动态适配包括两个方面,一是Windows10对多种分辨率界面的适配;二是当页面的控件不确定时会产生动态适配的问题。好的布局方案可以使应用程序兼容各种分辨率的区别。对于动态产生的控件,在布局时就要有足够的空间来适配会变化的空间结构和页面变化。

  4)实现复杂的布局:有时候界面需要实现一些复杂的布局,而对应的Windows10 是没有这样的布局控件去支持这样的复杂的布局效果的,这时候就需要自定义布局的规律来解决这样的问题。

  布局系统

  布局系统是指对Windows10 布局面板所进行的布局过程的统称。要定义一个可视化对象,必须将它放置于Panel或其他布局面板中。Panel类定义了在屏幕上绘制所有面板里的成员(Children属性)的布局行为。布局是一个递归系统,实现在屏幕上对元素进行大小调整、定位和绘制。在整个布局过程中,布局系统对布局面板成员的处理分为两个过程:一是测量处理过程,而是排列处理过程。每当面板里的成员改变其位置时,布局系统就可能触发一个新的处理过程,重新处理上面所说的两个过程。调用布局系统,总会发生以下一系列操作:

  (1)第一次递归遍历每个布局面板的子元素的大小。

  (2)计算在FrameworkElement类的子类控件元素上定义的大小调整属性,例如Width、Height和Margin。

  (3)应用布局面板特定逻辑,例如StackPanel 的水平布局。

  (4)第二次递归遍历负责吧子元素排到自己的相对位置

  (5)把所有的子元素绘制到屏幕上

  因此,了解布局系统的特性是很重要的,因为不必要的调用可能导致应用性能变差。

  布局系统的重要方法和属性

  Windows10 的 UI 元素有两个非常重要的基类,他们的继承层次结构如下:

1 Windows.UI.Xaml.DependencyObject
2     Windows.UI.Xaml.UIElement
3         Windows.UI.Xaml.FrameworkElement

  1)UIElement类:

  UIElement类是具有可视外观并可以处理基本输入的大多数对象的基类。关于布局,UIElement类有两个重要属性和两个重要方法:

  DesiredSize属性:只读属性,类型是Size类,表示在布局过程中测量处理过程中计算的大小。

  RenderSize属性:只读属性,类型为Size类,表示UI元素最终呈现的大小。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的大小。如果在该 UI 元素上实现了FrameworkElement.ArrangeOverride 方法,将会用此方法形成递归布局更新。参数finalRect表示布局中父对象为子对象计算的最终大小,作为System.Windows.Rect值提供。

  2)FrameworkElement类:

  FrameworkElement类是UIElement类的子类,为Windows10 布局中涉及的对象提供公共的API框架。FrameworkElement类中有两个和布局相关的virtual方法:MeasureOverride 方法和 ArrangeOverride 方法。如果为了满足特定的布局需求需要自定义布局面板,就需要重写MeasureOverride 和 ArrangeOverride 方法。

  protected virtual Size MearsureOverride(Size availableSize):提供Windows10 布局的度量处理过程的行为,可以重写该方法来定义自己的度量处理过程行为。参数availableSize 表示对象可以赋给子对象的可用大小,可以指定无穷大值(System.Double.PositiveInfinity),以指示对象的大小将调整为可用内容的大小,如果计算出来的值大于availableSize,将被截取同availableSize大小的部分。返回结果表示该对象在布局过程中基于不同因素未确定的所需的大小。

  protected virtual Size ArrangeOverride(Size finalSize):提供Windows10 布局的排列处理过程中的行为,可以重写该方法开定义其排列处理过程的行为。参数finalSize表示父级中此对象应用来排列自身及其子元素的最终区域。返回结果表示元素在布局中排列后使用的实际大小。

  自定义布局规则

  以下实现了一个新的自定义布局面板,将布局面板中的元素按照圆形的排列规则进行排列:

  1)创建布局类

  所有的布局面板都需要从Panel类派生,自定义其测量和排列的过程。Panel类中Children属性表示布局里的子对象,测量和排列的过程需要根据Children属性来获取面板中所有的子对象,再根据排列规矩对这些子对象进行测量和排列。

  Circle类的定义

 1 namespace Newdefine_panel
 2 {
 3     public class CirclePanel : Panel
 4     {
 5         //自定义半径变量
 6         private double _radius = 0;
 7         public CirclePanel() { }
 8         //注册半径依赖属性
 9         //"Radius"表示半径属性的名称
10         //typeof(double)表示半径属性类型
11         //typeof(CirclePanel)表示半径属性的归属这类型
12         // new PropertyMetadata(0.0, OnRadiusPropertyChanged)表示半径属性的元数据实例
13         public static readonly DependencyProperty RadiusProperty = DependencyProperty.RegisterAttached
14         ("Radius", typeof(double), typeof(CirclePanel), new PropertyMetadata(0.0, OnRadiusPropertyChanged));
15 
16         //定义半径属性
17         public double Radius
18         {
19             get { return (double)GetValue(RadiusProperty); }
20             set { SetValue(RadiusProperty, value); }
21         }
22 
23         //实现半径属性改变事件
24         private static void OnRadiusPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
25         {
26             //获取触发属性改变的CirclePanel对象
27             CirclePanel target = (CirclePanel)obj;
28             //获取传递进来的值,并赋值给半径变量
29             target._radius = (double)e.NewValue;
30             //使排列状态失效,进行重新排列
31             target.InvalidateArrange();
32         }
33 
34         //重载基类的MeasureOverride
35         protected override Size MeasureOverride(Size availableSize)
36         {
37             //处理测量子对象的逻辑
38             return new Size(width, heigh);
39         }
40         protected override Size ArrangeOverride(Size finalSize)
41         {
42              //处理排列子对象的逻辑
43              return finalSize;
44         }
45     }
46 
47 }

 

 2)实现测量过程

  测量过程是在Override 的 MeasureOverride方法中实现的,该方法首先要遍历所有子对象,通过Measure()方法来测量子对象的大小,通过这些信息给自定义面板分配大小。

 1   protected override Size MeasureOverride(Size availableSize)
 2         {
 3             //最大宽度变量
 4             double maxElementWidth = 0;
 5             //遍历所有子对象,并调用子对象的Measure方法进行测量,取出宽度最大的子对象
 6             foreach (UIElement child in Children)
 7             {
 8                 //Measure方法返回DesiredSize属性
 9                 child.Measure(availableSize);
10                 maxElementWidth = Math.Max(child.DesiredSize.Width, maxElementWidth);
11             }
12             //取两个半径的大小和最大宽度的两倍作为面板宽度
13             double panelWidth = 2 * this.Radius + 2 * maxElementWidth;
14             //计算面板的实际大小并返回Size对象。
15             double width = Math.Min(panelWidth, availableSize.Width);
16             double heigh = Math.Min(panelWidth, availableSize.Height);
17             return new Size(width, heigh);
18         }

  

  3)实现排列过程

  排列过程在重载的ArrangeOverride方法中实现。在ArrangeOverride方法中通过自定义的规则一一排列子对象。在实例中要实现的是把子对象按照一个固定的圆形进行排列,为此要计算每个子对象所占的角度,计算器坐标,然后按照一定的角度对子对像进行旋转来适应圆形的布局。实现代码如下:

 1 protected override Size ArrangeOverride(Size finalSize)
 2         {
 3             //当前的角度,从0开始排列
 4             double degree = 0;
 5             //计算每个子对象所占用的角度的大小
 6             double degreeStep = (double)360 / this.Children.Count;
 7 
 8             double mX = this.DesiredSize.Width / 2;
 9             double mY = this.DesiredSize.Height / 2;
10             //遍历所有的子对象进行排列
11             foreach(UIElement child in Children)
12             {
13                 //角度使用弧度制表示
14                 double angle = Math.PI * degree / 180.0;
15                 //根据弧度计算圆弧上的x,y值
16                 double x = Math.Cos(angle) * this._radius;
17                 double y = Math.Sin(angle) * this._radius;
18                 //使用变换效果让控件旋转角度degree
19                 RotateTransform rotateTransform = new RotateTransform();
20                 rotateTransform.Angle = degree;
21                 rotateTransform.CenterX = 0;
22                 rotateTransform.CenterY = 0;
23                 child.RenderTransform = rotateTransform;
24                 //排列子对像
25                 child.Arrange(new Rect(mX + x, mY + y, child.DesiredSize.Width, child.DesiredSize.Height));
26                 //角度递增
27                 degree += degreeStep;
28             }
29             return finalSize;
30         }

  

  4)布局规则应用

  自定义的圆形布局空间已经实现了。接下来在XAML页面中应用该布局面板来进行布局。在下面所示的XAML代码中还使用到了Slider控件的ValueChanged属性动态改变半径。

 1 //MainPage.XAMl代码
 2 
 3 <Page
 4     x:Class="Newdefine_panel.MainPage"
 5     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 6     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 7     xmlns:local="using:Newdefine_panel"
 8     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 9     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
10     mc:Ignorable="d">
11 
12     <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
13         <Grid.RowDefinitions>
14             <RowDefinition Height="Auto"/>
15             <RowDefinition Height="*"/>
16         </Grid.RowDefinitions>
17         <Slider Grid.Row="0" Value="5" ValueChanged="Slider_ValueChanged_1"></Slider>
18         <local:CirclePanel x:Name="circlePanel" Radius="50" Grid.Row="1"  HorizontalAlignment="Center" VerticalAlignment="Center">
19             <TextBlock>Start here</TextBlock>
20             <TextBlock>TextBlock 1</TextBlock>
21             <TextBlock>TextBlock 2</TextBlock>
22             <TextBlock>TextBlock 3</TextBlock>
23             <TextBlock>TextBlock 4</TextBlock>
24             <TextBlock>TextBlock 5</TextBlock>
25             <TextBlock>TextBlock 6</TextBlock>
26             <TextBlock>TextBlock 7</TextBlock>
27         </local:CirclePanel>
28     </Grid>
29 </Page>

以下通过Slider控件的ValueChanged来动态给圆形布局控件半径赋值

1 //MainPage.XAML.cs代码
2 private void Slider_ValueChanged_1(object sender, RangeBaseValueChangedEventArgs e)
3 {
4     if (circlePanel != null)
5     {
6         circlePanel.Radius = e.NewValue * 10;
7     }
8 }

  最终得到的应用程序运行效果如图所示:

读书笔记3:uwp布局原理与自定义布局设计