午後わてんのブログ

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

WPF、ItemsControlを使って要素を入れ子にできるカスタムコントロールThumb、グループ化みたいなもの

結果

結果

テストアプリのコード

2024WPF/20241207_ItemsControlCanvasPanelThumb at master · gogowaten/2024WPF github.com

作成と動作環境

CustomControl1.cs

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;

namespace _20241207_ItemsControlCanvasPanelThumb
{
    [ContentProperty(nameof(MyItems))]
    public class ItemsControlThumb : Thumb
    {

        public ObservableCollection<FrameworkElement> MyItems
        {
            get { return (ObservableCollection<FrameworkElement>)GetValue(MyItemsProperty); }
            set { SetValue(MyItemsProperty, value); }
        }
        public static readonly DependencyProperty MyItemsProperty =
            DependencyProperty.Register(nameof(MyItems), typeof(ObservableCollection<FrameworkElement>), typeof(ItemsControlThumb), new PropertyMetadata(null));

        static ItemsControlThumb()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemsControlThumb), new FrameworkPropertyMetadata(typeof(ItemsControlThumb)));
        }
        public ItemsControlThumb()
        {
            DataContext = this;
            MyItems = [];
        }

    }
}

Thumbクラスと継承したItemControlThumbクラス
FrameworkElement型のコレクションの依存関係プロパティのMyItemsを持たせた
コンストラクタでMyItemsを初期化しているけど、もしこれを依存関係プロパティの登録のPropertyMetadataで初期化すると起動時に無限ループになる(なった)なので、登録時はnullを既定値に指定している

BindingはDataContextに自身を指定

[ContentProperty(nameof(MyItems))]
クラスの頭でこれを書いておくと、XAMLで直接子要素をいれることができるようになる

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:_20241207_ItemsControlCanvasPanelThumb">


  <Style TargetType="{x:Type local:ItemsControlThumb}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:ItemsControlThumb}">
          <ItemsControl ItemsSource="{Binding MyItems}" 
                        Background="{TemplateBinding Background}">
            <ItemsControl.ItemsPanel>
              <ItemsPanelTemplate>
                <Canvas/>
              </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
          </ItemsControl>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

ItemsControlThumbの要素の構成
TemplateにItemsControlを指定、さらにItemPaneltemplaにCanvasを指定。これで追加された要素はCanvasに配置されるので、Canvas.leftとかを使って表示する位置を指定できる

Bindingは
ItemsControlのItemsSourceにMyItemsを指定
BackgroundはTemplateBindingでItemsControlThumbのBackgroundを引き継ぐ


MainWindow.xaml

<Window x:Class="_20241207_ItemsControlCanvasPanelThumb.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:_20241207_ItemsControlCanvasPanelThumb"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="400">
  <Grid>
    <Canvas>
      <Canvas.Resources>
        <Style TargetType="local:ItemsControlThumb">
          <EventSetter Event="DragDelta" Handler="Thumb_DragDelta"/>
        </Style>
      </Canvas.Resources>

      <local:ItemsControlThumb Canvas.Left="0" Canvas.Top="0" Width="300" Height="200" Background="MistyRose">
        <TextBlock Canvas.Left="10" Canvas.Top="10" Text="Group 1"/>
        <TextBlock Canvas.Left="10" Canvas.Top="30" Text="ItemsControlCanvasPanelThumb 1"/>
        <Button Canvas.Left="10" Canvas.Top="50" Content="Button"/>
        
        <local:ItemsControlThumb Canvas.Left="20" Canvas.Top="100" Width="100" Height="100" Background="LightSalmon">
          <TextBlock Canvas.Left="10" Canvas.Top="10" Text="Group 1-1"/>
          <TextBlock Canvas.Left="10" Canvas.Top="30" Text="ItemsControlCanvasPanelThumb 2"/>
          <CheckBox Canvas.Left="10" Canvas.Top="50" Content="CheckBox"/>
        </local:ItemsControlThumb>
        
        <local:ItemsControlThumb Canvas.Left="150" Canvas.Top="60" Width="120" Height="100" Background="LightSalmon">
          <TextBlock Canvas.Left="10" Canvas.Top="10" Text="Group 1-2"/>
          <TextBlock Canvas.Left="10" Canvas.Top="30" Text="ItemsControlCanvasPanelThumb 3"/>
          
          <local:ItemsControlThumb Canvas.Left="10" Canvas.Top="50" Width="100" Height="50" Background="Tomato">
            <TextBlock Canvas.Left="10" Canvas.Top="10" Text="Group 1-2-1"/>
            <TextBlock Canvas.Left="10" Canvas.Top="30" Text="ItemsControlCanvasPanelThumb 4"/>
          </local:ItemsControlThumb>
          
        </local:ItemsControlThumb>
      </local:ItemsControlThumb>

    </Canvas>
  </Grid>
</Window>

MyItemsはContentPropertyに指定してあるので、直接子要素を入れることができている

マウスドラッグ移動のイベントDragDeltaをResourcesのStyleで指定

      <Canvas.Resources>
        <Style TargetType="local:ItemsControlThumb">
          <EventSetter Event="DragDelta" Handler="Thumb_DragDelta"/>
        </Style>
      </Canvas.Resources>

Styleってプロパティを指定するものだと思っていたけど、イベントを指定するEventSetterってのがあったので使ってみた、へー便利


MainWindow.xaml.cs

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

namespace _20241207_ItemsControlCanvasPanelThumb
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        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;
                //↑ trueにしないと、重なっているThumb全てにイベントが伝播して動いてしまう
            }
        }
    }
}

マウスドラッグ移動の処理だけが書いてある
前回の方向キーでの移動でも使ったe.Handled = true;
これを書いておかないと

e.Handled = true;を書かなかった場合の動作
Thumbが重なったところでドラッグ移動すると動きがおかしくなる、2つ重なったくらいならガクガクするくらいだけど、3つ重なっていたら少しマウスを動かしただけで画面外にすっ飛んでいく


参照したところ

ドラッグ移動可能なオブジェクトをたくさん並べる - CoMoの日記 como-2.hatenadiary.org

マツオソフトウェアブログ: Canvasにリストの中身をBindingする方法 my-clip-devdiary.blogspot.com

F#でWPF --- 可変個のコントロール - 何でもプログラミング any-programming.hatenablog.com




感想

テストアプリ
エクセルの図形のグループ化みたいなのを目指しているけど、先は長い
要素の動的追加と削除、グループ化と解除、それに伴う処理が
要素同士の重なりの順番の調整
Groupのサイズの変更
サイズ変更は親要素にも知らせる必要がある?
キーボードフォーカスの範囲の設定と変更

2年前に挑戦したときはサイズ変更関係で詰まった



関連記事

次回は2日後
WPF、自動サイズCanvasをGroupThumbに使ってみた - 午後わてんのブログ gogowaten.hatenablog.com

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

一昨日
WPF、カスタムコントロール使ってみた、マウスドラッグ移動できるTextBlockを作成 - 午後わてんのブログ gogowaten.hatenablog.com