かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF4.5入門 その54 「カスタムコントロール」

UserControlで、独自コントロールを作る方法を紹介しましたが、UserControlではできないことがあります。ControlTemplateへ対応です。ControlTemplateへ対応した完全なWPFの独自コントロールを作るには、これから紹介するカスタムコントロールを作成する必要があります。

カスタムコントロールは、新規作成のカスタムコントロール(WPF)から作成します。作成すると、クラスが1つとThemesフォルダの中にGeneric.xamlが作成されます。このGeneric.xaml内にコントロールのデフォルトのStyleを定義してコントロールを作成します。コントロールのデフォルトのStyleのキーはクラスの静的コンストラクタで以下のようにDefaultStyleKey依存関係プロパティのデフォルト値を上書きすることで指定されています。

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown), 
new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}

Generic.xamlは、以下のようにデフォルトのStyle(型名がキーのStyle)のみが定義されています。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CreateControlSample02">
    <Style TargetType="{x:Type local:NumericUpDown}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

ここでは、UserControlと同じようにNumericUpDownコントロールを作成していきます。UserControlの時と同じようにXAMLを定義します。異なる点は、イベントハンドラの紐づけはXAMLで行わない点です。カスタムコントロールではXAMLではなく、C#で指定します。C#から参照するために、RepeatButtonには名前をつけています。名前は、カスタムコントロールではPART_名前という命名規約でつけることが多いです。UserControlと同様にVisualStateManagerの定義と、TextBlockのTextプロパティをコントロールのValueプロパティとBindingしています。

<ControlTemplate TargetType="{x:Type local:NumericUpDown}">
    <Border Background="{TemplateBinding Background}"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}" d:DesignWidth="231" d:DesignHeight="86">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="PositiveNegative">
                <VisualState x:Name="Positive" />
                <VisualState x:Name="Negative">
                    <Storyboard>
                        <ColorAnimation
                            Storyboard.TargetName="textBlockValue"
                            Storyboard.TargetProperty="Foreground.Color"
                            To="Red" />
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="21*"/>
                <RowDefinition Height="22*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock x:Name="textBlockValue" TextWrapping="Wrap" 
                       Text="{Binding Value, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:NumericUpDown}}}" 
                       Height="Auto" Grid.RowSpan="2" Width="Auto" 
                       HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="Black" />
            <RepeatButton x:Name="PART_UpButton" Content="Up" Grid.Column="1" Height="Auto" Width="Auto" Margin="2.5"/>
            <RepeatButton x:Name="PART_DownButton" Content="Down" Grid.Column="1" Grid.Row="1" Margin="2.5"/>
        </Grid>
    </Border>
</ControlTemplate>

NumericUpDownコントロールでは、UserControlと同様にValue依存関係プロパティの定義と、VisualStateの切り替えのコードを記述します。

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

private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((NumericUpDown)d).UpdateState(true);
}


public int Value
{
    get { return (int)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

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

カスタムコントロールのRepeatButtonにイベントハンドラの紐づけを行います。これは、カスタムコントロールにテンプレートが適用されたときに呼び出されるOnApplyTemplateメソッドで行います。ここでは、古いテンプレートから取得したコントロールの後始末と、新しいテンプレートから取得したコントロールの初期化を行います。ここでは、イベントハンドラの解除と登録がそれにあたります。テンプレートで定義されたコントロールの取得にはGetTemplateChildメソッドで名前を指定して取得します。

// XAMLで定義されたボタン格納用変数
private RepeatButton upButton;
private RepeatButton downButton;

// ボタンのクリックイベント
private void UpClick(object sender, RoutedEventArgs e) { this.Value++; }
private void DownClick(object sender, RoutedEventArgs e) { this.Value--; }

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    // 前のテンプレートのコントロールの後処理
    if (this.upButton != null)
    {
        this.upButton.Click -= this.UpClick;
    }
    if (this.downButton != null)
    {
        this.downButton.Click -= this.DownClick;
    }

    // テンプレートからコントロールの取得
    this.upButton = this.GetTemplateChild("PART_UpButton") as RepeatButton;
    this.downButton = this.GetTemplateChild("PART_DownButton") as RepeatButton;

    // イベントハンドラの登録
    if (this.upButton != null)
    {
        this.upButton.Click += this.UpClick;
    }
    if (this.downButton != null)
    {
        this.downButton.Click += this.DownClick;
    }

    // VSMの更新
    this.UpdateState(false);
}

このコントロールは、ControlTemplateをサポートした完全なコントロールです。以下のように定義することで見た目のカスタマイズが使用者側で出来るようになっています。

<StackPanel>
    <!-- 通常の見た目 -->
    <local:NumericUpDown />
    <!-- コントロールテンプレートの差し替え -->
    <local:NumericUpDown>
        <local:NumericUpDown.Template>
            <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
                <StackPanel>
                    <RepeatButton x:Name="PART_UpButton" Content="Up" />
                    <TextBlock Text="{Binding Value, RelativeSource={RelativeSource AncestorType=local:NumericUpDown}}" 
                               HorizontalAlignment="Center"/>
                    <RepeatButton x:Name="PART_DownButton" Content="Down" />
                </StackPanel>
            </ControlTemplate>
        </local:NumericUpDown.Template>
    </local:NumericUpDown>
</StackPanel>

実行結果を以下に示します。テンプレートが置き換わってUpボタンとDownボタンの位置が変わっていることが確認できます。

f:id:okazuki:20140908221204p:plain

過去記事