WPF 进阶 自定应元素(构建基本用户控件,创建无外观控件,支持可视化状态)
4,5,6集2h
1.构建基本的用户控件
新建一个类库程序集用于存放控件资源
为自定义控件库项目添加用户控件。
using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace CustomControls { /// <summary> /// Interaction logic for ColorPickerUserControl.xaml /// </summary> public partial class ColorPickerUserControl : System.Windows.Controls.UserControl { public ColorPickerUserControl() { InitializeComponent(); SetUpCommands(); } private void SetUpCommands() { // Set up command bindings. //CommandBinding binding = new CommandBinding(ApplicationCommands.Undo, // UndoCommand_Executed, UndoCommand_CanExecute); // this.CommandBindings.Add(binding); } private Color? previousColor; private static void UndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender; e.CanExecute = colorPicker.previousColor.HasValue; } private static void UndoCommand_Executed(object sender, ExecutedRoutedEventArgs e) { // Use simple reverse-or-redo Undo (like Notepad). ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender; colorPicker.Color = (Color)colorPicker.previousColor; } static ColorPickerUserControl() { ColorProperty = DependencyProperty.Register("Color", typeof(Color), typeof(ColorPickerUserControl), new FrameworkPropertyMetadata(Colors.Black, new PropertyChangedCallback(OnColorChanged))); RedProperty = DependencyProperty.Register("Red", typeof(byte), typeof(ColorPickerUserControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged))); GreenProperty = DependencyProperty.Register("Green", typeof(byte), typeof(ColorPickerUserControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged))); BlueProperty = DependencyProperty.Register("Blue", typeof(byte), typeof(ColorPickerUserControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged))); CommandManager.RegisterClassCommandBinding(typeof(ColorPickerUserControl), new CommandBinding(ApplicationCommands.Undo, UndoCommand_Executed, UndoCommand_CanExecute)); } //第一步定义静态字段 //为每个颜色成分定义单独的依赖项属性 public static DependencyProperty RedProperty; public static DependencyProperty GreenProperty; public static DependencyProperty BlueProperty; public static DependencyProperty ColorProperty; //添加标准的属性封装器 public Color Color { get { return (Color)GetValue(ColorProperty); } set { SetValue(ColorProperty, value); } } public byte Red { get { return (byte)GetValue(RedProperty); } set { SetValue(RedProperty, value); } } public byte Green { get { return (byte)GetValue(GreenProperty); } set { SetValue(GreenProperty, value); } } public byte Blue { get { return (byte)GetValue(BlueProperty); } set { SetValue(BlueProperty, value); } } //Color属性更新时,也更新Red Green Blue private static void OnColorChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender; Color oldColor = (Color)e.OldValue; Color newColor = (Color)e.NewValue; colorPicker.Red = newColor.R; colorPicker.Green = newColor.G; colorPicker.Blue = newColor.B; colorPicker.previousColor = oldColor; colorPicker.OnColorChanged(oldColor, newColor); } //属性回调函数复制使Color属性与Red Green Blue保持一致 private static void OnColorRGBChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender; Color color = colorPicker.Color; if (e.Property == RedProperty) color.R = (byte)e.NewValue; else if (e.Property == GreenProperty) color.G = (byte)e.NewValue; else 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(ColorPickerUserControl)); //创建标准的事件封装器来公开事件,事件封装器用于关联和删除事件监听程序 public event RoutedPropertyChangedEventHandler<Color> ColorChanged { add { AddHandler(ColorChangedEvent, value); } remove { RemoveHandler(ColorChangedEvent, value); } } //下面是在适当时候引发事件的代码,代码必须继承自RaiseEvent方法 private void OnColorChanged(Color oldValue, Color newValue) { RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>(oldValue, newValue); args.RoutedEvent = ColorPickerUserControl.ColorChangedEvent; RaiseEvent(args); } } }
创建控件外观的标记
使用一个Grid控件将3个Slider控件个预览颜色的Rectangle元素组合在一起
拾取器使用4个数据绑定表达式
<UserControl x:Class="CustomControls.ColorPickerUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name="colorPicker"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <Slider Name="sliderRed" Minimum="0" Maximum="255" Margin="{Binding ElementName=colorPicker,Path=Padding}" Value="{Binding ElementName=colorPicker,Path=Red}"></Slider> <Slider Grid.Row="1" Name="sliderGreen" Minimum="0" Maximum="255" Margin="{Binding ElementName=colorPicker,Path=Padding}" Value="{Binding ElementName=colorPicker,Path=Green}"></Slider> <Slider Grid.Row="2" Name="sliderBlue" Minimum="0" Maximum="255" Margin="{Binding ElementName=colorPicker,Path=Padding}" Value="{Binding ElementName=colorPicker,Path=Blue}"></Slider> <Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{Binding ElementName=colorPicker,Path=Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush Color="{Binding ElementName=colorPicker,Path=Color}"></SolidColorBrush> </Rectangle.Fill> </Rectangle> </Grid> </UserControl>
在另一个窗口中使用颜色拾取器
需要将名称空间和程序集映射过来
xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls"
使用定义的XML名称空间和用户空间类名
<lib:ColorPickerUserControl
Name="colorPicker" Margin="2" Padding="3" ColorChanged="colorPicker_ColorChanged" Color="Yellow"></lib:ColorPickerUserControl>
运行效果

2.创建无外观控件
修改拾取器的标记
添加文件夹Themes
添加generic.xaml
generic.xaml通常使用资源字典合并功能
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <!--generic.xaml通常使用资源字典合并功能--> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/CustomControls;component/themes/ColorPicker.xaml" /> <ResourceDictionary Source="/CustomControls;component/themes/FlipPanel.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
下面是ColorPicker.xaml的基本结构
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="{x:Type local:ColorPicker}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:ColorPicker}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <Slider Name="PART_RedSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}"></Slider> <Slider Grid.Row="1" Name="PART_GreenSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}"></Slider> <Slider Grid.Row="2" Name="PART_BlueSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}"></Slider> <Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{TemplateBinding Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Path=Color, RelativeSource={RelativeSource TemplatedParent}}"></SolidColorBrush> </Rectangle.Fill> </Rectangle> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
精简控件模板
在控件自身的初始化代码中配置所有绑定表达式。这样,模板就不需要指定这些细节了。
通过删除三个滑动条的value属性的绑定表达式,并为三个滑动条添加PART名称,从而为通过代码设置绑定做好准备:
<Slider Name="PART_RedSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}"></Slider> <Slider Grid.Row="1" Name="PART_GreenSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}"></Slider> <Slider Grid.Row="2" Name="PART_BlueSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}"></Slider>
为确保获得最大的灵活性,这里没有为Rectangle元素提供名称,而是为其内部的solid-
ColorBrush指定了名称。这样,可根据模板为颜色预览功能使用任何形状或任意元素。
<Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{TemplateBinding Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Path=Color, RelativeSource={RelativeSource TemplatedParent}}"></SolidColorBrush> </Rectangle.Fill> </Rectangle>
在初始化控件后,可连接绑定表达式,但有一种更好的方法。WPF有一个专用的
OnApplyTemplate()方法,如果需要在模板中查找元素并关联事件处理程序或添加数据绑定表达
式,应重写该方法。在该方法中,可以使用GetTemplateChild()方法(该方法继承自Framework-
EIement)查找所需的元素。
public override void OnApplyTemplate() { base.OnApplyTemplate(); RangeBase slider = GetTemplateChild("PART_RedSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Red"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } slider = GetTemplateChild("PART_GreenSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Green"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } slider = GetTemplateChild("PART_BlueSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Blue"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } /*
绑定其他两个滑动条的代码在本质上是相同的。绑定SolidColorBrush画刷的代码稍有区
别,因为SolidColorBrush画刷没有包含SetBinding()方法(该方法是在FrameworkElement类中
定义的)。一个比较容易的变通方法是为ColorPicker℃olor属性创建绑定表达式,使用指向源方
向的单向绑定。这样,当颜色抬取器的颜色改变后,将自动更新画刷。
*/ SolidColorBrush brush = GetTemplateChild("PART_PreviewBrush") as SolidColorBrush; if (brush != null) { Binding binding = new Binding("Color"); binding.Source = brush; binding.Mode = BindingMode.OneWayToSource; this.SetBinding(ColorPicker.ColorProperty, binding); } }

3.支持可视化状态
FlipPanel控件背后的基本思想是,为驻留内容提供两个表面,但每次只有一个表面是可见的。
为看到其他内容,需要在两个表面之间进行“翻转”。可通过控件模板定制翻转效果,但默认效果
使用在前面和后面之间进行过渡的淡化效果。根据应用程序,可以使用FlipPaneI控件
把数据条目表单与一些有帮助的文档组合起来,以便为相同的数据提供一个简单或较复杂的视
图,或在一个简单游戏中将问题和答案融合在一起。
编写FIlpPanel类
using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Windows.Controls.Primitives; namespace CustomControls {
//为表明FlipPanel使用这些部件和状态的事实,应为自定义控件类应用TemplatePart特性
[TemplatePart(Name = "FlipButton", Type = typeof(ToggleButton)), TemplatePart(Name = "FlipButtonAlternate", Type = typeof(ToggleButton)), TemplateVisualState(Name = "Normal", GroupName = "ViewStates"), TemplateVisualState(Name = "Flipped", GroupName = "ViewStates")]
//FlipPanel直接继承自Control基类。如果不需要特定控件类的功能,这是最好的起点。 public class FlipPanel : Control {
//与WPF元素中的几乎所有属性一样,应使用依赖项属性。
public static readonly DependencyProperty FrontContentProperty = DependencyProperty.Register("FrontContent", typeof(object), typeof(FlipPanel), null);
public static readonly DependencyProperty BackContentProperty = DependencyProperty.Register("BackContent", typeof(object), typeof(FlipPanel), null); //添加CornerRadius依赖项属性并使用该属性配置FlipPanel控件的默认控件模板中的Border元素:
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(FlipPanel), null);
//只需要添加另一个重要属性:IsFlipped,这个Ble皿类型的属性持续跟踪FlipPane1控件的当前状态向前面还是面向后面),使控件使用者能够通过编程翻转状态:
public static readonly DependencyProperty IsFlippedProperty = DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipPanel), null); //接着需要添加调用基类的GetValue()和SetValue()方法的常规.NET属性过程,
public object FrontContent { get { return GetValue(FrontContentProperty); } set { SetValue(FrontContentProperty, value); } } public object BackContent { get { return GetValue(BackContentProperty); } set { SetValue(BackContentProperty, value); } } public CornerRadius CornerRadius { get { return (CornerRadius)GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } } public bool IsFlipped { get { return (bool)GetValue(IsFlippedProperty); } set { SetValue(IsFlippedProperty, value); ChangeVisualState(true); } } static FlipPanel() {
//为通知控件从generic.xaml文件获取默认样式,需要在FlipPanel类的静态构造函数中调用DefaultStyleKeyProperty.OverrideMetadata()方法:
DefaultStyleKeyProperty.OverrideMetadata(typeof(FlipPanel), new FrameworkPropertyMetadata(typeof(FlipPanel))); } //需要在FIipPanel控件中添加一些内容以使该模板工作。诀窍是使用OnApplyTemplate方法
public override void OnApplyTemplate() { base.OnApplyTemplate(); ToggleButton flipButton = base.GetTemplateChild("FlipButton") as ToggleButton; if (flipButton != null) flipButton.Click += flipButton_Click; ToggleButton flipButtonAlternate = base.GetTemplateChild("FlipButtonAlternate") as ToggleButton; if (flipButtonAlternate != null) flipButtonAlternate.Click += flipButton_Click; this.ChangeVisualState(false); } private void flipButton_Click(object sender, RoutedEventArgs e) { this.IsFlipped = !this.IsFlipped; } /*处理控件支持的所有不同状态可能会变得很凌乱。为避免在整个控件代码中分散调用GoTOState方法,大多数控件添加了与在FlipPanel控件中添加的ChangeVisu*State()类似的方
法。该方法负责应用每个状态组中的正确状态。该方法中的代码使用if语句块(或switch语句)应用每个状态组的当前状态。
*/ private void ChangeVisualState(bool useTransitions) { if (!this.IsFlipped) { VisualStateManager.GoToState(this, "Normal", useTransitions); } else { VisualStateManager.GoToState(this, "Flipped", useTransitions); } UIElement front = FrontContent as UIElement; if (front != null) { if (IsFlipped) { front.Visibility = Visibility.Hidden; } else { front.Visibility = Visibility.Visible; } } UIElement back = BackContent as UIElement; if (back != null) { if (IsFlipped) { back.Visibility = Visibility.Visible; } else { back.Visibility = Visibility.Hidden; } } } } }
还需要为FlipPanel控件添加一个应用默认模板的样式。将该样式放在generic.xaml资源字
典中,正如在开发ColorPicker控件时所做的那样。下面是需要的基本骨架:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="{x:Type local:FlipPanel}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:FlipPanel}"> <Grid>
<!--可在VisualStateGroups元素内部使用具有合适名称的VisualStateGroup元素创建状态组。
在每个VisualStateGroup元素内部,为每个状态添加一个VisualState元素。对于FlipPanel面板,有一个包含两个可视化状态的组:-->
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="ViewStates">
<!--为定义自定义过渡,在VisualTransition元素中放置具有一个或多个动画的故事板。在FlipPane
示例中,可使用自定义过渡确保ToggleButton箭头更快地旋转自身,而淡化过程更缓慢:-->
<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"></DoubleAnimation> </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"></DoubleAnimation> </Storyboard> </VisualTransition> </VisualStateGroup.Transitions> <!--在默认控件模板中,动画使用简单的淡化效果从一个内容区域改变到另一个内容区域,并使
用旋转变换翻转ToggleButton箭头使其指向另一个方向。下面是完成这两个任务的标记:-->
<VisualState x:Name="Normal"> <Storyboard> <DoubleAnimation Storyboard.TargetName="BackContent" Storyboard.TargetProperty="Opacity" To="0" Duration="0" ></DoubleAnimation> </Storyboard> </VisualState> <VisualState x:Name="Flipped"> <Storyboard> <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="90" Duration="0"></DoubleAnimation> <DoubleAnimation Storyboard.TargetName="FrontContent" Storyboard.TargetProperty="Opacity" To="0" Duration="0"></DoubleAnimation> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Border x:Name="FrontContent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" > <ContentPresenter Content="{TemplateBinding FrontContent}"> </ContentPresenter> </Border> <Border x:Name="BackContent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" > <ContentPresenter Content="{TemplateBinding BackContent}"> </ContentPresenter> </Border> <!--ToggleButton是使用用于绘制圆的Ellipse元素和用于绘制箭头的Path元素绘制的,这两个元素都放在具有单个单元格的Grip面板中:-->
<ToggleButton Grid.Row="1" x:Name="FlipButton" RenderTransformOrigin="0.5,0.5" Margin="0,10,0,0" Width="19" Height="19"> <ToggleButton.Template> <ControlTemplate> <Grid> <Ellipse Stroke="#FFA9A9A9" Fill="AliceBlue" /> <Path Data="M1,1.5L4.5,5 8,1.5" Stroke="#FF666666" StrokeThickness="2" HorizontalAlignment="Center" VerticalAlignment="Center"> </Path> </Grid> </ControlTemplate> </ToggleButton.Template> <!--ToggleButton按钮还需要一个细节动画时将使用RotateTransform对象:改变箭头指向的RotateTransform对象。当创建状态-->
<ToggleButton.RenderTransform> <RotateTransform x:Name="FlipButtonTransform" Angle="-90"></RotateTransform> </ToggleButton.RenderTransform> </ToggleButton> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
使用FlipPanel


浙公网安备 33010602011771号