BackacheEngineerの技術的な備忘録

技術系でいろいろ書けたらなーと

XAML: Templateについて その1

やっとこさ来た XAML におけるデザイン作りこみの要、Template についてちょこちょこまとめる。

正直まとめるの大変な内容なので複数にわける。今回は「ItemsPanelTemplate」と「DataTemplate」だけ。

目次

Template の種類

Template は複数いる。全部じゃないと思うけど、自身が知った、知りたいものをまとめる。

種類 説明
DataTemplate 必ずしも外観を持たなくてよいデータオブジェクトに対して外観を持たせたいときに使用する Template 。例えば、グラデーションオブジェクト。グラデーションは色なので外観がない。そこで DataTemplate によって例えば楕円という外形を与え、その色としてグラデーションオブジェクトを使うといったことができる。
ItemsPanelTemplate ItemsControl の派生クラスで各要素をどんなパネルに表示するか変えたいときに使用する。この Template に指定できるのは Panel の派生クラスのみ。例えば、StackPanel である。
ControlTemplate 標準コントロールの外観を再定義するための Template 。超重要な Template 。その2にまとめた。
HierarchicalDataTemplate 階層構造を持ったものの外観を定義するための Template 。実問題におけるデータ構造は階層構造を持つことが多いと考えられるため、これは習得しておきたい。

ItemsPanelTemplate

Template の中で最も単純で簡単な Template である。これは、ItemsControl という複数のアイテムがソースになっているものの表示の外観を変更するものである。ここでは ListBox を例に書いていく。

ListBox はリストを表示するものである。 例えば、0~100 までのインデックスを持つ「itemN (N はindex番号)」のうち、偶数だけ表示するといったやつを載せる。

f:id:BackacheEngineer:20210503132918p:plain
ListBoxSample

xamlコードはこんな感じ。

        <ListBox HorizontalAlignment="Center"
                 ItemsSource="{Binding Source={StaticResource stringItems}, Path=ItemList}">
        </ListBox>

アイテムを作ってるクラスはこんな簡単なのを用意した。(さすがに100個も ListBoxItem は書いてられない。) これを window.resources にインスタンス作ってやって、xaml 側でバインディングしている。

    class StringItemsForListBox
    {
        public static IEnumerable<string> ItemList { get; private set; }

        static StringItemsForListBox()
        {
            var list = new List<int>();
            for (int i = 0; i < 100; i++)
            {
                list.Add(i);
            }

            ItemList = list.Where(x => x % 2 == 0).Select(x => $"item{x}");
        }

ここで、「2列とか3列にしたい」といったリスト表示の外観だけ変更したいときに「ItemsPanelTemplate」を使う。 そんな機会あるんかと言われたらそんなないかもだけど。しかし、ListBoxはアイテムの選択ができるため、「選択してこちょこちょしたいけどUIが。。。」なんてことがあるかもしれない。

やり方は簡単。xaml 側を下記のとおり変更する。3列に変更したものになる。

        <ListBox HorizontalAlignment="Center"
                 ItemsSource="{Binding Source={StaticResource stringItems}, Path=ItemList}">

            <!--#region PanelTemplateの変更 -->
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="3"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <!--#endregion-->
        </ListBox>

実行すると以下のとおり。

f:id:BackacheEngineer:20210503134159p:plain
ListBoxSample_Column3

サンプルは UniformGrid を使用したが、別に Panel の派生クラスなら何でもよい。 StackPanel でも別にいい。なんだったら自作のクラスでもいける。Panel の派生クラスならよいためだ。

DataTemplate

DataTemplate は前述したとおり、必ずしも外観が必要ないものにたいして外観を付け加えることができる。

Button とグラデーションオブジェクトを使って説明する。

以下のような xaml コードを考える。

<Button HorizontalAlignment="Center" VerticalAlignment="Center">
    <!-- このグラデーションがバインディングソース -->
    <LinearGradientBrush>
        <GradientStop Offset="0" Color="Red"/>
        <GradientStop Offset="1" Color="Blue"/>
    </LinearGradientBrush>
</Button>

これは、Button の Content に LinearGradientBrush というグラデーションオブジェクトを割り当てている。 Button は ContentControl の派生クラスであり、そのプロパティ Content は object 側のため、こういったオブジェクトならなんらエラーなく割り当てることができる。

しかし、これを実行してもグラデーションは出てこない。なんせグラデーションオブジェクトには外観がないため、ただの ToString したものが返ってくるからだ。つまり、LinearGradientBrush の型名が出てくるだけである。

そこで、DataTemplate で外観を与えるわけである。 今回は Ellipse を与える。

<Button HorizontalAlignment="Center" VerticalAlignment="Center">
    <!-- このグラデーションがバインディングソース -->
    <LinearGradientBrush>
        <GradientStop Offset="0" Color="Red"/>
        <GradientStop Offset="1" Color="Blue"/>
    </LinearGradientBrush>

    <!--#region DataTemplateを更新 -->
    <Button.ContentTemplate>
        <DataTemplate>
            <Ellipse Width="100" Height="100"
                     Fill="{Binding}"/>
        </DataTemplate>
    </Button.ContentTemplate>
    <!--#endregion-->
</Button>

この xaml コードの実行結果は以下のとおり。

f:id:BackacheEngineer:20210503143010p:plain
ButtonSample_Ellipse_Gradient

ちゃんとグラデーションがついた Ellipse が表示されていることがわかる。

ここで、Ellipse のプロパティ Fill へのバインディングに注目する。バインディングソースを一切指定していないのに特徴がある。 これは、ContentTemplate の DataContext に Button の Content が割り当てられているためである。つまり、既定のバインディングソースが割り当てられている状態、というのが正しい表現だろうか。

この DataTemaplate はリソースとしてインスタンスを作成できる(もちろん、ItemsPanelTemplate もできる)。つまり、1つの外観を複数のボタンに割り当てるといったことが可能になる。 一つ知識として知っておきたいのは、割り当てられたインスタンス1個1個に対してオブジェクトが作成されることである。

つまり、今回のような Ellipse のDataTemplate を 100個のボタンに割り当てると、100個の Ellipse が作成されるということである。 てっきり1オブジェクトに対して参照関係を作ってくれていると思っていたがそうではないらしいので注意がいる。

ちなみに、DataTemplate 単体でリソースにインスタンスを確保する機会は少ない(はず)。なぜなら、DataTemplate は Style に含めてインスタンス化できるためである。Style についてはまたいつかまとめる。ControlTemplate を触ればどのみち書くことになる。


DataTemplate で既存の外観を変更する

非常に簡単な使い方を書いたけれども、DataTemplate を用いて外観に手を加えることも可能である。つまり、ControlTemplate みたいな使い方である。

ItemsPanelTemplate でも紹介した ListBox に対して、「罫線のように各アイテムを線で囲む」という変更を加えていく。 ListBox は子要素として ListBoxItem を持っているが、既定では線がないので境界が見えない。そこに境界をつけるということである。

さっそくつけていく。各 Item の Template に対しての DataTemplate を変更する。 Border を使うことにより、境界線を実現する。

<ListBox HorizontalAlignment="Center"
         ItemsSource="{Binding Source={StaticResource stringItems}, Path=ItemList}"
         BorderBrush="Red">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border BorderBrush="Gray"
                    BorderThickness="1">
                <TextBlock Text="{Binding Content}" Margin="5"/>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>

    <!--#region PanelTemplateの変更 -->
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Columns="3"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <!--#endregion-->
</ListBox>

xaml 側の変更を実現するため、C# 側にも変更を加えている。 「itemN」をプロパティに格納し、それをバインディングする形にしないと Text に対してのバインディングソースがわからないためである。

class StringItemsForListBox
{
    public static IEnumerable<StringItemsForListBox> ItemList { get; private set; }

    public string Content { get; private set; }

    static StringItemsForListBox()
    {
        var list = new List<int>();
        for (int i = 0; i < 100; i++)
        {
            list.Add(i);
        }

        ItemList = list.Where(x => x % 2 == 0).Select(x => new StringItemsForListBox
        {
            Content = $"item{x}"
        });
    }
}

これを実行すると下記のようになる。

f:id:BackacheEngineer:20210503152159p:plain
ListBoxSample_Border

ControlTemplate のように外観を変更することができた。今回は境界線をつけるだけの簡単なものだったが、さらに Grid を使って行と列を定義して・・・と外観を設定することもできるため、DataTemplate の可能性は結構広いことになる。

まとめ

ItemsPanelTemplate と DataTemplate をまとめた。さらに概略だけをまとめたものを下記に載せる。

Template まとめ
ItemsPanelTemplate ・ItemsControl の表示の外観を簡単に変更できる。
・ 指定できるのは Panel の派生クラスのみ。
・ Panel の派生クラスなら自作クラスでもよいため、自由な変更も可能である。
DataTemplate ・必ずしも外観が必要とならないものに外観を与えることができる。
・ControlTemplate のように既存の外観を変更することもできる。