ListViewで複数の項目をドラッグアンドドロップで並び替える

# はじめに
1つのitemをドラッグアンドドロップで並び替えるのはすぐにできたが、複数のitemを一括で移動させる方法は調べても出てこなかったのでメモ。
`Shift`キーや`ctrl`キーで複数選択、そっから運びたい。

目指したのはエクスプローラー。

・View.detailの場合で実装してます

# ItemDragイベント
ドラッグアンドドロップ処理の開始。
選択中itemをつっこむ。
```C#
private void ItemDragMulti(object sender, ItemDragEventArgs e)
{
	ListView lv = (ListView)sender;
	DoDragDrop(lv.SelectedItems, DragDropEffects.Move);
}
```

# DragEnterイベント
一応、ドラッグの中身もチェック
```C#
private void DragEnterMulti(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(SelectedListViewItemCollection)))
    {
        e.Effect = DragDropEffects.None;
    }

    e.Effect = e.AllowedEffect; 
}
```

# DragOverイベント
DragOverイベントは、コントロールの境界を越えた時に発生するみたい。
ユーザーのドラッグ操作に合わせて、`InsertionMark`を表示させます。

```C#
private void DragOverMulti(object sender, DragEventArgs e)
{
    Point targetPoint = PointToClient(new Point(e.X, e.Y));
    
    // NearestIndex はドラッグ前itemとインデックスが同じ時、-1を返す
    int targetIndex = InsertionMark.NearestIndex(targetPoint)
    
    if (targetIndex > -1)
    {
        Rectangle itemBounds = GetItemRect(targetIndex);
        // itemの中点から下なら、InsertionMarkは次のitemに表示しますよ
        if (targetPoint.Y > itemBounds.Top + (itemBounds.Height / 2))
        {
            InsertionMark.AppearsAfterItem = true;
        }
        else
        {
            InsertionMark.AppearsAfterItem = false;
        }
    }
    InsertionMark.Index = targetIndex;
}
```


# DragDropイベント
いよいよ落とします。
`DragEventArgs`から、ItemDragイベントでつっこんだ選択中itemたちを引っ張り出していく。
下では、挿入先インデックスがわかったらメソッドに投げています。

```C#
private void DragDropMulti(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(SelectedListViewItemCollection)))
    {
        var srcItems = (SelectedListViewItemCollection)e.Data.GetData(typeof(SelectedListViewItemCollection));
        int destIndex = InsertionMark.Index;

        // 挿入マークが非表示である
        if (destIndex == -1)
        {
            return;
        }

        MoveRows(this, destIndex, srcItems);

        InsertionMark.Index = -1;
    }
}
```

## MoveRowsメソッド
DragOverイベントで、マウス座標が各itemの中点より下にあるかで、InsertionMarkの位置が変わるようにしていました。
こちらの動きもそれに合わせます。

あと、複数項目の移動に`foreach`で対応しています。
この時、複数項目かつ下から上への移動の時にはうまくいかず。
例えば、「5,6」番目を2番目にドラッグアンドドロップすると、「6,5」の順番で入ってしまいます。
これは5番目が先に移動し、その後の6番目の挿入も同じ`targetIndex`に挿入しているからですね。
この時だけ、項目毎に`targetIndex`をインクリメントしてやればオッケーです。

```C#
private void MoveRows(ListView list, int insertIndex, SelectedListViewItemCollection col)
{
    foreach (ListViewItem item in col)
    {
        int oldIndex = item.Index;

        // AppearsAfterItem が有効なら挿入先は1つ下へ
        int newIndex =
            InsertionMark.AppearsAfterItem
            ? insertIndex + 1
            : insertIndex;

        list.Items.Insert(newIndex, (ListViewItem)item.Clone());
        list.Items[newIndex].Selected = true;
        list.Items.Remove(item);

        // 下から上へは、複数項目の場合、順序が反転してしまう
        if (oldIndex > insertIndex)
        {
            insertIndex++;
        }
    }
}
```

# おわりに
複数項目のドラッグアンドドロップ、思ったよりネットに記事がなくてちょっと驚きました。(使う場面多そうだなぁと思ってたので。リサーチ力不足か...?)

ただ、今回の実装では以下と併用できず。
・仮想ビュー使用時 (たしか、選択中のitemが使えなかった気が...)
・`ListViewItemSorter`使用時 (はて...?)

仮想ビューは何となくできそうですが、`ListVIewItemSorter`を使ったときにドラッグアンドドロップがうまくいかなくなったのは検討もついておりません。
今回の実装に加えて、ヘッダー部クリックのソートも入れたいが...
うーん。


# 参考
・[方法: Windows フォーム ListView コントロールに挿入マークを表示する](https://learn.microsoft.com/ja-jp/dotnet/desktop/winforms/controls/how-to-display-an-insertion-mark-in-a-windows-forms-listview-control?view=netframeworkdesktop-4.8)
・[Drag&Dropを行う](https://dobon.net/vb/dotnet/control/draganddrop.html)