主頁 > .NET開發 > WPF進階技巧和實戰07--自定義元素01

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

2021-08-31 16:09:28 .NET開發

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

  • 樣式:可使用樣式方便地重用控制元件屬性的集合,甚至可以使用觸發器應用效果
  • 內容控制元件:所有繼承自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:
    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="https://www.cnblogs.com/vigorous/p/{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="https://www.cnblogs.com/vigorous/p/{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="https://www.cnblogs.com/vigorous/p/{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:
    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="https://www.cnblogs.com/vigorous/p/{Binding Path=Red, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
                        <Slider x:Name="sliderGreen" Grid.Row="1" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" Value="https://www.cnblogs.com/vigorous/p/{Binding Path=Green, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
                        <Slider x:Name="sliderBlue" Grid.Row="2" Margin="{TemplateBinding Padding}" Maximum="255" Minimum="0" Value="https://www.cnblogs.com/vigorous/p/{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="https://www.cnblogs.com/vigorous/p/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按鈕的外觀和位置,并修改當在前后內容區域之間進行切換時應用的影片效果,

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/295836.html

標籤:WPF

上一篇:[WPF 學習] 19. 增量更新

下一篇:六、從GitHub瀏覽Prism示例代碼的方式入門WPF下的Prism之MVVM中的FilteringEvents

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more