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

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

既定のコントロール テンプレートを使用する CheckBox

既定のコントロール テンプレート付きのチェック ボックス

カスタム コントロール テンプレートを使用する CheckBox

カスタム コントロール テンプレート付きのチェック ボックス

Parts and States モデルに従って作成されたコントロールは、その外観をカスタマイズできます。 Parts and States モデルは、Microsoft Expression Blend などのデザイナー ツールでサポートされるため、Parts and States モデルに従って作成されたコントロールであれば、これらのデザイナー ツールでカスタマイズできます。 このトピックでは、Parts and States モデルとは何か、また、このモデルに従ってどのように独自のコントロールを作成するのかについて説明します。 また、カスタム コントロールの例として、NumericUpDown を使用しながら、このモデルの考え方を説明します。 NumericUpDown は数値を表示するコントロールです。ユーザーは、このコントロールのボタンをクリックして数値を増減させることができます。 次の図は、このトピックで説明する NumericUpDown コントロールを示しています。

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

NumericUpDown カスタム コントロール

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

  • 前提条件

  • Parts and States モデル

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

  • コードから ControlTemplate のパーツを使用する際はベスト プラクティスに従う

  • コントロール コントラクトを提供する

  • コード例全体

前提条件

このトピックでは、読者が既存のコントロールに新しい ControlTemplate を作成する方法を知っていること、コントロール コントラクトの要素に精通していること、および「ControlTemplate の作成による既存のコントロールの外観のカスタマイズ」で説明されている概念を理解していることを前提としています。

メモメモ

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

Parts and States モデル

Parts and States モデルでは、コントロールの視覚的な構造と視覚的な動作をどのように定義するかを指定します。 Parts and States モデルに準拠するには、次の作業が必要となります。

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

  • コントロールのロジックと (コントロール テンプレートの) パーツとの接点となる部分を、特定のベスト プラクティスに従って定義する。

  • ControlTemplate に含まれる必要のある事柄を規定した "コントロール コントラクト" を提供する。

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

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

Parts and States モデルに従ってカスタム コントロールを作成するときは、コントロールの視覚的な構造と視覚的な動作をコントロールのロジックではなく ControlTemplate で定義します。 コントロールの視覚的な構造は、そのコントロールを構成するさまざまな FrameworkElement オブジェクトの複合体と考えることができます。 視覚的な動作とは、コントロールが特定の状態になったときにどのように表示されるかということです。 ControlTemplate を作成して、コントロールの視覚的な構造と視覚的な動作を指定する方法の詳細については、「ControlTemplate の作成による既存のコントロールの外観のカスタマイズ」を参照してください。

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

次の例は、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 は負の値を赤色でしか表示できなくなります。 コントロールの視覚的な動作は ControlTemplateControlTemplateVisualState オブジェクトを追加して指定します。 次の例は、Positive または Negative の状態に対応する VisualState オブジェクトを示しています。 Positive と Negative は同時に指定できない (同時に 2 つの状態であることはできない) ため、この例では VisualState オブジェクトを単一の VisualStateGroup にまとめています。 コントロールが Negative 状態に移行した場合、TextBlockForeground が赤色に変化します。 コントロールが Positive 状態の場合、Foreground が元の値に戻ります。 ControlTemplateVisualState オブジェクトを定義する方法については、「ControlTemplate の作成による既存のコントロールの外観のカスタマイズ」で詳しく説明しています。

メモメモ

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 オブジェクトを厳密に定義するとは限りません。意図的に、または間違いによって、それらのオブジェクトが省略される可能性があります。しかし、カスタム コントロールのロジックでは、これらのパーツが適切に機能することが必要となる場合があります。 ControlTemplate からの FrameworkElementVisualState オブジェクトの欠落に対応できることは、Parts and States モデルがコントロールに対して求めている条件の 1 つでもあります。 FrameworkElementVisualStateVisualStateGroup などが ControlTemplate に存在しない場合でも、カスタム コントロールが例外をスローする、またはエラーを報告することのないようにする必要があります。 このセクションでは、FrameworkElement オブジェクトとの対話や状態の管理に関するベスト プラクティスについて説明します。

FrameworkElement オブジェクトが存在しない可能性を想定する

ControlTemplateFrameworkElement オブジェクトを定義する場合、カスタム コントロールのロジックの中で、そのいくつかのオブジェクトとの対話が必要になることがあります。 たとえば、NumericUpDown コントロールは、ボタンの Click イベントをサブスクライブして Value を増減させ、TextBlockText プロパティを Value に設定します。 カスタム ControlTemplateTextBlock やボタンが省略され、そのコントロールのいくつかの機能が失われていても、コントロールは使用できます。ただし、そのような場合でも、コントロールがエラーを発生させないようにする必要があります。 たとえば、ControlTemplate に Value を変更するためのボタンが存在しないと、NumericUpDown はその機能を果たせなくなりますが、それによって、ControlTemplate を使用しているアプリケーションの実行が中断されることは避ける必要があります。

FrameworkElement オブジェクトの欠落に適切に対処するためのベスト プラクティスを次に示します。

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

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

  3. カスタム コントロールで処理するイベントのサブスクライブとアンサブスクライブを、FrameworkElement プロパティの set アクセサーで行います。

  4. 手順 2. で定義した FrameworkElement プロパティを、OnApplyTemplate メソッドで設定します。 これが、コントロールから ControlTemplateFrameworkElement にアクセスできる最も早いタイミングです。 FrameworkElementControlTemplate から取得する際は、x:Name を使用します。

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

以上の推奨事項に従って作成された NumericUpDown コントロールの例を次に示します。FrameworkElement オブジェクトの扱いに注目しながら参照してください。

この例では、NumericUpDown コントロールの視覚的な構造が ControlTemplate で定義されています。Value を増やすための RepeatButton は、その x:Name 属性が UpButton に設定されています。 次の例を見ると、ControlTemplate で定義された RepeatButton を表す UpButtonElement というプロパティが宣言されていることがわかります。 set アクセサーでは、まず、UpDownElement が null 以外の場合に、ボタンの 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 を示しています。 この例では、ControlTemplate から FrameworkElement オブジェクトを取得するために、GetTemplateChild メソッドが使用されています。 GetTemplateChild で、想定外の型の名前が指定された FrameworkElement が見つかった場合の対策がなされている点に注目してください。 また、指定された x:Name を持つ要素でも、型が間違っている場合は無視しています。これも 1 つのベスト プラクティスです。

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);
}

ControlTemplateFrameworkElement が存在していなくても、前の例に示したベスト プラクティスに従っていれば、コントロールの実行が妨げられることはありません。

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

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

次の例は、前の例に続き、コントロールの Positive 状態および Negative 状態に対応する VisualState オブジェクトを示しています。 Negative VisualStateStoryboard は、TextBlockForeground を赤色に変化させます。 NumericUpDown コントロールが Negative 状態になると、Negative 状態のストーリーボードが開始されます。 Negative 状態の Storyboard は、コントロールが Positive 状態に戻ると停止します。 Negative の Storyboard が停止した場合、Foreground が元の色に戻るため、Positive VisualState には、必ずしも 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 は NumericUpDown のコントロール コントラクトに存在しないことに注意してください。これは、コントロールのロジックが TextBlock を参照しないためです。 名前を持つ要素は ControlTemplate で参照されますが、コントロール コントラクトの一部である必要はありません。コントロールの新しい ControlTemplate は、その要素を参照する必要がないことがあるためです。 たとえば、NumericUpDown の新しい ControlTemplate を作成する人が、Foreground を変更して、その Value が負数であることを示さなかったとします。 この場合、コードも ControlTemplateTextBlock を名前で参照しません。

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

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 メソッドは、ストーリーボードを開始または停止するために必要なロジックを実行します。 コントロールがその状態を変化させるために GoToState を呼び出すと、VisualStateManager によって次の処理が実行されます。

  • これから移行しようとしている VisualStateStoryboard が存在する場合は、そのストーリーボードを開始します。 さらに、移行前の VisualStateStoryboard が存在する場合は、そのストーリーボードを終了します。

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

  • control の ControlTemplate に存在しない状態が指定された場合、GoToState は何も実行せずに、false を返します。

VisualStateManager の使用上のベスト プラクティス

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

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

  • 状態を遷移させるためのヘルパー メソッドを作成する。

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

単一のメソッドですべての状態を更新して VisualStateManager への呼び出しを集中化することで、コードを管理できる状態に保ちます。 次の例は、NumericUpDown コントロールのヘルパー メソッド 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 状態のストーリーボードは中断されず、ユーザーから見てもコントロールの外観は変化しません。

VisualStateManager は、GoToState が呼び出されたときに、どの状態を終了させればよいかを、VisualStateGroup オブジェクトを使って判断します。 VisualStateGroupControlTemplate に定義されており、コントロールは、それぞれの VisualStateGroup につき必ず 1 つの状態を持ちます。この状態が失われるのは、同じ VisualStateGroup 内の別の状態に移行するときだけです。 たとえば、NumericUpDown コントロールの ControlTemplate には、Positive と Negative という VisualState オブジェクトと、Focused と Unfocused という VisualState オブジェクトが、それぞれ異なる VisualStateGroup に定義されています (Focused と Unfocused VisualState が定義されているところは、このトピックの「コード例全体」で参照できます)。コントロールの状態が Positive から Negative に (または逆の状態に) 変化した場合、コントロールは Focused または Unfocused の状態のままになります。

一般に、コントロールの状態が変化するタイミングとしては、次の 3 つが考えられます。

  • ControlControlTemplate が適用されたとき。

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

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

それぞれのケースで NumericUpDown コントロールの状態を更新する例を次に示します。

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

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 が変化するたびにメソッドで呼び出されるため、UpdateStates の呼び出しは、Value が正から負に (または負から正に) 変化したときに行われます。 正数であるか負数であるかにかかわらず 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 作成者は、これから作成しようとしているテンプレートに何を定義するのかを知ることができます。 コントロール コントラクトには、3 つの要素があります。

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

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

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

新しい ControlTemplate の作成者が、まず必要とする情報は、コントロールのロジックに使用されている FrameworkElement オブジェクトと、それぞれのオブジェクトの型および名前です。 また、ControlTemplate の作成者は、さらにコントロールに想定されているすべての状態の名前と、それぞれの状態がどの VisualStateGroup に属しているかを知っておく必要があります。

前の NumericUpDown コントロールの場合、ControlTemplate には次の FrameworkElement オブジェクトが必要です。

コントロールに想定されている状態は次のとおりです。

コントロールに必要な FrameworkElement オブジェクトを指定するには、TemplatePartAttribute を使用して、必要な要素の名前と型を指定します。 コントロールに想定されている状態を指定するには、TemplateVisualStateAttribute を使用して、状態の名前とその所属先の VisualStateGroup を指定します。 TemplatePartAttribute および TemplateVisualStateAttribute は、コントロールのクラス定義に記述します。

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

次の例では、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 の作成による既存のコントロールの外観のカスタマイズ

コントロールのカスタマイズ