Building up the Script
An installer for an Excel Add-In consist in an application-specific part and an “Excel” part. During the application-specific part all application-related files are managed, like copying files, samples, user manuals and references, etc. During the “Excel” part all actions are carried out for hooking the Add-In into Excel.
Following this approach, I have designed the install script in such way that the application-related part is the main script and the Excel part is taken care of by an #include script with the only [Code] section that performs the factual installation (and removal) of the Add-In. The included script uses a set of #define statements and compiler directives inherited from the main script. I will focus on the included script and wrap it all up at the end of my story.
From InnoSetup I have used the following event routines for my customisation:
- InitializeSetup
- InitializeWizard
- CurStepChanged
- NextButtonClick
- ShouldSkipPage
- UpdateReadyMemo
- RegisterPreviousData
- InitializeUninstall
- InitializeUninstallProgressForm
- CurUninstallStepChanged
InitializeSetup – This routine runs very early so we use it to detect whether Excel is in 32- or in 64-bit (important to know if your Add-In uses components that are not 64-bit, for instance Microsoft Common Controls). It is also used to detect compatibility with the third-party application. Failure to comply will abort the installation.
InitializeWizard – All custom dialogs are created here, like the “Only for me” and “All Users” selection, the dialogs for selecting a folder for the help and samples and for the Add-In installation progress.
CurStepChanged – Used as a header for calling sub-routines dealing with Pre-Install, Post-Install and Success-Install (all custom routines).
NextButtonClick – Marks the selected components and will trap whether Excel is running before it is ready to install the files (from the [Files] section). Also used to memorise the values of the custom dialogs (see above).
ShouldSkipPage – Detects whether or not to show certain custom dialogs.
UpdateReadyMemo – Prepares the report of what the installer is about to do, including settings for custom dialogs.
RegisterPreviousData – Stores the user’s selection of the custom dialogs.
InitializeUninstall – Traps whether Excel is running or not.
InitializeUninstallProgressForm – This particular event routine is used to create a custom uninstall progress form, as InnoSetup does not provide access to normal custom progress forms which are available only during installation. Note: this event procedure is not properly documented.
CurUninstallStepChanged – Like CurStepChanged, used as a header for calling sub-routines dealing with Pre-UnInstall and Post-UnInstall (all custom routines).
Support functions
The following user-defined functions were created in support of the actions:
- CheckVBE6DLLVersion (Boolean, returns False if not compatible)
- GetDesktop (String, returns the path to the Desktop)
- GetExcelVersionNumberAsNumber (Integer, Excel version as whole number)
- GetExcelVersionNumberAsString (String, Excel version in the form of ‘xx.x’)
- GetExpandedConstant (String, parses constants with error trapping and defaults)
- GetMsgBoxCrLfCharacter (String, returns either <space> or carriage-return/line-feed)
- IsExcel2007OrHigher (Boolean, returns True with Excel 2007 or 2010)
- IsExcel64Bit (Boolean, returns True if Excel is installed as 64bit)
- IsExcelRunning (Boolean, returns True if Excel is running)
- IsInstaller (Boolean, returns True if setup is running as Installer)
- IsRunningSilent (Boolean, returns True if installer is running silent)
- IsTrustAddInsAndTemplatesChecked (Boolean, returns True if the setting is ‘On’ in Excel)
- IsTrustVBProjectChecked (Boolean, returns True if the setting is ‘On’ in Excel)
- SetTrustAddInsAndTemplates (Boolean, sets the setting and returns True if successful)
- SetTrustVBProject (Boolean, sets the setting and returns True if successful)
Code snippets
function CheckVBE6DLLVersion(): Boolean;
(*
Checks the version of VBE6.DLL.
Excel can crash adding code programmatically when the version of VBE6.DLL is equal to or
older than 6.5.10.24. This version of the DLL can be found all the way up to Excel 2007 and
it not automatically upgraded by Microsoft in its Office services packs or updates. Apparently
starting Excel 2007 SP2 a more recent and "bug-free" VBE6.DLL is being distributed.
*)
var
VBE6DLLPath: String;
VersionNumberString: String;
VerMajorMinor: Cardinal;
VerBuild: Cardinal;
VerMajorMinorNotGood: Cardinal;
VerBuildNotGood: Cardinal;
begin
// Default.
//
Result := true;
// The rule for computing the values are:
//
// Major & Minor: ( shl 16) or
// Revision & Build: ( shl 16) or
//
VerMajorMinorNotGood := (6 shl 16) or 5;
VerBuildNotGood := (10 shl 16) or 24;
// Fetch the location of VBE6.DLL, the version numbers of this file and compare.
//
RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VBA','Vbe6DllPath', VBE6DLLPath);
if GetVersionNumbers(VBE6DLLPath, VerMajorMinor, VerBuild) then begin
// Get a string with the data.
//
GetVersionNumbersString(VBE6DLLPath, VersionNumberString);
// Compare.
//
if not ((VerMajorMinor > VerMajorMinorNotGood) or ((VerMajorMinor = VerMajorMinorNotGood) and (VerBuild > VerBuildNotGood))) then begin
// Oops outdated version of VBE6.DLL'
//
if IsRunningSilent then begin
Log('Incompatible version of VBE6.DLL found. See ''README.TXT'' for details. Contact a System Administrator.');
end else begin
MsgBox('Warning: incompatible version of VBE6.DLL found.' + #13#10 + #13#10 +
'An incompatible version of a Visual Basic Environment module (VBE6.DLL) is installed on this computer (' + VersionNumberString + '). ' + GetMsgBoxCrLfCharacter +
'This module can under certain circumstances cause Excel to crash when used in junction with {#TheAppName}. ' + GetMsgBoxCrLfCharacter +
'It is highly recommended that you upgrade this module to a newer version.' + #13#10 + #13#10 +
'Contact a System Administrator for assistance.', mbInformation, mb_Ok);
end;
Result := false;
end;
end else begin
// This is Ok for the moment.
//
// MsgBox('Warning: VBE6.DLL not found.' + #13#10 + #13#10 +
// 'An module of the Visual Basic Environment is missing or could not be retrieved.' + #13#10 + #13#10 +
// 'Contact a System Administrator for assistance.', mbInformation, mb_Ok);
// Result := (true Or false);
end;
end;
function GetDesktop(S: String): String;
(*
Return the path to the Desktop.
*)
begin
if (InstallMode = 0) then begin
Result := GetExpandedConstant('{userdesktop}', '{commondesktop}', '');
end else begin
Result := GetExpandedConstant('{commondesktop}', '', '');
end;
end;
function GetExcelVersionNumberAsNumber(): Integer; (* Search Registry for installed Excel version. From highests version down. This extra function was created because 'StrToIntDef()' cannot handle transformation of strings containing 'decimals', which is what GetExcelVersionNumberAsString() returns. *) begin if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\14.0\Excel\InstallRoot', 'Path') then begin Result := 14; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\12.0\Excel\InstallRoot', 'Path') then begin Result := 12; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\11.0\Excel\InstallRoot', 'Path') then begin Result := 11; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\10.0\Excel\InstallRoot', 'Path') then begin Result := 10; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\9.0\Excel\InstallRoot', 'Path') then begin Result := 9; end else begin Result := 0; end; end;
function GetExcelVersionNumberAsString(): String; (* Search Registry for installed Excel version. From highests version down. *) begin if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\14.0\Excel\InstallRoot', 'Path') then begin Result := '14.0'; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\12.0\Excel\InstallRoot', 'Path') then begin Result := '12.0'; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\11.0\Excel\InstallRoot', 'Path') then begin Result := '11.0'; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\10.0\Excel\InstallRoot', 'Path') then begin Result := '10.0'; end else if RegValueExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\Office\9.0\Excel\InstallRoot', 'Path') then begin Result := '9.0'; end else begin Result := '0.0'; end; end;
function GetExpandedConstant(const First, Second, Default: String): String;
(*
Sometimes IS cannot expand constants and throws up exceptions.
This happens for instance with {userdocs} when it is mapped to a network drive.
*)
var
ResultString: String;
ExceptionRaised: Boolean;
begin
// Try first constant.
//
ExceptionRaised := false;
try
ResultString := ExpandConstant(First);
except
ExceptionRaised := true;
end;
// Try second is specified.
//
if ExceptionRaised then begin
if (Length(Trim(Second)) > 1) then begin
ExceptionRaised := false;
try
ResultString := ExpandConstant(Second);
except
ExceptionRaised := true;
end;
end;
end;
// Return whatever default.
//
if ExceptionRaised then begin
ResultString := Default;
end;
Result := ResultString;
end;
function GetMsgBoxCrLfCharacter(): String; (* Vista and W7 perform a UI-aware formatting of the text in a MsgBox breaking up lines at unpredictable positions so we can not insert hard carriage return/linefeeds in the text to break lines where we want. In earlier versions of Windows this is OK. Therefore, on Vista or W7 we return a space and let it handle formatting and use a hard CrLf in all other cases. *) var WindowsVersion: Integer; begin WindowsVersion := (GetWindowsVersion shr 24); if (WindowsVersion >= 6) then begin Result := ' '; end else begin Result := #13#10; end; end;
function IsExcel2007OrHigher(): Boolean; (* Excel 2007 has version number 12, so if the version on board is 12 or higher then it is Excel 2007 or higher. With Excel 2007+, XLAM Add-Ins formats can be used. *) begin Result := false; if (GetExcelVersionNumberAsNumber >= 12) then Result := true; end;
function IsExcel64Bit(): Boolean; (* 64-bit versions of Office create a Registry Key called 'Bitness' containing 'x86' for 32-bit apps and 'x64' for 64-bit apps. *) var BitnessString: string; begin Result := false; // Fetch the setting of 'Bitness'. If 'x64' then it is 64-bit. // if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\Office\' + GetExcelVersionNumberAsString + '\Excel', 'Bitness', BitnessString) then begin Result := (BitnessString = 'x64'); end; end;
function IsExcelRunning(const Typ: TMsgBoxType; const Buttons, Default: Integer; var MsgBoxResult: Integer): Boolean;
(*
Checks whether Excel is running in memory, by hooking to the COM Automation Object
'Excel.Application'. If the object exists, then Excel is running.
Another way would be scanning WMI processes for 'EXCEL.EXE' (simplified, no exception handling, no message boxes, etc.):
function IsExcelRunning(): Boolean;
var
loLocator: Variant;
loWMI: Variant;
loProcesses: Variant;
begin
Result := false;
loLocator := CreateOleObject('WBEMScripting.SWBEMLocator');
loWMI := loLocator.ConnectServer();
loWMI.Security_.ImpersonationLevel := 3; // Impersonate
loProcesses := loWMI.ExecQuery('Select * from Win32_Process Where Name = ''excel.exe''');
if (loProcesses.Count > 0) then Result := true;
loProcesses := Unassigned;
loWMI := Unassigned;
loLocator := Unassigned;
end;
*)
var
ExcelRunning: Variant;
begin
// Try to get an active Excel COM Automation object. If True, then Excel is Running.
// The installation cannot continue if Excel is running, so we start with 'not running'.
//
Result := false;
// Try to get an active Excel COM Automation object.
//
try
ExcelRunning := GetActiveOleObject('Excel.Application');
except
if (Length(Trim(GetExceptionMessage)) > 1) then Log(AddPeriod(Trim(GetExceptionMessage))) else Log('Microsoft Excel not running (this is Ok).');
end;
if Not VarIsEmpty(ExcelRunning) then begin
Result := true;
if IsRunningSilent then begin
Log('Microsoft Excel is running, cannot continue. Contact a System Administrator.');
MsgBoxResult := Default;
end else begin
MsgBoxResult := MsgBox('Oops! Microsoft Excel is running.' + #13#10 + #13#10 +
'The {#TheAppName} can only be installed when Microsoft Excel' + GetMsgBoxCrLfCharacter +
'is not running. Close all active sessions of Microsoft Excel and retry.' + #13#10 + #13#10 +
'If this notification is unexpected, contact a System Administrator.', Typ, Buttons);
end;
end;
// Don't need this instance anymore. It will also forcibly terminate the 'EXCEL.EXE' process.
//
ExcelRunning := Unassigned;
end;
function IsInstaller(): Boolean; (* IsInstaller() in itself does not exist but since it is the opposite of IsUninstaller, we can use the latter to mimic the missing function. *) begin Result := not IsUninstaller; end;
function IsRunningSilent(): Boolean; (* Indicates if the Installer or UnInstaller is running "silent". *) begin if IsInstaller then begin try Result := WizardSilent; except Result := false; end; end else begin try Result := UninstallSilent; except Result := false; end; end; end;
IsTrustAddInsAndTemplatesChecked
function IsTrustAddInsAndTemplatesChecked(): Boolean; (* Check if 'Trust all installed add-ins and templates' setting is turned 'On'. This is a particular Registry setting and impacts on Excel's capability to execute macro code in Add-Ins. *) var DontTrustInstalledFiles: Cardinal; begin // This value is established by Excel when not available in the Registry so we need to start 'True'. // Result := true; // Fetch the setting of 'Trust all installed add-ins and templates'. If '0' then it is enabled. // if RegQueryDWordValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Office\' + GetExcelVersionNumberAsString + '\Excel\Security','DontTrustInstalledFiles', DontTrustInstalledFiles) then begin Result := (DontTrustInstalledFiles = 0); end; end;
function IsTrustVBProjectChecked(): Boolean; (* Check if 'Trust access to Visual Basic project' setting is turned 'On'. This is a particular Registry setting and impacts on Excel's capability to execute macro code in Add-Ins. *) var AccessVBOM: Cardinal; begin // This value is established by Excel when not available in the Registry so we need to start 'True'. // Result := true; // Fetch the setting of 'Trust access to Visual Basic project'. If '1' then it is enabled. // Note: Excel 2000 does not have this setting so we keep the default 'true'. // if (GetExcelVersionNumberAsNumber >= 10) then begin if RegQueryDWordValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Office\' + GetExcelVersionNumberAsString + '\Excel\Security','AccessVBOM', AccessVBOM) then begin Result := (AccessVBOM = 1); end; end; end;
function SetTrustAddInsAndTemplates(): Boolean; (* Turns the 'Trust all installed add-ins and templates' setting 'On'. This is a particular Registry setting and impacts on Excel's capability to execute macro code in Add-Ins. *) begin Result := false; // Set the setting of 'Trust all installed add-ins and templates'. If '0' then it is enabled. // RegWriteDWordValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Office\' + GetExcelVersionNumberAsString + '\Excel\Security','DontTrustInstalledFiles', 0) // Return the result. // Result := IsTrustAddInsAndTemplatesChecked; end;
function SetTrustVBProject(): Boolean; (* Turns the 'Trust access to Visual Basic project' setting 'On'. This is a particular Registry setting and impacts on Excel's capability to execute macro code in Add-Ins. *) begin Result := false; // Set the setting of 'Trust access to Visual Basic project'. If '1' then it is enabled. // Note: Excel 2000 does not have this setting but it probably ignores it. // if (GetExcelVersionNumberAsNumber >= 10) then begin RegWriteDWordValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Office\' + GetExcelVersionNumberAsString + '\Excel\Security','AccessVBOM', 1) // Return the result. // Result := IsTrustVBProjectChecked; end else begin Result := true; end; end;
First of all, thank you for posting parts of your script.
In your code that checks if Excel is 64-bit, why not just hardcode ’14.0′ into the registry key name, since (as far I know) only Office 2010 can be installed in 64-bit version?
Comment by JP — September 8, 2011 @ 5:04 pm |
’14.0′ is insufficient to test for 32- or 64-bit Excel, since Excel 2010 can be installed in either version.
Comment by Jon Peltier — December 29, 2011 @ 7:54 pm |
That’s why I’ve added a function checking for 64bit.
Comment by dutchgemini — January 2, 2012 @ 10:10 am
Dutch – That’s what I meant by my comment to JP. Office 2010 introduced 64-bit versions, but also includes a 32-bit option.
Comment by Jon Peltier — January 2, 2012 @ 5:34 pm
In principle you are right, but I have the tendency to think ahead and parametrise as much as possible every function. I also expect Microsoft to develop other versions of Office/Excel beyond 2010 that support 64-bit so I do not need to touch this function anymore.
Dutch
Comment by dutchgemini — September 9, 2011 @ 8:23 am |
You’re right that future versions of Excel will probably support 64-bit, but I don’t see how you wouldn’t need to edit the function. You’ll still need to add each new Excel version to GetExcelVersionNumberAsString. What I would do is define an array of version numbers as a global at the top of the script, and loop through it wherever necessary.
Comment by JP — September 9, 2011 @ 12:00 pm |
I did consider that while thinking about when the next version of Excel will come out. The reason why it is currently hardcoded is that the code written this way is simple, hits the target very efficiently and is [for me] understandable. I evaluated that using loops on arrays or with counters would have added an excess degree of complexity and likely have the same amount of code lines of my solution, apart from requiring that the structure of Excel-related registry keys remain basically as they are now.
Comment by dutchgemini — September 9, 2011 @ 1:26 pm |