Windows Installer手引書 Part.20 カスタムアクションとインストールの進捗表示

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

## カスタムアクションとインストールの進捗表示

カスタムアクションで時間のかかる処理を行うと、インストールの進捗を示すプログレスバーが止まってしまい、ユーザーを不安にします。このようなことを防ぐために、Windows Installerにはカスタムアクションからプログレスバーの進捗をコントロールする機能が備えられています。また同時に、現在行っている処理の内容を文字で伝えることもできるようになっています。

この機能は、マイクロソフトが[Adding Custom Actions to the ProgressBar](https://docs.microsoft.com/en-us/windows/win32/msi/adding-custom-actions-to-the-progressbar)のなかでサンプルプログラムを提示しているので、このサンプルプログラムを題材に解説していきます。

## まずはプログレスバー

プログレスバーの制御と処理内容のメッセージ表示は連携していて、プログレスバーを更新することで文字表示も更新されます。プログレスバー制御のためには、Message Typeに`INSTALLMESSAGE_PROGRESS`を指定し、下表[^1]の内容のレコードを合わせてWindows Installerに送ります。`Field 1`がコマンドの種類を表し、`Field 2`以降がその引数であると考えると理解し易いと思います。

|Message name|Field 1|Field 2|Field 3|Field 4|
|:--|:--|:--|:--|:--|
|MasterReset|0:プログレスバーをリセットし、バー全体のTicks数を設定する|プログレスバー全体の合計Ticks数|0:左から右に進める<br>1:右から左に戻す|0:実行を進める。この場合、UIは残り時間を計算可能となり時間が表示される<br>1:遅延実行の準備中であることを示す。この場合、UIは「準備が終わるまでお待ちください」のメッセージを表示する|
|ActionInfo|1:プログレスバーの進め方に関する情報を設定する|１つのActionDataメッセージが進めるTicks数|0:ProgressReportメッセージを送ることでプログレスバーを進める<br>1:ActionDataメッセージごとにField 2で指定したTicks数プログレスバーを進める|未使用|
|ProgressReport|2:プログレスバーを進める|進めたいプログレスバーのTicks数|未使用|未使用|
|ProgressAddition|3:プログレスバー全体のTicks数に指定したTicks数を加える|加えるTicks数|未使用|未使用|

使い方としては、
1. immediate（即時実行）で`ProgressAddition`を使ってプログレスバー全体のTicks数を増やしておき、
2. deferred（遅延実行）で`ActionInfo`を使ってプログレスバーの進め方を指定し、
3. 続けて`ProgressReport`で実際にプログレスバーを進める
となります。`ProgressReport`を続けて呼べば、プログレスバーは進み続けます。

[Adding Custom Actions to the ProgressBar](https://docs.microsoft.com/en-us/windows/win32/msi/adding-custom-actions-to-the-progressbar)のサンプルプログラムからプログレスバーを制御するところだけ抜き出し、２つの関数に分割したプログラムを示します。

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


const UINT iTickIncrement = 10000;
const UINT iNumberItems = 5;
const UINT iTotalTicks = iTickIncrement * iNumberItems;

// 即時実行用
extern "C" __declspec(dllexport) UINT CAProgressImm(MSIHANDLE hInstall) {
	PMSIHANDLE hProgressRec = MsiCreateRecord(2);

	// INSTALLMESSAGE_PROGRESS - ProgressAddition
	MsiRecordSetInteger(hProgressRec, 1, 3);
	MsiRecordSetInteger(hProgressRec, 2, iTotalTicks);
	UINT iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hProgressRec);
	if (iResult == IDCANCEL) {
		return ERROR_INSTALL_USEREXIT;
	}

	return ERROR_SUCCESS;
}

// 遅延実行用
extern "C" __declspec(dllexport) UINT CAProgressDef(MSIHANDLE hInstall)
{
	UINT iResult = 0;
	PMSIHANDLE hProgressRec = MsiCreateRecord(3);

	// INSTALLMESSAGE_PROGRESS - ActionInfo
	MsiRecordSetInteger(hProgressRec, 1, 1);
	MsiRecordSetInteger(hProgressRec, 2, 1);
	MsiRecordSetInteger(hProgressRec, 3, 0);
	iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hProgressRec);
	if (iResult == IDCANCEL) {
		return ERROR_INSTALL_USEREXIT;
	}

	// INSTALLMESSAGE_PROGRESS - ProgressReport
	MsiRecordSetInteger(hProgressRec, 1, 2);
	MsiRecordSetInteger(hProgressRec, 2, iTickIncrement);
	MsiRecordSetInteger(hProgressRec, 3, 0);
	for (int i = 0; i < iTotalTicks; i += iTickIncrement){
		iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hProgressRec);
		if (iResult == IDCANCEL) {
			return ERROR_INSTALL_USEREXIT;
		}
		Sleep(1000);
	}

	return ERROR_SUCCESS;
}
```

さきほどの表にある各Fieldは`MsiRecordSetInteger()`関数でレコードに格納され、`MsiProcessMessage()`関数でその内容をWindows Installerに送っています。ここでの処理手順を箇条書きにすると、次のようになります。`[]`の中はレコードのField値を示します。

1. CAProgressImm()関数でレコードに`[3, iTotalTicks]`を指定してプログレスバー全体のTicks数を増やしておき、
2. CAProgressDef()関数でレコードに`[1, 1, 0]`を指定してプログレスバーの進め方を指定し、
3. さらに、forループのなかで`[2, iTickIncrement, 0]`という内容のレコードをiTotalTicks回呼ぶことで少しずつバーを進めます

もし、これらのカスタムアクションの実行中にユーザーがダイアログボックスの[Cancel]ボタンを押すと、MsiProcessMessage()関数の戻り値に`IDCANCEL`が返されるので、エラー処理を入れてあります。

以下にこれらのカスタムアクションを利用するためのWiX Toolsetのソースを示します。`CustomAction`エレメントで示したように、`CAProgressImm()`関数は`immediate`で、`CAProgressDef()`関数は`deferred`で呼びます。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="4F1C3B72-AC4C-4B51-BA71-2F4D26CBF8C8" Name="Part20_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="8c4264af-7d6a-4199-b160-38c1902976e6">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

        <UIRef Id="WixUI_Mondo"/>
        <Binary Id="CA" SourceFile="CA\CustomAction.dll"/>
        <CustomAction Id="PreparationBar" BinaryKey="CA" DllEntry="CAProgressImm" Execute="immediate"/>
        <CustomAction Id="IncProgressBar" BinaryKey="CA" DllEntry="CAProgressDef" Execute="deferred"/>
        <InstallExecuteSequence>
            <Custom Action="PreparationBar" After="PublishProduct"></Custom>
            <Custom Action="IncProgressBar" After="PreparationBar"></Custom>
        </InstallExecuteSequence>

        <Feature Id="ProductFeature" Title="Part20_01" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

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

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="cExe" Guid="{06598ED1-4F44-43F9-AAC2-7C655DC52B5C}">
                <File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

これを実行すると、プログレスバーが後半で「少し進んでは止まり」という動きが観測できます。これがカスタムアクションのforループでバーを進めているところです。

## 処理内容のメッセージ

カスタムアクション実行時にプログレスバーが動いていれば、「あぁ、何かやってるな」とユーザーに多少の安心感を与えることができますが、さらに進めて何をやっているか文字で情報を示すことができます。文字情報を表示するためには、始めにMessage Typeに`INSTALLMESSAGE_ACTIONSTART`を指定して下表の内容のレコードで、表示したい文字フォーマットをWindows Installerに送ります（詳細は[Sending Messages to Windows Installer Using MsiProcessMessage](https://docs.microsoft.com/en-us/windows/win32/msi/sending-messages-to-windows-installer-using-msiprocessmessage)を参照）。

|Field 1|Field 2|Field 3|
|:--|:--|:--|
|アクション名|表示する文字情報（ActionTextイベントで送られる）|ACTIONDATAメッセージ用のテンプレート。[数値]の書式でメッセージに埋め込むと、実行時に[数値]の部分をINSTALLMESSAGE_ACTIONDATAで送るFieldの内容に置き換えることができる。例えば[1]とした部分は、レコードのFileld 1で設定した数字か文字列の内容に置き換わる。（ActionDataイベントで送られる）|

そして、Message Typeに`INSTALLMESSAGE_ACTIONDATA`を指定してMsiProcessMessage()関数を使うことで文字情報を更新していきます。INSTALLMESSAGE_ACTIONDATAで送るレコードは、INSTALLMESSAGE_ACTIONSTARTを指定して送った上表のレコードのField 3のテンプレートの[数字]部分を特定の数字や文字に置き換えるために使用します。



先ほどのカスタムアクションのコードに、文字情報を表示する機能を加えたコードを以下に示します。ここでは、Field 3で与えるテンプレートで、[1], [2]を指定し、そこにMsiRecordSetInteger()関数で数値を入れていますが、[3], [4],...と増やしていったり、MsiRecordSetString()関数を使って文字を入れることも可能です。

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


const UINT iTickIncrement = 10000;
const UINT iNumberItems = 5;
const UINT iTotalTicks = iTickIncrement * iNumberItems;

// 即時実行用
extern "C" __declspec(dllexport) UINT CAProgressImm(MSIHANDLE hInstall) {
	PMSIHANDLE hProgressRec = MsiCreateRecord(2);

	// INSTALLMESSAGE_PROGRESS - ProgressAddition
	MsiRecordSetInteger(hProgressRec, 1, 3);
	MsiRecordSetInteger(hProgressRec, 2, iTotalTicks);
	UINT iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hProgressRec);
	if (iResult == IDCANCEL) {
		return ERROR_INSTALL_USEREXIT;
	}
	return ERROR_SUCCESS;
}

// 遅延実行用
extern "C" __declspec(dllexport) UINT CAProgressDef(MSIHANDLE hInstall)
{
	UINT iResult = 0;
	PMSIHANDLE hActionRec = MsiCreateRecord(3);
	PMSIHANDLE hProgressRec = MsiCreateRecord(3);

	// INSTALLMESSAGE_ACTIONSTART
	MsiRecordSetString(hActionRec, 1, TEXT("MyCustomAction"));
	MsiRecordSetString(hActionRec, 2, TEXT("Incrementing the Progress Bar..."));
	MsiRecordSetString(hActionRec, 3, TEXT("Incrementing tick [1] of [2]"));
	iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_ACTIONSTART, hActionRec);
	if (iResult == IDCANCEL) {
		return ERROR_INSTALL_USEREXIT;
	}

	// INSTALLMESSAGE_PROGRESS - ActionInfo
	MsiRecordSetInteger(hProgressRec, 1, 1);
	MsiRecordSetInteger(hProgressRec, 2, 1);
	MsiRecordSetInteger(hProgressRec, 3, 0);
	iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hProgressRec);
	if (iResult == IDCANCEL) {
		return ERROR_INSTALL_USEREXIT;
	}

	// INSTALLMESSAGE_PROGRESS - ProgressReport
	MsiRecordSetInteger(hProgressRec, 1, 2);
	MsiRecordSetInteger(hProgressRec, 2, iTickIncrement);
	MsiRecordSetInteger(hProgressRec, 3, 0);

	for (int i = 0; i < iTotalTicks; i += iTickIncrement){

		// INSTALLMESSAGE_ACTIONDATA
		MsiRecordSetInteger(hActionRec, 1, i);
		MsiRecordSetInteger(hActionRec, 2, iTotalTicks);
		iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_ACTIONDATA, hActionRec);
		if (iResult == IDCANCEL) {
			return ERROR_INSTALL_USEREXIT;
		}

		// INSTALLMESSAGE_PROGRESS - ProgressReport
		iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hProgressRec);
		if (iResult == IDCANCEL) {
			return ERROR_INSTALL_USEREXIT;
		}
		Sleep(1000);
	}
	return ERROR_SUCCESS;
}
```

このコードを先ほどの`Product.wxs`から呼ぶと、`INSTALLMESSAGE_ACTIONSTART`を指定して送ったレコードのうちField 2は表示されるのに、一番凝って実装したField 3が表示されないと思います。なぜかというと、WiX ToolsetのUIExtensionが提供するProgressDlgには、ActionDataイベントを受けて表示する機能がないからです。この機能を追加する方法はいくつか考えられますが、最も作業量が少ないのはMSIファイルのテーブルに直接項目を追加する方法です。まず、`EventMapping`テーブルに下図のようにActionDataイベントを追加します。
![Part20_02_EventMapping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/2e7b004a-ed30-0045-b039-b82ed806ac85.png)
さらに、イベントで受けた文字をダイアログに表示できるよう、`Control`テーブルに下図のようにTextコントロールを追加します。
![Part20_02_Control.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/843e6816-1032-cfc4-756f-f61724f8a196.png)
Textコントロールはプログレスバーの下に表示されるようにしました。
![Part20_02_ProgressDlg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/c402a33f-f340-7cd6-ce9e-6845b12cd38f.png)
しかし、ビルドのたびにOrcaで項目を追加するのは手間です。そこで、[Microsoft Windows SDK for Windows 7 and .NET Framework 4](https://www.microsoft.com/en-us/download/details.aspx?id=8279)に同梱されているMSI向けのスクリプト[^2]（"C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts\WiRunSQL.vbs"）を使ってテーブルに行を加えます[^3]。下記のバッチファイルにMSIファイルをDrag&Dropしてくるか、バッチファイルの引数にMSIファイルへのフルパスを与えて実行すれば、テーブルに必要な行が追加されます。

```Batchfile:addTableItem.bat
@echo off
set MSINAME=%1
set MSITOOL=C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts
rem 
set cmdLine=cscript //Nologo "%MSITOOL%\WiRunSQL.vbs" %MSINAME% "INSERT INTO `EventMapping` (`EventMapping`.`Dialog_`,`EventMapping`.`Control_`,`EventMapping`.`Event`,`EventMapping`.`Attribute`) VALUES ('ProgressDlg','ActionData','ActionData','Text')"
echo %cmdLine%
%cmdLine%
rem 
set cmdLine=cscript //Nologo "%MSITOOL%\WiRunSQL.vbs" %MSINAME% "INSERT INTO `Control` (`Control`.`Dialog_`,`Control`.`Control`,`Control`.`Type`,`Control`.`X`,`Control`.`Y`,`Control`.`Width`,`Control`.`Height`,`Control`.`Attributes`) VALUES ('ProgressDlg','ActionData','Text',20,130,285,10,3)"
echo %cmdLine%
%cmdLine%
pause
```

Visual Studioを使っているなら、ビルド後にこのバッチファイルを呼ぶようにしておけば、面倒がありません。
![Part20_02_PostBuild.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/e138394b-716c-a10d-da9b-48689809aaae.png)


[^1]: [Record Fields for Progress Bar Messages](Record Fields for Progress Bar Messages)の説明を1つの表に再構成し、[Session.Message method](https://docs.microsoft.com/en-us/windows/win32/msi/session-message#remarks)からMessage nameを持ってきました。
[^2]: ここに置いてあるスクリプトの説明は[Windows Installer Scripting Examples](https://docs.microsoft.com/en-us/windows/win32/msi/windows-installer-scripting-examples)にあります。Examplesとなっていますが、かなりちゃんと作りこまれているので実用的であり、製品開発にそのまま使っても差し支えないと思います。
[^3]: 今回は解説しませんが、SQL文を使ってMSIのテーブルを操作する方法は、[SQL Syntax](https://docs.microsoft.com/en-us/windows/win32/msi/sql-syntax)や[Examples of Database Queries Using SQL and Script](https://docs.microsoft.com/en-us/windows/win32/msi/examples-of-database-queries-using-sql-and-script)が参考になります。

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