Windows Presentation Foundation (WPF) 控件模型的扩展性大大减少了创建新控件的需求。 但是,在某些情况下,你仍可能需要创建自定义控件。 本主题讨论 Windows Presentation Foundation (WPF) 中减少您创建自定义控件需求的功能及不同的控件创作模型。 本主题还演示如何创建新控件。
编写新控件的替代方法
从历史上看,如果想要从现有控件获取自定义体验,则只能更改控件的标准属性,例如背景色、边框宽度和字号。 如果想要将控件的外观或行为扩展到这些预定义参数之外,则需要创建一个新控件,通常是从现有控件继承并重写负责绘制控件的方法。 尽管这仍然是一个选项,但 WPF 允许你使用其丰富的内容模型、样式、模板和触发器自定义现有控件。 以下列表提供了有关如何使用这些功能创建自定义和一致的体验的示例,而无需创建新控件。
丰富内容。 许多标准 WPF 控件都支持丰富的内容。 例如,Button的内容属性是Object类型,因此理论上任何内容都可以显示在Button对象上。 若要让按钮显示图像和文本,可以向StackPanel中添加一个图像和TextBlock,并将StackPanel分配给Content属性。 由于控件可以显示 WPF 视觉元素和任意数据,因此无需创建新控件或修改现有控件以支持复杂可视化效果。 有关 Button 的内容模型以及 WPF 中其他内容模型的详细信息,请参阅 WPF 内容模型。
风格。 A Style 是表示控件属性的值的集合。 通过使用样式,可以创建所需控件外观和行为的可重用表示形式,而无需编写新控件。 例如,假设你希望所有 TextBlock 控件都具有红色的 Arial 字体,字号为 14。 可以创建样式作为资源,并相应地设置相应的属性。 然后,添加到应用程序的每个 TextBlock 内容都将具有相同的外观。
数据模板。 使用 A DataTemplate ,可以自定义如何在控件上显示数据。 例如,一个 DataTemplate 可用于指定如何在一个 ListBox. 中显示数据。 有关此示例,请参阅 数据模板化概述。 除了自定义数据的外观外,还可以 DataTemplate 包含 UI 元素,这让你在自定义 UI 中具有很大的灵活性。 例如,通过使用 DataTemplate,可以创建一个 ComboBox ,其中每个项都包含复选框。
控件模板。 WPF 中的许多控件都使用 a ControlTemplate 定义控件的结构和外观,它将控件的外观与控件的功能分开。 可以通过重新定义控件的外观来大幅更改控件 ControlTemplate的外观。 例如,假设你想要一个看起来像停灯的控件。 此控件具有简单的用户界面和功能。 控件是三个圆圈,一次只能亮起其中一个。 经过一些思考,你可能会意识到,RadioButton 提供的是一次只能选中一个的功能,但 RadioButton 的默认外观看起来完全不像红绿灯上的灯光。 RadioButton由于使用控件模板来定义其外观,因此可以轻松重新定义ControlTemplate以符合控件的要求,并使用单选按钮制作交通信号灯。
注释
虽然一个RadioButton可以使用一个DataTemplate,但在本示例中,一个DataTemplate还不够。 `DataTemplate` 定义控件内容的外观。 在 RadioButton 的情况下,内容是指示 RadioButton 是否被选择的圆圈右侧的任何内容。 在停灯示例中,单选按钮只需是一个可以“亮起”的圆圈。由于停止灯的外观要求与默认外观RadioButton大相径庭,因此必须重新定义。ControlTemplate 一般情况下,用于 DataTemplate 定义控件的内容(或数据),用于 ControlTemplate 定义控件的结构。
触发器。 使用 A Trigger ,无需创建新控件即可动态更改控件的外观和行为。 例如,假设应用程序中有多个 ListBox 控件,并且希望每个 ListBox 控件中的项在选定时加粗和红色。 你的第一个反应可能是创建一个类,该类继承自ListBox并重写OnSelectionChanged方法以更改所选项的外观,但更好的方法是在ListBoxItem的样式中添加触发器以更改所选项的外观。 触发器使您能够更改属性值或基于属性值执行操作。 使用EventTrigger,可以在事件发生时启用执行动作。
有关样式、模板和触发器的详细信息,请参阅 样式和模板化。
通常,如果控件反映了现有控件的功能,但希望控件看起来不同,则应首先考虑是否可以使用本节中讨论的任何方法来更改现有控件的外观。
控件编写的模型
丰富的内容模型、样式、模板和触发器可最大程度地减少创建新控件的需求。 但是,如果需要创建新控件,请务必了解 WPF 中的不同控件创作模型。 WPF 提供了三个用于创建控件的常规模型,每个模型提供一组不同的功能和灵活性级别。 三个模型的基类是 UserControl, Control以及 FrameworkElement。
派生自 UserControl
在 WPF 中创建控件的最简单方法是派生自 UserControl。 在构建继承自UserControl的控件时,将现有组件添加到UserControl,为组件命名,并在 XAML 中引用事件处理程序。 然后,可以引用命名元素并在代码中定义事件处理程序。 此开发模型与 WPF 中用于应用程序开发的模型非常相似。
如果正确构建,UserControl可以利用丰富内容、样式和触发器带来的优势。 但是,如果控件继承自 UserControl,则使用控件的人员将无法使用 DataTemplate 或 ControlTemplate 自定义其外观。 必须从 Control 类或其派生类之一(而非 UserControl)派生,以创建自定义控件,该控件支持模板。
从用户控件 (UserControl) 派生的优势
如果以下所有条件适用,请考虑从 UserControl 派生:
你希望生成控件的方式与生成应用程序的方式类似。
控件仅包含现有组件。
无需支持复杂的自定义。
从控制派生
大多数现有的 WPF 控件使用的模型是从 Control 类派生的。 创建从类继承的 Control 控件时,可以使用模板定义其外观。 这样做可将作逻辑与视觉表示形式分开。 可以通过使用命令和绑定(而不是事件)来确保 UI 和逻辑的解耦,并在可能的情况下避免引用元素 ControlTemplate。 如果控件的 UI 和逻辑正确分离,控件的用户可以重新定义控件 ControlTemplate 以定制其外观。 尽管构建自定义Control并不像构建UserControl那么简单,自定义Control提供了最大的灵活性。
从控件派生的好处
如果适用以下任一项,请考虑从Control派生,而不是使用UserControl类:
你希望控制的外观能够通过ControlTemplate进行自定义。
你希望控件支持不同的主题。
从 FrameworkElement 派生
从 UserControl 或 Control 派生的控件依赖于组合现有元素。 对于许多方案,这是一个可接受的解决方案,因为继承自 FrameworkElement 的任何对象都可以位于其中 ControlTemplate。 但是,有时控件的外观需要的不仅仅是简单元素组合的功能。 对于这些方案,基于FrameworkElement的组件是正确的选择。
有两种标准方法用于构建基于FrameworkElement的组件:直接渲染和自定义元素组合。 直接呈现涉及重写FrameworkElement的方法,并提供DrawingContext操作,以显式定义组件的视觉效果。 这是Image和Border使用的方法。 自定义元素组合涉及使用类型的 Visual 对象来组合组件的外观。 有关示例,请参阅 使用 DrawingVisual 对象。 Track 是 WPF 中使用自定义元素组合的控件的示例。 还可以在同一控件中混合直接呈现和自定义元素组合。
从 FrameworkElement 继承的优势
请考虑在以下任一情况适用时从 FrameworkElement 派生:
你希望不仅限于简单元素组合,精确控制控件组件的视觉外观。
你希望通过定义自己的呈现逻辑来定义控件的外观。
你想要以新颖的方式组合现有元素,超越UserControl和Control所能实现的可能。
控件编写基础知识
如前所述,WPF 最强大的功能之一是超越设置控件的基本属性以更改其外观和行为的能力,但仍不需要创建自定义控件。 WPF 属性系统和 WPF 事件系统可以实现样式设置、数据绑定和触发器功能。 以下部分介绍了一些应该遵循的实践,不管你用于创建自定义控件的模型如何,以便自定义控件的用户可以使用这些功能,就像对 WPF 附带的控件一样。
使用依赖属性
当属性是依赖属性时,可以执行以下作:
设置样式中的属性。
将属性绑定到数据源。
使用动态资源作为属性值。
对属性进行动画处理。
如果希望控件的属性支持任何此功能,则应将其实现为依赖属性。 以下示例通过执行以下步骤定义一个名为Value
的依赖属性:
定义一个标识符DependencyProperty,命名为
ValueProperty
,作为public
static
readonly
字段。通过调用 DependencyProperty.Register向属性系统注册属性名称,以指定以下内容:
属性的名称。
属性类型。
拥有该属性的类型。
属性的元数据。 元数据包含属性的默认值 a CoerceValueCallback 和 a PropertyChangedCallback.
通过实现
get
属性和set
访问器,定义名为Value
CLR 包装器属性,该属性的名称与用于注册依赖属性的名称相同。 请注意,get
和set
访问器分别仅调用GetValue和SetValue。 建议依赖属性的访问器不包含其他逻辑,因为客户端和 WPF 可以绕过访问器并直接调用GetValueSetValue。 例如,当属性绑定到数据源时,不会调用该属性的访问set
器。 在更改值时,不要向 get 和 set 访问器添加其他逻辑,而应使用 ValidateValueCallback、CoerceValueCallback 和 PropertyChangedCallback 委托来响应或检查值。 有关这些回调的详细信息,请参阅 Dependency 属性回调和验证。为CoerceValueCallback定义一个名为
CoerceValue
的方法。CoerceValue
Value
确保大于或等于MinValue
或小于或等于MaxValue
。定义一个名为
OnValueChanged
的方法PropertyChangedCallback。OnValueChanged
创建一个 RoutedPropertyChangedEventArgs<T> 对象并准备引发ValueChanged
路由事件。 下一个部分将讨论路由事件。
/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(decimal), typeof(NumericUpDown),
new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
new CoerceValueCallback(CoerceValue)));
/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{
get { return (decimal)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static object CoerceValue(DependencyObject element, object value)
{
decimal newValue = (decimal)value;
NumericUpDown control = (NumericUpDown)element;
newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));
return newValue;
}
private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
NumericUpDown control = (NumericUpDown)obj;
RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
(decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
control.OnValueChanged(e);
}
''' <summary>
''' Identifies the Value dependency property.
''' </summary>
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(Decimal), GetType(NumericUpDown), New FrameworkPropertyMetadata(MinValue, New PropertyChangedCallback(AddressOf OnValueChanged), New CoerceValueCallback(AddressOf CoerceValue)))
''' <summary>
''' Gets or sets the value assigned to the control.
''' </summary>
Public Property Value() As Decimal
Get
Return CDec(GetValue(ValueProperty))
End Get
Set(ByVal value As Decimal)
SetValue(ValueProperty, value)
End Set
End Property
Private Shared Overloads Function CoerceValue(ByVal element As DependencyObject, ByVal value As Object) As Object
Dim newValue As Decimal = CDec(value)
Dim control As NumericUpDown = CType(element, NumericUpDown)
newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue))
Return newValue
End Function
Private Shared Sub OnValueChanged(ByVal obj As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
Dim control As NumericUpDown = CType(obj, NumericUpDown)
Dim e As New RoutedPropertyChangedEventArgs(Of Decimal)(CDec(args.OldValue), CDec(args.NewValue), ValueChangedEvent)
control.OnValueChanged(e)
End Sub
有关详细信息,请参阅 自定义依赖项属性。
使用路由事件
正如依赖属性扩展具有附加功能的 CLR 属性的概念一样,路由事件扩展了标准 CLR 事件的概念。 创建新的 WPF 控件时,最好将事件实现为路由事件,因为路由事件支持以下行为:
可以在多个控件的父级上处理事件。 如果事件是冒泡事件,则元素树中的单一父节点可以订阅该事件。 然后,应用程序作者可以使用一个处理程序来响应多个控件的事件。 例如,如果控件是每个项的一部分(因为它被包含在一个 DataTemplate 的 ListBox 中),应用程序开发人员可以在 ListBox 上为控件的事件定义事件处理程序。 每当任何控件上发生该事件时,将调用事件处理程序。
路由事件可在一个 EventSetter对象中使用,这使应用程序开发人员能够在样式中指定事件的处理程序。
路由事件可用于 EventTrigger,这对于使用XAML对属性进行动画处理非常有用。 有关详细信息,请参阅 动画概述。
以下示例通过以下步骤来定义路由事件:
定义一个名为
ValueChangedEvent
的标识符为public
static
readonly
字段。通过调用 EventManager.RegisterRoutedEvent 方法注册路由事件。 该示例在调用 RegisterRoutedEvent时指定以下信息:
事件的名称为
ValueChanged
.路由策略是指 Bubble首先调用源(引发事件的对象)上的事件处理程序,然后连续调用源父元素上的事件处理程序,从最近的父元素上的事件处理程序开始。
事件处理程序的类型是 RoutedPropertyChangedEventHandler<T>使用类型 Decimal 构造的。
事件的拥有类型为
NumericUpDown
.
声明一个名为
ValueChanged
的公共事件,并包含事件访问器声明。 示例在add
访问器声明和RemoveHandlerremove
访问器声明中调用AddHandler以使用 WPF 事件服务。创建一个名为
OnValueChanged
引发事件的ValueChanged
受保护虚拟方法。
/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
"ValueChanged", RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));
/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
RaiseEvent(args);
}
''' <summary>
''' Identifies the ValueChanged routed event.
''' </summary>
Public Shared ReadOnly ValueChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, GetType(RoutedPropertyChangedEventHandler(Of Decimal)), GetType(NumericUpDown))
''' <summary>
''' Occurs when the Value property changes.
''' </summary>
Public Custom Event ValueChanged As RoutedPropertyChangedEventHandler(Of Decimal)
AddHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
MyBase.AddHandler(ValueChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
MyBase.RemoveHandler(ValueChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As System.Object, ByVal e As RoutedPropertyChangedEventArgs(Of Decimal))
End RaiseEvent
End Event
''' <summary>
''' Raises the ValueChanged event.
''' </summary>
''' <param name="args">Arguments associated with the ValueChanged event.</param>
Protected Overridable Sub OnValueChanged(ByVal args As RoutedPropertyChangedEventArgs(Of Decimal))
MyBase.RaiseEvent(args)
End Sub
有关详细信息,请参阅 路由事件概述 和 创建自定义路由事件。
使用绑定
若要将控件的 UI 与其逻辑分离,请考虑使用数据绑定。 如果您使用ControlTemplate定义控件的外观,这一点尤为重要。 使用数据绑定时,可能无需从代码引用 UI 的特定部分。 最好避免引用 ControlTemplate 元素,因为当代码引用中 ControlTemplate 和 ControlTemplate 更改的元素时,引用的元素需要包含在新 ControlTemplate元素中。
以下示例更新TextBlockNumericUpDown
控件,为其分配名称,并在代码中按名称引用文本框。
<Border BorderThickness="1" BorderBrush="Gray" Margin="2"
Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
valueText.Text = Value.ToString();
}
Private Sub UpdateTextBlock()
valueText.Text = Value.ToString()
End Sub
以下示例使用绑定来完成相同的操作。
<Border BorderThickness="1" BorderBrush="Gray" Margin="2"
Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
有关数据绑定的详细信息,请参阅 数据绑定概述。
为设计师而设计
若要在 Visual Studio 的 WPF 设计器中接收对自定义 WPF 控件的支持(例如,使用“属性”窗口编辑属性),请遵循以下准则。 有关为 WPF 设计器进行开发的详细信息,请参阅 Visual Studio 中的设计 XAML。
依赖属性
请务必如前文中所述实现 CLR get
和 set
访问器,请参阅“使用依赖项属性”。设计器可以使用包装器来检测依赖项属性的存在,但它们(如 WPF 和控件的客户端)在获取或设置属性时不需要调用访问器。
附加属性
应使用以下准则在自定义控件上实现附加属性:
使用RegisterAttached方法创建的窗体具有PropertyName
Property
格式的public
static
readonly
DependencyProperty。 传递给 RegisterAttached 的属性名称必须与 PropertyName 匹配。实现一对名为
Set
PropertyName 和Get
PropertyName 的public
static
CLR 方法。 两种方法的第一个参数都应是派生自 DependencyProperty 的类。Set
PropertyName 方法还接受一个参数,该参数的类型与属性的已注册数据类型匹配。Get
PropertyName 方法应返回相同类型的值。Set
如果缺少 PropertyName 方法,则属性将标记为只读。Set
PropertyName 和Get
PropertyName 必须分别直接路由到目标依赖对象上的 GetValue 方法和 SetValue 方法。 设计器可以通过方法包装器调用或直接调用目标依赖项对象来访问附加属性。
有关附加属性的详细信息,请参阅 “附加属性概述”。
定义和使用共享资源
可以将控件包含在与应用程序相同的程序集中,也可以将控件打包到可在多个应用程序中使用的单独程序集中。 在大多数情况下,无论采用何种方法,本主题中讨论的信息都适用。 然而,有一个区别值得注意。 将控件放在与应用程序相同的程序集中时,可以自由地将全局资源添加到 App.xaml 文件。 但是,仅包含控件的程序集没有 Application 与之关联的对象,因此 App.xaml 文件不可用。
当应用程序查找资源时,它会按以下顺序查看三个级别:
元素级别。
系统从引用资源的元素开始,然后搜索逻辑父级的资源,依此类推,直到到达根元素。
应用程序级别。
对象 Application 定义的资源。
主题级别。
主题级字典存储在名为 Theme 的子文件夹中。 “主题”文件夹中的文件对应于主题。 例如,你可能具有 Aero.NormalColor.xaml、Luna.NormalColor.xaml、Royale.NormalColor.xaml 等。 还可以有一个名为 generic.xaml 的文件。 当系统在主题级别查找资源时,它会首先在特定于主题的文件中查找资源,然后在 generic.xaml 中查找它。
当控件位于独立于应用程序的程序集中时,必须将全局资源置于元素级别或主题级别。 这两种方法都有其优势。
在元素级别定义资源
可以通过创建自定义资源字典并将其与控件的资源字典合并,在元素级别定义共享资源。 使用此方法时,可以将资源文件命名为所需的任何内容,并且它可以与控件位于同一文件夹中。 元素级别的资源还可以使用简单字符串作为键。 以下示例创建名为 LinearGradientBrush Dictionary1.xaml 的资源文件。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<LinearGradientBrush
x:Key="myBrush"
StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Red" Offset="0.25" />
<GradientStop Color="Blue" Offset="0.75" />
</LinearGradientBrush>
</ResourceDictionary>
定义字典后,需要将其与控件的资源字典合并。 可以使用 XAML 或代码执行此作。
以下示例使用 XAML 合并资源字典。
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
此方法的缺点是每次引用对象时都会创建一个 ResourceDictionary 对象。 例如,如果库中有 10 个自定义控件,并使用 XAML 合并每个控件的共享资源字典,则创建 10 个相同的 ResourceDictionary 对象。 可以通过创建一个静态类来避免这种情况,该类合并代码中的资源并返回结果 ResourceDictionary。
以下示例创建一个返回共享 ResourceDictionary的类。
internal static class SharedDictionaryManager
{
internal static ResourceDictionary SharedDictionary
{
get
{
if (_sharedDictionary == null)
{
System.Uri resourceLocater =
new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml",
System.UriKind.Relative);
_sharedDictionary =
(ResourceDictionary)Application.LoadComponent(resourceLocater);
}
return _sharedDictionary;
}
}
private static ResourceDictionary _sharedDictionary;
}
以下示例在调用控件构造函数之前,将共享资源与自定义控件的资源合并。 由于该 SharedDictionaryManager.SharedDictionary
属性是静态属性,因此 ResourceDictionary 只创建一次。 由于资源字典是在调用 InitializeComponent
之前就已合并,因此控件可以在其 XAML 文件中使用这些资源。
public NumericUpDown()
{
this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
InitializeComponent();
}
在主题级别定义资源
WPF 使你可以为不同的 Windows 主题创建资源。 作为控件作者,可以定义特定主题的资源,以根据正在使用的主题来更改控件的外观。 例如,Windows 经典主题(Windows 2000 的默认主题)中的 Button 的外观不同于 Windows Luna 主题(Windows XP 的默认主题)中的 Button,因为 Button 为每个主题使用了不同的 ControlTemplate。
特定于主题的资源保存在具有特定文件名的资源字典中。 这些文件必须位于名为包含 Themes
控件的文件夹的子文件夹中。 下表列出了资源字典文件和与每个文件关联的主题:
资源字典文件名 | Windows 主题 |
---|---|
Classic.xaml |
Windows XP 上的经典 Windows 9x/2000 外观 |
Luna.NormalColor.xaml |
Windows XP 上的默认蓝色主题 |
Luna.Homestead.xaml |
Windows XP 上的橄榄主题 |
Luna.Metallic.xaml |
Windows XP 上的银主题 |
Royale.NormalColor.xaml |
Windows XP Media Center Edition 上的默认主题 |
Aero.NormalColor.xaml |
Windows Vista 上的默认主题 |
无需为每个主题定义资源。 如果未为某个主题定义资源,控件将检查 Classic.xaml
是否有该资源。 如果未在与当前主题或 in Classic.xaml
对应的文件中定义资源,则控件将使用泛型资源,该资源位于名为 generic.xaml
的资源字典文件中。 该文件 generic.xaml
位于与特定于主题的资源字典文件相同的文件夹中。 虽然 generic.xaml
与特定的 Windows 主题不对应,但它仍然是主题级字典。
具有主题和 UI 自动化支持示例的 C# 或 Visual Basic NumericUpDown 自定义控件包含控件 NumericUpDown
的两个资源字典:一个是 generic.xaml,另一个是 Luna.NormalColor.xaml。
在将 ControlTemplate 放入任意特定于主题的资源字典文件中时,必须为控件创建一个静态构造函数,并在 DefaultStyleKey 上调用 OverrideMetadata(Type, PropertyMetadata) 方法,如以下示例所示。
static NumericUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Shared Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
定义和引用主题资源的键
在元素级别定义资源时,可以将字符串分配为其键,并通过字符串访问资源。 在主题级别定义资源时,必须使用 ComponentResourceKey 作为关键字。 以下示例在 generic.xaml 中定义资源。
<LinearGradientBrush
x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:Painter},
ResourceId=MyEllipseBrush}"
StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Blue" Offset="0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>
以下示例通过将资源 ComponentResourceKey 指定为键来引用资源。
<RepeatButton
Grid.Column="1" Grid.Row="0"
Background="{StaticResource {ComponentResourceKey
TypeInTargetAssembly={x:Type local:NumericUpDown},
ResourceId=ButtonBrush}}">
Up
</RepeatButton>
<RepeatButton
Grid.Column="1" Grid.Row="1"
Background="{StaticResource {ComponentResourceKey
TypeInTargetAssembly={x:Type local:NumericUpDown},
ResourceId=ButtonBrush}}">
Down
</RepeatButton>
指定主题资源的位置
若要查找控件的资源,宿主应用程序需要知道程序集包含特定于控件的资源。 可以通过向包含控件的程序集添加 ThemeInfoAttribute 来实现此目的。 ThemeInfoAttribute具备一个GenericDictionaryLocation属性,用于指定泛型资源的位置,和一个ThemeDictionaryLocation属性,用于指定特定于主题的资源的位置。
以下示例将GenericDictionaryLocation和ThemeDictionaryLocation属性设置为SourceAssembly,以指定泛型资源和主题特定资源与控件位于同一程序集中。
[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
ResourceDictionaryLocation.SourceAssembly)]
<Assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)>