完善和扩展标准控件的方法:

  • 样式:可使用样式方便地重用控件属性的集合,甚至可以使用触发器应用效果
  • 内容控件:所有继承自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按钮的外观和位置,并修改当在前后内容区域之间进行切换时应用的动画效果。

WPF进阶技巧和实战07--自定义元素01的更多相关文章

  1. WPF进阶技巧和实战07--自定义元素02

    在01节中,研究了如何开发自定义控件,下节开始考虑更特殊的选择:派生自定义面板以及构建自定义绘图 创建自定义面板 创建自定义面板是一种比较常见的自定义控件开发子集,面板可以驻留一个或多个子元素,并且实 ...

  2. WPF进阶技巧和实战03-控件(3-文本控件及列表控件)

    系列文章链接 WPF进阶技巧和实战01-小技巧 WPF进阶技巧和实战02-布局 WPF进阶技巧和实战03-控件(1-控件及内容控件) WPF进阶技巧和实战03-控件(2-特殊容器) WPF进阶技巧和实 ...

  3. WPF进阶技巧和实战03-控件(4-基于范围的控件及日期控件)

    系列文章链接 WPF进阶技巧和实战01-小技巧 WPF进阶技巧和实战02-布局 WPF进阶技巧和实战03-控件(1-控件及内容控件) WPF进阶技巧和实战03-控件(2-特殊容器) WPF进阶技巧和实 ...

  4. WPF进阶技巧和实战06-控件模板

    逻辑树和可视化树 System.Windows.LogicalTreeHelper System.Windows.Media.VisualTreeHelper 逻辑树类(LogicalTreeHelp ...

  5. WPF进阶技巧和实战08-依赖属性与绑定01

    依赖项属性 定义依赖项属性 注意:只能为依赖对象(继承自DependencyObject的类)添加依赖项属性.WPF中的元素基本上都继承自DependencyObject类. 静态字段 名称约定(属性 ...

  6. WPF进阶技巧和实战08-依赖属性与绑定02

    将元素绑定在一起 数据绑定最简单的形式是:源对象是WPF元素而且源属性是依赖项属性.依赖项属性内置了更改通知支持,当源对象中改变依赖项属性时,会立即更新目标对象的绑定属性. 元素绑定到元素也是经常使用 ...

  7. WPF进阶技巧和实战09-事件(1-路由事件、鼠标键盘输入)

    理解路由事件 当有意义的事情发生时,有对象(WPF的元素)发送的用于通知代码的消息,就是事件的核心思想.WPF通过事件路由的概念增强了.NET事件模型.事件由允许源自某个元素的事件由另一个元素引发.例 ...

  8. WPF进阶技巧和实战09-事件(2-多点触控)

    多点触控输入 多点触控输入和传统的基于比的输入的区别是多点触控识别手势,用户可以移动多根手指以执行常见的操作,放大,旋转,拖动等. 多点触控的输入层次 WPF允许使用键盘和鼠标的高层次输入(例如单击和 ...

  9. WPF进阶技巧和实战03-控件(5-列表、树、网格02)

    数据模板 样式提供了基本的格式化能力,但是不管如何修改ListBoxItem,他都不能够展示功能更强大的元素组合,因为了每个ListBoxItem只支持单个绑定字段(通过DisplayMemberPa ...

随机推荐

  1. 【springboot】自动装配原理

    摘自:https://mp.weixin.qq.com/s/ZxY_AiJ1m3z1kH6juh2XHw 前言 Spring翻译为中文是"春天",的确,在某段时间内,它给Java开 ...

  2. 三:ServletContext对象

    一.ServletContext对象 1.什么是ServletContext对象 ServletContext代表是一个web应用的环境(上下文)对象,ServletContext对象 内部封装是该w ...

  3. JDBC中的元数据——3.结果集元数据

    package metadata; import java.sql.Connection; import java.sql.ParameterMetaData; import java.sql.Pre ...

  4. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  5. SpringBoot博客开发之异常处理

    异常处理: 背景: 最近在搭建属于自己的个人博客(码农小白的执念),自己搭建后端的时候首先考虑的是异常处理.个人也是一边学习一边做,难免有疏漏的地方,希望朋友们在不对的地方提醒下. 技术栈: spri ...

  6. Servlet学习笔记(四)之请求转发与重定向(RequestDispatcher与sendRedirect)

    ServletContext可以实现请求转发(ServletContext请求转发相关内容见之前博客:http://blog.csdn.net/megustas_jjc/article/details ...

  7. Qt编译工程提示qt creator no rule to make target opencv2/core/hal/interface.h need by debug解决方法

    总是提示 qt creator no rule to make target opencv2/core/hal/interface.h need by debug解决方法: 也算是花了整整两个小时踩坑 ...

  8. maven下载出错

    求解

  9. Mybatis(四)——

    test https://www.cnblogs.com/chiaki/p/14529418.html

  10. Redis的持久化机制与内存管理机制

    1.概述 Redis的持久化机制有两种:RDB 和 AOF ,这两种机制有什么区别?正式环境应该采用哪种机制? 我们的服务器内存资源是有限的,如果内存被Redis的缓存占满了怎么办?这就要看Redis ...