EternalWindows
MSI インストール編 / フィーチャの列挙

製品をインストールすることによって生じる結果とは、それほど意外なものではありません。 簡単に述べれば、ファイルが実際にインストールされたり、レジストリに書き込みが行われる くらいですが、それらはあくまでフィーチャをインストールことによって生じる結果です。 以前、ファイルやレジストリはMSIではコンポーネントという単位で扱われ、 コンポーネントはフィーチャがインストールすることで初めてインストールされると述べました。 このメカニズムにより、MSIファイルの開発者は一定のコンポーネント群毎にフィーチャを定義し、 たとえばその名前を「リファレンス」とするならば、フィーチャに関連付けられるコンポーネントには、 コンパイル済みHTMLヘルプファイルなどが含まれることになるでしょう。 特定の機能(上記の場合ではリファレンス)を必要とするかどうかをユーザーに尋ねることで、 ユーザーは自分にとって必要なファイルのみをインストールすることができるようになります。

Viaual Stduioのセットアッププロジェクトでは、MSIファイルを作成するときに フィーチャの指定を行うことができないため、常にDefaultFeatureという名のフィーチャに 全てのコンポーネントが関連付けられることになります。 当然ながら、このフィーチャは必ずインストールされるべきであり、 インストール時に特定のフィーチャを選択する必要もないことから、 結果としてユーザーも開発者もフィーチャの存在を意識することはなくなるでしょう。 しかし、ある製品において、どれだけのフィーチャが存在するかを取得できることは、 その製品が持てる機能を知れることになり、非常に有用といえます。 次に示すMsiEnumFeaturesは、指定した製品に含まれるフィーチャを列挙します。

UINT MsiEnumFeatures(
  LPCTSTR szProduct,
  DWORD iFeatureIndex,
  LPTSTR lpFeatureBuf,
  LPTSTR lpParentBuf
);

szProductは、フィーチャを列挙したい製品の製品コードを指定します。 iFeatureIndexは、ゼロベースでフィーチャのインデックスを指定します。 lpFeatureBufは、フィーチャのIDとなる文字列を受け取るバッファのアドレスを指定します。 lpParentBufは、取得したフィーチャの親のフィーチャIDが返りますが、NULLで問題ありません。

フィーチャIDからフィーチャの情報を取得するには、少しばかりの手順を踏むことになります。 これは一重に、レジストリ内で管理されている情報がフィーチャとその親フィーチャだけであり、 フィーチャに関して記述する文字列が格納されているのは、あくまでMSIファイルだからです。 しかし、それはデータベース系の関数を利用してFeatureテーブルを参照するということではなく、 データベースへのアクセスを簡略化するハンドルを取得するということを意味しています。 このハンドルの事を製品ハンドルと呼び、MsiOpenProductで取得することになります。

UINT MsiOpenProduct(
  LPCTSTR szProduct,
  MSIHANDLE *hProduct
);

szProductは、ハンドルを取得したい製品の製品コードを指定します。 hProductは、製品ハンドルを受け取るための変数のアドレスを指定します。 このハンドルが、製品に関連するMSIファイルのデータベースを指すことになります。

製品ハンドルを取得すれば、それを使用してMsiGetFeatureInfoを呼び出すことができます。

UINT MsiGetFeatureInfo(
  MSIHANDLE hProduct,
  LPCTSTR szFeature,
  DWORD *lpAttributes,
  LPTSTR lpTitleBuf,
  DWORD *pcchTitleBuf,
  LPTSTR lpHelpBuf,
  DWORD *pcchHelpBuf
);

hProductは、製品ハンドルを指定します。 szFeatureは、情報を取得したいフィーチャのIDを指定します。 lpAttributesは、フィーチャの属性フラグが返ります。 lpTitleBufは、フィーチャのタイトルを受け取るバッファのアドレスを指定します。 この値は、FeatureテーブルのTitleカラムに相当します。 pcchTitleBufは、lpTitleBufのサイズを格納した変数のアドレスを指定します。 lpHelpBufは、フィーチャについて記述した文字列を受け取るバッファのアドレスを指定します。 この値は、FeatureテーブルのDescriptionカラムに相当します。 pcchHelpBufは、lpHelpBufのサイズを格納した変数のアドレスを指定します。

今回のプログラムは、指定した製品のフィーチャを左側のリストボックスに列挙し、 選択したフィーチャの情報を右側のリストボックスに追加します。 WindowProcにて静的に宣言されているszProductCodeに、適切な製品コードを指定してください。

#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;
	static TCHAR     szProductCode[] = TEXT("");
	static MSIHANDLE hProduct = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		UINT  uResult;
		DWORD i;
		TCHAR szFeature[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);
	
		MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL);
		if (MsiOpenProduct(szProductCode, &hProduct) != ERROR_SUCCESS)
			return -1;

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

		return 0;
	}

	case WM_COMMAND: {
		int   nIndex;
		TCHAR szBuf[256];
		TCHAR szFeature[256];
		TCHAR szTitle[256];
		TCHAR szHelp[256];
		DWORD dwAttribute;
		DWORD dwTitleSize;
		DWORD dwHelpSize;

		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)szFeature);

		dwTitleSize = sizeof(szTitle);
		dwHelpSize = sizeof(szHelp);
		MsiGetFeatureInfo(hProduct, szFeature, &dwAttribute, szTitle, &dwTitleSize, szHelp, &dwHelpSize);

		wsprintf(szBuf, TEXT("Title : %s"), szTitle);
		SendMessage(hwndListBoxRight, LB_ADDSTRING, 0, (LPARAM)szBuf);
		wsprintf(szBuf, TEXT("Help : %s"), szHelp);
		SendMessage(hwndListBoxRight, LB_ADDSTRING, 0, (LPARAM)szBuf);

		lstrcpy(szBuf, TEXT("Attributes : "));
		if (dwAttribute & INSTALLFEATUREATTRIBUTE_FAVORLOCAL)
			lstrcat(szBuf, TEXT("Local "));
		if (dwAttribute & INSTALLFEATUREATTRIBUTE_FAVORSOURCE)
			lstrcat(szBuf, TEXT("Source "));
		if (dwAttribute & INSTALLFEATUREATTRIBUTE_FOLLOWPARENT)
			lstrcat(szBuf, TEXT("Parent "));
		if (dwAttribute & INSTALLFEATUREATTRIBUTE_FAVORADVERTISE)
			lstrcat(szBuf, TEXT("Advertise "));
		if (dwAttribute & INSTALLFEATUREATTRIBUTE_DISALLOWADVERTISE)
			lstrcat(szBuf, TEXT("DisallowAdvertise "));
		if (dwAttribute & INSTALLFEATUREATTRIBUTE_NOUNSUPPORTEDADVERTISE)
			lstrcat(szBuf, TEXT("NonSupportedAdvertise "));

		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:
		if (hProduct != NULL)
			MsiCloseHandle(hProduct);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

szProductCodeに指定する製品コードについては、 たとえば前節で列挙した製品コードのいずれかを指定してもよいでしょう。 hProductを静的に宣言するのは、MsiGetFeatureInfoの呼び出しに伴って 毎回製品ハンドルを取得するのが非効率だと考えたからであり、 WM_CREATEで予め取得しておくことにしています。

MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL);
if (MsiOpenProduct(szProductCode, &hProduct) != ERROR_SUCCESS)
	return -1;

MsiOpenProductの呼び出しは、見て分かるように単純です。 その前に呼び出しているMsiSetInternalUIという関数については後の節で説明します。 次に、MsiEnumFeaturesの呼び出しを確認します。

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

これについても、特に問題はないと思われます。 前節のMsiEnumProductsと同じように、関数がERROR_SUCCESSでない値を返すまで インデックスをカウントしつつ、指定していきます。

WM_COMMANDでは、選択されたフィーチャIDを取得し、MsiGetFeatureInfoを呼び出します。

dwTitleSize = sizeof(szTitle);
dwHelpSize = sizeof(szHelp);
MsiGetFeatureInfo(hProduct, szFeature, &dwAttribute, szTitle, &dwTitleSize, szHelp, &dwHelpSize);

dwAttributeの属性フラグについてですが、このフラグがあくまでMSIファイルから取得したもの であるということは常に意識しておいてください。 たとえば、フラグの値がFAVORLOCALとFAVORADVERTISEの組み合わせである場合、 そのフィーチャは最初はアドバタイズとしてインストールされ、 必要になって時点でローカルにインストールされるということを意味しますが、 この情報からは、現在のインストール状態というのは特定することはできません。 つまり、フィーチャは今もなおアドバタイズとしてインストールされたままなのか、 既にローカルのディスクにインストールされたのかが分からないわけです。 このような場合は、MsiQueryFeatureStateでインストール状態を確認します。

ローカルキャッシュパッケージについて

既に述べたように、MsiOpenProductは製品コードからMSIファイルをオープンするわけですが、 このMSIファイルのパスは一体どのように取得しているのでしょうか。 また、インストールが完了するとユーザーはMSIファイルを削除するかもしれませんから、 このような問題にはどのように対処しているのでしょうか。 実は、システムはインストールが完了した際にMSIファイルをローカルキャッシュパッケージとして C:\WINDOWS\Installer以下に名前を変更してコピーすることになっており、 その名前を含めたフルパスをレジストリに書き込むことになっています。 既にインストールされた他製品のMSIファイルの中身はそう見えるものではありませんが、 このローカルキャッシュパッケージの仕組みに着目し、取得したMSIファイルのパスを MsiOpenDatabaseに指定すれば、データベースをオープンすることができるでしょう。 ローカルキャッシュパッケージのパスは、MsiGetProductInfoにINSTALLPROPERTY_LOCALPACKAGEを 指定して取得することもできます。

MsiOpenProductが返す製品ハンドルを利用する関数として、 MsiGetFeatureInfoの他にMsiGetProductPropertyという関数があります。 この関数は、Propertyテーブルから指定されたPropertyカラムの値を取得しますが、 プロパティの値の一部はレジストリにも書き込まれているので、 それを取得するだけであれば、MsiGetProductInfoを呼び出したほうがよいと思われます。 MsiOpenProductの常にローカルキャッシュパッケージへアクセスするする設計と、 その設計であるからの波及なのか、MsiSetInternalUIでUIレベルを下げなければ 無意味なダイアログが表示される点を考えると、決して使いやすいとは言い難いものがあります。



戻る