Pull to refresh

Custom Action в WiX

Reading time11 min
Views29K
Custom Actions один из важнейших элементов в WiX, позволяющий производить любые действия в процессе установки или удаления программы, раширяющий возможности WiX. С помощью Custom Action мы можем подключить к нашему установщику VBScript, JScript, Dll библиотеку, исполняемый модуль и производить любые действия в процессе работы инсталлятора.

Рассмотрим пример — в процессе установки программы нам требуется указать путь к файлу на локальном компьютере, допустим к файлу лицензии.

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

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

1. Приветствие

2. Выбор файла лицензии


3. Начало установки

4. Окончание установки

В состав проекта входят следующие файлы:
  • Product.wxs описание продукта
  • Features.wxs опции установки
  • Files.wxs устанавливаемые файлы
  • Variables.wxi переменные
  • AddRemove.wxi параметры, влияющие на отображение продукта в панели Установка / удаление программ
  • WixUI_License.wxs окно с выбором файла лицензии
  • WixUI_Wizard.wxs описание шагов мастера установки

Добавим к нашему проекту Custom Action, для этого к проекту добавим файл CustomAction.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
 <Fragment>
 <Binary Id="BinBrowseForFile"
     SourceFile="BrowseForFile.vbs"/>
 <CustomAction Id='BrowseForFile'
        BinaryKey="BinBrowseForFile"
        VBScriptCall="CallTheAction"
        Return="check"/>
 </Fragment>
</Wix>


* This source code was highlighted with Source Code Highlighter.

С помощью элемента Binary мы можем добавить в наш проект файлы, которые не будут устанавливаться на целевой компьютер, но будут учавствовать в процессе работы установщика. В параметре SourceFile задается путь к файлу, в данном случае это BrowseForFile.vbs.

Элемент CustomAction описывает некоторое действие, выполняемое скриптом, динамической библиотекой, которое мы можем выполнить в процессе установки / удаления программы. Рассмотрим его параметры:

BinaryKey является ссылкой на элемент Binary.
VBScriptCall задает имя функции VBScript-а, которую следует вызвать.
Return в данном случае check, указывает на то, что вызов скрипта будет выполняться синхронно, а возвращенное значение будет проверено на успешность. Значение check является значением по умолчанию.

Другие возможные значения параметра Return:
asyncNoWait — асинхронный вызов, установщик не будет дожидаться окончания выполнения, которое может наступить даже после завершения работы установщика. Может быть полезным, когда мы захотим запустить нашу программу после завершения процесса установки.
asyncWait — асинхронный вызов, но, в отличе от предыдущего, установщик дождется завершения выполнения.
ignore — тоже что и check, но без порверки результата.

Элемент CustomAction имеет массу других параметров, перечислять которые здесь не имеет смысла, полное описание элемента CustomAction. Вы можете найти здесь.

Разобрались, перейдем к содержимому файла BrowseForFile.vbs

Function CallTheAction
 Set objDialog = CreateObject("UserAccounts.CommonDialog")
 objDialog.Filter = "Файл лицензии|*.txt|Все файлы|*.*"
 objDialog.FilterIndex = 1
 objDialog.InitialDir = "C:\"

 Dim intResult : intResult = objDialog.ShowOpen

 If intResult = 0 Then
  CallTheAction = msiDoActionStatusUserExit
  Exit Function
 End If

 Session.Property("LICENSE_FILE_PATH") = objDialog.FileName
 CallTheAction = msiDoActionStatusSuccess
End Function


* This source code was highlighted with Source Code Highlighter.

Этот скрипт выводит на экран диалог выбора файла и возвращает результат. Если пользователь укажет файл, и нажмет кнопку Открыть скрипт вернет путь к файлу, установив значение переменной LICENSE_FILE_PATH, определенной в файле Product.wxs

<Property Id="LICENSE_FILE_PATH">C:\</Property>

За вызов CustomAction у нас будет отвечать кнопка Обзор, определенная в файле WixUI_License.wxs

<Control Id="ButtonBrowse"
  Type="PushButton"
  ...
  Text="Обзор">
 <Publish Event="DoAction" Value="BrowseForFile" Order="1">1</Publish>
</Control>


* This source code was highlighted with Source Code Highlighter.

Элемент Publish, в данном случае, отвечает за реакцию за нажатие кнопки. Значение параметра Event (DoAction) указывает на необходимость вызова CustomAction с идентификатором, заданным в параметре Value (BrowseForFile).

Теперь нам нужно куда-то вывести значение переменной LICENSE_FILE_PATH, хранящей путь к нашему файлу. Отображением будет заниматься текстовое поле:

<Control Id="TextLicensePath"
 Type="Edit"
 ...
 Property="LICENSE_FILE_PATH">
</Control>


* This source code was highlighted with Source Code Highlighter.

Если сейчас собрать и запустить установщик то после нажатия кнопки Обзор и выбора файла увидим, что наше текстовое поле не обновляется.



Для того, чтобы понять причину проблемы нажмем кнопку Назад, затем Далее, поле обновилось. Снова выберем файл и «накроем» наше окно другим окном, затем вернем его. Поле снова обновилось. Ага, значит мы все сделали верно, а проблема заключается в том, что UI не обновляет содержимое поля при изменении Property. Что делать?

Один из вариантов — создать дубликат окна LicenseDlg и переключаться между этими окнами при нажатии кнопки Обзор. Реализация простая. Создаем копию файла WixUI_License.wxs, называем его, например, WixUI_License2.wxs. Переименовываем диалог LicenseDlg в LicenseDlg2. В сценарий мастера добавляем:

<Publish Dialog="LicenseDlg2" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
<Publish Dialog="LicenseDlg2" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>

<Publish Dialog="LicenseDlg" Control="ButtonBrowse" Event="NewDialog" Value="LicenseDlg2">1</Publish>
<Publish Dialog="LicenseDlg2" Control="ButtonBrowse" Event="NewDialog" Value="LicenseDlg">1</Publish>

* This source code was highlighted with Source Code Highlighter.

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

Запускаем, проверяем, работает. Но мне такой сопособ совершенно не нравится. Дублирующийся код, и необходимость каждый раз удалять, копировать, переименовывать диалог при внесении изменений в его структуру.

Вот если бы можно было как-то «прикрутить» сюда C++, я бы мигом разобрался с обновлением окна… А ведь можно. Как уже было написано в начале статьи, в качестве источников для CustomAction мы можем использовать Dll библиотеки. Скажу больше, готовая библиотека с исходниками, под нашу задачу, есть на сайте www.installsite.org.

Если глянуть в исходники этой библиотеки мы увидим, что функция BrowseForFile берет первоначальное значение пути к файлу из свойства PATHTOFILE, отображает диалог выбора файла и, в случае успеха, сохраняет в PATHTOFILE полученное значение пути к файлу. Далее она находит окно, которое необходимо обновить, по имени окна Default — InstallShield Wizard, находит в нем элемент с именем класса RichEdit20W и устанавливает ему текст, равный выбранному пути к файлу.

Можно взять эту библиотеку, подправить поиск окна, фильтр в диалоге выбора файлов и подключить к своему проекту. А можно, взяв за основу этот код, написать свою, более универсальную библиотеку. На мой взгляд библиотека должна уметь принимать в качестве входящего параметра фильтр для диалога выбора файлов и правильно находить окно установщика.

Создадим библиотеку для нашего проекта. Для этого добавим в проект новый проект (File — Add — New Project). Тип проекта Win32 Project.





Подключаем к проекту библиотеку Msi.lib



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

LIBRARY  "BrowseForFile"
EXPORTS
BrowseForFile
Существует и второй, более простой, способ создать Custom Action на С++, для этого в окне выбора нового проекта надо выбрать



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

Создадим функцию BrowseForFile

UINT __stdcall BrowseForFile(MSIHANDLE hInstall)
{
 long lErrMsg = 0;

 TCHAR szOriginalPath[MAX_PATH] = {0};
 TCHAR szDialogFilter[MAX_PATH] = {0};
 TCHAR szIndex[8] = {0};

 DWORD cchValue;

 // Получить значение BFF_PATH_TO_FILE
 cchValue = _countof(szOriginalPath);
 MsiGetProperty(hInstall, TEXT("BFF_PATH_TO_FILE"), szOriginalPath, &cchValue);

 // Получить значение BFF_FILE_DIALOG_FILTER
 cchValue = _countof(szDialogFilter);
 MsiGetProperty(hInstall, TEXT("BFF_FILE_DIALOG_FILTER"), szDialogFilter, &cchValue);

 size_t nFilterLength = wcslen(szDialogFilter);

 for(size_t i = 0; i < nFilterLength; ++i)
 {
  if(szDialogFilter[i] == '|')
  {
   szDialogFilter[i] = '\0';
  }
 }

 OPENFILENAME ofn = {0};

 // Инициализация структуры OPENFILENAME.
 ofn.lStructSize = sizeof(ofn);
 ofn.hwndOwner = GetForegroundWindow();
 ofn.lpstrFile = szOriginalPath;
 ofn.nMaxFile = _countof(szOriginalPath);
 ofn.lpstrFilter = szDialogFilter;
 ofn.nFilterIndex = 0;
 ofn.lpstrFileTitle = NULL;
 ofn.nMaxFileTitle = 0;
 ofn.lpstrInitialDir = NULL;

 if (GetOpenFileName(&ofn))
 {
  // Установить значение переменной.
  MsiSetProperty(hInstall, TEXT("BFF_PATH_TO_FILE"), szOriginalPath);
 }

 return ERROR_SUCCESS;
}


* This source code was highlighted with Source Code Highlighter.

На этом этапе наша библиотека умеет делать все то же самое, что делал скрипт. Добавим возможность обновления поля. В примере происходил поиск по имени окна, по моему это не правильно. От проекта к проекту имя окна может меняться, хотелось бы найти универсальный способ. Задавшись такой задачей, я решил проверить, первое, что пришло на ум — а какой класс имеют окна установщика? Посмотрев информацию об окне установщика с помощью утилиты Spy++ (входящей в состав Visual Studio) я обнаружил, что окно имеет класс MsiDialogCloseClass. Вот на него и будет опираться. Добавим код:

// Функция для поиска дочернего окна по классу окна.
BOOL CALLBACK EnumChildProc(HWND hWnd, LPARAM lParam)
{
 TCHAR szBuffer[100] = {0};

 GetClassName(hWnd, (LPTSTR)&szBuffer, _countof(szBuffer));

 if(_wcsicmp(szBuffer, (_T("RichEdit20W"))) == 0)
 {  
  // Вернуть хэндл на окно и остановить перебор дочерних окон.
  *(HWND*)lParam = hWnd;

  return FALSE;
 }

 return TRUE;
}


* This source code was highlighted with Source Code Highlighter.

И в функцию BrowseForFile, внутри условия:

if (GetOpenFileName(&ofn))

добавим:
// Поиск окна установщика.
HWND hInstallerWnd = FindWindow(_T("MsiDialogCloseClass"), NULL);

if(hInstallerWnd != NULL)
{
 HWND hWndChild = NULL;

 EnumChildWindows(hInstallerWnd, EnumChildProc, (LPARAM)&hWndChild);

 if(hWndChild != NULL)
 {
  SendMessage(hWndChild, WM_SETTEXT, 0, (LPARAM)szOriginalPath);
 }
}


* This source code was highlighted with Source Code Highlighter.

Все готово к использованию нашей библиотеки в пакете установки. В файле Product.wxs объявим два новых свойства
<Property Id="BFF_PATH_TO_FILE"></Property>
<Property Id="BFF_FILE_DIALOG_FILTER">Все файлы (*.*)|*.*|Файл лицензии (*.txt)|*.txt||</Property>

Через свойство BFF_PATH_TO_FILE наша Dll библиотека будет «общаться» с установщиком.
Свойство BFF_FILE_DIALOG_FILTER содержит фильтр для диалога выбора файла.

Изменим описание CustomAction на:

<Binary Id="BinBrowseForFile" SourceFile="..\Debug\BrowseForFile.DLL"/>
<CustomAction Id="BrowseForFile"
     BinaryKey="BinBrowseForFile"
     DllEntry="BrowseForFile"
     Return="check"/>


* This source code was highlighted with Source Code Highlighter.

И добавим новый CustomAction, который будет присваивать нужному нам свойству LICENSE_FILE_PATH значение, полученное в результате вызова Dll (BFF_PATH_TO_FILE)

<CustomAction Id='AssignPathToProperty'
     Property='LICENSE_FILE_PATH'
     Value='[BFF_PATH_TO_FILE]'/>


* This source code was highlighted with Source Code Highlighter.

Последнее, что осталось сделать это изменить код, отвечающий за нажатие кнопки, мы добавим в него вызов нового CustomAction

<Control Id="ButtonBrowse"
 ...
 Text="Обзор">
 <Publish Event="DoAction" Value="BrowseForFile" Order="1">1</Publish>
 <Publish Event="DoAction" Value="AssignPathToProperty" Order="2">1</Publish>
</Control>


* This source code was highlighted with Source Code Highlighter.

Готово, можно собирать и проверять.

Custom Action может быть вызван не только в ответ на какое-либо действие со стороны пользователя, но и автоматичсеки, в процессе работы установщика. Давайте рассмотрим простой пример, в котором нам необходимо будет в процессе установки программы создать папку Config в папке программы, а в процессе удаления удалить эту папку (есть и другой способ сделать тоже самое, используя дочерний раздел Directory. В данном случае такой способ выбран ради примера).

Начнем с создания папки в процессе установки. Создадим новый CustomAction:

<CustomAction Id="CreateConfigFolder" Script="vbscript">
 <![CDATA[
 On Error Resume Next
 Set objFso = CreateObject("Scripting.FileSystemObject")
 strFolderPath = Session.Property("INSTALLLOCATION") & "\Config"
 objFso.CreateFolder(strFolderPath)
]]>
</CustomAction>


* This source code was highlighted with Source Code Highlighter.

Как видно из примера, код скрипта можно прописывать прямо в теле элемента CustomAction, при этом нет необходимости создавать элемент Binary и ссылаться на него.

Для удаления папки в процессе удаления программы:

<CustomAction Id="RemoveConfigFolder" Script="vbscript">
 <![CDATA[
 On Error Resume Next
 Set objFso = CreateObject("Scripting.FileSystemObject")
 strFolderPath = Session.Property("INSTALLLOCATION") & "\Config"
 objFso.DeleteFolder(strFolderPath)
 ]]>
</CustomAction>


* This source code was highlighted with Source Code Highlighter.

Действия добавлены, теперь кто-то должен их вызвать и запустить. Добавим в файл Product.wxs код:

<InstallExecuteSequence>
 <Custom Action="CreateConfigFolder" After="InstallFinalize">Not Installed</Custom>
 <Custom Action="RemoveConfigFolder" Before="RemoveFiles">Installed</Custom>
</InstallExecuteSequence>


* This source code was highlighted with Source Code Highlighter.

Раздел InstallExecuteSequence позволяет разместить внутри себя другие разделы, которым можно указать когда именно они будут задействованы. В нашем примере создание папки будет происходить после завершения этапа InstallFinalize (завершение установки), а удаление перед этапом RemoveFiles (удаление файлов).

Обратите внимание, на то, что разделы Custom содержат внутри себя некоторый текст. Это логические условия, которые определяют, будет ли выполнено действие или нет. Installed — признак того, что продукт уже установлен. Т.е. CreateConfigFolder сработает в том случае, если продукт еще не установлен. RemoveConfigFolder в том случае, если продукт установлен, т.е. в процессе удаления программы.

На этом все, надеюсь было не слишком скучно. Если будут вопросы — не стесняйтесь, задавайте.

Исходники проекта
Бинарник Dll-ки с краткой инструкцией по применению

Ссылки на предыдущие статьи: Часть 1 (начало работы), Часть 2 (организация проекта), Часть 3 (пользовательские окна)
Tags:
Hubs:
Total votes 5: ↑4 and ↓1+3
Comments4

Articles