1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ドラッグドロップ時にイメージを表示する

Posted at

はじめに

Windowsエクスプローラーに存在しないファイルをドロップする(Part4)の続きですが、この記事単体でも読めます。Windowsエクスプローラーではドラッグ中に↓こういうイメージが表示されますが、それを自前のデスクトップアプリケーションでも表示する方法です。
image.png

実装

WinForms

WinFormsでは実は組み込みの機能提供があります。Control.DoDragDropを呼ぶ際にBitmapを渡してやることで簡単にドラッグ中のイメージ表示ができるのです。

dwEffect = control.DoDragDrop(data, allowedEffects, dragImage, cursorOffset, useDefaultDragImage);

ドラッグ中のテキスト表示にはDragEnterなどのイベントハンドラーにテキストを設定してやります。

private void Form1_DragEnter(object sender, DragEventArgs e)
{
    e.Effect = DragDropEffects.Link;
    e.DropImageType = DropImageType.Link;
    e.Message = "この文字列が表示される";

WinForms歴の長い人でも知らない人がいるのはないでしょうか。なんと、この機能が追加されたのは.NET7以降1。LTSという意味では.NET8以降です。2022年になってWinFormsはまだ進化する。しかもこういうあってもなくても微妙に困らないところを機能追加する。ちょっと感動しています。
しかしどこか処理が甘いのか、エクスプローラー上でドラッグしたときにイメージが消えずに残る現象が起きがちです。どうもWinFormsアプリ内でのドラッグドロップを念頭においたような経緯がありそう2で、あまりエクスプローラーのことは考えてないのかもしれません。
そこも解消したものが欲しければ、自分で直してPL投げるか、WPF版と同じやり方をします。

WPF

WPFといいつつ、COM実装なので言語・フレームワーク依らず汎用ソリューションです。やり方を知っているかどうかだけなのですが、どうにもそのやり方がまとまっていない。そしてやるのもまあまあ面倒くさい。そのためにこの記事にまとめているわけですが。情報源は以下のリンクです。

IDataObjectインタフェース

まずCOMのIDataObjectインタフェースをを実装してやります。Part4では渡したいデータだけをGetDataGetDataHereで渡すように実装していましたが、ドラッグイメージを表示するにはSetDataで動的に受け取ってGetDataで返せるようにする必要があります。
SetDataメソッドはそれほど難しくはありません。受け取ったデータを、重複チェックののち内部に保持するだけです。実装するうえで注意が必要なのはGetDataおよびGetDataHereの実装です。各メソッドの説明を見てもらえればわかる通り、GetDataout変数またはGetDataHereref変数で返したSTGMEDIUMは受け取った側で解放してしまいます。そのため返すSTGMEDIUMSetDataで受け取ったデータをディープコピーする必要があります。解放の詳細はReleaseStgMedium関数もご覧ください。ディープコピーできると思しきCopyStgMedium関数というものもあるのですが、これは詳細な仕様がよくわからないうえに、実装されているライブラリが他と異なったりして怪しいので今回はあえて避けました。また、GetDataHereの場合には渡されたSTGMEDIUMに直接書き込んでやる必要があり、メモリの再取得などは許されていない点にも注意が必要です。

IDragSourceHelper2インタフェース

IDragSourceHelper2インタフェースを定義します。

IDragSourceHelper2.cs
[ComImport]
[Guid("83E07D0D-0C5F-4163-BF1A-60B274051E40")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IDragSourceHelper2
{
    void InitializeFromBitmap(
        [In][MarshalAs(UnmanagedType.Struct)] ref ShDragImage dragImage,
        [In][MarshalAs(UnmanagedType.Interface)] IComDataObject dataObject);
    void InitializeFromWindow(
        [In] IntPtr hwnd,
        [In] ref Win32Point pt,
        [In][MarshalAs(UnmanagedType.Interface)] IComDataObject dataObject);
    void SetFlags(
        [In][MarshalAs(UnmanagedType.I4)] DSH_FLAGS dwFlags);
}

見ての通りCOMインタフェースです。InitializeFromWindowという気になるメソッドがありますが、まさにこれでドラッグ時のイメージを設定してやります。内部的にはIDataObjectSetDataなどが呼ばれるためあらかじめ実装しておく必要があったわけです。Helperの名に恥じない便利メソッドです。
インタフェースだけでは役に立ちません。実装が必要ですが、次のクラスを宣言してやればOKです。

DragDropHelper.cs
[ComImport]
[Guid("4657278A-411B-11d2-839A-00C04FD918D0")]
public class DragDropHelper { }

中身が空っぽですが構いません。実際の実装はComImportで持ってきます。なので無理やりキャストして使います。

var helper = (IDragSourceHelper2)new DragDropHelper();
helper.InitializeFromBitmap(ref dragImage, dataObject);

IDropTargetHelperインタフェース

IDropTargetHelperインタフェースはドラッグイメージを表示する側で使います。

IDropTargetHelper.cs
[ComVisible(true)]
[ComImport]
[Guid("4657278B-411B-11D2-839A-00C04FD918D0")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IDropTargetHelper
{
    void DragEnter(
        [In] IntPtr hwndTarget,
        [In][MarshalAs(UnmanagedType.Interface)] IComDataObject dataObject,
        [In] ref Win32Point pt,
        [In][MarshalAs(UnmanagedType.I4)] DragDropEffects effect);
    void DragLeave();
    void DragOver(
        [In] ref Win32Point pt,
        [In][MarshalAs(UnmanagedType.I4)] DragDropEffects effect);
    void Drop(
        [In, MarshalAs(UnmanagedType.Interface)] IComDataObject dataObject,
        [In] ref Win32Point pt,
        [In][MarshalAs(UnmanagedType.I4)] DragDropEffects effect);
    void Show(
        [In][MarshalAs(UnmanagedType.Bool)] bool show);
}

各メソッドを呼ぶことでドラッグイメージが表示されます。インタフェース実装は例によってDragDropHelperです。

DragDropHelper.cs
[ComImport]
[Guid("4657278A-411B-11d2-839A-00C04FD918D0")]
public class DragDropHelper { }
var helper = (IDropTargetHelper)new DragDropHelper();
helper.DragEnter(handle, comData, ref dPoint, e.Effects);

ちなみに、このインタフェースはドラッグイメージを受け取るだけの場合でも使えます。エクスプローラーにドロップはしないけど、エクスプローラーからのドロップはされる、という場合でもこのインタフェースを使うことで、アプリケーション側でドラッグイメージを表示することができます。

DROPDESCRIPTION構造体

DROPDESCRIPTION構造体はドラッグイメージと同時に表示するテキストを表します。

DROPDESCRIPION.cs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct DROPDESCRIPTION
{
    [MarshalAs(UnmanagedType.I4)]
    public DropImageType types;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string szMessage;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string? szInsert;
}

この構造体のメモリ表現を"DropDescription"というフォーマットでSetDataしてやることでテキストを表示させる準備ができます。

DragWindowフォーマット

DROPDESCRIPTION構造体だけではテキストを表示させることはできません。Shell Clipboard Formats#DragWindowの説明にある通り、このフォーマットで取得できるHWNDに対してDDWM_UPDATEWINDOWメッセージを送ってやる必要があります。

NativeMethods.PostMessage(hwnd, NativeMethods.DDWM_UPDATEWINDOW, IntPtr.Zero, IntPtr.Zero);

HWNDとかメッセージとか、マネージドコードではあまり意識しないところが出てきましたが、範囲を超えるので説明は割愛します。参考資料だけ載せます。

ソースコード全体

実装に必要なエッセンスを解説しましたが、あとは実装を見たほうが早いと思います。https://github.com/miswil/DropFiles/tree/master/DropMultipleFilesComAsyncDragImage にあります。

所感

WinFormsとWPFの足並みの揃わなさはなんなんでしょう。そりゃ中身が全然違うんだから簡単じゃないのはわかりますが、機能仕様レベルではなるべく同じにしてほしい…
今回の実装で一番大変だったのがCOMIDataObjectの実装ですが、いいリファレンス実装はないものでしょうか。なるべく公式に近いほうで。とにかくドキュメントになってない仕様が多々あって調べるのも容易じゃない。

  1. https://devblogs.microsoft.com/dotnet/winforms-enhancements-in-dotnet-7/

  2. https://github.com/dotnet/winforms/pull/6576

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?