LoginSignup
3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-15

前の記事へ  目次へ  次の記事へ

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

カスタムアクションで時間のかかる処理を行うと、インストールの進捗を示すプログレスバーが止まってしまい、ユーザーを不安にします。このようなことを防ぐために、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数 未使用 未使用

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

Adding Custom Actions to the ProgressBarのサンプルプログラムからプログレスバーを制御するところだけ抜き出し、2つの関数に分割したプログラムを示します。

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で呼びます。

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を参照)。

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()関数を使って文字を入れることも可能です。

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
さらに、イベントで受けた文字をダイアログに表示できるよう、Controlテーブルに下図のようにTextコントロールを追加します。
Part20_02_Control.png
Textコントロールはプログレスバーの下に表示されるようにしました。
Part20_02_ProgressDlg.png
しかし、ビルドのたびに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ファイルへのフルパスを与えて実行すれば、テーブルに必要な行が追加されます。

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

前の記事へ  目次へ  次の記事へ


  1. Record Fields for Progress Bar Messagesの説明を1つの表に再構成し、Session.Message methodからMessage nameを持ってきました。 

  2. ここに置いてあるスクリプトの説明はWindows Installer Scripting Examplesにあります。Examplesとなっていますが、かなりちゃんと作りこまれているので実用的であり、製品開発にそのまま使っても差し支えないと思います。 

  3. 今回は解説しませんが、SQL文を使ってMSIのテーブルを操作する方法は、SQL SyntaxExamples of Database Queries Using SQL and Scriptが参考になります。 

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1