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

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

A checkbox with the default control template. 一个具有默认控件模板的复选框

A checkbox with a custom control template. 一个具有自定义控件模板的复选框

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

NumericUpDown custom control. 一个自定义 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 控件的可视结构,其中包括 RepeatButton(用于增大 Value)、RepeatButton(用于减小 Value)和 TextBlock(用于显示 Value)。

<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 中指定控件的视觉行为。 以下示例演示 PositiveNegative 状态的 VisualState 对象。 PositiveNegative 相互排斥(控件始终恰好位于这两个控件中的一个),因此该示例将 VisualState 对象置于单个 VisualStateGroup 中。 当控件进入 Negative 状态时,TextBlockForeground 会变为红色。 当控件处于 Positive 状态时,Foreground 会恢复为其原始值。 在创建控件模板中进一步讨论了如何在 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. 设置步骤 2 中在 OnApplyTemplate 方法中定义的 FrameworkElement 属性。 这是 ControlTemplate 中的 FrameworkElement 可供控件使用的最早时间。 使用 FrameworkElementx:NameControlTemplate 获取它。

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

以下示例演示 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

按照前面示例中演示的做法操作,可确保控件在 ControlTemplate 缺少 FrameworkElement 时继续运行。

使用 VisualStateManager 管理状态

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

以下示例重复前面显示与控件的 PositiveNegative 状态对应的 VisualState 对象的示例。 NegativeVisualState 中的 Storyboard 会将 TextBlockForeground 转变为红色。 当 NumericUpDown 控件处于 Negative 状态时,Negative 状态下的情节提要会开始。 随后 Negative 状态下的 Storyboard 会在控件恢复为 Positive 状态时停止。 PositiveVisualState 不需要包含 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 提供了名称,但 TextBlock 不在 NumericUpDown 的控件协定中,因为控件的逻辑从不引用 TextBlock。 在 ControlTemplate 中引用的元素具有名称,但不需要是控件协定的一部分,因为控件的新 ControlTemplate 可能不需要引用该元素。 例如,为 NumericUpDown 创建新 ControlTemplate 的用户可能会决定不通过更改 Foreground 来指示 Value 是负值。 在这种情况下,代码和 ControlTemplate 都不会按名称引用 TextBlock

控件的逻辑负责更改控件的状态。 以下示例演示 NumericUpDown 控件在 Value 为 0 或更大时调用 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 会执行以下操作:

  • 如果控件即将进入的 VisualState 具有 Storyboard,则情节提要会开始。 那么,如果控件即将退出的 VisualState 具有 Storyboard,则情节提要会结束。

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

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

使用 VisualStateManager 的最佳做法

建议执行以下操作来维护控件的状态:

  • 使用属性跟踪其状态。

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

NumericUpDown 控件使用其 Value 属性跟踪它是处于 Positive 还是 Negative 状态。 NumericUpDown 控件还定义了 FocusedUnFocused 状态,会跟踪 IsFocused 属性。 如果使用不是天然对应于控件属性的状态,则可以定义私有属性以跟踪状态。

更新所有状态的单个方法可集中对 VisualStateManager 的调用并使代码可管理。 下面的示例演示 NumericUpDown 控件的帮助程序方法 UpdateStates。 当 Value 大于或等于 0 时,Control 处于 Positive 状态。 当 Value 小于 0 时,控件处于 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 状态的情节提要不会中断,用户不会看到控件发生更改。

在调用 GoToState 时,VisualStateManager 使用 VisualStateGroup 对象确定要退出的状态。 对于在其 ControlTemplate 中定义的每个 VisualStateGroup,控件始终处于一个状态,并且只有当它进入同一个 VisualStateGroup 中的另一个状态时,才会离开一个状态。 例如,NumericUpDown 控件的 ControlTemplate 在一个 VisualStateGroup 中定义了 PositiveNegativeVisualState 对象,在另一个中定义了 FocusedUnfocusedVisualState 对象。 (可以查看在本主题的完整示例部分中定义的 FocusedUnfocusedVisualState)当控件从 Positive 状态转换为 Negative 状态或进行相反转换时,控件仍保持 FocusedUnfocused 状态。

控件的状态可能会在三种典型情况下进行更改:

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

应在 OnApplyTemplate 方法中更新控件的状态,以便在应用 ControlTemplate 时,控件可显示正确的状态。 以下示例在 OnApplyTemplate 中调用 UpdateStates 以确保控件处于合适的状态。 例如,假设你创建了 NumericUpDown 控件,然后将其 Foreground 设置为绿色,并将 Value 设置为 -5。 如果在将 ControlTemplate 应用于 NumericUpDown 控件时未调用 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 放置在控件的类定义中。

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

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

[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

完整的示例

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

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

另请参阅