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

# はじめに
[Windowsエクスプローラーに存在しないファイルをドロップする（Part1）](https://qiita.com/miswil/items/b97dd4914a335fa21790)の続きです。
# What/How
この課題に対するソリューションは複数あり、やりたいことが複雑になるほど実装も複雑になります。複雑になるほど汎用ソリューションとして使えますが、保守性を考えると適切なレベルで妥協するほうがよいでしょう。
それぞれ内容が全く異なるため記事を分けます。簡単な順に
1. ファイルは1つだけで、ファイルのプロパティ情報（ファイル名・サイズ等）はすでに分かっており、ファイルのデータもすでに手元にある→[Part1](https://qiita.com/miswil/items/b97dd4914a335fa21790)
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→**本記事**
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。定義済み動作のみ許容する。→[Part3](https://qiita.com/miswil/items/9771cb1de32b0713b744)
1. ファイルは複数あり、ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる。デスクトップアプリを応答不能にしたくない。→[Part4](https://qiita.com/miswil/items/39dfdffde6df0d2a384c)
1. エクスプローラーにドロップするなら、せっかくだしドラッグ中のイメージとかテキストとかも表示したい。↓こういうの→[ドラッグドロップ時にイメージを表示する](https://qiita.com/miswil/items/d635469e8df023b58026)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247638/3531c1ff-3d22-29d1-a778-f02a0c5784ad.png)
1. いっそのことドロップ先のフォルダーがわかれば一番手っ取り早い。→[Windowsエクスプローラーへファイルドロップした際、ドロップ先のフォルダーを取得する](https://qiita.com/miswil/items/028da61ec3d02775b38e)

# 実装
## FileGroupDescriptorW
[Part1](https://qiita.com/miswil/items/b97dd4914a335fa21790)で[`FILEGROUPDESCRIPTOR`構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/shlobj_core/ns-shlobj_core-filegroupdescriptorw)について詳しく説明しました。この構造体はファイルのグループを表すため、複数ファイルに拡張することも容易です。最初の32ビットでファイル数を表す値を書き込み、それ以降ファイル数の数だけ`FILEDESCRIPTOR`構造体のバイナリ表現を書き込んでやります。
```csharp
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](https://learn.microsoft.com/ja-jp/windows/win32/shell/clipboard#cfstr_filecontents)には次のように説明があります。
>ターゲットが IDataObject::GetData を呼び出してデータを抽出する場合、FORMATETC 構造体の lindex メンバーをファイルの FILEDESCRIPTOR 構造体の 0 から始まるインデックスに、付随するCFSTR_FILEDESCRIPTOR形式で設定することで、特定のファイルを指定します。

`IDataObject::GetData`メソッド呼び出しの際に`lindex`というデータが渡され、それがFileGroupDescriptorで指定した`FILEDESCRIPTOR`構造体配列のうち、何番目のファイルのデータを要求しているのかを表しているとのことです。
`FORMATETC`や`lindex`といった見慣れないキーワードが出てきました。これらはWin32でドラッグ&ドロップを扱う際に出てくる用語です。.NETではこれらの値をラップしたり変換したりして扱いやすいマネージドの形式にしてくれています。
じゃあ`lindex`の値をどのようにラップしているかなんですが、実はこの情報は捨てられてしまっています[^1]。捨てられている以上どうしようもないので、推測してファイルのデータを返してやる必要があります。シンプルに「FileGroupDescriptorで指定した`FILEDESCRIPTOR`構造体配列の順にFileContentsは要求される」としましょう。あとは`IDataObject::GetData`メソッドから返すデータを適宜切り替えてやるだけです。
これを実現するには独自の`IDataObject`実装をしてやる必要があります。複数の`MemoryStream`を保持してFileContentsが要求されるごとに返す`MemoryStream`を切り替えていきます。実装の一部をピックアップします
```csharp: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);
}
```
`fileContents`は`IEnumerator<MemoryStream>`です。FileContentsが要求されるごとに`MoveNext`して返す`MemoryStream`を切り替えます。
WinFormsでは`DataObject`を継承して一部のメソッドをoverrideするだけで済みます。WPFでは`DataObject`が`sealed`のため、継承して実装をoverrideすることができません。そのためデザインパターンのProxyパターンを使います。

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

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

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

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

[^1]:[WPF dataobject.cs](https://github.com/dotnet/wpf/blob/v8.0.1/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs#L1512), [WinForms DataObject.cs](https://github.com/dotnet/winforms/blob/v8.0.1/src/System.Windows.Forms/src/System/Windows/Forms/DataObject.cs#L399)
[^2]:[ファイル属性定数](https://learn.microsoft.com/ja-jp/windows/win32/fileio/file-attribute-constants)