1. 程式人生 > 其它 >WPF進階技巧和實戰07--自定義元素01

WPF進階技巧和實戰07--自定義元素01

完善和擴充套件標準控制元件的方法:

  • 樣式:可使用樣式方便地重用控制元件屬性的集合,甚至可以使用觸發器應用效果
  • 內容控制元件:所有繼承自ContentControl類的控制元件都支援巢狀的內容。使用內容控制元件,可以快速建立聚集其他元素的複合控制元件(按鈕變成影象按鈕,列表變成影象列表)
  • 控制元件模板:所有WPF控制元件都是無外觀的,這意味著他們具有硬編碼的功能,但是他們的外觀是通過控制元件模板單獨定義的。使用新的控制元件模板替代預設模板,可重新構建基本控制元件
  • 資料模板:所有派生自ItemsControl的類都支援資料模板,通過資料模板可建立某些資料物件的富列表顯示。通過恰當的資料模板,可使用許多元素組合顯示每個項,這些組合可以是文字,影象甚至是可編輯控制元件。

理解自定義元素

建立自定義元素需要繼承的基類:

名稱 說明
FrameworkElement 最低階的基類。只有當希望重寫OnRender()方法並使用DrawContext從頭繪製內容時,才使用此方法
Control 當從頭開始建立控制元件時,這是最常用的起點。該類是所有使用者互動小元件的基類。Control類添加了用於設定背景、前景、字型和內容對齊方式等屬性。控制元件類自身設定了Tab順序,引入了滑鼠雙擊功能(MouseDoubleClick和PreviewMouseDoubleClick事件),最重要的是定義了Template屬性,為了無限靈活性,該屬性允許使用自定義元素樹替換其外觀
ContentControl 這是能夠顯示任意單一內容控制元件的基類。顯示的內容可以是元素或者結合使用模板的自定義物件(內容通過Content屬性設定,並且可以通過ContentTemplate屬性提供可選的模板)。
UserControl 這是可以用檢視配置的內容控制元件。儘管使用者控制元件和普通的內容控制元件是不同的,但是當希望對個視窗中快速重用使用者介面中的不變模組時(而不是建立真正的能在不同應用程式之間轉移的獨立控制元件),通常使用該基類
ItemsControl或Selector 是封裝列表類控制元件的基類,但是不支援選擇。而Selector類是支援選擇的控制元件更具體的基類。建立自定義控制元件不經常使用這些類,因為ListBox、ListView、TreeView控制元件的資料繫結特性提供了更大的靈活性
Panel 具有佈局邏輯控制元件的基類。佈局控制元件可以包含多個子元素,並根據特定的佈局語義安排這些子元素。通常,面板提供了用於設定子元素的附加屬性,配置如何安排子元素。
Decorator 封裝其他元素的元素的基類,並且提供了一種影象效果或特定的功能。兩個例子:Border、Viewbox。Border在元素周圍繪製線條,Viewbox控制元件使用動態縮放其內容。
特殊控制元件類 如果希望改進現有控制元件,可以直接繼承該控制元件。比如:可以建立基友內建驗證邏輯的TextBox控制元件。

建立基本的使用者控制元件

建立自定義顏色拾取器進行演示建立控制元件的各種重要概念。

定義依賴項屬性

新增自定義使用者控制元件之後,就是設計使用者控制元件對外界公開的公共介面。也就是說,設計控制元件的使用者使用的與顏色拾取器進行互動的屬性,方法和事件。

/// <summary>
  /// ColorPicker.xaml 的互動邏輯
  /// </summary>
  public partial class ColorPicker : UserControl
  {

    public Color Color { get => (Color)GetValue(ColorProperty); set => SetValue(ColorProperty, value); }
    public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Color), typeof(ColorPicker), new FrameworkPropertyMetadata(Colors.Black, propertyChangedCallback: ColorChangedCallback));
    private static void ColorChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      var colorPicker = (ColorPicker)d;
      var colorNew = (Color)e.NewValue;
     
      colorPicker.Red = colorNew.R;
      colorPicker.Green = colorNew.G;
      colorPicker.Blue = colorNew.B;
    }

    public byte Red { get => (byte)GetValue(RedProperty); set => SetValue(RedProperty, value); }
    public static readonly DependencyProperty RedProperty = DependencyProperty.Register("Red", typeof(byte), typeof(ColorPicker), new FrameworkPropertyMetadata(0, propertyChangedCallback: ColorRGBChangedCallback));

    public byte Green { get => (byte)GetValue(GreenProperty); set => SetValue(GreenProperty, value); }
    public static readonly DependencyProperty GreenProperty = DependencyProperty.Register("Green", typeof(byte), typeof(ColorPicker), new FrameworkPropertyMetadata(0, propertyChangedCallback: ColorRGBChangedCallback));

    public byte Blue { get => (byte)GetValue(BlueProperty); set => SetValue(BlueProperty, value); }
    public static readonly DependencyProperty BlueProperty = DependencyProperty.Register("Blue", typeof(byte), typeof(ColorPicker), new FrameworkPropertyMetadata(0, propertyChangedCallback: ColorRGBChangedCallback));
    private static void ColorRGBChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      var colorPicker = (ColorPicker)d;
      var color = colorPicker.Color;

      if (e.Property == RedProperty)
      {
        color.R = (byte)e.NewValue;
      }
      if (e.Property == GreenProperty)
      {
        color.G = (byte)e.NewValue;
      }
      if (e.Property == BlueProperty)
      {
        color.B = (byte)e.NewValue;
      }

      colorPicker.Color = color;
    }



    public ColorPicker()
    {
      InitializeComponent();
    }
  }

屬性變化回撥函式負責使Color屬性與Red,Green,Blue屬性保持一致。無論何時改變Red,Green,Blue屬性時,都會調整Color屬性。當設定Color屬性時,也會更新Red,Green,Blue的值。上述程式碼不會引起一系列無休止的呼叫。WPF不允許重新進入屬性變化回撥函式。

定義路由事件

無論何時修改Color屬性,不管是直接修改還是通過修改Red、Green、Blue成分,都會觸發ColorChangedCallback事件,進而觸發ColorChangedEvent路由事件。

//////////////////路由事件/////////////////
public static readonly RoutedEvent ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPicker));
public event RoutedPropertyChangedEventHandler<Color> ColorChanged
{
  add { AddHandler(ColorChangedEvent, value); }
  remove { RemoveHandler(ColorChangedEvent, value); }
}
private static void ColorChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var colorPicker = (ColorPicker)d;
  var colorNew = (Color)e.NewValue;

  colorPicker.Red = colorNew.R;
  colorPicker.Green = colorNew.G;
  colorPicker.Blue = colorNew.B;

  //觸發路由事件
  RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>((Color)e.OldValue, colorNew);
  args.RoutedEvent = ColorPicker.ColorChangedEvent;
  colorPicker.RaiseEvent(args);
}

新增標記

<UserControl
  x:Class="Course05.ColorPicker"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Padding="5">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Slider x:Name="sliderRed" Grid.Row="0" Margin="{Binding Path=Padding, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" Maximum="255" Minimum="0" Value="{Binding Path=Red, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" />
    <Slider x:Name="sliderGreen" Grid.Row="1" Margin="{Binding Path=Padding, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" Maximum="255" Minimum="0" Value="{Binding Path=Green, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" />
    <Slider x:Name="sliderBlue" Grid.Row="2" Margin="{Binding Path=Padding, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" Maximum="255" Minimum="0" Value="{Binding Path=Blue, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" />
    <Rectangle Grid.RowSpan="3" Grid.Column="1" Width="50" Stroke="Black" StrokeThickness="1">
      <Rectangle.Fill>
        <SolidColorBrush Color="{Binding Path=Color, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}, AncestorLevel=1}}" />
      </Rectangle.Fill>
    </Rectangle>
  </Grid>
</UserControl>

使用控制元件

<Window
  x:Class="Course05.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:local="clr-namespace:Course05"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  Title="MainWindow"
  Width="800"
  Height="450"
  mc:Ignorable="d">
  <Grid>
    <local:ColorPicker Width="500" Height="Auto" VerticalAlignment="Top" ColorChanged="ColorPicker_ColorChanged" Color="BurlyWood" />
    <TextBlock x:Name="txtColor" Margin="0,200,0,0" HorizontalAlignment="Center" VerticalAlignment="Top" Text="TextBlock" TextWrapping="Wrap" />
  </Grid>
</Window>
private void ColorPicker_ColorChanged(object sender, RoutedPropertyChangedEventArgs<Color> e)
{
  if (e != null && this.txtColor != null)
    txtColor.Text = e.NewValue.ToString();
}

命令支援

通過下面兩種方法為自定義控制元件新增命令支援:

  • 新增將控制元件連結到特定命令的命令繫結。通過這種方法,控制元件可以相應命令,而且不需要藉助任何外部程式碼。
  • 為命令建立新的RoutedUICommand物件,作為自定義控制元件的靜態欄位。然後為這個命令物件新增繫結。這種方法可使自定義控制元件支援沒有在基本命令集合中定義命令。
public ColorPicker()
{
  InitializeComponent();

  SetupCommands();
}

private Color? previousColor;

private void SetupCommands()
{
  CommandBinding binding = new CommandBinding(ApplicationCommands.Undo, UndoCommandExecuted, UndoCommandCanExecuted);
  this.CommandBindings.Add(binding);
}

private void UndoCommandCanExecuted(object sender, CanExecuteRoutedEventArgs e)
{
  e.CanExecute = previousColor.HasValue;
}

private void UndoCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
  this.Color = this.previousColor.Value;
}

更可靠的命令,使用CommandManager.RegisterClassCommandBinding方法關聯靜態的命令處理程式。

CommandManager.RegisterClassCommandBinding(typeof(ColorPicker), new CommandBinding(ApplicationCommands.Undo, UndoCommandExecuted, UndoCommandCanExecuted));

private static void UndoCommandStatisCanExecuted(object sender, CanExecuteRoutedEventArgs e)
{
  var colorPicker = (ColorPicker)sender;
  e.CanExecute = colorPicker.previousColor.HasValue;
}

private static void UndoCommandStatisExecuted(object sender, ExecutedRoutedEventArgs e)
{
  var colorPicker = (ColorPicker)sender;
  colorPicker.Color = colorPicker.previousColor.Value;
}

深入分析使用者控制元件

在後臺,UserControl類的工作方式和父類ContentControl非常相似,只有下面幾個區別:

  • UserControl改變了一些預設值。將IsTabStop和Focusable屬性設定為false,並將水平垂直對齊設定成Stretch,從而填充整個空間。
  • UserControl類應用了一個新的控制元件模板,該模板由包含ContentPresenter元素的Border元素組成。
  • UserControl類改變了路由事件的源。當事件從使用者控制元件內的控制元件向以外的元素冒泡或者隧道路由時,事件源變為指向使用者控制元件而不是原始元素。

從技術角度看,可改變使用者控制元件的模板。實際上,只需要進行很少的調整,就可以將所有模板移到模板中。

建立無外觀控制元件

建立無外觀控制元件需要繼承自控制元件基類,但是沒有設計表面的控制元件。相反,這個控制元件將其標記到預設模板中,可替換模板而不會影響控制元件邏輯。

修改顏色提取器的程式碼

public class ColorPicker2 : System.Windows.Controls.Control  {    //////////////////依賴項屬性/////////////////    public Color Color { get => (Color)GetValue(ColorProperty); set => SetValue(ColorProperty, value); }    public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Color), typeof(ColorPicker2), new FrameworkPropertyMetadata(Colors.Black, propertyChangedCallback: ColorChangedCallback));    private static void ColorChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)    {      var colorPicker = (ColorPicker2)d;      var colorNew = (Color)e.NewValue;      colorPicker.previousColor = (Color)e.OldValue;      colorPicker.Red = colorNew.R;      colorPicker.Green = colorNew.G;      colorPicker.Blue = colorNew.B;      //觸發路由事件      RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>((Color)e.OldValue, colorNew);      args.RoutedEvent = ColorPicker2.ColorChangedEvent;      colorPicker.RaiseEvent(args);    }    public byte Red { get => (byte)GetValue(RedProperty); set => SetValue(RedProperty, value); }    public static readonly DependencyProperty RedProperty = DependencyProperty.Register("Red", typeof(byte), typeof(ColorPicker2), new FrameworkPropertyMetadata((byte)0, propertyChangedCallback: ColorRGBChangedCallback));    public byte Green { get => (byte)GetValue(GreenProperty); set => SetValue(GreenProperty, value); }    public static readonly DependencyProperty GreenProperty = DependencyProperty.Register("Green", typeof(byte), typeof(ColorPicker2), new FrameworkPropertyMetadata((byte)0, propertyChangedCallback: ColorRGBChangedCallback));    public byte Blue { get => (byte)GetValue(BlueProperty); set => SetValue(BlueProperty, value); }    public static readonly DependencyProperty BlueProperty = DependencyProperty.Register("Blue", typeof(byte), typeof(ColorPicker2), new FrameworkPropertyMetadata((byte)0, propertyChangedCallback: ColorRGBChangedCallback));    private static void ColorRGBChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)    {      var colorPicker = (ColorPicker2)d;      var color = colorPicker.Color;      if (e.Property == RedProperty)      {        color.R = (byte)e.NewValue;      }      if (e.Property == GreenProperty)      {        color.G = (byte)e.NewValue;      }      if (e.Property == BlueProperty)      {        color.B = (byte)e.NewValue;      }      colorPicker.Color = color;    }    //////////////////路由事件/////////////////    public static readonly RoutedEvent ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPicker2));    public event RoutedPropertyChangedEventHandler<Color> ColorChanged    {      add { AddHandler(ColorChangedEvent, value); }      remove { RemoveHandler(ColorChangedEvent, value); }    }    public ColorPicker2()    {      SetupCommands();      DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker2), new FrameworkPropertyMetadata(typeof(ColorPicker2)));    }    private Color? previousColor;    private void SetupCommands()    {      CommandManager.RegisterClassCommandBinding(typeof(ColorPicker2), new CommandBinding(ApplicationCommands.Undo, UndoCommandStatisExecuted, UndoCommandStatisCanExecuted));    }    private static void UndoCommandStatisCanExecuted(object sender, CanExecuteRoutedEventArgs e)    {      var colorPicker = (ColorPicker2)sender;      e.CanExecute = colorPicker.previousColor.HasValue;    }    private static void UndoCommandStatisExecuted(object sender, ExecutedRoutedEventArgs e)    {      var colorPicker = (ColorPicker2)sender;      colorPicker.Color = colorPicker.previousColor.Value;    }  }

修改了繼承類,去掉了建構函式的 InitializeComponent();方法,增加了通知WPF為控制元件提供新的樣式。

修改顏色提取器的標記

增加顏色提取器的樣式:

<Style TargetType="{x:Type local:ColorPicker2}">    <Setter Property="Template">      <Setter.Value>        <ControlTemplate TargetType="{x:Type local:ColorPicker2}">          <Grid>            <Grid.ColumnDefinitions>              <ColumnDefinition Width="*" />              <ColumnDefinition Width="Auto" />            </Grid.ColumnDefinitions>            <Grid.RowDefinitions>              <RowDefinition Height="Auto" />              <RowDefinition Height="Auto" />              <RowDefinition Height="Auto" />            </Grid.RowDefinitions>            <Slider x:Name="sliderRed" Grid.Row="0" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" Value="{Binding Path=Red, RelativeSource={RelativeSource Mode=TemplatedParent}}" />            <Slider x:Name="sliderGreen" Grid.Row="1" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" Value="{Binding Path=Green, RelativeSource={RelativeSource Mode=TemplatedParent}}" />            <Slider x:Name="sliderBlue" Grid.Row="2" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" Value="{Binding Path=Blue, RelativeSource={RelativeSource Mode=TemplatedParent}}" />            <Rectangle Grid.RowSpan="3" Grid.Column="1" Width="50" Stroke="Black" StrokeThickness="1">              <Rectangle.Fill>                <SolidColorBrush Color="{Binding Path=Color, RelativeSource={RelativeSource Mode=TemplatedParent}}" />              </Rectangle.Fill>            </Rectangle>          </Grid>        </ControlTemplate>      </Setter.Value>    </Setter>         </Style>

注意此處繫結的擴招,一部分使用TemplateBinding,一部分使用Binding(將RelativeSource設定為指向模板的父元素,也就是自定義控制元件),這兩種形式原理基本一致。但是如果需要雙向繫結或者繫結到繼承自Freezable類的屬性時(SolidColorBrush),模板繫結就失效了。

精簡控制元件模板

  1. 新增部件名稱
<Style TargetType="{x:Type local:ColorPicker3}">    <Setter Property="Template">      <Setter.Value>        <ControlTemplate TargetType="{x:Type local:ColorPicker3}">          <Grid>            <Grid.ColumnDefinitions>              <ColumnDefinition Width="*" />              <ColumnDefinition Width="Auto" />            </Grid.ColumnDefinitions>            <Grid.RowDefinitions>              <RowDefinition Height="Auto" />              <RowDefinition Height="Auto" />              <RowDefinition Height="Auto" />            </Grid.RowDefinitions>            <Slider x:Name="PART_RedSlider" Grid.Row="0" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" />            <Slider x:Name="PART_GreenSlider" Grid.Row="1" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" />            <Slider x:Name="PART_BlueSlider" Grid.Row="2" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" />            <Rectangle Grid.RowSpan="3" Grid.Column="1" Width="50" Stroke="Black" StrokeThickness="1">              <Rectangle.Fill>                <SolidColorBrush x:Name="PART_PreviewBrush" />              </Rectangle.Fill>            </Rectangle>          </Grid>        </ControlTemplate>      </Setter.Value>    </Setter>  </Style>

刪除繫結,賦予名稱,這些名稱根據約定,都以PART_開頭。

  1. 操作模板控制元件

重寫方法OnApplyTemplate:

public override void OnApplyTemplate()    {      base.OnApplyTemplate();      {        RangeBase silder = GetTemplateChild("PART_RedSlider") as RangeBase;        if (silder != null)        {          Binding binding = new Binding("Red");          binding.Source = this;          binding.Mode = BindingMode.TwoWay;          silder.SetBinding(RangeBase.ValueProperty, binding);        }      }      {        RangeBase silder = GetTemplateChild("PART_GreenSlider") as RangeBase;        if (silder != null)        {          Binding binding = new Binding("Green");          binding.Source = this;          binding.Mode = BindingMode.TwoWay;          silder.SetBinding(RangeBase.ValueProperty, binding);        }      }      {        RangeBase silder = GetTemplateChild("PART_BlueSlider") as RangeBase;        if (silder != null)        {          Binding binding = new Binding("Blue");          binding.Source = this;          binding.Mode = BindingMode.TwoWay;          silder.SetBinding(RangeBase.ValueProperty, binding);        }      }      {        SolidColorBrush brush = GetTemplateChild("PART_PreviewBrush") as SolidColorBrush;        if (brush != null)        {          Binding binding = new Binding("Color");          binding.Source = brush;          binding.Mode = BindingMode.OneWayToSource;          this.SetBinding(ColorPicker3.ColorProperty, binding);        }      }    }
  1. 記錄模板部件
[TemplatePart(Name = "PART_RedSlider", Type =typeof(RangeBase))][TemplatePart(Name = "PART_GreenSlider", Type = typeof(RangeBase))][TemplatePart(Name = "PART_BlueSlider", Type = typeof(RangeBase))][TemplatePart(Name = "PART_PreviewBrush", Type = typeof(SolidColorBrush))]public class ColorPicker3 : System.Windows.Controls.Control

支援視覺化狀態

上面例子中的ColorPicker控制元件設計相對簡單,是因為它不涉及狀態(不具有焦點,滑鼠是否在上面懸停,是否禁用狀態來區分其視覺化外觀)。

下面的例子中FlipPanel,通過翻轉效果來切換兩種表面。可通過程式碼執行翻轉(通過設定名為IsFlipped的屬性),也可以使用一個便捷的按鈕來翻轉面板(除非控制元件使用者從模板中移除了此按鈕)。控制元件模板需要制定兩個獨立部分:FlipPanel控制元件的前後內容區域。需要一個方法在兩個狀態之間切換:翻轉狀態和不翻轉狀態,可通過模板新增觸發器來完成該工作。

  1. 開始編寫FlipPanel類
public class FlipPanel : Control{  public Object FrontContent { get => (Object)GetValue(FrontContentProperty); set => SetValue(FrontContentProperty, value); }  public static readonly DependencyProperty FrontContentProperty = DependencyProperty.Register("FrontContent", typeof(Object), typeof(FlipPanel), new FrameworkPropertyMetadata(null));  public Object BackContent { get => (Object)GetValue(BackContentProperty); set => SetValue(BackContentProperty, value); }  public static readonly DependencyProperty BackContentProperty = DependencyProperty.Register("BackContent", typeof(Object), typeof(FlipPanel), new FrameworkPropertyMetadata(null));  public bool IsFlipped { get => (bool)GetValue(IsFlippedProperty); set { SetValue(IsFlippedProperty, value); ChangeVisualStatus(true); } }  public static readonly DependencyProperty IsFlippedProperty = DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipPanel), new FrameworkPropertyMetadata(false));  private void ChangeVisualStatus(bool isFlipped)  {  }  public CornerRadius CornerRadius { get => (CornerRadius)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); }  public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(FlipPanel), null);  public FlipPanel()  {    DefaultStyleKeyProperty.OverrideMetadata(typeof(FlipPanel), new FrameworkPropertyMetadata(typeof(FlipPanel)));  }}

預設樣式的輪廓:

<Style TargetType="{x:Type local:FlipPanel}">  <Setter Property="Template">    <Setter.Value>      <ControlTemplate TargetType="{x:Type local:FlipPanel}">        <Grid>                 </Grid>      </ControlTemplate>    </Setter.Value>  </Setter></Style>
  1. 選擇部件和狀態

FlipPanel需要兩個狀態:

  • 正常狀態:只有前面的內容可見,後面的內容被翻轉、淡化或者被移出檢視
  • 翻轉狀態:只有後面的內容可見,前面的內容被動畫移出檢視

需要兩個部件:

FlipButton:單擊按鈕時,檢視從前面改到後面(或者從後面改到前面),FlipPanel通過處理按鈕事件來提供該服務

FlipPanelAlternate:這是一個可選元素,與FlipButton的工作方式相同。允許控制元件使用者在自定義模板中使用兩種不同的方法。一種選擇時使用可翻轉區域外的單個翻轉按鈕,另一種是選擇在可翻轉的兩側放置獨立的翻轉按鈕。

[TemplateVisualState(Name = "Normal", GroupName = "ViewStatus")][TemplateVisualState(Name = "Flipped", GroupName = "ViewStatus")][TemplatePart(Name = "FlipButton", Type = typeof(ToggleButton))][TemplatePart(Name = "FlipButtonAlternate", Type = typeof(ToggleButton))]public class FlipPanel : Control
  1. 預設控制元件模板
<Style TargetType="{x:Type local:FlipPanel}">    <Setter Property="Template">      <Setter.Value>        <ControlTemplate TargetType="{x:Type local:FlipPanel}">          <Grid>            <Grid.RowDefinitions>              <RowDefinition Height="Auto" />              <RowDefinition Height="Auto" />            </Grid.RowDefinitions>            <Border x:Name="FrontContent" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">              <ContentPresenter Content="{TemplateBinding FrontContent}" />            </Border>            <Border x:Name="BackContent" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">              <ContentPresenter Content="{TemplateBinding BackContent}" />            </Border>            <ToggleButton x:Name="FlipButton" Grid.Row="1" Width="20" Height="20" Margin="0,10,0,0" RenderTransformOrigin="0.5,0.5">              <ToggleButton.Template>                <ControlTemplate>                  <Grid>                    <Ellipse Fill="AliceBlue" Stroke="#FFA9A9A9" />                    <Path HorizontalAlignment="Center" VerticalAlignment="Center" Data="M1,1.5L4.5,5 8,1.5" Stroke="#FF666666" StrokeThickness="2" />                  </Grid>                </ControlTemplate>              </ToggleButton.Template>              <ToggleButton.RenderTransform>                <RotateTransform x:Name="FlipButtonTransform" Angle="-90" />              </ToggleButton.RenderTransform>            </ToggleButton>            <VisualStateManager.VisualStateGroups>              <VisualStateGroup x:Name="ViewStatus">                <VisualStateGroup.Transitions>                  <VisualTransition GeneratedDuration="0:0:0.7" To="Flipped">                    <Storyboard>                      <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="90" Duration="0:0:0.2" />                    </Storyboard>                  </VisualTransition>                  <VisualTransition GeneratedDuration="0:0:0.7" To="Normal">                    <Storyboard>                      <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="-90" Duration="0:0:0.2" />                    </Storyboard>                  </VisualTransition>                </VisualStateGroup.Transitions>                <VisualState x:Name="Normal">                  <Storyboard>                    <DoubleAnimation Storyboard.TargetName="BackContent" Storyboard.TargetProperty="Opacity" To="0" Duration="0" />                    <DoubleAnimation Storyboard.TargetName="FrontContent" Storyboard.TargetProperty="Opacity" To="1" Duration="0" />                  </Storyboard>                </VisualState>                <VisualState x:Name="Flipped">                  <Storyboard>                    <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="90" Duration="0" />                    <DoubleAnimation Storyboard.TargetName="FrontContent" Storyboard.TargetProperty="Opacity" To="0" Duration="0" />                    <DoubleAnimation Storyboard.TargetName="BackContent" Storyboard.TargetProperty="Opacity" To="1" Duration="0" />                  </Storyboard>                </VisualState>              </VisualStateGroup>            </VisualStateManager.VisualStateGroups>          </Grid>        </ControlTemplate>      </Setter.Value>    </Setter>  </Style>
  1. 使用FlipPanel控制元件
<Grid >  <local:FlipPanel x:Name="panel" HorizontalAlignment="Left" Margin="128,33,0,0" VerticalAlignment="Top" Height="216" Width="536">    <local:FlipPanel.FrontContent>      <StackPanel>        <TextBlock Text="FrontContext"/>      </StackPanel>    </local:FlipPanel.FrontContent>    <local:FlipPanel.BackContent>      <StackPanel>        <TextBlock Text="BackContent"/>        <Button Content="123" Width="100" Height="20" Click="Button_Click"/>      </StackPanel>    </local:FlipPanel.BackContent>  </local:FlipPanel></Grid>private void Button_Click(object sender, RoutedEventArgs e){  panel.IsFlipped = !panel.IsFlipped;}
  1. 使用不同的控制元件模板

已經設計好的自定義控制元件極其靈活,可以使用新模板來修改ToggleButton按鈕的外觀和位置,並修改當在前後內容區域之間進行切換時應用的動畫效果。