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

Windows Presentation Foundation (WPF)使你能够创建可自定义其外观的控件。 可以通过创建新的ControlTemplate来更改CheckBox的外观,超出仅仅设置属性能够做到的范围。 下图显示了一个 CheckBox 使用默认 ControlTemplate 以及一个 CheckBox 使用自定义 ControlTemplate 的示例。

带有默认控件模板的复选框。 使用默认控件模板的 CheckBox

带有自定义控件模板的复选框。 使用自定义控件模板的 CheckBox

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

NumericUpDown 自定义控件。 自定义 NumericUpDown 控件

本主题包含以下部分:

先决条件

本主题假定你知道如何为现有控件创建新 ControlTemplate 控件,熟悉控件协定中的元素是什么,并了解 在创建控件模板中讨论的概念。

注释

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

部件和状态模型

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

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

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

  • 提供控制合同以明确应包含在ControlTemplate中的内容。

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

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

使用部件和状态模型创建自定义控件时,您可以在控件的 ControlTemplate 中定义其视觉结构和视觉行为,而不是在其逻辑中。 控件的可视结构是构成控件的对象的组合 FrameworkElement 。 视觉行为是控件处于特定状态时显示的方式。 有关创建 ControlTemplate 指定控件的视觉结构和视觉行为的详细信息,请参阅 创建控件的模板

NumericUpDown控件示例中,视觉结构包括两个RepeatButton控件和一个TextBlock。 如果在控件的代码 NumericUpDown 中添加这些控件--在它的构造函数中--这些控件的位置将是不可更改的。 应在 ControlTemplate中定义控件的视觉结构和视觉行为,而不是在代码中定义。 然后,应用程序开发人员可以自定义按钮和 TextBlock 的位置,并指定在 Value 为负数时发生的行为,因为 ControlTemplate 可以被替换。

以下示例显示了 NumericUpDown 控件的可视结构,其中包括用于增加 ValueRepeatButton、用于减少 ValueRepeatButton 和用于显示 ValueTextBlock

<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 可视行为是,如果值为负值,则值为红色字体。 如果在代码中更改ForegroundTextBlock值,并且Value为负时,NumericUpDown将始终显示红色负值。 您可以通过将VisualState对象添加到ControlTemplate来指定控件在ControlTemplate中的视觉行为。 以下示例显示了VisualState对象在PositiveNegative状态下的情况。 Positive 并且 Negative 是互斥的(控件始终位于两者中的一个),因此本示例将 VisualState 对象放入单个 VisualStateGroup对象中。 当控件进入 Negative 状态时,Foreground 变为 TextBlock 红色。 当控件处于 Positive 状态时,返回 Foreground 其原始值。 在ControlTemplate中定义对象的问题在创建控件模板中进一步讨论。

注释

请务必在 VisualStateManager.VisualStateGroupsFrameworkElementControlTemplate 上设置附加属性。

<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作者可能会故意或错误地省略FrameworkElementVisualState对象,但控件的逻辑可能需要这些部分才能正常运行。 部件和状态模型规定,您的控件应具备对ControlTemplate没有FrameworkElementVisualState对象的抵抗力。 控件不应在ControlTemplate中缺少FrameworkElementVisualStateVisualStateGroup时引发异常或报告错误。 本部分介绍与 FrameworkElement 对象交互和管理状态的建议做法。

预期会缺少 FrameworkElement 对象

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

确保以下做法能让您的控件正确响应缺少 FrameworkElement 对象的情况:

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

  2. 为需要与之交互的每个 FrameworkElement 属性定义专用属性。

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

  4. 设置 FrameworkElement 在方法的步骤 2 OnApplyTemplate 中定义的属性。 这是FrameworkElementControlTemplate中最早可供控件使用的。 使用x:NameFrameworkElementControlTemplate获得它。

  5. 在访问FrameworkElement的成员之前,请检查其是否为nullnull如果是,请不要报告错误。

以下示例演示控件 NumericUpDown 如何根据前面的列表中的建议与 FrameworkElement 对象交互。

在定义ControlTemplate中控件NumericUpDown的可视结构的示例中,增加ValueRepeatButton已将其x:Name属性设置为UpButton。 以下示例声明了一个名为UpButtonElement的属性,该属性表示在ControlTemplate中声明的RepeatButtonset 访问器首先检查 UpDownElement 是否等于 null,如果不相等,就取消订阅按钮的 Click 事件,然后设置属性,最后订阅新的 Click 事件。 另一个属性也已定义, RepeatButton但此处未显示,另一个调用 DownButtonElement

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

下面的示例演示 NumericUpDown 控件的 OnApplyTemplate。 该示例使用GetTemplateChild该方法从中ControlTemplate获取FrameworkElement对象。 请注意,该示例保护GetTemplateChild查找FrameworkElement时指定名称不符合预期类型的情况。 最好忽略具有指定 x:Name 但类型错误的元素。

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

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

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

    UpdateStates(False)
End Sub

通过遵循前面的示例中展示的方法,可以确保当FrameworkElement缺失时ControlTemplate仍然能够继续运行。

使用 VisualStateManager 管理状态

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

以下示例重复前面的示例,该示例显示VisualStatePositiveNegative控件的状态对应的对象。 在StoryboardNegativeVisualState中,ForegroundTextBlock变为红色。 当 NumericUpDown 控件处于 Negative 状态时,Negative 情节提要将开始。 然后,Storyboard在控件返回到Positive状态时在Negative状态停止。 Positive VisualState不需要包含Storyboard,因为当NegativeStoryboard停止时,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被赋予了一个名称,但因为控件的逻辑从未引用过TextBlockTextBlock不在NumericUpDown的控制合约中。 引用在 ControlTemplate 内的元素有名称,但不需要成为控件协定的一部分,因为新的控件 ControlTemplate 可能不需要引用这些元素。 例如,有人为NumericUpDown创建了新的ControlTemplate,可能会决定不通过更改Foreground来指示Value为负值。 在这种情况下,代码和 ControlTemplate 都不按名称引用 TextBlock

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

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

该方法GoToState执行启动和停止情节板所需的逻辑,并确保操作的适当性。 当控件调用 GoToState 更改其状态时, VisualStateManager 请执行以下操作:

使用 VisualStateManager 的最佳做法

建议采取以下措施以维护控件的状态:

  • 使用属性跟踪其状态。

  • 创建在状态之间转换的帮助程序方法。

控件 NumericUpDown 使用其 Value 属性来跟踪它是否处于 PositiveNegative 处于状态。 该NumericUpDown控件还定义FocusedUnFocused状态,以跟踪IsFocused属性。 如果使用与控件属性不自然对应的状态,可以定义一个私有属性来跟踪这些状态。

更新所有状态的单个方法集中调用 VisualStateManager 并使您的代码易于管理。 下面的示例演示控件 NumericUpDown 的帮助程序方法 UpdateStates。 如果 Value 大于或等于 0,则 Control 处于 Positive 状态。 当小于 0 时 Value ,控件处于 Negative 状态。 当 IsFocusedtrue时,控件处于 Focused 状态;否则,它处于 Unfocused 状态。 无论状态发生何种更改,控件都可以在需要更改其状态时调用 UpdateStates

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

如果将状态名称 GoToState 传递给控件已处于该状态时, GoToState 则不执行任何作,因此无需检查控件的当前状态。 例如,如果 Value 从一个负数更改为另一个负数,则 Negative 状态的场景不会中断,用户也不会注意到控件的变化。

VisualStateManager 使用 VisualStateGroup 对象来确定调用 GoToState 时要退出的状态。 控件在其ControlTemplate中每个VisualStateGroup具体定义一种状态,并且只有当它从相同的VisualStateGroup中进入另一状态时,才会离开当前状态。 例如,控件ControlTemplateNumericUpDown在一个VisualStateGroup控件中定义了PositiveNegativeVisualState对象,在另一个控件中定义了FocusedUnfocusedVisualState对象。 在本主题的完整示例部分中,可以看到FocusedUnfocusedVisualState的定义。当控件从Positive状态切换到Negative状态时,或从Negative状态切换到Positive状态时,该控件仍保持在FocusedUnfocused状态中。

有三个典型的位置,控件的状态可能会更改:

以下示例演示了在这些情况下更新控件的状态 NumericUpDown

应在OnApplyTemplate方法中更新控件的状态,以便应用ControlTemplate时控件显示为正确的状态。 下面的示例在 OnApplyTemplate 中调用 UpdateStates,以确保控件处于适当状态。 例如,假设你创建了一个 NumericUpDown 控件,然后将其设置为 Foreground 绿色和 Value -5。 如果在应用于NumericUpDown控件时ControlTemplate未调用UpdateStates控件,则控件不处于Negative状态,值为绿色而不是红色。 必须调用 UpdateStates 以将控件 Negative 置于状态。

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

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

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

    UpdateStates(False)
End Sub

当属性发生更改时,通常需要更新控件的状态。 下面的示例演示了整个 ValueChangedCallback 方法。 因为当 Value 发生变化时会调用 ValueChangedCallback,所以如果 Value 从正变为负或反之亦然,该方法会调用 UpdateStates。 当Value发生变化但仍保持正值或负值时,调用UpdateStates是可以接受的,因为在这种情况下,控件不会改变状态。

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

在事件发生时,可能还需要更新状态。 以下示例展示了NumericUpDownControl上调用UpdateStates来处理GotFocus事件。

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

帮助你 VisualStateManager 管理控件的状态。 通过使用 , VisualStateManager可确保控件在状态之间正确转换。 如果按照本部分中介绍的建议进行作 VisualStateManager,则控件的代码将保持可读且可维护。

提供控制合同

你提供一个控制协定,以便 ControlTemplate 作者知道要放入模板的内容。 控制合同有三个元素:

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

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

  • 对控件外观有影响的公共属性。

创建新 ControlTemplate 人员需要知道控件逻辑使用的对象 FrameworkElement 、每个对象的类型及其名称。 ControlTemplate作者还需要知道控件可以处于的每个可能状态的名称,以及VisualStateGroup该状态所在的状态。

返回该 NumericUpDown 示例时,控件需要 ControlTemplate 具有以下 FrameworkElement 对象:

该控件可以处于以下状态:

要指定控制所期望的FrameworkElement对象,您可以使用TemplatePartAttribute来指定预期元素的名称和类型。 若要指定控件的可能状态,请使用TemplateVisualStateAttribute来指定状态的名称及其所属的VisualStateGroup。 将 TemplatePartAttributeTemplateVisualStateAttribute 放置在控件的类定义上。

影响控件外观的任何公共属性也是您的控件合约的一部分。

以下示例指定 FrameworkElement 控件的对象和状态 NumericUpDown

[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; }
}
<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 TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

完整的示例

以下示例是ControlTemplate的整个NumericUpDown控件。

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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的逻辑。

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

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

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

另请参阅