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
<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>
注意が必要なのはこのファイルのビルドアクションを「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>
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; } } }
コンストラクタを定義し、まず「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」メソッドが呼び出されるので、これをオーバーライドすればいい。