{ GLOBAL CONSTANTS } const { To be able to automatically remove addins from Excel''s list of activated addins during uninstall, we keep a central repository of addins in the registry. If Your Company deploys only one addin (determined by the AppID), this repository will of course contain only this one addin. } AddinsRegistry = 'Software\{#SetupSetting("AppPublisher")}\{#SetupSetting("AppID")}'; { MinExcelVersion and MaxExcelVersion provide boundaries for the minimum and maximum versions of Excel that an add-in can be installed for. The setup code will loop through the Office registry entries for these versions. The installer checks if any of these Excel versions exist; if not, the addin may still be extracted from the setup package, but no activation will take place. The user will be informed about this. } MinExcelVersion = 9; { Excel 2000 } MaxExcelVersion = 16; { Excel 2016 } { Maximum number of addins to check. This is a safety variable that serves to prevent infinite loops. } MaxAddins = 255; { Windows API constants } WM_CLOSE = $10; MAX_PATH = 250; { GLOBAL VARIABLES } var ExcelNotInstalled: boolean; ExcelExePath: string; OkToCopyLog: boolean; { The destination folder is determined at run-time depending on the Excel locale. It is cached in this variable by the GetDestDir() function. } DestDir: string; { If a localized Addins path could not be found in the registry, the addin needs to be registered with Excel using the full path. This global variable is set by the GetDestDir() function. } RegisterWithFullPath: boolean; { Looks up the localized Addins folder in the registry. This function is used in the [Files] section of the script, where function calls always expect a parametrized function. This function does not require a parameter. Therefore, a dummy parameter is defined. } function GetDestDir(dummyparameter: string): string; var Addins: string; s: string; CallName: string; i: integer; begin if DestDir = '' then begin CallName := 'GetDestDir(' + dummyparameter + '): '; log(CallName+'Trying to find addins folder name'); { Note the trailing backslash } DestDir := ExpandConstant('{userappdata}\Microsoft\'); { Loop through possible version numbers of Excel and find out if any installed version uses an addin folder other than "addins". This can be the case with international versions (other than English). If an addin folder name that is different from "Addins" is found, the addin will be installed into a dedicated folder. } for i := 8 to 32 do begin s := ''; if RegQueryStringValue(HKEY_CURRENT_USER, 'Software\Microsoft\Office\' +IntToStr(i)+'.0\Common\General', 'AddIns', s) then begin if Length(Addins) > 0 then begin { If the Addins variable has been set already and we encounter a different name, reset everything in order to use a dedicated name further on. } if s <> Addins then begin { Set the Addins variable to a zero-length string to force using a dedicated dir name later. } log(CallName+'Found alternative Addins key for version '+IntToStr(i)+': "'+s+'"'); Addins := ''; { Once a single dir name that is different from "Addins" was found, we can exit the loop. } break; end end else begin { Addins variable has zero length: Set it to the current value of s } log(CallName+'Found first Addins key: version '+IntToStr(i)+', "'+s+'"'); Addins := s; end end end; { Check if the Addins variable contains something now; if not, use a default value ('XL Toolbox') } if Addins = '' then begin DestDir := ExpandConstant('{userappdata}\Microsoft\Addins\'); RegisterWithFullPath := true; log(CallName+'Using dedicated folder: "'+DestDir+'"'); end else begin DestDir := ExpandConstant('{userappdata}\Microsoft\' + Addins); RegisterWithFullPath := false; log(CallName+'Installing to default Addins folder: ' + DestDir); end; end; result := DestDir; end; { Helper function to convert boolean values into a string (used for logging). } function BoolToStr(b: boolean): string; begin if b then result := 'TRUE' else result := 'FALSE' end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssDone then OkToCopyLog := True; end; { This function is called by InnoSetup. It determines whether the "Tasks" page of the Wizard shall be displayed or not. If Excel is not installed on the system, this page will be skipped (returns TRUE). } function ShouldSkipPage(PageID: Integer): Boolean; begin Result := (PageID = wpSelectTasks) and ExcelNotInstalled; end; { This function checks if any version of Excel from "MinVer" to "MaxVer" is installed by looking up an Excel-specific registry key. It is used in conjunction with a "Check" parameter in the [Files] section, and also by the InitializeWizard() function below. This can be used to install certain files only for specific versions of Excel. See Wikipedia for a list of Excel versions. } function IsExcelVersionInstalled(MinVer: integer; MaxVer: integer): boolean; var xl: integer; begin for xl := MinVer to MaxVer do if RegKeyExists(HKEY_CURRENT_USER, 'Software\Microsoft\Office\'+IntToStr(xl)+'.0\Excel\Options') then begin Result := true; end; Log('IsExcelVersionInstalled('+IntToStr(MinVer)+', '+ IntToStr(MaxVer)+') returns '+BoolToStr(result)); end; { =========================================================== == Activating/deactivating addins =========================================================== } { Returns a numbered value in the form expected for Excel addins in the Windows registry. For example, the list of addins is stored as "OPEN", "OPEN1", "OPEN2" etc. } function GetNumberedValue(KeyName: string; Num: integer): string; begin if Num = 0 then begin Result := KeyName; end else begin Result := KeyName+IntToStr(Num); end end; { Activates an addin for use in Excel by adding an "OPEN" value with the addin's name to the Excel registry key. The MinVer and MaxVer parameters indicate the Excel versions for which an addin shall be installed. } procedure ActivateAddin(MinVer: integer; MaxVer: integer); var xl, i: integer; Key, Value: string; AddinName: string; CallName: String; begin { Only proceed if the task 'Activate addin' is selected. } {if not IsTaskSelected('ActivateAddin') then Exit;} {if not IsTaskSelected('ExcelAddin\Activate') then Exit;} if not IsTaskSelected('ExcelAddin') then Exit; { Activated Excel addins are usually stored in the registry with their file name only (without path). If the standard addins folder could not be determined by the GetDestDir() function, the entire path is used instead. The GestDestDir() function will have set the global variable RegisterWithFullPath accordingly. } if RegisterWithFullPath then begin AddinName := '"' + CurrentFileName + '"'; end else begin AddinName := '"' + ExtractFileName(CurrentFileName) + '"'; end; { Build the string for the log file. } CallName := 'RegisterAddin('+IntToStr(MinVer)+', '+ IntToStr(MaxVer)+'): '; Log(CallName+'Processing ' + AddInName); { Create a registry key for each Excel version that this addin is to be activated for. } xl := MinVer; repeat { This registry key scheme is used by Excel 2000 and newer. Excel 97 uses a different scheme; but since Excel 97 is really ancient, we ignore it here. } Key := 'Software\Microsoft\Office\'+IntToStr(xl)+'.0\Excel\Options'; { If the registry key exists, assume that the Excel version is installed. } if RegKeyExists(HKEY_CURRENT_USER, Key) then begin { Find the next available OPEN value } i := 0; repeat { Check if a registry value OPEN[i] exists (OPEN, OPEN1, OPEN2, ...). If it does not exist, we can write the addin name into "OPEN[i+1]" and finish. If it does exist, check if it contains our addin. If so, finish. If not, increase i and try again. } if RegQueryStringValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', i), Value) then begin { Check if the value happens to match the current addin's name. } if uppercase(Value) = uppercase(AddinName) then begin { The addin is activated already, so we can quit. } break; end else begin { Increase the iterator variable to test the next OPEN[i] value. } i := i+1 end end else begin { The OPEN[i] value does not exist yet: Write to the log file that we are modifying the registry now. } Log(CallName+'Writing registry: '+Key+' ==> '+IntToStr(i)); RegWriteStringValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', i), AddinName); break; end; until i > MaxAddins end; { Move on to the next Excel version. } xl := xl+1; until xl > MaxVer; { Now, save the addin name in a repository in order to be able to remove it from Excel's list of active addins during uninstall. This loop is very similar to the one above. } i := 0; repeat if RegQueryStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i), Value) then begin { A numbered value for the current value of i exists already. } if uppercase(Value) = uppercase(AddinName) then begin { The addin is already contained in this repository; break out of the loop. } break; end else begin { Check the next numbered value } i := i+1; end end else begin { Numbered value Addin[i] does not exist yet: Store the addin in the repository. Also store the Excel versions for which it was activated. } Log(CallName+'Writing registry: '+Key+' ==> '+IntToStr(i)); RegWriteStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i), AddinName); RegWriteStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)+'_MinVer', IntToStr(MinVer)); RegWriteStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)+'_MaxVer', IntToStr(MaxVer)); break; end; until i > MaxAddins; end; { This function is called by the DeinitializeUninstall() procedure. It loops through the Excel registry keys for the Excel versions indicated by MinVer and MaxVer and removes occurrences of "AddinName". If an occurrence is found, the corresponding "OPENx" value is deleted, and all values "OPENy", "OPENz" etc. are renamed as "OPENx", "OPENy" and so on to preserve consecutive numbering of "OPEN" values. } procedure DeactivateAddin(AddinName: string; MinVer: integer; MaxVer: integer); var xl, i, j: integer; Key, Value: string; begin { Make sure the addin name is enclosed in double quotes. } if AddinName[1] <> '"' then AddinName := '"'+AddinName+'"'; { Loop through Excel versions. } xl := MinVer; repeat Key := 'Software\Microsoft\Office\'+IntToStr(xl)+'.0\Excel\Options'; { Check if the Excel version denoted by xl exists. } if RegKeyExists(HKEY_CURRENT_USER, Key) then begin i := 0; repeat { Check if an OPEN[i] key exists that holds our addin's name. } if RegQueryStringValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', i), Value) then begin if uppercase(Value) = uppercase(AddinName) then begin { Addin was found: remove the registry key. } RegDeleteValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', i)); { Adjust the numbering of any remaining OPEN keys. } j := i+1; repeat if RegQueryStringValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', j), Value) then begin RegWriteStringValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', j-1), Value) end else begin { If the jth OPEN value does not exist, we need to delete the previous one, but only if j is bigger than i+1 } if j > i+1 then begin RegDeleteValue(HKEY_CURRENT_USER, Key, GetNumberedValue('OPEN', j-1)); end; break; end; { Move to the next numbered OPEN[i] key whose number needs to be adjusted. } j := j+1; until j > MaxAddins; end; { Move to the next numbered OPEN[i] key. } i := i+1; end else begin { No more values, so let's finish. } break; end; until i > MaxAddins; end; { Move on to the next Excel version. } xl := xl+1; until xl > MaxVer; end; { This procedure is called by the InnoSetup uninstaller once the uninstall process is completed. Each addin that is found in the addin repository that was created during installation ("AddinsRegistry" constant) is deactivated and unregistered by removing it from Excel's registry key. } procedure DeinitializeUninstall(); var i: integer; Value: string; VerInfo: string; MinVer, MaxVer: integer; begin i := 0; repeat { Is there an "AddIn[i]" value stored in our personal registry key? } if RegQueryStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i), Value) then begin { Obtain the minimum and maximum Excel version numbers for which this addin was registered. } if RegQueryStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)+'_MinVer', VerInfo) then begin MinVer := StrToInt(VerInfo); end else begin { Fallback if the registry value is not found. } MinVer := 9; end; if RegQueryStringValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)+'_MaxVer', VerInfo) then begin MaxVer := StrToInt(VerInfo); end else begin { Fallback if the registry value is not found. } MaxVer := 20; end; DeactivateAddin(Value, MinVer, MaxVer); { Remove the addin from the repository. } RegDeleteValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)); RegDeleteValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)+'_MinVer'); RegDeleteValue(HKEY_CURRENT_USER, AddinsRegistry, GetNumberedValue('Addin', i)+'_MaxVer') end else begin { If there is no Addin[i] value in the repository for the current iteration, do not expect there to be any other values with higher numbers, and break out of the loop. } break; end; { Move to the next Addin[i] value in the repository. } i := i+1; until i > MaxAddins; { If the repository is now empty, remove the registry key in order to leave no trace of the installation on the system. } RegDeleteKeyIfEmpty(HKEY_CURRENT_USER, AddinsRegistry); end; { This function is used in the [Files] section to determine which of the add-ins (.XLA for 2000-2003 or .XLAM for 2007-2013) shall be copied to the target system. If no Excel is installed so far, return TRUE to copy both; otherwise, if Excel is installed, copy the one that is appropriate for the installed Excel version. } function ShouldInstallFile(MinVer: integer; MaxVer: integer): boolean; begin if ExcelNotInstalled then begin result := true; end else begin result := IsExcelVersionInstalled(MinVer, MaxVer); end end; { Determines if a 64-bit version of Excel is installed. Note that there may be a 32-bit version of Excel running on a 64-bit version of Windows. } function Excel64(): boolean; var bitness: string; callname: string; begin callname := 'Excel64: '; if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'Software\Wow6432Node\Microsoft\Office\14.0\Outlook', 'Bitness', bitness) then begin Log(callname+'Wow6432 registry key: Bitness = ' + bitness); if bitness = 'x64' then Result := true; end else begin Log(callname+'Wow6432 registry key not present'); end; end; { =========================================================== } { == Win32 API { =========================================================== } #include "win32.iss" { =========================================================== } { == Pre install { =========================================================== } function GetUninstallRegKeyStr: string; var AppID: string; begin { The following code requires the InnoSetup Preprocessor (ISPP). NB: The double curly brackets at the beginning of the AppID string are not properly processed by ISPP: They remain as-is. However, in the registry, the AppID has just a single leading curly bracket, so we need to remove the first one. } AppID := Copy('{#SetupSetting("AppID")}', 2, 255); result := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\'+AppId+'_is1'; end; { This function checks the command-line parameters for the /UPDATE switch which indicates that setup was started by an update procedure of the Excel addin. If the switch is present, Excel is shut down silently, rather than displaying a message to the user. When the installation is complete, Excel is started again. It is up to the addin to use or not use this switch and implement appropriate behavior before and after the installation process. } function InitializeSetup(): boolean; var i: LongInt; hWnd: LongInt; IsUpdate: boolean; bCancel: boolean; CallName: string; begin CallName:='InitializeSetup(): '; Log(CallName+'Examining command line...'); for i := 1 to ParamCount do begin if uppercase(ParamStr(i)) = '/UPDATE' then begin Log(CallName+'UPDATE switch found'); hwnd := FindWindowByClassName('XLMAIN'); if hWnd<>0 then begin { Before we attempt to shut down Excel, we fetch its executable path, so that we can restart it later. } ExcelExePath := GetProcessExePath(Hwnd); Log(CallName+'Sending WM_CLOSE to XLMAIN...'); SendMessage(hWnd, WM_CLOSE, 0, 0); { WM_CLOSE: $10 } { After sending the WM_CLOSE message, we need to give Excel a second to completely shut down and remove the Mutex; otherwise, the Setup program will abort if started with /SP- /SILENT A delay of 500 ms is sufficient } Sleep(500); IsUpdate := true; end end end; { If this setup program is *not* run as an update, we need to check if Excel is running and if so, tell the user that we are going to attempt to close it. } if IsUpdate then begin Log(CallName+'Running as UPDATE --> exiting with Result := TRUE'); { Returning True indicates that InnoSetup may continue. } Result := true end else begin hWnd := FindWindowByClassName('XLMAIN'); { If Excel is running, hWnd is different from 0. } while (hWnd <> 0) and not bCancel do begin { Inform the user that Excel needs to be closed. } if SuppressibleMsgBox(CustomMessage('ExcelIsRunning'), mbConfirmation, MB_OKCANCEL, IDOK) <> IDOK then begin Log(CallName+'Excel running - user aborted setup.'); bCancel := true; end else begin Log(CallName+'Excel running - attempting to close...'); { Use the Win32 api to shut down Excel. } ExcelExePath := GetProcessExePath(Hwnd); SendMessage(hWnd, WM_CLOSE, 0, 0); { WM_CLOSE: $10 } Sleep(200); { Check if Excel has been shut down successfully. } hWnd := FindWindowByClassName('XLMAIN'); end; end; { If Excel is no longer running, hWnd will be 0 at this point, so we can return True to indicate to InnoSetup that it may proceed. } Result := (hWnd = 0); end; end; { This function is called by InnoSetup during the initialization step of the setup process. It checks if Excel is installed on the system at all. } procedure InitializeWizard(); var CallName: string; begin CallName := 'InitializeWizard(): '; Log(CallName); if not IsExcelVersionInstalled(MinExcelVersion, MaxExcelVersion) then begin Log(CallName+'Excel not installed!'); CreateOutputMsgPage(wpWelcome, CustomMessage('ExcelNotInstalled'), CustomMessage('ExcelNotInstalledCaption'), CustomMessage('ExcelNotInstalledExplanation')); ExcelNotInstalled := true; end; end; { =========================================================== == Post install =========================================================== } { If this setup program is operating in "Update" mode (with the /UPDATE switch), the InitializeSetup() function placed the path of the executable Excel image file in the global variable ExcelExePath. We can use it to automatically restart Excel. } procedure DeinitializeSetup(); var ExitCode: Integer; CallName: String; begin CallName := 'DeinitializeSetup(): '; { Restart Excel if it has been running before. } if Length(ExcelExePath) > 0 then begin Log(CallName+'Restarting Excel...'); Log(CallName+'--> '+ExcelExePath); Exec(ExcelExePath, '', '', SW_SHOW, ewNoWait, ExitCode); end else begin Log(CallName+'Excel was not running when setup was started.'); end; { Copy the log file to the installation } if OkToCopyLog then FileCopy( ExpandConstant('{log}'), AddBackslash(GetDestDir(''))+'{#product}\{#logfile}', false); { The following line requires administrator privileges during setup. It is therefore commented out until a way is found to delete the log file after savely without administrator privileges. When uncommenting, make sure to replace the '>>' with closing curly braces. } { RestartReplace(ExpandConstant('{log>>'), ''); } end; { vim: set ft=pascal ts=2 sts=2 sw=2 noet tw=60 fo+=lj :}