控件创作概述

Windows Presentation Foundation (WPF) 控件模型的扩展性极大地减少了创建新控件的需要。 但在某些情况下,仍可能需要创建自定义控件。 本主题讨论可最大限度减少在 Windows Presentation Foundation (WPF) 中创建自定义控件以及其他控件创作模型的需要的功能。 本主题还演示如何创建新控件。

编写新控件的替代方法

以前,如果要通过现有控件获取自定义体验,只能更改控件的标准属性,例如背景色、边框宽度和字号。 如果希望在这些预定义参数的基础之上扩展控件的外观或行为,则需要创建新控件,常用的方法是继承现有控件并重写负责绘制该控件的方法。 虽然这仍是一种可选方法,但也可以利用 WPF 的丰富内容模型、样式、模板和触发器来自定义现有的控件。 下表提供了一些示例,演示如何在不创建新控件的情况下使用这些功能来实现一致的自定义体验。

  • 丰富内容。 很多标准 WPF 控件都支持丰富内容。 例如,Button 的内容属性为 Object 类型,因此从理论上讲,任何内容都可以显示在 Button 上。 若要使按钮显示图像和文本,可以将图像和 TextBlock 添加到 StackPanel 中,然后将 StackPanel 分配给 Content 属性。 由于这些控件可以显示 WPF 视觉元素和任意数据,因此,降低了创建新控件或修改现有控件来支持复杂可视化效果的需求。 有关 Button 的内容模型和 WPF 中其他内容模型的详细信息,请参阅 WPF 内容模型

  • 样式。 Style 是表示控件属性的值的集合。 使用样式可创建所需控件外观和行为的可重用表示形式,而无需编写新控件。 例如,假设需要所有 TextBlock 控件的字体都为红色 Arial 字体,并且字号为 14。 可以创建一个样式作为资源,并相应设置适当属性。 这样,添加到应用程序中的每个 TextBlock 都将具有相同的外观。

  • 数据模板。 通过 DataTemplate,可以自定义数据在控件上的显示方式。 例如,DataTemplate 可用于指定数据在 ListBox 中的显示方式。 有关这种情况的示例,请参阅数据模块化概述。 除了自定义数据的外观,DataTemplate 还可以包含 UI 元素,这大大增加了自定义 UI 的灵活性。 例如,使用 DataTemplate 可以创建 ComboBox,其中每一项都包含一个复选框。

  • 控件模板。 WPF 中的许多控件使用 ControlTemplate 定义控件的结构和外观,因为它可以将控件的外观和功能区分开来。 重新定义 ControlTemplate 可以极大地更改控件的外观。 例如,假设需要一个看起来像交通信号灯的控件。 此控件具有简单的用户界面和功能。 该控件有三个圆圈,一次只有一个圆圈亮起。 经过一番考虑后,用户可能意识到 RadioButton 提供了一次只选择一项的功能,但是 RadioButton 的默认外观完全不像交通信号灯上的灯。 由于 RadioButton 使用控件模板定义外观,因此很容易重新定义 ControlTemplate 以符合该控件的要求,从而使用单选按钮来制作交通信号灯。

    注意

    尽管 RadioButton 可以使用 DataTemplate,但在本例中,光有 DataTemplate 还不够。 DataTemplate 定义控件内容的外观。 对于 RadioButton,指示是否选择 RadioButton 的圆圈右侧显示出来的全部都是该控件的内容。 在交通信号灯的示例中,单选按钮只需要是可“点亮”的圆圈。由于交通信号灯的外观要求与 RadioButton 的默认外观存在很大差异,因此,有必要重新定义 ControlTemplate。 一般情况下,DataTemplate 用于定义控件的内容(或数据),ControlTemplate 用于定义控件的构成方式。

  • 触发器。 使用 Trigger 可以动态更改控件的外观和行为,无需创建新控件。 例如,假设应用程序中有多个 ListBox 控件,但需要每个 ListBox 中的项在选中时都显示为红色粗体。 用户首先想到的可能是创建一个从 ListBox 继承的类,然后重写 OnSelectionChanged 方法,以更改选中项的外观,不过,更好的方法是向 ListBoxItem 的样式添加一个更改选中项外观的触发器。 触发器可以更改属性值或根据属性值执行操作。 EventTrigger 使用户可以在事件发生时执行操作。

有关样式、模板和触发器的详细信息,请参阅样式设置和模板化

一般情况下,如果控件可以镜像现有控件的功能,但希望该控件具有不同的外观,则应先考虑是否可以使用本节中讨论的某些方法来更改现有控件的外观。

控件创作模型

通过丰富内容模型、样式、模板和触发器,最大程度地减少创建新控件的需要。 但是,如果确实需要创建新控件,则理解 WPF 中的不同控件创作模型就显得非常重要。 WPF 提供三个用于创建控件的常规模型,每个模型都提供不同的功能集和灵活度。 三个模型的基类是 UserControlControlFrameworkElement

从 UserControl 派生

在 WPF 中创建控件的最简单方法是从 UserControl 派生。 生成继承自 UserControl 的控件时,会将现有组件添加到 UserControl,命名这些组件,然后在 XAML 中引用事件处理程序。 随后可以在代码中引用命名的元素和定义事件处理程序。 此开发模型与用于在 WPF 中开发应用程序的模型非常相似。

如果生成无误,UserControl 可以利用丰富内容、样式和触发器的优势。 但是,如果控件继承自 UserControl,则使用该控件的用户将无法使用 DataTemplateControlTemplate 来自定义其外观。 因此,有必要从 Control 类或其派生类(UserControl 除外)之一中派生,以创建支持模板的自定义控件。

从 UserControl 派生的优点

如果符合以下所有情况,请考虑从 UserControl 派生:

  • 希望采用与生成应用程序相似的方法生成控件。

  • 控件仅包含现有组件。

  • 无需支持复杂的自定义项。

从 Control 派生

Control 类派生是大多数现有 WPF 控件使用的模型。 创建从 Control 类派生的控件时,可使用模板定义它的外观。 通过这种方式,可以将运算逻辑从视觉表示形式中分离出来。 通过使用命令和绑定(而不是事件)并尽可能避免引用 ControlTemplate 中的元素,也可确保分离 UI 和逻辑。 如果控件的 UI 和逻辑正确分离,该控件的用户即可重新定义控件的 ControlTemplate,从而自定义其外观。 尽管生成自定义 Control 不像生成 UserControl 那样简单,但自定义 Control 还是提供了最大的灵活性。

从 Control 派生的优点

如果符合以下任一情况,请考虑从 Control 派生,而不要使用 UserControl 类:

  • 希望控件的外观能通过 ControlTemplate 进行自定义。

  • 希望控件支持不同的主题。

从 FrameworkElement 派生

UserControlControl 派生的控件依赖于组合现有元素。 在很多情况下,这是一种可接受的解决方案,因为从 FrameworkElement 继承的任何对象都可以位于 ControlTemplate 中。 但是,某些时候,简单的元素组合不能满足控件的外观需要。 对于这些情况,使组件基于 FrameworkElement 才是正确的选择。

生成基于 FrameworkElement 的组件有两种标准方法:直接呈现和自定义元素组合。 直接呈现涉及的操作包括:重写 FrameworkElementOnRender 方法,并提供显式定义组件视觉对象的 DrawingContext 操作。 这是由 ImageBorder 使用的方法。 自定义元素组合涉及的操作包括:使用 Visual 类型的对象组合组件的外观。 有关示例,请参阅使用 DrawingVisual 对象Track 是 WPF 中使用自定义元素组合的控件示例。 在同一控件中,也可以混合使用直接呈现和自定义元素组合。

从 FrameworkElement 派生的优点

如果符合以下任何情况,请考虑从 FrameworkElement 派生:

  • 希望对控件的外观进行精确控制,而不仅仅是简单的元素组合提供的效果。

  • 希望通过定义自己的呈现逻辑来定义控件的外观。

  • 希望以一种 UserControlControl 之外的新颖方式组合现有元素。

控件创作基础知识

如前所述,WPF 的最强大功能之一在于,它能够在不需要创建自定义控件的情况下,不只是通过设置控件的基本属性来更改其外观和行为。 样式设置、数据绑定和触发器功能通过 WPF 属性系统和 WPF 事件系统实现。 以下各部分介绍应遵循的一些做法(与创建自定义控件时所用的模型无关),以便自定义控件的用户可以像使用 WPF 附带的控件一样使用这些功能。

使用依赖属性

当属性为依赖属性时,可以执行以下操作:

  • 在样式中设置该属性。

  • 将该属性绑定到数据源。

  • 使用动态资源作为该属性的值。

  • 对该属性进行动画处理。

如果希望控件的属性支持以上任一功能,应将该属性实现为依赖属性。 下面的示例通过执行以下操作定义一个名为 Value 的依赖属性:

  • 将一个名为 ValuePropertyDependencyProperty 标识符定义为 publicstaticreadonly 字段。

  • 通过调用 DependencyProperty.Register 向属性系统注册该属性名,以指定以下内容:

  • 通过实现该属性的 getset 访问器,定义一个名为 Value(与用来注册该依赖属性的名称相同)的 CLR 包装器属性。 请注意,getset 访问器仅分别调用 GetValueSetValue。 建议依赖属性的访问器不要包含其他逻辑,因为客户端和 WPF 可绕过这两个访问器直接调用 GetValueSetValue。 例如,如果属性绑定到数据源,则不会调用该属性的 set 访问器。 不要向 get 和 set 访问器添加其他逻辑,应使用 ValidateValueCallbackCoerceValueCallbackPropertyChangedCallback 委托在值更改时进行响应或检查该值。 有关这些回叫的详细信息,请参阅依赖属性回叫和验证

  • CoerceValueCallback 定义方法,名为 CoerceValueCoerceValue 确保 Value 大于或等于 MinValue 且小于或等于 MaxValue

  • PropertyChangedCallback 定义方法,名为 OnValueChangedOnValueChanged 创建一个 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 控件时,将事件实现为路由事件也是一种好方法,因为路由事件支持以下行为:

  • 事件可以在多个控件的父级上进行处理。 如果事件是浮升事件,元素树中的单个父级可订阅该事件。 然后,应用程序作者可以使用一个处理程序来响应多个控件的该事件。 例如,如果控件属于 ListBox 中的每个项(因为它包含在 DataTemplate 中),应用程序开发人员可以为该控件的 ListBox 事件定义相应的事件处理程序。 每当其中任何控件发生该事件时,都会调用该事件处理程序。

  • 路由事件可在 EventSetter 中使用,应用程序开发人员通过它可以在样式内指定事件的处理程序。

  • 路由事件可在 EventTrigger 中使用,这对于使用 XAML 对属性进行动画处理很有用。 有关详细信息,请参阅 动画概述

下面的示例通过执行以下操作定义了一个路由事件:

  • 将一个名为 ValueChangedEventRoutedEvent 标识符定义为 publicstaticreadonly 字段。

  • 可通过调用 EventManager.RegisterRoutedEvent 方法来注册路由事件。 该示例在调用 RegisterRoutedEvent 时指定以下信息:

    • 事件名称是 ValueChanged

    • 路由策略为 Bubble,这意味着首先调用源(引发事件的对象)上的事件处理程序,然后从最近的父元素上的事件处理程序开始,相继调用源的各个父元素上的事件处理程序。

    • 事件处理程序的类型是 RoutedPropertyChangedEventHandler<T>(使用 Decimal 类型构造)。

    • 该事件的所属类型为 NumericUpDown

  • 声明一个名为 ValueChanged 的公共事件,并包含事件访问器声明。 该示例调用 add 访问器声明中的 AddHandlerremove 访问器声明中的 RemoveHandler 来使用 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 中。

下面的示例更新 NumericUpDown 控件的 TextBlock,向它分配一个名称,然后在代码中按名称引用该文本框。

<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 getset 访问器,如前面的“使用依赖属性”中所述。设计器可以使用包装器来检测某个依赖属性是否存在,但与 WPF 和控件客户端一样,在获取或设置属性时不需要使用设计器来调用访问器。

附加属性

应使用以下准则在自定义控件上实现附加属性:

  • 具有一个使用 RegisterAttached 方法创建的 publicstaticreadonlyDependencyProperty,其形式为 PropertyNameProperty。 传递到 RegisterAttached 的属性名称必须与 PropertyName 匹配。

  • 实现一对名为Set属性名称Get属性名称publicstatic CLR 方法。 这两种方法都应接受从 DependencyProperty 派生的类作为其第一个参数。 Set属性名称方法还接受其类型与属性的注册数据类型匹配的参数。 Get属性名称 方法应返回相同类型的值。 如果缺少 Set属性名称方法,则该属性标记为只读。

  • SetPropertyNameGetPropertyName 必须分别直接路由到目标依赖对象的 GetValueSetValue 方法。 通过调用方法包装器或直接调用目标依赖对象,设计器可以访问附加属性。

有关附加属性的详细信息,请参阅附加属性概述

定义和使用共享资源

可以将控件包含在应用程序所在的程序集中,也可以将控件打包到可在多个应用程序中使用的单独程序集中。 大多数情况下,不管使用什么方法,本主题中讨论的信息都适用。 但有一处差异值得注意。 将控件放入应用程序所在的程序集中时,可以随意向 App.xaml 文件添加全局资源。 但只包含控件的程序集没有与之关联的 Application 对象,因此 App.xaml 文件不可用。

当应用程序查找资源时,它会按以下顺序在三个级别进行查找:

  1. 元素级别。

    系统从引用该资源的元素开始搜索,接着搜索逻辑父级的资源,依此类推,直至到达根元素。

  2. 应用程序级别。

    Application 对象定义的资源。

  3. 主题级别。

    主题级别的字典存储在名为“Themes”的子文件夹中。 Themes 文件夹中的文件与主题对应。 例如,可能有 Aero.NormalColor.xaml、Luna.NormalColor.xaml、Royale.NormalColor.xaml 等。 可能还有一个名为 generic.xaml 的文件。 当系统在主题级别查找资源时,它会先在特定于主题的文件中查找相应资源,然后在 generic.xaml 中进行查找。

当控件位于独立于应用程序的程序集中时,必须将全局资源放在元素级别或主题级别。 这两种方法都各有优点。

在元素级别定义资源

可以通过创建自定义资源字典并将其与控件的资源字典合并,在元素级别定义共享资源。 采用此方法时,可以为资源文件指定任意名称,并且资源文件可以与控件位于同一文件夹中。 元素级别的资源还可以使用简单字符串作为键。 下面的示例创建一个名为 Dictionary1.xaml 的 LinearGradientBrush 文件。

<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;
}

下面的示例先在一个自定义控件的构造函数中将共享资源与该控件的资源合并,然后再调用 InitializeComponent。 由于 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 中检查资源。 如果在与当前主题对应的文件或 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 属性,前者指定常规资源的位置,后者指定特定于主题的资源的位置。

下面的示例将 GenericDictionaryLocationThemeDictionaryLocation 属性设置为 SourceAssembly,以指定常规资源和特定于主题的资源与控件位于同一程序集中。

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
           ResourceDictionaryLocation.SourceAssembly)]
<Assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)>

另请参阅