Inno Setup: ISD Format | 
First of all, please note that this is not any sort of official specification or anything. I'm not one of the developers of Inno Setup; I'm just another user. But this is I think a cool way to create your own custom wizard pages in Inno Setup 5 or later, and I recommend it to anyone creating Inno Setup installations. What'd be really cool would be for the makers of ISFD or similar to adopt this design, since I think it lends itself quite well to that. But that's up to them. Anywho, on with the show:
Introduced with Inno Setup 5 is a new design for custom wizard pages, where each page (including both builtin and custom pages) have a page object, and Inno Setup itself tracks the page's position in the installation. From version 5.0.2 it also supports event handlers defined directly on the page objects.
The upshot of all of this means that the code required to create and work with a custom page can be almost entirely contained within a single file, which is then simply included in the main script.
The advantages of this are twofold. Firstly, it provides better encapsulation and separation. All the code relating to a particular page is kept in a single file, meaning that it's much easier to locate any code you want to change. Secondly, it permits easier sharing of code between installations. You could even create a library of pages that you commonly add to setups, and within each setup script include just those pages that you actually need. This means that you can simply "plugin" a particular page with only a couple of lines of code, instead of having to copy large chunks from script to script. And it has the added advantage that if you improve the page's code (perhaps adding an extra feature, or fixing a bug), then you can immediately use that from any installation script.
What follows is a description and explanation of the design I'm using for my own Inno Setup 5 scripts. I've documented it here so that other people can use a similar design -- as such it's free to use for any purpose whatsoever. A credit would be nice (even if it's only in your scriptfile), but is not required.
  Firstly, there's your normal installation script.  We'll call this
  setup.iss for the purposes of examples.  You may well have
  more than one of these, but usually only one per installer :-)
  Next, we come to the core of the ISD design.  For each new custom page you
  want to add to your setup, you create a file with the .isd
  extension.  (The extension is of course purely arbitrary.  That's the one
  that I use because it sounds good to me, but if you don't like it you can
  choose an alternative.)  By convention, there are two different ways that
  I name these files, depending on how they are going to get used.  The files
  that are intended to be used in any installation I give a regular name,
  such as AskAboutSomething.isd.  Those that are intended only
  for one specific installation I tend to prefix with the name of the
  installation script, such as setup-DoSomethingSpecific.isd.
  And finally, there's the normal cloud of .isl files and of
  course the actual files that you're trying to install :-)
  We'll start by looking at how to include the .isd files in
  your installation script, and how to put the pages into your actual
  installation.
  For the purposes of this example, we'll assume that you've already created
  the .isd files, and that they're called page1.isd
  through to page5.isd, just for convenience.
  Let's start by just including page1.isd, putting it immediately
  after the Welcome page:
| setup.iss | 
|---|
[Code] #include "page1.isd" procedure InitializeWizard(); begin CreatePage_Page1(wpWelcome); end;  | 
  Simple, isn't it?  As we'll see later, each .isd file defines
  a CreatePage_ function that takes care of the actual work of
  creating a page.  All you need to do is to tell it when you want it to
  appear.  Now, for a more complicated example.  Let's display page1 and page2
  one after the other, after the Welcome page, and then page3 after the
  directory selection page:
| setup.iss | 
|---|
[Code] #include "page1.isd" #include "page2.isd" #include "page3.isd" procedure InitializeWizard(); var LastId: Integer; begin LastId := CreatePage_Page1(wpWelcome); LastId := CreatePage_Page2(LastId); LastId := CreatePage_Page3(wpSelectDir); end;  | 
It should be fairly obvious how this works. If you want one page to follow another, you simply pass the return value back in as the previous page (as shown in the page2 line). When you want to break the chain and insert your next page further along, then you ignore the return value and pass one of the standard page ID constants in instead (as in the page3 line).
  If you want to use conditional compilation (with ISPP), then the process is
  very similar.  For this example, we want page1, page2, and page3 to follow
  after wpWelcome, and page4 and page5 after wpSelectDir.  However, page2 and
  page4 should only be included if UPGRADE has been
  #defined:
| setup.iss | 
|---|
[Code] #include "page1.isd" #include "page3.isd" #include "page5.isd" #ifdef UPGRADE #include "page2.isd" #include "page4.isd" #endif procedure InitializeWizard(); var LastId: Integer; begin LastId := CreatePage_Page1(wpWelcome); #ifdef UPGRADE LastId := CreatePage_Page2(LastId); #endif LastId := CreatePage_Page3(LastId); LastId := wpSelectDir; #ifdef UPGRADE LastId := CreatePage_Page4(LastId); #endif LastId := CreatePage_Page5(LastId); end;  | 
  Page2 is fairly simple -- we can just surround it with an #ifdef
  and things will work fine.  Imagine what the code would look like without
  that line, and you'll see that it creates page1 and page3 as expected.
  Page4, on the other hand, is a bit trickier.  If we simply #ifdeffed
  the line out as before, then page5 would appear in the wrong place.  So we're
  manually changing the value of LastId outside the #ifdef so that
  everything works properly.  If you find this confusing, or if you want to keep
  consistency, then it's possible to rewrite all the examples this way.  So here's
  that three-page example again, rewritten so that all the function calls are similar:
| setup.iss | 
|---|
[Code] #include "page1.isd" #include "page2.isd" #include "page3.isd" procedure InitializeWizard(); var LastId: Integer; begin LastId := wpWelcome; LastId := CreatePage_Page1(LastId); LastId := CreatePage_Page2(LastId); LastId := wpSelectDir; LastId := CreatePage_Page3(LastId); end;  | 
In many respects, you may find this even clearer to understand. If that's the case, then by all means go for it :-)
This one we'll present a little differently. Since these do the main gruntwork, we'll first show you a skeleton file that contains everything, but doesn't really do anything:
| skeleton.isd | 
|---|
[CustomMessages]
SkeletonCaption=Skeleton Caption
SkeletonDescription=Skeleton description
[Code]
procedure Skeleton_Activate(Page: TWizardPage);
begin
end;
function Skeleton_ShouldSkipPage(Page: TWizardPage): Boolean;
begin
    Result := False;
end;
function Skeleton_BackButtonClick(Page: TWizardPage): Boolean;
begin
    Result := True;
end;
function Skeleton_NextButtonClick(Page: TWizardPage): Boolean;
begin
    Result := True;
end;
procedure Skeleton_CancelButtonClick(Page: TWizardPage; var Cancel,
                                     Confirm: Boolean);
begin
end;
function CreatePage_Skeleton(PreviousPageId: Integer): Integer;
var
    Page: TWizardPage;
begin
    Page := CreateCustomPage(PreviousPageId,
                  ExpandConstant('{cm:SkeletonCaption}'),
                  ExpandConstant('{cm:SkeletonDescription}'));
    // add controls to the page here
    Page.OnActivate := @Skeleton_Activate;
    Page.OnShouldSkipPage := @Skeleton_ShouldSkipPage;
    Page.OnBackButtonClick := @Skeleton_BackButtonClick;
    Page.OnNextButtonClick := @Skeleton_NextButtonClick;
    Page.OnCancelButtonClick := @Skeleton_CancelButtonClick;
    Result := Page.ID;
end; | 
  Make sure you examine the above skeleton carefully.  In practise, you won't
  need all of those event handlers -- I've never found a need for
  BackButtonClick, and only rarely for CancelButtonClick.
  If you don't have any code for a handler then you can leave out both the
  handler routine and the assignment of the handler to the page object (in the
  CreatePage routine).
  As it stands, if you included this skeleton file in your installation (as
  already discussed), it would appear as a blank page, apart from the captions
  (which would normally go in an .isl file, if you're creating
  a multilingual installation -- but for the purposes of this example they
  appear at the top of the file).
It's important to remember that the names of all these routines must be unique across all pages you're going to be including, since they will all be visible to the compiler. This is why the "name" of the page (in this case, "Skeleton") has been included as part of each routine's name.
  For most controls, you can declare them as local variables inside the
  CreatePage_ routine, since they don't need to be referenced
  anywhere else.  Sometimes, however, you need to update a control in the
  Activate handler, or retrieve the user-selected value in the
  NextButtonClick handler.  For these examples, we'll assume
  that you've already defined a string variable called
  GlobalVarFromMainInstall in your setup.iss file,
  and that you've set it to your desired initial value before calling the
  CreatePage_ routine.  Remember that you have to declare such
  variables before you #include the .isd
  file.  There are two approaches you can take to this:
| TextboxGlobal.isd | 
|---|
    [Code]
var
  TextboxGlobal_TextBox: TEdit;
function TextboxGlobal_NextButtonClick(Page: TWizardPage): Boolean;
begin
  GlobalVarFromMainInstall := TextboxGlobal_TextBox.Text;
  Result := True;
end;
function CreatePage_TextboxGlobal(PreviousPageId: Integer): Integer;
var
    Page: TWizardPage;
begin
    Page := CreateCustomPage(PreviousPageId, 'Example textbox',
                  'Global variable'));
    TextboxGlobal_TextBox := TEdit.Create(Page);
    TextboxGlobal_TextBox.Text := GlobalVarFromMainInstall;
    TextboxGlobal_TextBox.Width := Page.SurfaceWidth;
    TextboxGlobal_TextBox.Parent := Page.Surface;
    Page.OnNextButtonClick := @TextboxGlobal_NextButtonClick;
    Result := Page.ID;
end; | 
| TextboxNamed.isd | 
|---|
    [Code]
function TextboxNamed_NextButtonClick(Page: TWizardPage): Boolean;
var
  MyTextbox: TEdit;
begin
  MyTextbox := TEdit(Page.FindComponent('MyTextbox'));
  GlobalVarFromMainInstall := MyTextbox.Text;
  Result := True;
end;
function CreatePage_TextboxNamed(PreviousPageId: Integer): Integer;
var
    Page: TWizardPage;
    MyTextbox: TEdit;
begin
    Page := CreateCustomPage(PreviousPageId, 'Example textbox',
                             'Named controls'));
    MyTextbox := TEdit.Create(Page);
    MyTextbox.Name := 'MyTextbox';
    MyTextbox.Text := GlobalVarFromMainInstall;
    MyTextbox.Width := Page.SurfaceWidth;
    MyTextbox.Parent := Page.Surface;
    Page.OnNextButtonClick := @TextboxNamed_NextButtonClick;
    Result := Page.ID;
end; | 
What if you don't want a page to appear? There are two different approaches to this, depending on when you know that the page shouldn't be shown.
CreatePage_ routine that the page
    will never be needed in this particular setupPreviousPageId, like so:
    | UnnecessaryPage1.isd | 
|---|
    [Code]
function CreatePage_UnnecessaryPage1(PreviousPageId: Integer): Integer;
var
    Page: TWizardPage;
begin
    if ThisPageIsNecessary() then begin
      Page := CreateCustomPage(PreviousPageId, 'Unnecessary page?',
                    'It was necessary!'));
      // ... continue with controls & event handlers
      Result := Page.ID;
    end else
      Result := PreviousPageId;
end; | 
ShouldSkipPage event.  This is
    called whenever the user is about to go to that page (clicked Next on the
    previous page or Back on the following page), which means that you can
    "change your mind" several times during the course of the setup, to
    reflect whatever the user happens to have chosen at the time :-)
    | UnnecessaryPage2.isd | 
|---|
    [Code]
function UnnecessaryPage2_ShouldSkipPage(Page: TWizardPage): Boolean;
begin
  Result := not ThisPageIsNecessary();
end;
function CreatePage_UnnecessaryPage2(PreviousPageId: Integer): Integer;
var
    Page: TWizardPage;
begin
    Page := CreateCustomPage(PreviousPageId, 'Unnecessary page?',
                             'It was necessary!'));
    // ... continue with controls & event handlers
    Page.OnShouldSkipPage := @UnnecessaryPage2_ShouldSkipPage;
    Result := Page.ID;
end; | 
  You'll notice that all of the above discussion focuses on completely custom
  pages (those created with CreateCustomPage).  But what if you
  want to use one of the other, partially prebuilt pages, such as
  TInputQueryWizardPage?  Well, you can still do it, but to make
  it easier we'll use a slightly different skeleton:
| skeletonInputQuery.isd | 
|---|
[CustomMessages]
SkeletonInputQueryCaption=SkeletonInputQuery Caption
SkeletonInputQueryDescription=SkeletonInputQuery description
SkeletonInputQuerySubcaption=SkeletonInputQuery sub-caption
[Code]
procedure SkeletonInputQuery_Activate(Sender: TWizardPage);
var
    Page: TInputQueryWizardPage;
begin
    Page := TInputQueryWizardPage(Sender);
end;
function SkeletonInputQuery_ShouldSkipPage(Sender: TWizardPage): Boolean;
var
    Page: TInputQueryWizardPage;
begin
    Page := TInputQueryWizardPage(Sender);
    Result := False;
end;
function SkeletonInputQuery_BackButtonClick(Sender: TWizardPage): Boolean;
var
    Page: TInputQueryWizardPage;
begin
    Page := TInputQueryWizardPage(Sender);
    Result := True;
end;
function SkeletonInputQuery_NextButtonClick(Sender: TWizardPage): Boolean;
var
    Page: TInputQueryWizardPage;
begin
    Page := TInputQueryWizardPage(Sender);
    Result := True;
end;
procedure SkeletonInputQuery_CancelButtonClick(Sender: TWizardPage; var Cancel,
                                               Confirm: Boolean);
var
    Page: TInputQueryWizardPage;
begin
    Page := TInputQueryWizardPage(Sender);
end;
function CreatePage_SkeletonInputQuery(PreviousPageId: Integer): Integer;
var
    Page: TInputQueryWizardPage;
begin
    Page := CreateInputQueryPage(PreviousPageId,
                  ExpandConstant('{cm:SkeletonInputQueryCaption}'),
                  ExpandConstant('{cm:SkeletonInputQueryDescription}'),
                  ExpandConstant('{cm:SkeletonInputQuerySubcaption}'));
    // add fields (through Page.Add) here
    Page.OnActivate := @SkeletonInputQuery_Activate;
    Page.OnShouldSkipPage := @SkeletonInputQuery_ShouldSkipPage;
    Page.OnBackButtonClick := @SkeletonInputQuery_BackButtonClick;
    Page.OnNextButtonClick := @SkeletonInputQuery_NextButtonClick;
    Page.OnCancelButtonClick := @SkeletonInputQuery_CancelButtonClick;
    Result := Page.ID;
end; | 
  As you can see, this is much the same as before, but we've added a
  typecast inside each event function.  This allows you to access the
  Values that the user has entered.  The other standard pages
  can be used similarly, by substituting the corresponding page object type.
  Now that you've seen all that code, what's the most common usage?  Most
  people would I think only need to use the NextButtonClick
  handler, in order to save some user-selected data into a global variable.
  Possibly also the Activate handler, to hide or disable some
  controls, or to modify the default value based on previous user input.
  Occasionally the ShouldSkipPage handler, when a page is only
  needed some of the time.  It's unlikely you'll need one of the other two.
  Have a look at the Controls examples above for a simplistic example
  of using the NextButtonClick handler to save data in a global
  variable.
  Well, that's it from me.  I think I've covered everything now.  Hopefully
  you'll find this sort of page design as useful and as easy to use as I have!
  ;-)  If you have any comments, questions, or suggestions,
  please feel free to contact me either on the Inno Setup newsgroups or by
  using the Feedback option on the left-hand-side.  (You'll probably need to
  scroll up a bit to see it!)
| Last Modified: 2006-07-09 03:30:37 UTC |