控件创作概述Control Authoring Overview

Windows Presentation Foundation (WPF)Windows Presentation Foundation (WPF) 控件模型的扩展性极大地减少了创建新控件的需要。The extensibility of the Windows Presentation Foundation (WPF)Windows Presentation Foundation (WPF) control model greatly reduces the need to create a new control. 但在某些情况下,仍可能需要创建自定义控件。However, in certain cases you may still need to create a custom control. 本主题讨论可最大限度减少在 Windows Presentation Foundation (WPF)Windows Presentation Foundation (WPF) 中创建自定义控件以及其他控件创作模型的需要的功能。This topic discusses the features that minimize your need to create a custom control and the different control authoring models in Windows Presentation Foundation (WPF)Windows Presentation Foundation (WPF). 本主题还演示如何创建新控件。This topic also demonstrates how to create a new control.

编写新控件的替代方法Alternatives to Writing a New Control

以前,如果要通过现有控件获取自定义体验,只能更改控件的标准属性,例如背景色、边框宽度和字号。Historically, if you wanted to get a customized experience from an existing control, you were limited to changing the standard properties of the control, such as background color, border width, and font size. 如果希望在这些预定义参数的基础之上扩展控件的外观或行为,则需要创建新控件,常用的方法是继承现有控件并重写负责绘制该控件的方法。If you wished to extend the appearance or behavior of a control beyond these predefined parameters, you would need to create a new control, usually by inheriting from an existing control and overriding the method responsible for drawing the control. 虽然这仍是一种可选方法,但也可以利用 WPFWPF 的丰富内容模型、样式、模板和触发器来自定义现有的控件。Although that is still an option, WPFWPF enables to you customize existing controls by using its rich content model, styles, templates, and triggers. 下表提供了一些示例,演示如何在不创建新控件的情况下使用这些功能来实现一致的自定义体验。The following list gives examples of how these features can be used to create custom and consistent experiences without having to create a new control.

  • 丰富内容。Rich Content. 很多标准 WPFWPF 控件都支持丰富内容。Many of the standard WPFWPF controls support rich content. 例如,Button 的 content 属性的类型为 Object,因此理论上可以在 Button上显示任何内容。For example, the content property of a Button is of type Object, so theoretically anything can be displayed on a Button. 若要让按钮显示图像和文本,可以将图像和 TextBlock 添加到 StackPanel 并将 StackPanel 分配给 Content 属性。To have a button display an image and text, you can add an image and a TextBlock to a StackPanel and assign the StackPanel to the Content property. 由于这些控件可以显示 WPFWPF 视觉元素和任意数据,因此,降低了创建新控件或修改现有控件来支持复杂可视化效果的需求。Because the controls can display WPFWPF visual elements and arbitrary data, there is less need to create a new control or to modify an existing control to support a complex visualization. 有关 WPFWPF中的 Button 和其他内容模型的内容模型的详细信息,请参阅WPF 内容模型For more information about the content model for Button and other content models in WPFWPF, see WPF Content Model.

  • 样式。Styles. Style 是表示控件属性的值的集合。A Style is a collection of values that represent properties for a control. 使用样式可创建所需控件外观和行为的可重用表示形式,而无需编写新控件。By using styles, you can create a reusable representation of a desired control appearance and behavior without writing a new control. 例如,假设您希望所有 TextBlock 控件都具有红色的 Arial 字体,字体大小为14。For example, assume that you want all of your TextBlock controls to have red, Arial font with a font size of 14. 可以创建一个样式作为资源,并相应设置适当属性。You can create a style as a resource and set the appropriate properties accordingly. 然后,添加到应用程序中的每个 TextBlock 都具有相同的外观。Then every TextBlock that you add to your application will have the same appearance.

  • 数据模板。Data Templates. 使用 DataTemplate 可以自定义数据在控件上的显示方式。A DataTemplate enables you to customize how data is displayed on a control. 例如,可以使用 DataTemplate 来指定数据在 ListBox中的显示方式。For example, a DataTemplate can be used to specify how data is displayed in a ListBox. 有关这种情况的示例,请参阅数据模块化概述For an example of this, see Data Templating Overview. 除了自定义数据的外观以外,DataTemplate 可以包括 UI 元素,这为你提供了自定义 Ui 的很大灵活性。In addition to customizing the appearance of data, a DataTemplate can include UI elements, which gives you a lot of flexibility in custom UIs. 例如,通过使用 DataTemplate,可以创建一个 ComboBox,其中每一项都包含一个复选框。For example, by using a DataTemplate, you can create a ComboBox in which each item contains a check box.

  • 控件模板。Control Templates. WPFWPF 中的许多控件都使用 ControlTemplate 来定义控件的结构和外观,将控件的外观与控件的功能分隔开来。Many controls in WPFWPF use a ControlTemplate to define the control's structure and appearance, which separates the appearance of a control from the functionality of the control. 您可以通过重新定义控件的 ControlTemplate来大幅更改控件的外观。You can drastically change the appearance of a control by redefining its ControlTemplate. 例如,假设需要一个看起来像交通信号灯的控件。For example, suppose you want a control that looks like a stoplight. 此控件具有简单的用户界面和功能。This control has a simple user interface and functionality. 该控件有三个圆圈,一次只有一个圆圈亮起。The control is three circles, only one of which can be lit up at a time. 在某些反射后,你可能会意识到 RadioButton 仅提供一次选择一个的功能,但 RadioButton 的默认外观看起来不像是交通信号灯上的灯。After some reflection, you might realize that a RadioButton offers the functionality of only one being selected at a time, but the default appearance of the RadioButton looks nothing like the lights on a stoplight. 由于 RadioButton 使用控件模板来定义其外观,因此,可以轻松地重新定义 ControlTemplate 以满足控件的要求,并使用单选按钮发出停止信号。Because the RadioButton uses a control template to define its appearance, it is easy to redefine the ControlTemplate to fit the requirements of the control, and use radio buttons to make your stoplight.

    备注

    尽管 RadioButton 可以使用 DataTemplate,但在此示例中,DataTemplate 不足。Although a RadioButton can use a DataTemplate, a DataTemplate is not sufficient in this example. DataTemplate 定义控件内容的外观。The DataTemplate defines the appearance of the content of a control. RadioButton的情况下,内容就是显示 RadioButton 是否选中的圆圈右侧显示的任何内容。In the case of a RadioButton, the content is whatever appears to the right of the circle that indicates whether the RadioButton is selected. 在交通信号灯的示例中,单选按钮只需要是可“点亮”的圆圈。In the example of the stoplight, the radio button needs just be a circle that can "light up." 因为交通信号灯的外观要求与 RadioButton默认外观有所不同,所以需要重新定义 ControlTemplateBecause the appearance requirement for the stoplight is so different than the default appearance of the RadioButton, it is necessary to redefine the ControlTemplate. 通常,DataTemplate 用于定义控件的内容(或数据),而 ControlTemplate 用于定义控件的构建方式。In general a DataTemplate is used for defining the content (or data) of a control, and a ControlTemplate is used for defining how a control is structured.

  • 触发器。Triggers. Trigger 允许动态更改控件的外观和行为,而无需创建新的控件。A Trigger allows you to dynamically change the appearance and behavior of a control without creating a new control. 例如,假设应用程序中有多个 ListBox 控件,并希望每个 ListBox 中的项在被选中时均为粗体和红色。For example, suppose you have multiple ListBox controls in your application and want the items in each ListBox to be bold and red when they are selected. 你的第一个 instinct 可能是创建一个继承自 ListBox 的类,并重写 OnSelectionChanged 方法以更改选定项的外观,但更好的方法是将触发器添加到更改所选项外观的 ListBoxItem 的样式中。Your first instinct might be to create a class that inherits from ListBox and override the OnSelectionChanged method to change the appearance of the selected item, but a better approach is to add a trigger to a style of a ListBoxItem that changes the appearance of the selected item. 触发器可以更改属性值或根据属性值执行操作。A trigger enables you to change property values or take actions based on the value of a property. 使用 EventTrigger 可以在发生事件时执行操作。An EventTrigger enables you to take actions when an event occurs.

有关样式、模板和触发器的详细信息,请参阅样式设置和模板化For more information about styles, templates, and triggers, see Styling and Templating.

一般情况下,如果控件可以镜像现有控件的功能,但希望该控件具有不同的外观,则应先考虑是否可以使用本节中讨论的某些方法来更改现有控件的外观。In general, if your control mirrors the functionality of an existing control, but you want the control to look different, you should first consider whether you can use any of the methods discussed in this section to change the existing control's appearance.

控件创作模型Models for Control Authoring

通过丰富内容模型、样式、模板和触发器,最大程度地减少创建新控件的需要。The rich content model, styles, templates, and triggers minimize the need for you to create a new control. 但是,如果确实需要创建新控件,则理解 WPFWPF 中的不同控件创作模型就显得非常重要。However, if you do need to create a new control, it is important to understand the different control authoring models in WPFWPF. WPFWPF 提供三个用于创建控件的常规模型,每个模型都提供不同的功能集和灵活度。provides three general models for creating a control, each of which provides a different set of features and level of flexibility. 这三个模型的基类 UserControlControlFrameworkElementThe base classes for the three models are UserControl, Control, and FrameworkElement.

从 UserControl 派生Deriving from UserControl

WPFWPF 中创建控件的最简单方法是从 UserControl派生。The simplest way to create a control in WPFWPF is to derive from UserControl. 生成从 UserControl继承的控件时,可以将现有组件添加到 UserControl,为组件命名,并在 可扩展应用程序标记语言 (XAML)Extensible Application Markup Language (XAML)中引用事件处理程序。When you build a control that inherits from UserControl, you add existing components to the UserControl, name the components, and reference event handlers in 可扩展应用程序标记语言 (XAML)Extensible Application Markup Language (XAML). 随后可以在代码中引用命名的元素和定义事件处理程序。You can then reference the named elements and define the event handlers in code. 此开发模型与用于在 WPFWPF 中开发应用程序的模型非常相似。This development model is very similar to the model used for application development in WPFWPF.

如果已正确生成,UserControl 可以利用丰富内容、样式和触发器的好处。If built correctly, a UserControl can take advantage of the benefits of rich content, styles, and triggers. 但是,如果控件继承自 UserControl,则使用控件的用户将无法使用 DataTemplateControlTemplate 自定义其外观。However, if your control inherits from UserControl, people who use your control will not be able to use a DataTemplate or ControlTemplate to customize its appearance. 需要从 Control 类或它的派生类之一(而不是 UserControl)派生,以创建支持模板的自定义控件。It is necessary to derive from the Control class or one of its derived classes (other than UserControl) to create a custom control that supports templates.

从 UserControl 派生的优点Benefits of Deriving from UserControl

如果满足以下所有条件,请考虑从 UserControl 派生:Consider deriving from UserControl if all of the following apply:

  • 希望采用与生成应用程序相似的方法生成控件。You want to build your control similarly to how you build an application.

  • 控件仅包含现有组件。Your control consists only of existing components.

  • 无需支持复杂的自定义项。You don't need to support complex customization.

从 Control 派生Deriving from Control

Control 类派生是大多数现有 WPFWPF 控件所使用的模型。Deriving from the Control class is the model used by most of the existing WPFWPF controls. 创建从 Control 类继承的控件时,可以使用模板定义其外观。When you create a control that inherits from the Control class, you define its appearance by using templates. 通过这种方式,可以将运算逻辑从视觉表示形式中分离出来。By doing so, you separate the operational logic from the visual representation. 你还可以通过使用命令和绑定而不是事件,并尽可能避免引用 ControlTemplate 中的元素,从而确保分离 UI 和逻辑。You can also ensure the decoupling of the UI and logic by using commands and bindings instead of events and avoiding referencing elements in the ControlTemplate whenever possible. 如果控件的 UI 和逻辑正确分离,则控件的用户可以重新定义控件的 ControlTemplate 以自定义其外观。If the UI and logic of your control are properly decoupled, a user of your control can redefine the control's ControlTemplate to customize its appearance. 尽管构建自定义 Control 并不像构建 UserControl那样简单,但自定义 Control 可提供最大的灵活性。Although building a custom Control is not as simple as building a UserControl, a custom Control provides the most flexibility.

从 Control 派生的优点Benefits of Deriving from Control

如果满足以下任一条件,请考虑从 Control 派生,而不是使用 UserControl 类:Consider deriving from Control instead of using the UserControl class if any of the following apply:

  • 希望控件的外观可通过 ControlTemplate进行自定义。You want the appearance of your control to be customizable via the ControlTemplate.

  • 希望控件支持不同的主题。You want your control to support different themes.

从 FrameworkElement 派生Deriving from FrameworkElement

派生自 UserControlControl 的控件依赖于组合现有元素。Controls that derive from UserControl or Control rely upon composing existing elements. 在许多情况下,这是一个可接受的解决方案,因为从 FrameworkElement 继承的任何对象都可以在 ControlTemplate中。For many scenarios, this is an acceptable solution, because any object that inherits from FrameworkElement can be in a ControlTemplate. 但是,某些时候,简单的元素组合不能满足控件的外观需要。However, there are times when a control's appearance requires more than the functionality of simple element composition. 对于这些方案,在 FrameworkElement 上以组件为基础是正确的选择。For these scenarios, basing a component on FrameworkElement is the right choice.

可以通过两种标准方法来生成基于 FrameworkElement的组件:直接呈现和自定义元素组合。There are two standard methods for building FrameworkElement-based components: direct rendering and custom element composition. 直接呈现涉及重写 FrameworkElementOnRender 方法,并提供显式定义组件视觉对象的 DrawingContext 操作。Direct rendering involves overriding the OnRender method of FrameworkElement and providing DrawingContext operations that explicitly define the component visuals. 这是 ImageBorder使用的方法。This is the method used by Image and Border. 自定义元素组合涉及使用 Visual 类型的对象来编写组件的外观。Custom element composition involves using objects of type Visual to compose the appearance of your component. 有关示例,请参阅使用 DrawingVisual 对象For an example, see Using DrawingVisual Objects. TrackWPFWPF 中使用自定义元素组合的控件的一个示例。Track is an example of a control in WPFWPF that uses custom element composition. 在同一控件中,也可以混合使用直接呈现和自定义元素组合。It is also possible to mix direct rendering and custom element composition in the same control.

从 FrameworkElement 派生的优点Benefits of Deriving from FrameworkElement

如果满足以下任一条件,请考虑从 FrameworkElement 派生:Consider deriving from FrameworkElement if any of the following apply:

  • 希望对控件的外观进行精确控制,而不仅仅是简单的元素组合提供的效果。You want to have precise control over the appearance of your control beyond what is provided by simple element composition.

  • 希望通过定义自己的呈现逻辑来定义控件的外观。You want to define the appearance of your control by defining your own render logic.

  • 你想要以 novel 的方式撰写现有元素,这超出了 UserControlControl所能实现的范围。You want to compose existing elements in novel ways that go beyond what is possible with UserControl and Control.

控件创作基础知识Control Authoring Basics

如前所述,WPFWPF 的最强大功能之一在于,它能够在不需要创建自定义控件的情况下,不只是通过设置控件的基本属性来更改其外观和行为。As discussed earlier, one of the most powerful features of WPFWPF is the ability to go beyond setting basic properties of a control to change its appearance and behavior, yet still not needing to create a custom control. 样式设置、数据绑定和触发器功能通过 WPFWPF 属性系统和 WPFWPF 事件系统实现。The styling, data binding, and trigger features are made possible by the WPFWPF property system and the WPFWPF event system. 以下各部分介绍应遵循的一些做法(与创建自定义控件时所用的模型无关),以便自定义控件的用户可以像使用 WPFWPF 附带的控件一样使用这些功能。The following sections describe some practices that you should follow, regardless of the model you use to create the custom control, so that users of your custom control can use these features just as they would for a control that is included with WPFWPF.

使用依赖属性Use Dependency Properties

当属性为依赖属性时,可以执行以下操作:When a property is a dependency property, it is possible to do the following:

  • 在样式中设置该属性。Set the property in a style.

  • 将该属性绑定到数据源。Bind the property to a data source.

  • 使用动态资源作为该属性的值。Use a dynamic resource as the property's value.

  • 对该属性进行动画处理。Animate the property.

如果希望控件的属性支持以上任一功能,应将该属性实现为依赖属性。If you want a property of your control to support any of this functionality, you should implement it as a dependency property. 下面的示例通过执行以下操作定义一个名为 Value 的依赖属性:The following example defines a dependency property named Value by doing the following:

  • 将名为 ValuePropertyDependencyProperty 标识符定义为 public static readonly 字段。Define a DependencyProperty identifier named ValueProperty as a public static readonly field.

  • 通过调用 DependencyProperty.Register将属性名称注册到属性系统,以指定以下各项:Register the property name with the property system, by calling DependencyProperty.Register, to specify the following:

  • 通过实现属性的 getset 访问器,定义一个名为 Value的 CLR 包装属性,该属性与用于注册依赖属性的名称相同。Define a CLR wrapper property named Value, which is the same name that is used to register the dependency property, by implementing the property's get and set accessors. 请注意,getset 访问器只能分别调用 GetValueSetValueNote that the get and set accessors only call GetValue and SetValue respectively. 建议依赖属性的访问器不包含其他逻辑,因为客户端和 WPFWPF 可以绕过访问器并直接调用 GetValueSetValueIt is recommended that the accessors of dependency properties not contain additional logic because clients and WPFWPF can bypass the accessors and call GetValue and SetValue directly. 例如,如果属性绑定到数据源,则不会调用该属性的 set 访问器。For example, when a property is bound to a data source, the property's set accessor is not called. 不要将其他逻辑添加到 get 和 set 访问器,而是使用 ValidateValueCallbackCoerceValueCallbackPropertyChangedCallback 委托来响应或在更改时检查该值。Instead of adding additional logic to the get and set accessors, use the ValidateValueCallback, CoerceValueCallback, and PropertyChangedCallback delegates to respond to or check the value when it changes. 有关这些回叫的详细信息,请参阅依赖属性回叫和验证For more information on these callbacks, see Dependency Property Callbacks and Validation.

  • 定义名为 CoerceValueCoerceValueCallback 的方法。Define a method for the CoerceValueCallback named CoerceValue. CoerceValue 确保 Value 大于或等于 MinValue 且小于或等于 MaxValueCoerceValue ensures that Value is greater or equal to MinValue and less than or equal to MaxValue.

  • 定义名为 OnValueChangedPropertyChangedCallback的方法。Define a method for the PropertyChangedCallback, named OnValueChanged. OnValueChanged 创建一个 RoutedPropertyChangedEventArgs<T> 对象,并准备引发 ValueChanged 路由事件。OnValueChanged creates a RoutedPropertyChangedEventArgs<T> object and prepares to raise the ValueChanged routed event. 路由事件在下一节中讨论。Routed events are discussed in the next section.

       /// <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

有关详细信息,请参阅自定义依赖属性For more information, see Custom Dependency Properties.

使用路由事件Use Routed Events

正如依赖属性扩展具有其他功能的 CLR 属性的概念一样,路由事件扩展了标准 CLR 事件的概念。Just as dependency properties extend the notion of CLR properties with additional functionality, routed events extend the notion of standard CLR events. 在创建新的 WPFWPF 控件时,将事件实现为路由事件也是一种好方法,因为路由事件支持以下行为:When you create a new WPFWPF control, it is also good practice to implement your event as a routed event because a routed event supports the following behavior:

  • 事件可以在多个控件的父级上进行处理。Events can be handled on a parent of multiple controls. 如果事件是浮升事件,元素树中的单个父级可订阅该事件。If an event is a bubbling event, a single parent in the element tree can subscribe to the event. 然后,应用程序作者可以使用一个处理程序来响应多个控件的该事件。Then application authors can use one handler to respond to the event of multiple controls. 例如,如果您的控件是 ListBox 中每一项的一部分(因为它包含在 DataTemplate中),则应用程序开发人员可以在 ListBox上定义控件事件的事件处理程序。For example, if your control is a part of each item in a ListBox (because it is included in a DataTemplate), the application developer can define the event handler for your control's event on the ListBox. 每当其中任何控件发生该事件时,都会调用该事件处理程序。Whenever the event occurs on any of the controls, the event handler is called.

  • 可以在 EventSetter中使用路由事件,使应用程序开发人员能够在样式中指定事件的处理程序。Routed events can be used in an EventSetter, which enables application developers to specify the handler of an event within a style.

  • 可以在 EventTrigger中使用路由事件,这对于使用 XAMLXAML对属性进行动画处理很有用。Routed events can be used in an EventTrigger, which is useful for animating properties by using XAMLXAML. 有关详细信息,请参阅 动画概述For more information, see Animation Overview.

下面的示例通过执行以下操作定义了一个路由事件:The following example defines a routed event by doing the following:

  • 将名为 ValueChangedEventRoutedEvent 标识符定义为 public static readonly 字段。Define a RoutedEvent identifier named ValueChangedEvent as a public static readonly field.

  • 通过调用 EventManager.RegisterRoutedEvent 方法来注册路由事件。Register the routed event by calling the EventManager.RegisterRoutedEvent method. 该示例在调用 RegisterRoutedEvent时指定以下信息:The example specifies the following information when it calls RegisterRoutedEvent:

    • 事件名称是 ValueChangedThe name of the event is ValueChanged.

    • 路由策略是 Bubble的,这意味着将首先调用源(引发事件的对象)上的事件处理程序,然后连续调用源的父元素上的事件处理程序,并从最接近的父元素。The routing strategy is Bubble, which means that an event handler on the source (the object that raises the event) is called first, and then event handlers on the source's parent elements are called in succession, starting with the event handler on the closest parent element.

    • 事件处理程序的类型是 RoutedPropertyChangedEventHandler<T>的,使用 Decimal 类型构造。The type of the event handler is RoutedPropertyChangedEventHandler<T>, constructed with a Decimal type.

    • 该事件的所属类型为 NumericUpDownThe owning type of the event is NumericUpDown.

  • 声明一个名为 ValueChanged 的公共事件,并包含事件访问器声明。Declare a public event named ValueChanged and includes event-accessor declarations. 该示例调用 add 访问器声明中的 AddHandler,并将 remove 访问器声明中的 RemoveHandler 用于使用 WPFWPF 事件服务。The example calls AddHandler in the add accessor declaration and RemoveHandler in the remove accessor declaration to use the WPFWPF event services.

  • 创建一个名为 OnValueChanged 的受保护的虚拟方法,该方法会引发 ValueChanged 事件。Create a protected, virtual method named OnValueChanged that raises the ValueChanged event.

/// <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

有关详细信息,请参阅路由事件概述创建自定义路由事件For more information, see Routed Events Overview and Create a Custom Routed Event.

使用绑定Use Binding

若要将控件的 UI 与其逻辑分离,请考虑使用数据绑定。To decouple the UI of your control from its logic, consider using data binding. 如果使用 ControlTemplate定义控件的外观,则这一点特别重要。This is particularly important if you define the appearance of your control by using a ControlTemplate. 使用数据绑定时,或许可以避免在代码中引用 UI 的特定部分。When you use data binding, you might be able to eliminate the need to reference specific parts of the UI from the code. 最好避免引用 ControlTemplate 中的元素,因为当代码引用 ControlTemplate 中的元素并且更改 ControlTemplate 时,所引用的元素需要包含在新的 ControlTemplate中。It's a good idea to avoid referencing elements that are in the ControlTemplate because when the code references elements that are in the ControlTemplate and the ControlTemplate is changed, the referenced element needs to be included in the new ControlTemplate.

下面的示例更新 NumericUpDown 控件的 TextBlock,并为其指定名称并按名称在代码中引用 textbox。The following example updates the TextBlock of the NumericUpDown control, assigning a name to it and referencing the textbox by name in code.

<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

下面的示例使用绑定来达到相同的目的。The following example uses binding to accomplish the same thing.

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

有关数据绑定的详细信息,请参阅数据绑定概述For more information about data binding, see Data Binding Overview.

设计器的设计Design for Designers

若要在 Visual Studio 的 WPF 设计器中接收自定义 WPF 控件的支持(例如,用属性窗口的属性编辑),请遵循以下准则。To receive support for custom WPF controls in the WPF Designer for Visual Studio (for example, property editing with the Properties window), follow these guidelines. 有关开发 WPF 设计器的详细信息,请参阅在 Visual Studio 中设计 XAMLFor more information on developing for the WPF Designer, see Design XAML in Visual Studio.

依赖项属性Dependency Properties

请确保按照前面所述的 "使用依赖属性" 中所述,实现 CLR getset 访问器。Be sure to implement CLR get and set accessors as described earlier, in "Use Dependency Properties." 设计器可以使用包装器来检测某个依赖属性是否存在,但与 WPFWPF 和控件客户端一样,在获取或设置属性时不需要使用设计器来调用访问器。Designers may use the wrapper to detect the presence of a dependency property, but they, like WPFWPF and clients of the control, are not required to call the accessors when getting or setting the property.

附加属性Attached Properties

应使用以下准则在自定义控件上实现附加属性:You should implement attached properties on custom controls using the following guidelines:

  • 具有一个 public static readonly DependencyProperty 使用Property 方法创建的窗体类型名称RegisterAttachedHave a public static readonly DependencyProperty of the form PropertyNameProperty that was creating using the RegisterAttached method. 传递给 RegisterAttached 的属性名称必须与PropertyName匹配。The property name that is passed to RegisterAttached must match PropertyName.

  • 实现一对名为Set属性名称Get属性名称public static CLR 方法。Implement a pair of public static CLR methods named SetPropertyName and GetPropertyName. 两种方法都应接受从 DependencyProperty 派生的类作为其第一个参数。Both methods should accept a class derived from DependencyProperty as their first argument. Set属性名称方法还接受其类型与属性的注册数据类型匹配的参数。The SetPropertyName method also accepts an argument whose type matches the registered data type for the property. Get属性名称 方法应返回相同类型的值。The GetPropertyName method should return a value of the same type. 如果缺少 Set属性名称方法,则该属性标记为只读。If the SetPropertyName method is missing, the property is marked read-only.

  • Set propertynameGetpropertyname必须分别路由到目标依赖对象上的 GetValueSetValue 方法。Set PropertyName and GetPropertyName must route directly to the GetValue and SetValue methods on the target dependency object, respectively. 通过调用方法包装器或直接调用目标依赖对象,设计器可以访问附加属性。Designers may access the attached property by calling through the method wrapper or making a direct call to the target dependency object.

有关附加属性的详细信息,请参阅附加属性概述For more information on attached properties, see Attached Properties Overview.

定义和使用共享资源Define and Use Shared Resources

可以将控件包含在应用程序所在的程序集中,也可以将控件打包到可在多个应用程序中使用的单独程序集中。You can include your control in the same assembly as your application, or you can package your control in a separate assembly that can be used in multiple applications. 大多数情况下,不管使用什么方法,本主题中讨论的信息都适用。For the most part, the information discussed in this topic applies regardless of the method you use. 但有一处差异值得注意。There is one difference worth noting, however. 将控件放入应用程序所在的程序集中时,可以随意向 App.xaml 文件添加全局资源。When you put a control in the same assembly as an application, you are free to add global resources to the App.xaml file. 但仅包含控件的程序集没有关联的 Application 对象,因此,app.xaml 文件不可用。But an assembly that contains only controls does not have an Application object associated with it, so an App.xaml file is not available.

当应用程序查找资源时,它会按以下顺序在三个级别进行查找:When an application looks for a resource, it looks at three levels in the following order:

  1. 元素级别。The element level.

    系统从引用该资源的元素开始搜索,接着搜索逻辑父级的资源,依此类推,直至到达根元素。The system starts with the element that references the resource and then searches resources of the logical parent and so forth until the root element is reached.

  2. 应用程序级别。The application level.

    Application 对象定义的资源。Resources defined by the Application object.

  3. 主题级别。The theme level.

    主题级别的字典存储在名为“Themes”的子文件夹中。Theme-level dictionaries are stored in a subfolder named Themes. Themes 文件夹中的文件与主题对应。The files in the Themes folder correspond to themes. 例如,可能有 Aero.NormalColor.xaml、Luna.NormalColor.xaml、Royale.NormalColor.xaml 等。For example, you might have Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml, and so on. 可能还有一个名为 generic.xaml 的文件。You can also have a file named generic.xaml. 当系统在主题级别查找资源时,它会先在特定于主题的文件中查找相应资源,然后在 generic.xaml 中进行查找。When the system looks for a resource at the themes level, it first looks for it in the theme-specific file and then looks for it in generic.xaml.

当控件位于独立于应用程序的程序集中时,必须将全局资源放在元素级别或主题级别。When your control is in an assembly that is separate from the application, you must put your global resources at the element level or at the theme level. 这两种方法都各有优点。Both methods have their advantages.

在元素级别定义资源Defining Resources at the Element Level

可以通过创建自定义资源字典并将其与控件的资源字典合并,在元素级别定义共享资源。You can define shared resources at the element level by creating a custom resource dictionary and merging it with your control’s resource dictionary. 采用此方法时,可以为资源文件指定任意名称,并且资源文件可以与控件位于同一文件夹中。When you use this method, you can name your resource file anything you want, and it can be in the same folder as your controls. 元素级别的资源还可以使用简单字符串作为键。Resources at the element level can also use simple strings as keys. 下面的示例创建一个名为 Dictionary1 的 LinearGradientBrush 资源文件。The following example creates a LinearGradientBrush resource file named 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>

定义字典后,需要将其与控件的资源字典合并。Once you have defined your dictionary, you need to merge it with your control's resource dictionary. 可以使用 XAMLXAML 或代码执行此操作。You can do this by using XAMLXAML or code.

下面的示例通过使用 XAMLXAML 合并资源字典。The following example merges a resource dictionary by using XAMLXAML.

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

此方法的缺点是每次引用 ResourceDictionary 对象时都会创建一个。The disadvantage to this approach is that a ResourceDictionary object is created each time you reference it. 例如,如果库中有10个自定义控件,并使用 XAML 合并每个控件的共享资源字典,则可以创建10个完全相同的 ResourceDictionary 对象。For example, if you have 10 custom controls in your library and merge the shared resource dictionaries for each control by using XAML, you create 10 identical ResourceDictionary objects. 可以通过创建一个在代码中合并资源的静态类并返回生成的 ResourceDictionary来避免这种情况。You can avoid this by creating a static class that merges the resources in code and returns the resulting ResourceDictionary.

下面的示例创建一个返回共享 ResourceDictionary的类。The following example creates a class that returns a shared 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;
}

下面的示例先在一个自定义控件的构造函数中将共享资源与该控件的资源合并,然后再调用 InitializeComponentThe following example merges the shared resource with the resources of a custom control in the control's constructor before it calls InitializeComponent. 由于 SharedDictionaryManager.SharedDictionary 是静态属性,因此 ResourceDictionary 只创建一次。Because the SharedDictionaryManager.SharedDictionary is a static property, the ResourceDictionary is created only once. 因为资源字典在调用 InitializeComponent 前已合并,所以控件可以在它的 XAMLXAML 文件中使用资源。Because the resource dictionary was merged before InitializeComponent was called, the resources are available to the control in its XAMLXAML file.

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();

}

在主题级别定义资源Defining Resources at the Theme Level

通过 WPFWPF 可以为不同的 Windows 主题创建资源。WPFWPF enables you to create resources for different Windows themes. 作为控件作者,可以为特定主题定义资源,以根据所使用的主题更改控件的外观。As a control author, you can define a resource for a specific theme to change your control's appearance depending on what theme is in use. 例如,Windows 经典主题中 Button 的外观(Windows 2000 的默认主题)不同于 Windows Luna 主题中的 Button (Windows XP 的默认主题),因为 Button 对每个主题使用不同的 ControlTemplateFor example, the appearance of a Button in the Windows Classic theme (the default theme for Windows 2000) differs from a Button in the Windows Luna theme (the default theme for Windows XP) because the Button uses a different ControlTemplate for each theme.

特定于主题的资源以特定文件名保留在资源字典中。Resources that are specific to a theme are kept in a resource dictionary with a specific file name. 这些文件必须位于一个名为 Themes 的文件夹中,此文件夹是包含该控件的文件夹的子文件夹。These files must be in a folder named Themes that is a subfolder of the folder that contains the control. 下表列出了资源字典文件以及与每个文件关联的主题:The following table lists the resource dictionary files and the theme that is associated with each file:

资源字典文件名Resource dictionary file name Windows 主题Windows theme
Classic.xaml Windows XP 中的经典 Windows 9x/2000 外观Classic Windows 9x/2000 look on Windows XP
Luna.NormalColor.xaml Windows XP 上的默认蓝色主题Default blue theme on Windows XP
Luna.Homestead.xaml Windows XP 上的橄榄色主题Olive theme on Windows XP
Luna.Metallic.xaml Windows XP 上的银色主题Silver theme on Windows XP
Royale.NormalColor.xaml Windows XP Media Center Edition 上的默认主题Default theme on Windows XP Media Center Edition
Aero.NormalColor.xaml Windows Vista 上的默认主题Default theme on Windows Vista

无需为每一种主题都定义资源。You do not need to define a resource for every theme. 如果没有为特定主题定义资源,控件将在 Classic.xaml 中检查资源。If a resource is not defined for a specific theme, then the control checks Classic.xaml for the resource. 如果在与当前主题对应的文件或 Classic.xaml 中没有定义资源,控件将使用常规资源,该资源位于名为 generic.xaml 的资源字典文件中。If the resource is not defined in the file that corresponds to the current theme or in Classic.xaml, the control uses the generic resource, which is in a resource dictionary file named generic.xaml. generic.xaml 文件与特定于主题的资源词典文件位于同一文件夹中。The generic.xaml file is located in the same folder as the theme-specific resource dictionary files. 尽管 generic.xaml 不与特定的 Windows 主题对应,但它仍是一个主题级别字典。Although generic.xaml does not correspond to a specific Windows theme, it is still a theme-level dictionary.

C#Visual Basic NumericUpDown 自定义控件与主题和 UI 自动化支持示例包含两个用于 NumericUpDown 控件的资源字典:一个在泛型中,另一个在 luna.normalcolor.xaml 中。The C# or Visual Basic NumericUpDown custom control with theme and UI automation support sample contains two resource dictionaries for the NumericUpDown control: one is in generic.xaml, and the other is in Luna.NormalColor.xaml.

ControlTemplate 放入任何特定于主题的资源字典文件中时,必须为控件创建一个静态构造函数,并对 DefaultStyleKey调用 OverrideMetadata(Type, PropertyMetadata) 方法,如以下示例中所示。When you put a ControlTemplate in any of the theme-specific resource dictionary files, you must create a static constructor for your control and call the OverrideMetadata(Type, PropertyMetadata) method on the DefaultStyleKey, as shown in the following example.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Shared Sub New()
    DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
定义和引用主题资源的键Defining and Referencing Keys for Theme Resources

在元素级别定义资源时,可以指定一个字符串作为它的键,然后通过该字符串访问该资源。When you define a resource at the element level, you can assign a string as its key and access the resource via the string. 在主题级别定义资源时,必须使用 ComponentResourceKey 作为密钥。When you define a resource at the theme level, you must use a ComponentResourceKey as the key. 以下示例定义 generic.xaml 中的资源。The following example defines a resource in 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 指定为键来引用资源。The following example references the resource by specifying the ComponentResourceKey as the key.

<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>
指定主题资源的位置Specifying the Location of Theme Resources

若要找到控件的资源,承载应用程序需要知道相应程序集包含特定于控件的资源。To find the resources for a control, the hosting application needs to know that the assembly contains control-specific resources. 可以通过将 ThemeInfoAttribute 添加到包含控件的程序集来完成此目的。You can accomplish that by adding the ThemeInfoAttribute to the assembly that contains the control. ThemeInfoAttribute 具有 GenericDictionaryLocation 属性,该属性指定泛型资源的位置,而 ThemeDictionaryLocation 属性指定特定于主题的资源的位置。The ThemeInfoAttribute has a GenericDictionaryLocation property that specifies the location of generic resources, and a ThemeDictionaryLocation property that specifies the location of the theme-specific resources.

下面的示例将 GenericDictionaryLocationThemeDictionaryLocation 属性设置为 SourceAssembly,以指定泛型和特定于主题的资源与控件位于同一程序集中。The following example sets the GenericDictionaryLocation and ThemeDictionaryLocation properties to SourceAssembly, to specify that the generic and theme-specific resources are in the same assembly as the control.

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

请参阅See also