SHFileOperation関数

便利なファイル操作関数

ファイル操作2ディレクトリ操作の項では単純なファイルやディテクトリの操作方法を説明しましたが、SHFileOperation関数を使用することでもファイルやディレクトリのコピー、移動、名前変更、削除を行うことができます。
この関数は、ディレクトリの丸ごとコピーや削除ができます。

int SHFileOperationW(
 LPSHFILEOPSTRUCTW lpFileOp
);
SHFILEOPSTRUCT構造体lpFileOpに基づいてファイルやディレクトリのコピー、移動、名前変更、削除を行う。
成功した場合は0以外を、失敗した場合は0を返す。

この関数はSHFILEOPSTRUCT構造体のメンバにセットした値を使用してファイルやディレクトリを操作します。

typedef struct _SHFILEOPSTRUCTW {
 HWND hwnd;
 UINT wFunc;
 PCZZWSTR pFrom;
 PCZZWSTR pTo;
 FILEOP_FLAGS fFlags;
 BOOL fAnyOperationsAborted;
 LPVOID hNameMappings;
 PCWSTR lpszProgressTitle;
} SHFILEOPSTRUCTW, *LPSHFILEOPSTRUCTW;
SHFileOperation関数が行うファイル操作情報を格納する構造体。

hwndメンバは関数がダイアログボックスを表示するとき、親となるウィンドウのハンドルです。

wFuncメンバは実行する操作を以下の定数から指定します。

定数 説明
FO_COPY コピー
FO_DELETE 削除
FO_MOVE 移動
FO_RENAME 名前変更
複数のファイル名を同時に変更することはできない
(代わりにFO_MOVEを使用する)

pFromメンバは操作する対象ファイルのフルパス(絶対パス、完全修飾パス)です。
つまりドライブ名から指定する必要があります。
ファイル名にはワイルドカードが使用できます。
(「ファイル名」のみ。パス文字列のディレクトリを示す箇所には使用できない)
(ワイルドカードについてはファイルとフォルダの検索#ワイルドカードを参照)
パスの終わりにはNULL文字(\0)を付ける必要があり、NULL文字で区切ることで複数のパスを指定できます。
(例えば「"C:\\abc\0C:\\def\0"」など)

ダブルNULL終端文字列

pFromメンバにはダブルNULL終端文字列という特殊な文字列を使用します。
(double-null-terminated string)
これはひとつの文字列内に複数の項目を含ませるためのものです。

C言語の文字列はNULL文字(\0)を文字列の終端として使用します。
ダブルNULL終端文字列はNULL文字を各項目の区切り文字として使用します。
文字列自体の終端は連続したNULL文字(\0\0)で表わします。
(ダブルNULL)


//「aaa」と「bbb」を含む文字列
WCHAR txt[] = L"aaa\0bbb\0";

//txt[0] is 'a'
//txt[1] is 'a'
//txt[2] is 'a'
//txt[3] is '\0'
//txt[4] is 'b'
//txt[5] is 'b'
//txt[6] is 'b'
//txt[7] is '\0'
//txt[8] is '\0' ←自動的に付加される
//txt[9] is unknown

C言語の文字列リテラルは終端にNULL文字が自動的に付加されるので、ソース上に記述する文字列の終端にはNULL文字をひとつ記述するだけで自動的にダブルNULLになります。

ダブルNULL終端文字列であることを明確にするために、コード上にもダブルNULLを書く場合もあります。
プログラム上は1文字分の無駄になりますが大したコストではありません。


//ダブルNULL終端文字列であることを明示するために
//あえてNULL文字を二つ記述しておく
WCHAR txt[] = L"aaa\0bbb\0\0";

//内部的には終端にはNULL文字が3つ連続する
//"aaa\0bbb\0\0\0"

当然ですが、この文字列は普通の文字列として使用すると最初の項目のみが有効な文字列として認識されます。
また、C言語(C++)以外の言語ではNULL終端文字列ではないことがほとんどで、文字列末尾に自動的にNULL文字を付ける処理などはないので、コード上でもダブルNULLの記述が必要です。

pToメンバはファイルの宛先です。
コピー(FO_COPY)と移動(FO_MOVE)の操作で使用します。
削除(FO_DELETE)と名前変更(FO_RENAME)の操作時はNULLを指定します。
文字列の仕様はpFromメンバと基本的に同じです。
(フルパスで指定、NULL文字で複数パスを指定可能、ダブルNULLで終了しなければならない)
ただしワイルドカードは指定できません。
複数のパスを指定する場合はfFlagsメンバにFOF_MULTIDESTFILESフラグを含める必要があります。

wFuncFO_RENAMEを指定して名前を変更する時、複数のファイル名を指定して名前を変更することはできません。
代わりにFO_MOVEを指定することで名前を複数同時に変更できます。

fFlagsメンバは動作を制御するフラグです。
以下の定数の組み合わせを指定します。

定数 説明
FOF_MULTIDESTFILES pToメンバに複数の宛先パスを指定可能にする
(pFromメンバにも同数のパスを指定する)
FOF_SILENT 進捗状況ダイアログボックスを表示しない
FOF_RENAMEONCOLLISION 宛先に同じファイル名が存在する場合に自動で名前を変更する
FOF_NOCONFIRMATION 選択肢のあるダイアログにすべて「はい」で応答する
(ダイアログを表示しない)
FOF_WANTMAPPINGHANDLE FOF_RENAMEONCOLLISIONフラグによりファイルの名前が変更されたとき、新旧の名前マッピングオブジェクトをhNameMappingsメンバに割り当てる
このメンバは不要になったらSHFreeNameMappings関数を使用して解放する必要がある
FOF_ALLOWUNDO 可能であればアンドゥ(操作の取り消し)を許可する
削除操作の場合、ファイルはゴミ箱に送られる
(このフラグを指定しなければゴミ箱に送られず完全削除される)
操作対象をフルパスで指定しなければこのフラグは無効になる
FOF_FILESONLY ワイルドカードファイル名を指定した場合、操作はファイルのみに対して行う
(フォルダは操作できない)
FOF_SIMPLEPROGRESS 進捗状況ダイアログを、個々のファイル名を表示しないシンプルなものにする
FOF_NOCONFIRMMKDIR ディレクトリの作成が必要な場合、ユーザーに問い合わせず自動で作成する
(ダイアログを表示しない)
FOF_NOERRORUI エラー発生時にユーザーにメッセージダイアログを表示しない
FOF_NOCOPYSECURITYATTRIBS ファイルのセキュリティ属性をコピーしない
(宛先フォルダのセキュリティ属性を使用する)
FOF_NORECURSION サブディレクトリを操作対象にしない
FOF_NO_CONNECTED_ELEMENTS 接続されたファイルを操作対象にせず、明示的に指定したファイルのみを操作対象にする
(htmlファイルと画像ファイルなど)
FOF_WANTNUKEWARNING 削除操作時、ファイルをゴミ箱に送らずに完全削除する場合に警告を表示する。
このフラグはFOF_NOCONFIRMATIONフラグの動作を一部上書きする
FOF_NO_UI ユーザーにあらゆる画面表示を見せない
(FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR)

fAnyOperationsAbortedメンバは、操作が完了する前にユーザーにより中止された場合はTRUE(1)が格納されます。
それ以外の場合はFALSE(0)が格納されます。

hNameMappingsメンバは、fFlagsメンバにFOF_WANTMAPPINGHANDLEフラグが指定されている場合に、新旧のファイル名を格納した構造体のハンドルが格納されます。
(後述)

lpszProgressTitleメンバは、fFlagsメンバにFOF_SIMPLEPROGRESSフラグが指定されている場合に、進捗状況ダイアログのタイトルバーに表示する文字列を指定します。
このフラグがセットされていない場合は使用されません。

SHFileOperation関数の戻り値は、関数が成功した場合に0以外を返します。
これはあくまでも「関数が成功したか否か」であって、操作が実際に行われたかどうかではありません。
ダイアログが表示され、ユーザーがキャンセルした場合も関数は正常終了しています。
目的の操作が完了したか否かはfAnyOperationsAbortedメンバにセットされた値を調べます。

名前マッピングオブジェクト

fFlagsメンバにFOF_RENAMEONCOLLISIONフラグを含めると、宛先に同じ名前のファイルが既に存在する場合に自動的に名前を変更した上でコピー等が行われます。
このときFOF_WANTMAPPINGHANDLEフラグも同時に含めると、hNameMappingsメンバに名前マッピングオブジェクトアドレスが格納されます。
これは変更される前のファイル名と変更した後のファイル名を保存しています。

struct HANDLETOMAPPINGS {
 UINT uNumberOfMappings;
 LPSHNAMEMAPPING lpSHNameMapping;
};
複数の名前マッピングオブジェクトを格納する構造体。

この構造体はライブラリに定義されていません。
自分で定義して使用する必要があります。
uNumberOfMappingsメンバは次のlpSHNameMappingメンバの配列の要素数が格納されます。
lpSHNameMappingメンバはSHNAMEMAPPING構造体の配列です。

typedef struct _SHNAMEMAPPINGW {
 LPWSTR pszOldPath;
 LPWSTR pszNewPath;
 int cchOldPath;
 int cchNewPath;
} SHNAMEMAPPINGW, *LPSHNAMEMAPPINGW;
名前マッピングオブジェクトを格納する構造体。

pszOldPathメンバは変更前の古いファイル名です。
pszNewPathメンバは変更後の新しいファイル名です。
cchOldPathメンバは古いファイル名の文字数です。
cchNewPathメンバは新しいファイル名の文字数です。

名前マッピングオブジェクトを使用する場合、最後にhNameMappingsメンバにSHFreeNameMappingsを使用してメモリを解放する必要があります。

void SHFreeNameMappings(
 HANDLE hNameMappings
);
名前マッピングオブジェクトhNameMappingsを解放する。

これらを利用して、名前の変更が行われた場合にログを残したり別の動作をさせることができます。


#include <windows.h>
#include <strsafe.h>

//HANDLETOMAPPINGS構造体を自分で定義しておく
typedef struct {
	UINT uNumberOfMappings;
	LPSHNAMEMAPPING lpSHNameMapping;
} HANDLETOMAPPINGS;

#define BUFFERSIZE (MAX_PATH * 2 + 16)

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	/*
		ディスク上にはあらかじめ		
		「C:\1\a.txt」
		「C:\1\b.txt」
		「C:\1\c.txt」
		「C:\2\a.txt」
		が存在するとする
	*/

	WCHAR buf[BUFFERSIZE];
	SHFILEOPSTRUCT fo;

	//ゼロクリア
	fo = (SHFILEOPSTRUCT){ 0 };

	//コピー操作
	fo.wFunc = FO_COPY;

	//送信元
	fo.pFrom =
		L"C:\\1\\a.txt\0"
		L"C:\\1\\b.txt\0"
		L"C:\\1\\c.txt\0";

	//宛先
	fo.pTo =
		L"C:\\2\\a.txt\0"
		L"C:\\2\\b.txt\0"
		L"C:\\2\\c.txt\0";

	//複数の宛先
	//衝突ファイル名を自動的にリネーム
	//名前マッピングオブジェクトの使用
	fo.fFlags =
		FOF_MULTIDESTFILES |
		FOF_RENAMEONCOLLISION |
		FOF_WANTMAPPINGHANDLE;

	//ファイル操作開始
	SHFileOperation(&fo);

	//名前の衝突があった
	if (fo.hNameMappings != NULL) {
		//HANDLETOMAPPINGSを受け取る
		HANDLETOMAPPINGS* mappings = (HANDLETOMAPPINGS*)fo.hNameMappings;

		for (int i = 0; i < mappings->uNumberOfMappings; i++) {
			StringCchPrintf(buf, BUFFERSIZE,
				L"%s\nは\n%s\nに名前が変更されました。",
				mappings->lpSHNameMapping[i].pszOldPath,
				mappings->lpSHNameMapping[i].pszNewPath);
			MessageBox(NULL, buf, L"情報", MB_OK);
		}

		//名前マッピングオブジェクトを破棄
		SHFreeNameMappings(fo.hNameMappings);
	}

	return 0;
}

このコードはコピー先に同じファイル名が既に存在する場合に、名前を変更してコピーします。
(変更後の名前はWindowsが自動で決定する)
そのファイルの古いファイル名と新しいファイル名をメッセージボックスで表示します。
名前マッピングオブジェクトの使用例

接続されたファイル(関連付けられたファイル)

HTMLファイル(.htmlまたは.htm)は、画像ファイルやCSSファイルなどのHTMLページを構成するファイルと「接続」することができます。
例えば「abc.html」ファイルがあるとき、同じ階層に「abc.files」(または「abc_files」)というフォルダを作ると、そのフォルダは「abc.html」と接続された状態になります。

この状態で「abc.html」ファイルまたは「abc.files」フォルダのどちらかに移動等の操作を行うと、もう一方も一緒に操作されます。
fFlagsメンバにFOF_NO_CONNECTED_ELEMENTSフラグを含めることで、この接続を無視して単体での移動等が可能になります。