Silverlightで再利用可能なカスタムコントロールを作る その1

Silverlightでカスタムのコントロールを作る場合、通常「UserControl」クラスを継承して作ることになるけど、この方法だと再利用性がかなり低い*1

なので、「UserControl」から派生するのではなく、UIコントロールの基底クラスである「Control」クラスから直接派生して、再利用可能なコントロールを作ってみた。
参考にしたのは以下のURLの記事

再利用可能といっても漠然としているので、今回はテンプレートを使って外観をカスタマイズできるようにすることを目的にする。
作るのは上下ボタンで値の増減ができるご存じ「NumericUpDown」コントロール。WinFormsでは標準で用意されているけど、Silverlightには今のところないのでこれを作る。

以下は画面イメージ

画面イメージ

この上下ボタンの部分をテンプレートでカスタマイズできるようにする。

プロジェクトの作成

開発にはVisual Studio 2008(Standard以上)を使用する。

まず「Silverlight アプリケーション」プロジェクトで「NumericUpDownDemo」というプロジェクトを作る。あわせてSilverlightコンテンツをホストするWebプロジェクトも作っておく。

プロジェクト構成

「NumericUpDownDemo」プロジェクトに「Controls」というフォルダを作成し、その中に「NumericUpDown.cs」というクラスファイルを追加する。

このクラスは「Control」クラスから派生させる。

Controls/NumericUpDown.cs
using System;
using System.Windows.Controls;

namespace NumericUpDownDemo.Controls {
    public class NumericUpDown : Control {
    }
}

必ずしも「Control」クラスから派生させる必要はなく、何かしらのコンテンツがある場合は「ContentControl」、コレクション要素を表示する場合は「ItemsControl」から派生させるなど用途に応じて使い分ければいい。

この時点でもコントロールとして十分機能しているので、以下のように「Page.xaml」に記述すれば何も表示されないが実行することはできる。

Page.xaml
<UserControl x:Class="NumericUpDownDemo.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:NumericUpDownDemo.Controls"
    Width="400" Height="300">
    
    <Grid x:Name="LayoutRoot">
        <my:NumericUpDown />
    </Grid>
</UserControl>

では、このコントロールの外観を作っていこう。見ての通り「UserControl」クラスを用いてカスタムコントロールを作る場合と異なり、このクラスには対応するXAMLファイルが存在しない。では一体どうやって作るのかというとWPFと同じ機構(らしい)を用いる。

「Generic.xaml」というXAMLファイルを用意し、この中でコントロールに対してスタイルを設定する。

とりあえずやってみよう。プロジェクトに「Generic.xaml」というファイルを追加し*2、以下のように記述する。

Generic.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:NumericUpDownDemo.Controls">
    
</ResourceDictionary>

ルート要素名が違うだけで名前空間は通常のXAMLと同じ。

注意が必要なのはこのファイルのビルドアクションを「Resource」にしておく必要があること。

あと、バグなのか仕様なのかはわからないが、プロジェクトのルートディレクトリに置いておかないと何故か認識しなかった。

スタイルの定義

では、こいつにスタイルを定義していく。

Generic.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:NumericUpDownDemo.Controls">

    <Style TargetType="my:NumericUpDown">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="my:NumericUpDown">
                    <Grid Background="{TemplateBinding Background}">
                        <TextBox x:Name="valueTextBox" HorizontalAlignment="Left" Margin="1" />

                        <StackPanel x:Name="buttonsPane" Grid.Column="1" Width="{TemplateBinding ButtonWidth}"
                                    VerticalAlignment="Center" HorizontalAlignment="Right">
                            <Button x:Name="upButton" Margin="0,0,0,0.5" Cursor="Hand">
                                <Polygon Points="0,3, 10,3, 5,0" Fill="Black"
                                         HorizontalAlignment="Center" VerticalAlignment="Center" />
                            </Button>

                            <Button x:Name="downButton" Margin="0,0.5,0,0" Cursor="Hand">
                                <Polygon Points="0,0, 10,0, 5,3" Fill="Black"
                                         HorizontalAlignment="Center" VerticalAlignment="Center" />
                            </Button>
                        </StackPanel>

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

「Template」プロパティにテンプレートを設定している。

Gridをコンテナにして左側にテキストボックス、右側に上下ボタンを配置している。上下ボタンのコンテナであるStackPanelに「ButtonWidth」というプロパティをバインドしているが、これはボタンの横幅を設定するための依存プロパティで後で定義する。

クラス側を以下のように変更する。

Controls/NumericUpDown.cs
using System;
using System.Windows;
using System.Windows.Controls;

namespace NumericUpDownDemo.Controls {
    public class NumericUpDown : Control {
        /// <summary>
        /// ButtonWidth
        /// </summary>
        public static readonly DependencyProperty ButtonWidthProperty = DependencyProperty.Register(
            "ButtonWidth", typeof(double), typeof(NumericUpDown), new PropertyMetadata((d, e) => { })
        );
        /// <summary>
        /// 上下ボタンの横幅を取得、設定します。
        /// </summary>
        public double ButtonWidth {
            get {
                return (double)this.GetValue(ButtonWidthProperty);
            }
            set {
                this.SetValue(ButtonWidthProperty, value);
            }
        }

        /// <summary>
        /// 標準的なコンストラクタ
        /// </summary>
        public NumericUpDown() {
            this.DefaultStyleKey = typeof(NumericUpDown);
            // デフォルトの値
            this.Width = 100;
            this.Height = 28;
            this.ButtonWidth = 24;
        }
    }
}

「ButtonWidth」という依存プロパティを定義している。

コンストラクタを定義し、まず「this.DefaultStyleKey = typeof(NumericUpDown);」というコードを記述しているが、これは「Generic.xaml」で定義したスタイルを適用するのに必要になる。意味はよくわからない。

あとはそれぞれのプロパティの規定値を設定している。

これで一度実行してみると、以下のように表示される。

当たり前だがテキストボックスの横幅が中途半端だ。この横幅はコントロール自信の横幅からボタンの横幅を引いた大きさにすればいい。

この処理はコードで書く必要がある。そのためにはまずテキストボックスへの参照を取ってくる必要があるが、これらのテキストボックスやボタンはスタイルで外部から設定されているため、いつものように「FindName」メソッドで取ってこれるわけではない。

では、どうするかというとスタイルからテンプレートが適用されると「OnApplyTemplate」というメソッドが呼び出されるようになっているので、このメソッドをオーバーライドすればいい。

Controls/NumericUpDown.cs
private TextBox valueTextBox;
private FrameworkElement buttonsPane;

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

    valueTextBox = (TextBox)this.GetTemplateChild("valueTextBox");
    buttonsPane = (FrameworkElement)this.GetTemplateChild("buttonsPane");
}

「GetTemplateChild」メソッドを使えばテンプレートからコントロールの参照を取得することができる。このコードを見ればわかる通り、このコントロールは「valueTextBox」という名前で定義されたTextBox(buttonsPaneも同様)がテンプレートの中に存在しないと成立しないことになる。

これではユーザが独自のテンプレートを定義する時にテンプレートにどんなコントロールが必要かわからないので、「TemplatePartAttribute」を使って知らせることができる(これに意味があるかどうかはよくわからない)。

Controls/NumericUpDown.cs
[TemplatePart(Name = "valueTextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "buttonsPane", Type = typeof(FrameworkElement))]
public class NumericUpDown : Control {
}

後はコントロールのサイズが変更された時にテキストボックスのサイズを合わせて変更してやればいい。コントロールのサイズが変更されると「ArrangeOverride」メソッドが呼び出されるので、これをオーバーライドすればいい。

Controls/NumericUpDown.cs
protected override Size ArrangeOverride(Size finalSize) {
    valueTextBox.Width = this.Width - buttonsPane.Width - 2;

    return base.ArrangeOverride(finalSize);
}

これで実行すればテキストボックスの横幅がいい感じに表示される。

なんか長くなってしまったので、次回に続く・・・

*1:例えば他のプロジェクトで使えない、継承ができないとか

*2:ここではApp.xamlをコピーして、コードビハインドファイルを削除して作った