WPF学习 - 自定义Panel

WPF中的Panel(面板),是继承自FrameworkElement的抽象类,表示一个可以用来排列子元素的面板。

在WPF中,一种预设了几种常用的面板,如Grid、StackPanel、WrapPanel等。他们都是继承自Panel类,并拥有自己的排列子元素的逻辑。

因此,想要自定义一个Panel,核心的问题就是如何排列子元素。

例如,我们做一个轴排列的元素,即元素沿着X、Y轴排列。我们定义,以面板宽度(Width)或者高度(Height)的中点,为坐标原点,水平向右为X轴,垂直向下位Y轴。如下图所示

 

先上源代码:

后代代码:

  1     public class AxisPanel:Panel
  2     {
  3        internal static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
  4            "Oritentation",typeof(Orientation),typeof(AxisPanel),new FrameworkPropertyMetadata(Orientation.Horizontal,FrameworkPropertyMetadataOptions.AffectsArrange));
  5 
  6 
  7         /// <summary>
  8         /// 指示子元素的布局方向
  9         /// </summary>
 10         public Orientation Orientation
 11         {
 12             get => (Orientation)GetValue(OrientationProperty);
 13             set => SetValue(OrientationProperty, value);
 14         }
 15 
 16 
 17         /// <summary>
 18         /// 测量子元素,以确定自己的DesiredSize。
 19         /// 此方法由父级元素自动调用
 20         /// </summary>
 21         /// <param name="availableSize">父级元素提供给该面板的可用空间尺寸</param>
 22         /// <returns>此面板对象需要的尺寸(DesiredSize)</returns>
 23         protected override Size MeasureOverride(Size availableSize)
 24         {
 25             double width = 0, height = 0;
 26             switch(Orientation)
 27             {
 28                 case Orientation.Horizontal:
 29                     // 水平布局
 30                     foreach(UIElement child in Children)
 31                     {
 32                         child.Measure(availableSize);
 33                         width += child.DesiredSize.Width;           // 计算面板需要的宽度;
 34 
 35                         if(child.DesiredSize.Height > height)       // 计算面板需要的高度:为所有元素中最大的高度
 36                         {
 37                             height = child.DesiredSize.Height;
 38                         }
 39                     }
 40                     break;
 41 
 42                 case Orientation.Vertical:
 43                     // 垂直布局
 44                     foreach(UIElement child in Children)
 45                     {
 46                         child.Measure(availableSize);
 47                         height += child.DesiredSize.Height;     // 计算面板需要的高度
 48                             
 49                         if(child.DesiredSize.Width > width)
 50                         {
 51                             width = child.DesiredSize.Width;    // 计算面板需要宽度:为所有元素中最大的宽度
 52                         }
 53                     }
 54                     break;
 55             }
 56 
 57             return new Size(width, height);
 58         }
 59 
 60 
 61 
 62         /// <summary>
 63         /// 排列子元素
 64         /// 此方法由父级元素自动调用
 65         /// </summary>
 66         /// <param name="finalSize">父级元素提供给该面板用于布局的最终尺寸。</param>
 67         /// <returns></returns>
 68         protected override Size ArrangeOverride(Size finalSize)
 69         {
 70             double totalWidth = 0, totalHeight = 0;                                         // 面板的总宽度、高度
 71             switch(Orientation)
 72             {
 73                 case Orientation.Horizontal:                                                // 水平排列:需要计算子元素左上角的坐标点,已经用于排列子元素的区域大小
 74                     double x1, y1;                                                      // 子元素左上角点的坐标
 75                     for (int i=0;i<Children.Count;i++)
 76                     {
 77                         x1 = totalWidth;                                                    // 子元素的x坐标,应该为总宽度的值
 78                         y1 = (finalSize.Height - Children[i].DesiredSize.Height) / 2;       // 子元素的y坐标,始终保存在轴上
 79                         totalWidth += Children[i].DesiredSize.Width;                        // 总宽度增加
 80                         Rect rect = new(x1, y1, Children[i].DesiredSize.Width, Children[i].DesiredSize.Height);       // 放置子元素的矩形区域
 81                         Children[i].Arrange(rect);                                          // 放置子元素
 82                     }
 83                     break;
 84 
 85                 case Orientation.Vertical:
 86                     double x2, y2=0;
 87 
 88                     for (int i = 0; i < Children.Count;i++)
 89                     {
 90                         x2 = (finalSize.Width - Children[i].DesiredSize.Width) / 2;
 91                         y2 = totalHeight;
 92                         totalHeight += Children[i].DesiredSize.Height;                      // 所有子元素的总高度
 93                         Rect rect2 = new(x2, y2, Children[i].DesiredSize.Width, Children[i].DesiredSize.Height);
 94                         Children[i].Arrange(rect2);
 95                     }
 96                     break;
 97             }
 98             return finalSize;
 99         }
100     }
View Code

前端代码:

1     <rayPanel:AxisPanel Orientation="Horizontal">
2         <Button Content="Button1" Width="100" Height="40"/>
3         <Button Content="Button2" Width="100" Height="40" Canvas.Left="100"/>
4         <Button Content="Button3" Width="100" Height="40" Canvas.Left="200"/>
5         <TextBlock Text="你在么?"/>
6     </rayPanel:AxisPanel>
View Code

显示效果如下:

                (当Orientation = “Horizontal”)                                                     (当Orientation="Vertical")

 

名词简写:

元素:表示一个继承自UIElement类的实例。

Frame元素:表示一个继承自FrameworkElement类的实例。

因为FrameworkElement是继承自UIElement的,因此在很多时候,一个FrameworkElement也可以被称为元素。只有在涉及到FrameworkElement类自己的属性、方法时,会被称为Frame元素。

 

WPF的布局,分为两个步骤:

1. 测量:确定子元素需要的尺寸(DesiredSize)。

2. 排列:确定子元素的位置,大小。

1. 测量:

在UIElement中,定义了两个方法:Measure() 和 MeasureCore()。在FrameworkElement中还定义了MeasureOverride()方法。他们的工作方式是:

1). 父级元素调用子元素实例的Measure(),此时Measure()方法会做一堆的逻辑检查,然后调用其MeasureCore()方法。在MeasureCore()方法中,真正的去计算元素的期望尺寸(DesiredSize)。在UIElement类中,MeasureCore()返回的尺寸永远是(0,0)(请看微软的源代码:UIElement.MeasureCore())。这意味着如果子元素是一个UIElement的实例,则其期望尺寸永远会是(0,0)。

2).FrameworkElement继承了UIElement,重写并密封了MeasureCore()方法。因此,当调用子元素的Measure()方法时,子元素的Measure()方法实际上会调用重写后的MeasureCore()方法,以计算Frame元素的期望尺寸。

3).在FrameworkElement重写的MeasureCore()方法中,实际上又会调用MeasureOverride()方法。MeasureOverride()方法是一个虚方法,可供用户重写,以实现用户自己的测量逻辑,同时还能保证相关的检查不被遗漏。

2. 排列:

排列的工作方式,与“测量”是一样的。不再赘述。

 

由此可知,要实现自己的布局,就有三个选择:

1)继承自UIElement,并重写MeasureCore()和ArrangeCore()方法(并添加需要的属性等)。此路径难度很大。

2)继承自FrameworkElement,并重写MeasureOverride()和ArrangeOverride()方法(并添加需要的属性等)。此路径难度不小。

3)继承自Panel,并重写MeasureOverride()和ArrageOverride()方法(并添加需要的属性等)。此路径难度最小。因为Panel类已经定义了很多相关的属性、方法可共用户使用。

 

参数的意义

  在MeasureOverride()和ArrageOverride()方法中,两个参数及返回值的意义是不同的:

  MeasureOverride():该方法的参数availableSize,表示父元素,可以用来布局该元素的可用空间。此参数由WPF布局系统确定;返回值为该元素期望的尺寸(DesiredSize)。自定义面板中,如果用户不重写该方法(实现自己的测量方式),则会返回一个(0,0)的期望尺寸。

  ArrangeOverride():该方法的参数finalSize,表示父元素,最终分配给该元素,用于排列子元素的尺寸。当WPF的布局系统测量完各控件的尺寸后,开始依次排列子元素。当排列到本元素时,剩余的空间可能与MeasureOverride()方法中的availableSize不同。因此此时传递进来的,是可以用于排列子元素的最终的空间大小。在ArrangeOverride()方法中的,可以按照自己的逻辑去排列子元素。并最终返回一个尺寸值(Size类型),告诉WPF的布局系统,该元素最终使用了多少空间。

 

以上为自己学习并理解的。如有错误,还请指正。

posted @ 2022-09-04 18:03  卓尔不设凡  阅读(1045)  评论(0)    收藏  举报