Windows Installer手引書 Part.18 カスタムアクションでダイアログボックスを表示する

[**前の記事へ**](https://qiita.com/tohshima/items/626b0462be9ea010621e)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/bb1a8401a620654569cb)

## カスタムアクションとUI

カスタムアクションでは、
1. ダイアログボックスの表示
2. インストールログへの任意の文字出力
3. インストールの進捗バーやバーの周囲に表示される文字のコントロール
といった3つの機能を、「Windows Installerにメッセージを送る」という１つの仕組みで呼び出すことができます。今回の記事では、1.のダイアログボックスの表示を扱います。Windows Installerにメッセージを送る方法を簡単に言うと、「レコード」という形式で送るべきメッセージを作成し、[MsiProcessMessage()](https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msiprocessmessage)関数でレコードをWindows Installerに送る、となります。

## レコード

レコードの構造は、C言語で言うところの構造体[^1]の配列に似ています[^2]。そして１つのレコードには下記３種類のデータを１つずつ格納できます。

- 整数
- 文字列
- ストリーム

そして、レコードを扱うための関数をWindows Installerが用意しています。レコードの生成、書き込み、廃棄の関数は下表の通りです。

|関数名|機能|
|:--|:--|
|[MsiCreateRecord()](https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msicreaterecord)|レコードの生成|
|[MsiRecordSetInteger()](https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msirecordsetinteger)|整数の設定|
|[MsiRecordSetString()](https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msirecordsetstringa)|文字列の設定[^3]|
|[MsiRecordSetStream()](https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msirecordsetstreama)|ストリームの設定[^3]|
|[MsiCloseHandle()](https://docs.microsoft.com/en-us/windows/win32/api/msi/nf-msi-msiclosehandle)|レコードの破棄|

ダイアログボックスを表示するには、レコードを１つ生成し、レコードに表示する文字列を設定してやります。

## 基本

[前回](https://qiita.com/tohshima/items/626b0462be9ea010621e)説明を先送りしたコードを少し改造して以下に示します。ここではダイアログボックスを表示するコードを書いています。これを例に説明していきましょう。

```C++:ダイアログボックスを表示する
PMSIHANDLE hRecord = MsiCreateRecord(1);
MsiRecordSetString(hRecord, 0, "This is test message.");
MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_ERROR | MB_OK), hRecord);
```

１行目で`MsiCreateRecord()`関数を使用し、レコードを１つ生成します。レコードの型を`PMSIHANDLE `とすると、関数を抜けたとき自動的にレコードが破棄されます[^4]。２行目の`MsiRecordSetString()`関数で１つ目（０から数え始まるので第２引数に０を指定）のレコードに文字列を設定します。そして、第３引数に本文の文字列を指定。３行目の`MsiProcessMessage()`関数を使い、レコードをWindow Installerに送ります。第２引数の指定で、色々な種類のダイアログボックスを表示できるようになっています。ここでは、この指定に従い、[OK]ボタンが一つの黄色アイコンのエラーダイアログボックスが表示されます。
![part18TestMessage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/8a9c1b2e-e5d2-f9da-d315-b558f1fbf564.png)

## Error Tableにメッセージを格納する

MSIファイルのErrorテーブルに、ダイアログボックスで表示する文字列を格納することができます。[Errorテーブルの説明](https://docs.microsoft.com/ja-jp/windows/win32/msi/error-table)によると、カスタムアクション用として使えるerror numberは25000から30000ということになっているので、この範囲の番号を使用します。WiX Toolsetでは、下記のようにUIエレメントの中にErrorエレメントを定義してErrorテーブルを定義します。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="C2667EC3-95EA-4A5A-81C6-8627E1B9E2CA" Name="Part18_02" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="82b1414a-ba67-43f6-bf7e-090828edd195">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate />

        <UI>
            <Error Id="25000">Error table Message #1.</Error>
        </UI>
        <Binary Id="CA" SourceFile="CA\CustomAction.dll"/>
        <CustomAction Id="cShowMsg" BinaryKey="CA" DllEntry="showMessages" Execute="immediate"/>
        <InstallExecuteSequence>
            <Custom Action="cShowMsg" After="InstallInitialize">NOT Installed</Custom>
        </InstallExecuteSequence>
        
        
		<Feature Id="ProductFeature" Title="Part18_02" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="Part18_02" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{EB49E325-3BB1-4998-BD93-0C559B770D10}">
                <File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

C言語側では、下記のようにMsiRecordSetInteger()関数を使ってerror numberをレコードに格納します。

```C++:CustomAction.cpp
#pragma comment(lib, "msi.lib")
#include <windows.h>
#include <msiquery.h>

extern "C" __declspec(dllexport) UINT showMessages(MSIHANDLE hInstall) {
    PMSIHANDLE hRecord = MsiCreateRecord(1);
    MsiRecordSetInteger(hRecord, 1, 25000);
    MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_ERROR | MB_OK), hRecord);
    return ERROR_SUCCESS;
}
```

これを実行すると、エラーテーブルに格納した文字列がダイアログボックスに表示されるのがわかります。
![part18useErrorTable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/77e6ad41-4de5-fa92-405e-1152b42723e7.png)

## ダイアログボックスに表示するアイコンの種類を変える

今まで、黄色い三角の「!」マークアイコンを表示していましたが、青い丸の「!」マークアイコンを表示できます。
![part18warningMessage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/22e2bf97-1c2f-230b-27c1-9a42f18a0cf3.png)
これを表示するには、MsiProcessMessage()関数の第2引数に`INSTALLMESSAGE_WARNING`を与えます。

```C++
MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_WARNING | MB_OK), hRecord);
```

また、アイコンなしにするには`INSTALLMESSAGE_USER`を与えます。
![part18noIcon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/545cff76-6d96-9b9f-497f-27ed534ebf9e.png)

```C++
MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER | MB_OK), hRecord);
```

ここまでで紹介したアイコンは、`INSTALLMESSAGE_ERROR `、`INSTALLMESSAGE_WARNING`、`INSTALLMESSAGE_USER`の既定のアイコン表示ですが、`MB_ICONERROR`、`MB_ICONQUESTION`、`MB_ICONWARNING`、`MB_ICONINFORMATION`等を指定することで、既定とは異なるアイコンを使うこともできます。例えば、下記のようにしてやれば、

```C++
MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER | MB_ICONQUESTION | MB_OK), hRecord);
```

「?」マークのアイコンを表示できます。
![part18questionIcon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/1f670f00-2fd4-878f-b85c-2004068e7065.png)

## ダイアログボックスのボタンの種類を変える

これまでOKボタン一つのダイアログボックスを表示し、ダイアログボックスを閉じるためにボタンを利用しました。しかし、[ここを読め](https://docs.microsoft.com/en-us/windows/win32/api/msiquery/nf-msiquery-msiprocessmessage#display-of-message-boxes)ばわかるように、２つ以上のボタンを表示して、ユーザーの意思を確認することができます。種類として使用できるのは、`MB_OK`、`MB_OKCANCEL`、`MB_ABORTRETRYIGNORE`、`MB_YESNOCANCEL`、`MB_YESNO`、`MB_RETRYCANCEL`です。ユーザーが押したボタンの判別は、[Win32 APIのMessageBox()関数と同じくReturn value](https://docs.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-messagebox#return-value)を使います。以下に`MB_OKCANCEL`の使用例を示します。一つ目の`MsiProcessMessage()`関数の第２引数に注目してください。

これを実行すると、[OK]ボタンと[Cancel]ボタンを備えたダイアログボックスを表示し、どちらかのボタンを押すと次に押したボタンの内容を示すダイアログボックスを表示します。

```C++:CustomAction.cpp
#pragma comment(lib, "msi.lib")
#include <windows.h>
#include <msiquery.h>

extern "C" __declspec(dllexport) UINT showMessages(MSIHANDLE hInstall) {
    char* msg = NULL;
    PMSIHANDLE hRecord = MsiCreateRecord(1);
    MsiRecordSetString(hRecord, 0, "Dialog Box Button test.");
    int ret = MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER | MB_OKCANCEL), hRecord);
    switch (ret) {
    case IDOK:
        msg = "IDOK";
        break;
    case IDCANCEL:
        msg = "IDCANCEL";
        break;
    case IDABORT:
        msg = "IDABORT";
        break;
    case IDRETRY:
        msg = "IDRETRY";
        break;
    case IDIGNORE:
        msg = "IDIGNORE";
        break;
    case IDYES:
        msg = "IDYES";
        break;
    case IDNO:
        msg = "IDNO";
        break;
    case IDTRYAGAIN:
        msg = "IDTRYAGAIN";
        break;
    case IDCONTINUE:
        msg = "IDCONTINUE";
        break;
    default:
        msg = "UNKNOWN";
        break;
    }
    MsiRecordSetString(hRecord, 0, msg);
    MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER | MB_OK), hRecord);
    return ERROR_SUCCESS;
}
```

以下に指定できるオプションと実際のダイアログボックスの表示、`MsiProcessMessage()`関数の戻り値の対応を示します。

|Option|ダイアログボックスの表示|MsiProcessMessage()<br>の戻り値|
|:--|:--|:--|
|MB_OK|![part18MB_OK.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/363aca46-74a4-636e-fd50-c3d954ad4e15.png)|IDOK|
|MB_OKCANCEL|![part18okCancel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/35a6f63f-d8f7-2687-7bfa-8c342f5aae7c.png)|IDOK<br>IDCANCEL|
|MB_ABORTRETRYIGNORE|![part18MB_ABORTRETRYIGNORE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/2bc58285-733a-2840-b649-ffffb154245b.png)|IDCANCEL<br>IDRETRY<br>IDIGNORE|
|MB_YESNOCANCEL|![part18MB_YESNOCANCEL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/2b8dce5e-0595-ea81-b066-10af0dc59ebe.png)|IDYES<br>IDNO<br>IDCANCEL|
|MB_YESNO|![part18MB_YESNO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/10a46383-e164-19dc-d044-f0a8a54a08ab.png)|IDYES<br>IDNO|
|MB_RETRYCANCEL|![part18MB_RETRYCANCEL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/39d7765d-9271-feb6-46d7-23344d0c96c8.png)|IDRETRY<br>IDCANCEL|

## デフォルトボタンの変更

ダイアログボックスが表示されたとき、最初にフォーカスを持っているボタンをデフォルトボタンと呼びます。フォーカスを持ったボタンは、SpaceキーやEnterキーを押せばすぐに反応しますので、インストーラーにとっては安全な方向に進むようデフォルトボタンを設定しておくべきです。このために、`MB_DEFBUTTON1`、`MB_DEFBUTTON2`、`MB_DEFBUTTON3`のオプションが用意されています。

|Option|用途|
|:--|:--|
|MB_DEFBUTTON1|１番左のボタンをデフォルトボタンに設定する|
|MB_DEFBUTTON2|左から２番目のボタンをデフォルトボタンに設定する|
|MB_DEFBUTTON3|左から３番目のボタンをデフォルトボタンに設定する|

例えば下記のように指定したとします。

```C++
MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER | MB_YESNO | MB_DEFBUTTON2), hRecord);
```

そうすると、左から２番目のボタンであるNOボタンにフォーカスが当たった状態でダイアログボックスが開きます。
![Part18defaultButton.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/6a1b491c-5186-fc13-a3be-b2d24e4db711.png)


[^1]: そういえばPascalの構造体はRecordですね！
[^2]: Windows InstallerではMSIテーブルの各行もレコードとして扱います。
[^3]: リンク先をよく見ると、関数名の末尾にAが付いたものとWが付いたものがあることが分かります。前者はAnsi、後者はWideのことですが、実際に関数を使うときには、AもWも付ける必要はありません。コンパイラが自動的にどちらか適切な方を呼んでくれます。
[^4]: そのため、ここで説明したような用途で`MsiCloseHandle()`関数を使うことはありません。

[**前の記事へ**](https://qiita.com/tohshima/items/626b0462be9ea010621e)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/bb1a8401a620654569cb)