カスタムアクションとインストールの進捗表示
カスタムアクションで時間のかかる処理を行うと、インストールの進捗を示すプログレスバーが止まってしまい、ユーザーを不安にします。このようなことを防ぐために、Windows Installerにはカスタムアクションからプログレスバーの進捗をコントロールする機能が備えられています。また同時に、現在行っている処理の内容を文字で伝えることもできるようになっています。
この機能は、マイクロソフトが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:左から右に進める 1:右から左に戻す |
0:実行を進める。この場合、UIは残り時間を計算可能となり時間が表示される 1:遅延実行の準備中であることを示す。この場合、UIは「準備が終わるまでお待ちください」のメッセージを表示する |
ActionInfo | 1:プログレスバーの進め方に関する情報を設定する | 1つのActionDataメッセージが進めるTicks数 | 0:ProgressReportメッセージを送ることでプログレスバーを進める 1:ActionDataメッセージごとにField 2で指定したTicks数プログレスバーを進める |
未使用 |
ProgressReport | 2:プログレスバーを進める | 進めたいプログレスバーのTicks数 | 未使用 | 未使用 |
ProgressAddition | 3:プログレスバー全体のTicks数に指定したTicks数を加える | 加えるTicks数 | 未使用 | 未使用 |
使い方としては、
- immediate(即時実行)で
ProgressAddition
を使ってプログレスバー全体のTicks数を増やしておき、 - deferred(遅延実行)で
ActionInfo
を使ってプログレスバーの進め方を指定し、 - 続けて
ProgressReport
で実際にプログレスバーを進める
となります。ProgressReport
を続けて呼べば、プログレスバーは進み続けます。
Adding Custom Actions to the ProgressBarのサンプルプログラムからプログレスバーを制御するところだけ抜き出し、2つの関数に分割したプログラムを示します。
#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値を示します。
- CAProgressImm()関数でレコードに
[3, iTotalTicks]
を指定してプログレスバー全体のTicks数を増やしておき、 - CAProgressDef()関数でレコードに
[1, 1, 0]
を指定してプログレスバーの進め方を指定し、 - さらに、forループのなかで
[2, iTickIncrement, 0]
という内容のレコードをiTotalTicks回呼ぶことで少しずつバーを進めます
もし、これらのカスタムアクションの実行中にユーザーがダイアログボックスの[Cancel]ボタンを押すと、MsiProcessMessage()関数の戻り値にIDCANCEL
が返されるので、エラー処理を入れてあります。
以下にこれらのカスタムアクションを利用するためのWiX Toolsetのソースを示します。CustomAction
エレメントで示したように、CAProgressImm()
関数はimmediate
で、CAProgressDef()
関数はdeferred
で呼びます。
<?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を参照)。
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()関数を使って文字を入れることも可能です。
#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イベントを追加します。
さらに、イベントで受けた文字をダイアログに表示できるよう、Control
テーブルに下図のようにTextコントロールを追加します。
Textコントロールはプログレスバーの下に表示されるようにしました。
しかし、ビルドのたびにOrcaで項目を追加するのは手間です。そこで、Microsoft Windows SDK for Windows 7 and .NET Framework 4に同梱されているMSI向けのスクリプト2("C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\sysmgmt\msi\scripts\WiRunSQL.vbs")を使ってテーブルに行を加えます3。下記のバッチファイルにMSIファイルをDrag&Dropしてくるか、バッチファイルの引数にMSIファイルへのフルパスを与えて実行すれば、テーブルに必要な行が追加されます。
@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を使っているなら、ビルド後にこのバッチファイルを呼ぶようにしておけば、面倒がありません。
-
[Record Fields for Progress Bar Messages](Record Fields for Progress Bar Messages)の説明を1つの表に再構成し、Session.Message methodからMessage nameを持ってきました。 ↩
-
ここに置いてあるスクリプトの説明はWindows Installer Scripting Examplesにあります。Examplesとなっていますが、かなりちゃんと作りこまれているので実用的であり、製品開発にそのまま使っても差し支えないと思います。 ↩
-
今回は解説しませんが、SQL文を使ってMSIのテーブルを操作する方法は、SQL SyntaxやExamples of Database Queries Using SQL and Scriptが参考になります。 ↩