【WPF學習】第六十七章 建立自定義面板

  前面兩個章節分別介紹了兩個自定義控件:自定義的ColorPicker和FlipPanel控件。接下來介紹派生自定義面板以及構建自定義繪圖控件。ide

  建立自定義面板是一種特殊但較常見的自定義控件開發子集。前面以及介紹過有關面板方面的知識,瞭解到面板駐留一個或多個子元素,而且實現了特定的佈局邏輯以恰當地安排子元素。若是但願構建本身的可拖動的工具欄或可停靠的窗口系統,自定義面板是很重要的元素。當建立須要非標準特定佈局的組合控件時,自定義面板一般頗有用的,例如停靠工具欄。函數

  接下里介紹一個基本的Canvas面板部分以及一個加強版本的WrapPanel面板兩個簡單的示例。工具

1、兩步佈局過程佈局

  每一個面板都使用相同的設備:負責改變子元素尺寸和安排子元素的兩步佈局過程。第一階段是測量階段(measure pass),在這一階段面板決定其子元素但願具備多大的尺寸。第二個階段是排列階段(layout pass),在這一階段爲每一個控件指定邊界。這兩個步驟是必需的,由於在決定如何分割可用空間時,面板須要考慮全部子元素的指望。ui

  能夠經過重寫名稱爲MeasureOverride()和ArrangeOverride()方法,爲這兩個步驟添加本身的邏輯,這兩個方法是做爲WPF佈局系統的一部分在FrameworkElement類中定義的。奇特的名稱使用標識MeasureOverride()和ArrangeOverride()方法代替在MeasureCore()和ArrangeCore()方法中定義的邏輯,後兩個方法在UIElement類中定義的。這兩個方法是不能被重寫的。this

  一、MeasureOverride()方法spa

  第一步是首先使用MeasureOverride()方法決定每一個子元素但願多大的空間。然而,即便是在MeasureOverride()方法中,也不能爲子元素提供無限空間,至少,也應當將自元素限制在可以適應面板可用空間的範圍以內。此外,可能但願更嚴格地限制子元素。例如,具備按比例分配尺寸的兩行的Grid面板,會爲子元素提供可用高度的通常。StackPanel面板會爲第一個元素提供全部可用空間,而後爲第二個元素提供剩餘的空間等等。3d

  每一個MeasureOverride()方法的實現負責遍歷子元素集合,並調用每一個子元素的Measure()方法。當調用Measure()方法時,須要提供邊界框——決定每一個子空間最大可用空間的Size對象。在MeasureOverride()方法的最後,面板返回顯示全部子元素所需的空間,並返回它們所指望的尺寸。code

  下面是MeasureOverride()方法的基本結構,其中沒有具體的尺寸細節:xml

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()方法時,將會獲得佈局容器及其全部子元素所須要的總尺寸。

  二、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對象。實際上,許多佈局容器就是採用這一步驟來佔據提供的全部空間。

2、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;
        }

3、更好的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;
        }
MeasureOverride

  上面代碼中的重要細節是檢查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;
            }
        }
ArrangeOverride

  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>

  下圖顯示瞭如何解釋上面的標記:

相關文章
相關標籤/搜索