【WPF学习】第六十四章 构建基本的用户控件
创建一个简单用户控件是开始自定义控件的好方法。本章主要介绍创建一个基本的颜色拾取器。接下来分析如何将这个控件分解成功能更强大的基于模板的控件。
创建基本的颜色拾取器很容易。然而,创建自定义颜色拾取器仍是有价值的联系,因为这不仅演示了构建控件的各种重要概念,而且提供了一个实用的功能。
可为颜色拾取器创建自定义对话框。但如果希望创建能集成进不同窗口的颜色拾取器,使用自定义控件是更好的选择。最简单的自定义控件类型是用户控件,当设计窗口或页面时通过用户控件可以使用相同的方式组装多个元素。因为仅通过直接组合现有控件并添加功能并不能实现颜色拾取器,所以用户控件看起来是更合理的选择。
典型的颜色拾取器允许用户通过单击颜色梯度中的某个位置或分别指定红、绿和蓝三元色成分来选择颜色。下图显示了创建的基本颜色拾取器。该颜色拾取器包含三个Slider控件,这些控件用于调节颜色成分,同时使用Rectangle元素预览选择的颜色。
一、定义依赖性属性
创建颜色拾取器的第一步是为自定义控件库项目添加用户控件。当添加用户控件后,Visual Studio会创建XAML标记文件和相应的包含初始化代码即事件处理代码的自定义类。这与创建新的窗口或也卖弄是相同的——唯一的区别在与顶级容器是UserControl类:
<UserControl x:Class="CustomControls.ColorPickerUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" Name="colorPicker"> </UserControl>
最简单的起点是设计用户控件对外界公开的公共接口。换句话说,就是设计控件使用者使用的鱼颜色拾取器进行交互的属性、方法和事件。
最基本的细节是Color属性——毕竟,颜色拾取器不过是用于显示和选择颜色的特定工具。为支持WPF特性,如数据绑定、样式以及动画,控件的可写属性几乎都是依赖项属性。
在前面章节中学习过,创建依赖项属性的第一步是为之定义静态字段,并在属性名称的后面加上单词Property:
public static DependencyProperty ColorProperty;
Color属性将允许控件使用者通过代码设置或检索颜色值。然而,颜色拾取器中的滑动条控件也允许用户修改当前颜色的一个方面。为实现这一设计,当滑动条额值发生变化时,需要使用事件处理程序进行响应,并且响应地更新Color属性。但使用数据绑定关联滑动条会更加清晰。为使用数据绑定,需要将每个颜色成分定义为单独的依赖项属性:
public static DependencyProperty RedProperty; public static DependencyProperty GreenProperty; public static DependencyProperty BlueProperty;
尽管Color属性存储了System.Windows.Media.Color对象,但Red、Green以及Blue属性将存储表示每个颜色成分的单个字节值。
为属性定义静态字段只有第一步。还需要有静态构造函数,用于在用户控件中注册这些依赖性属性,指定属性的名称、数据类型以及拥有属性的控件类。可通过传递具有正确标记设置的FrameworkPropertyMetadata对象,在静态构造函数中指定选择的特定属性特性(如值继承)。还可指出在什么地方为验证、数据强制以及属性更改通知关联回调函数。
在颜色拾取器中,只需要考虑一个因素——当各种属性变化时需要关联回调函数进行响应。因为Red、Green和Blue属性实际上时Color属性的不同表示,并且如果一个属性发生变化,就需要确保其他属性保持同步。
下面是注册颜色拾取器的4个依赖性属性的静态构造函数的代码:
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))); }
现在已经定义了依赖性属性,可添加标准的属性封装器,使范文它们变得更加容易,并可在XAML中使用它们:
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); } }
请记住,属性封装器不能包含任何逻辑,因为可直接使用DependencyObject基类的SetValue()和GetValue()方法设置和检索属性。例如,在这个示例中的属性同步逻辑是使用回调函数实现的,当属性发生变化时通过属性封装器或者直接调用SetValue()方法引发回调函数。
属性变化回调函数负责使Color属性与Red、Green以及Blue属性保持一致。无论何时Red、Green以及Blue属性发生变化,都会相应地调整Color属性:
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; }
当设置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; }
尽管很明显,但当各个属性试图改变其他属性时,上面的代码不会引起一系列无休止的调用。因为WPF不允许重新进入属性变化回调函数。例如,如果改变Color顺序,就会触发OnColorChanged()方法。OnColorChanged()方法会修改Red、Green以及Blue属性,从而触发OnColorRGBChanged()回调方法三次(每个属性触发一次)。然而,OnColorRGBChanged()方法不会再次触发OnColorChanged()方法。
二、定义路由事件
通过添加路由事件,当发生一些事情时用于通知控件使用者。在颜色拾取器示例中,当颜色发生变化后,触发一个事件是很有用处的。尽管可将这个事件定义为普通的.NET事件,但使用路由事件可提供冒泡和隧道特性,从而可在更高层次的父元素中处理事件。
与依赖项属性一样,定义路由事件的一个步骤是为值创建静态属性,并在时间名称的后面添加单词Event:
public static readonly RoutedEvent ColorChangedEvent;
然后可在静态构造函数中注册事件。在静态构造函数中指定事件的名称、路由策略、签名以及拥有事件的类:
ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPickerUserControl));
不一定要为事件签名创建新的委托,有时可重用已经存在的委托。两个有用的委托是RoutedEventHandler(用于不带额外信息的路由事件)和RoutedPropertyChangedEventHandler(用于提供属性发生变化之后的旧值和新值得路由事件)。上例中使用RoutedPropertyChangedEventHandler委托,是被类型参数化了的泛型委托。所以,可为任何属性数据类型使用该委托,而不会牺牲类型安全功能。
定义并注册事件后,需要创建标准的.NET事件封装器来公开事件。事件封装器可用于关联和删除事件监听程序:
public event RoutedPropertyChangedEventHandler<Color> ColorChanged { add { AddHandler(ColorChangedEvent, value); } remove { RemoveHandler(ColorChangedEvent, value); } }
最后的细节是在适当时候引发事件的代码。该代码必须调用继承自DependencyObject基类的RaiseEvent()方法。
在颜色拾取器示例中,只需要在OnColorChanged()方法之后添加如下代码即可:
RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>(oldColor, newColor); args.RoutedEvent = ColorChangedEvent; colorPicker.RaiseEvent(args);
请记住,无论何时修改Color属性,不管是直接修改还是通过修改Red、Green以及Blue成分,都会触发OnColorChanged()回调函数。
三、添加标记
现在已经定义好用户控件的公有接口,需要做的所有工作就是创建控件外观的标记。在这个示例中,需要使用一个基本Grid控件将三个Slider控件和预览颜色的Rectangle元素组合在一起。技巧是使用数据绑定表达式,将这些控件连接到合适的属性,而不需要使用事件处理代码。
总之,颜色拾取器中总共使用4个数据绑定表达式。三个滑动条被绑定到Red、Green和Blue属性。而且属性值得允许范围是0~255(一个字节可以接受的数值)。Rectangle.Fill属性使用SolidColorBrush画刷进行设置。画刷的Color属性被绑定到用户控件的Color属性。
下面是完整的标记:
<UserControl x:Class="CustomControls.ColorPickerUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" Name="colorPicker"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </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>
用于用户控件的标记和无外观控件的控件模板扮演相同的角色。如果希望使标记中的一些细节是可配置的,可使用将他们连接到控件属性的绑定表达式。例如,目前Rectangle元素的宽度被固定为50个单位。然而,可使用数据绑定表达式从用户控件的依赖性属性中提取数值来代替这些细节。这样,控件使用者可通过修改属性来选择不同的宽度。同样,可使笔画颜色和宽度也是可变的。然而,如果希望使控件具有真正的灵活性,最好的创建无外观的控件,并在模板中定义标记。
偶尔可选用数据绑定表达式,重用已在控件中定义过的核心属性。例如,UserControl类使用Padding属性在外侧边缘和用户定义的内部内容之间添加空间(这一细节是通过UserControl控件的控件模板实现的)。然而,也可以使用Padding属性在每个滑动条的周围设置空间,如下所示:
<Slider Name="sliderRed" Minimum="0" Maximum="255" Margin="{Binding ElementName=colorPicker,Path=Padding}" Value="{Binding ElementName=colorPicker,Path=Red}"></Slider>
类似地,也可从UserControl类的BorderThickness和BorderBrush属性为Rectan元素获取边框设置。同样,这样快捷方式对于创建简单的控件是非常合理的,但可通过引入额外的属性(如SliderMargin、PreviewBorderBrush以及PreviewBorderThickness)或创建功能完备的基于模板的控件加以改进。
四、使用控件
现在完成了控件,使用该控件很容易。为在另一个窗口中使用颜色拾取器,首先需要将程序集合.NET名称控件映射到XAML名称空间,如下所示:
<Window x:Class="CustomControlsClient.ColorPickerUserControlTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls" ...>
使用定义的XML名称控件和用户控件类名,在XAML标记中可像创建其他类型的对象那样创建自定义的用户控件。还可在控件标记中设置它的属性,以及直接关联事件处理程序,如下所示:
<lib:ColorPickerUserControl Name="colorPicker" Margin="2" Padding="3" ColorChanged="colorPicker_ColorChanged" Color="Yellow"></lib:ColorPickerUserControl>
因为Color属性使用Color数据类型,并且Color数据类型使用TypeConverter特性进行了修饰,所以在设置Color属性之前,WPF知道使用ColorConverter转换器将颜色名称字符串转换成相应的Color对象。
处理ColorChanged事件的代码很简单:
private void colorPicker_ColorChanged(object sender, RoutedPropertyChangedEventArgs<Color> e) { if (lblColor != null) lblColor.Text = "The new color is " + e.NewValue.ToString(); }
现在已经完成了自定义控件。
五、命令支持
许多控件具有命令支持。可使用以下两种方法为自定义控件添加命令支持:
- 添加将控件链接到特定命令的命令绑定。通过这种方法,控件可以相应命令,而且不需要借助于任何外部代码。
- 为命令创建新的RoutedUICommand对象,作为自定义控件的静态字段。然后为这个命令对象添加命令绑定。这种方法可使自定义控件自动支持没有在基本命令类集合中定义的命令。
接下来的将使用第一种方法为ApplicationCommands.Undo命令添加支持。
在颜色拾取器中为了支持Undo功能,需要使用成员字段跟踪以前选择的颜色:
private Color? previousColor;
将该字段设置为可空是合理的,因为当第一次创建控件时,还没有设置以前选择的颜色。
当颜色发生变化时,只需要记录旧值。可通过在OnColorChanged()方法的最后添加以下代码行来达到该目的:
colorPicker.previousColor = oldColor;
现在已经具备了支持Undo命令需要的基础框架。剩余的工作是创建将控件链接到命令以及处理CanExecute和Executed事件的命令绑定。
第一次创建控时是创建命令绑定的最佳时机。例如,下面的代码使用颜色拾取器的构造函数为ApplicationCommands.Undo命令添加命令绑定:
public ColorPickerUserControl() { InitializeComponent(); SetUpCommands(); } private void SetUpCommands() { CommandBinding binding = new CommandBinding(ApplicationCommands.Undo, UndoCommand_Executed, UndoCommand_CanExecute); this.CommandBindings.Add(binding); }
为使命令奏效,需要处理CanExecute事件,并且只要有以前的颜色值就允许执行命令:
private void UndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = previousColor.HasValue; }
最后,当执行命令后,可交换新的颜色:
private void UndoCommand_Executed(object sender, ExecutedRoutedEventArgs e) { this.Color = (Color)previousColor; }
可通过两种不同方式触发Undo命令。当用户控件中的某个元素具有焦点时,可以使用默认的Ctrl+Z组合键绑定,也可为客户添加用于触发命令的按钮,如下所示:
<Button Command="Undo" CommandTarget="{Binding ElementName=colorPicker}" Margin="5,0,5,0" Padding="2">Undo</Button>
这两种方法都会丢弃当前颜色并应用以前的颜色。
更可靠的命令
前面描述的技术是将命令链接到控件的相当合理的方法,但这不是在WPF元素和专业控件中使用的技术。这些元素使用更可靠的方法,并使用CommandManager.RegisterClassCommandBinding()方法关联静态的命令处理程序。
上一个示例中演示的实现存在问题:使用公用CommandBindings集合。这使得命令比较脆弱,因为客户可自由修改CommandBindings集合。而使用RegisterClassCommandBinding()方法无法做到这一点。WPF控件使用的就是这种方法。例如,如果查看TextBox的CommandBindings集合,不会发现任何用于硬编码命令的绑定,例如Undo、Redo、Cut、Copy以及Paste等命令,因为他们被注册为类绑定。
这种技术非常简单。不在实例构造函数中创建命令绑定,而必须在静态构造函数中创建命令绑定,使用如下所示的代码:
CommandManager.RegisterClassCommandBinding(typeof(ColorPickerUserControl), new CommandBinding(ApplicationCommands.Undo, UndoCommand_Executed, UndoCommand_CanExecute));
尽管上面的代码变化不大,但有一个重要变化。因为 UndoCommand_Executed()和UndoCommand_CanExecute()方法是在构造函数中引用的,所以必须是静态方法。为检索实例数据(例如当前颜色和以前颜色的信息),需要将事件发送者转换为ColorPickerUserControl对象,并使用该对象。
下面是修改之后的命令处理代码:
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) { ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender; colorPicker.Color = (Color)colorPicker.previousColor.Value; }
此外,这种技术不局限于命令。如果希望将事件处理逻辑硬编码到自定义控件,可通过EventManager.RegisterClassHandler()方法使用类事件处理程序。类事件处理程序总在实例事件处理程序之前调用,从而允许开发人员很容易地抑制事件。
六、深入分析用户控件
用户控件提供了一种非常简单的,但是有一定限制的创建自定义控件的方法。为理解其中的原因,深入分析用户控件的工作原理是很有帮助的。
在后台,UserControl类的工作方式和其父类ContentControl非常类似。实际上,只有几个重要的区别:
- UserControl类改变了一些默认值。即该类将IsTabStop和Focusable属性设置为false(从而在Tab顺序中没有占据某个单独的额位置),并将HorizontalAlignment和VerticalAlignment属性设置为Stretch(而非Left或Top),从而可以填充可用空间。
- UserControl类应用了一个新的控件模板,该模板由包含ContentPresenter元素的Border元素组成。ContentPresenter元素包含了用标记添加的内容。
- UserControl类改变了路由事件的源。当事件从用户控件内的控件向用户控件外的元素冒泡或隧道路由时,事件源变为指向用户控件而不是原始元素。这提供了更好的封装性。
用户控件和其他类型的自定义控件之间最重的区别是设计用户控件的方法。与所有控件一样,用户控件有控件模板。然而,很少改变控件模板——反而,将作为自定义用户控件类的一部分提供标记,并且当创建了控件后,会使用InitializeComponet()方法处理这个标记。另一个方面,无外观控件是没有标记——需要的所有内容都在模板中。
普通的ContentControl控件具有下面的简单模板:
<ControlTemplate TargetType="ContentControl"> <ContentPresenter ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" Content="{TemplateBinding ContentControl.Content}"/> </ControlTemplate>
这个模板仅填充所提供的内容并应用可选的内容模板。Padding、Background、HorizontalAlignment以及VerticalAlignment等熟悉没有任何影响(除非显示绑定属性)。
UserControl类有一个类似的模板,并又更多的细节。最明显的是,它添加了一个Border元素并将其属性绑定到用户控件的BorderBrush、BorderThickness、Background以及Padding属性,以确保它们具有相同的含义。此外,内部的ContentPresenter元素已绑定到对齐属性。
<ControlTempalte TargetType="UserControl"> <Border BorderBrush="{TemplateBinding Border.BorderBrush}" BorderThickness="{TemplateBinding Border.BorderThickness}" Background="{TemplateBinding Border.Background}" Padding="{TemplateBinding Border.Padding}" SnapsToDevicePixels="True"> <ContentPresenter HorizontalAlignment="{TemplateBinding Control.HorizontalAlignment}" VerticalAlignment="{TemplateBinding Control.VerticalAlignment}" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" Contenttemplate="{TemplateBinding ContentControl.ContentTemplate}" Content="{TemplateBinding ContentControl.Content}"/> </Border> </ControlTemplate>
从技术角度看,可改变用户控件的模板。实际上,只需要进行很少的调整,就可以将所有标记移到模板中。但却是没有理由采取该方法——如果希望得到更灵活的控件,时可视化外观和由自定义控件类定义的借款分开,创建无外观的自定义控件可能会更好一些。
本章程序源代码:CustomControl.zip