Приветствую!

В этой статье я покажу вам свой вариант использования набора утилит WixToolSet для создания кастомных диалоговых окон с возможностью получения предустановленной информации (пароли, явки и прочие параметры). И приведу пример использования кастомных окон в простом кейсе.


Тем, кому лениво читать, предлагаю ознакомиться с проектом на github.

Проект состоит из двух частей:

  1. Установщик MSI — установщик основного продукта, в задачи которого входит:
    a.  запросить пароли пользователя и рута;
    b. скопировать скрипт в назначенную папку и запустить его с параметрами, по результату работы которого мы получаем файл с паролями;
    c. открыть блокнотом итоговый файл.

  2. Установщик EXE — альтернативный установщик, который позволяет предварительно установить все необходимые компоненты для работы основного продукта, а после и сам продукт.

  3. Его задачи и то, что мы делаем:
    a. устанавливаем необходимые компоненты (в качестве примера представлен код тихой установки MSSQL Server 2016);
    b. запрашиваем пароли пользователя и рута;
    c. запускаем установщик основного продукта, где полученные пароли пользователя и рута передаем в качестве параметров (установщик основного продукта запускается в тихом режиме, без диалоговых окон);
    d. установщик основного продукта выполняет все действия, указанные в п.1, за исключением п.1.а.

Я не буду заострять внимание на основах создания проектов windows installer и executable packages, а также описывать связи между ними, так как тема данной статьи — кастомизация. Считаю, что данного кейса для демонстрации возможностей оных вполне хватает. Кто столкнулся с набором утилит впервые, предлагаю рабочий проект на github и несколько полезных ссылок в конце статьи. Поехали!

Кастомизация пакетного установщика MSI

По сценарию работы основного продукта у пользователя необходимо запросить пароли пользователя и рута, в этом нам поможет кастомное диалоговое окно.

Рисунок №1
Рисунок №1

Для этого создаем в проекте файл PasswordDlg.wxs со следующим содержанием:

<!-- Перевод для ссылок вида !(loc.name) указываем в файле локализации Variables.wxl
     Стили формата {\WixUI_Font_Name} описываются в файле сценария MyWixUI_InstallDir.wxs -->
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
 <Fragment>
  <UI>
    <!-- Задаем баннер, файл которого расположен в корне проекта. Требования к размерам картинки можно посмотреть тут -->
    <Binary Id="BannerBmp" SourceFile="Banner.bmp" />
    <!-- Задаем имя своего окна Id="PasswordDlg" и указываем название в поле Title -->
    <Dialog Id="PasswordDlg" Width="370" Height="270" Title="!(loc.PasswordDialogTitle)">
    <!-- Задаем оглавление окна, используем координаты X и Y, где центр оси левый верхний угол, и размер блока W,H -->
      <Control Id="Title" Type="Text" X="15" Y="6" Width="160" Height="15" Transparent="yes" NoPrefix="yes" Text="{\WixUI_Font_Title}!(loc.PasswordTitle)" />
    <!-- Задаем описание окна -->
      <Control Id="Description" Type="Text" X="25" Y="23" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.PasswordTitleDescription)"  	NoWrap="no"/>
    <!-- Задаем banner, где в поле Text указывается Bynary Id, объявленный выше -->
      <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="BannerBmp" />
      <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
    <!-- Выводим информацию-пояснение к диалоговому окну -->
      <Control Id="DialogInfo" Type="Text" X="20" Y="60" Width="228" Height="45" Text="!(loc.PasswordDialogInfo)" TabSkip="yes" Transparent="yes" />
    <!-- Задаем поле для ввода пароля пользователя и привязываем к полю параметр ProperPasswordUser -->
      <Control Id="LabelPwdUser" Type="Text" X="20" Y="100" Height="17" Width="95" Transparent="yes" Text="!(loc.LabelPasswordUser)" />
      <Control Id="EditPwdUser" Type="Edit" X="100" Y="97"  Height="17" Width="150" Property="ProperPasswordUser" />
    <!-- Задаем поле для ввода пароля рута и привязываем к полю параметр ProperPasswordRoot -->
      <Control Id="LabelPwdRoot" Type="Text" X="20" Y="120" Height="17" Width="95" Transparent="yes" Text="!(loc.LabelPasswordRoot)" />
      <Control Id="EditPwdRoot" Type="Edit" X="100" Y="117"  Height="17" Width="150" Property="ProperPasswordRoot" />
    <!-- Выводим пояснение о требованиях к сложности пароля для пользователя -->
      <Control Id="DialogDefaultPwdInfo" Type="Text" X="20" Y="150" Width="300" Height="40" Text="!(loc.PasswordDefaultPwdDescription)" TabSkip="yes" Transparent="yes" Disabled="yes" />
    <!-- Рисуем разделительную линию и функциональные кнопки Назад, Далее и Отмена -->
      <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
      <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.PasswordButtonNext)" />
      <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.PasswordButtonBack)" />
      <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.PasswordButtonCancel)">
        <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
      </Control>
    </Dialog>
  </UI>
  </Fragment>
</Wix>

В качестве локализации у нас будет выступать файл Variables.wxl со следующим содержанием:

<!-- Тут, я думаю, понятно все и без комментариев -->
<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="ru-RU" xmlns="http://schemas.microsoft.com/wix/2006/localization">
  <String Id="PasswordTitle">Настройка сервера</String>
  <String Id="PasswordTitleDescription">Укажите информацию и нажмите кнопку "Далее"</String>
  <String Id="PasswordDialogTitle">Установка [ProductName]</String>
  <String Id="PasswordDialogInfo">Укажите пароли пользователей базы данных:</String>
  <String Id="PasswordDefaultPwdDescription">Для обеспечения инфоромационной безопасности и сохранности корпоротивной информации, необходимо заменить пароли на более сложные</String>
  <String Id="PasswordEnter">Введите пароль:</String>
  <String Id="PasswordButtonNext">Далее</String>
  <String Id="PasswordButtonBack">Назад</String>
  <String Id="PasswordButtonCancel">Отмена</String>

  <String Id="LabelUserName">Пользователь:</String>
  <String Id="LabelPasswordUser">Пароль для пользователя:</String>
  <String Id="LabelPasswordRoot">Пароль для рута:</String>
</WixLocalization>

Далее, нам необходимо создать свой сценарий диалоговых окон (далее - сценарий), куда будет добавлено новое окно для запроса пароля пользователя и рута. В нашем проекте мы используем сценарий WixUI_InstallDir, файл которого расположен по пути: C:\Program Files (x86)\WiX Toolset v3.11\SDK\wixui\WixUI_InstallDir.wxs.

Копируем этот файл в наш проект, предварительно переименовав в MyWixUI_InstallDir.wxs.

Набор утилит WixToolSet предоставляет несколько стандартных сценариев диалоговых окон. Список всех доступных сценариев можно посмотреть тут.

Как вы поняли из названия, сценарий представляет собой цепочку из диалоговых окон с описанием действий при нажатии на кнопки: «Далее», «Назад», «Отмена» и т.д. Нам остается всего лишь вставить свое окно в цепочку и указать ссылки на предыдущее и следующее окна.

Полный список доступных диалоговых окон можно посмотреть тут.

Содержание файла MyWixUI_InstallDir.wxs у нас следующее:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
      <!-- Даем новое имя нашему сценарию -->
      <UI Id="MyWixUI_InstallDir">
        <!-- Определяем стили текста, WixUI_Font_Title также используется в PasswordDlg.wxs -->WixUI_Font_Title используется в PasswordDlg.wxs
        <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
        <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
        <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
        <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
        <Property Id="WixUI_Mode" Value="InstallDir" />
        <!-- Перечень ссылок на вспомогательные диалоговые окна -->
        <DialogRef Id="BrowseDlg" />
        <DialogRef Id="DiskCostDlg" />
        <DialogRef Id="ErrorDlg" />
        <DialogRef Id="FatalError" />
        <DialogRef Id="FilesInUse" />
        <DialogRef Id="MsiRMFilesInUse" />
        <DialogRef Id="PrepareDlg" />
        <DialogRef Id="ProgressDlg" />
        <DialogRef Id="ResumeDlg" />
        <DialogRef Id="UserExit" />
        <!-- Далее, мы публикуем диалоговые окна в определенном порядке, параметры публикации описаны тут, а условия тут -->
        <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
        <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
        <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
        
        <!-- Начало сценария, окно приветствия -->
        <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">NOT Installed</Publish>
        <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
        <!-- Для окна лицензионного соглашения изменяем переход на PasswordDlg -->
        <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
        <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="PasswordDlg">LicenseAccepted = "1"</Publish>
        <!-- Добавляем наше диалоговое окно, при этом меняем значение Value для перехода на InstallDirDlg и возврата к LicenseAgreementDlg →

        <Publish Dialog="PasswordDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
        <Publish Dialog="PasswordDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">1</Publish>
        <!-- Для окна выбора пути установки изменяем возврат к PasswordDlg -->
        <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="PasswordDlg">1</Publish>
        <!-- Дальше можно ничего не трогать -->
        <Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
        <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
        <Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
        <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
        <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
        <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
        <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
        <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
        <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
        <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
        <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
        <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
        <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
        <Property Id="ARPNOMODIFY" Value="1" />
        </UI>
        <UIRef Id="WixUI_Common" />
    </Fragment>
</Wix>

Теперь осталось добавить наш новый сценарий, картинки баннера и хедера в проект, для этого в файле Product.wxs указываем:

<!-- Тут указываем свою цепочку диалоговых окон -->
<UIRef Id="MyWixUI_InstallDir"/>
<!-- Тут указываем картинку для баннера -->
<WixVariable Id="WixUIBannerBmp" Value="Banner.bmp" />
<!-- Тут указываем картинку для хедера диалогового окна -->
<WixVariable Id="WixUIDialogBmp" Value="Dialog.bmp" />

На этом вся работы выполнена, можно запускать проект и наслаждаться результатом.

Рисунок №2
Рисунок №2

Кастомизация исполняемого установщика EXE

Основная цель использования альтернативного установщика EXE - инсталляция дополнительных компонентов для работы основного продукта. Также для удобства мы запросим пароли пользователя и рута, передадим их в качестве параметров в установщик MSI, который запускается в тихом режим, без демонстрации диалоговых окон.

Для наших целей нам подойдет кастомизация окна опций установщика MSI, где в качестве шаблона установщика мы используем RtfLargeTheme.xml и файл локализации RtfTheme.wxl.

Копируем эти файлы в наш проект, предварительно переименовав в MyRtfLargeTheme.xml и MyRtfTheme.wxl, соответственно. Файлы расположены тут: C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes.

Чтобы попасть в опции необходимо нажать кнопку “Опции” в окне приветствия, после запуска установщика EXE, далее нам откроется окно запроса паролей пользователя и рута, как показано на рисунке 3.

Рисунок №3
Рисунок №3

Для получения такого окна необходимо изменить файл шаблона MyRtfLargeTheme.xml, разделы <Page Name=“Install“> и <Page Name=“Options“>, содержание которого выглядит так:

<!--Перевод для ссылок вида #(loc.name) указываем в файле локализации, обратите внимание на то, что в проекте MSI ссылки были вида !(loc.name)-->
<!--Описываем окно Приветствия, добавляем версию проекта и кнопку Опции-->
<Page Name="Install">
    <Text X="11" Y="80" Width="-11" Height="-70" TabStop="no" FontId="2" HexStyle="0x800000" DisablePrefix="yes" />
    <Richedit Name="EulaRichedit" X="12" Y="81" Width="-12" Height="-71" TabStop="yes" FontId="0" />
        <Text Name="InstallVersion" X="11" Y="-41" Width="210" Height="17" FontId="3" DisablePrefix="yes" HideWhenDisabled="yes">#(loc.InstallVersion)</Text>
        <Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
        <!--Добавляем кнопку Опции, где и буду запрашиваться наши параметры-->
        <Button Name="OptionsButton" X="-191" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
        <Button Name="InstallButton" X="-91" Y="-11" Width="95" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
        <Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
    </Page>


<!--Описываем окно Опции, добавляем поля для ввода паролей пользователя и рута-->
<Page Name="Options">
        <Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
         <!--Создаем поле для ввода пароля пользователя--> 
        <Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsUserPwd)</Text>
        <Editbox Name="UserPwdEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes">DefaultUserPwd</Editbox>
         <!--Создаем поле для ввода пароля рута--> 
        <Text X="11" Y="171" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsRootPwd)</Text>
        <Editbox Name="RootPwdEditbox" X="11" Y="193" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes">DefaultRootPwd</Editbox>
        <!--Кнопки ОК и Отмена-->
        <Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
        <Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#!(loc.!(loc.(loc.OptionsCancelButton)</Button>
    </Page>

Содержание файла локализации MyRtfTheme.wxl я показывать не буду, думаю, что с ним вы разберетесь самостоятельно.

Нам осталось указать получившиеся шаблон и локализацию в проект, для этого нам нужно внести изменения в файл Bundle.wxs, добавив следующее:

<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
      <bal:WixStandardBootstrapperApplication
          <!-- Укажем файл лицензионного соглашения, который расположен в корне проекта--> 
	     LicenseFile="EULA-RU.rtf"
          <!-- Укажем иконку для проекта, которая расположена в корне проекта--> 
	     LogoFile="logo.ico"
          <!-- Укажем возможность починки проекта через инсталлятор-->
	     SuppressRepair="no"
          <!-- Укажем созданные шаблон и файл локализации, которые расположены в корне проекта--> 
          ThemeFile="MyRtfLargeTheme.xml"
          LocalizationFile="MyRtfTheme.wxl"
          <!-- Показываем версию проекта в окне установщика--> 
          ShowVersion="yes"/>
</BootstrapperApplicationRef>

<!-- Создадим две переменные для хранения значений паролей, полученных в окне опций. Использовать переменные не обязательно, но в данном случае нас интересует параметр Hidden=”yes”, который позволяет скрыть значение паролей при передаче в качестве параметров.-->

<!-- В Value записываем значение, полученное из контрола EditBox [UserPwdEditbox], определенного в опциях -->
<Variable Name="UserPwdVariable" Type="string" bal:Overridable="yes" Value="[UserPwdEditbox]" Hidden="yes" />

<!-- В Value записываем значение, полученное из контрола EditBox [RootPwdEditbox], определенного в опциях -->
<Variable Name="RootPwdVariable" Type="string" bal:Overridable="yes" Value="[RootPwdEditbox]" Hidden="yes" />

<!-- В цепочке мы указываем наш установщик и передаем пароли как параметры -->
<Chain>
   <!--Установка дополнительной компоненты-->
   <ExePackage Id="InstallPackage" …
   </ExePackage>
    
   <!--Запускаем основной проект и параметрами передаем пароли-->
   <MsiPackage Id="InstallMSI" SourceFile= "$(var.WixToolSet_MSI.TargetDir)WixToolSet_MSI.msi"
               DisplayName="Установка MSI"
               Visible="yes"
               Vital="yes">
      <MsiProperty Name="APPLICATIONFOLDER" Value="[SourceFolder]"/>
<!--Передаем значения полученных паролей в качестве параметров-->
      <MsiProperty Name="PROPPWDROOT" Value="[RootPwdVariable]" />
      <MsiProperty Name="PROPPWDUSER" Value="[UserPwdVariable]" />
   </MsiPackage>
</Chain>

В итоге у нас получилась некая обертка, которая позволяет установить все необходимые зависимости или компоненты для вашего продукта, а после сам продукт, предварительно запросив всю необходимую информацию.

Рисунок №4
Рисунок №4

Как мы видим, сложности в получении необходимой информации для полной и качественной установки вашего приложения нет, все будет зависеть только от вашей фантазии и разумности.

Надеюсь, сильно ругать не будете, это моя первая статья. Проба пера!

Всем спасибо и до скорого!

Автор статьи: Сокол Даниил


П.С. Полезные ссылки для изучения набора утилит WixToolSet.

Статьи от @Terror, где описаны основы работы с wixtoolset:

  • Создание инсталлятора с помощью WiX.

  • Создание инсталлятора с помощью WiX. Часть 2.

  • Создание инсталлятора с помощью WiX. Часть 3.

  • Статья от автора @Revolution, где описан прекрасный способ сборки пакета msi со сторонними файлами:

  • Автоматическое добавление файлов в WiX инсталлятор.

Комментарии (2)


  1. tomasloh
    16.12.2022 22:49
    +1

    А можно ли выполнить функцию над полученной строкой? Скажем проверить, что два пароля совпадают или получить хеш?


    1. reb00s
      19.12.2022 10:23
      +1

      В теории да, но я такого не делал. Если я правильно понял кейс, то необходимо после нажатия кнопки "Далее" выполнить определенные действия, по результату чего перейти на следующее окно, либо вывести сообщение об ошибке, так?
      Вообще, я не вижу смысла усложнять инсталлятор и добавлять какие либо проверки, но если очень хочется... :)
      Предлагаю смотреть в сторону следующих статей: https://stackoverflow.com/questions/33690724/wix-custom-dialog-when-previous-version-exists, https://stackoverflow.com/questions/16336684/inserting-custom-action-between-dialogs-installuisequence-in-wix