Custom Actions is one of the most important elements in
WiX , allowing you to perform any actions in the process of installing or removing a program that expands the capabilities of WiX. With the help of Custom Action, we can connect to our installer VBScript, JScript, Dll library, executable module and perform any actions in the process of the installer.
Consider an example - during the installation of the program we need to specify the path to the file on the local computer, let's say the license file.
To begin with, we will create a project, add a window with a choice of the license file (so that the article is not filled with unnecessary code, I will only give you important code fragments in my opinion. You can download and view the sources of the project at the end of the article).
I created a project, excluded unnecessary windows from the installation process, added a new window in which the file was selected. As a result, the installer will contain the following windows:
')
1. Greetings2. Select license file
3. Starting installation4. End of installationThe project includes the following files:
- Product.wxs product description
- Features.wxs installation options
- Files.wxs installable files
- Variables.wxi variables
- AddRemove.wxi parameters affecting the product display in the Add / Remove Programs panel
- WixUI_License.wxs window with selection of license file
- WixUI_Wizard.wxs description of installation wizard steps
Let's add the
Custom Action to our project, for this we will add the
CustomAction.wxs file to the project
.<? 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 .
With the help of the
Binary element, we can add files to our project that will not be installed on the target computer, but will participate in the process of the installer. The
SourceFile parameter specifies the path to the file, in this case,
BrowseForFile.vbs .
The
CustomAction element describes some action performed by a script, a dynamic library, which we can perform during the installation / uninstall process. Consider its parameters:
BinaryKey is a reference to the
Binary element.
VBScriptCall sets the name of the VBScript function to call.
Return in this case,
check , indicates that the script will be called synchronously, and the returned value will be checked for success. The
check value is the default value.
Other possible values for
Return :
asyncNoWait is an asynchronous call, the installer will not wait until the end of execution, which may occur even after the installer has completed. It can be useful when we want to run our program after the installation process is complete.
asyncWait is an asynchronous call, but, unlike the previous one, the installer will wait for completion.
Ignore is the same as
check , but without checking the result.
The
CustomAction element has a lot of other parameters, which list does not make sense here, a full description of the
CustomAction element. You can find it
here .
Understood, go to the contents of the file
BrowseForFile.vbsFunction 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 .
This script displays a file selection dialog and returns the result. If the user specifies a file and clicks the
Open button, the script will return the path to the file by setting the value of the
LICENSE_FILE_PATH variable defined in the
Product.wxs file
< Property Id ="LICENSE_FILE_PATH" > C:\ </ Property >
For the
CustomAction call, we will have the
Browse button, defined in the
WixUI_License.wxs file
.< 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 .
The
Publish element, in this case, is responsible for the reaction for pressing the button. The value of the
Event (DoAction) parameter indicates the need to call
CustomAction with the identifier specified in the
Value parameter (BrowseForFile).
Now we need to output the value of the
LICENSE_FILE_PATH variable that stores the path to our file. The display will deal with the text field:
< Control Id ="TextLicensePath"
Type ="Edit"
...
Property ="LICENSE_FILE_PATH" >
</ Control >
* This source code was highlighted with Source Code Highlighter .
If we now assemble and run the installer, then after clicking the
Browse button and selecting a file, we will see that our text field is not updated.

In order to understand the cause of the problem, click the
Back button, then
Next , the field has been updated. Again, select the file and “cover” our window with another window, then return it. The field has been updated again. Yeah, so we did everything right, and the problem is that the UI does not update the field contents when the
Property changes. What to do?
One option is to create a duplicate
LicenseDlg window and switch between these windows when you click the
Browse button. The implementation is simple. Create a copy of the file
WixUI_License.wxs , call it, for example,
WixUI_License2.wxs . Rename the
LicenseDlg dialog to
LicenseDlg2 . In the script wizard add:
< 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 .
The first two lines duplicate the behavior of the dialog
LicenseDlg , the second are responsible for the transitions between duplicate windows.
We start, check, work. But I absolutely do not like this method. Duplicate code, and the need to delete, copy, rename the dialogue every time you make changes to its structure.
Now, if it were possible to somehow “tie” C ++ here, I would instantly figure out how to update the window ... But you can. As it was already written at the beginning of the article, we can use Dll libraries as sources for
CustomAction . I will say more, a ready-made library with source codes, for our task, is available on the site
www.installsite.org .
If you look at the sources of this library, we see that the
BrowseForFile function takes the initial value of the file path from the
PATHTOFILE property, displays the file selection dialog and, if successful, saves the received path value to the
PATHTOFILE . Then it finds the window that needs to be updated, by the name of the
Default window
- the InstallShield Wizard , finds in it an element with the name of the class
RichEdit20W and sets it with the text equal to the selected path to the file.
You can take this library, correct the search window, filter in the file selection dialog and connect to your project. And you can, based on this code, write your own, more universal library. In my opinion, the library should be able to accept as an input parameter a filter for the file selection dialog and correctly find the installer window.
Let's create a library for our project. To do this, add a new project to the project (
File - Add - New Project ). Project Type
Win32 Project .


We connect to the project library
Msi.lib
Next, we will create a file with a description of the exported functions, we will immediately agree that ours will be called
BrowseForFileLIBRARY "BrowseForFile"
EXPORTS
BrowseForFile
There is a second, simpler way to create a Custom Action in C ++. To do this, select the new project in the window for selecting a new project.

But for the first time it is better to create everything with your hands in order to understand how it works, and only then you can trust the creation to the master.
Create a
BrowseForFile function
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 .
At this stage, our library can do everything the same as the script did. Add the ability to update the field. In the example, the search was performed by the name of the window, in my opinion this is not correct. From a project to a project, the name of the window may change, I would like to find a universal way. Having set myself such a task, I decided to check, the first thing that came to mind - and what class do installer windows have? Looking at the installer window information using the
Spy ++ utility (included in Visual Studio), I found that the window has the class
MsiDialogCloseClass . Here on it will rely. Add the code:
// .
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 .
And in the
BrowseForFile function, inside the condition:
if (GetOpenFileName(&ofn))
add:
// .
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 .
Everything is ready to use our library in the installation package. In the file
Product.wxs, we will declare two new properties.
< Property Id ="BFF_PATH_TO_FILE" ></ Property >
< Property Id ="BFF_FILE_DIALOG_FILTER" > (*.*)|*.*| (*.txt)|*.txt|| </ Property >
Through the
BFF_PATH_TO_FILE property
, our Dll library will “communicate” with the installer.
The
BFF_FILE_DIALOG_FILTER property contains a filter for the file selection dialog.
Let's change the description of
CustomAction to:
< 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 .
And we will add a new
CustomAction , which will assign the
LICENSE_FILE_PATH property we need to the value received as a result of the Dll call (
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 .
The last thing left to do is change the code responsible for pressing the button, we will add to it a call to the new
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 .
Done, you can collect and check.
Custom Action can be called not only in response to any action from the user, but also automatically, in the process of the installer. Let's look at a simple example in which we will need to create the
Config folder in the program folder during the installation process, and delete this folder during the uninstall process (there is another way to do the same using the child
Directory section. In this case, this method was chosen for the sake of ).
Start by creating a folder during the installation process. Create a new
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 .
As you can see from the example, the script code can be written directly into the body of the CustomAction element, while there is no need to create the Binary element and refer to it.
To delete a folder during the removal process:
< 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 .
Actions are added, now someone has to call them and run. Add the
following code to the
Product.wxs file:
< 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 .
The
InstallExecuteSequence section allows
you to place within yourself other partitions, which you can specify when they will be used. In our example, the folder will be created after the
InstallFinalize stage is
completed (installation is completed), and the deletion is
done before the
RemoveFiles stage (file deletion).
Notice that the
Custom sections contain some text inside. These are logical conditions that determine whether an action will be performed or not.
Installed is a sign that the product is already installed. Those.
CreateConfigFolder will work if the product is not yet installed.
RemoveConfigFolder if the product is installed, i.e. in the process of removing the program.
That's all, I hope it was not too boring. If you have questions, feel free to ask.
Project sourcesBinary Dll-ki with brief instructions for useLinks to previous articles:
Part 1 (getting started) ,
Part 2 (project organization) ,
Part 3 (custom windows)