共用方式為


建立外觀可自訂的控制項

Windows Presentation Foundation (WPF) 可讓您建立可自訂其外觀的控制項。 例如,除了設定屬性之外,您還可以建立新的 ControlTemplate 來變更 CheckBox 的外觀。 下圖顯示使用預設 ControlTemplateCheckBox,以及使用自訂 ControlTemplateCheckBox

A checkbox with the default control template.搭配預設控制項範本的核取方塊。NDP_CheckBoxDefault 使用預設控制項範本的 CheckBox

A checkbox with a custom control template.搭配自訂控制項範本的核取方塊。NDP_CheckBoxCustom 使用自訂控制項範本的 CheckBox

如果您在建立控制項時遵循組件和狀態模型,則可以自訂控制項的外觀。 Blend for Visual Studio 這類設計工具支援組件和狀態模型,因此當您遵循此模型時,將可以在這些類型的應用程式中自訂控制項。 本主題討論組件和狀態模型,以及如何在建立自己的控制項時遵循此模型。 本主題使用自訂控制項範例 (NumericUpDown) 來說明此模型的哲學。 NumericUpDown 控制項會顯示數值,而使用者可以按一下控制項的按鈕予以增加或減少。 下圖顯示本主題中所討論的 NumericUpDown 控制項。

NumericUpDown custom control.NumericUpDown 自訂控制項。NDP_NumericUPDown 自訂 NumericUpDown 控制項

本主題包含下列幾節:

必要條件

本主題假設您知道如何為現有控制項建立新的 ControlTemplate、熟悉控制項合約上的元素,以及瞭解建立控制項的範本中所討論的概念。

注意

若要建立可以自訂其外觀的控制項,您必須建立繼承自 Control 類別或其中一個子類別 (非 UserControl) 的控制項。 繼承自 UserControl 的控制項是可快速建立的控制項,但不會使用 ControlTemplate,而且您無法自訂其外觀。

組件和狀態模型

組件和狀態模型指定如何定義控制項的虛擬結構和虛擬行為。 若要遵循組件和狀態模型,您應該執行下列動作:

  • 在控制項的 ControlTemplate 中定義的虛擬結構和虛擬行為。

  • 當您控制項的邏輯與控制項範本的組件互動時,請遵循特定最佳做法。

  • 提供控制項合約,以指定 ControlTemplate 中應該包括的內容。

當您在控制項的 ControlTemplate 中定義視覺結構和視覺行為時,應用程式作者可以建立新的 ControlTemplate 而不是撰寫程式碼,來變更控制項的視覺結構和視覺行為。 您必須提供控制項合約,以告知應用程式作者應該在 ControlTemplate 中定義的 FrameworkElement 物件和狀態。 當您與 ControlTemplate 中的組件互動時,您應該遵循一些最佳做法,以讓控制項正確地處理不完整的 ControlTemplate。 如果您遵循這三個原則,則應用程式作者將能夠針對控制項建立 ControlTemplate,就像針對 WPF 隨附控制項建立一樣地輕鬆。 下節將詳細說明所有這些建議。

在 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 控制項的視覺行為是,值在負數時會是紅色字型。 如果您在 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. 設定您已在 OnApplyTemplate 方法的步驟 2 中所定義的 FrameworkElement 屬性。 這是 ControlTemplateFrameworkElement 可供控制項使用的最早時間。 使用 FrameworkElementx:Name,以從 ControlTemplate 予以取得。

  5. 存取其成員之前,請先確認 FrameworkElement 不是 null。 如果其為 null,則請不要報告錯誤。

下列範例顯示 NumericUpDown 控制項如何根據上述清單中的建議與 FrameworkElement 物件互動。

ControlTemplate 中定義 NumericUpDown 控制項視覺結構的範例中,可增加 ValueRepeatButton 將其 x:Name 屬性設定為 UpButton。 下列範例宣告稱為 UpButtonElement 的屬性,而此屬性代表 ControlTemplate 中所宣告的 RepeatButton。 如果 UpDownElement 不是 null,則 set 存取子會先取消訂閱按鈕的 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 狀態的分鏡腳本。 然後,控制項回復為 Positive 狀態時,會停止處於 Negative 狀態的 StoryboardPositiveVisualState 不需要包含 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 的控制項合約中,因為控制項的邏輯永遠不會參考 TextBlockControlTemplate 中所參考的元素具有名稱,但不需要是控制項合約的一部分,因為控制項的新 ControlTemplate 可能不需要參考該元素。 例如,可針對 NumericUpDown 建立新 ControlTemplate 的人員可能會決定變更 Value,不要指出 Foreground 是負數。 在該情況下,程式碼和 ControlTemplate 都不會依名稱參考 TextBlock

控制項的邏輯負責變更控制項的狀態。 下列範例顯示 NumericUpDown 控制項會呼叫 GoToState 方法,以在 Value 是 0 或更大值時進入 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 控制項的協助程式方法 UpdateStatesValue 大於或等於 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 狀態的分鏡腳本,而且使用者將不會看到控制項中的變更。

VisualStateManager 會使用 VisualStateGroup 物件來判斷您要在呼叫 GoToState 時結束的狀態。 針對每個定義於其 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,所以方法會呼叫 UpdateStates,以防 Value 從正數變更為負數,反之亦然。 可以接受在 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

另請參閱