Handling Progress Messages Using MsiSetExternalUI
The following sample demonstrates how to code a simple callback handler to receive Windows Installer progress messages during an installation.
Note
When using MsiSetExternalUI with a message type of INSTALLMESSAGE_FILESINUSE, the message sent to the external UI handler function does not contain any information about files in use or window titles used by the FilesInUse dialog box. You should use MsiSetExternalUIRecord to obtain information.
#include <windows.h>
// Globals
//
// common data information fields
int g_rgiField[3]; //array of fields to handle INSTALLOGMODE_COMMONDATA data
WORD g_wLANGID = LANG_NEUTRAL; // initialize to neutral language
//
// progress information fields
int iField[4]; //array of record fields to handle INSTALLOGMODE_PROGRESS data
int g_iProgressTotal = 0; // total ticks on progress bar
int g_iProgress = 0; // amount of progress
int iCurPos = 0;
BOOL bFirstTime = TRUE;
BOOL g_bForwardProgress = TRUE; //TRUE if the progress bar control should be incremented in a forward direction
BOOL g_bScriptInProgress = FALSE;
BOOL g_bEnableActionData; //TRUE if INSTALLOGMODE_ACTIONDATA messages are sending progress information
BOOL g_bCancelInstall = FALSE; //Should be set to TRUE if the user clicks Cancel button.
// In the following snippet, note that the internal user
// interface level is set to INSTALLLEVEL_NONE. If the internal
// user interface level is set to anything other than
// INSTALLUILEVEL_NONE, the user interface level is
// INSTALLUILEVEL_BASIC by default and the installer only
// displays an initial dialog. If the authored wizard
// sequence of the package is to be displayed, the user
// interface level should be set to INSTALLUILEVEL_FULL.
// If the external user interface handler is to have full
// control of the installation user interface, the user
// interface level must be set to INSTALLUILEVEL_NONE.
// Because an external UI handler cannot handle the
// INSTALLMESSAGE_RESOLVESOURCE message,
// Windows Installer allows a UI level,INSTALLUILEVEL_SOURCERESONLY
// that will allow an external UI handler to have full control while also still
// permitting an install to resolve the source
MsiSetInternalUI(INSTALLUILEVEL(INSTALLUILEVEL_NONE|INSTALLUILEVEL_SOURCERESONLY), NULL);
MsiSetExternalUI (TestMyBasicUIHandler,
INSTALLLOGMODE_PROGRESS|INSTALLLOGMODE_FATALEXIT|INSTALLLOGMODE_ERROR
|INSTALLLOGMODE_WARNING|INSTALLLOGMODE_USER|INSTALLLOGMODE_INFO
|INSTALLLOGMODE_RESOLVESOURCE|INSTALLLOGMODE_OUTOFDISKSPACE
|INSTALLLOGMODE_ACTIONSTART|INSTALLLOGMODE_ACTIONDATA
|INSTALLLOGMODE_COMMONDATA|INSTALLLOGMODE_PROGRESS|INSTALLLOGMODE_INITIALIZE
|INSTALLLOGMODE_TERMINATE|INSTALLLOGMODE_SHOWDIALOG,
TEXT("TEST"));
MsiInstallProduct(/*full path to package*/,NULL);
//
// FUNCTION: TestMyBasicUIHandler()
//
// PURPOSE: Demonstrates usage of an external user interface handler for MSI
//
// COMMENTS:
//
int _stdcall TestMyBasicUIHandler(LPVOID pvContext, UINT iMessageType, LPCSTR szMessage)
{
// File costing is skipped when applying Patch(es) and INSTALLUILEVEL is NONE.
// Workaround: Set INSTALLUILEVEL to anything but NONE only once.
if (bFirstTime == TRUE)
{
UINT r1 = MsiSetInternalUI(INSTALLUILEVEL_BASIC, NULL);
bFirstTime = FALSE;
}
if (!szMessage)
return 0;
INSTALLMESSAGE mt;
UINT uiFlags;
mt = (INSTALLMESSAGE)(0xFF000000 & (UINT)iMessageType);
uiFlags = 0x00FFFFFF & iMessageType;
switch (mt)
{
//Premature termination
case INSTALLMESSAGE_FATALEXIT:
/* Get fatal error message here and display it*/
return MessageBox(0, szMessage, TEXT("FatalError"), uiFlags);
case INSTALLMESSAGE_ERROR:
{
/* Get error message here and display it*/
// language and caption can be obtained from common data msg
MessageBeep(uiFlags & MB_ICONMASK);
return MessageBoxEx(0, szMessage, TEXT("Error"), uiFlags, g_wLANGID);
}
case INSTALLMESSAGE_WARNING:
/* Get warning message here and display it */
return MessageBox(0, szMessage, TEXT("Warning"), uiFlags);
case INSTALLMESSAGE_USER:
/* Get user message here */
// parse uiFlags to get Message Box Styles Flag and return appopriate value, IDOK, IDYES, etc.
return IDOK;
case INSTALLMESSAGE_INFO:
return IDOK;
case INSTALLMESSAGE_FILESINUSE:
/* Display FilesInUse dialog */
// parse the message text to provide the names of the
// applications that the user can close so that the
// files are no longer in use.
return 0;
case INSTALLMESSAGE_RESOLVESOURCE:
/* ALWAYS return 0 for ResolveSource */
return 0;
case INSTALLMESSAGE_OUTOFDISKSPACE:
/* Get user message here */
return IDOK;
case INSTALLMESSAGE_ACTIONSTART:
/* New action started, any action data is sent by this new action */
g_bEnableActionData = FALSE;
return IDOK;
case INSTALLMESSAGE_ACTIONDATA:
// only act if progress total has been initialized
if (0 == g_iProgressTotal)
return IDOK;
SetDlgItemText(/*handle to your dialog*/,/*identifier of your actiontext control*/, szMessage);
if(g_bEnableActionData)
{
SendMessage(/*handle to your progress control*/,PBM_STEPIT,0,0);
}
return IDOK;
case INSTALLMESSAGE_PROGRESS:
{
if(ParseProgressString(const_cast<LPSTR>(szMessage)))
{
// all fields off by 1 due to c array notation
switch(iField[0])
{
case 0: // Reset progress bar
{
//field 1 = 0, field 2 = total number of ticks, field 3 = direction, field 4 = in progress
/* get total number of ticks in progress bar */
g_iProgressTotal = iField[1];
/* determine direction */
if (iField[2] == 0)
g_bForwardProgress = TRUE;
else // iField[2] == 1
g_bForwardProgress = FALSE;
/* get current position of progress bar, depends on direction */
// if Forward direction, current position is 0
// if Backward direction, current position is Total # ticks
g_iProgress = g_bForwardProgress ? 0 : g_iProgressTotal;
SendMessage(/*handle to your progress control*/, PBM_SETRANGE32, 0, g_iProgressTotal);
// if g_bScriptInProgress, finish progress bar, else reset (and set up according to direction)
SendMessage(/*handle to your progress control*/, PBM_SETPOS, g_bScriptInProgress ? g_iProgressTotal : g_iProgress, 0);
iCurPos = 0;
/* determine new state */
// if new state = 1 (script in progress), could send a "Please wait..." msg
// new state = 1 means the total # of progress ticks is an estimate, and may not add up correctly
g_bScriptInProgress = (iField[3] == 1) ? TRUE : FALSE;
break;
}
case 1: // ActionInfo
{
//field 1 = 1, field 2 will contain the number of ticks to increment the bar
//ignore if field 3 is zero
if(iField[2])
{
// movement direction determined by g_bForwardProgress set by reset progress msg
SendMessage(/*handle to your progress control*/, PBM_SETSTEP, g_bForwardProgress ? iField[1] : -1*iField[1], 0);
g_bEnableActionData = TRUE;
}
else
{
g_bEnableActionData = FALSE;
}
break;
}
case 2: //ProgressReport
{
// only act if progress total has been initialized
if (0 == g_iProgressTotal)
break;
iCurPos += iField[1];
//field 1 = 2,field 2 will contain the number of ticks the bar has moved
// movement direction determined by g_bForwardProgress set by reset progress msg
SendMessage(/*handle to your progress control*/, PBM_SETPOS, g_bForwardProgress ? iCurPos : -1*iCurPos, 0);
break;
}
case 3: // ProgressAddition - fall through (we don't care to handle it -- total tick count adjustment)
default:
{
break;
}
}
}
if(g_bCancelInstall == TRUE)
{
return IDCANCEL;
}
else
return IDOK;
}
case INSTALLMESSAGE_COMMONDATA:
{
if (ParseCommonDataString(const_cast<LPSTR>(szMessage)))
{
// all fields off by 1 due to c array notation
switch (g_rgiField[0])
{
case 0:
// field 1 = 0, field 2 = LANGID, field 3 = CodePage
g_wLANGID = g_rgiField[1];
break;
case 1:
// field 1 = 1, field 2 = CAPTION
/* you could use this as the caption for MessageBoxes */
break;
case 2:
// field 1 = 2, field 2 = 0 (hide cancel button) OR 1 (show cancel button)
ShowWindow(/*handle to cancel button control on the progress indicator dialog box*/, g_rgiField[1] == 0 ? SW_HIDE : SW_SHOW);
break;
default:
break;
}
}
return IDOK;
}
// this message is received prior to internal UI initialization, no string data
case INSTALLMESSAGE_INITIALIZE:
return IDOK;
// Sent after UI termination, no string data
case INSTALLMESSAGE_TERMINATE:
return IDOK;
//Sent prior to display of authored dialog or wizard
case INSTALLMESSAGE_SHOWDIALOG:
return IDOK;
default:
return 0;
}
}
//
// FUNCTION: ParseCommonDataString(LPSTR sz)
//
// PURPOSE: Parses the common data message sent to the INSTALLUI_HANDLER callback
//
// COMMENTS: Ignores the 3rd field and the caption common data message. Assumes correct syntax.
//
BOOL ParseCommonDataString(LPSTR sz)
{
char *pch = sz;
if (0 == *pch)
return FALSE; // no msg
while (*pch != 0)
{
char chField = *pch++;
pch++; // for ':'
pch++; // for sp
switch (chField)
{
case '1': // field 1
{
// common data message type
g_rgiField[0] = *pch++ - '0';
if (g_rgiField[0] == 1)
return FALSE; // we are ignoring caption messages
break;
}
case '2': // field 2
{
// because we are ignoring caption msg, these are all ints
g_rgiField[1] = FGetInteger(pch);
return TRUE; // done processing
}
default: // unknown field
{
return FALSE;
}
}
pch++; // for space (' ') between fields
}
return TRUE;
}
//
// FUNCTION: FGetInteger(char*& pch)
//
// PURPOSE: Converts the string (from current pos. to next whitespace or '\0')
// to an integer.
//
// COMMENTS: Assumes correct syntax. Ptr is updated to new position at whitespace
// or null terminator.
//
int FGetInteger(char*& rpch)
{
char* pchPrev = rpch;
while (*rpch && *rpch != ' ')
rpch++;
*rpch = '\0';
int i = atoi(pchPrev);
return i;
}
//
// FUNCTION: ParseProgressString(LPSTR sz)
//
// PURPOSE: Parses the progress data message sent to the INSTALLUI_HANDLER callback
//
// COMMENTS: Assumes correct syntax.
//
BOOL ParseProgressString(LPSTR sz)
{
char *pch = sz;
if (0 == *pch)
return FALSE; // no msg
while (*pch != 0)
{
char chField = *pch++;
pch++; // for ':'
pch++; // for sp
switch (chField)
{
case '1': // field 1
{
// progress message type
if (0 == isdigit(*pch))
return FALSE; // blank record
iField[0] = *pch++ - '0';
break;
}
case '2': // field 2
{
iField[1] = FGetInteger(pch);
if (iField[0] == 2 || iField[0] == 3)
return TRUE; // done processing
break;
}
case '3': // field 3
{
iField[2] = FGetInteger(pch);
if (iField[0] == 1)
return TRUE; // done processing
break;
}
case '4': // field 4
{
iField[3] = FGetInteger(pch);
return TRUE; // done processing
}
default: // unknown field
{
return FALSE;
}
}
pch++; // for space (' ') between fields
}
return TRUE;
}