はじめに
Windowsエクスプローラーに存在しないファイルをドロップする(Part4)の続きですが、この記事単体でも読めます。Windowsエクスプローラーではドラッグ中に↓こういうイメージが表示されますが、それを自前のデスクトップアプリケーションでも表示する方法です。
実装
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では渡したいデータだけをGetData
かGetDataHere
で渡すように実装していましたが、ドラッグイメージを表示するにはSetData
で動的に受け取ってGetData
で返せるようにする必要があります。
SetData
メソッドはそれほど難しくはありません。受け取ったデータを、重複チェックののち内部に保持するだけです。実装するうえで注意が必要なのはGetData
およびGetDataHere
の実装です。各メソッドの説明を見てもらえればわかる通り、GetData
のout
変数またはGetDataHere
のref
変数で返したSTGMEDIUM
は受け取った側で解放してしまいます。そのため返すSTGMEDIUM
はSetData
で受け取ったデータをディープコピーする必要があります。解放の詳細はReleaseStgMedium
関数もご覧ください。ディープコピーできると思しきCopyStgMedium
関数というものもあるのですが、これは詳細な仕様がよくわからないうえに、実装されているライブラリが他と異なったりして怪しいので今回はあえて避けました。また、GetDataHere
の場合には渡されたSTGMEDIUM
に直接書き込んでやる必要があり、メモリの再取得などは許されていない点にも注意が必要です。
IDragSourceHelper2
インタフェース
IDragSourceHelper2
インタフェースを定義します。
[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
という気になるメソッドがありますが、まさにこれでドラッグ時のイメージを設定してやります。内部的にはIDataObject
のSetData
などが呼ばれるためあらかじめ実装しておく必要があったわけです。Helperの名に恥じない便利メソッドです。
インタフェースだけでは役に立ちません。実装が必要ですが、次のクラスを宣言してやればOKです。
[ComImport]
[Guid("4657278A-411B-11d2-839A-00C04FD918D0")]
public class DragDropHelper { }
中身が空っぽですが構いません。実際の実装はComImport
で持ってきます。なので無理やりキャストして使います。
var helper = (IDragSourceHelper2)new DragDropHelper();
helper.InitializeFromBitmap(ref dragImage, dataObject);
IDropTargetHelper
インタフェース
IDropTargetHelper
インタフェースはドラッグイメージを表示する側で使います。
[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です。
[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
構造体はドラッグイメージと同時に表示するテキストを表します。
[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
の実装ですが、いいリファレンス実装はないものでしょうか。なるべく公式に近いほうで。とにかくドキュメントになってない仕様が多々あって調べるのも容易じゃない。