かずきのBlog@hatena

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

WPF4.5入門 その53 「ユーザーコントロール」

コントロールの作成

WPFでは、コントロールの見た目を少し変えたいといった要求は、StyleやDataTemplate、ControlTemplateを使って簡単に実現できます。そのため、Windows Formのころに行っていた、見た目を変えるためのカスタムコントロールの作成は、ほとんど不要になっています。複数のコントロールを組み合わせたコントロールの作成や、独自の動作をするコントロールなど既存のコントロールのカスタマイズで対応できないような要件のみに限られています。

UserControl

UserControlは、複数のコントロールを組み合わせたコントロールを作成するのに向いています。UserControlは、Visual Studioのアイテムテンプレートからユーザーコントロール(WPF)を選択することで作成できます。これまでのWindowをベースに開発していたのと同じ要領で、デザイナを使って開発が出来る点が大きな特徴です。

以下にUserControlをデザイナで開いている画面を示します。Windowの開発と変わりがないことが確認できます。

f:id:okazuki:20140908203417p:plain

UserControlの例として、NumericUpDownコントロールを作成する手順について示します。新規作成からユーザーコントロール(WPF)を選択し、NumericUpDownという名前で作成します。作成したら、以下のように2行2列のGridを作り数字を表示するためのTextBlockと、数字を増やしたり減らしたりするためのRepeatButtonを置きます。

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:local="clr-namespace:CreateControlSample01" 
    x:Class="CreateControlSample01.NumericUpDown" 
    mc:Ignorable="d" 
    d:DesignHeight="100" d:DesignWidth="287">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <RepeatButton Content="Up" Grid.Column="1" Margin="2.5" Click="UpButton_Click"/>
        <RepeatButton Content="Down" Grid.Column="1" Grid.Row="1" 
Margin="2.5" Click="DownButton_Click"/>
        <TextBlock x:Name="textBlockValue" Grid.RowSpan="2" TextWrapping="Wrap" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5" Foreground="Black"/>
    </Grid>
</UserControl>

次に、NumericUpDownの値を保持するためのValue依存関係プロパティをNumericUpDownコントロールに作成します。

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

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

画面のTextBlockのTextプロパティとValueプロパティをバインドします。今回は、BindingのRelativeSourceというものを使ってBindingの元になるオブジェクトを、コントロールのツリーを親へ親へ辿っていってNumericUpDownコントロールに行きあたるまで探索するように指定しています(FindAncestor)。

Text="{Binding Value, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:NumericUpDown}}}"

そして、RepeatButtonのクリックイベントで、Valueプロパティの値を操作します。

private void UpButton_Click(object sender, RoutedEventArgs e)
{
    this.Value++;
}

private void DownButton_Click(object sender, RoutedEventArgs e)
{
    this.Value--;
}

これで、NumeridUpDownコントロールは完成です。UserControlを作成すると、デザイナのツールボックスで自動的に表示されるので、通常のコントロールと同じ要領で画面に配置することが出来ます。

f:id:okazuki:20140908203624p:plain

VisualStateManager

UserControlでNumericUpDownコントロールを作成したので、ここで、コントロールの状態に応じてアニメーションを行うVisualStateManagerという機能について紹介します。VisualStateManagerは、見た目の状態を管理する機能です。StyleのTriggerなどでIsMouseOverがTrueの時にアニメーションを実行するといったことが可能でしたが、VisualStateManagerは、状態に名前を付けて管理することが出来る点が異なります。状態の遷移はプログラムから行うので、Triggerに比べてより複雑な条件を指定することが出来ます。

VisualStateManagerは、VisualStateManagerクラスのVisualStateGroups添付プロパティでコントロールに対して設定します。VisualStateGroups添付プロパティには、x:Nameで名前を付けたVisualStateGroupを設定します。VisualStateGroupの中には、x:Nameで名前をつけたVisualStateが定義できます。このVisualStateの中にStoryboardを設定してアニメーションを定義します。VisualStateGroupの役割ですが、同一のVisualStateGroup内のVisualStateは同時に1つしかアクティブになれないという制約があります。逆にいうと、異なる意味を持つVisualStateを別のVisualStateGroupに置くことで、同時に複数のVisualStateを有効にするといったことが可能になっています。

ここでは、Valueの値がマイナスのときだけValueの値を赤色にするVisualStateを定義したいと思います。VisualStateGroupの名前をNegativePositiveにして、その中にNegativeとPositiveというVisualStateを定義します。

<UserControl
    ...省略...
    d:DesignHeight="100" d:DesignWidth="287">
    <Grid>
        <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>
</UserControl>

NegativeのVisualState内には、TextBlockの値を赤色にするアニメーションを定義しています。VisualStateの定義が終わったのでコードビハインドで、VisualStateの切り替え処理を書きます。VisualStateの切り替えは、VisualStateManager.GoToStateメソッドを使います。GoToStateメソッドは、VisualStateを切り替えるコントロールと、VisualState名と、VisualStateが切り替わるときのアニメーション効果を使用するかどうかを設定します。

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 NumericUpDown()
{
    InitializeComponent();
    this.UpdateState(false);
}

...省略...

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

UpdateStateというVisualStateを切り替える処理を作り、Valueプロパティの変更時と、NumericUpDownコントロールの初期化時に呼び出しています。初期化のときはアニメーション効果は不要なため切り替え効果はfalseを指定しています。Valueプロパティの値が変わった時は切り替え効果を有効にするためtrueを設定しています。

NumeridUpDownコントロールを実行すると以下のようにマイナスのときは赤色になることが確認できます。

f:id:okazuki:20140908203837p:plain

VisualStateManagerは、フォーカスの状態に応じた見た目の管理など組み込みのコントロールの様々な個所で使用されています。

BlendでのVisualStateManagerの設定方法

VisualStateManagerは、複雑になると手書きするのが大変になってくるためBlendを使って作成するのが一般的です。Blendの状態タブを開くとベースという状態がデフォルトで選択されていて、その下にVisualStateGroupとVisualStateを定義できるようになっています。VisualStateを選択すると、通常のアニメーション作成と同じ要領で切り替え時の動作を設定できます。

以下の画面例は、NumericUpDownコントロールでNegativeのVisualStateを選択したときの表示です。

f:id:okazuki:20140908203928p:plain

過去記事