EternalWindows
独自インストール / インストーラーの外観

多くの開発者にとって、インストーラーの作成は簡単に済ましたい部分だと思われます。 Windows標準のインストーラーであるMSIは、インストールに必要な基本的な処理をサポートしていますから、 この形式のファイルをVisual Studioなどで作成すれば、 それだけでインストーラーは完成したことになります。 このようなMSIの手軽さと比較して、一からコードを書いてインストーラーを作成することは非効率ともいえますが、 場合によってはそれが必要になることもあります。 インストーラーのexeファイルを作成するということは、開発者が用意した独自のコードを実行できるということですから、 MSIの枠を超えた柔軟性のあるインストールを提供することも可能になるわけです。 ただし、MSIでもカスタムアクションという仕組みで独自のコードを実行できるため、 本当に独自のインストーラーを作成する必要があるかは、よく検討する必要があります。

独自のインストールを行うにあたって実装すべきことは多数あります。 たとえば、レジストリにアプリケーションの情報を書き込む必要がありますし、 デスクトップにショートカットを作成することもあります。 また、インストールしたいファイル群を1つの単位に圧縮し、 それをインストール時に展開する必要もあるでしょう。 ただし、残念ながら今回作成するインストーラーでは、この圧縮と展開の処理は実装されていません。 つまり、アプリケーションが格納されているフォルダを、別のパスへコピーしているだけです。 想定している使い方としては、CDなどにインストーラーとフォルダを格納しておき、 インストーラーの起動を通じてフォルダのコピーを行います。

setup.exeが今回作成することになるサンプルであり、これがインストーラーの役割を果たします。 My Appがフォルダごとコピーされることになるため、このフォルダに必要なファイルを格納しておきます。 setup.exeを起動すると次のようなウインドウが表示されます。

エディットコントロールには、インストール先となるフォルダパスを入力します。 「参照」ボタンを押せば、ダイアログからフォルダを選択することができます。 2つのチェックボックスについては文字通りの意味であり、 指定箇所にショートカットを作成したい場合はチェックを付けます。 「インストール」ボタンを押すと、My Appフォルダがエディットコントロールに入力されたパスへコピーされます。 この他、「プログラムのアンインストール」からアプリケーションを削除できるように、レジストリへの書き込みも行います。

ウインドウに表示されている各種コントロールは、WM_CREATEで作成されています。

case WM_CREATE: {
	LPWSTR lpszPath;
	
	CoInitialize(NULL);

	if (!FileCheck())
		return -1;

	CreateWindowEx(0, TEXT("BUTTON"), TEXT("参照"), WS_CHILD | WS_VISIBLE, 650, 40, 70, 30, hwnd, (HMENU)ID_PATH, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
	CreateWindowEx(0, TEXT("EDIT"), TEXT(""), WS_CHILD | WS_VISIBLE | WS_BORDER, 30, 40, 600, 30, hwnd, (HMENU)ID_PATH_EDIT, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
	CreateWindowEx(0, TEXT("BUTTON"), TEXT("デスクトップにショートカットを作成"), WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, 30, 90, 300, 40, hwnd, (HMENU)ID_CHECK_DESKTOP, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
	CreateWindowEx(0, TEXT("BUTTON"), TEXT("スタートメニューにショートカットを作成"), WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, 30, 140, 330, 40, hwnd, (HMENU)ID_CHECK_STARTMENU, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
	CreateWindowEx(0, TEXT("BUTTON"), TEXT("インストール"), WS_CHILD | WS_VISIBLE, 30, 190, 150, 40, hwnd, (HMENU)ID_INSTALL, ((LPCREATESTRUCT)lParam)->hInstance, NULL);

	SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, NULL, &lpszPath);
	SetWindowTextW(GetDlgItem(hwnd, ID_PATH_EDIT), lpszPath);
	CoTaskMemFree(lpszPath);

	return 0;
}

ID_PATH_EDITで識別されるコントロールはエディットコントロールであり、 ここにはインストール先のパスを入力することになります。 今回は既定のインストール先としてProgramFilesを設定していますが、 これを変更する場合はSHGetKnownFolderPathの第1引数に適切な値を指定します。 この関数はシステムに存在する既知のパスを取得することができ、 従来のSHGetFolderPathを置き換えるものです。 FileCheckという自作関数は、インストールすることになるフォルダに特定のファイルが存在するかを調べます。

BOOL FileCheck()
{
	WCHAR szFilePath[MAX_PATH];
	WCHAR szFolderPath[MAX_PATH];
	WCHAR szProductPath[MAX_PATH];
	WCHAR szBuf[512];
	
	GetCurrentDirectoryW(sizeof(szFolderPath) / sizeof(WCHAR), szFolderPath);
	wsprintfW(szProductPath, L"%s\\%s", szFolderPath, g_szProductName);

	wsprintfW(szFilePath, L"%s\\%s", szProductPath, g_szMainExeName);
	if (GetFileAttributesW(szFilePath) == -1) {
		wsprintfW(szBuf, L"%sに%sが存在しません。", szProductPath, g_szMainExeName);
		MessageBoxW(NULL, szBuf, NULL, MB_ICONWARNING);
		return FALSE;
	}
	
	wsprintfW(szFilePath, L"%s\\%s", szProductPath, g_szUninstExeName);
	if (GetFileAttributesW(szFilePath) == -1) {
		wsprintfW(szBuf, L"%sに%sが存在しません。", szProductPath, g_szUninstExeName);
		MessageBoxW(NULL, szBuf, NULL, MB_ICONWARNING);
		return FALSE;
	}

	return TRUE;
}

インストールすることになるフォルダの名前は、g_szProductNameで識別されるものとし、 それはカレントディレクトリに存在するとします。 szProductPathの下には、実際のアプリケーション(g_szMainExeName)とアンインストーラー(g_szUninstExeName)が存在することを前提としているため、 GetFileAttributesの戻り値を通じてその存在を確認しています。 ちなみに、g_szProductNameなどは次のように定義されています。

WCHAR g_szProductName[] = L"My App";
WCHAR g_szMainExeName[] = L"sample.exe";
WCHAR g_szUninstExeName[] = L"uninst.exe";
WCHAR g_szDisplayName[] = L"SampleApplication";

この定義から分かるように、カレントディレクトリにはMy Appというフォルダが存在し、 さらにその下にsample.exeとuninst.exeが存在している必要があります。 g_szDisplayNameは、デスクトップショートカットの名前と、 「プログラムのアンインストール」の名前として使用されます。

インストーラーのファイル名をsetupにしたり、 アンインストーラーのファイル名をuninstにしたりしているのは意味があります。 Windows Vistaでこれらの文字列を含むファイルは、起動時に管理者として昇格するためのダイアログが表示されるため、 管理者でなければ成功しない動作を行うインストーラーなどでは便利です。

ファイルの保存先について

インストーラーによってインストールされたアプリケーションは、 アプリケーション内で扱う情報をどこに保存することになるのでしょうか。 プログラミングの視点から考えれば、カレントディレクトリにファイルを保存することが最も手軽といえますが、 Program Filesのような管理者でしか書き込みできないディレクトリの場合は、通常のユーザーでは書き込みに失敗してしまいます。 また、管理者であったとしても、Windows Vistaから登場したUACが有効な場合は、Program Filesにファイルは保存されません。 保存先となるのは、Program Filesへ書き込めないときのために用意された専用のディレクトリであり、 このような仮想化が働いてしまうと、UACが無効になった途端にファイルを参照できなくなる問題があります。 極端な話、アプリケーションを管理者として起動するようにマークしておけば、こうした問題は発生しませんが、 ただファイルを保存するためだけに、アプリケーションが昇格するのは好ましくないといえます。

ファイルの保存先をProgram Filesから、ユーザープロファイル以下に変更すれば問題は一気に解決します。 ユーザープロファイルとはWindows Vistaであれば"C:\Users\<username>"以下のことですが、 ここならばユーザーはファイルを保存できるからです。 具体的な保存先ですが、ドキュメントフォルダならばユーザーが普通に中身を確認することもありますから、 データフォルダを対象にすればよいでしょう。 このフォルダのパスは次のようにして取得できます。

TCHAR szPath[MAX_PATH];
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, szPath);

// Vistaならば C:\Users\<username>\AppData\Roaming
// XPならば C:\Documents and Settings\<username>\Application Data

得られるパスがXPとVistaで異なるように、ファイルパスはソース上にハードコードするべきではなく、 SHGetFolderPathを呼び出して取得すべきであるといえます。 ちなみに、Windows VistaではSHGetKnownFolderPathにFOLDERID_RoamingAppDataを指定する方法もあります。



戻る