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

# はじめに
[Windowsエクスプローラーに存在しないファイルをドロップする（Part3）](https://qiita.com/miswil/items/9771cb1de32b0713b744)の続きです。
# What/How
この課題に対するソリューションは複数あり、やりたいことが複雑になるほど実装も複雑になります。複雑になるほど汎用ソリューションとして使えますが、保守性を考えると適切なレベルで妥協するほうがよいでしょう。
それぞれ内容が全く異なるため記事を分けます。簡単な順に
1. ファイルは1つだけで、ファイルのプロパティ情報（ファイル名・サイズ等）はすでに分かっており、ファイルのデータもすでに手元にある→[Part1](https://qiita.com/miswil/items/b97dd4914a335fa21790)
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。未定義動作が起きる可能性を許容する。→[Part2](https://qiita.com/miswil/items/49025648cc65fc7ae6e0)
1. ファイルは複数あり、ファイルのプロパティ情報はすでに分かっており、ファイルのデータもすでに手元にある。定義済み動作のみ許容する。→[Part3](https://qiita.com/miswil/items/9771cb1de32b0713b744)
1. ファイルは複数あり、ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる。デスクトップアプリを応答不能にしたくない。→**本記事**
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にしろ、Part1~3のコードはすべて同期的に動作していました。つまり、`DataObject`内の処理はすべてUIスレッドで実行され、その間`DoDragDrop`メソッドは戻らず以降の処理は実行されませんでした。これはつまり、`DataObject`内で時間のかかる処理（ネットワーク上のファイル取得など）を行った場合、UIが固まることを意味します。Part4の前提は「ファイルのプロパティ情報は取得に時間がかかり、ファイルのデータも取得に時間がかかる」ですので、なるべくだったらユーザーがドロップ操作をするまでデータ取得は遅延させたい。しかしドロップしてからデータを取得してはUIが固まる。事前にデータを取得しておくことはできるけど、ユーザーがドラッグ&ドロップをしなければ無駄になってしまうので避けたい。
この悩みを解決してくれるのが[`IDataObjectAsyncCapability`インタフェース](https://learn.microsoft.com/ja-jp/windows/win32/api/shldisp/nn-shldisp-idataobjectasynccapability)です。名前の通り、`DataObject`にAsyncなCapabilityを付与するためのインタフェースです。このインタフェースを実装したときの動作については[シェルデータ転送シナリオの処理-シェルオブジェクトの非同期的なドラッグ アンド ドロップ](https://learn.microsoft.com/ja-jp/windows/win32/shell/datascenarios#dragging-and-dropping-shell-objects-asynchronously)で詳しく説明されています。簡単にいうと、Windowsエクスプローラー側で別スレッドを立ててそこで`DataObject`処理を行い、終わったら通知してくれる仕組みです。

# `IStream`インタフェース
[`IStream`インタフェース](https://learn.microsoft.com/ja-jp/windows/win32/api/objidl/nn-objidl-istream)というインタフェースがあります。これはデータの流れを抽象的に表すいわゆる"Stream"のCOMインタフェースです。Part3まではファイルのデータはすでにメモリに保持しており、それをWindowsエクスプローラーに渡す処理を書いてきましたが、今回はファイルのデータを非同期的に取得します。そのデータをメモリ上に保持してからWindowsエクスプローラーに渡すのはいかにも効率が悪いです。そこで`IStream`インタフェースを通じてデータを渡すことにします。この`IStream`インタフェースは`IDataObject`で渡すことのできるデータ媒体となっており、抽象的なデータを効率よく転送することができます。

# 実装
Part3でフレームワーク依存の内部実装まで突っ込んで疲れたので、Part4ではNativeな実装を心がけます。Win32なコードが本格的に出ますが、ドキュメント通りに実装すればそれほど難しくはありません。また、Win32で書いておけばWinFormsでもWPFでも同じコードを使えます。
## `IDataObjectAsyncCapability`インタフェース
まず[`IDataObjectAsyncCapability`インタフェース](https://learn.microsoft.com/ja-jp/windows/win32/api/shldisp/nn-shldisp-idataobjectasynccapability)をC#で使うためにインタフェースを定義します。
```csharp:IDataObjectAsyncCapability.cs
[ComImport]
[Guid("3D8B0590-F691-11d2-8EA9-006097DF5BD4")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IDataObjectAsyncCapability
{
    void SetAsyncMode([In][MarshalAs(UnmanagedType.Bool)] bool fDoOpAsync);
    void GetAsyncMode([Out][MarshalAs(UnmanagedType.Bool)] out bool pfIsOpAsync);
    void StartOperation([In] IBindCtx pbcReserved);
    void InOperation([Out][MarshalAs(UnmanagedType.Bool)] out bool pfInAsyncOp);
    void EndOperation([In] int hResult, [In] IBindCtx pbcReserved, [In][MarshalAs(UnmanagedType.U4)] DragDropEffects dwEffects);
}
```
普段マネージドコードに慣れ親しんでいるひとには見慣れないAttributeがたくさんあると思いますが、これらの解説は範囲を超えるので割愛します。ネイティブコードと共有できるインタフェースを宣言し、その間のデータ変換を定義してる程度の理解で十分です。
実装はすごく簡単。渡された値を保持するだけです。このインタフェースは実装していること自体が重要で、その中身はあまり重要ではありません。
```csharp:MyDataObject.cs
void IDataObjectAsyncCapability.SetAsyncMode(bool fDoOpAsync)
{
    this.isAsync = fDoOpAsync;
}
void IDataObjectAsyncCapability.GetAsyncMode(out bool pfIsOpAsync)
{
    pfIsOpAsync = this.isAsync;
}
void IDataObjectAsyncCapability.StartOperation(IBindCtx pbcReserved)
{
    this.isInAsyncOperation = true;
}
void IDataObjectAsyncCapability.InOperation([Out][MarshalAs(UnmanagedType.Bool)] out bool pfInAsyncOp)
{
    pfInAsyncOp = this.isInAsyncOperation;
}
void IDataObjectAsyncCapability.EndOperation(int hResult, IBindCtx pbcReserved, DragDropEffects dwEffects)
{
    this.isInAsyncOperation = false;
}
```
## `IDataObject`インタフェース
[Part3](https://qiita.com/miswil/items/9771cb1de32b0713b744)でも出てきた[`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェース](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.comtypes.idataobject?view=net-8.0&viewFallbackFrom=windowsdesktop-8.0)の`GetData`メソッド実装を示します。
```csharp:MyDataObject.cs
public void GetData(ref FORMATETC format, out STGMEDIUM medium)
{
    medium = new STGMEDIUM();
    var query = this.QueryGetData(ref format);
    if (query != NativeMethods.S_OK)
    {
        Marshal.ThrowExceptionForHR(query);
        return;
    }
    if (format.cfFormat == FileGroupDescriptorId)
    {
        if (this.isAsync)
        {
            if (this.isInAsyncOperation)
            {
                // (1)
                medium.unionmember =
                    this.AllocFileGroupDescriptorToHGlobalAsync(IntPtr.Zero).Result;
                medium.tymed = TYMED.TYMED_HGLOBAL;
            }
            else
            {
                // (2)
                medium.unionmember = IntPtr.Zero;
                medium.tymed = TYMED.TYMED_NULL;
            }
        }
        else
        {
            // (3)
            medium.unionmember =
                this.AllocFileGroupDescriptorToHGlobalAsync(IntPtr.Zero).Result;
            medium.tymed = TYMED.TYMED_HGLOBAL;
        }
    }
    else if (format.cfFormat == FileContentsId &&
        this.fcFetches![format.lindex] is not null)
    {
        var fcFetch = this.fcFetches![format.lindex];
        var stream = fcFetch!().Result;
        var comStream = new ReadStream(stream);
        this.fetchedStreams.Add(comStream);
        medium.unionmember = Marshal.GetIUnknownForObject(comStream);
        medium.tymed = TYMED.TYMED_ISTREAM;
    }
}
```
### FileGroupDescriptor要求時
(1) 非同期操作かつドロップされた
この場合には別スレッドで呼ばれているため`Task.Result`でスレッドを停止してもUIは固まりません。
(2) 非同期操作かつドロップされてない
ドロップ前にフライングでFileGroupDescriptorを取得されることがあります。ここでスレッドを停止させるとUIが固まるため`Task`を待機するわけにはいきません。この場合素直にNULLをデータとして返します。最終的に使われるファイル情報は(1)呼び出しで取得されるため、ここではNULLを返してかまいません。
(3) 同期操作
同期操作の場合にはUIが固まるのもやむなしです。おとなしく`Task`を待機します。
### FileContents要求時
[Part3](https://qiita.com/miswil/items/9771cb1de32b0713b744)と同じようにlindexを使ってファイルを区別します。この時点では既に別スレッドで動いているため`Task`を待機します。その後得られた`Stream`を`IStream`実装でラップし、`Marshal.GetIUnknownForObject`メソッドでポインタにして呼び出し元に返します。ここで得られるポインタはマネージドオブジェクトそのもののポインタではなく、Com Callable Wrapper[^1]と呼ばれるオブジェクトのポインタになりますが、深く気にする必要はありません。
## `IStream`インタフェース
[`IStream`インタフェース](https://learn.microsoft.com/ja-jp/windows/win32/api/objidl/nn-objidl-istream)は`System.Runtime.InteropServices.ComTypes.IStream`に既に宣言されているため改めて定義する必要はありません。今回は必要最小限のメソッドだけ実装しています。
```csharp:ReadStream.cs
internal sealed class ReadStream : IStream, IDisposable
{
    private readonly Stream proxiedStream;
    public ReadStream(Stream stream)
    {
        this.proxiedStream = stream;
    }
    void IStream.Read(byte[] buffer, int bufferSize, IntPtr bytesReadPtr)
    {
        int bytesRead = proxiedStream.Read(buffer, 0, (int)bufferSize);
        if (bytesReadPtr != IntPtr.Zero)
        {
            Marshal.WriteInt32(bytesReadPtr, bytesRead);
        }
    }
    void IStream.Stat(out STATSTG streamStats, int grfStatFlag)
    {
        streamStats = new STATSTG
        {
            type = (int)STGTY.STGTY_STREAM,
            grfMode = 0
        };
        try
        {
            streamStats.cbSize = proxiedStream.Length;
        }
        catch (NotSupportedException) { }
        switch (proxiedStream.CanRead, proxiedStream.CanWrite)
        {
            case (true, true):
                streamStats.grfMode |= (int)StgmConstants.STGM_READWRITE;
                break;
            case (true, false):
                streamStats.grfMode |= (int)StgmConstants.STGM_READ;
                break;
            case (false, true):
                streamStats.grfMode |= (int)StgmConstants.STGM_WRITE;
                break;
            default:
                throw new IOException();
        }
    }
    // 以下略
```
# ドラッグの開始
`System.Runtime.InteropServices.ComTypes.IDataObject`インタフェースを使ってドラッグを開始するにはWin32のAPIを呼び出さなくてはなりません。下記宣言でWin32 APIの[`DoDragDrop`](https://learn.microsoft.com/ja-jp/windows/win32/api/ole2/nf-ole2-dodragdrop)をマネージドコードから使えるようにします。
```csharp:NativeMethods.cs
[DllImport("ole32.dll")]
public static extern int DoDragDrop(
    IComDataObject pDataObject,
    IDropSource pDropSource,
    [MarshalAs(UnmanagedType.I4)] DragDropEffects dwOKEffect,
    [Out][MarshalAs(UnmanagedType.I4)] out DragDropEffects pdwEffect);
```
# 言語およびフレームワーク
今回はC#と.NETで実装しましたが、COMは言語やフレームワークを問いません。C, C++, Visual Basic, Delphi, Pythonなどでも同じように実装できます。
# ソースコード全体
上記のコードそのままではなくさらにブラッシュアップしていますが、https://github.com/miswil/DropFiles/tree/master/DropMultipleFilesComAsync にあります。
# 所感
この記事を書きたくてPart1から段階を踏んでいました。`IDataObjectAsyncCapability`インタフェースと`IStream`インタフェースまで使ってマネージドコードでドラッグ&ドロップをしている技術情報は少なく、ネイティブコードやCOMという技術も相まって、ここまでの情報をまとめるには結構苦労しました。いま改めて調べればある程度の情報は出てきますが、キーワードにたどり着くまでが難しいものです。

[^1]:[COM 呼び出し可能ラッパー](https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/com-callable-wrapper)