午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

DataTemplateSelector使ってみた、実際便利

DataのリストをBindingするだけで、それぞれに合ったTemplateを適用して表示することができる

DataTypeの識別用列挙体を用意しておいて
enum Type { Text, Ellipse, Rect }

Dataクラス
class MyData

  • MyType、識別用
  • MyText、文字列
  • MyLeft、左位置

あとはMyDataのリストを作成して、ItemsControlのItemsSourceプロパティにBinding
今回はItemsControlを使ったけど、ListBoxやStackPanelなどのItemsSourceプロパティを持つPanel系の要素なら使えると思う

結果

テストアプリの動作
3つのドラッグ移動できる要素はすべてThumb要素のTemplateを改変したもので、
文字列のtextblock thumbはTextBlockに改変
図形の丸と四角はEllipseとRectangle

textボタンではそれぞれのDataのプロパティに変更を加えている、文字列には👍️を追加、丸はLeftに+10、四角はTopに+10

BindingしているDataはこれだけ



テストアプリのコード

2024WPF/20241228_DataTemplateSelector_de_Thumb at master · gogowaten/2024WPF

github.com

ソリューションエクスプローラーでファイルの配置確認



環境




Class1.cs

using System.Windows;

namespace _20241228_DataTemplateSelector_de_Thumb
{
    /// <summary>
    /// データタイプの識別用列挙体
    /// </summary>
    public enum ThumbType { None = 0, Text, Ellipse, Rect }

    /// <summary>
    /// データ用
    /// 殆どを依存関係プロパティにしているのは、値を更新したときにBinding先に通知するため
    /// </summary>
    public class MyData : DependencyObject
    {
        public ThumbType Type { get; }

        public MyData(ThumbType type)
        {
            this.Type = type;
        }

        #region 依存関係プロパティ

        public string MyText
        {
            get { return (string)GetValue(MyTextProperty); }
            set { SetValue(MyTextProperty, value); }
        }
        public static readonly DependencyProperty MyTextProperty =
            DependencyProperty.Register(nameof(MyText), typeof(string), typeof(MyData), new PropertyMetadata(string.Empty));


        public double MyLeft
        {
            get { return (double)GetValue(MyLeftProperty); }
            set { SetValue(MyLeftProperty, value); }
        }
        public static readonly DependencyProperty MyLeftProperty =
            DependencyProperty.Register(nameof(MyLeft), typeof(double), typeof(MyData), new PropertyMetadata(0.0));

        public double MyTop
        {
            get { return (double)GetValue(MyTopProperty); }
            set { SetValue(MyTopProperty, value); }
        }
        public static readonly DependencyProperty MyTopProperty =
            DependencyProperty.Register(nameof(MyTop), typeof(double), typeof(MyData), new PropertyMetadata(0.0));

        public double MyVolume
        {
            get { return (double)GetValue(MyVolumeProperty); }
            set { SetValue(MyVolumeProperty, value); }
        }
        public static readonly DependencyProperty MyVolumeProperty =
            DependencyProperty.Register(nameof(MyVolume), typeof(double), typeof(MyData), new PropertyMetadata(30.0));

        #endregion 依存関係プロパティ
    }


}

値の格納用のクラスなのに、行数が多いのは依存関係プロパティを使っているからなんだけど、これは通知プロパティのほうが行数少なくて済んだかもしれない


Class2.cs

using System.Windows.Controls;
using System.Windows;

namespace _20241228_DataTemplateSelector_de_Thumb
{
  
    public class MyDTSelector : DataTemplateSelector
    {
        public DataTemplate? DT1 { get; set; }
        public DataTemplate? DT2 { get; set; }
        public DataTemplate? DT3 { get; set; }

        /// <summary>
        /// 今回の場合だと、引数のitemにMyDataが入っているので、
        /// データタイプを識別してそれぞれに合ったDataTemplateを返している
        /// それぞれのDataTemplateの設定はXAMLの方で行っている
        /// </summary>
        /// <param name="item"></param>
        /// <param name="container"></param>
        /// <returns></returns>
        public override DataTemplate? SelectTemplate(object item, DependencyObject container)
        {

            if (item is not MyData)
            {
                return base.SelectTemplate(item, container);
            }
            else if (item is MyData dd)
            {
                if (dd.Type == ThumbType.Text) { return DT1; }
                else if (dd.Type == ThumbType.Ellipse) { return DT2; }
                else if (dd.Type == ThumbType.Rect) { return DT3; }
                else { return base.SelectTemplate(item, container); }
            }
            return base.SelectTemplate(item, container);
        }
    }
}

今回の要DataTemplateSelectorクラスを継承したMyDTSelectorクラス
SelectTemplateメソッドをOverride、そこでDataのTypeを識別して、それぞれに合ったDataTemplateを返している

返しているDataTemplate(DT1からDT3)は、このクラスだけで見ると空っぽに見えるので、こんなのを返して何になるんだろうと思ったら、このクラスを使う側(XAMLのほう)でDataTemplateを入れて使うみたいで、次がその使う側のMainWindow.xaml


MainWindow.xaml

<Window x:Class="_20241228_DataTemplateSelector_de_Thumb.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20241228_DataTemplateSelector_de_Thumb"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="500">

  <Window.Resources>

    <Style x:Key="ts" TargetType="Thumb">
      <EventSetter Event="DragDelta" Handler="Thumb_DragDelta"/>
      <Setter Property="Canvas.Left" Value="{Binding MyLeft, Mode=TwoWay}"/>
      <Setter Property="Canvas.Top" Value="{Binding MyTop, Mode=TwoWay}"/>
    </Style>

    <DataTemplate x:Key="ddText" DataType="local:MyData">
      <Thumb Style="{StaticResource ts}">
        <Thumb.Template>
          <ControlTemplate>
            <TextBlock Text="{Binding MyText}"/>
          </ControlTemplate>
        </Thumb.Template>
      </Thumb>
    </DataTemplate>

    <DataTemplate x:Key="ddEllipse" DataType="local:MyData">
      <Thumb Style="{StaticResource ts}">
        <Thumb.Template>
          <ControlTemplate>
            <Grid>
              <Ellipse Width="{Binding MyVolume}" Height="{Binding MyVolume}" Fill="Gold"/>
              <TextBlock Text="{Binding MyText}" VerticalAlignment="Center"/>
            </Grid>
          </ControlTemplate>
        </Thumb.Template>
      </Thumb>
    </DataTemplate>

    <DataTemplate x:Key="ddRect" DataType="local:MyData">
      <Thumb Style="{StaticResource ts}">
        <Thumb.Template>
          <ControlTemplate>
            <Grid>
              <Rectangle Width="{Binding MyVolume}" Height="{Binding MyVolume}" Fill="DodgerBlue"/>
              <TextBlock Text="{Binding MyText}" VerticalAlignment="Center"/>
            </Grid>
          </ControlTemplate>
        </Thumb.Template>
      </Thumb>
    </DataTemplate>

  </Window.Resources>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="150"/>
    </Grid.ColumnDefinitions>

    <ItemsControl x:Name="ic" ItemsSource="{Binding}" FontSize="30">
      <ItemsControl.ItemTemplateSelector>
        <local:MyDTSelector
            DT1="{StaticResource ddText}"
            DT2="{StaticResource ddEllipse}"
            DT3="{StaticResource ddRect}"/>
      </ItemsControl.ItemTemplateSelector>
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Canvas/>
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
      <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
          <Setter Property="Canvas.Left" Value="{Binding MyLeft}"/>
          <Setter Property="Canvas.Top" Value="{Binding MyTop}"/>
        </Style>
      </ItemsControl.ItemContainerStyle>
    </ItemsControl>

    <!--動作確認用Panel-->
    <StackPanel Grid.Column="1">
      <Button Content="test" Click="Button_Click"/>
      <ListBox ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
          <DataTemplate DataType="{x:Type local:MyData}">
            <StackPanel>
              <TextBlock Text="{Binding MyLeft, StringFormat=left {0:0.0}}"/>
              <TextBlock Text="{Binding MyTop, StringFormat=top {0:0.0}}"/>
              <Separator/>
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </StackPanel>
  </Grid>
</Window>

Window.Resourceの中で各種DataTemplateを用意、Keyプロパティに名前を付けておく、18、28、41行目

各種DataTemplate
ドラッグ移動させたかったので元の要素はThumbで、そのTemplateを改変している
12行目、StyleでThumb用のスタイルを用意しておいて、各DataTemplateのThumbのスタイルに適用している
ここで肝要なのが14、15行目のBindingのModeをTwoWay設定で、これを明記しておかないとドラッグ移動してくれなかった

DataTemplateSelectorを使っているところは

ItemsControl
64から67行目、ItemsControlのItemTemplateSelectorにMyDTSelectorを指定、MyDTSelectorのプロパティのDT1からDT3に、さっきのDataTemplateを指定している
これで空っぽだと思っていたDT1からDT3に、DataTemplateが入ったことになるみたい、そういう使い方なのねえ

69行目からの5行でItemsPanelをCanvasに変更している、これで各ItemのCanvas.Leftを指定で左位置が変更できる

74行目からのItemContainerStyle
ここでもCanvas.LeftとかをBindingする必要があるんだけど、よくわかっていなくて
まず、TargetTypeをContentPresenterにする必要がわからん
Canvas.LeftとかのBindingも、ItemTemplateSelectorのほうのStyleで行っているから要らないと思うんだけどねえ、そのくせModeはTwoWayと明記する必要がないのもよくわからん


MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace _20241228_DataTemplateSelector_de_Thumb
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public List<MyData> MyDatas { get; set; }
        public MainWindow()
        {
            InitializeComponent();

            MyDatas = [
                new MyData(ThumbType.Text)
                {
                    MyLeft = 30,
                    MyTop = 10,
                    MyText = "textblock thumb"
                },
                new MyData(ThumbType.Ellipse)
                {
                    MyLeft = 30,
                    MyTop = 80,
                    MyVolume = 100
                },
                new MyData(ThumbType.Rect)
                {
                    MyLeft = 130,
                    MyTop= 140,
                    MyVolume = 100
                }];

            DataContext = MyDatas;
        }

        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            if (sender is Thumb t)
            {
                Canvas.SetLeft(t, Canvas.GetLeft(t) + e.HorizontalChange);
                Canvas.SetTop(t, Canvas.GetTop(t) + e.VerticalChange);
                e.Handled = true;
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MyDatas[0].MyText += "👍️";
            MyDatas[1].MyLeft += 10;
            MyDatas[2].MyTop += 10;
        }
    }
}

MyData作成してBindingとドラッグ移動処理、ボタンクリックでの動作
どうでもいいけど絵文字の👍️は色違いの💩に見える



参照したところ

WPF】DataTemplateSelectorクラスを使用して、動的にコントロールの種類を変更する方法【C#】 #Xaml - Qiita qiita.com

DataTemplateSelectorでDataTemplateを切り替える – 山本隆の開発日誌 www.gesource.jp

[WPF] データに応じてリストの表示形式を切り替える #C# - Qiita qiita.com

TemplateSelectorとDataTriggerを使用した一例 #C# - Qiita qiita.com

WPFで列挙型の表示(DataTemplateSelector)|Memeplexes memeplex.blog.shinobi.jp


感想

昔からこうしたくて色々試してようやくここまでできた(歓喜将軍)
XAMLは難しいけど、使えれば便利なんだなあ

いままで移動できる各種要素を表現するには、Thumbクラスを継承したそれぞれのクラスを作成していた、今回はクラスを作ること無く各種Templateだけを作成して、あとはDataを渡すだけでできた

まだ足りない
あとはグループ化(階層化)できるのかとか、細かい動作(フォーカス範囲指定、右クリックメニュー)を入れられるのかってとこ

これは違った
ItemContainerStyleSelector
これはStyleを適用する型が決まっているようで、ItemsControlならContentPresenter型、ListBoxならListBoxItem型とかで、Thumb用のStyleは指定できなかった


関連記事

gogowaten.hatenablog.com

gogowaten.hatenablog.com



3日後
WPF、この一ヶ月でのカスタムコントロールThumbのマウスドラッグ移動のまとめ - 午後わてんのブログ
gogowaten.hatenablog.com