« OLEドラッグ&ドロップ対応にする方法(ファイル編) | トップページ | TextBox のマウス位置に該当する行・桁位置を求める »

2011年2月 4日 (金)

OLEドラッグ&ドロップ対応にする方法(オブジェクト編)

.NETにおけるOLEドラッグ&ドロップは、いろいろなデータを取り扱えますが、その制約について改めて確認しておきます。
Control.DoDragDrop メソッドで、『データは、基本マネージ クラス (String 、 Bitmap 、または Metafile)、あるいは ISerializable または IDataObject を実装するオブジェクトのいずれかである必要があります。』と説明されています。
つまり、これらのデータである限り、OLEドラッグ&ドロップが出来るという事です。

ではここで、前回ファイル編で使用したドロップソース側のサンプルを修正して、ListBox から ListView に置き換えてみます。ここでは、View プロパティを View.Details 、MultiSelect プロパティを false に設定している前提とします。そしてドラッグした、ListViewItem 自身をドラッグで受け渡すデータとしてみます。なお、ListViewItem は、ISerializable インターフェースを実装しているので、前述のデータ受け渡し制約の範囲内となります。

なお、今回も最後にサンプルコードがダウンロード出来るようにしています。詳細はそちらを見て頂きたいのですが、ListViewItem として、改造前同様任意のフォルダ内のファイル情報を表示しています。但し、今回の記事の関連で、若干ながら凝った造りにしています。
では、さっそく本題に入ります。

1.オブジェクトのドラッグ&ドロップを実現する記述
まず、ListView では ItemDrag という便利なイベントが用意されています。ListBox でやっていた様な、MouseDown / MouseMove / MouseUp イベントを使用したドラッグ開始判断は不要となり、以下の様なシンプルな記述で、ドラッグ開始処理が実現出来ます。

private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
{
    DragDropEffects dde = listView1.DoDragDrop(e.Item, DragDropEffects.All);
}

これが、ドロップソースとしての必要最小限のコードになるでしょう。では、次にドロップターゲット側の記述です。
こちらも、まずは最小限のコードとなる様に、DragDropEffects 効果は Copy のみとすれば、この様な感じでしょうか。

private void listView1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(ListViewItem)))
        e.Effect = DragDropEffects.Copy;
}

private void listView1_DragDrop(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(ListViewItem))) return;
    ListViewItem dropItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem));

    // ドロップ位置から挿入位置となるインデックス値を決定する
    Point dropPoint = listView1.PointToClient(new Point(e.X, e.Y));
    ListViewItem positionItem = listView1.GetItemAt(dropPoint.X, dropPoint.Y);
    int dropIndex = listView1.Items.IndexOf(positionItem);

    // 項目の範囲外でドロップしたときは、最後尾に追加する様にインデックス値を設定
    if (dropIndex < 0)
        dropIndex = listView1.Items.Count;

    // ドラッグされてきた項目を挿入
    ListViewItem insertItem = listView1.Items.Insert(dropIndex, (ListViewItem)dropItem.Clone());
    insertItem.Selected = true;
}

ドラッグ&ドロップそのものより、ドロップ後の ListViewItem 挿入処理が大半を占めます。これでコンパイルして出来た実行ファイルを二重起動させると、他プロセスへの ListViewItem の複写も出来ます。
以前とちょっと違うのが、GetDataPresent の記述です。ここで、Type 指定を行なっています。こうすると、別のクラスのオブジェクト(例えばTreeNode)の受け入れは拒否されます。ListViewItem クラスだけを受け入れたいので、こうやっておくと別のクラスオブジェクトを受け入れてしまうトラブルを避けられます。

2.オブジェクトによるドラッグ&ドロップの注意点
ListViewItem をドロップターゲットとして受け付ける別アプリケーションが存在した場合、その挙動はどうなるでしょう。想像がつくと思いますが、当然上記1.の様な記述であれば、ドラッグ&ドロップを受け入れてしまいます。ここで、上記のドラッグ&ドロップ記述を行なった、DropObject1 と、DropObject2 のサンプルを用意してみました。内容的には、カラムの位置替えと ImageList の登録位置替えを行なって、同じアイコンを表示する場合の、ImageIndex 値を変えています。これで DropObject1 DropObject2 間のドラッグ&ドロップを行なえば、当然ドラッグ元と同じプロパティ値で表示しようとするので、結果としておかしな表示になります。

DropObject1
DropObject2(一見同じに見えるアイコンも、ImageIndex 値は変えている)
DropObject2(DropObject1から、"bin","Form1.cs"を連続してドラッグ&ドロップ後)

わざわざ例示するまでもなかったかもしれませんが、この様にオブジェクトをドラッグ&ドロップとして受け入れる場合は、このあたりに注意が必要かもしれません。まあ、ListViewItem を受け入れるアプリケーション等滅多にないでしょうから、無視する(仮にあったとしてもそれは仕様だから仕方ないと割り切る)手もありだとは思います。

しかし、一般論として、オブジェクトのドラッグ&ドロップは、自前のアプリケーションだけを対象とするべきですね。また、本来OLEドラッグ&ドロップでなくてよい、つまり自プロセス内だけに限定してのドラッグ&ドロップをやりたい場合もあるかと思います。その様な場合に、本来対象外となるアプリケーション間とのドラッグ&ドロップを受け入れ拒否出来れば、何も問題は起きません。その対策を考えてみましょう。

3.自前アプリケーション間だけを対象にしたドラッグ&ドロップ
先程使用した、DataObject1, DataObject2 のアプリケーションで例えると、DataObject1 が自前、DataObject2 が自前でないアプリケーションと仮定します。つまり、DataObject2 のソースはなく、プログラム修正は出来ないと仮定します。
DataObject2 が、ListViewItem クラスをドロップターゲットとして受け入れるので、DataObject1 側では、ListViewItem クラスをドラッグデータとして使用している限り、問題は解決出来ません。従って、ここでは、ListViewItem クラスに代わる独自クラスを使用します。独自クラスであれば、そもそも一般の他アプリケーションに使用される事がありません。

ここでは、元々利用したい ListViewItem クラスから継承させてもいいのですが、保存したいプロパティが多くない事もあり、継承はしていません。独自クラス DragDropItem の定義は、以下の様にしてみました。

[Serializable]
public class DragDropItem
{
    public string Text;
    public int ImageIndex;
    public StringCollection SubItems = new StringCollection();
    public DragDropItem(ListViewItem lvi)
    {
        this.Text = lvi.Text;
        this.ImageIndex = lvi.ImageIndex;
        foreach (ListViewItem.ListViewSubItem subItem in lvi.SubItems)
            this.SubItems.Add(subItem.Text);
    }
}

この DragDropItem をドラッグデータとして使用する様に変更した各ドラッグ&ドロップ関連イベント処理は、以下のとおりです。

private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
{
    DragDropItem ddi = new DragDropItem((ListViewItem)e.Item);
    DragDropEffects dde = listView1.DoDragDrop(ddi, DragDropEffects.All);
}

private void listView1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(DragDropItem)))
        e.Effect = DragDropEffects.Copy;
}

private void listView1_DragDrop(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(DragDropItem))) return;
    DragDropItem dropItem = (DragDropItem)e.Data.GetData(typeof(DragDropItem));

    // ドロップ位置から挿入位置となるインデックス値を決定する
    Point dropPoint = listView1.PointToClient(new Point(e.X, e.Y));
    ListViewItem positionItem = listView1.GetItemAt(dropPoint.X, dropPoint.Y);
    int dropIndex = listView1.Items.IndexOf(positionItem);

    // 項目の範囲外でドロップしたときは、最後尾に追加する様にインデックス値を設定
    if (dropIndex < 0)
        dropIndex = listView1.Items.Count;

    // ドラッグされてきた項目を挿入
    ListViewItem insertItem = listView1.Items.Insert(dropIndex,
        dropItem.Text, dropItem.ImageIndex);
    for (int i = 1;i < dropItem.SubItems.Count;++i)
        insertItem.SubItems.Add(dropItem.SubItems[i]);
    insertItem.Selected = true;
}

これで、DragDrop1 と DragDrop2 の間でのドラッグ&ドロップが出来ないようにする事が出来ました。一方、DragDrop1 を複数起動しての、別プロセス間でのドラッグ&ドロップは行なえます。

4.自プロセスだけを対象にしたドラッグ&ドロップ
この場合、何も DoDragDrop メソッドを使用する理由はありません。通常の MouseDown / MouseMove / MouseUp イベントで、ドラッグ&ドロップの処理をしている様に、カーソル表示を変更してもOKな気がします。

しかしここでは、DoDragDrop を使用して自プロセスだけを対象にしたドラッグ&ドロップを動かせないか、試してみました。まず着目したのが、最初に挙げたドラッグデータとしての制約です。
シリアライズ出来ないオブジェクトを入れてドラッグ&ドロップ処理を実行してみましょう。

ここで、先程改良した DragDropItem クラス使用の DragDrop1 をテスト的に再改造してみましょう。改造点は1箇所、DragDropItemクラス定義の直前に宣言している、

[Serializable]

の行をなくします。これで実行すると、DoDragDrop メソッドはもちろん、GetDataPresent メソッドもきちんと動作します。しかし、GetData メソッドを実行すると、自プロセスなら正常実行されますが、他プロセスなら GetData メソッドにて InvalidCastException が発生します(型キャストを行なった場合)。要するに as 演算子を使用して例外発生を避け、GetData メソッドの結果が null かどうかを判断すれば、自プロセスかどうかが簡単に判別出来ます。
この結果から、DragEnter 及び DragDrop イベント処理を以下のように書き換えると、自プロセスのみを対象にしたドラッグ&ドロップが出来る様です。

private void listView1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(DragDropItem)))
    {
        // 他プロセスからのドラッグデータであれば、dropItem は null になる
        DragDropItem dropItem = e.Data.GetData(typeof(DragDropItem)) as DragDropItem;
        if (dropItem != null)
            e.Effect = DragDropEffects.Copy;
    }
}

private void listView1_DragDrop(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(DragDropItem))) return;
    DragDropItem dropItem = e.Data.GetData(typeof(DragDropItem)) as DragDropItem;
    if (dropItem == null) return;   // 他プロセスからのドラッグデータでは、null となる

    // ドロップ位置から挿入位置となるインデックス値を決定する
    Point dropPoint = listView1.PointToClient(new Point(e.X, e.Y));
    ListViewItem positionItem = listView1.GetItemAt(dropPoint.X, dropPoint.Y);
    int dropIndex = listView1.Items.IndexOf(positionItem);

    // 項目の範囲外でドロップしたときは、最後尾に追加する様にインデックス値を設定
    if (dropIndex < 0)
        dropIndex = listView1.Items.Count;

    // ドラッグされてきた項目を挿入
    ListViewItem insertItem = listView1.Items.Insert(dropIndex, dropItem.Text, dropItem.ImageIndex);
    for (int i = 1;i < dropItem.SubItems.Count;++i)
        insertItem.SubItems.Add(dropItem.SubItems[i]);
    insertItem.Selected = true;
}

5.MultiSelect な ListView のドラッグ&ドロップ
さて、今までは ListView が、単一 Item しか 選択出来ない前提で、ドラッグ&ドロップを行なって来ました。では、ListView を複数選択可能にしてのドラッグ&ドロップ処理はどうなるでしょうか。

まず、基本的に MultiSelect でも、ItemDrag イベントは1回しか発生しません。まあ、これは当然かもしれませんが、引数で受け取れる ItemDragEventArgs.Item プロパティは、最後に選択した ListViewItem しか格納しない様です。つまり、MultiSelect では、このプロパティは(実質的に)使えません。

また、当然現在選択中の項目をドラッグ対象にするでしょうから、ListView.SelectedItems プロパティである ListView.SelectedListViewItemCollection クラスを DoDragDrop メソッドで指定したいところです。ところが、これはシリアライズ可能ではありません。従って、他プロセスへのドラッグ&ドロップを前提にした場合は、使用出来ません。
また、自プロセスのみのドラッグ&ドロップのみを対象にした場合でも、このクラスは親となる ListView の選択状態を変更すれば変わります。ドロップ処理で新たに追加された項目のみを選択状態にする等の処理を入れると、うまく動作しない場合もあります。もし現時点ではうまく動かせても、将来の仕様変更の可能性を考えると、使用しない方が無難でしょう。

つまり、MultiSelect によるドラッグ&ドロップは、本来独自クラスを用意する必要があります。

この場合、既に3.及び4.で説明した内容をちょっと応用すれば実現出来ます。それではおもしろくないので、既存のシリアライズ可能な別クラスを使用してみましょう。この場合、C#2003なら ArrayList、C#2005以降なら List<T> が使えそうです。ここは、C#2005使用を前提に、List<T>クラスを使用したソースを示します。

private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
{
    List<ListViewItem> selectItems =
        new List<ListViewItem>(listView1.SelectedItems.Count);
    foreach (ListViewItem lvi in listView1.SelectedItems)
        selectItems.Add(lvi);
    DragDropEffects dde = listView1.DoDragDrop(selectItems, DragDropEffects.All);
}

private void listView1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(List<ListViewItem>)))
        e.Effect = DragDropEffects.Copy;
}

private void listView1_DragDrop(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(List<ListViewItem>))) return;
    List<ListViewItem> dropItems = (List<ListViewItem>)e.Data.GetData(typeof(List<ListViewItem>));

    // ドロップ位置から挿入位置となるインデックス値を決定する
    Point dropPoint = listView1.PointToClient(new Point(e.X, e.Y));
    ListViewItem positionItem = listView1.GetItemAt(dropPoint.X, dropPoint.Y);
    int dropIndex = listView1.Items.IndexOf(positionItem);

    // 項目の範囲外でドロップしたときは、最後尾に追加する様にインデックス値を設定
    if (dropIndex < 0)
        dropIndex = listView1.Items.Count;

    // ドラッグされてきた項目を挿入
    listView1.SelectedItems.Clear();
    for (int i = dropItems.Count -1;i >= 0;--i)
    {
        ListViewItem insertItem = listView1.Items.Insert(dropIndex, (ListViewItem)dropItems[i].Clone());
        insertItem.Selected = true;
    }
}

これで他プロセスに対してのドラッグ&ドロップが実現出来ました。
また自プロセスのみをドラッグ&ドロップ対象にする場合、適切なシリアライズ出来ないコレクションクラスがない様なので、ちょっと困りますね。自プロセスでドラッグ開始したか、フラグを設けて処理するのもありでしょうが、ここは独自クラスを作成した方がいいと思います。個人的には、この様な限定された内容なら、List<T> を直接継承しても良いのではないかと思います。しかし、継承を推奨していない以上、公に使用するのもどうかという事で、ここでは Collection<T> から継承します。
よって、独自クラスの定義はこんな感じです。

// 自プロセスのみを対象にドラッグ&ドロップするので[Serializable]は入れない
public class DragDropItems : Collection<ListViewItem>
{
    public DragDropItems(ListView.SelectedListViewItemCollection lvc)
    {
        foreach (ListViewItem lvi in lvc)
            this.Add(lvi);
    }
}

そして、ドラッグ&ドロップ関連の処理は、以下の様な感じです。

private void listView1_ItemDrag(object sender, ItemDragEventArgs e)
{
    DragDropItems ddi = new DragDropItems(listView1.SelectedItems);
    DragDropEffects dde = listView1.DoDragDrop(ddi, DragDropEffects.All);
}

private void listView1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(typeof(DragDropItems)))
    {
        // 他プロセスからのドラッグデータであれば、dropItems は null になる
        DragDropItems dropItems = e.Data.GetData(typeof(DragDropItems))
            as DragDropItems;
        if (dropItems != null)
            e.Effect = DragDropEffects.Copy;
    }
}

private void listView1_DragDrop(object sender, DragEventArgs e)
{
    if (!e.Data.GetDataPresent(typeof(DragDropItems))) return;
    DragDropItems dropItems = e.Data.GetData(typeof(DragDropItems))
        as DragDropItems;
    // 他プロセスからのドラッグデータでは、null となる
    if (dropItems == null) return;

    // ドロップ位置から挿入位置となるインデックス値を決定する
    Point dropPoint = listView1.PointToClient(new Point(e.X, e.Y));
    ListViewItem positionItem = listView1.GetItemAt(dropPoint.X, dropPoint.Y);
    int dropIndex = listView1.Items.IndexOf(positionItem);

    // 項目の範囲外でドロップしたときは、最後尾に追加する様にインデックス値を設定
    if (dropIndex < 0)
        dropIndex = listView1.Items.Count;

    // ドラッグされてきた項目を挿入
    listView1.SelectedItems.Clear();
    for (int i = dropItems.Count -1;i >= 0;--i)
    {
        ListViewItem insertItem = listView1.Items.Insert(dropIndex,
            (ListViewItem)dropItems[i].Clone());
        insertItem.Selected = true;
    }
}

これで、自プロセスのみを対象にしたドラッグ&ドロップが実現出来ます。

以上で、オブジェクトに対するドラッグ&ドロップの説明は終わりです。最後に、今回の記事に使用したサンプルソース一式をCAB形式で圧縮して、ここに置いておきます。
「DropObject.cab」をダウンロード


« OLEドラッグ&ドロップ対応にする方法(ファイル編) | トップページ | TextBox のマウス位置に該当する行・桁位置を求める »

C# Tips」カテゴリの記事

コメント

この記事へのコメントは終了しました。

トラックバック


この記事へのトラックバック一覧です: OLEドラッグ&ドロップ対応にする方法(オブジェクト編):

« OLEドラッグ&ドロップ対応にする方法(ファイル編) | トップページ | TextBox のマウス位置に該当する行・桁位置を求める »