|
For an installation program I wanted to give the user an option to automatically scan for a particular file on his hard drive or maybe only in a subtree of a drive.
A search through the Inno Setup newsgroups brings up several posts to discourage people to do so. It is understandable, because network and local drives could contain several million files or a similar amount of folders. It could take hours to read the directory tree in and search for a single file. The reasonable alternative is to find the file through other clues like registry entries.
However, the location of the file my installation program is supposed to look for is stored nowhere. The software saves its settings in a Windows ini file, and this file resides just next to the executable file in the same folder.
Preparing the Inno Setup script to search for it in a given folder tree or an entire drive seems helpful to the user.
It didn't even look very complicated as the helpfile has a pretty good example on how to use the functions FirndFirst and FindNext. The only minor obstacle was my limited Pascal knowledge which had become a bit rusty over the years, and of course because Inno Setup's Pascal script is not comparable with a fully-featured Pascal compiler.
That's probably the reason why my first attempt didn't want to work on larger directory trees. I kept getting an 'Out of memory' message after a few seconds. I had tried to read the tree into a string array (TArrayOfString) by dynamically expanding it as files and folder names where coming in from the tree reading function. I wanted a flexible function to read the folder tree and then go through the array and process every single file and directory in a second step outside that reading function.
procedure ProcessDirectory (RootDir: String; Progress: Boolean); var NewRoot: String; FilePath: String; FindRec: TFindRec; begin NewRoot := AddBackSlash (RootDir); if FindFirst (NewRoot + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') AND (FindRec.Name <> '..') then begin FilePath := NewRoot + FindRec.Name; if FindRec.Attributes AND FILE_ATTRIBUTE_DIRECTORY > 0 then ReadDirectory (FilePath, Progress) else begin // Start action --> // . // Add your custom code here. // FilePath contains the file name // including its full path name. // Try not to call a function for every file // as this could take a very long time. // . // <-- End action. end; end; until NOT FindNext (FindRec); finally FindClose(FindRec); end; end; end;
When this function is called, the installer can't do anything else. In effect, that means that not even the installation wizard's window can be moved. The user interface blocks completely without feeding anything back.
In order to show at least something in Inno Setup's wizard, a progress page can be inserted. Reading and processing a directory tree is a serial operation, meaning that it's finishing time can't be predicted in advance. This is why I set the progress bar up to change its status every 1000 files or folders, and when it has hit the right side of the bar it rolls over to the left again.
var ProgressPage: TOutputProgressWizardPage; ProgressValue: Integer; ArrayLen: LongInt; procedure ProcessDirectory (RootDir: String; Progress: Boolean); var NewRoot: String; FilePath: String; FindRec: TFindRec; begin NewRoot := AddBackSlash (RootDir); if FindFirst (NewRoot + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') AND (FindRec.Name <> '..') then begin FilePath := NewRoot + FindRec.Name; if FindRec.Attributes AND FILE_ATTRIBUTE_DIRECTORY > 0 then ProcessDirectory (FilePath, Progress) else begin // Start action --> // . // Add your custom code here. // FilePath contains the file name // including its full path name. // Try not to call a function for every file // as this could take a very long time. // . // <-- End action. ArrayLen := ArrayLen + 1; if (Progress) then begin if (ArrayLen mod 1000) = (ArrayLen / 1000) then begin ProgressValue := ProgressValue + 1; if ProgressValue = 100 then ProgressValue := 0; ProgressPage.SetProgress (ProgressValue, 100); end; end; end; end; until NOT FindNext (FindRec); finally FindClose(FindRec); end; end; end; procedure CurStepChanged (CurStep: TSetupStep); var lI: LongInt; Dir: String; begin if (CurStep = ssInstall) then begin // The folder to scan. Dir := 'C:\'; // The progress page. ProgressPage := CreateOutputProgressPage (CustomMessage ('ProgressTitle'), CustomMessage ('ProgressCaption')); ProgressPage.SetText (CustomMessage ('ProgressText'), Dir); ProgressPage.SetProgress(0, 0); ProgressPage.Show; // Scan the folder. ProcessDirectory (Dir, TRUE); // Hide the progress page. try finally ProgressPage.Hide; end; end; end;
With the progress bar moving, the installation program gained a nice touch towards the standards of what people expect from a fashionate installer. Saying that, this is however only on the surface. There is no chance for the user to interrupt the disk scan unless they use the Taskmanager and kill the setup process.
As mentioned earlier, the scan could take a very long time, maybe even several hours. It is unacceptable not to provide an option to cancelling the process.
The Cancel button is actually there, stuck on the installation wizard. It's just not visible while the progress page works.
To get the button back, it needs to be made visible:
WizardForm.CancelButton.Visible := TRUE;
Once the Cancel button is visible it automatically gets its function back. Of course, it can't interrupt the directory reading function while files are read in and being processed. When Cancel is clicked, the directory scan happily carries on, and as soon as it is finished Inno Setup asks if the installation is to be aborted.
This still is not how it should be. The scan should be stopped immediately, or at least as quickly as possible.
Inno Setup's script engine provides an event function for the Cancel button: CancelButtonClick ().
With a simple message box identical to the 'Exit Setup' confirmation message I tricked the user in believing that they see Inno Setup's original confirmation:
procedure CancelButtonClick (CurPageID: Integer; var Cancel, Confirm: Boolean); // No confirmation message, because we roll our own. // The base idea has been nicked from // http://www.vincenzo.net/isxkb/index.php?title=No_%27Exit_Setup%27_message . // The only difference to Inno Setup's 'Exit Setup' message box is its title, // but that's only a minor cosmetic glitch. begin Confirm := FALSE; if (MsgBox (SetupMessage (msgExitSetupMessage), mbConfirmation, MB_YESNO) = IDYES) then begin bExitSetup := TRUE; end; end;
The difference between this 'Exit Setup' confirmation box and Inno Setup's own can be seen in the window title.


The original confirmation message box is called 'Exit Setup' (image on the top) while mine contains the title of the setup (bottom image), which is 'Setup' by default. Although this is a fairly good working solution already, I was not happy with this difference.
Since Inno Setup uses the Windows API MessageBox (), I was sure it can be used too to correct the window title. I found the declaration for Pascal script on the ISXKB under MessageBox () and nicked it from there:
[Code] function MessageBox (hWnd: Integer; lpText, lpCaption: String; uType: Cardinal): Integer; external 'MessageBoxA@user32.dll stdcall';
Pascal seems a bit picky when it comes to types, hence I didn't want to bother with all those required conversions and looked up the parameter constants in the file WinUser.h from the Windows SDK instead. Here's the relevant excerpt from that file:
/* * MessageBox() Flags */ #define MB_OK 0x00000000L #define MB_OKCANCEL 0x00000001L #define MB_ABORTRETRYIGNORE 0x00000002L #define MB_YESNOCANCEL 0x00000003L #define MB_YESNO 0x00000004L #define MB_RETRYCANCEL 0x00000005L #if(WINVER >= 0x0500) #define MB_CANCELTRYCONTINUE 0x00000006L #endif /* WINVER >= 0x0500 */ #define MB_ICONHAND 0x00000010L #define MB_ICONQUESTION 0x00000020L #define MB_ICONEXCLAMATION 0x00000030L #define MB_ICONASTERISK 0x00000040L #if(WINVER >= 0x0400) #define MB_USERICON 0x00000080L #define MB_ICONWARNING MB_ICONEXCLAMATION #define MB_ICONERROR MB_ICONHAND #endif /* WINVER >= 0x0400 */ #define MB_ICONINFORMATION MB_ICONASTERISK #define MB_ICONSTOP MB_ICONHAND #define MB_DEFBUTTON1 0x00000000L #define MB_DEFBUTTON2 0x00000100L #define MB_DEFBUTTON3 0x00000200L #if(WINVER >= 0x0400) #define MB_DEFBUTTON4 0x00000300L #endif /* WINVER >= 0x0400 */ #define MB_APPLMODAL 0x00000000L #define MB_SYSTEMMODAL 0x00001000L #define MB_TASKMODAL 0x00002000L #if(WINVER >= 0x0400) #define MB_HELP 0x00004000L // Help Button #endif /* WINVER >= 0x0400 */ #define MB_NOFOCUS 0x00008000L #define MB_SETFOREGROUND 0x00010000L #define MB_DEFAULT_DESKTOP_ONLY 0x00020000L #if(WINVER >= 0x0400) #define MB_TOPMOST 0x00040000L #define MB_RIGHT 0x00080000L #define MB_RTLREADING 0x00100000L #endif /* WINVER >= 0x0400 */
I used the values directly and changed the function accordingly:
procedure CancelButtonClick (CurPageID: Integer; var Cancel, Confirm: Boolean); begin Confirm := FALSE; if (MessageBox (0, SetupMessage (msgExitSetupMessage), SetupMessage (msgExitSetupTitle), 4 + 32) = 6) then begin bExitSetup := TRUE; end; end;
The outcome is an example Inno Setup script that scans the entire C: drive. If the user gets bored of watching the scan they can press the Cancel button and terminate the installer.


Here's the example script:
Open ReadDirectoryTree.iss ReadDirectoryTree.iss: ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "My Program" #define MyAppVerName "My Program 1.5" #define MyAppPublisher "My Company, Inc." #define MyAppURL "http://www.example.com/" #define MyAppExeName "MyProg.exe" [Setup] ; NOTE: The value of AppId uniquely identifies this application. ; Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{753F2C1F-EA02-488E-A204-A047F1EFA0BC} AppName={#MyAppName} AppVerName={#MyAppVerName} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={pf}\{#MyAppName} DefaultGroupName={#MyAppName} OutputBaseFilename=setup Compression=lzma SolidCompression=yes [Languages] Name: english; MessagesFile: compiler:Default.isl [CustomMessages] english.ProgressTitle=Searching english.ProgressCaption=Searching for files english.ProgressText=Searching for files... [Code] var ProgressPage: TOutputProgressWizardPage; ProgressValue: Integer; ArrayLen: LongInt; bExitSetup: Boolean; procedure ProcessDirectory (RootDir: String; Progress: Boolean); var NewRoot: String; FilePath: String; FindRec: TFindRec; begin if bExitSetup then Exit; NewRoot := AddBackSlash (RootDir); if FindFirst (NewRoot + '*', FindRec) then begin try repeat if (FindRec.Name <> '.') AND (FindRec.Name <> '..') then begin FilePath := NewRoot + FindRec.Name; if FindRec.Attributes AND FILE_ATTRIBUTE_DIRECTORY > 0 then ProcessDirectory (FilePath, Progress) else begin // Start action --> // . // Add your custom code here. // FilePath contains the file name // including its full path name. // Try not to call a function for every file // as this could take a very long time. // . // <-- End action. ArrayLen := ArrayLen + 1; if (Progress) then begin if (ArrayLen mod 1000) = (ArrayLen / 1000) then begin ProgressValue := ProgressValue + 1; if ProgressValue = 100 then ProgressValue := 0; ProgressPage.SetProgress (ProgressValue, 100); end; end; end; end; if (bExitSetup) then Exit; until NOT FindNext (FindRec); finally FindClose(FindRec); end; end; end; function MessageBox (hWnd: Integer; lpText, lpCaption: String; uType: Cardinal): Integer; external 'MessageBoxA@user32.dll stdcall'; procedure CancelButtonClick (CurPageID: Integer; var Cancel, Confirm: Boolean); begin Confirm := FALSE; if (MessageBox (0, SetupMessage (msgExitSetupMessage), SetupMessage (msgExitSetupTitle), 4 + 32) = 6) then begin bExitSetup := TRUE; end; end; procedure CurStepChanged (CurStep: TSetupStep); var lI: LongInt; Dir: String; begin if (CurStep = ssInstall) then begin // The folder to scan. Dir := 'C:\'; // The progress page. ProgressPage := CreateOutputProgressPage (CustomMessage ('ProgressTitle'), CustomMessage ('ProgressCaption')); ProgressPage.SetText (CustomMessage ('ProgressText'), Dir); ProgressPage.SetProgress(0, 0); ProgressPage.Show; // Make the Cancel button visible during the operation. ;WizardForm.CancelButton.Visible := TRUE; // Scan the folder. ProcessDirectory (Dir, TRUE); // Hide the progress page. try finally ProgressPage.Hide; end; end; end;
Open ReadDirectoryTree.iss
|