プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

IFileOperationで進行ダイアログ付きでディレクトリ操作を行う

確認環境: Windows 7 SP1 64bit
目的: Explorerと同様の進行ダイアログを表示しつつ、ディレクトリの移動/コピー/リネーム/ゴミ箱に移動/完全に削除、を行いたい

この記事では、.NETからCOM相互運用およびP/Invokeを経由してIFileOperationを扱うサンプルコードを記述しています。
IFileOperation関係の次の記事: IFileOperationで異なる種類の操作を登録した時のUI表示の話
2017/03/29(水): PreserveSigにfalse設定時の戻り値の型を勘違いしていたので修正。[retval]属性のついた引数が戻り値になるのが正解ですが、今までは[out]属性の引数が戻り値になると勘違いしていました。

全体的な注意点

IFileOperation interface のRemarksにあるように、このインターフェースはSTAスレッドでのみ使用可能です。MTAスレッドでは依然として SHFileOperation function を使用する必要があります。

.NETで必要な宣言

へろぱ的ブログ: .NET から IFileOperation を使う にあるように、MSDN Magazine December 2007中にサンプルコードが含まれています。
.chmファイルの目次にて、Columnsフォルダー中の「.NET の問題 Windows Vista での IFileOperation」(ただし筆者の環境では目次中の非ASCII文字が化けていました)を開き、本文の上にある「コードのダウンロード」のリンクからC#のサンプルコードを入手できます。
サンプルコード中には IFileOperation interface, IFileOperationProgressSink interface, IShellItem interface, SHCreateItemFromParsingName function などの宣言が含まれています。
ただし、以下の間違いがあるので注意が必要です。

IFileOperation::SetOwnerWindow の引数がuint型になっている
元々はHWND型なので、.NETではIntPtr型やHandleRef型が正しい。
IFileOperation::NewItem の戻り値がuint型になっている
PreserveSigAttribute が適用されておらずretval属性の引数もないので、void型が正しい。
列挙型のメンバーに、ドキュメントにない値が入っている/ドキュメントにある値が入っていない
FileOperationFlags のメンバーに FOF_MULTIDESTFILES が含まれているが IFileOperation::SetOperationFlags の説明には無い。またWindows7で導入された FOFX_COPYASDOWNLOAD などがメンバーに無い。

サンプルコードに以下の変更を加えれば、より扱いやすくなります:

COMコクラスとしてFileOperationを宣言する
newで生成できます。 COM 相互運用性 - 第 1 部 : C# クライアント チュートリアル (C#) に説明があります。
using System.Runtime.InteropServices;

[ComImport]
[Guid("3ad05575-8857-4850-9277-11b85bdb8e09")]
public class FileOperation { }
`ref GUID`引数を、`[In][MarshalAs(UnmanagedType.LPStruct)] Guid`に変更する
refで渡す必要が無くなり、フィールドをreadonlyにできます。 The confusing UnmanagedType.LPStruct marshaling directive - Adam Nathan's Old Blog - Site Home - MSDN Blogs に説明があります。

使用時の説明

以下のサンプルコードでは、簡単のためCOMオブジェクトの解放処理は一切記述していません。
一応、 RCWはファイナライズで自動的にReleaseするっぽい。検証してみた。 - gounx2の日記 にあるようにRCWのFinalizeでCOMオブジェクトは解放されます。(MSDN Magazine November 2007 の内容が引用されています。)
実際のコードでは、COMオブジェクトを使わなくなる時に Marshal.FinalReleaseComObject MethodMarshal.ReleaseComObject Method を呼び出してください。

同じく簡単のため、ファイルパスからIShellItemを生成する関数を用意します。こちらも同様に、本来は戻り値を解放する必要があります。
SHCreateItemFromParsingNameの宣言時に DllImportAttribute.PreserveSig Field をfalseに設定しているため、戻り値はvoid型になります。

private static readonly Guid ShellItemGuid = typeof(IShellItem).GUID;
public static IShellItem CreateShellItem(string path)
{
    SHCreateItemFromParsingName(path, null, ShellItemGuid, out object value);
    return (IShellItem)value;
}
IFileOperation::PerformOperations method のHRESULT

例に漏れず、正常終了時は0になります。
一方で、次の値になる場合がありました。

「フォルダーの上書き確認」ダイアログが表示されている状態でキャンセルする
COPYENGINE_E_USER_CANCELLED(0x80270000)
移動やコピー進行中にキャンセルする
ERROR_CANCELLED(0x800704C7)
CopyItemなどを一切呼び出さず処理を登録していない状態でPerformOperationsを呼び出す
E_UNEXPECTED(0x8000FFFF)

ただし IFileOperation::GetAnyOperationsAborted method のRemarksに次の記述があるので、PerformOperatonsの戻り値は気にしないほうがいいのかもしれません。

You should call IFileOperation::GetAnyOperationsAborted regardless of whether IFileOperation::PerformOperations returned a success or failure code. A success code can be returned even if the operation was stopped by the user or the system.

親ウィンドウの指定

SetOperationFlagsで非表示にするよう設定していない場合は、PerformOperationsの実行中に進行ダイアログが表示されます。
初期状態では親ウィンドウが設定されていないため、他のウィンドウをアクティブにすると後ろにダイアログが隠れてしまいます。
IFileOperation::SetOwnerWindow method を呼び出すと、指定ウィンドウよりも手前に表示されるようになります。

進行ダイアログがモーダルになる、という訳ではないので注意してください。
進行ダイアログ表示中でも指定ウィンドウは操作可能です。PerformOperationsの前後でコントロールの有効無効を切り替える必要があるかもしれません。

ディレクトリの移動/コピー

IFileOperation::MoveItem methodIFileOperation::CopyItem method を使って処理を登録した後に、PerformOperationsを呼び出します。(移動/コピーにかぎらず)複数の処理を登録できます。
移動する場合のサンプルコードは次のようになります。コピーの場合はCopyItemを使う点だけが異なります。
移動先に同名のディレクトリが存在する場合は、フォルダーの上書き確認ダイアログが表示されます。

var fo = (IFileOperation)new FileOperation();
fo.MoveItem(
    CreateShellItem(@"c:\test\DirA"),
    CreateShellItem(@"c:\test\DirB"),
    null,
    null);
fo.PerformOperations();
before:
c:\test
├dirA
│└test.txt
└dirB

after:
c:\test
└dirB
  └dirA
    └test.txt
ディレクトリのリネーム

IFileOperation::RenameItem method で処理を登録します。
リネーム先と同名のディレクトリが存在する場合は、フォルダーの上書き確認ダイアログが表示されます。

var fo = (IFileOperation)new FileOperation();
fo.RenameItem(
    CreateShellItem(@"c:\test\DirA"),
    "DirC",
    null);
fo.PerformOperations();
before:
c:\test
└dirA
  └test.txt

after:
c:\test
└dirC
  └test.txt
ディレクトリをゴミ箱に移動

IFileOperation::SetOperationFlags でFOF_ALLOWUNDOを設定し(Noteにあるように既定でこのフラグが入っています)、 IFileOperation::DeleteItem method で登録します。
(MSDNの説明を見ても、そのフラグだとは全然分かりませんでした。Win8以降では FOFX_ADDUNDORECORD や FOFX_RECYCLEONDELETE を使えるかもしれませんが、Win7ではSetOperationFlagsがE_INVALIDARG(0x80070057)を返します。
→ 2017/03/29(水)追記: IFileOperationの前身である SHFileOperation function の方では、RemarksにFOF_ALLOWUNDOフラグでゴミ箱へ移動、とありました。)
ゴミ箱のプロパティで「削除の確認メッセージを表示する」がチェックされている場合は、確認ダイアログが表示されます。チェックされていない場合は確認ダイアログ無しでゴミ箱へ送られます。
→ 2018/11/22(木)追記: FOFX_RECYCLEONDELETEやFOFX_ADDUNDORECORDの振る舞いの実験の記事を書きました。

var fo = (IFileOperation)new FileOperation();
fo.SetOperationFlags(FileOperationFlags.AllowUndo);
fo.DeleteItem(
    CreateShellItem(@"c:\test\DirA"),
    null);
fo.PerformOperations();
before:
c:\test
└dirA
  └test.txt

after:
c:\test
ディレクトリを完全に削除

SetOperationFlagsでFOF_ALLOWUNDOを外し、DeleteItemで登録します。
ゴミ箱のプロパティにかかわらず、確認ダイアログが表示されます。

var fo = (IFileOperation)new FileOperation();
fo.SetOperationFlags(0);
fo.DeleteItem(
    CreateShellItem(@"c:\test\DirA"),
    null);
fo.PerformOperations();
before:
c:\test
└dirA
  └test.txt

after:
c:\test