tips - ドラッグ&ドロップ操作によるリストの並べ替え 2

 マウスによるドラッグ&ドロップ操作によってアイテムを並べ替えることができるリストを ビヘイビアで実現する一例を紹介します。

 サンプルプロジェクトはここからダウンロードできます。

ページ内リンク

概要

 ItemsSource プロパティに設定した場合でも Items プロパティに設定した場合でも対応してみました。 説明は省略という暴挙に出ますが。がが。 しかもいきなり Adorner とか付いててゴーストも表示されますが。

namespace DragAndDropList.Views.Behaviors
{
    using System;
    using System.Collections;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;

    public class ItemDragDropBehavior
    {
        #region IsEnabled 添付プロパティ
        public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(ItemDragDropBehavior), new FrameworkPropertyMetadata(false, OnIsEnabledChanged));
        public static bool GetIsEnabled(DependencyObject target)
        {
            return (bool)target.GetValue(IsEnabledProperty);
        }
        public static void SetIsEnabled(DependencyObject target, bool value)
        {
            target.SetValue(IsEnabledProperty, value);
        }

        /// <summary>
        /// IsEnabled 添付プロパティ値変更イベントハンドラ
        /// </summary>
        /// <param name="d">イベント発行元</param>
        /// <param name="e">イベント引数</param>
        private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as ItemsControl;
            // ItemDragDropBehavior ビヘイビアは ItemsControl 派生コントロールを対象としている
            if (control != null)
            {
                if (GetIsEnabled(control))
                {
                    // IsEnabled プロパティが true のときにイベントハンドラを登録する
                    control.PreviewMouseLeftButtonDown += control_PreviewMouseLeftButtonDown;
                    control.PreviewMouseMove += control_PreviewMouseMove;
                    control.PreviewDragEnter += control_PreviewDragEnter;
                    control.PreviewDragOver += control_PreviewDragOver;
                    control.PreviewDragLeave += control_PreviewDragLeave;
                    control.PreviewDrop += control_PreviewDrop;
                    control.PreviewMouseUp += control_PreviewMouseUp;
                }
                else
                {
                    // IsEnabled プロパティが false のときにイベントハンドラを登録解除する
                    control.PreviewMouseLeftButtonDown -= control_PreviewMouseLeftButtonDown;
                    control.PreviewMouseMove -= control_PreviewMouseMove;
                    control.PreviewDragEnter -= control_PreviewDragEnter;
                    control.PreviewDragOver -= control_PreviewDragOver;
                    control.PreviewDragLeave -= control_PreviewDragLeave;
                    control.PreviewDrop -= control_PreviewDrop;
                    control.PreviewMouseUp -= control_PreviewMouseUp;
                }
            }
        }
        #endregion IsEnabled 添付プロパティ

        #region private static フィールド
        /// <summary>
        /// マウス左ボタンによってこのビヘイビアによるドラッグ&ドロップ操作シーケンスに移行したことを確認するためのフラグ
        /// </summary>
        private static bool _isMouseDown;

        /// <summary>
        /// ドラッグされているアイテムに対する論理親 (このビヘイビアが複数のコントロールに適用されることを考慮している)
        /// </summary>
        private static ItemsControl _parent;

        /// <summary>
        /// ドラッグされているアイテム
        /// </summary>
        private static object _draggedItem;

        /// <summary>
        /// アイテムを掴むときのマウスポインタの位置
        /// </summary>
        private static Point _originPoint;

        /// <summary>
        /// ゴースト表示のための Adorner
        /// </summary>
        private static SimpleAdorner _adorner;
        #endregion private static フィールド

        #region イベントハンドラ
        private static void control_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _parent = sender as ItemsControl;
            if (_parent == null)
                return;

            // ドラッグされるアイテムを取得する
            FrameworkElement draggedItem = null;
            UIElement adornedElement = null;
            if ((_parent.ItemsSource as IList) != null)
            {
                draggedItem = e.OriginalSource as FrameworkElement;
                if (draggedItem == null)
                    return;
                var container = _parent.ContainerFromElement(draggedItem) as FrameworkElement;
                if (container != null)
                {
                    _draggedItem = container.DataContext;
                    adornedElement = container;
                }
            }
            else if ((_parent.Items as IList) != null)
            {
                draggedItem = e.Source as FrameworkElement;
                if (draggedItem == null)
                    return;
                _draggedItem = _parent.ContainerFromElement(draggedItem);
                adornedElement = _draggedItem as UIElement;
            }
            if (_draggedItem == null)
                return;

            // ドラッグ開始位置を取得する
            var vw = Window.GetWindow(_parent);
            _originPoint = vw.PointToScreen(e.GetPosition(vw));

            // Adorner を定義する
            _adorner = new SimpleAdorner(_parent, adornedElement, e.GetPosition(_draggedItem as UIElement));

            _isMouseDown = true;
            Console.WriteLine("MouseLeftButtonDown");
        }

        private static void control_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (_isMouseDown && (e.LeftButton == MouseButtonState.Pressed))
            {
                // 現在位置を取得する
                var vw = Window.GetWindow(sender as DependencyObject);
                Point pt = vw.PointToScreen(e.GetPosition(vw));
                if (CheckDistance(_originPoint, pt))
                {
                    Console.WriteLine("DoDragDrop - Start");
                    DragDrop.DoDragDrop(sender as DependencyObject, _draggedItem, DragDropEffects.Move);
                    Console.WriteLine("DoDragDrop - end");

                    // ドラッグ&ドロップが終わったらここに戻ってくるので
                    // ここで状態をクリアする
                    CleanUp();
                }
            }
        }

        private static void control_PreviewDragEnter(object sender, DragEventArgs e)
        {
            // Adorner の表示を復帰する
            if (_adorner != null)
                _adorner.Attach();
            Console.WriteLine("DragEnter : " + sender.ToString());
        }

        private static void control_PreviewDragOver(object sender, DragEventArgs e)
        {
            // ドラッグ操作中のマウス動作中は Adorner の位置を更新する
            if (_adorner != null)
            {
                _adorner.CurrentPoint = e.GetPosition(_adorner.AdornedElement);
            }
            Console.WriteLine("DragOver : " + sender.ToString());
        }

        private static void control_PreviewDragLeave(object sender, DragEventArgs e)
        {
            // ドラッグ操作中にコントロールからはみ出した場合は Adorner を隠す
            if (_adorner != null)
                _adorner.Detach();
            Console.WriteLine("DragLeave : " + sender.ToString());
        }

        private static void control_PreviewDrop(object sender, DragEventArgs e)
        {
            if (!_isMouseDown)
                return;

            var itemsControl = sender as ItemsControl;
            if (itemsControl == null)
                return;

            // ドロップする位置にあるアイテムのインデックスを取得する
            IList itemsSourceList = null;
            object dropTargetItem = null;
            if ((itemsControl.ItemsSource as IList) != null)
            {
                itemsSourceList = itemsControl.ItemsSource as IList;
                dropTargetItem = e.OriginalSource as FrameworkElement;
                if (dropTargetItem != null)
                {
                    var container = itemsControl.ContainerFromElement(dropTargetItem as FrameworkElement) as FrameworkElement;
                    dropTargetItem = container != null ? container.DataContext : null;
                }
            }
            else if ((itemsControl.Items as IList) != null)
            {
                itemsSourceList = itemsControl.Items as IList;
                dropTargetItem = e.Source as FrameworkElement;
            }
            if (itemsSourceList != null)
            {
                var dropTargetItemIndex = dropTargetItem != null ? itemsSourceList.IndexOf(dropTargetItem) : itemsSourceList.Count;

                // ドラッグされたアイテムをドロップした位置に差し替える
                if (_parent == itemsControl)
                {
                    itemsSourceList.Remove(_draggedItem);
                    itemsSourceList.Insert(dropTargetItemIndex < 0 ? itemsSourceList.Count : dropTargetItemIndex, _draggedItem);
                }
                else
                {
                    var parentList = _parent.ItemsSource as IList;
                    parentList = parentList == null ? _parent.Items as IList : parentList;

                    var index = parentList.IndexOf(_draggedItem);
                    try
                    {
                        parentList.Remove(_draggedItem);
                        itemsSourceList.Insert(dropTargetItemIndex < 0 ? itemsSourceList.Count : dropTargetItemIndex, _draggedItem);
                    }
                    catch (Exception err)
                    {
                        parentList.Insert(index, _draggedItem);
                    }
                }
            }

            e.Handled = true;
            Console.WriteLine("Drop");
        }

        private static void control_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            // 単純なクリックで Drop 操作されないようにここで状態をクリアする
            CleanUp();
            Console.WriteLine("MouseUp");
        }
        #endregion イベントハンドラ

        #region ヘルパ
        private static bool CheckDistance(Point x, Point y)
        {
            return true;

            // こっちにすると
            // アイテム同士の境界線ギリギリからドラッグを開始すると
            // 別のアイテムに IsMouseOver が移ってしまって
            // ドラッグするアイテムと選択中アイテムが一致しなくなってしまう
            //return (Math.Abs(x.X - y.X) >= SystemParameters.MinimumHorizontalDragDistance)
            //    || (Math.Abs(x.Y - y.Y) >= SystemParameters.MinimumVerticalDragDistance);
        }

        /// <summary>
        /// すべての状態をクリアする
        /// </summary>
        private static void CleanUp()
        {
            _isMouseDown = false;
            _parent = null;
            _draggedItem = null;
            if (_adorner != null)
            {
                _adorner.Detach();
                _adorner = null;
            }
        }
        #endregion ヘルパ
    }

    internal class SimpleAdorner : Adorner
    {
        private bool isAttached;
        private AdornerLayer _layer;
        public Point Offset { get; set; }

        public SimpleAdorner(Visual layerControl, UIElement adornedElement, Point point)
            : base(adornedElement)
        {
            _layer = AdornerLayer.GetAdornerLayer(layerControl);

            CurrentPoint = point;
            Offset = new Point(10, 10);
            Attach();
        }

        public static readonly DependencyProperty CurrentPointProperty = DependencyProperty.Register("CurrentPoint", typeof(Point), typeof(SimpleAdorner), new FrameworkPropertyMetadata(default(Point), FrameworkPropertyMetadataOptions.AffectsRender));
        public Point CurrentPoint
        {
            get { return (Point)GetValue(CurrentPointProperty); }
            set { SetValue(CurrentPointProperty, value); }
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            //base.OnRender(drawingContext);

            var pt = new Point(CurrentPoint.X + Offset.X, CurrentPoint.Y + Offset.Y);
            var rect = new Rect(pt, this.AdornedElement.RenderSize);
            var brush = new VisualBrush(this.AdornedElement);
            brush.Opacity = 0.3;

            drawingContext.DrawRectangle(brush, null, rect);
        }

        public void Attach()
        {
            if (!isAttached)
            {
                isAttached = true;
                _layer.Add(this);
            }
        }

        public void Detach()
        {
            if (isAttached)
            {
                isAttached = false;
                _layer.Remove(this);
            }
        }
    }
}
Code 1 : ItemDragDropBehavior ビヘイビア
Fig.1 : サンプル

Designed by CSS.Design Sample