WPF教程十三:自定義控件進階可視化狀態與自定義Panel


如果你敲了上一篇的代碼,經過上一篇各種問題的蹂躪,我相信自定義控件基礎部分其實已經了解的七七八八了。那么我們開始進階,現在這篇講的才是真正會用到的核心的東西。簡化你的代碼。給你提供更多的可能,掌握了這篇,才能發揮出來WPF的威力。這一篇學完我們的鳥槍就要換大炮了。

ColorPicker例子分離了行為和可視化外觀,其他人員可以動態改變外觀的模板。因為不涉及到狀態,所以來說相對簡單。現在我們來基於現在的內容深入一個難的。

首先創建通過自定義FlipContentPanel控件學習自定義控件下VisualStateManager的使用,一個同類型的數據,我們給2個展示狀態,一個是簡易的頁面、另外一個是詳細的頁面,我們通過一個狀態切換這2種不同UI的呈現效果,狀態切換時播放一個過渡動畫。這一篇主要是基於上一篇的ColorPicker學習到的自定義控件的知識結合visualStateManager、動畫來實現一個基於狀態切換的頁面。通過狀態來管理並控制頁面呈現的內容,這樣就可以通過狀態來實現不同的外觀,同時基於自定義控件可以很輕松的實現復雜的效果,並且代碼易於維護。

首先,我們創建一個繼承自Control的FlipContentPanel自定義無外觀控件類。該類包含2個狀態:Flipped和Normal。

我們將使用是否是Flipped來控制頁面切換2個不同的呈現內容。

重復使用propdp=>2次tab創建我們需要的3個依賴項屬性DetailsContent、OverviewContent、IsFlipped,代碼如下:

 public object DetailsContent
        {
            get { return (object)GetValue(DetailsContentProperty); }
            set { SetValue(DetailsContentProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DetailsContent.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DetailsContentProperty =
            DependencyProperty.Register("DetailsContent", typeof(object), typeof(FlipContentPanel));
        public object OverviewContent
        {
            get { return (object)GetValue(OverviewContentProperty); }
            set { SetValue(OverviewContentProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OverviewContent.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OverviewContentProperty =
            DependencyProperty.Register("OverviewContent", typeof(object), typeof(FlipContentPanel));

        public bool IsFlipped
        {
            get { return (bool)GetValue(IsFlippedProperty); }
            set
            {
                SetValue(IsFlippedProperty, value);
            }
        }

  // Using a DependencyProperty as the backing store for IsFlipped.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsFlippedProperty =
            DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipContentPanel));

在靜態構造函數中給FlipContentPanel.cs添加默認外觀。

 static FlipContentPanel()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(FlipContentPanel), new FrameworkPropertyMetadata(typeof(FlipContentPanel)));
        }

我們設計了2個頁面,一個DetailsContent頁面,一個OverviewContent頁面,我們在這2個頁面的默認樣式中各添加一個按鈕,用來控制切換VisualState。在上一篇講的OnApplyTemplate()中我們從模板中獲取控件,然后綁定觸發事件。這一篇我們結合模板中的按鈕在onApplyTemplate()的過程中查找元素並添加事件,用於控制切換狀態,這樣內置集成在自定義控件中的好處是如果有元素了就附加事件,如果沒有就不附加事件,兩個按鈕起名為FlipButton和FlipButtonAlternate。

 public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ToggleButton flipButton = base.GetTemplateChild("FlipButton") as ToggleButton;
            if (flipButton != null)
            {
                flipButton.Click += FlipButton_Click;
            }

            ToggleButton flipAlternateButton = base.GetTemplateChild("FlipButtonAlternate") as ToggleButton;
            if (flipAlternateButton != null)
            {
                flipAlternateButton.Click += FlipButton_Click;
            }
            ChangedVisualState(false);
        }

        private void FlipButton_Click(object sender, RoutedEventArgs e)
        {
            this.IsFlipped = !this.IsFlipped 
        }

		protected void ChangedVisualState(bool value)
        {
            if (IsFlipped)
            {
                VisualStateManager.GoToState(this, "Flipped", value);
            }
            else
            {
                VisualStateManager.GoToState(this, "Normal", value);
            }
        }

同時修改依賴項屬性IsFlipped的Set方法,當IsFlipped發生改變時,去修改VisualState。(PS:這里經過大佬們的提醒,在這種屬性值的Get、Set里盡量不要寫內容,容易養成習慣以后給自己留坑,SetValue代碼走不到,這部分代碼修改如下:)

   public bool IsFlipped
        {
            get { return (bool)GetValue(IsFlippedProperty); }
            set
            {
                SetValue(IsFlippedProperty, value); 
            }
        }
        // Using a DependencyProperty as the backing store for IsFlipped.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsFlippedProperty =
            DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipContentPanel),new PropertyMetadata(false,IsFlippedChanged));

        private static void IsFlippedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        { 
            if (d is FlipContentPanel flipContentPanel)
            {
                flipContentPanel.ChangedVisualState(true);
            }  
        } 

這樣加上2個狀態,2個按鈕在模板中我們就有4個對象。我們使用特性在類上方標明這4個對象。

 	[TemplateVisualState(Name = "Flipped",GroupName = "VisualState")]
    [TemplateVisualState(Name ="Normal",GroupName = "VisualState")]
    [TemplatePart(Name = "FlipButton", Type = typeof(ToggleButton))]
    [TemplatePart(Name = "FlipButtonAlternate", Type = typeof(ToggleButton))]
    public class FlipContentPanel : Control

這樣我們的FlipContentPanel無外觀自定義控件就寫完了。他包含了2個狀態,會從控件模板中獲取2個按鈕,並添加事件,用於切換頁面。在靜態構造函數中還會讀取默認外觀。

接下來我們去寫FlipContentPanel的默認外觀。

新建一個資源字典名字叫做FlipContentPanel:

在其中添加Style,TargetType為我們的FlipContentPanel並重寫template。

Template中的ContrlTemplate我們在一個Grid中寫2個同樣大小和位置的Border。這2個border中包含我們用來呈現2個不同頁面內容的ContentPresenter。然后通過切換2個Border的Visibility屬性來控制2個border的(相當於頁面的)顯示或隱藏的切換。

對應的ContentPresenter 綁定我們的依賴項屬性OverviewContent和DetailsContent。每個ContentPresenter上面還有一個固定位置的ToggleButton,用來切換當前的顯示內容,這2個ToggleButton的Name保持和無外觀控件中我們定義的FlipButton和FlipButtonAlternate一致,用於在OnApplyTemplate()階段能給這2個ToggleButton綁定事件,用於切換VisualState。同時我們在ControlTemplate中添加VisualState的管理器。用於切換狀態,其實VisualStateManager這里想使用的好也需要單獨講一下,我在這里就被坑了2天。想實現的功能特別牛逼,但是發現這里如果應用不好的話,問題會特別多,實現出來的效果跟預期不一樣。以后會單獨在Blend下講這個VisualStateManager。因為他有VisualStateGrounps的概念。可以很好的完成很多種狀態之前的切換。這里我們就寫一個最簡單的切換過程中漸顯和漸隱,整體代碼如下:

 <Style TargetType="{x:Type local:FlipContentPanel}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:FlipContentPanel}">
                    <Grid>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="VisualState">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition From="Normal" To="Flipped" GeneratedDuration="0:0:0.5">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DetailsContentBorder">
                                                <EasingDoubleKeyFrame KeyTime="0" Value="0"/>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualTransition>
                                    <VisualTransition From="Flipped" To="Normal" GeneratedDuration="0:0:0.5" >
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DetailsContentBorder">
                                                <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.5"  Value="0"/>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualTransition>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Flipped">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="OverviewContentBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Normal">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="DetailsContentBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups> 
                        <Border x:Name="OverviewContentBorder" 
                                Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:FlipContentPanel},Mode=FindAncestor},Path=ActualHeight}" 
                                Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:FlipContentPanel},Mode=FindAncestor},Path=ActualWidth}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Background="{TemplateBinding Background}">
                            <Grid>
                                <ContentPresenter Content="{TemplateBinding OverviewContent}"/>
                                <ToggleButton x:Name="FlipButton" Width="50" Height="50" VerticalAlignment="Bottom" HorizontalAlignment="Left"  Content="顯示詳情"/>
                            </Grid>
                        </Border>
                        <Border x:Name="DetailsContentBorder" BorderBrush="{TemplateBinding BorderBrush}" VerticalAlignment="Bottom" HorizontalAlignment="Left" 
                                Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:FlipContentPanel},Mode=FindAncestor},Path=ActualHeight}" 
                                Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:FlipContentPanel},Mode=FindAncestor},Path=ActualWidth}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Background="{TemplateBinding Background}">
                            <Grid>
                                <ContentPresenter Content="{TemplateBinding DetailsContent}"/>
                                <ToggleButton x:Name="FlipButtonAlternate" Width="50" Height="50" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="30" Content="收起"/>
                            </Grid>
                        </Border> 
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

注意上面的代碼中From="Normal" To="Flipped" 和From="Flipped" To="Normal"2個VisualTransition。它們是狀態切換時執行的。 是狀態切換后執行的。所以我設置了不同的動畫。這里一定要多練以下。這里整體就這么多,配個圖把,這里卡了我3個晚上。

XAML使用的代碼如下,然后配個圖:

<Window x:Class="CustomElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CustomElement"  
        xmlns:usercontrols="clr-namespace:CustomElement.UserControls"
        xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls"  
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
             <Grid Background="Firebrick">
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <lib:FlipContentPanel x:Name="flipPanel" Background="AntiqueWhite" Grid.Row="1" BorderBrush="DarkBlue" BorderThickness="3" IsFlipped="false">
            <lib:FlipContentPanel.DetailsContent>
                <Grid>
                    <TextBlock Background="Red" Text="我是詳情頁"/>
                </Grid>
            </lib:FlipContentPanel.DetailsContent>
            <lib:FlipContentPanel.OverviewContent>
                <Grid>
                    <TextBlock Background="Yellow" Text="我是列表頁"/>
                </Grid>
            </lib:FlipContentPanel.OverviewContent>
        </lib:FlipContentPanel> 
    </Grid> 
</Window>

進來下更進一步積累自定義控件的知識,學習自定義面板及構建自定義繪圖控件。

創建自定義面板是一種比較常見的自定義控件開發子集。面板駐留一個或多個子元素,並且實現了特定的布局邏輯以恰當地安排其子元素。如果希望構建自己的可拖動的工具欄或可停靠的窗口系統,自定義面板是很重要的元素。當創建需要非標准特定布局的組合控件時,自定義面板通常是很有用的,例如停靠工具欄。

面板在工作時,主要有2件事情:負責改變子元素尺寸和安排子元素的兩步布局過程。第一個階段是測量階段(measure pass),在這一階段面板決定其子元素希望具有多大的尺寸。第二個階段是排列階段(layout pass),在這一階段為每個控件指定邊界。這兩個步驟是必須的,因為在決定如何分割可用空間時,面板需要考慮所有子元素的期望。

可以通過重寫MeasureOverride()和ArrangeOverride()方法,為這兩個步驟添加自己的邏輯,這兩個方法作為WPF布局系統的一部分在FrameworkElement類種定義的。使用MeasureOverride()和ArrangeOverride()方法代替在UIElement類中定義的MeasureCore()和ArrangeCore()。這2個方法是不能被重寫的。

接下來我們分析一下這2個方法都在做什么:

1.MeasureOverride()

首先使用MeasureOverride()方法決定每個子元素希望多大的空間,每個MeasureOverride()方法的實現負責遍歷子元素集合,並調用每個子元素的Measure()方法。當調用Measure()方法時,需要提供邊界框-決定每個子控件最大可用控件的Size對象。在MeasureOverride()方法的最后,面板返回顯示所有子元素所需的空間,並返回它們所期望的尺寸。在測量過程的結尾,布局容器必須返回它所期望的尺寸。在簡單的面板中,可以通過組合每個資源需要的期望尺寸計算面板所期望的尺寸。

2.ArrangeOverride()方法

測量完所有元素后,就可以在可用的空間中排列元素了。布局系統調用面板的ArrangeOverride()方法,而面板為每個子元素調用Arrange()方法,以告訴子元素為它分配了多大的空間。

當時有Measure()方法測量條目時,傳遞能夠定義可用空間邊界的Size對象。當時有Arrange()方法放置條目時,傳遞能夠定義條目尺寸和位置的System.Windows.Rect對象。

了解了這兩步,我們來實現一個Canvas面板。

Canvas面板在它們希望的位置放置子元素,並且為子元素設置它們希望的尺寸。所以,Canvas面板不需要計算如何分割可用空間。MeasureOverride()階段可以為每個子元素提供無限的空間。

  protected override Size MeasureOverride(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。我們只用Left和Top附加依賴項屬性來實現一個簡易版。

public class CanvasClone : Panel
    {
        protected override Size MeasureOverride(Size availableSize)
        {
            Size size = new Size(double.PositiveInfinity, double.PositiveInfinity);
            foreach (UIElement element in base.InternalChildren)
            {
                element.Measure(size);
            }
            return new Size();
        }
        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (UIElement element in base.InternalChildren)
            {
                double x = 0;
                double y = 0;
                double left = Canvas.GetLeft(element);
                if (!double.IsNaN(left))
                {
                    x = left;
                }
                double top = Canvas.GetTop(element);
                if(!double.IsNaN(top))
                {
                    y = top;
                }
                element.Arrange(new Rect(new Point(x, y), element.DesiredSize)); 
            }
            return finalSize;
        }
    }

Xaml代碼:

<Window x:Class="CustomElement.CustomPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CustomElement"
        xmlns:customPanel="clr-namespace:CustomElement.Panels"
        mc:Ignorable="d"
        Title="CustomPanel" Height="450" Width="800">
    <Grid>
        <customPanel:CanvasClone>
            <TextBlock Text="asg" Canvas.Left="100" Canvas.Top="100"/>
        </customPanel:CanvasClone> 
    </Grid>
</Window>

我們就看到了TextBlock被放置在了靠近左上角100,100的位置。。這里不考慮其他問題,因為只是為了了解自定義面板。

接下來我們創建一個擴展的WrapPanel面板。WrapPanel的工作原理是,該面板逐個布置其子元素,一旦當前行寬度用完,就會切換到下一行。但有時我們需要強制立即換行,以便在新行中啟動某個特定控件。盡管WrapPanel面板原本沒有提供這一功能,但通過創建自定義控件可以方便地添加該功能。只需要添加一個請求換行的附加依賴項屬性即可。此后,面板中的子元素可使用該屬性在適當位置換行。

我們添加WrapBreakPanel類,繼承自Panel。這里因為要自定義所以不使用代碼片段添加附加依賴項屬性,而是手寫,並設置AffectsMeasure和AffectsArrange為True。我們要在每次LineBreakBefore屬性變更時,都觸發新的排列階段。在測量階段元素按行排列,除非太大或者LineBreakBefore屬性設置為true,否則每個元素都被添加到當前行中。

using System;
using System.Windows;
using System.Windows.Controls;

namespace CustomElement.Panels
{
    public class WrapBreakPanel : Panel
    {

        static WrapBreakPanel()
        {
            FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
            metadata.AffectsArrange = true;
            metadata.AffectsMeasure = true;
            LineBreakBeforeProperty =
             DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(WrapBreakPanel), metadata);
        }

        public static readonly DependencyProperty LineBreakBeforeProperty;

        public static void SetLineBreakBefore(UIElement element, Boolean value)
        {
            element.SetValue(LineBreakBeforeProperty, value);
        }

        public static Boolean GetLineBreakBefore(UIElement element)
        {
            return (bool)element.GetValue(LineBreakBeforeProperty);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            Size currentLineSize = new Size();
            Size panelSize = new Size();

            foreach (UIElement element in base.InternalChildren)
            {
                element.Measure(availableSize);
                Size desiredSize = element.DesiredSize;
                if (GetLineBreakBefore(element) || currentLineSize.Width + desiredSize.Width > availableSize.Width)
                {
                    //切換到新行,空間用完,或者通過設置附加依賴項屬性請求換行
                    panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);
                    panelSize.Height += currentLineSize.Height;
                    currentLineSize = desiredSize;
                    //如果元素太寬無法使用最大行寬進行匹配,則只需要為其指定單獨的行。
                    if (desiredSize.Width > availableSize.Width)
                    {
                        panelSize.Width = Math.Max(desiredSize.Width, panelSize.Width);
                        panelSize.Height += desiredSize.Height;
                        currentLineSize = new Size();
                    }
                }
                else
                {
                    //添加到當前行。
                    currentLineSize.Width += desiredSize.Width;
                    //確保線條與最高的元素一樣高
                    currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);
                }
            }
            //返回適合所有元素所需的大小。
            //通常,這是約束的寬度,高度基於元素的大小。
            //但是,如果一個元素的寬度大於面板的寬度。
            //所需的寬度將是該行的寬度。
            panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);
            panelSize.Height += currentLineSize.Height;
            return panelSize;
        }
        protected override Size ArrangeOverride(Size finalSize)
        {
            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 > finalSize.Width)
                {
                    //換行
                    arrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, i);
                    accumulatedHeight += currentLineSize.Height;
                    currentLineSize = desiredSize;
                    if (desiredSize.Width > finalSize.Width)
                    {
                        arrangeLine(accumulatedHeight, desiredSize.Height, i, ++i);
                        accumulatedHeight += desiredSize.Height;
                        currentLineSize = new Size();
                    }
                    firstInLine = i;
                }
                else
                {
                    //繼續當前前行。
                    currentLineSize.Width += desiredSize.Width;
                    currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);
                }
            }
            if (firstInLine < elements.Count)
            {
                arrangeLine(accumulatedHeight, currentLineSize.Height, firstInLine, elements.Count);
            }
            return finalSize;
        }
        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;
            }
        }
    }
}

調用的XAML代碼:

<Window x:Class="CustomElement.CustomPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CustomElement"
        xmlns:customPanel="clr-namespace:CustomElement.Panels"
        mc:Ignorable="d"
        Title="CustomPanel" Height="450" Width="800">
    <Grid>
        <customPanel:CanvasClone>
            <TextBlock Text="asg" Canvas.Left="100" Canvas.Top="100"/>
        </customPanel:CanvasClone>
        <customPanel:WrapBreakPanel>
            <Button>No Break Here</Button>
            <Button>No Break Here</Button>
            <Button>No Break Here</Button>
            <Button>No Break Here</Button>
            <Button customPanel:WrapBreakPanel.LineBreakBefore="True" FontWeight="Bold" Content="Button with Break"/>
            <Button>No Break Here</Button>
            <Button>No Break Here</Button>
            <Button>No Break Here</Button>
            <Button>No Break Here</Button>
        </customPanel:WrapBreakPanel>
    </Grid>
</Window>

在MeasureOverride階段,主要是測量元素的位置和是否需要換行。在ArrangeOverride階段,每計算滿顯示一行的元素后,就開始繪制這一行。這里只需要了解着這個MeasureOverride和ArrangeOverride在干什么就行。目前這里不需要掌握,因為后面會講到列表虛擬化和數據虛擬化,會更詳細的講相關的設計內容。這里只要直到,有自定義面板可以自己設計列表的呈現,就可以了。

我創建了一個C#相關的交流群。用於分享學習資料和討論問題,這個propuev也在群文件里。歡迎有興趣的小伙伴:QQ群:542633085


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM