EternalWindows
MSI インストール編 / 製品の列挙

MSIによるインストールとは、単に1つのインストールの手段というわけではなく、 インストールされたアプリケーションを製品として扱うという特徴を持っています。 製品として扱うことで、製品の様々な情報をMSIの関数で統一的に取得することができるようになります。 製品は、次に示すレジストリキーにて管理されています。

レジストリキー 意味
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\
Windows\CurrentVersion\Installer
UserDataサブキー以下に、システムに存在するユーザーをテキスト形式のSIDで表したサブキーが列挙されている。 S-1-5-18を参照すれば、システムに存在する全ユーザーに対してインストールされた製品を確認することができ、 その他のSIDを見れば特定のユーザーにインストールされた製品を確認することができる。
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer Productsサブキー以下に、システムに存在する全ユーザーに対してインストールされた製品が列挙される。 ユーザーが見ることのない、内部的な情報を管理している。
HKEY_CURRENT_USER\Software\Microsoft\Installer Productsサブキー以下に、現在ログオンしているユーザーに対してインストールされた製品が列挙される。 ユーザーが見ることのない、内部的な情報を管理している。

基本的には、1行目に記したレジストリキーから情報を参照できますが、 それで足りない場合は他のレジストリキーを参照するとよいと思われます。 レジストリでは、製品をGUID形式の製品コードで表していないので注意してください。

さて、製品の情報がレジストリに格納されているとはいえ、 基本的にはMSI関数を呼び出せば目的の情報を得ることができます。 たとえば、システムにインストールされている全ての製品の製品コードを取得したいのであれば、 MsiEnumProductsを呼び出すとよいでしょう。

UINT MsiEnumProducts(
  DWORD iProductIndex,
  LPTSTR lpProductBuf
);

iProductIndexは、取得したい製品のインデックスをゼロベースで指定します。 基本的には0から順番にカウントしつつ指定し、関数の戻り値がERROR_SUCCESSとなるまで それを続けることになるでしょう。 lpProductBufは、指定したインデックスに相当する製品の製品コードが返ります。

製品コードを取得すれば、MsiGetProductInfoで製品の情報を取得できます。

UINT MsiGetProductInfo(
  LPCTSTR szProduct,
  LPCTSTR szProperty,
  LPTSTR lpValueBuf,
  DWORD *pcchValueBuf
);

szProductは、情報を取得したい製品の製品コードを指定します。 szPropertyは、取得したい製品のプロパティを指定します。 lpValueBufは、プロパティの値を受け取るためのバッファのアドレスを指定します。 pcchValueBufは、バッファのサイズを格納した変数のアドレスを指定します。

MsiGetProductInfoで言うところのプロパティとは、MSIファイルのPropertyテーブルと 近い意味を持ちますが、いくつか新しいプロパティを定義しています。 次に、プロパティと指定できる定数と文字列の一部を示します。

プロパティ 意味
INSTALLPROPERTY_INSTALLEDPRODUCTNAME インストールされた製品の名前。
INSTALLPROPERTY_LOCALPACKAGE インストールの際に使用したMSIファイルをキャッシュしているパス。
RegOwner インストール時に登録したユーザー名。

INSTALLPROPERTY_XXXの実際の値は文字列であり、INSTALLPROPERTY_INSTALLEDPRODUCTNAMEならば、 その値はInstalledProductNameとなっています。 RegOwnerなど一部のプロパティには専用の定義が存在しないため、 これらのプロパティに関しては文字列を直接指定することになります。

今回のプログラムは、MsiEnumProductsで取得した製品コードを左側のリストボックスに追加し、 選択された製品コードの情報を右側のリストボックスに追加します。

#include <windows.h>
#include <msi.h>

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

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndListBoxLeft = NULL;
	static HWND hwndListBoxRight = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		UINT  uResult;
		DWORD i;
		TCHAR szProductCode[256];

		hwndListBoxLeft = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | LBS_NOTIFY, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		hwndListBoxRight = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | LBS_NOTIFY, 0, 0, 0, 0, hwnd, (HMENU)2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
	
		for (i = 0;; i++) {
			uResult = MsiEnumProducts(i, szProductCode);
			if (uResult != ERROR_SUCCESS)
				break;
			SendMessage(hwndListBoxLeft, LB_ADDSTRING, 0, (LPARAM)szProductCode);
		}

		return 0;
	}

	case WM_COMMAND: {
		int   i;
		int   nIndex;
		DWORD dwSize;
		TCHAR szValue[256];
		TCHAR szBuf[256];
		TCHAR szProductCode[256];
		LPTSTR lpszPropertyName[] = {
			INSTALLPROPERTY_INSTALLEDPRODUCTNAME, INSTALLPROPERTY_PUBLISHER, INSTALLPROPERTY_VERSIONSTRING,
			INSTALLPROPERTY_INSTALLSOURCE, INSTALLPROPERTY_INSTALLLOCATION, INSTALLPROPERTY_INSTALLDATE,
			INSTALLPROPERTY_LOCALPACKAGE, INSTALLPROPERTY_HELPLINK, INSTALLPROPERTY_URLINFOABOUT,
			TEXT("RegOwner"), TEXT("RegCompany"), TEXT("ProductID")
		};
		int nPropertyCount = sizeof(lpszPropertyName) / sizeof(lpszPropertyName[0]);

		if ((HWND)lParam == hwndListBoxRight || HIWORD(wParam) != LBN_SELCHANGE)
			return 0;

		SendMessage(hwndListBoxRight, LB_RESETCONTENT, 0, 0);

		nIndex = (int)SendMessage(hwndListBoxLeft, LB_GETCURSEL, 0, 0);
		SendMessage(hwndListBoxLeft, LB_GETTEXT, nIndex, (LPARAM)szProductCode);

		for (i = 0; i < nPropertyCount; i++) {
			dwSize = sizeof(szValue);
			MsiGetProductInfo(szProductCode, lpszPropertyName[i], szValue, &dwSize);
			wsprintf(szBuf, TEXT("%s : %s"), lpszPropertyName[i], szValue);
			SendMessage(hwndListBoxRight, LB_ADDSTRING, 0, (LPARAM)szBuf);
		}

		return 0;
	}

	case WM_SIZE:
		MoveWindow(hwndListBoxLeft, 0, 0, LOWORD(lParam) / 2, HIWORD(lParam), TRUE);
		MoveWindow(hwndListBoxRight, LOWORD(lParam) / 2, 0, LOWORD(lParam) / 2, HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

MsiEnumProductsが返す製品コードは、システム全体にインストールされた製品も 特定のユーザーにインストールされた製品も対象とするため、 それなりの数の製品コードがリストボックスに追加されるのではないかと思います。 MsiEnumProductsを呼び出しているのは、WM_CREATEの次の部分です。

for (i = 0;; i++) {
	uResult = MsiEnumProducts(i, szProductCode);
	if (uResult != ERROR_SUCCESS)
		break;
	SendMessage(hwndListBoxLeft, LB_ADDSTRING, 0, (LPARAM)szProductCode);
}

iを製品インデックスとして利用し、ループでカウントしていきます。 MsiEnumProductsは、製品が存在しなくなったときにERROR_MORE_DATAを返すため、 このときは関数が成功していないとみなし、ループを抜けることになります。

WM_COMMANDでは、左側のリストボックスで選択された製品コードを取得し、 それを基にMsiGetProductInfoを呼び出しています。

for (i = 0; i < nPropertyCount; i++) {
	dwSize = sizeof(szValue);
	MsiGetProductInfo(szProductCode, lpszPropertyName[i], szValue, &dwSize);
	wsprintf(szBuf, TEXT("%s : %s"), lpszPropertyName[i], szValue);
	SendMessage(hwndListBoxRight, LB_ADDSTRING, 0, (LPARAM)szBuf);
}

lpszPropertyNameは、MsiGetProductInfoに指定可能なプロパティを持つ配列です。 プロパティの値を取得し、プロパティ名と値を文字列として整形してから追加します。

ユーザー情報の取得

製品をインストールする際、場合によってはユーザー名や企業名、製品IDなどの入力を 求められることがありますが、これらの情報はユーザー情報という分類に入ります。 ユーザー情報は、MsiGetProductInfoで取得することもできますが、 MsiGetUserInfoという専用の関数が用意されているので、その使い方を検討します。

void ShowUserInfo(LPTSTR lpszProductCode)
{
	TCHAR szUserName[256]  = {0};
	TCHAR szOrgName[256]   = {0};
	TCHAR szProductId[256] = {0};
	DWORD dwUserNameSize   = sizeof(szUserName);
	DWORD dwOrgNameSize    = sizeof(szOrgName);
	DWORD dwProductIdSize  = sizeof(szProductId);

	MsiGetUserInfo(lpszProductCode, szUserName, &dwUserNameSize, szOrgName, &dwOrgNameSize, szProductId, &dwProductIdSize);

	MessageBox(NULL, szUserName, TEXT("ユーザー名"), MB_OK);
	MessageBox(NULL, szOrgName, TEXT("企業名"), MB_OK);
	MessageBox(NULL, szProductId, TEXT("製品ID"), MB_OK);
}

この自作関数は、呼び出し元から渡された製品コードを基にMsiGetUserInfoを呼び出し、 製品のユーザー情報を取得して表示するのが目的です。 MsiGetUserInfoの第1引数は製品コードで、第2引数はユーザー名、第3引数はバッファのサイズ、 後の引数も企業名、製品IDというように同じ要領で続きます。 指定した情報が存在しなかった場合は、noneという文字列が格納されます。



戻る