はじめに
Windowsエクスプローラーに存在しないファイルをドロップする(Part2)の続きです。
What/How
この課題に対するソリューションは複数あり、やりたいことが複雑になるほど実装も複雑になります。複雑になるほど汎用ソリューションとして使えますが、保守性を考えると適切なレベルで妥協するほうがよいでしょう。
それぞれ内容が全く異なるため記事を分けます。簡単な順に
- ファイルは1つだけで、ファイルのプロパティ情報(ファイル名・サイズ等)はすでに分かっており、ファイルのデータもすでに手元にある→Part1
- ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→Part2
- ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。定義済み動作のみ許容する。→本記事
- ファイルは複数あり、ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる。デスクトップアプリを応答不能にしたくない。→Part4
- エクスプローラーにドロップするなら、せっかくだしドラッグ中のイメージとかテキストとかも表示したい。↓こういうの→ドラッグドロップ時にイメージを表示する
- いっそのことドロップ先のフォルダーがわかれば一番手っ取り早い。→Windowsエクスプローラーへファイルドロップした際、ドロップ先のフォルダーを取得する
実装
Part2では未定義動作になる理由として、.NETがWin32 APIのパラメータを捨ててしまうためと説明しました。そこで今回はWin32 API対応コードを書いてパラメータを直接受け取れるようにします。
System.Runtime.InteropServices.ComTypes.IDataObject
System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースはWin32 APIにおけるデータ交換(ドラッグ&ドロップ、クリップボードコピー)操作に使われます。より詳しく説明すれば"COMインタフェース"と呼ばれるもので、プロセス間でデータを共有できているのはCOMの仕組みのおかげです12。WinFormsで使われるSystem.Windows.Forms.DataObject
やWPFのSystem.Windows.DataObject
はいずれもこのインタフェースを実装しているので、別プロセスで実行環境も異なるWindowsエクスプローラーとデータをやり取りできるようになっています。System.Windows.Forms.DataObject
やSystem.Windows.DataObject
ではWin32のアンマネージドな領域をうまくラップしてマネージドコードから使いやすくしてくれますが、その副作用で一部のパラメータが捨てられてしまいます。そこで、System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースを直接実装することで生のパラメータを受け取れるようにするのが今回の趣旨です。
とはいえ、本格的にSystem.Runtime.InteropServices.ComTypes.IDataObject
インタフェースを実装することはしません。せっかくSystem.Windows.Forms.DataObject
やSystem.Windows.DataObject
で既に実装されているのですから、ほとんどそれを再利用します。デザインパターンにおけるProxyです。
class MyDataObject : System.Runtime.InteropServices.ComTypes.IDataObject
{
private DataObject dataObject;
Part2ではWinFormsのほうは継承で新しいIDataObject
実装を作ることができましたが、今回はそれはできずWPFと同じようにProxyパターンで実装する必要があります。System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースを実装しているメソッドがvirtualではなくoverrideできないためです。
ここでWPFでのみ注意が必要な点として、新しく実装するクラスはSystem.Windows.IDataObject
インタフェースを実装してはいけません。System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースのみ実装してください。
// NG!!!
class MyDataObject : System.Windows.IDataObject, System.Runtime.InteropServices.ComTypes.IDataObject
{
private DataObject dataObject;
これは、両方のインタフェースが実装されている場合には、System.Windows.IDataObject
インタフェースの実装を優先して呼び出すという.NETの仕様によるものです。ドキュメントなどに明記がないものではありますが、実際のDataObject
がそう動作しています。今後この動作はたぶん変わらないでしょう(変わると互換性が著しく失われるので)。
GetData
メソッドとGetDataHere
メソッド
System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースのデータ取得メソッドを実装します。ここでFileContentsを取得しようとしているときには、どのファイルかを示すlindex
パラメータを受け取れるようになります。
void IComDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium)
{
if (DataFormats.GetDataFormat(format.cfFormat).Name == "FileContents" &&
this.fileContents is not null)
{
this.dataObject.SetData("FileContents", this.fileContents[format.lindex]);
}
((IComDataObject)this.dataObject).GetData(ref format, out medium);
}
void IComDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium)
{
if (DataFormats.GetDataFormat(format.cfFormat).Name == "FileContents" &&
this.fileContents is not null)
{
this.dataObject.SetData("FileContents", this.fileContents[format.lindex]);
}
((IComDataObject)this.dataObject).GetDataHere(ref format, ref medium);
}
lindex
の値によって、FileContentsに設定するMemoryStream
を切り替えてやります。あとは元のDataObject
実装に丸投げしてやるだけです。
ドラッグ&ドロップの開始
System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースを実装したクラスが出来上がったら、そのオブジェクトを直接使ってドラッグ&ドロップを開始します。
var myData = new MyDataObject();
DragDrop.DoDragDrop((Border)sender, myData, DragDropEffects.Copy);
解説
ソースの説明は適宜していたので、さらに詳細な実装の話をします。System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースの扱いはWPF, WinFormsそれぞれに独特で、詳細を知らないと期待通りに動作しない場合があります。独自実装などを加える際にはどのインタフェースを実装しどうやって使うのかをよく注意してください。
WPFの場合
WPFでドラッグ&ドロップを開始するときにはDragDrop.DoDragDrop(DependencyObject dragSource, object data, DragDropEffects allowedEffects)
staticメソッドを呼び出しますが、このメソッドは第二引数で渡されたdata
がSystem.Windows.DataObject
ではない場合に、System.Windows.DataObject
でラップする動作をします3。
var data = new DataObject(myData);
System.Windows.DataObject
はコンストラクタ第一引数で渡された引数がSystem.Windows.IDataObject
インタフェースを実装する場合には、そのインタフェース経由でSystem.Windows.IDataObject
インタフェースメソッド、およびSystem.Runtime.InteropServices.ComTypes.IDataObject
インタフェースメソッドへの呼び出しをディスパッチします。あるいは、System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースを実装している場合にはそのインタフェース経由でSystem.Windows.IDataObject
インタフェースメソッド、およびSystem.Runtime.InteropServices.ComTypes.IDataObject
インタフェースメソッドへの呼び出しをディスパッチします4。インタフェースが一致しない部分はデータ変換を行い、また一部のメソッドはNotImplementedとして扱います。
これが、System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースとSystem.Windows.IDataObject
インタフェースを同時に実装してはいけない理由です。System.Windows.IDataObject
インタフェースを実装している場合、そのインタフェース経由で処理がディスパッチされるため、System.Runtime.InteropServices.ComTypes.IDataObject
インタフェース実装は使われなくなってしまいます。
WinFormsの場合
WinFormsでは逆の動きになります。ドラッグ&ドロップを開始するときに呼び出すControl::DoDragDrop
メソッドでは、System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースはそのまま使われます5。そのためWinFormsではSystem.Runtime.InteropServices.ComTypes.IDataObject
インタフェースとSystem.Windows.Forms.IDataObject
インタフェースを同時に実装したオブジェクトでControl::DoDragDrop
メソッドを呼び出した場合、System.Runtime.InteropServices.ComTypes.IDataObject
インタフェースメソッドのみ使われSystem.Windows.Forms.IDataObject
インタフェースの実装は生かされません。
一方、System.Windows.Forms.DataObject
のコンストラクタに引数を渡した場合の動きはWPFと同様になります。各インタフェースメソッドから内部オブジェクトに処理をディスパッチします6。
ソースコード全体
上記のコードそのままではなくさらにブラッシュアップしていますが、https://github.com/miswil/DropFiles/tree/master/DropMultipleFilesCom にあります。
所感
だんだん怪しいにおいがし始めてきました。フレームワークの内部実装まで気にし始めるくらいなら、素直にWin32を直接使ったほうがきれいな気さえします。