前面两个章节分别介绍了两个自定义控件:自定义的ColorPicker和FlipPanel控件。接下来介绍派生自定义面板以及构建自定义绘图控件。
创建自定义面板是一种特殊但较常见的自定义控件开发子集。前面以及介绍过有关面板方面的知识,了解到面板驻留一个或多个子元素,并且实现了特定的布局逻辑以恰当地安排子元素。如果希望构建自己的可拖动的工具栏或可停靠的窗口系统,自定义面板是很重要的元素。当创建需要非标准特定布局的组合控件时,自定义面板通常很有用的,例如停靠工具栏。
接下里介绍一个基本的Canvas面板部分以及一个增强版本的WrapPanel面板两个简单的示例。
一、两步布局过程
每个面板都使用相同的设备:负责改变子元素尺寸和安排子元素的两步布局过程。第一阶段是测量阶段(measure pass),在这一阶段面板决定其子元素希望具有多大的尺寸。第二个阶段是排列阶段(layout pass),在这一阶段为每个控件指定边界。这两个步骤是必需的,因为在决定如何分割可用空间时,面板需要考虑所有子元素的期望。
可以通过重写名称为MeasureOverride()和ArrangeOverride()方法,为这两个步骤添加自己的逻辑,这两个方法是作为WPF布局系统的一部分在FrameworkElement类中定义的。奇特的名称使用标识MeasureOverride()和ArrangeOverride()方法代替在MeasureCore()和ArrangeCore()方法中定义的逻辑,后两个方法在UIElement类中定义的。这两个方法是不能被重写的。
1、MeasureOverride()方法
第一步是首先使用MeasureOverride()方法决定每个子元素希望多大的空间。然而,即使是在MeasureOverride()方法中,也不能为子元素提供无限空间,至少,也应当将自元素限制在能够适应面板可用空间的范围之内。此外,可能希望更严格地限制子元素。例如,具有按比例分配尺寸的两行的Grid面板,会为子元素提供可用高度的一般。StackPanel面板会为第一个元素提供所有可用空间,然后为第二个元素提供剩余的空间等等。
每个MeasureOverride()方法的实现负责遍历子元素集合,并调用每个子元素的Measure()方法。当调用Measure()方法时,需要提供边界框——决定每个子空间最大可用空间的Size对象。在MeasureOverride()方法的最后,面板返回显示所有子元素所需的空间,并返回它们所期望的尺寸。
下面是MeasureOverride()方法的基本结构,其中没有具体的尺寸细节:
protected override Size MeasureOverride(Size constraint)
{//Examine all the childrenforeach (UIElement element in base.InternalChildren){//Ask each child how much space it would like,given the//availableSize constraintSize availableSize=new Size{...};element.Measure(availableSize);//(you can now read element.DesiredSize to get the requested size.)}//Indicate how mush space this panel requires.//This will be used to set the DesiredSize property of the panel.return new Size(...);
}
Measure()方法不返回数值。在为每个子元素调用Measure()方法之后,子元素的DesiredSize属性提供了请求的尺寸。可以在为后续子元素执行计算是(以及决定面板需要的总空间时)使用这一信息。
因为许多元素直接调用了Measure()方法之后才会渲染它们自身,所以必须为每个子元素调用Measure()方法,即使不希望限制子元素的尺寸或使用DesiredSize属性也同样如此。如果希望让所有子元素能够自由获得它们所希望的全部空间,可以传递在两个方向上的值都是Double.PositiveInfinity的Size对象(ScrollViewer是使用这种策略的一个元素,原因是它可以处理任意数量的内容)。然后子元素会返回其中所有内容所需要的空间。否则,子元素通常会返回其中内容需要的空间或可用空间——返回较小值。
在测量过程的结尾,布局容器必须返回它所期望的尺寸。在简单的面包中,可以通过组合每个子元素的期望尺寸计算面板所期望的尺寸。
Measure()方法触发MeasureOverride()方法。所以如果在一个布局容器中放置另一个布局容器,当调用Measure()方法时,将会得到布局容器及其所有子元素所需要的总尺寸。
2、ArrangeOverride()方法
测量完所有元素后,就可以在可用的空间中排列元素了。布局系统调用面板的ArrangeOverride()方法,而面板为每个子元素调用Arrange()方法,以高速子元素为它分配了多大的控件(Arrange()方法会触发ArrangeOverride()方法,这与Measure()方法会触发MeasureOverride()方法非常类似).
当使用Measure()方法测量条目时,传递能够定义可用空间边界的Size对象。当使用Arrange()方法放置条目时,传递能够定义条目尺寸和位置的System.Windows.Rect对象。这时,就像使用Canvas面板风格的X和Y坐标放置每个元素一样(坐标确定布局容器左上角与元素左上角之间的距离)。
下面是ArrangeOverride()方法的基本结构。
protected override Size ArrangeOverride(Size arrangeBounds)
{//Examine all the children.foreach(UIElement element in base.InternalChildren){//Assign the child it's bounds.Rect bounds=new Rect(...);element.Arrange(bounds);//(You can now read element.ActualHeight and element.ActualWidth to find out the size it used ..)}//Indicate how much space this panel occupies.//This will be used to set the AcutalHeight and ActualWidth properties//of the panel.return arrangeBounds;
}
当排列元素时,不能传递无限尺寸。然而,可以通过传递来自DesiredSize属性值,为元素提供它所期望的数值。也可以为元素提供比所需尺寸更大的空间。实际上,经常会出现这种情况。例如,垂直的StackPanel面板为其子元素提供所请求的高度,但是为了子元素提供面板本身的整个宽度。同样,Grid面板使用具有固定尺寸或按比例计算尺寸的行,这些行的尺寸可能大于其内部元素所期望的尺寸。即使已经在根据内容改变尺寸的容器中放置了元素,如果使用Height和Width属性明确设置了元素的尺寸,那么仍可以扩展该元素。
当使元素比所期望的尺寸更大时,就需要使用HorizontalAlignment和VerticalAlignment属性。元素内容被放置到指定边界内部的某个位置。
因为ArrangeOverride()方法总是接收定义的尺寸(而非无限的尺寸),所以为了设置面板的最终尺寸,可以返回传递的Size对象。实际上,许多布局容器就是采用这一步骤来占据提供的所有空间。
二、Canvas面板的副本
理解这两个方法的最快捷方法是研究Canvas类的内部工作原理,Canvas是最简单的布局容器。为了创建自己的Canvas风格的面板,只需要简单地继承Panel类,并且添加MeasureOverride()和ArrangeOverride()方法,如下所示:
public class CanvasClone:System.Windows.Controls.Panel{...}
Canvas面板在他们希望的位置放置子元素,并且为子元素设置它们希望的尺寸。所以,Canvas面板不需要计算如何分割可用空间。这使得MeasureOverride()方法非常简单。为每个子元素提供无限的空间:
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{Size size = new Size(double.PositiveInfinity, double.PositiveInfinity);foreach (UIElement element in base.InternalChildren){element.Measure(size);}return new Size();
}
注意,MeasureOverride()方法返回空的Size对象。这意味着Canvas 面板根本不请求人和空间,而是由用户明确地为Canvas面板指定尺寸,或者将其放置到布局容器中进行拉伸以填充整个容器的可用空间。
ArrangeOverride()方法包含的内容稍微多一些。为了确定每个元素的正确位置,Canvas面板使用附加属性(Left、Right、Top以及Bottom)。附加属性使用定义类中的两个辅助方法实现:GetProperty()和SetProperty()方法。
下面是用于排列元素的代码:
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize){foreach (UIElement element in base.InternalChildren){double x = 0;double y = 0;double left = Canvas.GetLeft(element);if (!DoubleUtil.IsNaN(left)){x = left;}double top = Canvas.GetTop(element);if (!DoubleUtil.IsNaN(top)){y = top;}element.Arrange(new Rect(new Point(x, y), element.DesiredSize));}return finalSize;}
三、更好的WrapPanel面板
WrapPanel面板执行一个简单的功能,该功能有有时十分有用。该面板逐个地布置其子元素,一旦当前行的宽度用完,就会切换到下一行。但有时候需要采用一种方法来强制立即换行,以便在新行中启动某个特定控件。尽管WrapPanel面板原本没有提供这一功能,但通过创建自定义控件可以方便地添加该功能。只需要添加一个请求换行的附加属性即可。此后,面板中的子元素可使用该属性在适当位置换行。
下面的代码清单显示了WrapBreakPanel类,该类添加了LineBreakBeforeProperty附加属性。当将该属性设置为true时,这个属性会导致在元素之前立即换行。
public class WrapBreakPanel : Panel{public static DependencyProperty LineBreakBeforeProperty;static WrapBreakPanel(){FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();metadata.AffectsArrange = true;metadata.AffectsMeasure = true;LineBreakBeforeProperty = DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(WrapBreakPanel), metadata);}...}
与所有依赖项属性一样,LineBreakBefore属性被定义成静态字段,然后在自定义类的静态构造函数中注册该属性。唯一的区别在于进行注册时使用的是RegisterAttached()方法而非Register()方法。
用于LineBreakBefore属性的FrameworkPropertyMetadata对象明确指定该属性影响布局过程。所以,无论何时设置该属性,都会触发新的排列阶段。
这里没有使用常规属性封装器封装这些附加属性,因为不在定义它们的同一个类中设置它们。相反,需要提供两个静态方法,这来改那个方法能够使用DependencyObject.SetValue()方法在任意元素上设置这个属性。下面是LineBreakBefore属性需要的代码:
/// <summary>
/// 设置附加属性值
/// </summary>
/// <param name="element"></param>
/// <param name="value"></param>
public static void SetLineBreakBefore(UIElement element, Boolean value)
{element.SetValue(LineBreakBeforeProperty, value);
}/// <summary>
/// 获取附加属性值
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static Boolean GetLineBreakBefore(UIElement element)
{return (bool)element.GetValue(LineBreakBeforeProperty);
}
唯一保留的细节是当执行布局逻辑时需要考虑该属性。WrapBreakPanel面板的布局逻辑以WrapPanel面板的布局逻辑为基础。在测量阶段,元素按行排列,从而使面板能够计算需要的总空间。除非太大或LineBreakBefore属性被设置为true。否则每个元素都呗添加到当前行中。下面是完整的代码:
protected override Size MeasureOverride(Size constraint){Size currentLineSize = new Size();Size panelSize = new Size();foreach (UIElement element in base.InternalChildren){element.Measure(constraint);Size desiredSize = element.DesiredSize;if (GetLineBreakBefore(element) ||currentLineSize.Width + desiredSize.Width > constraint.Width){// Switch to a new line (either because the element has requested it// or space has run out).panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);panelSize.Height += currentLineSize.Height;currentLineSize = desiredSize;// If the element is too wide to fit using the maximum width of the line,// just give it a separate line.if (desiredSize.Width > constraint.Width){panelSize.Width = Math.Max(desiredSize.Width, panelSize.Width);panelSize.Height += desiredSize.Height;currentLineSize = new Size();}}else{// Keep adding to the current line.currentLineSize.Width += desiredSize.Width;// Make sure the line is as tall as its tallest element.currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);}}// Return the size required to fit all elements.// Ordinarily, this is the width of the constraint, and the height// is based on the size of the elements.// However, if an element is wider than the width given to the panel,// the desired width will be the width of that line.panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);panelSize.Height += currentLineSize.Height;return panelSize;}
上面代码中的重要细节是检查LineBreakBefore属性。这实现了普遍WrapPanel面板没有提供的额外逻辑。
ArrangeOverride()方法的代码几乎相同。区别在于:面板在开始布局一行之前需要决定该行的最大高度(根据最高的元素确定)。这样,每个元素可以得到完整数量的可用空间,可用控件占用行的整个高度。与使用普通的WrapPanel面板进行布局时的过程相同。下面是完整的代码:
protected override Size ArrangeOverride(Size arrangeBounds){int firstInLine = 0;Size currentLineSize = new Size();double accumulatedHeight = 0;UIElementCollection elements = base.InternalChildren;for (int i = 0; i < elements.Count; i++){Size desiredSize = elements[i].DesiredSize;if (GetLineBreakBefore(elements[i]) || currentLineSize.Width + desiredSize.Width > arrangeBounds.Width) //need to switch to another line{arrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, i);accumulatedHeight += currentLineSize.Height;currentLineSize = desiredSize;if (desiredSize.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line{arrangeLine(accumulatedHeight, desiredSize.Height, i, ++i);accumulatedHeight += desiredSize.Height;currentLineSize = new Size();}firstInLine = i;}else //continue to accumulate a line{currentLineSize.Width += desiredSize.Width;currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);}}if (firstInLine < elements.Count)arrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, elements.Count);return arrangeBounds;}private void arrangeLine(double y, double lineHeight, int start, int end){double x = 0;UIElementCollection children = InternalChildren;for (int i = start; i < end; i++){UIElement child = children[i];child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineHeight));x += child.DesiredSize.Width;}}
WrapBreakPanel面板使用起来十分简便。下面的一些标记演示了使用WrapBreakPanel面板的一个示例。在该例中,WrapBreakPanel面板正确地分割行,并且根据其子元素的尺寸计算所需的尺寸:
<Window x:Class="CustomControlsClient.WrapBreakPanelTest"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls"Title="WrapBreakPanelTest" Height="300" Width="300"><StackPanel><StackPanel.Resources><Style TargetType="{x:Type Button}"><Setter Property="Margin" Value="3"></Setter><Setter Property="Padding" Value="5"/></Style></StackPanel.Resources><TextBlock Padding="5" Background="LightGray">Content above the WrapBreakPanel.</TextBlock><lib:WrapBreakPanel><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button lib:WrapBreakPanel.LineBreakBefore="True" FontWeight="Bold">Button with Break</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button><Button>No Break Here</Button></lib:WrapBreakPanel><TextBlock Padding="5" Background="LightGray">Content below the WrapBreakPanel.</TextBlock></StackPanel>
</Window>
下图显示了如何解释上面的标记:
往期精彩回顾
【.net core】电商平台升级之微服务架构应用实战
.Net Core微服务架构技术栈的那些事
Asp.Net Core 中IdentityServer4 授权中心之应用实战
Asp.Net Core 中IdentityServer4 授权中心之自定义授权模式
Asp.Net Core 中IdentityServer4 授权流程及刷新Token
Asp.Net Core 中IdentityServer4 实战之 Claim详解
Asp.Net Core 中IdentityServer4 实战之角色授权详解
Asp.Net Core 中间件应用实战中你不知道的那些事
Asp.Net Core Filter 深入浅出的那些事-AOP
Asp.Net Core EndPoint 终结点路由工作原理解读
ASP.NET CORE 内置的IOC解读及使用