前面两个章节分别介绍了两个自定义控件:自定义的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>

  下图显示了如何解释上面的标记: