EternalWindows
独自インストール / アンインストーラーサンプル

今回のプログラムは、前節のインストーラーに対するアンインストーラーです。

#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>

#pragma comment (lib, "shlwapi.lib")

WCHAR g_szProductName[] = L"My App";
WCHAR g_szDisplayName[] = L"SampleApplication";
WCHAR g_szUninstFilePath[MAX_PATH];

void DeleteShortcut();
void DeleteFolder(LPWSTR lpszTargetDir);
void Unregister();
void GetInstallPath(LPWSTR lpszPath, DWORD dwSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR szFolderPath[MAX_PATH];

	if (!IsUserAnAdmin()) {
		MessageBox(NULL, TEXT("管理者として起動してください。"), NULL, MB_ICONWARNING);
		return -1;
	}
	
	CoInitialize(NULL);

	DeleteShortcut();

	GetModuleFileNameW(NULL, g_szUninstFilePath, sizeof(g_szUninstFilePath) / sizeof(WCHAR));
	GetInstallPath(szFolderPath, sizeof(szFolderPath));
	DeleteFolder(szFolderPath);
	
	Unregister();

	MessageBox(NULL, TEXT("アンインストールしました。"), TEXT("OK"), MB_OK);

	CoUninitialize();

	return 0;
}

void DeleteShortcut()
{
	LPWSTR     lpszPath;
	WCHAR      szFolderPath[MAX_PATH];
	WCHAR      szLinkPath[MAX_PATH];
	HRESULT    hr;
	IShellItem *psi;

	SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, NULL, &lpszPath);
	wsprintfW(szFolderPath, L"%s\\%s", lpszPath, g_szProductName);
	hr = SHCreateItemFromParsingName(szFolderPath, NULL, IID_PPV_ARGS(&psi));
	if (SUCCEEDED(hr)) {
		IFileOperation *pFileOperation;
		hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation));
		if (SUCCEEDED(hr)) {
			pFileOperation->SetOperationFlags(FOF_NO_UI);
			pFileOperation->DeleteItem(psi, NULL);
			pFileOperation->PerformOperations();
			pFileOperation->Release();
		}
		psi->Release();
	}
	CoTaskMemFree(lpszPath);

	SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &lpszPath);
	wsprintfW(szLinkPath, L"%s\\%s.lnk", lpszPath, g_szDisplayName);
	if (GetFileAttributesW(szLinkPath) != -1)
		DeleteFileW(szLinkPath);
	CoTaskMemFree(lpszPath);
}

void DeleteFolder(LPWSTR lpszTargetDir)
{
	HANDLE           hFindFile;
	WIN32_FIND_DATAW findData;
	WCHAR            szPath[MAX_PATH];
	WCHAR            szSearch[MAX_PATH];

	wsprintfW(szSearch, L"%s\\*", lpszTargetDir);
	hFindFile = FindFirstFileW(szSearch, &findData);

	do {
		if (lstrcmpW(findData.cFileName, L"..") != 0 && lstrcmpW(findData.cFileName, L".") != 0) {
			wsprintfW(szPath, L"%s\\%s", lpszTargetDir, findData.cFileName);
			if (PathIsDirectoryW(szPath)) {
				DeleteFolder(szPath);
				RemoveDirectoryW(szPath);
			}
			else {
				if (lstrcmpW(szPath, g_szUninstFilePath) == 0)
					MoveFileExW(szPath, NULL, MOVEFILE_DELAY_UNTIL_REBOOT);
				else
					DeleteFileW(szPath);
			}
		}

	} while(FindNextFileW(hFindFile, &findData));

	FindClose(hFindFile);
}

void Unregister()
{
	WCHAR szKey[256];

	wsprintfW(szKey, L"SOFTWARE\\%s", g_szProductName);
	RegDeleteTreeW(HKEY_LOCAL_MACHINE, szKey);

	wsprintfW(szKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", g_szProductName);
	RegDeleteKeyW(HKEY_LOCAL_MACHINE, szKey);
}

void GetInstallPath(LPWSTR lpszPath, DWORD dwSize)
{
	HKEY  hKey;
	LONG  lResult;
	WCHAR szKey[256];

	wsprintfW(szKey, L"SOFTWARE\\%s", g_szProductName);
	lResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, szKey, 0, KEY_QUERY_VALUE, &hKey);
	if (lResult == ERROR_SUCCESS) {
		lResult = RegQueryValueExW(hKey, L"InstallDir", NULL, NULL, (LPBYTE)lpszPath, &dwSize);
		RegCloseKey(hKey);
	}
}

アンインストール処理の順番としては、最初にショートカットの削除、 次にアプリケーションフォルダの削除、そして最後にレジストリ登録の削除になっています。 まず、ショートカットの削除から確認します。

void DeleteShortcut()
{
	LPWSTR     lpszPath;
	WCHAR      szFolderPath[MAX_PATH];
	WCHAR      szLinkPath[MAX_PATH];
	HRESULT    hr;
	IShellItem *psi;

	SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, NULL, &lpszPath);
	wsprintfW(szFolderPath, L"%s\\%s", lpszPath, g_szProductName);
	hr = SHCreateItemFromParsingName(szFolderPath, NULL, IID_PPV_ARGS(&psi));
	if (SUCCEEDED(hr)) {
		IFileOperation *pFileOperation;
		hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation));
		if (SUCCEEDED(hr)) {
			pFileOperation->SetOperationFlags(FOF_NO_UI);
			pFileOperation->DeleteItem(psi, NULL);
			pFileOperation->PerformOperations();
			pFileOperation->Release();
		}
		psi->Release();
	}
	CoTaskMemFree(lpszPath);

	SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &lpszPath);
	wsprintfW(szLinkPath, L"%s\\%s.lnk", lpszPath, g_szDisplayName);
	if (GetFileAttributesW(szLinkPath) != -1)
		DeleteFileW(szLinkPath);
	CoTaskMemFree(lpszPath);
}

スタートメニューへのパスはFOLDERID_CommonProgramsを参照していたため、 このパスとg_szProductNameを連結すれば、ショートカットを作成しているフォルダのパスを取得できます。 SHCreateItemFromParsingNameを呼び出せばフォルダのIShellItemを取得でき、 これをIFileOperation::DeleteItemに指定すれば削除を行うよう宣言できます。 実際の削除はIFileOperation::PerformOperationsで行われるため、この呼び出しを忘れてはいけません。 デスクトップに作成されたショートカットについては、 そのパスを完成させてからGetFileAttributesを呼び出します。 これが成功する場合はショートカットが存在することを意味しますから、 DeleteFileで削除します。

続いて、アプリケーションフォルダの削除について説明します。 このフォルダもスタートメニューのときのように、フォルダ毎削除すればよいように思えますが、 フォルダ内にアンインストーラーの実行ファイルが存在する関係上、それは上手くいきません。 よって、フォルダ内のアイテムを明示的に削除していくことになります。

void DeleteFolder(LPWSTR lpszTargetDir)
{
	HANDLE           hFindFile;
	WIN32_FIND_DATAW findData;
	WCHAR            szPath[MAX_PATH];
	WCHAR            szSearch[MAX_PATH];

	wsprintfW(szSearch, L"%s\\*", lpszTargetDir);
	hFindFile = FindFirstFileW(szSearch, &findData);

	do {
		if (lstrcmpW(findData.cFileName, L"..") != 0 && lstrcmpW(findData.cFileName, L".") != 0) {
			wsprintfW(szPath, L"%s\\%s", lpszTargetDir, findData.cFileName);
			if (PathIsDirectoryW(szPath)) {
				DeleteFolder(szPath);
				RemoveDirectoryW(szPath);
			}
			else {
				if (lstrcmpW(szPath, g_szUninstFilePath) == 0)
					MoveFileExW(szPath, NULL, MOVEFILE_DELAY_UNTIL_REBOOT);
				else
					DeleteFileW(szPath);
			}
		}

	} while(FindNextFileW(hFindFile, &findData));

	FindClose(hFindFile);
}

FindFirstFileを呼び出せば、フォルダに存在するアイテムを列挙できます。 アイテムのパスを完成させてPathIsDirectoryを呼び出し、 これがTRUEである場合はアイテムがフォルダであることを意味しているため、 DeleteFolderを再帰呼び出します。 ここで制御が返った際にはフォルダが空になっているはずですから、RemoveDirectoryでフォルダを削除できます。 一方、アイテムがファイルである場合はそれがアンインストーラーのEXEであるかを調べます。 これが成功する場合は、MoveFileExの第2引数にNULLを指定して第3引数にMOVEFILE_DELAY_UNTIL_REBOOTを指定します。 これにより、このファイルは再起動後にシステムによって削除されます。 ただし、空のフォルダが残ってしまうため、それはユーザーが手動で削除することになります。 アンインストーラーでないファイルは、DeleteFileで問題なく削除できるはずです。

レジストリに書き込まれた情報は、Unregisterによって削除されることになります。

void Unregister()
{
	WCHAR szKey[256];

	wsprintfW(szKey, L"SOFTWARE\\%s", g_szProductName);
	RegDeleteTreeW(HKEY_LOCAL_MACHINE, szKey);

	wsprintfW(szKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", g_szProductName);
	RegDeleteKeyW(HKEY_LOCAL_MACHINE, szKey);
}

HKEY_LOCAL_MACHINE\SOFTWAREには独自のキーを作成していため、これを削除することになります。 RegDeleteKeyではなくRegDeleteTreeを呼び出しているのは、 この独自のキーにサブキーを持たせるようなこともあり、それをまとめて削除できるようにするためです。 一方、「プログラムのアンインストール」のために作成したキーでは、サブキーを持たせることはないはずですから、 RegDeleteKeyを呼び出しています。

旧技術のアンインストーラー

Windows Vista以降だけに限定せず、従来のWindowsでも動作するアンインストーラーの例を以下に示します。

#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>

#pragma comment (lib, "shlwapi.lib")

TCHAR g_szProductName[] = TEXT("My App");
TCHAR g_szDisplayName[] = TEXT("SampleApplication");
TCHAR g_szUninstFilePath[MAX_PATH];

void DeleteShortcut();
void DeleteFolder(LPTSTR lpszTargetDir);
void Unregister();
void GetInstallPath(LPWSTR lpszPath, DWORD dwSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR szFolderPath[MAX_PATH];

	if (!IsUserAnAdmin()) {
		MessageBox(NULL, TEXT("管理者として起動してください。"), NULL, MB_ICONWARNING);
		return -1;
	}

	CoInitialize(NULL);

	DeleteShortcut();
	
	GetModuleFileName(NULL, g_szUninstFilePath, sizeof(g_szUninstFilePath) / sizeof(TCHAR));
	GetInstallPath(szFolderPath, sizeof(szFolderPath));
	DeleteFolder(szFolderPath);
	
	Unregister();

	MessageBox(NULL, TEXT("アンインストールしました。"), TEXT("OK"), MB_OK);

	CoUninitialize();

	return 0;
}

void DeleteShortcut()
{
	TCHAR szPath[MAX_PATH];
	TCHAR szFolderPath[MAX_PATH];
	TCHAR szLinkPath[MAX_PATH];
	
	SHGetFolderPath(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, szPath);
	wsprintf(szFolderPath, TEXT("%s\\%s"), szPath, g_szProductName);
	if (GetFileAttributes(szFolderPath) != ERROR_FILE_NOT_FOUND) {
		SHFILEOPSTRUCT fileOp;

		ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT));
		fileOp.wFunc  = FO_DELETE;
		fileOp.pFrom  = szFolderPath;
		fileOp.fFlags = FOF_NO_UI;

		SHFileOperation(&fileOp);
	}
	
	SHGetFolderPath(NULL, CSIDL_DESKTOP, NULL, 0, szPath);
	wsprintf(szLinkPath, TEXT("%s\\%s.lnk"), szPath, g_szDisplayName);
	if (GetFileAttributes(szLinkPath) != -1)
		DeleteFile(szLinkPath);
}

void DeleteFolder(LPTSTR lpszTargetDir)
{
	HANDLE          hFindFile;
	WIN32_FIND_DATA findData;
	TCHAR           szPath[MAX_PATH];
	TCHAR           szSearch[MAX_PATH];

	wsprintf(szSearch, TEXT("%s\\*"), lpszTargetDir);
	hFindFile = FindFirstFile(szSearch, &findData);

	do {
		if (lstrcmp(findData.cFileName, TEXT("..")) != 0 && lstrcmp(findData.cFileName, TEXT(".")) != 0) {
			wsprintf(szPath, TEXT("%s\\%s"), lpszTargetDir, findData.cFileName);
			if (PathIsDirectory(szPath)) {
				DeleteFolder(szPath);
				RemoveDirectory(szPath);
			}
			else {
				if (lstrcmp(szPath, g_szUninstFilePath) == 0)
					MoveFileEx(szPath, NULL, MOVEFILE_DELAY_UNTIL_REBOOT);
				else
					DeleteFile(szPath);
			}
		}

	} while(FindNextFile(hFindFile, &findData));

	FindClose(hFindFile);
}

void Unregister()
{
	TCHAR szKey[256];

	wsprintf(szKey, TEXT("SOFTWARE\\%s"), g_szProductName);
	SHDeleteKey(HKEY_LOCAL_MACHINE, szKey);

	wsprintf(szKey, TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s"), g_szProductName);
	RegDeleteKey(HKEY_LOCAL_MACHINE, szKey);
}

void GetInstallPath(LPTSTR lpszPath, DWORD dwSize)
{
	HKEY  hKey;
	LONG  lResult;
	TCHAR szKey[256];

	wsprintf(szKey, TEXT("SOFTWARE\\%s"), g_szProductName);
	lResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, szKey, 0, KEY_QUERY_VALUE, &hKey);
	if (lResult == ERROR_SUCCESS) {
		lResult = RegQueryValueEx(hKey, TEXT("InstallDir"), NULL, NULL, (LPBYTE)lpszPath, &dwSize);
		RegCloseKey(hKey);
	}
}

このコードでは、IFileOperationではなくSHFileOperationを使用し、 SHGetKnownFolderPathではなくSHGetFolderPathを使用しています。 また、RegDeleteTreeではなくSHDeleteKeyを使用しています。



戻る