Thursday, September 2, 2010

Inno Setup custom data directory location

In my last post, I forgot to point you to Inno Setup; go get the Inno Setup QuickStart Pack to get going.
I showed how to copy a directory full of data, like tutorials or sample data, that might change depending on the customer. That means the files are not known when the installer is compiled.

Here's what it looked like:
Source: {src}\data\*; DestDir: C:\MyCompany\data;
   Flags: external recursesubdirs skipifsourcedoesntexist onlyifdoesntexist uninsneveruninstall;
   Permissions: users-modify


This time, I 'm going to show how to let the user choose where this directory is located, and whether to install the contents of the directory at all.

First, let's show an obvious choice:

Source: {src}\data\*; DestDir: {userdocs}\MyCompany\data; Flags: [as above....]

That new constant will put the data in a subdirectory of My Documents, for the user that installs the program. This might be fine for you, if each user of your program is going to install it themselves, and generate their own data. This is exactly what Microsoft had in mind with the My Documents folder.

However, it doesn't work for us. Often, an IT person will install the program, and several users on the computer with different logins will use it and need to access the same data. Our data is also big, so we don't want multiple copies made.

Here's what we actually use:

Source: {src}\data\*; DestDir: {code:GetDataDir}; Check: InstallSampleData; Flags: [as above...]

We use two 'code' pieces to make this work. First we as the user where to put the data, and the function GetDataDir retrieves that value. Second, we ask the user whether to install the data we provide, and InstallSampleData retrieves that answer. Here's what those two routines look like, in the [Code] section of our script:

function GetDataDir(Param: String): String;
begin
  { Return the selected DataDir }
  //MsgBox('GetDataDir.', mbError, MB_OK);
  Result := DataDirPage.Values[0];
end;
 

function InstallSampleData(): Boolean;
begin
  { Return the value of the 'install' radiobutton }
  //MsgBox('InstallSampleData.', mbError, MB_OK);
  Result := SampleDataPage.Values[0];
end;
 

Pretty simple - they are just retrieving the answers and returning them. The commented MsgBox calls can let you know when these routines are being called when you run your installer, for the curious.

But where are we getting these values? Custom wizard pages. We need some answers that don't really fit the normal flow of the installer, but can be inserted pretty easily. I followed the CodeDlg.iss example provided with Inno Setup pretty closely, so it's worth looking at that, too. I'm actually writing this blog post because there isn't an explanation to go along with some of the Inno Setup examples.

[Code]
// global vars
var
  DataDirPage: TInputDirWizardPage;
  SampleDataPage: TInputOptionWizardPage;
  DataDirVal: String;

// custom wizard page setup, for data dir.
procedure InitializeWizard;
begin
  { Taken from CodeDlg.iss example script }
  { Create custom pages to show during install }

  DataDirPage := CreateInputDirPage(wpSelectDir,
    '{#ShortAppName} Data Directory', '',
    'Please select a location for {#AppName} data. (We recommend the default.)',
    False, '');
  DataDirPage.Add('');

  { Set default values, using settings that were stored last time if possible }
  if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\MyCompanyProg',
     'DataDir', DataDirVal) then begin
    DataDirPage.Values[0] := DataDirVal;
  end else
    DataDirPage.Values[0] := 'C:\
MyCompany\data\';
// you might replace the previous with this, which is a per-user way
// to retrieve the previous value:
//   DataDirPage.Values[0] := GetPreviousData('DataDir', 'C:\MyCompany\data\');


  SampleDataPage := CreateInputOptionPage(DataDirPage.ID,
    'Install Sample Data', 'The sample scene is used in our tutorials and training.',
    'Should the sample scene be installed?   ( Recommended for new users, ~ XX GB )',
    True, False);
  SampleDataPage.Add('Install');
  SampleDataPage.Add('Do not install');

  SampleDataPage.Values[0] := True;
end;

// This is needed only if you use
GetPreviousData above.
procedure RegisterPreviousData(PreviousDataKey: Integer);
begin
  SetPreviousData(PreviousDataKey, 'DataDir', DataDirPage.Values[0]);
end;


function DataDirExists(): Boolean;
begin
  { Find out if data dir already exists }
  Result := DirExists(GetDataDir(''));
end;


Whew! As you can see, it's starting to get long. Basically, I'm created two pre-formatted wizard pages, that let the use choose a directory, and choose between two (or more) options with a radio button. When they are display, their vals will get changed by the user. Then during the installation phase, those values are retrieved and used to create and copy the data directory. We are also using Inno Setup PreProcessor values for the application name in the dialog, so check that out in ISPPExample1.iss.

But wait, there's one more thing:

[Dirs]
; If this directory already exists, it sets permissions on all files inside.
; this can take a LONG time (~5 min) so only install if it doesn't already exist.
Name: {code:GetDataDir}; Check: not DataDirExists; Flags: uninsneveruninstall; Permissions: users-modify


The directory won't get created if we don't put anything in it, so we need to include an entry in the Dirs section. And as the comment says, it's worth it to not touch it if it already exists, because setting permissions on a lot of files can take a LONG time, and the installer appears to hang while it's doing that.

That's it for a custom, shared data directory and the Inno Setup script to make it happen. Please let me know if you found this useful, or if it need fixes. Thanks!
Post a Comment