Share via


建立外觀可自訂的控制項

Windows Presentation Foundation (WPF) 能夠讓您建立外觀可加以自訂的控制項。 例如,您可以透過建立新的 ControlTemplate 來變更 CheckBox 的外觀,功能遠比設定屬性來得強大。 下圖顯示使用預設 ControlTemplateCheckBox,以及使用自訂 ControlTemplateCheckBox

使用預設控制項樣板的 CheckBox

具有預設控制項範本的核取方塊。

使用自訂控制項樣板的 CheckBox

具有自訂控制項範本的核取方塊。

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

自訂的 NumericUpDown 控制項

NumericUpDown 自訂控制項。

此主題包括下列章節:

  • 必要條件

  • 組件和狀態模型

  • 在 ControlTemplate 中定義控制項的視覺化結構和視覺化行為

  • 在程式碼中使用 ControlTemplate 的組件

  • 提供控制項合約

  • 完整範例

必要條件

本主題假設您已知道如何為現有控制項建立新的 ControlTemplate、熟悉控制項合約上的項目,並了解透過建立 ControlTemplate 自訂現有控制項的外觀中所討論的概念。

注意事項注意事項

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

組件和狀態模型

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

  • 在控制項的 ControlTemplate 中定義視覺化結構和視覺化行為。

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

  • 提供控制項合約,指定 ControlTemplate 中應包括哪些項目。

在控制項的 ControlTemplate 中定義視覺化結構和視覺化行為之後,應用程式作者即可藉由建立新的 ControlTemplate 變更控制項的視覺化結構和視覺化行為,而不需要撰寫程式碼。 您必須提供控制項合約,用以告知應用程式作者應在 ControlTemplate 中定義哪些 FrameworkElement 物件和狀態。 與 ControlTemplate 中的組件互動時應遵循某些最佳做法,您的控制項才能正確處理不完整的 ControlTemplate。 如果您遵循上述三個原則,應用程式作者便能夠為您的控制項建立 ControlTemplate,過程就如同為 WPF 隨附的控制項建立控制項範本一樣簡單。 下一節將詳細說明每項建議。

在 ControlTemplate 中定義控制項的視覺化結構和視覺化行為

使用組件和狀態模型建立您自訂的控制項時,請在控制項的 ControlTemplate 中定義其視覺化結構和視覺化行為,而不要在其邏輯中定義。 控制項的視覺化結構是組成控制項之 FrameworkElement 物件的複合。 視覺化行為則是控制項處於特定狀態時的顯示方式。 如需建立 ControlTemplate 以指定控制項之視覺化結構和視覺化行為的詳細資訊,請參閱透過建立 ControlTemplate 自訂現有控制項的外觀

在 NumericUpDown 控制項的範例中,視覺化結構包括兩個 RepeatButton 控制項和一個 TextBlock。 如果您將這些控制項加入 NumericUpDown 控制項的程式碼中 (例如加入其建構函式中),則控制項的位置將無法改變。 因此,請不要在控制項的程式碼中定義控制項的視覺化結構和視覺化行為,而是在 ControlTemplate 中定義。 接著應用程式開發人員就能自訂按鈕位置和 TextBlock,並指定當 Value 為負時應發生的行為,因為 ControlTemplate 是可以取代的。

下列範例顯示 NumericUpDown 控制項的視覺化結構,其中包括一個增加 Value 的 RepeatButton、一個減少 Value 的 RepeatButton,以及一個用來顯示 Value 的 TextBlock

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

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

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

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

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

  </Grid>
</ControlTemplate>

NumericUpDown 控制項有一項視覺化行為是,當值為負時,以紅色字型顯示該值。 如果您在 Value 為負時變更程式碼中 TextBlockForeground,則 NumericUpDown 會永遠顯示紅色的負值。 您可以在 ControlTemplate 中加入 VisualState 物件,藉此在 ControlTemplate 中指定控制項的視覺化行為。 下列範例顯示 Positive 和 Negative 狀態的 VisualState 物件。 Positive 和 Negative 彼此互斥 (表示控制項必定處於這兩種狀態其中一種),所以範例會將 VisualState 物件置於單一 VisualStateGroup 中。 當控制項進入 Negative 狀態時,TextBlockForeground 就會變成紅色。 當控制項處於 Positive 狀態時,Foreground 又會恢復其原始值。 透過建立 ControlTemplate 自訂現有控制項的外觀中會進一步討論在 ControlTemplate 中定義 VisualState 物件。

注意事項注意事項

請務必在 ControlTemplate 的根 FrameworkElement 上設定 VisualStateManager.VisualStateGroups 附加屬性。

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

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

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

        </VisualState>

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

在程式碼中使用 ControlTemplate 的組件

ControlTemplate 作者可能會故意或不小心省略了 FrameworkElementVisualState 物件,但控制項邏輯可能需要這些組件才能正常運作。 組件和狀態模型可指定控制項應該對遺失 FrameworkElementVisualState 物件的 ControlTemplate 保持彈性, 也就是說,當 ControlTemplate 中遺失 FrameworkElementVisualStateVisualStateGroup 時,控制項不應擲回例外狀況或報告錯誤。 本節說明與 FrameworkElement 物件互動以及管理狀態的建議做法。

預測遺失的 FrameworkElement 物件

ControlTemplate 中定義 FrameworkElement 物件時,控制項邏輯可能需要與其中某些物件互動。 例如,NumericUpDown 控制項訂閱按鈕的 Click 事件以便增加或減少 Value,並將 TextBlockText 屬性設為 Value。 如果自訂的 ControlTemplate 省略 TextBlock 或按鈕,控制項會損失部分功能但仍可接受,不過您應該要確定控制項不會因此造成錯誤。 例如,如果 ControlTemplate 不含可變更 Value 的按鈕,則 NumericUpDown 會失去功能,但使用此 ControlTemplate 的應用程式仍可繼續執行。

下列做法可確保您的控制項正確回應遺失的 FrameworkElement 物件:

  1. 為程式碼中需要參考的每個 FrameworkElement 設定 x:Name 屬性 (Attribute)。

  2. 為需要進行互動的每個 FrameworkElement 定義私用屬性 (Property)。

  3. FrameworkElement 屬性 (Property) 的 set 存取子中訂閱控制項要處理的事件,再取消訂閱。

  4. OnApplyTemplate 方法中設定您在步驟 2 中定義的 FrameworkElement 屬性 (Property)。 這是控制項最早能接觸到 ControlTemplateFrameworkElement 的地方。 使用 FrameworkElement 的 x:Name,從 ControlTemplate 取得此值。

  5. 在存取 FrameworkElement 的成員之前,先檢查其是否為 null。 如果為 null,不會報告錯誤。

下列範例顯示 NumericUpDown 控制項如何以符合上列建議的方式,來與 FrameworkElement 物件互動。

ControlTemplate 中定義 NumericUpDown 控制項之視覺化結構的範例中,RepeatButton (用於增加 Value) 的 x:Name 屬性 (Attribute) 設為 UpButton。 下列範例宣告一個名為 UpButtonElement 的屬性 (Property),代表在 ControlTemplate 中宣告的 RepeatButton。 如果 UpDownElement 不是 null,則 set 存取子會先取消訂閱按鈕的 Click 事件、接著設定屬性,然後再訂閱 Click 事件。 另外也會為另一個 RepeatButton 定義一個名為 DownButtonElement 的屬性,但不會在此處顯示。

Private m_upButtonElement As RepeatButton

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

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

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

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

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

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

下列範例顯示 NumericUpDown 控制項的 OnApplyTemplate。 此範例使用 GetTemplateChild 方法取得 ControlTemplate 中的 FrameworkElement 物件。 請注意,此範例會預防 GetTemplateChild 找到具有指定名稱之 FrameworkElement 但型別不正確的情節。 這也是忽略具有指定之 x:Name 但型別錯誤之項目的最佳做法。

Public Overloads Overrides Sub OnApplyTemplate()

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

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

    UpdateStates(false);
}

遵循以上範例中所顯示的做法,即可確保當 ControlTemplate 遺失 FrameworkElement 時,您的控制項仍可繼續執行。

使用 VisualStateManager 管理狀態

VisualStateManager 會記錄控制項的狀態,並執行在狀態間轉換時所需的邏輯。 將 VisualState 物件加入至 ControlTemplate 時,應將這些物件加入至 VisualStateGroup,然後將 VisualStateGroup 加入 VisualStateManager.VisualStateGroups 附加屬性,如此 VisualStateManager 就能存取這些物件。

下列範例會重複之前的範例,根據控制項的 Positive 和 Negative 狀態顯示 VisualState 物件。 Negative VisualState 中的 Storyboard 會將 TextBlockForeground 轉為紅色。 當 NumericUpDown 控制項位於 Negative 狀態時,Negative 狀態中的 Storyboard 便會啟動。 而當控制項回復 Positive 狀態時,Negative 狀態中的 Storyboard 就會停止。 Positive VisualState 不需要包含 Storyboard,因為當 Negative 的 Storyboard 停止時,Foreground 就會恢復其原始顏色。

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

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

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

        </VisualState>

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

請注意,TextBlock 會加以命名,但是 TextBlock 不在 NumericUpDown 的控制項合約中,因為控制項的邏輯絕不會參考 TextBlock。 雖然 ControlTemplate 中參考的項目都有名稱,但是不一定要是控制項合約的一部分,因為控制項的新 ControlTemplate 可能不需要參考該項目。 例如,為 NumericUpDown 建立新 ControlTemplate 的人可能決定不藉由變更 Foreground 的方式指出 Value 是負的。 在這種情況下,程式碼和 ControlTemplate 都不會透過名稱參考 TextBlock

控制項邏輯負責變更控制項的狀態。 下列範例顯示當 Value 大於或等於 0 時,NumericUpDown 控制項會呼叫 GoToState 方法進入 Positive 狀態,而當 Value 小於 0 時則進入 Negative 狀態。

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

GoToState 方法會執行正確啟動和停止 Storyboard 所需的邏輯。 當控制項呼叫 GoToState 變更其狀態時,VisualStateManager 會進行下列作業:

  • 如果控制項將進入的 VisualState 具有 Storyboard,Storyboard 就會啟動。 接著,如果控制項將離開的 VisualState 具有 Storyboard,Storyboard 就會停止。

  • 如果控制項已經處於指定的狀態中,GoToState 不會採取任何動作,並且會傳回 true。

  • 如果指定的狀態不存在於 control 的 ControlTemplate 中,則 GoToState 不會採取任何動作,並且會傳回 false。

使用 VisualStateManager 的最佳做法

建議您執行下列作業來維持控制項的狀態:

  • 使用屬性追蹤其狀態。

  • 建立 Helper 方法以在狀態之間轉換。

NumericUpDown 控制項會使用 Value 屬性來追蹤它是處於 Positive 或 Negative 狀態。 NumericUpDown 控制項還會定義 Focused 和 UnFocused 狀態,用來追蹤 IsFocused 屬性。 如果您使用的狀態並未自然回應控制項的屬性,可以定義私用屬性來追蹤狀態。

更新所有狀態的單一方法會集中呼叫 VisualStateManager,並且讓您的程式碼維持在可管理的範圍內。 下列範例顯示 NumericUpDown 控制項的 Helper 方法 UpdateStates。 當 Value 大於或等於 0 時,Control 處於 Positive 狀態。 當 Value 小於 0 時,控制項處於 Negative 狀態。 當 IsFocused 為 true 時,控制項會處於 Focused 狀態;否則為 Unfocused 狀態。 控制項可以在需要變更狀態時呼叫 UpdateStates,無論狀態變更為何。

Private Sub UpdateStates(ByVal useTransitions As Boolean)

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

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

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

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

}

如果您傳遞給 GoToState 的狀態名稱為控制項所處的狀態,則 GoToState 不會執行任何動作,所以您不需要檢查控制項的目前狀態。 例如,如果 Value 從一個負數變更為另一個負數,則 Negative 狀態的 Storyboard 不會中斷,使用者也不會看到控制項出現任何變更。

當您呼叫 GoToState 時,VisualStateManager 會使用 VisualStateGroup 物件判斷要離開哪個狀態。 控制項在每一個於 ControlTemplate 定義的 VisualStateGroup 中都會處於某一種狀態,而只有在同一 VisualStateGroup 中進入另一種狀態時,才會離開該狀態。 例如,NumericUpDown 控制項的 ControlTemplate 在某個 VisualStateGroup 中定義了 Positive 和 Negative VisualState 物件,並在另一個群組中定義了 Focused 和 Unfocused VisualState 物件。 您可以在本主題中看見完整範例一節中定義的 Focused 和 Unfocused VisualState。當控制項從 Positive 狀態進入 Negative 狀態 (或反向變更) 時,控制項會保持為 Focused 或 Unfocused 狀態。

控制項狀態可能發生變更的典型位置有以下三個:

下列範例示範更新上述情形中的 NumericUpDown 控制項狀態。

您應該在 OnApplyTemplate 方法中更新控制項狀態,如此當套用 ControlTemplate 時控制項才會出現在正確狀態中。 下列範例會在 OnApplyTemplate 中呼叫 UpdateStates,以確保控制項處於正確狀態。 例如,假設您建立了 NumericUpDown 控制項,然後將它的 Foreground 設為綠色以及 Value 設為 -5。 如果您在 ControlTemplate 套用至 NumericUpDown 控制項時未呼叫 UpdateStates,則控制項不會處於 Negative 狀態,而且值將是綠色,不是紅色。 您必須呼叫 UpdateStates 以將控制項放到 Negative 狀態。

Public Overloads Overrides Sub OnApplyTemplate()

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

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

    UpdateStates(false);
}

當屬性變更時,您經常需要更新控制項狀態。 下列範例顯示完整的 ValueChangedCallback 方法。 由於 ValueChangedCallback 是在 Value 變更時呼叫,因此方法會在 Value 從正值變為負值 (或相反) 時呼叫 UpdateStates。 當 Value 有所變更但仍維持正值或負值時,仍然可以呼叫 UpdateStates,因為在這種情況下控制項並不會變更狀態。

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

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

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

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

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

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

發生事件時,您可能也需要更新狀態。 下列範例顯示 NumericUpDown 在 Control 上呼叫 UpdateStates,以處理 GotFocus 事件。

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

VisualStateManager 可協助您管理控制項狀態。 藉由使用 VisualStateManager,可以確保控制項正確轉換其狀態。 如果您在使用 VisualStateManager 時遵循本節所述建議,您的控制項程式碼將可維持可讀性並且易於維護。

提供控制項合約

您可以提供控制項合約,這樣可讓 ControlTemplate 作者了解範本中包括哪些項目。 控制項合約有三個項目:

  • 控制項邏輯使用的視覺化項目。

  • 控制項的狀態,以及每一種狀態所屬的群組。

  • 影響控制項視覺外觀的公用屬性。

建立新的 ControlTemplate 時,需要知道控制項邏輯使用哪些 FrameworkElement 物件、每個物件的所屬型別,以及其名稱為何。 ControlTemplate 作者也需要知道控制項會進入的每個可能狀態之名稱,以及狀態屬於哪個 VisualStateGroup

回到 NumericUpDown 範例,控制項預期 ControlTemplate 擁有下列 FrameworkElement 物件:

控制項可以處於下列狀態:

若要指定控制項應預期哪些 FrameworkElement 物件,請使用 TemplatePartAttribute 指定預期項目的名稱和型別。 若要指定控制項的可能狀態,請使用 TemplateVisualStateAttribute 指定狀態的名稱以及狀態所屬的 VisualStateGroup。 將 TemplatePartAttributeTemplateVisualStateAttribute 放入控制項的類別定義。

任何會影響控制項外觀的公用屬性,也都是控制項合約的一部分。

下列範例指定 NumericUpDown 控制項的 FrameworkElement 物件和狀態。

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

完整範例

下列範例是 NumericUpDown 控制項的完整 ControlTemplate

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


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


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

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

                </VisualState>

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

              <VisualStateGroup Name="FocusStates">

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

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

            </VisualStateManager.VisualStateGroups>

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

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

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

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

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

下列範例顯示 NumericUpDown 的邏輯。

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

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

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

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

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

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

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

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

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

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

    Public Custom Event ValueChanged As ValueChangedEventHandler

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

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

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

    End Event


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


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

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

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

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

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

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

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

        Set(ByVal value As RepeatButton)

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

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

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

    Private m_upButtonElement As RepeatButton

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

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

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

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

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


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

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


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

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs
    Private _value As Integer

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

        _value = num
        RoutedEvent = id
    End Sub

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

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

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

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

            set
            {
                SetValue(ValueProperty, value);

            }
        }

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

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

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

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

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


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


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

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

        }

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

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

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

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

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

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

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

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

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

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


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

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


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

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

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

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

請參閱

概念

透過建立 ControlTemplate 自訂現有控制項的外觀

其他資源

控制項自訂