LoginSignup
1
1

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

Last updated at Posted at 2024-01-20

はじめに

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

What/How

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

  1. ファイルは1つだけで、ファイルのプロパティ情報(ファイル名・サイズ等)はすでに分かっており、ファイルのデータもすでに手元にある→Part1
  2. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→本記事
  3. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。定義済み動作のみ許容する。→Part3
  4. ファイルは複数あり、ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる。デスクトップアプリを応答不能にしたくない。→Part4
  5. エクスプローラーにドロップするなら、せっかくだしドラッグ中のイメージとかテキストとかも表示したい。↓こういうの→ドラッグドロップ時にイメージを表示する
    image.png
  6. いっそのことドロップ先のフォルダーがわかれば一番手っ取り早い。→Windowsエクスプローラーへファイルドロップした際、ドロップ先のフォルダーを取得する

実装

FileGroupDescriptorW

Part1FILEGROUPDESCRIPTOR構造体について詳しく説明しました。この構造体はファイルのグループを表すため、複数ファイルに拡張することも容易です。最初の32ビットでファイル数を表す値を書き込み、それ以降ファイル数の数だけFILEDESCRIPTOR構造体のバイナリ表現を書き込んでやります。

var fileGroupDescriptorBinary = new MemoryStream();
fileGroupDescriptorBinary.Write(BitConverter.GetBytes(files.Count()));
var fdSize = Marshal.SizeOf<FILEDESCRIPTOR>();
var fdArray = new byte[fdSize];
var fdPtr = Marshal.AllocHGlobal(fdSize);
try
{
    foreach (var file in files)
    {
        var fileDescriptor = new FILEDESCRIPTORFactory().Create(
            file.Name,
            file.CreationTime, file.LastAccessTime, file.LastWriteTime,
            file.Size);
        Marshal.StructureToPtr(fileDescriptor, fdPtr, false);
        Marshal.Copy(fdPtr, fdArray, 0, fdSize);
        fileGroupDescriptorBinary.Write(fdArray);
    }
}
finally
{
    Marshal.FreeHGlobal(fdPtr);
}
fileGroupDescriptorBinary.Position = 0;
dataObject.SetData("FileGroupDescriptorW", fileGroupDescriptorBinary);

FileContents

FileGroupDescriptorは構造が複数ファイルに対応していましたが、FileContentsはMemoryStreamを指定するだけです。FileGroupDescriptorで複数指定したファイルにうち、Windowsエクスプローラーがどのファイルのデータを要求しているのか、どうしたらわかるのでしょう。シェル クリップボードの形式/CFSTR_FILECONTENTSには次のように説明があります。

ターゲットが IDataObject::GetData を呼び出してデータを抽出する場合、FORMATETC 構造体の lindex メンバーをファイルの FILEDESCRIPTOR 構造体の 0 から始まるインデックスに、付随するCFSTR_FILEDESCRIPTOR形式で設定することで、特定のファイルを指定します。

IDataObject::GetDataメソッド呼び出しの際にlindexというデータが渡され、それがFileGroupDescriptorで指定したFILEDESCRIPTOR構造体配列のうち、何番目のファイルのデータを要求しているのかを表しているとのことです。
FORMATETClindexといった見慣れないキーワードが出てきました。これらはWin32でドラッグ&ドロップを扱う際に出てくる用語です。.NETではこれらの値をラップしたり変換したりして扱いやすいマネージドの形式にしてくれています。
じゃあlindexの値をどのようにラップしているかなんですが、実はこの情報は捨てられてしまっています1。捨てられている以上どうしようもないので、推測してファイルのデータを返してやる必要があります。シンプルに「FileGroupDescriptorで指定したFILEDESCRIPTOR構造体配列の順にFileContentsは要求される」としましょう。あとはIDataObject::GetDataメソッドから返すデータを適宜切り替えてやるだけです。
これを実現するには独自のIDataObject実装をしてやる必要があります。複数のMemoryStreamを保持してFileContentsが要求されるごとに返すMemoryStreamを切り替えていきます。実装の一部をピックアップします

MyDataObject.cs
public object GetData(string format, bool autoConvert)
{
    if (format == "FileContents" && 
        this.fileContents is not null &&
        this.fileContents.MoveNext())
    {
        return fileContents.Current;
    }
    return this.dataObject.GetData(format, autoConvert);
}

fileContentsIEnumerator<MemoryStream>です。FileContentsが要求されるごとにMoveNextして返すMemoryStreamを切り替えます。
WinFormsではDataObjectを継承して一部のメソッドをoverrideするだけで済みます。WPFではDataObjectsealedのため、継承して実装をoverrideすることができません。そのためデザインパターンのProxyパターンを使います。

解説

仮定としておいた「FileGroupDescriptorで指定したFILEDESCRIPTOR構造体配列の順にFileContentsは要求される」ですが、これが正しい保証はありません。経験上こうならなかったことはないですが、どのドキュメントにも記載はありません。そのため「未定義動作が起きる可能性を許容する」ことが前提の実装です。定義済み動作のみの実装はPart3で解説しますが、Win32の世界により深く進んでいきます。

応用

ファイルだけでなく、フォルダーを作ったりその中にファイルを格納したりすることもできます。FILEDESCRIPTOR構造体のdwFileAttributesFILE_ATTRIBUTE_DIRECTORY(0x10)2を指定するとフォルダーが作成されます。さらに、そのフォルダーの中に格納するファイル・フォルダーはcFileNameに相対パスを記載してやります。

ソースコード全体

上記のコードそのままではなくさらにブラッシュアップしていますが、https://github.com/miswil/DropFiles/tree/master/DropMultipleFiles にあります。

所感

未定義動作なんて説明するんじゃないよ、というお叱りは受けそうですが、まだかろうじて.NETの範囲で済ませられる妥協点とご理解ください…

  1. WPF dataobject.cs, WinForms DataObject.cs

  2. ファイル属性定数

1
1
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
1