ドラッグ&ドロップ

今回はWPFのDataGridでドラッグ&ドロップを利用して行を入れ替えるプログラムに挑戦してみました! C#初心者が書いたプログラムなので、いろいろとおかしな箇所があるかも知れません。流用する際は都度修正してください。

※この記事は2023/10/31時点の情報です。

MainWindow.xaml
今回のメインとなるDataGridを配置しています。

<Window x:Class="DragGrid.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:DragGrid"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DataGrid Name="dataGrid"
                  ItemsSource="{Binding Items}"
                  CanUserAddRows="False"
                  AutoGenerateColumns="False"
                  SelectionMode="Single"
                  SelectionUnit="FullRow"
                  AllowDrop="True"
                  CanUserReorderColumns="False"
                  CanUserSortColumns="False"
                  PreviewMouseLeftButtonDown="DataGrid_PreviewMouseLeftButtonDown"
                  PreviewMouseMove="DataGrid_PreviewMouseMove"
                  Drop="DataGrid_Drop"
                  PreviewDragOver="DataGrid_DragOver"
                  PreviewDrop="DataGrid_PreviewDrop">
            <DataGrid.Columns>
                <DataGridTextColumn Header="ID" Binding="{Binding ID}" />
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

MainWindow.xaml.cs
ドラッグ&ドロップ関連の処理を記述しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace DragGrid
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private bool isRowDragging;
        private Item draggedItem;
        Point MyPoint;//マウスクリックの位置用

        public MainWindow()
        {
            InitializeComponent();
            // ViewModelのインスタンスを作成し、DataContextに設定
            MainViewModel viewModel = new MainViewModel();
            DataContext = viewModel;
            dataGrid.PreviewMouseMove += DataGrid_PreviewMouseMove;
            dataGrid.Drop += DataGrid_Drop;
        }

        private void DataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            isRowDragging = true;
            //クリック位置取得
            MyPoint = e.GetPosition(this);
            var row = FindVisualParent<DataGridRow>(e.OriginalSource as DependencyObject);
            if (row != null)
            {
                draggedItem = row.Item as Item;
            }
        }

        private void DataGrid_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (isRowDragging && e.LeftButton == MouseButtonState.Pressed)
            {
                DragDrop.DoDragDrop(dataGrid, draggedItem, DragDropEffects.Move);
            }

        }

        private void DataGrid_DragOver(object sender, DragEventArgs e)
        {
            if (isRowDragging)
            {
                // ドラッグ中にリアルタイムでスクロール
                var scrollViewer = FindVisualChild<ScrollViewer>(dataGrid);
                if (scrollViewer != null)
                {
                    //今のマウスの座標
                    var mouseP = e.GetPosition(this);

                    //スクロール位置の指定
                    if (MyPoint.Y < mouseP.Y)
                    {
                        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + 1);
                    }
                    else
                    {
                        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - 1);
                    }
                }
            }
        }

        private void DataGrid_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(typeof(Item)))
            {
                var target = FindVisualParent<DataGridRow>(e.OriginalSource as DependencyObject);
                if (target != null)
                {
                    var targetItem = target.Item as Item;
                    if (targetItem != null)
                    {
                        var viewModel = (MainViewModel)DataContext;
                        int dropIndex = viewModel.Items.IndexOf(draggedItem);

                        if (e.GetPosition(dataGrid).Y < 0)
                        {
                            // ドロップ先が DataGrid の上にある場合、一番上に挿入
                            viewModel.Items.Remove(draggedItem);
                            viewModel.Items.Insert(0, draggedItem);
                        }
                        else if (e.GetPosition(dataGrid).Y > dataGrid.ActualHeight)
                        {
                            // ドロップ先が DataGrid の下にある場合、一番下に挿入
                            viewModel.Items.Remove(draggedItem);
                            viewModel.Items.Add(draggedItem);
                        }
                        else
                        {
                            int targetIndex = viewModel.Items.IndexOf(targetItem);
                            if (targetIndex != -1 && targetIndex != dropIndex)
                            {
                                viewModel.Items.Move(dropIndex, targetIndex);
                            }
                        }
                    }
                }
            }
            isRowDragging = false;
        }

        private void DataGrid_PreviewDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(typeof(Item)))
            {
                var target = FindVisualParent<DataGridRow>(e.OriginalSource as DependencyObject);
                if (target != null)
                {
                    var targetItem = target.Item as Item;
                    if (targetItem != null)
                    {
                        var viewModel = (MainViewModel)DataContext;
                        int dropIndex = viewModel.Items.IndexOf(draggedItem);

                        if (e.GetPosition(dataGrid).Y < 0)
                        {
                            // ドロップ先が DataGrid の上にある場合、一番上に挿入
                            viewModel.Items.Remove(draggedItem);
                            viewModel.Items.Insert(0, draggedItem);
                        }
                        else if (e.GetPosition(dataGrid).Y > dataGrid.ActualHeight)
                        {
                            // ドロップ先が DataGrid の下にある場合、一番下に挿入
                            viewModel.Items.Remove(draggedItem);
                            viewModel.Items.Add(draggedItem);
                        }
                        else
                        {
                            int targetIndex = viewModel.Items.IndexOf(targetItem);
                            if (targetIndex != -1 && targetIndex != dropIndex)
                            {
                                viewModel.Items.Move(dropIndex, targetIndex);
                            }
                        }
                    }
                }
            }
            isRowDragging = false;
        }

        private T FindVisualParent<T>(DependencyObject child) where T : DependencyObject
        {
            DependencyObject parentObject = VisualTreeHelper.GetParent(child);

            if (parentObject == null) return null;

            if (parentObject is T parent)
            {
                return parent;
            }
            else
            {
                return FindVisualParent<T>(parentObject);
            }
        }

        private T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is T found)
                {
                    return found;
                }
                else
                {
                    T childOfChild = FindVisualChild<T>(child);
                    if (childOfChild != null)
                        return childOfChild;
                }
            }
            return null;
        }
    }
}

Item.cs
サンプルデータを格納するItemクラスとViewModelのクラスです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DragGrid
{
    using System.Collections.ObjectModel;
    using System.ComponentModel;

    // データモデル
    public class Item : INotifyPropertyChanged
    {
        private int id;
        private string name;

        public int ID
        {
            get { return id; }
            set { id = value; OnPropertyChanged("ID"); }
        }

        public string Name
        {
            get { return name; }
            set { name = value; OnPropertyChanged("Name"); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    // ViewModel
    public class MainViewModel
    {
        public ObservableCollection<Item> Items { get; set; }

        public MainViewModel()
        {
            Items = new ObservableCollection<Item>
            {
                new Item { ID = 1, Name = "Item 1" },
                new Item { ID = 2, Name = "Item 2" },
                new Item { ID = 3, Name = "Item 3" },
                new Item { ID = 4, Name = "Item 4" },
                new Item { ID = 5, Name = "Item 5" }
            };
        }
    }

}

以下がアプリケーションを実行した画面です。選択した行をドラッグ&ドロップで入れ替えできました!

DataGridでドラッグ&ドロップ実行1

DataGridでドラッグ&ドロップ実行2

MainWindow.xaml
ここからはタッチ操作でのドラッグ&ドロップのソースです。 タッチ系のイベントを設定しています。

<Window x:Class="DataGridDragDrop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataGrid DragDrop" Height="300" Width="400">
    <Grid>
        <DataGrid x:Name="dataGrid" ItemsSource="{Binding Data}" CanUserReorderColumns="True"
                  CanUserSortColumns="True" CanUserAddRows="False" AllowDrop="True"
                  SelectionMode="Extended" SelectionUnit="FullRow"
                  PreviewMouseLeftButtonDown="DataGrid_PreviewMouseLeftButtonDown"
                  PreviewMouseMove="DataGrid_PreviewMouseMove" Drop="DataGrid_Drop" DragOver="DataGrid_DragOver"
                  PreviewMouseLeftButtonUp="DataGrid_PreviewMouseLeftButtonUp"
                  TouchDown="DataGrid_TouchDown" TouchMove="DataGrid_TouchMove" TouchUp="DataGrid_TouchUp"
                  ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" Margin="21,25,23.6,28.4">
            <DataGrid.Resources>
                <Style TargetType="DataGridRow">
                    <EventSetter Event="PreviewMouseDown" Handler="Row_PreviewMouseDown"/>
                </Style>
            </DataGrid.Resources>
        </DataGrid>
    </Grid>
</Window>

MainWindow.xaml.cs
タッチ系のイベントで実行される処理を記述します。 タッチ操作の場合、ドラッグ時にデータグリッドのスクロールイベントも動いてしまうので e.Handledで伝搬を止めています。これを記述しないと行を掴むことができずドラッグできませんでした。

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace DataGridDragDrop
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<DataItem> Data { get; set; }
        private bool isDragging;
        private DataItem draggedItem;
        private bool isTouchDown;
        private Point touchStartPosition;
        private DataGridRow draggedRow;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Data = new ObservableCollection<DataItem>();

            // ダミーデータの作成(テスト用)
            for (int i = 1; i <= 10; i++)
            {
                Data.Add(new DataItem { ID = i, Name = "Item " + i });
            }
        }

        private void DataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            DataGrid grid = sender as DataGrid;
            if (grid == null)
                return;

            var row = FindVisualParent<DataGridRow>(e.OriginalSource as DependencyObject);
            if (row != null)
            {
                draggedItem = (DataItem)row.Item;
                isDragging = true;
                draggedRow = row;
                touchStartPosition = e.GetPosition(grid);
            }
        }

        private void DataGrid_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            DataGrid grid = sender as DataGrid;
            if (isDragging && e.LeftButton == MouseButtonState.Pressed)
            {
                Point currentPosition = e.GetPosition(grid);

                if (isTouchDown)
                {
                    if (Math.Abs(currentPosition.Y - touchStartPosition.Y) > SystemParameters.MinimumVerticalDragDistance)
                    {
                        isDragging = false;
                        isTouchDown = false;
                        DataGridRow row = FindVisualParent<DataGridRow>(e.OriginalSource as DependencyObject);
                        if (row != null)
                        {
                            row.IsSelected = true;
                        }
                    }
                }
                else
                {
                    Image img = new Image { Source = new BitmapImage(new Uri("pack://application:,,,/blank.png")) };
                    DataObject data = new DataObject(typeof(DataItem), draggedItem);
                    DragDrop.DoDragDrop(img, data, DragDropEffects.Move);
                    isDragging = false;
                    draggedItem = null;
                    draggedRow = null;
                }
            }
        }

        private void DataGrid_DragOver(object sender, DragEventArgs e)
        {
            if (!isTouchDown)
            {
                Point currentPosition = e.GetPosition(dataGrid);
                ScrollDataGridOnDrag(currentPosition);
            }
        }

        private void ScrollDataGridOnDrag(Point currentPosition)
        {
            double scrollSpeed = 1;

            ScrollViewer scrollViewer = FindVisualChild<ScrollViewer>(dataGrid);
            if (scrollViewer != null)
            {
                if (currentPosition.Y < SystemParameters.MinimumVerticalDragDistance)
                {
                    if (scrollViewer.VerticalOffset > 0)
                    {
                        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - scrollSpeed);
                    }
                }
                else if (currentPosition.Y > dataGrid.ActualHeight - SystemParameters.MinimumVerticalDragDistance)
                {
                    if (scrollViewer.VerticalOffset < scrollViewer.ScrollableHeight)
                    {
                        scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + scrollSpeed);
                    }
                }
            }
        }


        private static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is T)
                {
                    return (T)child;
                }
                else
                {
                    T childOfChild = FindVisualChild<T>(child);
                    if (childOfChild != null)
                    {
                        return childOfChild;
                    }
                }
            }
            return null;
        }

        private void Row_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            DataGridRow row = sender as DataGridRow;
            if (row != null && e.LeftButton == MouseButtonState.Pressed)
            {
                draggedItem = (DataItem)row.Item;
                isDragging = true;
                draggedRow = row;
                touchStartPosition = e.GetPosition(dataGrid);
            }
        }


        private void DataGrid_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            isTouchDown = false;
        }

        private void DataGrid_Drop(object sender, DragEventArgs e)
        {
            DataGrid grid = sender as DataGrid;
            if (grid == null || draggedItem == null)
                return;

            if (draggedItem != null && draggedRow != null)
            {
                var newIndex = GetDataGridRowIndex(e.GetPosition(grid));
                if (newIndex < 0)
                    return;

                int targetIndex = Data.IndexOf(draggedItem);
                Data.Move(targetIndex, newIndex);
            }
        }

        private int GetDataGridRowIndex(Point position)
        {
            var result = -1;
            var hitTest = VisualTreeHelper.HitTest(dataGrid, position);
            if (hitTest != null)
            {
                var row = FindVisualParent<DataGridRow>(hitTest.VisualHit);
                if (row != null)
                {
                    result = dataGrid.Items.IndexOf(row.Item);
                }
            }
            return result;
        }

        private void DataGrid_TouchDown(object sender, TouchEventArgs e)
        {
            DataGrid grid = sender as DataGrid;
            if (grid == null)
                return;

            var row = FindVisualParent<DataGridRow>(e.OriginalSource as DependencyObject);
            if (row != null)
            {
                draggedItem = (DataItem)row.Item;
                isDragging = true;
                draggedRow = row;
                touchStartPosition = e.GetTouchPoint(grid).Position;
                isTouchDown = true;

                // 選択された行を選択状態にする
                row.IsSelected = true;

                // タッチダウンイベントをハンドルしてスクロールを抑制
                e.Handled = true;
            }
        }

        private void DataGrid_TouchMove(object sender, TouchEventArgs e)
        {
            DataGrid grid = sender as DataGrid;
            if (isDragging && isTouchDown)
            {
                Point currentPosition = e.GetTouchPoint(grid).Position;

                if (Math.Abs(currentPosition.Y - touchStartPosition.Y) > SystemParameters.MinimumVerticalDragDistance)
                {
                    isDragging = false;
                    isTouchDown = false;

                    // タッチムーブイベントをハンドルしてスクロールを抑制
                    e.Handled = true;

                    // ドラッグ処理を開始
                    Image img = new Image { Source = new BitmapImage(new Uri("pack://application:,,,/blank.png")) };
                    DataObject data = new DataObject(typeof(DataItem), draggedItem);
                    DragDrop.DoDragDrop(img, data, DragDropEffects.Move);
                }
            }
        }

        private void DataGrid_TouchUp(object sender, TouchEventArgs e)
        {
            isDragging = false;
            isTouchDown = false;
        }

        private static T FindVisualParent<T>(DependencyObject obj) where T : DependencyObject
        {
            while (obj != null)
            {
                if (obj is T parent)
                    return parent;
                obj = VisualTreeHelper.GetParent(obj);
            }
            return null;
        }
    }

    public class DataItem
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }
}

管理人情報