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

# はじめに
表題を詳しく説明すると、「ドライブ上にまだ存在しないデータ（メモリ上だったり、ネットワーク上だったり）について、デスクトップアプリケーションからWindowsエクスプローラーにドラッグ&ドロップすることで、ドロップ先にデータを書き込んだファイルを作成する」という意味です。
この動作を確認できる身近な例は、Webブラウザーの画像データです。画像データはWebブラウザーにレンダリングされているメモリ上のデータですが、画像をドラッグしてWindowsエクスプローラーにドロップすると画像ファイルが作成されます。その中身のデータは当然元の画像データです。
ドラッグ・ドロップを用いてファイルを操作する場合、たとえば.NETではファイルのパスを指定します。つまり、すでにドライブ上にファイルが存在している必要があります。
```csharp
var data = new DataObject();
data.SetData(DataFormats.FileDrop, "C:\example.txt");
```
しかし、今回のテーマである「存在しないファイル」はそもそもパスがありません。何を指定してやればよいのでしょう。
# 対象プラットフォーム
フレームワークは.NET、そしてデスクトップアプリケーションということでWinForms、WPFを基本にして、プログラミング言語はC#で記述します。しかし最終的には言語やフレームワークに依存しなくなります。
# 環境情報
Windowsエクスプローラーという外部アプリが関係しますが、動作確認は以下バージョンで行っています。
* .NET 8.0.101
* OS
    * エディション	Windows 11 Pro
    * バージョン	22H2
    * OS ビルド	22621.3007
    * エクスペリエンス	Windows Feature Experience Pack 1000.22681.1000.0

# What/How
この課題に対するソリューションは複数あり、やりたいことが複雑になるほど実装も複雑になります。複雑になるほど汎用ソリューションとして使えますが、保守性を考えると適切なレベルで妥協するほうがよいでしょう。
それぞれ内容が全く異なるため記事を分けます。簡単な順に
1. ファイルは1つだけで、ファイルのプロパティ情報（ファイル名・サイズ等）はすでに分かっており、ファイルのデータもすでに手元にある→**本記事**
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→[Part2](https://qiita.com/miswil/items/49025648cc65fc7ae6e0)
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)

# 実装
まずはコードです。WinFormsでもWPFでも同じです。実装だけでいい人はこれでOK。
```csharp
var fileSize = 10L;
var data = new DataObject();
var fileInfo = new MemoryStream();
var fileInfoWriter = new BinaryWriter(fileInfo);
// (1)
fileInfoWriter.Write(1);
// (2)
fileInfoWriter.Write(0x40);
fileInfoWriter.Write(new byte[16 + 4 * 2 + 4 * 2 + 4 + 4 * 2 * 3]);
fileInfoWriter.Write((uint)(fileSize >> 32));
fileInfoWriter.Write((uint)(fileSize & 0xFFFFFFFF));
fileInfoWriter.Write(Encoding.Unicode.GetBytes("example.txt".PadRight(260, '\0')));
fileInfoWriter.Flush();
fileInfo.Position = 0;
data.SetData("FileGroupDescriptorW", fileInfo);
var fileContents = new MemoryStream();
var fileContentsWriter = new StreamWriter(fileContents);
fileContentsWriter.Write("1234567890");
fileContentsWriter.Flush();
fileContents.Position = 0;
data.SetData("FileContents", fileContents);
```
とりあえずこうやって作った`DataObject`を使ってドラッグを開始すればWindowsエクスプローラーにドロップできてファイルが作成されます。
# 解説
オブジェクトのバイナリ表現とか、配列のメモリ配置とか、ポインターとか、C/C++な内容があるためそこは堪えてください。
## データ形式
SetDataの引数として、見慣れないデータ形式を使っています。
|データ形式|解説|
|:-|:-|
|`FileGroupDescriptorW`|ファイルのプロパティ情報を格納する|
|`FileContents`|ファイルの内容を格納する|

これらはWindowsエクスプローラーが`DataObject`に要求するデータ形式[^1]の一部です。この形式に対して適切なデータを用意してやることで、Windowsエクスプローラー側が勝手に読み込んでいいように処理してくれます。
## FileGroupDescriptorW
[シェル クリップボードの形式/CFSTR_FILEDESCRIPTOR](https://learn.microsoft.com/ja-jp/windows/win32/shell/clipboard#cfstr_filedescriptor)の説明をだいぶ端折ると、グローバルメモリオブジェクト上の[`FILEGROUPDESCRIPTOR`構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/shlobj_core/ns-shlobj_core-filegroupdescriptorw)を表すデータ形式です。グローバルメモリ上では、最初の32ビットで配列数を表し、それ以降で[`FILEDESCRIPTOR`構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/shlobj_core/ns-shlobj_core-filedescriptorw)の配列を表します。
コードを見返すと、(1)でファイル数（今回は1）のバイナリ表現を書き込んで、(2)以降に`FILEDESCRIPTOR`構造体のバイナリ表現を1つ書き込んでいます。`FILEDESCRIPTOR`構造体のバイナリ表現は、メンバーが宣言された順番で書き込みます。あとは[コード](#1つのファイルをwindowsエクスプローラーにドロップする)と[FILEDESCRIPTOR構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/shlobj_core/ns-shlobj_core-filedescriptorw)を見比べてみればなにを書き込んでいるかわかると思います。注意が必要な点としては、[`FILETIME`構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/minwinbase/ns-minwinbase-filetime)とファイルサイズを表す部分です。32ビット変数2つを組み合わせて使っており、若干の変換が必要となります。

:::note
ここでクイズです。`MemoryStream`はグローバルメモリオブジェクトでしょうか。正解はNoです。マネージドオブジェクトですもんね。でも心配は不要。[`DataObject`で変換してくれます](https://github.com/dotnet/wpf/blob/v8.0.1/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs#L1784)。
:::
## FileContents
ファイルのデータを表すデータ形式です。[シェル クリップボードの形式/CFSTR_FILECONTENTS](https://learn.microsoft.com/ja-jp/windows/win32/shell/clipboard#cfstr_filecontents)ではいろいろ難しいことを書いていますが、要はファイルのデータを直接`MemoryStream`に書き込んでください。例によって、グローバルメモリへの変換は`DataObject`内で行ってくれます。
# 応用編
## ドラッグ&ドロップ以外のデータ交換
クリップボードでも`IDataObject`を使ってデータを指定できます。こうすれば「貼り付け」操作で新しいファイルを作成できます。
```csharp
Clipboard.SetDataObject(data);
```
## `FILEDESCRIPTOR`構造体のバイナリ変換
一番面倒なのが`FILEDESCRIPTOR`構造体をバイナリ表現に変換する部分です。上記のようにバイナリを直接扱ってもいいんですが、オブジェクト指向じゃない。
そこで登場するのが`Marshal`クラスです。この先何度もお世話になるので今のうちに紹介しておきます。`Marshal`クラスの`StructureToPtr`メソッドを使うことで構造体をまるっとバイナリに変換することができます。詳しくは[型のマーシャリング](https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/type-marshalling)を参照してください。
### 下準備
`FILEDESCRIPTOR`構造体は.NETには存在しないので、構造体を定義します。
```csharp
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct FILEDESCRIPTOR
{
    public uint dwFlags;
    public Guid clsid;
    public Size sizel;
    public Point pointl;
    public uint dwFileAttributes;
    public FILETIME ftCreationTime;
    public FILETIME ftLastAccessTime;
    public FILETIME ftLastWriteTime;
    public uint nFileSizeHigh;
    public uint nFileSizeLow;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string cFileName;
}
```
`StructLayoutAttribute`や`MarshalAsAttribute`がバイナリシリアル化のときの動作に影響します[^4]。メンバー変数の名前は重要ではないですが、宣言順は絶対に変えないようにしましょう。
### `FILEDESCRIPTOR`構造体の構築
`FILEDESCRIPTOR`構造体はメンバー設定に細かいルールがあります。詳細は[FILEDESCRIPTOR構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/shlobj_core/ns-shlobj_core-filedescriptorw)に詳しく記述があります。なんらかのファクトリを作成するのが便利です。
### バイナリ化（マーシャリング）
`Marshal`クラスを使い`FILEDESCRIPTOR`構造体をバイナリ表現に変換します。今回の文脈では特に「マーシャリング」と呼ばれますが、これはマネージドコード（.NETアプリ）とネイティブコード（Windowsエクスプローラー）の間でデータやり取りがある場合のデータ変換を表す用語です。
```csharp
var fileGroupDescriptorBinary = new MemoryStream();
fileGroupDescriptorBinary.Write(BitConverter.GetBytes(1));
var fileDescriptor = // FILEDESCRIPTORの生成
var fdSize = Marshal.SizeOf<FILEDESCRIPTOR>();
var fdPtr = Marshal.AllocHGlobal(fdSize);
try
{
    Marshal.StructureToPtr(fileDescriptor, fdPtr, false);
    var fdArray = new byte[fdSize];
    Marshal.Copy(fdPtr, fdArray, 0, fdSize);
    fileGroupDescriptorBinary.Write(fdArray);
}
finally
{
    Marshal.FreeHGlobal(fdPtr);
}
```
`Marshal.AllocHGlobal`メソッドはグローバルメモリ空間にメモリブロックを確保し、`Marshal.FreeHGlobal`はそれを解放します。グローバルメモリ空間上にいちど変換してからそれをbyte配列にコピーしさらに`MemoryStream`に書き戻すのは非常に面倒ですが、これ以外にいい方法を知っている方いたら教えてください。

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

# 所感
今回の内容はお決まりの解決法で目新しいものはありませんが、やはりここから始める必要があるでしょう。情報源や`DataObject`の内部処理までまとめられたのはよかったと思います。

[^1]:[シェル クリップボードの形式](https://learn.microsoft.com/ja-jp/windows/win32/shell/clipboard)に記載があります。なお、CFSTR_FILEDESCRIPTOR, CFSTR_FILECONTENTSといった定数からどうやって対応する文字列を手に入れるのかというと、先述のリンク先でこれらの定数はShlobj.hで定義されているとあるので、WindowsSDKのインストール先からShlobj.hを探してきてその中から定数定義を見つけます。CFSTR_FILEDESCRIPTORのほうは、CFSTR_FILEDESCRIPTOR"A", CFSTR_FILEDESCRIPTOR"W"という定数のどちらかになっていますが、これらは単に文字コードがANSIがUNICODEかの違いです。ANSIを選ぶ理由はないので[^2]かならずCFSTR_FILEDESCRIPTORWのほうを使います。
なお、リンク先で「CFSTR_XXX 形式識別子を RegisterClipboardFormat に渡します。」といった記述がありますが、これは.NETが内部的に行ってくれるため[^3]不要です。
[^2]:[Win32 と C++ の概要 / 文字列の操作](https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/working-with-strings)
[^3]:[WPF DataFormats.cs](https://github.com/dotnet/wpf/blob/v8.0.1/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/DataFormats.cs#L104), [WinForms DataFormats.cs](https://github.com/dotnet/winforms/blob/v8.0.1/src/System.Windows.Forms/src/System/Windows/Forms/DataFormats.cs#L185)
[^4]:[構造体のマーシャリングをカスタマイズする](https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/customize-struct-marshalling)