さて、こんなタイトルで記事を書きながら、何もカスタムコントロールを作ってない前回でしたが、今回からは何か作っていきます!!
ということで、今回はDomainUpDownコントロールの超劣化版を作ってみようと思います。
WindowsFormのDomainUpDownとは、ちょっと違った動きをするものを作ってみようと思います。
どう違うかというと、上矢印ボタンを押して項目の最後まで移動したときに最初の項目に移動させる。下矢印ボタンを押して項目の最初まで移動したときに最後の項目に移動させる。
例えば、下のように選択状態を遷移させたいということです。
項目が3つある時にボタンをどんどん押した時選択項目の遷移
未選択 → 項目1 → 項目2 → 項目3 → 未選択 → 項目1 → …
未選択状態の時に、なんて表示するかもカスタマイズできると嬉しいですね。
ということで、以下のような感じで作っていきます!
基本となるクラスを選定
DomainUpDownSampleという名前で、プロジェクトを作成します。
新規作成で、DomainUpDownという名前でカスタムコントロールを作成します。
このウィザードで新規作成すると、基本クラスとしてControlが作成されています。
今回のDomainUpDownコントロールは、複数項目の中から1つの項目を選択するものなので、ItemsControlの子にあたるSystem.Windows.Controls.Primitives.Selectorを継承して作成します。
using System.Windows;
using System.Windows.Controls.Primitives;
namespace DomainUpDownSample
{
public class DomainUpDown : Selector
{
static DomainUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DomainUpDown), new FrameworkPropertyMetadata(typeof(DomainUpDown)));
}
}
}
見た目の作成(仮)
次に、見た目を作るためにGeneric.xamlを編集します。
DomainUpDownコントロールには、上下に選択項目を移動させるButtonと現在選択中の項目を表示させるためのContentControlをGridに配置しています。
この中で、コントロールの動作に影響を与える2つのボタンは、必須のコントロールなので、~Partという名前をつけておきます。(そういう慣例っぽい)
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DomainUpDownSample">
<Style TargetType="{x:Type local:DomainUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DomainUpDown}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- SelectedItem -->
<ContentControl
Name="selectedContent"
Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding SelectedItem}"
ContentTemplate="{TemplateBinding ItemTemplate}" />
<!-- UpButton -->
<Button
Grid.Row="0" Grid.Column="1"
Name="UpButtonPart"
Padding="1"
MinWidth="8"
MinHeight="8">
<Polygon Points="0,6 6,6 3,0" Fill="Black" />
</Button>
<!-- DownButton -->
<Button
Grid.Row="1" Grid.Column="1"
Name="DownButtonPart"
Padding="1"
MinWidth="8"
MinHeight="8">
<Polygon Points="0,0 6,0 3,6" Fill="Black"/>
</Button>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
とりあえず、見た目を確認するために、Window1.xamlに適当においてみます。
<Window x:Class="DomainUpDownSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:DomainUpDownSample"
Title="Window1" Height="75" Width="150">
<Grid>
<l:DomainUpDown />
</Grid>
</Window>
実行すると下のようになります。それっぽい。
裏方の作成
見た目がそれっぽくなったので、裏方の作成をします。
このDomainUpDownコントロールには、必須のコントロールとして、UpButtonPartとDownButtonPartという名前のButtonコントロールがあります。
このことを示すために、TemplatePartAttributeという属性を使って外に明示するのが、お作法になってるみたいなので、やっておきます。
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
namespace DomainUpDownSample
{
[TemplatePart(Name = UpButtonPart, Type = typeof(Button))]
[TemplatePart(Name = DownButtonPart, Type = typeof(Button))]
public class DomainUpDown : Selector
{
#region TemplateParts
private const string UpButtonPart = "UpButtonPart";
private const string DownButtonPart = "DownButtonPart";
#endregion
static DomainUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DomainUpDown), new FrameworkPropertyMetadata(typeof(DomainUpDown)));
}
}
}
ここら辺は、特にやらなくてもたぶん動作には関係ないと思います…。(Blendとか使うときに困るかも?)
次に、ボタンが押されたときの処理を記述していきます。
まず、Generic.xamlに定義されている2つのボタンを取得しないと話しになりません。
これは、どうやって取得するのかというとOnApplyTemplateというメソッドをオーバーライドして、そこで取得します。
OnApplyTemplateは、テンプレートが適用されたときに呼び出されるので、テンプレート内に定義されているコントロールを取得するのには最適です。
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
namespace DomainUpDownSample
{
[TemplatePart(Name = UpButtonPart, Type = typeof(Button))]
[TemplatePart(Name = DownButtonPart, Type = typeof(Button))]
public class DomainUpDown : Selector
{
#region TemplateParts
// 省略
#endregion
#region UpDownButton
// 省略
#endregion
static DomainUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DomainUpDown), new FrameworkPropertyMetadata(typeof(DomainUpDown)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Templateが変わったので、古いテンプレートのボタンに登録してある
// イベントハンドラは消しておく
if (_upButton != null)
{
_upButton.Click -= UpExecute;
}
if (_downButton != null)
{
_downButton.Click -= DownExecute;
}
// 新しいテンプレートからボタンを取得してイベントハンドラと結びつける
_upButton = GetTemplateChild(UpButtonPart) as Button;
if (_upButton != null)
{
_upButton.Click += UpExecute;
}
_downButton = GetTemplateChild(DownButtonPart) as Button;
if (_downButton != null)
{
_downButton.Click += DownExecute;
}
}
/// <summary>
/// 上矢印ボタンが押されたときの処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UpExecute(object sender, RoutedEventArgs e)
{
// TODO : あとで
}
/// <summary>
/// 下矢印ボタンが押されたときの処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DownExecute(object sender, RoutedEventArgs e)
{
// TODO : あとで
}
}
}
後は、UpExecuteメソッドとDownExecuteメソッドでSelectedIndexの値を更新します。
/// <summary>
/// 上矢印ボタンが押されたときの処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UpExecute(object sender, RoutedEventArgs e)
{
var index = SelectedIndex + 1;
if (index >= this.Items.Count)
{
// indexの範囲が要素数を超えた場合は未選択状態に戻す
index = -1;
}
SelectedIndex = index;
}
/// <summary>
/// 下矢印ボタンが押されたときの処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DownExecute(object sender, RoutedEventArgs e)
{
var index = SelectedIndex - 1;
if (index < -1)
{
// indexの範囲が未選択状態よりも小さくなったら最後の要素を選択した状態にする
index = this.Items.Count - 1;
}
SelectedIndex = index;
}
SelectedIndexが範囲外にならないように、値のチェックをしてから更新をしています。
とりあえず動かしてみよう
この状態でも、実はもう動いたりします。
Window1.xamlを以下のように書き換えて、DomainUpDownに項目を追加して動かしてみます。
<Window x:Class="DomainUpDownSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:DomainUpDownSample"
Title="Window1" Height="75" Width="150">
<Grid>
<l:DomainUpDown>
<ContentControl>aaa</ContentControl>
<ContentControl>bbb</ContentControl>
<ContentControl>ccc</ContentControl>
</l:DomainUpDown>
</Grid>
</Window>
実行すると…
最初は何も選択されていません
ボタンを押すと選択項目が順番に変わっていきます
未選択状態の表示をカスタマイズしよう
意外とあっさり動いちゃった感じがしますので、もうちょい作っていきます。
今の状態だと、何も選択されていない時が真っ白で味気ないです。このときの表示をカスタマイズできるようにしてみます。
何も選択されたない時の見た目は、DataTemplateで定義できるようにしようと思います。
なので、DomainUpDownコントロールに「NoSelectedTemplate」という名前の依存プロパティを追加します。
#region NoSelectedTemplate
/// <summary>
/// 未選択状態の時に適用するテンプレートを取得または設定する。
/// </summary>
public DataTemplate NoSelectedTemplate
{
get { return (DataTemplate)GetValue(NoSelectedTemplateProperty); }
set { SetValue(NoSelectedTemplateProperty, value); }
}
// デフォルト状態では何も無いテンプレートを使用する。
public static readonly DependencyProperty NoSelectedTemplateProperty =
DependencyProperty.Register("NoSelectedTemplate", typeof(DataTemplate), typeof(DomainUpDown), new UIPropertyMetadata(new DataTemplate()));
#endregion
そして、Generic.xamlのControlTemplateに、未選択状態の時に表示するContentControlを追加して、初期状態で見えないようにしておきます。
<Style TargetType="{x:Type local:DomainUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DomainUpDown}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<!-- 省略 -->
<!-- SelectedItem -->
<!-- 省略 -->
<!-- NoSelectedContent -->
<ContentControl
Name="noSelectedContent"
Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ContentTemplate="{TemplateBinding NoSelectedTemplate}"
Visibility="Hidden"/>
<!-- UpButton -->
<!-- 省略 -->
<!-- DownButton -->
<!-- 省略 -->
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
そして、SelectedIndexが-1の時だけ、このnoSelectedContentという名前のContentControlを表示するようにTriggerを仕掛けます。
<Style TargetType="{x:Type local:DomainUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DomainUpDown}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<!-- 省略 -->
</Border>
<ControlTemplate.Triggers>
<Trigger Property="SelectedIndex" Value="-1">
<Setter TargetName="selectedContent" Property="Visibility" Value="Hidden" />
<Setter TargetName="noSelectedContent" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
以上で多分完成です。
ちゃんと動くか確認してみよう
ということで、ちゃんと動くか確認してみます。
さっきはXAMLに表示する項目を直接書きましたが、今回は、いつものPersonクラスを作成してBindingで表示させてみます。
ということで、いつものPersonクラスを作成します。
namespace DomainUpDownSample
{
public class Person
{
public string Name { get; set; }
}
}
Window1.csのコンストラクタで適当に5個くらいデータを作ります。
using System.Linq;
using System.Windows;
namespace DomainUpDownSample
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
// とりあえずのデータ
DataContext = Enumerable.Range(0, 5).Select(i =>
new Person { Name = "田中 太郎 " + i }).ToList();
}
}
}
XAML側でDomainUpDownコントロールのItemsSourceにBindingします。
<Window x:Class="DomainUpDownSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:DomainUpDownSample"
Title="Window1" Height="75" Width="150">
<Grid>
<l:DomainUpDown ItemsSource="{Binding}">
</l:DomainUpDown>
</Grid>
</Window>
ItemTemplateとNoSelectedTemplateを指定します。
<Window x:Class="DomainUpDownSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:DomainUpDownSample"
Title="Window1" Height="75" Width="150">
<Grid>
<l:DomainUpDown ItemsSource="{Binding}">
<!-- 名前を表示するよ -->
<l:DomainUpDown.ItemTemplate>
<DataTemplate DataType="{x:Type l:Person}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</l:DomainUpDown.ItemTemplate>
<!-- 何も選択されていない時の表示 -->
<l:DomainUpDown.NoSelectedTemplate>
<DataTemplate>
<TextBlock Text="未選択状態" />
</DataTemplate>
</l:DomainUpDown.NoSelectedTemplate>
</l:DomainUpDown>
</Grid>
</Window>
実行してみると…
起動直後は何も選択されてない常態で
ボタンを押していくと、順番に選択項目が変わっていきます(テンプレートも適用されてる)
とりあえずちゃんと動くっぽいです。
ソースコードの全体は、以下からダウンロードできます。
とりあえず独習で作ってるので、おかしな点とか作法を守ってない点とかがあればご指摘下さいm(_ _)m