【WPF学习】第六十七章 创建自定义面板
前面两个章节分别介绍了两个自定义控件:自定义的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 children foreach (uielement element in base.internalchildren) { //ask each child how much space it would like,given the //availablesize constraint size 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>
下图显示了如何解释上面的标记: