EternalWindows
独自インストール / ダイアログとオペレーション

多くのインストーラーは、インストール先の既定のフォルダとしてProgramFilesを既定値としていますが、 場合によってはこれを変更したいことがあります。 こうした状況で役に立つのはフォルダを選択するためのダイアログであり、 従来ではSHBrowseForFolderがこの目的として使用されていました。 しかし、Windows Vista以降ではIFileOpenDialogが推奨されているため、 今回はこれを使用することにしています。

if (LOWORD(wParam) == ID_PATH) {
	IFileOpenDialog *pFileOpenDialog;
	HRESULT         hr;
	IShellItem      *psiFolder;
	IShellItem      *psiParent;
	WCHAR           sz[MAX_PATH];
	LPWSTR          lpszItem;
	DWORD           dwOptions;

	hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpenDialog));
	if (FAILED(hr))
		return 0;

	GetWindowTextW(GetDlgItem(hwnd, ID_PATH_EDIT), sz, MAX_PATH);
	SHCreateItemFromParsingName(sz, NULL, IID_PPV_ARGS(&psiFolder));
	psiFolder->GetParent(&psiParent);
	psiFolder->GetDisplayName(SIGDN_NORMALDISPLAY, &lpszItem);

	pFileOpenDialog->SetFolder(psiParent);
	pFileOpenDialog->SetFileName(lpszItem);
	pFileOpenDialog->GetOptions(&dwOptions);
	pFileOpenDialog->SetOptions(dwOptions | FOS_PICKFOLDERS);

	hr = pFileOpenDialog->Show(hwnd);
	if (SUCCEEDED(hr)) {
		LPWSTR     lpszPath;
		IShellItem *psi;

		hr = pFileOpenDialog->GetResult(&psi);
		if (SUCCEEDED(hr)) {
			psi->GetDisplayName(SIGDN_FILESYSPATH, &lpszPath);
			SetWindowTextW(GetDlgItem(hwnd, ID_PATH_EDIT), lpszPath);
			CoTaskMemFree(lpszPath);
			psi->Release();
		}
	}
	
	CoTaskMemFree(lpszItem);
	psiParent->Release();
	pFileOpenDialog->Release();
}

CoCreateInstanceにCLSID_FileOpenDialogを指定すれば、IFileOpenDialogを取得できます。 このインターフェースはIFileDialogを継承しているため、 オブジェクトはIFileDialogでも識別可能です。 GetWindowTextを呼び出しているのは既定のフォルダパスを取得するためであり、 これをSHCreateItemFromParsingNameに指定することで、そのパスをIShellItemで識別できます。 GetParentで親フォルダのIShellItemを取得しているのは、 IFileDialog::SetFolderでこの親フォルダをオープンするためです。 この親フォルダでは、psiFolderで識別されるアイテムが既定で選択されてほしいため、 GetDisplayNameで名前を取得し、これをIFileDialog::SetFileNameに指定します。 ダイアログがフォルダのみを選択できるように、IFileDialog::SetOptionsにはFOS_PICKFOLDERSを指定しますが、 このときに既存のオプションが失われないように、IFileDialog::GetOptionsで取得した値も含めます。 IFileDialog::Showの呼び出しで実際にダイアログが表示され、 IFileOpenDialog::GetResultで選択されたアイテムを取得できます。 GetDisplayNameにSIGDN_FILESYSPATHを指定すればアイテムのフルパスを取得できるため、 これをエディットコントロールに設定します。

インストールボタンが押された場合は、次の処理が実行されます。

else if (LOWORD(wParam) == ID_INSTALL) {
	HANDLE hThread;
	DWORD  dwThreadId;
	
	GetWindowTextW(GetDlgItem(hwnd, ID_PATH_EDIT), g_szTargetFolder, MAX_PATH);

	EnableWindow(GetDlgItem(hwnd, ID_PATH), FALSE);
	EnableWindow(GetDlgItem(hwnd, ID_INSTALL), FALSE);

	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, hwnd, 0, &dwThreadId);
}

エディットコントロール上で入力されたテキストをインストール先としたいため、 GetWindowTextでこれを取得しています。 EnableWindowを呼び出して各種コントロールを無効にしているのは、 インストール中にボタンを操作させないためです。 CreateThreadを呼び出しているのは、インストールにおけるファイルのコピーを専用のスレッドで行うためです。 コピーは時間が掛かることがあり、それによってメインスレッドが待機してしまってはUI操作ができなくなるため、 コピー用のスレッドを作成しています。 スレッドの実装は次のようになっています。

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IFileOperation *pFileOperation;
	IShellItem     *psiFrom, *psiTo;
	HRESULT        hr;
	WCHAR          szProductPath[MAX_PATH];
	WCHAR          szFolderPath[MAX_PATH];
	BOOL           bAnyOperationsAborted;
	HWND           hwnd = (HWND)lpParameter;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation));
	if (FAILED(hr)) {
		PostMessage(hwnd, WM_ENDOPERATION, 1, 0);
		return 0;
	}

	GetCurrentDirectoryW(sizeof(szFolderPath) / sizeof(WCHAR), szFolderPath);
	wsprintfW(szProductPath, L"%s\\%s", szFolderPath, g_szProductName);
	
	SHCreateItemFromParsingName(szProductPath, NULL, IID_PPV_ARGS(&psiFrom));
	SHCreateItemFromParsingName(g_szTargetFolder, NULL, IID_PPV_ARGS(&psiTo));

	pFileOperation->CopyItem(psiFrom, psiTo, g_szProductName, NULL);
	pFileOperation->PerformOperations();
	
	pFileOperation->GetAnyOperationsAborted(&bAnyOperationsAborted);
	PostMessage(hwnd, WM_ENDOPERATION, bAnyOperationsAborted, 0);

	psiTo->Release();
	psiFrom->Release();
	pFileOperation->Release();

	return 0;
}

従来では、フォルダのコピーはSHFileOperationで行っていましたが、 WindowsVista以降ではIFileOperationの使用が推奨されています。 これはCoCreateInstanceにCLSID_FileOperationを指定することで取得できます。 CoInitializeはスレッド単位で必要になるため、呼び出すようにしておきます。 コピーの宣言はIFileOperation::CopyItemで可能ですが、 このためにはコピー元とコピー先のフォルダのIShellItemが必要になります。 コピー元フォルダはカレントディレクトリとg_szProductNameを連結させることで作成でき、 コピー先フォルダはg_szTargetFolderに格納されているため、 これらをSHCreateItemFromParsingNameへ指定すればよいことになります。 CopyItemの第3引数はコピーされたアイテムに設定する名前であり、 上記ではg_szProductNameになっていることから、コピー前とコピー後の名前は同じになります。 CopyItemはコピーの宣言を行うだけで、実際にコピーを行うわけではないことに注意してください。 宣言された操作はPerformOperationsによって初めて実行されるため、PerformOperationsは必ず呼び出すようにします。 PerformOperationsはコピーが終了またはキャンセルされるまで制御を返さず、 コピー中にユーザーがキャンセルしたかどうかは、 GetAnyOperationsAbortedを呼び出すことで分かります。 WM_ENDOPERATIONはコピーの終了を通知するためのオリジナルメッセージであり、 このときにWPARAMとしてbAnyOperationsAbortedを指定することで、 メインスレッドはキャンセルの有無を確認できます。


戻る