次の方法で共有


カスタマイズ可能な外観を持つコントロールの作成

Windows Presentation Foundation (WPF) では、外観をカスタマイズできるコントロールを作成できます。 たとえば、新しいCheckBoxを作成することで、プロパティの設定以外のControlTemplateの外観を変更できます。 次の図は、既定のCheckBoxを使用するControlTemplateと、カスタム CheckBoxを使用するControlTemplateを示しています。

既定のコントロール テンプレートを含むチェック ボックス。 既定のコントロール テンプレートを使用する CheckBox

カスタム コントロール テンプレートを含むチェック ボックス。 カスタム コントロール テンプレートを使用する CheckBox

コントロールの作成時にパーツと状態のモデルに従うと、コントロールの外観がカスタマイズ可能になります。 Blend for Visual Studio などのデザイナー ツールではパーツモデルと状態モデルがサポートされているため、このモデルに従うと、これらの種類のアプリケーションでコントロールがカスタマイズできるようになります。 このトピックでは、パーツと状態モデルと、独自のコントロールを作成するときにそれに従う方法について説明します。 このトピックでは、カスタム コントロールの例 ( NumericUpDown) を使用して、このモデルの理念を説明します。 NumericUpDown コントロールには数値が表示され、ユーザーはコントロールのボタンをクリックして増減できます。 次の図は、このトピックで説明する NumericUpDown コントロールを示しています。

NumericUpDown カスタム コントロール。 カスタム NumericUpDown コントロール

このトピックには、次のセクションが含まれています。

[前提条件]

このトピックでは、既存のコントロールの新しい ControlTemplate を作成する方法、コントロール コントラクトの要素について理解していること、およびコントロールの テンプレートの作成で説明されている概念を理解していることを前提としています。

外観をカスタマイズできるコントロールを作成するには、 Control クラスまたは UserControl 以外のサブクラスの 1 つを継承するコントロールを作成する必要があります。 UserControlから継承するコントロールは、すばやく作成できるコントロールですが、ControlTemplateを使用せず、外観をカスタマイズすることはできません。

パーツと状態モデル

パーツと状態モデルでは、コントロールの視覚的構造と視覚的な動作を定義する方法を指定します。 パーツと状態のモデルに従うには、次の操作を行う必要があります。

  • コントロールの ControlTemplate で視覚的な構造と視覚的な動作を定義します。

  • コントロールのロジックがコントロール テンプレートの一部と対話する場合は、特定のベスト プラクティスに従います。

  • ControlTemplateに含める必要のあるものを指定するコントロール コントラクトを指定します。

コントロールの ControlTemplate で視覚的な構造と視覚的な動作を定義すると、アプリケーションの作成者は、コードを記述する代わりに新しい ControlTemplate を作成することで、コントロールの視覚的な構造と視覚的な動作を変更できます。 あなたは、アプリケーション作成者に対して、FrameworkElementで定義すべきControlTemplateオブジェクトと状態を指示するコントロールコントラクトを提供する必要があります。 コントロールが不完全なControlTemplateを適切に処理できるように、ControlTemplate内のパーツを操作するときは、いくつかのベスト プラクティスに従う必要があります。 これらの 3 つの原則に従うと、アプリケーションの作成者は、WPF に付属するコントロールに対して可能な限り簡単にコントロールの ControlTemplate を作成できます。 次のセクションでは、これらの推奨事項について詳しく説明します。

ControlTemplate でのコントロールのビジュアル構造とビジュアル動作の定義

パーツと状態モデルを使用してカスタム コントロールを作成する場合は、ロジックではなく、その ControlTemplate でコントロールの視覚的構造と視覚的な動作を定義します。 コントロールの視覚的な構造は、コントロールを構成 FrameworkElement オブジェクトの複合です。 視覚的な動作は、コントロールが特定の状態にある場合に表示される方法です。 コントロールの視覚的構造と視覚的動作を指定する ControlTemplate の作成の詳細については、「コントロール のテンプレートを作成する」を参照してください。

NumericUpDown コントロールの例では、ビジュアル構造には 2 つのRepeatButton コントロールとTextBlockが含まれています。 NumericUpDown コントロールのコードにこれらのコントロールを追加すると、そのコンストラクター内で、たとえば、それらのコントロールの位置は変更できなくなります。 コードでコントロールのビジュアル構造と視覚的な動作を定義する代わりに、 ControlTemplateで定義する必要があります。 次に、アプリケーション開発者は、ボタンとTextBlockの位置をカスタマイズし、Valueを置き換えることができるため、ControlTemplateが負の場合に発生する動作を指定します。

次の例は、NumericUpDown コントロールの視覚的な構造を示しています。これには、RepeatButtonを上げるValueRepeatButtonを減らすValueTextBlockを表示する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 コントロールの視覚的な動作は、負の値の場合は赤いフォントになります。 Foregroundが負の場合にコード内のTextBlockValueを変更すると、NumericUpDownは常に赤色の負の値を表示します。 ControlTemplateVisualState オブジェクトを追加して、ControlTemplateのコントロールの視覚的な動作を指定します。 次の例は、VisualStateおよびPositive状態のNegative オブジェクトを示しています。 PositiveNegative は相互に排他的であるため (コントロールは常に 2 つのうちの 1 つになります)、この例では、 VisualState オブジェクトを 1 つの VisualStateGroupに配置します。 コントロールがNegative状態になると、ForegroundTextBlockが赤に変わります。 コントロールが Positive 状態になると、 Foreground は元の値に戻ります。 VisualState ControlTemplateオブジェクトの定義については、「コントロールのテンプレートを作成する」で詳しく説明します。

VisualStateManager.VisualStateGroupsのルート FrameworkElementで、ControlTemplate添付プロパティを設定してください。

<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作成者は、意図的に、または誤って、FrameworkElementまたはVisualStateオブジェクトを省略する場合がありますが、コントロールのロジックでこれらの部分が適切に機能することが必要になる場合があります。 パーツと状態モデルでは、ControlTemplateまたはFrameworkElement オブジェクトが不足しているVisualStateに対してコントロールに回復性を持たせる必要があることを指定します。 FrameworkElementVisualStateVisualStateGroup、またはControlTemplateがない場合、コントロールは例外をスローしたり、エラーを報告したりしないでください。 このセクションでは、 FrameworkElement オブジェクトを操作し、状態を管理するための推奨されるプラクティスについて説明します。

欠落している FrameworkElement オブジェクトを予測する

FrameworkElementControlTemplateオブジェクトを定義する場合、コントロールのロジックで一部のオブジェクトと対話する必要がある場合があります。 たとえば、NumericUpDown コントロールは、ボタンのClick イベントをサブスクライブしてValueを増減し、TextTextBlockプロパティをValueに設定します。 カスタム ControlTemplateTextBlock またはボタンを省略した場合、コントロールの機能の一部が失われることは許容されますが、コントロールでエラーが発生しないことを確認する必要があります。 たとえば、 ControlTemplateValueを変更するボタンが含まれていない場合、 NumericUpDown はその機能を失いますが、 ControlTemplate を使用するアプリケーションは引き続き実行されます。

次のプラクティスでは、不足している FrameworkElement オブジェクトに対してコントロールが適切に応答するようにします。

  1. コードで参照する必要がある各x:NameFrameworkElement属性を設定します。

  2. 対話する必要がある各 FrameworkElement のプライベート プロパティを定義します。

  3. FrameworkElement プロパティの set アクセサーで、コントロールが処理するイベントの登録および登録解除を行います。

  4. FrameworkElement メソッドの手順 2 で定義したOnApplyTemplateプロパティを設定します。 これは、FrameworkElement内のControlTemplateがコントロールで利用可能になる最も早いタイミングです。 x:NameFrameworkElementを使用して、ControlTemplateから取得します。

  5. メンバーにアクセスする前に、 FrameworkElementnull されていないことを確認します。 null場合は、エラーを報告しないでください。

次の例は、前の一覧の推奨事項に従って、 NumericUpDown コントロールが FrameworkElement オブジェクトと対話する方法を示しています。

NumericUpDownControlTemplate コントロールのビジュアル構造を定義する例では、RepeatButton増加するValuex:Name属性がUpButtonに設定されています。 次の例では、UpButtonElementで宣言されているRepeatButtonを表すControlTemplateというプロパティを宣言します。 set アクセサーは、ClickUpDownElementされていない場合は、最初にボタンのnull イベントのサブスクライブを解除してから、プロパティを設定してから、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

次の例は、OnApplyTemplate コントロールのNumericUpDownを示しています。 この例では、GetTemplateChild メソッドを使用して、FrameworkElementからControlTemplate オブジェクトを取得します。 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

前の例で示したプラクティスに従うことで、 ControlTemplateFrameworkElementが見つからない場合でも、コントロールが引き続き実行されるようにします。

VisualStateManager を使用して状態を管理する

VisualStateManagerはコントロールの状態を追跡し、状態間の遷移に必要なロジックを実行します。 VisualState ControlTemplateオブジェクトを追加するときは、VisualStateGroupにオブジェクトを追加し、VisualStateGroup添付プロパティにVisualStateManager.VisualStateGroupsを追加して、VisualStateManagerがそれらにアクセスできるようにします。

次の例では、コントロールのVisualStatePositiveの状態に対応するNegative オブジェクトを示す前の例を繰り返します。 Storyboard NegativeVisualStateは、ForegroundTextBlockを赤に変えます。 NumericUpDown コントロールがNegative状態になると、Negative状態のストーリーボードが開始されます。 その後、コントロールがStoryboard状態に戻ると、Negative状態のPositiveが停止します。 PositiveVisualStateが停止すると、Storyboardは元の色に戻るため、StoryboardNegativeForegroundを含める必要はありません。

<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を参照しないため、NumericUpDownTextBlockのコントロール コントラクト内にありません。 ControlTemplateで参照される要素には名前がありますが、コントロールの新しいControlTemplateがその要素を参照する必要がない可能性があるため、コントロール コントラクトの一部である必要はありません。 たとえば、ControlTemplateの新しいNumericUpDownを作成するユーザーは、Valueを変更して、Foregroundが負の値であることを示さないことを決定する場合があります。 その場合、コードも ControlTemplate も名前で TextBlock を参照しません。

コントロールのロジックは、コントロールの状態を変更する役割を担います。 次の例は、NumericUpDown コントロールが GoToState メソッドを呼び出して、Positiveが 0 以上の場合にValue状態に移行し、Negativeが 0 未満の場合のValue状態を示しています。

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 は次の処理を行います。

  • コントロールの VisualStateStoryboardがある場合は、ストーリーボードが開始されます。 次に、コントロールの元の VisualStateStoryboardがある場合、ストーリーボードは終了します。

  • コントロールが既に指定された状態にある場合、 GoToState はアクションを実行せず、 trueを返します。

  • 指定された状態がControlTemplatecontrolに存在しない場合、GoToStateはアクションを実行せず、falseを返します。

VisualStateManager を使用するためのベスト プラクティス

コントロールの状態を維持するには、次の操作を行うことをお勧めします。

  • プロパティを使用して状態を追跡します。

  • 状態間を遷移するヘルパー メソッドを作成します。

NumericUpDown コントロールは、Value プロパティを使用して、Positive状態かNegative状態かを追跡します。 NumericUpDown コントロールでは、Focused プロパティを追跡するUnFocused状態とIsFocused状態も定義します。 コントロールのプロパティに自然に対応しない状態を使用する場合は、プライベート プロパティを定義して状態を追跡できます。

すべての状態を更新する 1 つのメソッドは、 VisualStateManager の呼び出しを一元化し、コードを管理しやすくします。 次の例は、 NumericUpDown コントロールのヘルパー メソッド ( UpdateStates) を示しています。 Valueが 0 以上の場合、ControlPositive状態になります。 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を呼び出すときに終了する状態を決定します。 コントロールは、VisualStateGroupで定義されているControlTemplateごとに常に 1 つの状態になり、同じVisualStateGroupから別の状態になったときにのみ状態を残します。 たとえば、ControlTemplate コントロールのNumericUpDownは、1 つのPositive内のNegativeオブジェクトとVisualStateVisualStateGroup オブジェクト、および別のFocusedオブジェクトとUnfocusedVisualState オブジェクトを定義します。 (このトピックの「Focused」セクションで定義されているUnfocusedVisualStateを確認できます。コントロールがPositive状態からNegative状態になった場合、またはその逆の場合、コントロールはFocused状態またはUnfocused状態のままです。

コントロールの状態が変わる一般的な場所は 3 つあります。

  • ControlTemplateControlに適用される場合。

  • プロパティが変更されたとき。

  • イベントが発生した場合。

次の例では、このような場合に NumericUpDown コントロールの状態を更新する方法を示します。

OnApplyTemplateが適用されたときにコントロールが正しい状態になるように、ControlTemplate メソッドのコントロールの状態を更新する必要があります。 次の例では、UpdateStatesOnApplyTemplateを呼び出して、コントロールが適切な状態であることを確認します。 たとえば、 NumericUpDown コントロールを作成し、その Foreground を緑に設定し、 Value を -5 に設定するとします。 UpdateStatesControlTemplate コントロールに適用されたときにNumericUpDownを呼び出さない場合、コントロールは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 メソッド全体を示しています。 ValueChangedCallbackValueが変更されたときに呼び出されるため、UpdateStatesが正から負またはその逆に変更された場合に、メソッド呼び出しValueUpdateStatesが変化しても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));
}
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

また、イベントが発生したときに状態を更新することが必要になる場合もあります。 次の例は、NumericUpDown イベントを処理するために、UpdateStatesControlGotFocus呼び出しを行っていることを示しています。

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 作成者がテンプレートに入力する内容を認識できるようにします。 コントロール コントラクトには、次の 3 つの要素があります。

  • コントロールのロジックで使用されるビジュアル要素。

  • 各状態が属するコントロールとグループの状態。

  • コントロールに視覚的に影響を与えるパブリック プロパティ。

新しい ControlTemplate を作成するユーザーは、コントロールのロジックで使用されるオブジェクト FrameworkElement 、各オブジェクトの種類、およびその名前を把握する必要があります。 また、 ControlTemplate 作成者は、コントロールが存在する可能性のある各状態の名前と、その状態 VisualStateGroup を把握する必要があります。

NumericUpDownの例に戻り、コントロールはControlTemplateに次のFrameworkElement オブジェクトがあることを想定しています。

コントロールの状態は次のとおりです。

コントロールが予期 FrameworkElement オブジェクトを指定するには、必要な要素の名前と型を指定する TemplatePartAttributeを使用します。 コントロールの使用可能な状態を指定するには、 TemplateVisualStateAttributeを使用します。このは、状態の名前と、そのコントロールが属する VisualStateGroup を指定します。 コントロールのクラス定義に TemplatePartAttributeTemplateVisualStateAttribute を配置します。

コントロールの外観に影響を与えるパブリック プロパティも、コントロール コントラクトの一部です。

次の例では、FrameworkElement コントロールのNumericUpDown オブジェクトと状態を指定します。

[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

コード例全体

次の例は、ControlTemplate コントロールのNumericUpDown全体です。

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


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


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

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

                </VisualState>

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

              <VisualStateGroup Name="FocusStates">

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

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

            </VisualStateManager.VisualStateGroups>

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

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

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

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

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

次の例は、 NumericUpDownのロジックを示しています。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

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

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

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

            set
            {
                SetValue(ValueProperty, value);
            }
        }

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

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

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

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

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

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

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

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

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

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

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

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

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

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

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

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

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

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


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

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

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

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

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

        public int Value
        {
            get { return _value; }
        }
    }
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

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

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

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

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

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

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

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

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

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

    Public Custom Event ValueChanged As ValueChangedEventHandler

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

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

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

    End Event


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


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

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

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

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

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

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

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

        Set(ByVal value As RepeatButton)

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

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

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

    Private m_upButtonElement As RepeatButton

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

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

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

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

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


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

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


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

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs

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

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

こちらも参照ください