创建具有可自定义外观的控件

Windows Presentation Foundation (WPF) 使您能够创建其外观可自定义的控件。 例如,您可以通过创建新的 ControlTemplate 来更改 CheckBox 的外观,使之超越设置属性所能达到的效果。 下图演示一个使用默认 ControlTemplateCheckBox 和一个使用自定义 ControlTemplateCheckBox

一个使用默认控件模板的 CheckBox

一个具有默认控件模板的复选框。

一个使用自定义控件模板的 CheckBox

一个具有自定义控件模板的复选框。

如果您在创建控件时遵循部件和状态模型,则控件的外观将是可自定义的。 诸如 Microsoft Expression Blend 的设计器工具支持部件和状态模型,因此,当您遵循此模型时,在这些类型的应用程序中,您的控件将是可自定义的。 本主题讨论部件和状态模型,以及您在创建自己的控件时如何遵循该模型。 本主题使用一个自定义控件示例 NumericUpDown 来说明此模型的原理。 NumericUpDown 控件显示一个数值,用户可通过单击控件的按钮来增加或减少该数值。 下图演示了在本主题中讨论的 NumericUpDown 控件。

自定义 NumericUpDown 控件

NumericUpDown 自定义控件。

本主题包含以下各节:

  • 系统必备组件

  • 部件和状态模型

  • 在 ControlTemplate 中定义控件的可视结构和可视行为

  • 在代码中使用 ControlTemplate 的部件

  • 提供控件协定

  • 完整的示例

系统必备组件

本主题假设您了解如何为现有控件创建新的 ControlTemplate,熟悉控件协定中各元素的含义,并且了解在通过创建 ControlTemplate 自定义现有控件的外观中讨论的概念。

注意注意

若要创建可自定义其外观的控件,您必须创建一个控件,此控件从 Control 类或其一个子类(UserControl 除外)继承。从 UserControl 继承的控件是一个可以快速创建的控件,但它不使用 ControlTemplate,因此您无法自定义其外观。

部件和状态模型

部件和状态模型指定如何定义控件的可视结构和可视行为。 若要遵循部件和状态模型,应执行以下操作:

  • 在控件的 ControlTemplate 中定义可视结构和可视行为。

  • 当控件的逻辑与控件模板的部件交互时,请遵循某些最佳做法。

  • 提供控件协定,以指定应在 ControlTemplate 中包含哪些内容。

当您在控件的 ControlTemplate 中定义可视结构和可视行为时,应用程序作者可以通过创建新的 ControlTemplate 而不是编写代码来更改控件的可视结构和可视行为。 您必须提供控件协定,以告知应用程序作者应在 ControlTemplate 中定义哪些 FrameworkElement 对象和状态。 当您与 ControlTemplate 中的部件进行交互时,应遵循某些最佳做法,以便控件正确地处理不完整的 ControlTemplate。 如果您遵循这三个原则,则应用程序作者将能够像对随 WPF 提供的控件一样,轻松地为您的控件创建 ControlTemplate。 下面一节详细地说明其中的每条建议。

在 ControlTemplate 中定义控件的可视结构和可视行为

当您使用部件和状态模型创建自定义控件时,可以在其 ControlTemplate 中(而不是在其逻辑中)定义控件的可视结构和可视行为。 控件的可视结构是构成控件的 FrameworkElement 对象的组合。 可视行为是当控件处于特定状态时所显示的方式。 有关创建用于指定控件的可视结构和可视行为的 ControlTemplate 的更多信息,请参见通过创建 ControlTemplate 自定义现有控件的外观

在 NumericUpDown 控件的示例中,可视结构包括两个 RepeatButton 控件和一个 TextBlock。 如果您在 NumericUpDown 控件的代码(例如,其构造函数)中添加这些控件,则这些控件的位置将不可改变。 请勿在控件的代码中定义其可视结构和可视行为,而应在 ControlTemplate 中定义这些内容。 然后,应用程序开发人员可以自定义按钮和 TextBlock 的位置,并指定当 Value 为负时所发生的行为(因为可以替换 ControlTemplate)。

下面的示例演示 NumericUpDown 控件的可视结构,它包括一个可增加 Value 的 RepeatButton、一个可减小 Value 的 RepeatButton 和一个可显示 Value 的 TextBlock

<ControlTemplate TargetType="src:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
              Margin="7,2,2,2" Grid.RowSpan="2" 
              Background="#E0FFFFFF"
              VerticalAlignment="Center" 
              HorizontalAlignment="Stretch">

        <!--Bind the TextBlock to the Value property-->
        <TextBlock Name="TextBlock"
                   Width="60" TextAlignment="Right" Padding="5"
                   Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                     AncestorType={x:Type src:NumericUpDown}}, 
                     Path=Value}"/>
      </Border>

      <RepeatButton Content="Up" Margin="2,5,5,0"
        Name="UpButton"
        Grid.Column="1" Grid.Row="0"/>
      <RepeatButton Content="Down" Margin="2,0,5,5"
        Name="DownButton"
        Grid.Column="1" Grid.Row="1"/>

      <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
        Stroke="Black" StrokeThickness="1"  
        Visibility="Collapsed"/>
    </Grid>

  </Grid>
</ControlTemplate>

NumericUpDown 控件的可视行为是:如果值为负,则值为红色字体。 如果您当 Value 为负时在代码中更改 TextBlockForeground,则 NumericUpDown 将始终显示红色的负值。 您可以通过将 VisualState 对象添加到 ControlTemplate 中,在 ControlTemplate 中指定控件的可视行为。 下面的示例演示 Positive 和 Negative 状态的 VisualState 对象。 Positive 和 Negative 是互斥的(控件始终处于这两种状态之一),因此,该示例将 VisualState 对象放到单个 VisualStateGroup 中。 当控件进入 Negative 状态时,TextBlockForeground 将变为红色。 当控件处于 Positive 状态时,Foreground 将返回到其原始值。 将在通过创建 ControlTemplate 自定义现有控件的外观中进一步讨论如何在 ControlTemplate 中定义 VisualState 对象。

注意注意

请务必设置 ControlTemplate 的根 FrameworkElement 上的 VisualStateManager.VisualStateGroups 附加属性。

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

在代码中使用 ControlTemplate 的部件

ControlTemplate 作者可能会故意或错误地遗漏 FrameworkElementVisualState 对象,但控件的逻辑可能需要这些部件才能正常运行。 部件和状态模型指定您的控件应能够处理缺少 FrameworkElementVisualState 对象的 ControlTemplate。 如果 ControlTemplate 中缺少 FrameworkElementVisualStateVisualStateGroup,则控件不应引发异常或报告错误。 本节描述了与 FrameworkElement 对象交互以及管理状态的建议做法。

预计缺少的 FrameworkElement 对象

当您在 ControlTemplate 中定义 FrameworkElement 对象时,控件的逻辑可能需要与其中某些对象进行交互。 例如,NumericUpDown 控件订阅按钮的 Click 事件以增加或减少 Value,并将 TextBlockText 属性设置为 Value。 如果自定义 ControlTemplate 遗漏 TextBlock 或按钮,则控件失去其某些功能是可接受的,但您应确保控件不会导致错误。 例如,如果 ControlTemplate 不包含用于更改 Value 的按钮,则 NumericUpDown 将失去该功能,但使用 ControlTemplate 的应用程序将继续运行。

下面的做法将确保您的控件正确地响应缺少 FrameworkElement 对象的情况:

  1. 为您需要在代码中引用的每个 FrameworkElement 设置 x:Name 特性。

  2. 为您需要交互的每个 FrameworkElement 定义私有属性。

  3. 订阅和取消订阅控件在 FrameworkElement 属性的 set 访问器中处理的任何事件。

  4. 设置您在 OnApplyTemplate 方法的步骤 2 中定义的 FrameworkElement 属性。 这是 ControlTemplate 中的 FrameworkElement 可用于控件的最早方式。 使用 FrameworkElement 的 x:Name 从 ControlTemplate 中获取该元素。

  5. 在访问 FrameworkElement 的各成员之前,检查它是否为 null。 如果它为 null,则不报告错误。

下面的示例演示 NumericUpDown 控件如何按照上面列表中的建议与 FrameworkElement 对象交互。

在用于在 ControlTemplate 中定义 NumericUpDown 控件的可视结构的示例中,增加 Value 的 RepeatButton 已将其 x:Name 特性设置为 UpButton。 下面的示例声明一个称为 UpButtonElement 的属性,它表示在 ControlTemplate 中声明的 RepeatButton。 set 访问器首先取消订阅按钮的 Click(即使 UpDownElement 不为 null),然后它设置该属性,接着它订阅 Click 事件。 在此还为另一个称为 DownButtonElement 的 RepeatButton 定义了属性,但并未显示。

Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal value As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = value

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property
private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}

下面的示例演示 NumericUpDown 控件的 OnApplyTemplate。 该示例使用 GetTemplateChild 方法从 ControlTemplate 获取 FrameworkElement 对象。 请注意,此示例可防止出现 GetTemplateChild 使用非预期类型的指定名称查找 FrameworkElement 的情况。 忽略具有指定的 x:Name 但类型不正确的元素也是一种最佳做法。

Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

通过遵循在前面示例中演示的做法,您可以确保控件将在 ControlTemplate 缺少 FrameworkElement 时继续运行。

使用 VisualStateManager 管理状态

VisualStateManager 可跟踪控件的状态,并执行在状态之间转换所需的逻辑。 当您将 VisualState 对象添加到 ControlTemplate 时,您可以将它们添加到 VisualStateGroup,并将 VisualStateGroup 添加到 VisualStateManager.VisualStateGroups 附加属性,以便 VisualStateManager 可以访问它们。

下面的示例重复前一个用于演示与控件的 Positive 和 Negative 状态对应的 VisualState 对象的示例。 Negative VisualState 中的 StoryboardTextBlockForeground 变为红色。 当 NumericUpDown 控件处于 Negative 状态时,处于 Negative 状态的演示图板将开始。 然后,当控件返回到 Positive 状态时,处于 Negative 状态的 Storyboard 将停止。 Positive VisualState 不需要包含 Storyboard,因为当 Negative 的 Storyboard 停止时,Foreground 将返回到其原始颜色。

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

请注意,会为 TextBlock 提供一个名称,但 TextBlock 不位于 NumericUpDown 的控件协定中,因为控件的逻辑从不引用 TextBlock。 在 ControlTemplate 中引用的元素具有名称,但不需要属于控件协定,因为控件的新 ControlTemplate 可能不需要引用该元素。 例如,为 NumericUpDown 创建新 ControlTemplate 的人员可能决定不通过更改 Foreground 指示 Value 为负。 在这种情况下,代码和 ControlTemplate 都不会按名称引用 TextBlock

控件的逻辑负责更改控件的状态。 下面的示例演示 NumericUpDown 控件调用 GoToState 方法,以在 Value 大于或等于 0 时进入 Positive 状态;而当 Value 小于 0 时进入 Negative 状态。

If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}

GoToState 方法可执行相应地启动和停止演示图板所需的逻辑。 当控件调用 GoToState 以更改其状态时,VisualStateManager 将执行以下操作:

  • 如果控件要进入的 VisualState 具有 Storyboard,演示图板将开始运行。 然后,如果控件要来自的 VisualState 具有 Storyboard,演示图板将结束运行。

  • 如果控件已处于指定的状态,则 GoToState 不执行任何操作并会返回 true。

  • 如果在 control 的 ControlTemplate 中不存在指定的状态,则 GoToState 不执行任何操作并会返回 false。

使用 VisualStateManager 的最佳做法

建议您执行以下操作以保持控件的状态:

  • 使用属性来跟踪其状态。

  • 创建帮助器方法以在状态之间进行转换。

NumericUpDown 控件使用其 Value 属性跟踪它是处于 Positive 还是 Negative 状态。 The NumericUpDown 控件还定义 Focused 和 UnFocused 状态,这些状态可跟踪 IsFocused 属性。 如果使用的状态无法自然对应于控件的属性,则可以定义一个私有属性来跟踪状态。

更新所有状态的单个方法会集中对 VisualStateManager 的调用并使代码易于管理。 下面的示例演示 NumericUpDown 控件的帮助器方法 UpdateStates。 当 Value 大于或等于 0 时,Control 处于 Positive 状态。 当 Value 小于 0 时,控件处于 Negative 状态。 当 IsFocused 为 true 时,控件处于 Focused 状态;否则它处于 Unfocused 状态。 每当控件需要更改其状态时,都可以调用 UpdateStates,这与什么状态发生更改无关。

Private Sub UpdateStates(ByVal useTransitions As Boolean)

    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If IsFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

    End If
End Sub
private void UpdateStates(bool useTransitions)
{
    if (Value >= 0)
    {
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }

    if (IsFocused)
    {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
    }

}

如果您在控件已处于 GoToState 状态时将状态名称传递到该状态,GoToState 将不执行任何操作,因此,您不需要检查控件的当前状态。 例如,如果 Value 从一个负数更改为另一个负数,则 Negative 状态的演示图板将不会中断,用户在控件中将看不到更改。

当您调用 GoToState 时,VisualStateManager 使用 VisualStateGroup 对象来确定要退出哪种状态。 对于在控件的 ControlTemplate 中定义的每个 VisualStateGroup,控件始终处于一种状态;仅当控件从同一个 VisualStateGroup 进入另一个状态时,才离开前一状态。 例如,NumericUpDown 控件的 ControlTemplate 在一个 VisualStateGroup 中定义 Positive 和 Negative VisualState 对象,而在另一个组中定义 Focused 和 Unfocused VisualState 对象。 (您可以查看在本主题的完整的示例一节中定义的 Focused 和 Unfocused VisualState)当控件从 Positive 状态进入 Negative 状态(或相反)时,控件将保持在 Focused 或 Unfocused 状态。

在以下三种典型的情况下,控件的状态可能更改:

下面的示例演示在这些情况下如何更新 NumericUpDown 控件的状态。

您应在 OnApplyTemplate 方法中更新控件的状态,以便当应用 ControlTemplate 时,控件以正确的状态出现。 下面的示例在 OnApplyTemplate 中调用 UpdateStates,以确保控件处于适当的状态。 例如,假设您创建 NumericUpDown 控件,然后将其 Foreground 设置为绿色并将 Value 设置为 -5。 如果在将 ControlTemplate 应用于 NumericUpDown 控件时未调用 UpdateStates,则控件将不处于 Negative 状态,并且值为绿色而非红色。 必须调用 UpdateStates 才能使控件处于 Negative 状态。

Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub
public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

当属性更改时,您通常需要更新控件的状态。 下面的示例演示整个 ValueChangedCallback 方法。 因为当 Value 更改时将调用 ValueChangedCallback,所以,当 Value 从正更改为负(或相反)时,此方法将调用 UpdateStates。 当 Value 更改但仍保持为正或负时调用 UpdateStates 是可接受的,因为在该情况下,控件将不更改状态。

Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = CInt(args.NewValue)

    ' Call UpdateStates because the Value might have caused the
    ' control to change ValueStates.
    ctl.UpdateStates(True)

    ' Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
private static void ValueChangedCallback(DependencyObject obj, 
    DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, 
            newValue));
}

当发生事件时,可能也需要更新状态。 下面的示例演示 NumericUpDown 如何对 Control 调用 UpdateStates 来处理 GotFocus 事件。

Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}

VisualStateManager 可帮助您管理控件的状态。 通过使用 VisualStateManager,您可以确保控件在各状态之间正确地进行转换。 如果您遵循本节介绍的建议来使用 VisualStateManager,则控件的代码将保持可读性和可维护性。

提供控件协定

提供控件协定以便 ControlTemplate 作者将了解在模板中放入哪些内容。 控件协定具有三个元素:

  • 控件的逻辑使用的可视元素。

  • 控件的状态以及每个状态所属的组。

  • 以可视方式影响控件的公共属性。

创建新的 ControlTemplate 的人员需要了解控件的逻辑使用哪些 FrameworkElement 对象、每个对象属于什么类型以及各个对象的名称。 ControlTemplate 作者还需要了解控件可能所处的每种可能状态的名称,以及状态属于哪个 VisualStateGroup

返回到 NumericUpDown 示例,控件预期 ControlTemplate 具有以下 FrameworkElement 对象:

控件可能处于以下状态:

若要指定控件预期的 FrameworkElement 对象,可以使用 TemplatePartAttribute,它指定预期元素的名称和类型。 若要指定控件的可能状态,可以使用 TemplateVisualStateAttribute,它指定状态的名称以及状态所属的 VisualStateGroup。 将 TemplatePartAttributeTemplateVisualStateAttribute 放在控件的类定义中。

影响控件外观的任何公共属性也是控件协定的一部分。

下面的示例为 NumericUpDown 控件指定 FrameworkElement 对象和状态。

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly BackgroundProperty As DependencyProperty
    Public Shared ReadOnly BorderBrushProperty As DependencyProperty
    Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
    Public Shared ReadOnly FontFamilyProperty As DependencyProperty
    Public Shared ReadOnly FontSizeProperty As DependencyProperty
    Public Shared ReadOnly FontStretchProperty As DependencyProperty
    Public Shared ReadOnly FontStyleProperty As DependencyProperty
    Public Shared ReadOnly FontWeightProperty As DependencyProperty
    Public Shared ReadOnly ForegroundProperty As DependencyProperty
    Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
    Public Shared ReadOnly PaddingProperty As DependencyProperty
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty
    Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty


    Private _Background As Brush
    Public Property Background() As Brush
        Get
            Return _Background
        End Get
        Set(ByVal value As Brush)
            _Background = value
        End Set
    End Property

    Private _BorderBrush As Brush
    Public Property BorderBrush() As Brush
        Get
            Return _BorderBrush
        End Get
        Set(ByVal value As Brush)
            _BorderBrush = value
        End Set
    End Property

    Private _BorderThickness As Thickness
    Public Property BorderThickness() As Thickness
        Get
            Return _BorderThickness
        End Get
        Set(ByVal value As Thickness)
            _BorderThickness = value
        End Set
    End Property

    Private _FontFamily As FontFamily
    Public Property FontFamily() As FontFamily
        Get
            Return _FontFamily
        End Get
        Set(ByVal value As FontFamily)
            _FontFamily = value
        End Set
    End Property

    Private _FontSize As Double
    Public Property FontSize() As Double
        Get
            Return _FontSize
        End Get
        Set(ByVal value As Double)
            _FontSize = value
        End Set
    End Property

    Private _FontStretch As FontStretch
    Public Property FontStretch() As FontStretch
        Get
            Return _FontStretch
        End Get
        Set(ByVal value As FontStretch)
            _FontStretch = value
        End Set
    End Property

    Private _FontStyle As FontStyle
    Public Property FontStyle() As FontStyle
        Get
            Return _FontStyle
        End Get
        Set(ByVal value As FontStyle)
            _FontStyle = value
        End Set
    End Property

    Private _FontWeight As FontWeight
    Public Property FontWeight() As FontWeight
        Get
            Return _FontWeight
        End Get
        Set(ByVal value As FontWeight)
            _FontWeight = value
        End Set
    End Property

    Private _Foreground As Brush
    Public Property Foreground() As Brush
        Get
            Return _Foreground
        End Get
        Set(ByVal value As Brush)
            _Foreground = value
        End Set
    End Property

    Private _HorizontalContentAlignment As HorizontalAlignment
    Public Property HorizontalContentAlignment() As HorizontalAlignment
        Get
            Return _HorizontalContentAlignment
        End Get
        Set(ByVal value As HorizontalAlignment)
            _HorizontalContentAlignment = value
        End Set
    End Property

    Private _Padding As Thickness
    Public Property Padding() As Thickness
        Get
            Return _Padding
        End Get
        Set(ByVal value As Thickness)
            _Padding = value
        End Set
    End Property

    Private _TextAlignment As TextAlignment
    Public Property TextAlignment() As TextAlignment
        Get
            Return _TextAlignment
        End Get
        Set(ByVal value As TextAlignment)
            _TextAlignment = value
        End Set
    End Property

    Private _TextDecorations As TextDecorationCollection
    Public Property TextDecorations() As TextDecorationCollection
        Get
            Return _TextDecorations
        End Get
        Set(ByVal value As TextDecorationCollection)
            _TextDecorations = value
        End Set
    End Property

    Private _TextWrapping As TextWrapping
    Public Property TextWrapping() As TextWrapping
        Get
            Return _TextWrapping
        End Get
        Set(ByVal value As TextWrapping)
            _TextWrapping = value
        End Set
    End Property

    Private _VerticalContentAlignment As VerticalAlignment
    Public Property VerticalContentAlignment() As VerticalAlignment
        Get
            Return _VerticalContentAlignment
        End Get
        Set(ByVal value As VerticalAlignment)
            _VerticalContentAlignment = value
        End Set
    End Property
End Class
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}

完整的示例

下面的示例为 NumericUpDown 控件的整个 ControlTemplate

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VSMCustomControl">


  <Style TargetType="{x:Type local:NumericUpDown}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:NumericUpDown">
          <Grid  Margin="3" 
                Background="{TemplateBinding Background}">


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

                <!--Make the Value property red when it is negative.-->
                <VisualState Name="Negative">
                  <Storyboard>
                    <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(Color)"/>
                  </Storyboard>

                </VisualState>

                <!--Return the control to its initial state by
                    return the TextBlock's Foreground to its 
                    original color.-->
                <VisualState Name="Positive"/>
              </VisualStateGroup>

              <VisualStateGroup Name="FocusStates">

                <!--Add a focus rectangle to highlight the entire control
                    when it has focus.-->
                <VisualState Name="Focused">
                  <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual" 
                                                   Storyboard.TargetProperty="Visibility" Duration="0">
                      <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                          <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                      </DiscreteObjectKeyFrame>
                    </ObjectAnimationUsingKeyFrames>
                  </Storyboard>
                </VisualState>

                <!--Return the control to its initial state by
                    hiding the focus rectangle.-->
                <VisualState Name="Unfocused"/>
              </VisualStateGroup>

            </VisualStateManager.VisualStateGroups>

            <Grid>
              <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
              </Grid.ColumnDefinitions>

              <Border BorderThickness="1" BorderBrush="Gray" 
                Margin="7,2,2,2" Grid.RowSpan="2" 
                Background="#E0FFFFFF"
                VerticalAlignment="Center" 
                HorizontalAlignment="Stretch">
                <!--Bind the TextBlock to the Value property-->
                <TextBlock Name="TextBlock"
                  Width="60" TextAlignment="Right" Padding="5"
                  Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                                 AncestorType={x:Type local:NumericUpDown}}, 
                                 Path=Value}"/>
              </Border>

              <RepeatButton Content="Up" Margin="2,5,5,0"
                Name="UpButton"
                Grid.Column="1" Grid.Row="0"/>
              <RepeatButton Content="Down" Margin="2,0,5,5"
                Name="DownButton"
                Grid.Column="1" Grid.Row="1"/>

              <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
                Stroke="Black" StrokeThickness="1"  
                Visibility="Collapsed"/>
            </Grid>

          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

下面的示例演示 NumericUpDown 的逻辑。

Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        Me.IsTabStop = True
    End Sub

    Public Shared ReadOnly ValueProperty As DependencyProperty =
        DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
                          New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                            ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = CInt(args.NewValue)

        ' Call UpdateStates because the Value might have caused the
        ' control to change ValueStates.
        ctl.UpdateStates(True)

        ' Call OnValueChanged to raise the ValueChanged event.
        ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
    End Sub

    Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                                         GetType(ValueChangedEventHandler), GetType(NumericUpDown))

    Public Custom Event ValueChanged As ValueChangedEventHandler

        AddHandler(ByVal value As ValueChangedEventHandler)
            Me.AddHandler(ValueChangedEvent, value)
        End AddHandler

        RemoveHandler(ByVal value As ValueChangedEventHandler)
            Me.RemoveHandler(ValueChangedEvent, value)
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.RaiseEvent(e)
        End RaiseEvent

    End Event


    Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
        ' Raise the ValueChanged event so applications can be alerted
        ' when Value changes.
        MyBase.RaiseEvent(e)
    End Sub


#Region "NUDCode"
    Private Sub UpdateStates(ByVal useTransitions As Boolean)

        If Value >= 0 Then
            VisualStateManager.GoToState(Me, "Positive", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Negative", useTransitions)
        End If

        If IsFocused Then
            VisualStateManager.GoToState(Me, "Focused", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

        UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
        DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

    Private Property DownButtonElement() As RepeatButton
        Get
            Return m_downButtonElement
        End Get

        Set(ByVal value As RepeatButton)

            If m_downButtonElement IsNot Nothing Then
                RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
            m_downButtonElement = value

            If m_downButtonElement IsNot Nothing Then
                AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
        End Set
    End Property

    Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value -= 1
    End Sub

    Private m_upButtonElement As RepeatButton

    Private Property UpButtonElement() As RepeatButton
        Get
            Return m_upButtonElement
        End Get

        Set(ByVal value As RepeatButton)
            If m_upButtonElement IsNot Nothing Then
                RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
            m_upButtonElement = value

            If m_upButtonElement IsNot Nothing Then
                AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
        End Set
    End Property

    Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value += 1
    End Sub

    Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
        MyBase.OnMouseLeftButtonDown(e)
        Focus()
    End Sub


    Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
        MyBase.OnGotFocus(e)
        UpdateStates(True)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
        MyBase.OnLostFocus(e)
        UpdateStates(True)
    End Sub
#End Region
End Class


Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
                                             ByVal e As ValueChangedEventArgs)

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs
    Private _value As Integer

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        _value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
        Get
            Return _value
        End Get
    End Property
End Class
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace VSMCustomControl
{
    [TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
    [TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
    [TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
    public class NumericUpDown : Control
    {
        public NumericUpDown()
        {
            DefaultStyleKey = typeof(NumericUpDown);
            this.IsTabStop = true;
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(int), typeof(NumericUpDown),
                new PropertyMetadata(
                    new PropertyChangedCallback(ValueChangedCallback)));

        public int Value
        {
            get
            {
                return (int)GetValue(ValueProperty);
            }

            set
            {
                SetValue(ValueProperty, value);

            }
        }

        private static void ValueChangedCallback(DependencyObject obj, 
            DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Call UpdateStates because the Value might have caused the
            // control to change ValueStates.
            ctl.UpdateStates(true);

            // Call OnValueChanged to raise the ValueChanged event.
            ctl.OnValueChanged(
                new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, 
                    newValue));
        }

        public static readonly RoutedEvent ValueChangedEvent =
            EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                          typeof(ValueChangedEventHandler), typeof(NumericUpDown));

        public event ValueChangedEventHandler ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }


        protected virtual void OnValueChanged(ValueChangedEventArgs e)
        {
            // Raise the ValueChanged event so applications can be alerted
            // when Value changes.
            RaiseEvent(e);
        }


        private void UpdateStates(bool useTransitions)
        {
            if (Value >= 0)
            {
                VisualStateManager.GoToState(this, "Positive", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Negative", useTransitions);
            }

            if (IsFocused)
            {
                VisualStateManager.GoToState(this, "Focused", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Unfocused", useTransitions);
            }

        }

        public override void OnApplyTemplate()
        {
            UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
            DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
            //TextElement = GetTemplateChild("TextBlock") as TextBlock;

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

            set
            {
                if (downButtonElement != null)
                {
                    downButtonElement.Click -=
                        new RoutedEventHandler(downButtonElement_Click);
                }
                downButtonElement = value;

                if (downButtonElement != null)
                {
                    downButtonElement.Click +=
                        new RoutedEventHandler(downButtonElement_Click);
                }
            }
        }

        void downButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value--;
        }

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

            set
            {
                if (upButtonElement != null)
                {
                    upButtonElement.Click -=
                        new RoutedEventHandler(upButtonElement_Click);
                }
                upButtonElement = value;

                if (upButtonElement != null)
                {
                    upButtonElement.Click +=
                        new RoutedEventHandler(upButtonElement_Click);
                }
            }
        }

        void upButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value++;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            Focus();
        }


        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            UpdateStates(true);
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);
            UpdateStates(true);
        }
    }


    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}

请参见

概念

通过创建 ControlTemplate 自定义现有控件的外观

其他资源

控件自定义