DataGridView:行のドラッグ&ドロップとドロップ先のインディケーター表示(サンプルあり)
📋 目次(クリックで展開)
DataGridView で一覧画面を表示する際、ユーザーの任意の順序に行を並び替えさせたい場合があります。列の移動についてはプロパティの設定による標準機能でサポートされていますが、行の移動をドラッグ&ドロップで行うには独自の制御が必要です。
今回は、行のドラッグ&ドロップの実装方法に加え、列移動時のようにドロップ先を行と行の間の線(インディケーター)で視覚的に示す UI の構築手順について解説します。
行移動とインディケーター描画の仕組み
この処理を実現するには、DataGridView の複数のイベントを連携させます。
MouseDown/MouseMove:クリックされた行インデックスを保持し、ドラッグ操作を開始(DoDragDrop)します。DragOver:現在マウスがどの行の上にあるかを特定し、セルの上半分にあるか下半分にあるかを判定して、インディケーターを描画すべき位置(行の上側か下側か)を割り出します。RowPostPaint:セルの背景やテキストが描画された後に呼び出されるイベントです。DragOverで割り出したターゲット位置に合わせて、e.Graphics.DrawLineを使用して行の境界にインディケーター(線)を描画します。DragDrop:ドラッグが確定したタイミングで、データソース(リストなど)の要素を並び替えます。
実装コード
以下は、BindingList をデータソースとしてバインドし、行をドラッグした際にドロップ先を示す青いインディケーターを表示しながら、行の並び替えを行うコード例です。
Form1.cs
using System.ComponentModel;
namespace DataGridView_Row_Drag_Drop{ public partial class Form1 : Form { private BindingList<PersonModel> _people = new BindingList<PersonModel>();
// ドラッグ&ドロップ制御用の変数 private int _draggedRowIndex = -1; private int _targetRowIndex = -1; private bool _isTargetHoverBottom = false; private Rectangle _dragBoxFromMouseDown;
public Form1() { InitializeComponent(); InitializeDataGridView(); }
private void InitializeDataGridView() { // テスト用データの準備 _people.Add(new PersonModel(1, "じゅんや", 23)); _people.Add(new PersonModel(2, "きょうこ", 25)); _people.Add(new PersonModel(3, "えみ", 24)); _people.Add(new PersonModel(4, "たつや", 28)); _people.Add(new PersonModel(5, "なおき", 33)); _people.Add(new PersonModel(6, "ゆうか", 25)); _people.Add(new PersonModel(7, "まゆみ", 24));
dataGridView1.DataSource = _people; dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
// ドラッグ&ドロップを受け入れる設定 dataGridView1.AllowDrop = true;
// 必要なイベントを購読 dataGridView1.MouseDown += DataGridView1_MouseDown; dataGridView1.MouseMove += DataGridView1_MouseMove; dataGridView1.DragOver += DataGridView1_DragOver; dataGridView1.DragLeave += DataGridView1_DragLeave; dataGridView1.DragDrop += DataGridView1_DragDrop; dataGridView1.RowPostPaint += DataGridView1_RowPostPaint; }
// --- 1. ドラッグ開始の制御 ---
private void DataGridView1_MouseDown(object? sender, MouseEventArgs e) { var hit = dataGridView1.HitTest(e.X, e.Y); // 有効なデータ行(新規入力行を除く)がクリックされたか確認 if (hit.RowIndex >= 0 && hit.RowIndex < dataGridView1.RowCount && !dataGridView1.Rows[hit.RowIndex].IsNewRow) { _draggedRowIndex = hit.RowIndex;
// ドラッグを開始するまでの遊び(DragSize)を設定 Size dragSize = SystemInformation.DragSize; _dragBoxFromMouseDown = new Rectangle(new Point(e.X - (dragSize.Width / 2), e.Y - (dragSize.Height / 2)), dragSize); } else { _draggedRowIndex = -1; _dragBoxFromMouseDown = Rectangle.Empty; } }
private void DataGridView1_MouseMove(object? sender, MouseEventArgs e) { if ((e.Button & MouseButtons.Left) == MouseButtons.Left) { // 一定距離以上マウスが動いたらドラッグを開始 if (_dragBoxFromMouseDown != Rectangle.Empty && !_dragBoxFromMouseDown.Contains(e.X, e.Y)) { if (_draggedRowIndex >= 0) { dataGridView1.DoDragDrop(dataGridView1.Rows[_draggedRowIndex], DragDropEffects.Move); } } } }
// --- 2. ドラッグ中の制御と位置特定 ---
private void DataGridView1_DragOver(object? sender, DragEventArgs e) { e.Effect = DragDropEffects.Move;
// 画面座標からDataGridView内のローカル座標に変換 Point clientPoint = dataGridView1.PointToClient(new Point(e.X, e.Y)); var hit = dataGridView1.HitTest(clientPoint.X, clientPoint.Y);
int oldTarget = _targetRowIndex; bool oldHoverBottom = _isTargetHoverBottom;
if (hit.RowIndex >= 0 && hit.RowIndex < dataGridView1.RowCount) { _targetRowIndex = hit.RowIndex;
// マウスがセルの上半分にあるか下半分にあるかを判定 Rectangle rect = dataGridView1.GetRowDisplayRectangle(hit.RowIndex, false); if (clientPoint.Y > rect.Top + (rect.Height / 2)) { _isTargetHoverBottom = true; // 下側にインディケーターを表示 } else { _isTargetHoverBottom = false; // 上側にインディケーターを表示 } } else { _targetRowIndex = -1; _isTargetHoverBottom = false; }
// 移動先が変わった場合のみ再描画を行い、インディケーターの位置を更新 if (_targetRowIndex != oldTarget || _isTargetHoverBottom != oldHoverBottom) { dataGridView1.Invalidate(); } }
private void DataGridView1_DragLeave(object? sender, EventArgs e) { // 画面外にドラッグが出たらインディケーターを消去 _targetRowIndex = -1; dataGridView1.Invalidate(); }
// --- 3. インディケーターの描画処理 ---
private void DataGridView1_RowPostPaint(object? sender, DataGridViewRowPostPaintEventArgs e) { // 現在描画中の行が、ドラッグのターゲット行であるか確認 if (_targetRowIndex >= 0 && e.RowIndex == _targetRowIndex) { // 列移動の線に合わせて、少し太めの青いペンを用意 using var pen = new Pen(Color.DodgerBlue, 2);
// 上端か下端か、判定に合わせてY座標を設定 int y = _isTargetHoverBottom ? e.RowBounds.Bottom - 1 : e.RowBounds.Top;
// 行の左端から右端にむけて水平線を描画 e.Graphics.DrawLine(pen, e.RowBounds.Left, y, e.RowBounds.Right, y); } }
// --- 4. ドロップ時のデータ並び替え ---
private void DataGridView1_DragDrop(object? sender, DragEventArgs e) { if (_draggedRowIndex >= 0 && _targetRowIndex >= 0) { int destIndex = _targetRowIndex; if (_isTargetHoverBottom) { destIndex++; }
// 新規入力行(最下行)へのドロップを防ぐ調整 if (destIndex > _people.Count) { destIndex = _people.Count; }
// 自分より後ろの行に挿入する場合のインデックス位置調整 if (_draggedRowIndex < destIndex) { destIndex--; }
// 同一位置へのドロップでなければリストを並び替える if (_draggedRowIndex != destIndex && destIndex >= 0 && destIndex < _people.Count) { var item = _people[_draggedRowIndex]; _people.RemoveAt(_draggedRowIndex); _people.Insert(destIndex, item); } }
// 変数を初期化して再描画 _draggedRowIndex = -1; _targetRowIndex = -1; dataGridView1.Invalidate(); } }
/// <summary> /// データモデルクラス /// </summary> public class PersonModel { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; }
public PersonModel(int id, string name, int age) { Id = id; Name = name; Age = age; } }}留意すべき注意点と実装の背景
行のドラッグ&ドロップ UI を実装する際、スムーズな操作感とデータの不整合を防ぐために把握しておくとよい点が3つあります。
手動並び替えと列ソートの排他設計
今回のサンプルコードを動かすと、列ヘッダーをクリックしても並び替え(ソート)が動作しない状態になります。これはドラッグ処理の影響ではなく、データソースに使用している BindingList<T> が標準でソート機能を備えていないことによる仕様です。
実務の画面設計においては、この挙動は不具合ではなく、むしろ望ましい状態といえます。手動で並び替える画面において列ソートを許可してしまうと、ユーザーが決めた独自の順序が列クリック一つで上書きされ、失われてしまうためです。手動並び替え画面では、各列の SortMode プロパティを NotSortable に設定し、ソート機能自体を明示的に無効化しておく方法が適しています。
マウス位置による上下判定とインディケーターの同期
単純に行単位でドロップ先を特定するだけでは、行の上側に挿入したいのか下側に挿入したいのかが区別できず、列移動のような細かな操作感が再現できません。
サンプルコードでは DragOver イベント内で、ターゲット行の表示領域(GetRowDisplayRectangle)の高さから、マウスがセルの上半分と下半分のどちらに位置しているかを計算しています。この情報を RowPostPaint に引き渡して線の描画位置を Top と Bottom で切り替えることで、列移動時と同様の「行と行の間に線が挟まる」表現が可能になります。
移動先が切り替わったときのみ再描画(Invalidate)を行うことで、処理の重複による画面のちらつきを抑えることができます。
要素を挿入する際のインデックスの調整
リスト(BindingList)の要素を移動させる際、ドラッグした元の位置よりも後ろのインデックスへ挿入する場合(例:2行目を5行目の位置へ移動)は、計算の仕様に注意が必要です。
先に RemoveAt で元の要素を削除すると、リスト全体の要素数が1つ減るため、後続の要素のインデックスがすべて前にずれます。そのため、削除した後にそのままのインデックス値で Insert を行うと、意図した位置よりも1つ後ろにずれて挿入されてしまいます。これを防ぐために、サンプルコードでは元の位置が挿入先より前である場合にインデックス値を1つ減らす補正処理を記述しています。
まとめ
今回は、DataGridView で行のドラッグ&ドロップを実装し、列移動のようなドロップ先インディケーターを表示する方法について解説しました。
RowPostPaint によるカスタム描画と、マウス位置の上下判定を組み合わせることで、視覚的に分かりやすいグリッド操作環境を構築できます。
DragOverでマウスが行の上半分にあるか下半分にあるかを判定するRowPostPaint内で条件に応じた境界(Top / Bottom)に線を描画する- ユーザーが決定した順序を保護するため、列ソート機能とは組み合わせず排他にする
- リストの要素を並び替える際は、削除によるインデックスのずれを補正する
ユーザーによる任意の順序指定が必要なマスタ設定画面などの利便性向上に、参考になりましたら幸いです。
💬 コメント