WixToolsetで、MSIにカスタムダイアログを挿入する

# はじめに

WixToolsetでは、インストーラーのUIとして公式で拡張を出している。
この公式拡張UIは、[ビルド時にプロパティを指定することにより、背景を変更したり、
特定の文言を変更することはできる。](https://wixtoolset.org/docs/tools/wixext/wixui/)
しかし、丸ごと新しいダイアログを挿入したり、画面遷移を、入れ替えるといったことはプロパティではできず、もう少し複雑な手順を踏む必要がある。
この記事では、MSIにカスタムダイアログを作成して挿入する方法を記述する。
公式にも説明は存在するが、それだけでは足りなかったのでここで書く。
WixToolsetの概要に関しては下記関連記事で。
https://qiita.com/skitoy4321/items/194888be042e5c4c32ad

なお、WixToolsetとしては、UIが複雑になる場合は[Burnフレームワーク](https://wixtoolset.org/docs/tools/burn/)を使い、MSI自体のUIは使わないことを推奨している模様なので、複雑なことをしたくなったらこの辺りを検討してみるのもいいかもしれない。

# 大まかな手順

1. Wixプロジェクトの作成
2. Wixプロジェクトに[WixToolset.UI.wixext](https://www.nuget.org/packages/WixToolset.UI.wixext/)の導入
3. [wixtoolsetリポジトリのUI拡張ライブラリ](https://github.com/wixtoolset/wix/tree/v4.0.1/src/ext/UI/wixlib)から下記ファイルの抜き出し
    * WixUI_*.wxsのどれか
    * ひな形にしたいダイアログのwxsファイル
4. 抜き出したファイルの修正
5. メインのPackage要素への追加

今回は手間削減のためwixtoolsetリポジトリからの抜き出しをしているが、気合で一から作ることも可能ではある。

# 抜き出したファイルの修正ポイント

## ダイアログのwxsファイル

基本的に[`/Wix/Fragment/UI/Dialog`](https://wixtoolset.org/docs/schema/wxs/dialog/)を宣言して記述していく。

まずDialog要素に下記属性を設定する

* Id: 後で指定するために必要、プロジェクト内でユニークである必要がある
* Width, Height: それぞれ幅と高さで、ピクセル単位で指定する
* Title: ウィンドウタイトル

更にその下に[Control](https://wixtoolset.org/docs/schema/wxs/control/)を配置していく。
共通項目として下記を設定する。

* Id: 後で指定するために必要、プロジェクト内でユニークである必要がある
* Width, Height: 幅と高さ
* X, Y: コントロールの基点、左上が基準点
* Type: コントロールのタイプ(ボタン、テキストボックス等)
    * 使用可能なものは[MSIのControlの仕様に依存する](https://learn.microsoft.com/en-us/windows/win32/msi/controls)

ウィンドウの大きさ、各コントロールの大きさ等は直接数値で指定していくことになるので、適宜計算する必要がある。
現時点ではプレビュー機能等は無いので、実際にビルドして確認する必要がある。

通常のダイアログ例としては[InstallDirDlg](https://github.com/wixtoolset/wix/blob/v4.0.1/src/ext/UI/wixlib/InstallDirDlg.wxs)が参考になる。
`!(loc.[ID])`という記述があるが、これはローカライズでwxlで指定したIDを使うことにより、ビルド時の言語設定に基づいたテキストに置き換わるものとなる。

## WixUI_*.wxs

このファイルでは、個別ダイアログをどのように遷移させるかという部分を記述していく。
良く使われるであろう[WixUI_InstallDir.wxs](https://github.com/wixtoolset/wix/blob/v4.0.1/src/ext/UI/wixlib/WixUI_InstallDir.wxs)を元に解説していく。

### WixUI_InstallDir.wxsの構成

WixUI_InstallDir.wxsは、アーキテクチャ依存の部分と共通部分に分かれている。
プリプロセッサブロック(`<?foreach WIXUIARCH ...?><?endforeach?>`)で囲まれている部分がそれとなる。
これはv4からとなるが、サフィックスにアーキテクチャを追加することで、wixの方でビルド時に場合分けを行ってくれるようになる。
主にカスタムアクションで実行するDLLが異なる場合等に使用する。

### 名前の変更

まず独自のWixUIを使用する場合、既存の拡張パッケージが提供するものと重複してはならないので、名前を変更する。
`/Wix/Flagment/UI@Id`を変更する。
このId値が、Package内部で指定するIDとなる。
また、`/Wix/Flagment/UIRef@Id="WixUI_InstallDir"`となっている部分も、新しい名前に変更する。
拡張の方で"WixUI_"というプレフィックスは特別扱いされているようなので、"WixUI_"部分は残しておくこと。

### ダイアログの表示順と条件の修正

上記で作成したDialogを、UI要素の中でDialogRefすると、そのUIの中でダイアログが使用可能になる。
WixというかMSIでは、ボタンを押した、ツリーを選択した等の時に発生したイベントの動作を[Publish](https://wixtoolset.org/docs/schema/wxs/publish/)で指定する。
Publishで指定する属性の値は、共通で以下の値を設定する。

* "Dialog": ダイアログ名
* "Control": イベント発生元のコントロール名
* "Order": 順番、小さい順に実行され、記述が無い場合は"1"として扱われる
    * 同一の場合は、先に宣言されたものから実行される
* "Condition": Publish要素が実行される条件、デフォルトは常に実行

次に、やりたいことによって以下のように分かれる

* MSIプロパティを設定する
    * "Property"にプロパティ名、"Value"に値を入れる
* カスタムアクションを実行する
    * "Event"に"DoAction"、"Value"にカスタムアクション名を指定する
* モーダルダイアログを表示する(元の画面は保持する)
    * "Event"に"SpawnDialog"、"Value"にダイアログ名を指定する
* 新しい画面に遷移する
    * "Event"に"NewDialog"、"Value"にダイアログ名を指定する
* ダイアログを閉じる
    * "Event"に"EndDialog"を指定し、"Value"には[EndDialogの仕様に従う](https://learn.microsoft.com/en-us/windows/win32/msi/enddialog-controlevent)

他、Event値に使える値としては、[MSIのControlEventの仕様に従う](https://learn.microsoft.com/en-us/windows/win32/msi/controlevent-overview)。
MSIの定石として、画面の遷移はNewDialogとConditionの制御で、リンクリストのように各ダイアログの遷移を表現する。

例えば、ライセンス同意画面とインストールフォルダ決定画面の間に挟みたい場合、[LicenseAgreementDlgのNextボタン](https://github.com/wixtoolset/wix/blob/6e6eb47812742cfb61a1c3328ba662ec2886a4e2/src/ext/UI/wixlib/WixUI_InstallDir.wxs#L62)を以下のようにする
```xml
<Publish 
  Dialog="LicenseAgreementDlg" 
  Control="Next"
  Event="NewDialog" 
  Value="[カスタムダイアログID]" 
  Condition="LicenseAccepted = &quot;1&quot;" />
```

そして、[InstallDirDlgのBackボタンイベント](https://github.com/wixtoolset/wix/blob/6e6eb47812742cfb61a1c3328ba662ec2886a4e2/src/ext/UI/wixlib/WixUI_InstallDir.wxs#L64)を、以下のように書き換える
```xml
<Publish
  Dialog="InstallDirDlg" 
  Control="Back" 
  Event="NewDialog" 
  Value="[カスタムダイアログID]" />
```

更に、以下二行をUI配下に置く
```xml
<Publish Dialog="[カスタムダイアログID]" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
<Publish Dialog="[カスタムダイアログID]" Control="Next" Event="NewDialog" Value="InstallDirDlg" />
```

これでカスタムダイアログが挿入できたことになる。

## プロパティの間接参照

例えば、ファイルパス選択ダイアログのようなものを想定する。
普通考えられる方法としては、SpawnDialogした画面の中で何らかのパスを入力した後"OK"ボタンを押してプロパティを入力内容の値でセットするというものになるが、普通のプロパティでは、プロパティ名が決め打ち状態になるため、ライブラリ化したい場合等は都合が悪い。

具体的にどのように書くかというのは、[BrowseDlg](https://github.com/wixtoolset/wix/blob/v4.0.1/src/ext/UI/wixlib/BrowseDlg.wxs)が参考になる。
これは指定プロパティにディレクトリパスを入れるものになるが、Controlの属性にIndirect="yes"を設定することにより、"_BrowseProperty"に設定されたプロパティを間接参照することができる。

例えば、BrowseDlgをSpawnDialogする直前、"_BrowseProperty"に何らかのプロパティを設定し、BrowseDlgで"OK"を押すと、"_BrowseProperty"に設定されていたプロパティの値が書き換わるということになる。
WixUI側では[WixUI_InstallDirの該当行](https://github.com/wixtoolset/wix/blob/6e6eb47812742cfb61a1c3328ba662ec2886a4e2/src/ext/UI/wixlib/WixUI_InstallDir.wxs#L68-L69)の書き方が参考になる。
ChangeFolderボタンを押すと、

1. _BrowsePropertyから"WIXUI_INSTALLDIR"プロパティを間接参照する
2. BrowseDlgダイアログをSpawnDialogする

というように処理される。

# Package側での参照

WixUIの記述ができたら、Package要素でそれを参照する。
[ui:WixUI](https://wixtoolset.org/docs/schema/ui/wixui/)を`Wix/Package`以下に追加する。
WixToolset.UI.wixextパッケージは導入しておくこと
例:
```xml
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
    xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
  <Package Name="customdlg" Manufacturer="TODO Manufacturer" Version="1.0.0.0" UpgradeCode="[put-guid-here]">
    <!-- 途中の要素は省略 -->
    <!-- 参考にしたWixUI_InstallDirではInstallDirectoryの設定が必要 -->
    <ui:WixUI Id="[UI@Idで設定した値]" InstallDirectory="INSTALLFOLDER"/>
  </Package>
</Wix>
```

# 終わりに

カスタムダイアログの挿入手順を書いたが、MSIの仕様に由来する部分はあるものの、実際面倒な所はある。
ただ、Burn自体の動作で要件をカバーできない場合や、自前ブートストラッパーを作るのが難しい等、
様々な制約からMSIの仕組みを使ったUIを構築するという手は捨てきれないので、何かの参考になればと思う。