外観をカスタマイズできるコントロールの作成

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

A checkbox with the default control template. 既定のコントロール テンプレートを使用するチェックボックス

A checkbox with a custom control template. カスタム コントロール テンプレートを使用するチェックボックス

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

NumericUpDown custom control. カスタム NumericUpDown コントロール

このトピックは、次のセクションで構成されています。

必須コンポーネント

このトピックを理解するには、既存のコントロール用に新しい ControlTemplate を作成する方法、コントロール コントラクトの要素、「コントロールのためのテンプレートを作成する」で説明されている概念を理解している必要があります。

注意

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

パーツと状態モデル

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

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

  • コントロールのロジックでコントロール テンプレートのパーツとやり取りするときに、特定のベスト プラクティスに従います。

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

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

ControlTemplate でのコントロールの視覚的構造と視覚的動作の定義

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

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

次の例では、NumericUpDown コントロールの視覚的構造を示します。これには、Value を増やすための RepeatButtonValue を減らすための RepeatButtonValue を表示するための 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 でコントロールの視覚的動作を指定するには、ControlTemplateVisualState オブジェクトを追加します。 次の例では、Positive 状態と Negative 状態に対する VisualState オブジェクトを示します。 PositiveNegative は相互に排他的である (コントロールは常に必ず 2 つの状態のどちらか一方になります) ため、この例では VisualState オブジェクトを 1 つの VisualStateGroup に格納します。 コントロールが Negative 状態になると、TextBlockForeground が赤に変わります。 コントロールが Positive 状態のときは、Foreground は元の値に戻ります。 ControlTemplate での VisualState オブジェクトの定義については、「コントロールのためのテンプレートを作成する」で詳しく説明されています。

注意

VisualStateManager.VisualStateGroups 添付プロパティの設定は、ControlTemplate のルート FrameworkElement で必ず行ってください。

<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 オブジェクトを省略する場合がありますが、コントロールのロジックでは、それらのパーツを正しく機能させることが必要な場合があります。 パーツと状態モデルでは、FrameworkElement オブジェクトまたは VisualState オブジェクトが不足している ControlTemplate に対して、コントロールに回復性が必要であることが指定されています。 FrameworkElementVisualState、または VisualStateGroupControlTemplate にない場合でも、コントロールで例外をスローしたり、エラーを報告したりしてはなりません。 ここでは、FrameworkElement オブジェクトとの操作と状態の管理に関して推奨される方法について説明します。

FrameworkElement オブジェクトがない場合を想定する

ControlTemplateFrameworkElement オブジェクトを定義した場合、コントロールのロジックでその一部を操作することが必要になる場合があります。 たとえば、NumericUpDown コントロールでは、Value を増減するためにボタンの Click イベントをサブスクライブし、TextBlockText プロパティを Value に設定します。 カスタム ControlTemplateTextBlock またはボタンが省略されている場合、コントロールの機能が部分的に失われてもかまいませんが、コントロールが原因でエラーが発生しないようにする必要があります。 たとえば、ControlTemplateValue を変更するボタンが含まれていない場合、NumericUpDown の機能は失われますが、ControlTemplate を使用するアプリケーションは実行を続けます。

次の方法を使用すると、FrameworkElement オブジェクトがなくてもコントロールは正しく応答するようになります。

  1. コードで参照する必要のある各 FrameworkElement に対して、x:Name 属性を設定します。

  2. 操作する必要がある各 FrameworkElement に対して、プライベート プロパティを定義します。

  3. FrameworkElement プロパティの set アクセサーで、コントロールが処理するすべてのイベントをサブスクライブおよびサブスクライブ解除します。

  4. ステップ 2 で定義した FrameworkElement プロパティを、OnApplyTemplate メソッドで設定します。 これは、ControlTemplate 内の FrameworkElement をコントロールで使用できる最も早い段階です。 それを ControlTemplate から取得するには、FrameworkElementx:Name を使用します。

  5. メンバーにアクセスする前に、FrameworkElementnull ではないことを確認します。 null であっても、エラーを報告しないようにします。

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

ControlTemplateNumericUpDown コントロールの視覚的構造を定義する例では、Value を増やす RepeatButtonx:Name 属性が UpButton に設定されています。 次の例では、ControlTemplate で宣言されている RepeatButton を表す UpButtonElement という名前のプロパティが宣言されています。 set アクセサーでは、UpDownElementnull ではない場合は、最初にボタンの 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 オブジェクトを取得します。 この例では、名前は指定されたものであっても型が予想とは異なる FrameworkElementGetTemplateChild で検出された場合に対する保護が行われていることに注意してください。 また、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 では、コントロールの状態が追跡され、状態間の遷移に必要なロジックが実行されます。 ControlTemplateVisualState オブジェクトを追加する場合は、それらを VisualStateGroup に追加し、VisualStateGroupVisualStateManager.VisualStateGroups 添付プロパティに追加して、VisualStateManager でそれらにアクセスできるようにします。

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

<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 が参照されないため、TextBlockNumericUpDown のコントロール コントラクトに含まれません。 ControlTemplate で参照される要素には名前がありますが、コントロールの新しい ControlTemplate でその要素を参照する必要がない場合があるため、コントロール コントラクトに含める必要はありません。 たとえば、NumericUpDown の新しい ControlTemplate を作成する開発者は、Foreground を変更することによって Value が負であることを示す必要はないと判断することがあります。 その場合、コードでも 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 では次のことが行われます。

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

  • コントロールが既に指定された状態になっている場合、GoToState では何も行われず、true が返されます。

  • 指定された状態が controlControlTemplate に存在しない場合、GoToState では何も行われず、false が返されます。

VisualStateManager の操作に関するベスト プラクティス

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

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

  • ヘルパー メソッドを作成して、状態間の切り替えを行います。

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

すべての状態を 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 状態のストーリーボードは中断されず、ユーザーが目にするコントロールに変化はありません。

GoToState を呼び出すと、VisualStateManager では VisualStateGroup オブジェクトを使用して、終了する状態が決定されます。 コントロールは、その ControlTemplate で定義されている各 VisualStateGroup に対して常に 1 つの状態になり、同じ VisualStateGroup から別の状態になるときにのみ状態が変わります。 たとえば、NumericUpDown コントロールの ControlTemplate では、PositiveNegativeVisualState オブジェクトが 1 つの VisualStateGroup で定義されており、FocusedUnfocusedVisualState オブジェクトが別のグループで定義されています。 (定義されている FocusedUnfocusedVisualState は、このトピックの「完全な例」セクションで確認できます)。コントロールが Positive 状態から Negative 状態に変化すると、またはその逆に変化しても、コントロールは Focused 状態または Unfocused 状態のままになります。

コントロールの状態は、一般に次の 3 つの場合に変わる可能性があります。

  • ControlTemplateControl に適用されたとき。

  • プロパティが変化したとき。

  • イベントが発生したとき。

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

ControlTemplate が適用されたときにコントロールが正しい状態で表示されるように、OnApplyTemplate メソッドでコントロールの状態を更新する必要があります。 次の例では、OnApplyTemplateUpdateStates を呼び出して、コントロールが適切な状態であることを確認しています。 たとえば、NumericUpDown コントロールを作成し、その Foreground を緑に、Value を -5 に設定するとします。 NumericUpDown コントロールに ControlTemplate が適用されたときに UpdateStates を呼び出さないと、コントロールは Negative 状態にならず、値は赤ではなく緑色になります。 UpdateStates を呼び出して、コントロールを Negative 状態にする必要があります。

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

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

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

    UpdateStates(False)
End Sub

プロパティが変化したら、多くの場合、コントロールの状態を更新する必要があります。 次の例では、ValueChangedCallback メソッド全体を示します。 Value が変化すると ValueChangedCallback が呼び出されるので、Value が正から負または負から正に変化したら、そのメソッドで UpdateStates を呼び出します。 Value は変化したが、正または負はそのままである場合でも、UpdateStates を呼び出してかまいません。その場合、コントロールの状態は変更されないためです。

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

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

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
            newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

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

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

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

また、イベントが発生したときも、状態の更新が必要になる場合があります。 次の例では、NumericUpDownControl に対して UpdateStates を呼び出し、GotFocus イベントを処理しています。

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

VisualStateManager は、コントロールの状態を管理するのに役立ちます。 VisualStateManager を使用すると、コントロールの状態を正しく遷移させることができます。 このセクションで説明されている推奨事項に従って VisualStateManager を操作すると、コントロールのコードが読みやすくなり、保守も容易になります。

コントロール コントラクトの提供

テンプレートに配置する内容を ControlTemplate の作成者がわかるように、コントロール コントラクトを提供します。 コントロール コントラクトには、次の 3 つの要素があります。

  • コントロールのロジックが使用する視覚的要素。

  • コントロールの状態および各状態が所属するグループ。

  • コントロールに対して視覚的に作用するパブリック プロパティ。

新しい 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

関連項目