[WP8.1UI控件编程]Windows Phone自定义布局规则
3.2 自定义布局规则
上一节介绍了Windows Phone的系统布局面板和布局系统的相关原理,那么系统的布局面板并不一定会满足所有的你想要实现的布局规律,如果有一些特殊的布局规律,系统的布局面板是不支持,这时候就需要去自定义实现一个布局面板,在自定义的布局面板里面封装布局规律的逻辑。那么我们这一节从一个实际的需求出发,来实现一个自定义规律的布局面板。我们这一小节要实现的布局规律是把布局面板里面的子元素,按照圆形的排列规则进行排列,下面我们来看下这个例子的详细实现过程。
3.2.1 创建布局类
在Windows Phone要实现类似Grid、StackPanel的自定义布局规则的面板,首先要做的事情是要创建一个自定义的布局类。所有的布局面板都需要从Panel类派生,自定义实现其测量和排列的过程。Panel类中的Children属性表示是布局面板里面的子对象,测量和排列的过程中需要根据Children属性来获取面板中所有的子对象,然后再根据相关的规律对这些子对象进行测量和排列。
如果我们的布局类需要外面传递进来一些特殊的参数,那么就需要我们在布局类里面去实现相关的属性。当然像Heigh、Width等这些Panel类原本就支持的属性我们就无需再去定义,如我们在这个例子里面要实现的圆形布局,这时候是需要一个圆形的半径大小的,这个半径的大小就可以作为一个属性让外面把数值传递进来,然后布局类再根据这个半径的大小来进行处理对子对象的测量和排列。需要注意的是,自定义的半径属性发生改变的时候,需要调用InvalidateArrange方法重新触发布局的排列过程,否则修改半径后将不会起到任何作用。
代码清单3-2:自定义布局规则(源代码:第3章\Examples_3_2)
下面我们来看一下,自定义的CirclePanel类:
public class CirclePanel : Panel { //自定义的半径变量 private double _radius = 0; public CirclePanel() { } //注册半径依赖属性 //"Radius" 表示半径属性的名称 // typeof(double) 表示半径属性的类型 // typeof(CirclePanel) 表示半径属性的归属者类型 // new PropertyMetadata(0.0, OnRadiusPropertyChanged)) 表示半径属性的元数据实例,0.0是默认值,OnRadiusPropertyChanged是属性改变的事件 public static readonly DependencyProperty RadiusProperty = DependencyProperty.RegisterAttached ("Radius", typeof(double), typeof(CirclePanel), new PropertyMetadata(0.0, OnRadiusPropertyChanged)); //定义半径属性 public double Radius { get { return (double)GetValue(RadiusProperty); } set { SetValue(RadiusProperty, value); } } //实现半径属性改变事件 private static void OnRadiusPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { //获取触发属性改变的CirclePanel对象 CirclePanel target = (CirclePanel)obj; //获取传递进来的最新的值,并赋值给半径变量 target._radius = (double)e.NewValue; //使排列状态失效,进行重新排列 target.InvalidateArrange(); } //重载基类的MeasureOverride方法 protected override Size MeasureOverride(Size availableSize) { //处理测量子对象的逻辑 return availableSize; } //重载基类的ArrangeOverride方法 protected override Size ArrangeOverride(Size finalSize) { //处理排列子对象的逻辑 return finalSize; } }
3.2.2 实现测量过程
测量的过程是在重载的MeasureOverride方法上实现,在MeasureOverride方法上需要做的第一件事情就是要把所有的子对象都遍历一次,调用其Measure方法来测量子对象的大小。然后在测量的过程中可以获取到子对象测量出来的宽度高度,我们可以根据这些信息来给自定义的面板分配其大小。
protected override Size MeasureOverride(Size availableSize) { //最大的宽度的变量 double maxElementWidth = 0; //遍历所有的子对象,并调用子对象的Measure方法进行测量,取出最大的宽度的子对象 foreach (UIElement child in Children) { //测量子对象 child.Measure(availableSize); maxElementWidth = Math.Max(child.DesiredSize.Width, maxElementWidth); } //两个半径的大小和最大的宽度的两倍最为面板的宽度 double panelWidth = 2 * this.Radius + 2 * maxElementWidth; //取面板的所分配的高度宽度和计算出来的宽度的最小值最为面板的实际大小 double width = Math.Min(panelWidth, availableSize.Width); double heigh = Math.Min(panelWidth, availableSize.Height); return new Size(width, heigh); }
3.2.3 实现排列过程
排列的过程是在重载的ArrangeOverride方法上实现,在ArrangeOverride方法上通过相关的规则把子对象一一地进行排列。我们在例子里面要实现的是把子对象按照一个固定的圆形进行排列,所以在ArrangeOverride方法上需要计算每个子对象所占的角度大小,通过角度计算子对象在面板中的坐标,然后按照一定的角度对子对象进行旋转来适应圆形的布局。排列原理图如图3.7所示,实现代码如下。
protected override Size ArrangeOverride(Size finalSize) { //当前的角度,从0开始排列 double degree = 0; //计算每个子对象所占用的角度大小 double degreeStep = (double)360 / this.Children.Count; //计算 double mX = this.DesiredSize.Width / 2; double mY = this.DesiredSize.Height / 2; //遍历所有的子对象进行排列 foreach (UIElement child in Children) { //把角度转换为弧度单位 double angle = Math.PI * degree / 180.0; //根据弧度计算出圆弧上的x,y的坐标值 double x = Math.Cos(angle) * this._radius; double y = Math.Sin(angle) * this._radius; //使用变换效果让控件旋转角度degree RotateTransform rotateTransform = new RotateTransform(); rotateTransform.Angle = degree; rotateTransform.CenterX = 0; rotateTransform.CenterY = 0; child.RenderTransform = rotateTransform; //排列子对象 child.Arrange(new Rect(mX + x, mY + y, child.DesiredSize.Width, child.DesiredSize.Height)); //角度递增 degree += degreeStep; } return finalSize; }
3.2.4 应用布局规则
在上面我们已经把自定义的圆形布局控件实现了,现在要在XAML页面上应用该布局面板来进行布局。在这个例子里面,我们还通过一个Slider控件来动态改变布局面板的半径大小,来观察布局的变化。
首先我们在XAML页面上引入布局面板所在的空间,如下所示:
xmlns:local="clr-namespace:CustomPanelDemo"
然后在XAML页面上运用自定义的圆形布局控件,并且通过Slider控件的ValueChanged来动态给圆形布局控件的半径赋值。代码如下:
MainPage.xaml文件主要代码 ------------------------------------------------------------------------------------------------------------------ <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Slider Grid.Row="0" Value="5" ValueChanged="Slider_ValueChanged_1"></Slider> <local:CirclePanel x:Name="circlePanel" Radius="50" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock>Start here</TextBlock> <TextBlock>TextBlock 1</TextBlock> <TextBlock>TextBlock 2</TextBlock> <TextBlock>TextBlock 3</TextBlock> <TextBlock>TextBlock 4</TextBlock> <TextBlock>TextBlock 5</TextBlock> <TextBlock>TextBlock 6</TextBlock> <TextBlock>TextBlock 7</TextBlock> </local:CirclePanel> </Grid>
MainPage.xaml.cs文件主要代码 ------------------------------------------------------------------------------------------------------------------ private void Slider_ValueChanged_1(object sender, RangeBaseValueChangedEventArgs e) { if (circlePanel != null) { circlePanel.Radius = e.NewValue * 10; } }