LoginSignup
0

Windowsエクスプローラーに存在しないファイルをドロップする(Part3)

Last updated at Posted at 2024-01-20

はじめに

Windowsエクスプローラーに存在しないファイルをドロップする(Part2)の続きです。

What/How

この課題に対するソリューションは複数あり、やりたいことが複雑になるほど実装も複雑になります。複雑になるほど汎用ソリューションとして使えますが、保守性を考えると適切なレベルで妥協するほうがよいでしょう。
それぞれ内容が全く異なるため記事を分けます。簡単な順に

  1. ファイルは1つだけで、ファイルのプロパティ情報(ファイル名・サイズ等)はすでに分かっており、ファイルのデータもすでに手元にある→Part1
  2. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→Part2
  3. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。定義済み動作のみ許容する。→本記事
  4. ファイルは複数あり、ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる。デスクトップアプリを応答不能にしたくない。→Part4
  5. エクスプローラーにドロップするなら、せっかくだしドラッグ中のイメージとかテキストとかも表示したい。↓こういうの→ドラッグドロップ時にイメージを表示する
    image.png
  6. いっそのことドロップ先のフォルダーがわかれば一番手っ取り早い。→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.DataObjectSystem.Windows.DataObjectではWin32のアンマネージドな領域をうまくラップしてマネージドコードから使いやすくしてくれますが、その副作用で一部のパラメータが捨てられてしまいます。そこで、System.Runtime.InteropServices.ComTypes.IDataObjectインタフェースを直接実装することで生のパラメータを受け取れるようにするのが今回の趣旨です。
とはいえ、本格的にSystem.Runtime.InteropServices.ComTypes.IDataObjectインタフェースを実装することはしません。せっかくSystem.Windows.Forms.DataObjectSystem.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メソッドを呼び出しますが、このメソッドは第二引数で渡されたdataSystem.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として扱います。
image.png
これが、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を直接使ったほうがきれいな気さえします。

  1. コンポーネント オブジェクト モデル (COM)

  2. Windows-Based プログラムでの COM の使用

  3. DragDrop.cs

  4. WPF dataobject.cs

  5. Control.cs

  6. WinForms DataObject.cs

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
0