ドラッグ移動可能なオブジェクトをたくさん並べる

今回は前回作ったドラッグ移動可能なオブジェクトをCanvas上にたくさん並べる方法を考えてみます。

方法としては前回のThumbのXAMLをコピペ増殖する方法、あるいはThumbをカスタムコントロール化してコードビハインドで動的に追加する方法も考えられますが、今回はWPFっぽいアプローチでやってみたいと思います。

WPFのリストボックスの柔軟性

WPFはコントロール内部の表現力が非常に強力です。リストボックスの各項目の表示のカスタマイズは具体的にはListBox.ItemTemplate(DataTemplate型)をカスタマイズすれば実現できます。どのようなカスタマイズが可能なのかはMSDNのデータテンプレートの概要がよくまとまっていると思います。

ただ、リストボックスのカスタマイズの柔軟性はこのような各リスト項目のカスタマイズに留まらず、各リストアイテムの配置(パネル)もカスタマイズできます。例えばリストアイテムを円形に並べるなど、等間隔に並べる以外の配置が可能です。今回のテーマはこのようなリストボックスのレイアウトの柔軟性を利用して実装してみます。

サンプル

まずXAMLから

    <DockPanel>
        <Button Content="追加" DockPanel.Dock="Top"
                HorizontalAlignment="Center"
                Click="Button_Click" />
        <ItemsControl Name="DragList">
            
            <!--配置方式(パネル)をカスタマイズ-->
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <!--リストアイテムの表示をカスタマイズ-->
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Thumb DragDelta="Thumb_DragDelta" >
                        <Thumb.Template>
                            <ControlTemplate>

                                <!--ドラッグ対象のオブジェクトを定義する-->
                                <Grid Width="100" Height="30">
                                    <Ellipse Fill="LightBlue" Stroke="Blue" />
                                    <TextBlock Text="{Binding}" 
                                               HorizontalAlignment="Center" 
                                               VerticalAlignment="Center"/>
                                </Grid>

                            </ControlTemplate>
                        </Thumb.Template>
                    </Thumb>
                </DataTemplate>
            </ItemsControl.ItemTemplate>

        </ItemsControl>
    </DockPanel>

今回はListBoxの代わりに親玉のItemsControlを使ってます。理由は後述します。ItemsControl.ItemTemplateのカスタム部分に前回のサンプルとほぼ同等のものを入れます。またレイアウトのカスタマイズ部分にCanvasを入れました。これはThumbの表示位置を調整するのにCanvas.Leftなどを使っているからです。この方針も前回と同様ですが入れる部分が若干変わっています。

次にコードビハインドです。

        //追加ボタンのイベントハンドラ
        private void Button_Click(object sender, RoutedEventArgs e) {
            DragList.Items.Add(DragList.Items.Count + 1);
        }

        //Thumbコントロールのドラッグイベント処理
        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e) {

            var thumb = sender as Thumb;
            if (thumb == null) return;

            //親コントロールを探す
            var parent = thumb.Parent();
            if (parent == null) return;

            double x = Canvas.GetLeft(parent);
            if (double.IsNaN(x)) x = 0;
            double y = Canvas.GetTop(parent);
            if (double.IsNaN(y)) y = 0;

            //ドラッグ量に応じてThumbコントロールを移動する
            Canvas.SetLeft(parent, x + e.HorizontalChange);
            Canvas.SetTop(parent, y + e.VerticalChange);
        }

イベント処理を二つやっています。
一つ目が追加ボタンのクリックでItemsControl(ListBox)の項目を追加しています。

二つ目のイベント処理が、楕円オブジェクトをドラッグした時の処理です。処理内容は基本的に前回のサンプルと同様なのです。違いはCanvas.Leftを設定するのがThumbコントロール自体ではなく親オブジェクトとしている点です。実は実行時の要素の親子関係をダンプしてみると下のようになります。Canvasの直下にThumbがなく中にContentPresenterが挟まっているのが分かります。親のContentPresenterを操作します。

MainWindow
 System.Windows.Controls.Border
  System.Windows.Documents.AdornerDecorator
   System.Windows.Controls.ContentPresenter
    System.Windows.Controls.DockPanel
     System.Windows.Controls.Button: 追加
      Microsoft.Windows.Themes.ButtonChrome
       System.Windows.Controls.ContentPresenter
        System.Windows.Controls.TextBlock
     System.Windows.Controls.ItemsControl Items.Count:3
      System.Windows.Controls.Border
       System.Windows.Controls.ItemsPresenter
        System.Windows.Controls.Canvas
         System.Windows.Controls.ContentPresenter
          System.Windows.Controls.Primitives.Thumb
           System.Windows.Controls.Grid
            System.Windows.Shapes.Ellipse
            System.Windows.Controls.TextBlock
         System.Windows.Controls.ContentPresenter
          System.Windows.Controls.Primitives.Thumb
           System.Windows.Controls.Grid
            System.Windows.Shapes.Ellipse
            System.Windows.Controls.TextBlock
         System.Windows.Controls.ContentPresenter
   System.Windows.Documents.AdornerLayer

なお事前に初期化していないので初回のCanvas.LeftとTopの値はDouble.NaNになっています。不具合があるのでゼロにするif文を挟んでいます。前回の例はXAML上で明示的にゼロを設定して回避していました。うまく移動処理が働かない場合には、この二点が落とし穴になっているかも知れません。

完成のイメージ

実行して追加ボタンを押すと楕円のオブジェクトが現れます。これをドラッグして好きな位置に配置できます。できあがり。

このようにWPFのリストボックスの柔軟性が分かると思います。ただ、ここまで自由に配置するともはやリストボックスと言う印象は無いと思います。実際にWPFのListBoxや親クラスのItemsControlはリストボックスを実現するものというよりも、「データのコレクションに対応するWPFオブジェクトを一つ一つ動的に作成して配置する機能を提供するもの」と理解した納得が行くのでは無いでしょうか。

ItemsControlとListBoxの違いですがリスト項目を選択できる機能の有無です。ItemsControlには選択項目のハイライトや選択イベントなどがありません。今回は項目のハイライト表示がむしろ邪魔になったのでシンプルなItemsControlを採用しました。