|
Printable version of this article ForewordFirstly, please note that I'm just another user of Inno Setup, not one of its developers. To the best of my knowledge, information contained in this article was correct at the time it was written, but it's possible that it contains errors, or that an upgrade to Inno has rendered something mentioned here unnecessary or incorrect. This is intended as an aide, rather than anything definitive :) PrerequistitesI'm expecting you to be using Inno Setup 5. Some of the material covered here is also applicable to IS4, but for most of the cooler stuff you need IS5. In fact, some of the material is applicable to any sort of programming, not only Inno Setup scripts. A lot of general principles are still relevant. I'm also expecting you to be reasonably familiar with programming, if not Pascal specifically. This isn't a programming tutorial -- while I will be covering some of the syntax of Pascal, I won't cover much of the semantics -- you're expected to have some basic programming knowledge already :-)
Now let's get down to it. Contents
[Code] basics: readabilityThe most important thing to bear in mind at all times is that your code needs to be readily understandable. This is true for all programming, incidentally -- not just scripting. Don't forget that your code may need to be read, understood, and maintained by someone else (especially for a commercial or other team project) -- and even if you're working on something completely private, you'll need to be able to come back after three years (having completely forgotten why you were doing certain things) and be able to easily pick up where you left off. There are a number of techniques that enhance code readability. I'm not going to go into too much detail here (there are plenty of books on the subject already), but I'll mention some of the ones that I think are the most important:
[Code] basics: global variablesLike I said, this isn't really a Pascal tutorial, but I'll cover some of the basics. Global variables are considered a "code smell" -- ie. something that makes your code smell bad, and thus should be used as little as possible. Since ROPS (Inno's scripting language) doesn't support classes, however, you'll find that to do anything nontrivial, a certain amount of global variables are required. You should try to minimise the number of globals you use, but don't do so at the expense of the readability of your code.
To declare a global variable, just put a
Globals are typically declared at the top of your program (ie. at the top of
your [Code] basics: commentsThe [Code] section uses Pascal-style comments, which are quite different from Inno's standard leading-semicolon. In fact, the leading-semicolon form will not work inside the [Code] section, just as the Pascal comments don't work outside of [Code]. So be careful.
So what are the Pascal comments? There are actually two types. One is
the familiar single-line comment, which starts at the comment marker and
continues until the end of the line. This is written using two forward
slashes (
It's important to realise that if you start a block comment, you must end it
with the matching terminator. You can't stop a [Code] basics: strings
I won't go into too much detail here -- everybody knows what strings are.
But it's worthwhile mentioning a few details. First: strings are kept
inside single quotes (
Hashed characters can be concatenated with each other and with quoted strings
without having to use the
There's one more thing you need to watch out for. Strings, unlike arrays
(which we'll cover later), use a one-based index. This means that if you
have a string variable Arithmetic expressions
This is all pretty standard, and should be familiar to anyone with
programming experience. The one thing to watch out for is that in Pascal,
the slash operator ( [Code] basics: defining constants
Not to be confused with Inno Setup constants (such as
[Code] basics: defining enumerations
I'm sure we all know that a
The above snippet illustrates a Pascal convention: giving each alterative a prefix from the initials of its parent type or variable. The reason for doing this is because these values all share the same namespace with each other (and with other constants), and so you can get a clash quite quickly. [Code] basics: defining typesWhen working with enumerations (above) and arrays (below) in particular, you can get quite complicated constructs for your variable types. You need to make sure that you only specify them once. There are two reasons for this: firstly, to avoid duplication (rule 3), and secondly, to avoid incompatibility. To demonstrate that, have a look at this:
The above code will not compile, even though it may seem reasonable at first
glance. You'll get a Type Mismatch error on the line that calls
Note: standard Delphi convention (shared by some flavours of Pascal,
including Inno Setup's) is to prefix a type name with "T". Following
this convention, [Code] basics: arrays
There are times when you need to work with lists of things. Inno already
provides direct support for lists of strings (either through
|
if condition then begin // ... end; if condition then begin // ... end else if condition then begin // ... end else begin // ... end; |
Note that semicolons are forbidden immediately before an else
;
this is because in Pascal semicolons are used to separate statements, and
the else
is considered part of the same statement as the
if
. Stick to the begin
/end
rules that
I've already outlined and you should be fine. (This is also one of the
reasons why I keep begin
and end
on the same line
as the if
statement -- things read better when you get to the
else
, and writing it like this actually reinforces the fact that
you shouldn't put a semicolon on the end
just before the
else
.
Now onto the conditions themselves. A valid condition is simply any
expression with Boolean
type. Which could be simply calling
a function that returns a Boolean
, or it could be using a
relational operator, or even some combination of both.
The relational operators are fairly straightforward: =
tests
for equality, <>
for inequality, and then you've got
less-than, greater-than, etc, as you'd expect. Note that in Pascal
=
always represents an equality test, unlike in some other
languages -- assignment uses the :=
statement instead, which
cannot be used inside an expression anyway.
The boolean operators are and
, or
, and
not
. (There's also an xor
operator, but that's
seldom useful.) These are fairly self-explanatory, but there is one
important thing you need to remember: they have quite high precedence.
This means that a statement like this:
if a = 5
or a = 6 then begin
.. will not do what you'd naturally expect. Since the and
has higher precedence, the above is equivalent to this:
if a = (5
or a) = 6 then begin
.. which will end up doing some quite strange bitwise operations and not
at all what you thought it would. The solution is to explicitly bracket
everything else around the boolean operators, like so:
if (a = 5)
or (a = 6) then begin
See True, false, or maybe below
for another bit of potentially unexpected behaviour.
There's one other type of conditional block in Pascal, and that's the
case
statement. It's similar to switch
in C and
Select Case
in VB -- it jumps to one particular block of
statements depending on the value of a single variable. That variable must
be an ordinal type (integer, character, or enumeration) -- you can't use
strings or floating-point numbers, as with constants. And the selectors
cannot be variables, they must be constant.
case variable of option1: begin // ... end; option2: begin // ... end; option3: begin // ... end; else begin // ... end; end; |
There are a few things worth pointing out here. Note that I've changed my
standard indentation pattern a little. This is because it's better to
preserve code readability than to stick to rigid style rules, and it just so
happens that the above style is (I think) the clearest and most readable way
to structure this particular statement. Note the final else
and
end
-- they don't have a corresponding begin
.
That's because they relate directly back to the case
statement
itself. The else
block is executed if none of the other
selectors match. (It's optional, but it's usually worthwhile to include,
even if for no other reason than to display an error dialog saying "Oops,
the developer forgot to handle something. Tell them what an idiot they
are." :-)
In addition, you are not limited to single values for those selector options.
You may also specify comma-separated values (eg. 4, 6, 129
),
ranges of values (eg. 4..79
), and of course combinations of the
two. (You can't "fall through" one handler block to another, the way you
can in C.)
It's fairly common to want to do things repetitively, and this is where loops come in. Once again, looping structures should be familiar to anyone who is used to programming, so I won't explain in detail. What you may not be familiar with is the specific syntax used in Pascal for these structures, and so that's what I'll cover here:
// A for loop, with increasing index: for variable := low to high do begin // ... end; // A for loop, with decreasing index: for variable := high downto low do begin // ... end; // Do something while the condition is true: while condition do begin // ... end; // Repeat something until the condition is true: repeat // ... until condition; |
There are four points worth noting about these. First, Pascal does not
support "steps" in the for
loop. The index variable you
specify will either increase by one or decrease by one each time -- you
can't make it increase by two each time, for example, although you can
accomplish the same effect through multiplication within the loop body.
Second, the variable in for
loops is limited to integers in
ROPS. (In regular Pascal, you can also use characters and enumeration
constants, just like with arrays.)
Third, the while
loop will be skipped entirely if the condition
is false the first time. The repeat
loop will always execute at
least once. This should be pretty much as you'd expect, given the placement
of the conditions within the loop structure.
Finally, note that the repeat
...until
loop does
not use the begin
or end
keywords. Despite this,
it still permits you use multiple statements within the loop.
Normally, a routine ends when execution "falls off" the end. At that point,
control returns to the calling routine (or to Inno, for an event function).
However, sometimes you want to leave the routine earlier. The most common
case is returning early from a function. Pascal doesn't have an equivalent to
C's return x;
statement (which returns a specific value).
Instead, whatever value is stored in the Result
variable when
control leaves the function is the value that's returned.
A few useful points about the Result
variable: this is an implied
variable. You don't need to explicitly declare it -- it's automatically
created based on the function's definition. It is always the same type as the
function itself. In all other respects, however, it is a normal variable.
You can assign and reassign it as much as you want, use it in comparisons,
whatever. But whatever it holds when the function returns is what gets
returned to the caller.
So, now we know about Result
. A fairly common coding style is
to test for preconditions near the start of your routine, and simply bail out
(with some appropriate return value) if conditions aren't satisfactory. For
example, in your InitializeSetup
function you may want to check
that a particular third-party component is already installed. If it isn't,
then there isn't really much point in continuing. Now, you could do this
with nested if
statements -- but that quickly gets awful. Have
a look at this:
BAD CODE |
---|
function InitializeSetup(): Boolean; begin Result := False; if Component1IsInstalled() then begin if Component2IsInstalled() then begin if Component3IsInstalled() then begin Result := True; // ... end else begin MsgBox('Sorry, no component 3.', mbError, MB_OK); end; end else begin MsgBox('Sorry, no component 2.', mbError, MB_OK); end; end else begin MsgBox('Sorry, no component 1.', mbError, MB_OK); end; end; |
Is this good code? No. All those nested if
statements are
messy -- and what's more, they violate the proximity guideline: the error
message telling the user that component 1 couldn't be found is nowhere near
the code that does the checking. So, let's write it a different way. The
next approach is sometimes referred to as "exit early" -- and that's exactly
what we're going to do, with the help of the exit
keyword:
function InitializeSetup(): Boolean; begin Result := False; if not Component1IsInstalled() then begin MsgBox('Sorry, no component 1.', mbError, MB_OK); exit; end; if not Component2IsInstalled() then begin MsgBox('Sorry, no component 2.', mbError, MB_OK); exit; end; if not Component3IsInstalled() then begin MsgBox('Sorry, no component 3.', mbError, MB_OK); exit; end; Result := True; // ... end; |
Doesn't that look better? Another way of writing it would have the
Result := False;
line removed from the start and instead written
immediately before each of the exit;
lines. This makes it look a
lot more like the C return
statement. Of course, doing that in
this case would lead to a bit of duplication, but it's not an especially bad
form, so go ahead and do it that way if you feel more comfortable with it.
So, exit
will exit from the routine. What if you don't want
something that drastic? What if you just want to break out of a
for
, while
, or repeat
loop? The
question brings the answer: it's the break
keyword. Similarly,
if you want to abandon the current iteration of the loop and jump right back
to the increment/condition, you use the continue
keyword.
with
I'm including this section just so people can recognise this particular language construct and understand what it actually means. In my opinion, however, this statement is the Spawn of EvilTM and should not be used under any circumstances.
BAD CODE |
---|
MyPage := CreateInputOptionPage(wpSelectComponents, 'a', 'b', 'c', True, False); with MyPage do begin Add('1'); Add('2'); SelectedValueIndex := 1; end; |
So, what does all that mean? Within a with
block, you can
refer to methods and properties of a specific variable without having to
specify the variable name each time. In the above snippet, for example, the
first line is actually calling MyPage.Add('1');
, and the third
is equivalent to MyPage.SelectedValueIndex := 1;
.
"What's wrong with that?", I hear you ask. The problem is ambiguity.
Look again. If you hadn't noticed that with
statement, or knew
how it worked, you'd think that this is just calling a normal procedure and
assigning to a regular variable. So using with
has actually
made it harder to understand this code, which is of course a step in
the wrong direction.
It gets worse. Imagine that the with
block above was in a
procedure somewhere, and took a parameter called Name
, which we
want to add as a third option. So let's insert a new line just below the two
existing Add
s, right? Something like this:
Add(Name);
But that won't work. You see, MyPage
also has a property called
Name
. And because the with
block is closer than the
parameter declaration, the property takes precedence -- so you're actually
calling MyPage.Add(MyPage.Name);
. And there is in fact no
way to access the parameter from within the with
block -- it's
completely hidden.
Ok, so we'll just look up all the properties and methods, and then make sure we pick a unique name. That's safe, right? Wrong. Inno is an evolving product, which means that new properties and methods are getting added to objects all the time. So what may be safe today, could be unsafe tomorrow -- and suddenly your code stops working for no readily apparent reason. That sort of problem is very difficult to spot -- especially if the code itself is only executed under certain conditions.
Given all that, why would people use with
? It only provides a
single benefit: less typing. It's frequently introduced when people want to
manipulate several properties on an object buried several levels deep, for
example if you wanted to mess with the properties of
WizardForm.NextButton.Parent
. But there's a much safer way to
do that -- just declare a local variable with a nice short name. Then you
can assign the object to it using its long-winded name and thereafter just
use the short name for everything.
The moral of the story: don't use with
. Ever. It provides no
benefits and comes with significant dangers.
(For the curious: I have less objection to VB's With
statement.
The difference is that in VB's With
, you have to use a leading
dot when referring to a property or method on the named variable, which
eliminates the ambiguity problem. Until you start nesting with
statements, of course. (shudder))
Ah, boolean logic. If you've made it this far then I'd hope that you already know how boolean logic works in general -- you did read the Conditional statements section, didn't you?
When writing conditional logic, some people like to be very explicit,
writing such things as:
if Test = True then ...
You may have also noticed that I never do that. There's two very good
reasons for that:
True
is of course
when it is already a Boolean. And of course all the if
statement wants is a Boolean. You've already got what it wants, so why go do
an unnecessary comparison? Aha! But what about testing if it's equal to
False
? Well, you could do that, but it's tidier just to use
not
instead (eg. if not Test then
).True
or False
, right?
Wrong. Booleans are internally stored as bytes, which can of course contain
any number from 0 through to 255. Normally, False
is encoded as
0, and True
as 1. But sometimes things can get confused
(especially when you're interacting with external code which has possibly
been written in a different language, which might have different definitions
for True
and False
[for example, VB defines
True
as 255, not 1]), and you get some other value
instead.
That last one deserves a bit more explanation. Fortunately, in logic contexts
(for example, in the if
statement or when using the boolean
operators and
and or
) Pascal has a rule which says
"if it's zero, it's False
, and if it's not False
,
then it's True
". So if you somehow get a "17" in there, then
your code will still work if you just use if x then
or if
not x then
. But if you had said if x = True then
... well,
True
is 1, and 17 isn't 1 now, is it? So you've gotten yourself
into a weird situation where the variable is True
but is
not equal to True
.
Comparing against False
is not as dangerous. Just about
everybody agrees that False
is 0, and 0 is the only value that
can be False
. So if you really must use a comparison, you
can fairly safely do comparisons in terms of False
(eg. if
x = False then
, if x <> False then
). But really, it's
much cleaner if you just leave out the comparison altogether.
So, in summary: don't ever compare anything with True
!
In Pascal, you must declare a routine before it can be called. Sometimes that just isn't convenient, especially when you're trying to keep related functions together. This is where forward references come in handy:
procedure Routine2(); forward; procedure Routine1(); begin // ... Routine2(); // ... end; procedure Routine2(); begin // ... end; |
ExpandConstant
You'll frequently need to use Inno constants from within your [Code]. To do
this, you need to use the ExpandConstant
support function. Why?
Because the actual value of those constants isn't actually known until
installation time (and sometimes not even then -- {app}
for
example isn't known until after the wpSelectDir
page). This
means that the compiler can't do anything with them, and you need to call
a function to expand them at runtime.
Using ExpandConstant
is easy. Here's an example:
if FileExists(ExpandConstant('{app}\readme.txt')) then
... --
this checks to see if the readme.txt
file exists in the
application's folder. Note that you can put other text in there as well,
not only the constant itself. This is in fact recommended, since Inno
will automatically compensate and generate a valid path, whether
{app}
already has a trailing backslash or not.
ISPP variable "constants" (such as {#SomeVariable}
) are an
exception to this rule. You do not need to use ExpandConstant
on
them (unless of course the ISPP variable expands to contain {app}
or some other constant). This is because unlike the Inno constants, the
actual value of the ISPP constants is known at compile time.
Sometimes, to decide whether you should install a particular file or run a
particular subinstall task, you just need to use a more complicated condition
than that permitted by [Components]
, [Tasks]
,
Flags
, or Windows versions. That's when it's time to use a
Check
function. Again, these are covered pretty well in the
helpfile, so I'll be brief. Check functions must be
functions, and must return a Boolean. Normally they won't take any
parameters, but you can specify some if you want to.
Another point worth mentioning is that the Check parameter itself is considered outside of the Pascal "scope" rules, meaning that you do not need to define a forward reference for it, or to make sure your [Code] section comes before the others, or anything like that.
Now here comes the fun part. Check functions are not guaranteed to be called, nor are they guaranteed to be called only once. That's not quite as bad as it sounds, and when you think about it you will see why it's that way. Inno will call the Check function whenever it thinks, to the best of its knowledge, that the entry in question should be used. The Check function acts as a sort of tiebreak, in effect. If Inno has already decided that the entry shouldn't be installed, then there's no point in calling the Check function -- nothing it can do would change Inno's mind. If Inno thinks that it should be installed, however, it then gives the Check function a chance to veto it. And for some types of entries, Inno may need to ask the question again -- and if it does, then it will have to call your function again. Why? Simply because Inno has no idea what you're actually checking for. For all it knows, conditions might have changed in the interim, and while it shouldn't have been installed before, now it should be. The upshot of all of this is that you need to make sure that your Check functions simply answer the question, without any other side effects.
For most types of Check functions (those on [Files]
or
[Registry]
entries for example), Inno will wait until it's
performing the actual installation before asking. This means that you'll be
able to use custom wizard pages to make your decisions, among other things.
For some types, however (such as those on [Components]
), Inno
will ask the question almost immediately, before any wizard pages
have been shown. That's because of when Inno fills out all the pages -- just
before it calls InitializeWizard
, in fact. So for this type of
Check function you won't be able to use custom pages to help make your
decisions. If you're in doubt about when a particular Check function is
getting called, simply put a MsgBox into it. That'll quickly show you
exactly when and how often it's getting called -- although bear in mind that
a different version of Inno may end up calling it differently.
As mentioned above, Inno may call your Check functions more than once, just in case your conditions change. But what if your conditions don't change? What if your test is particularly lengthy, or won't work quite the same if you do it a second time? There are a couple of ways to resolve this:
InitializeSetup
or in CurStepChanged
(see
Hooking into the start or end of the installation).
Store the result into a global variable. In your Check function, simply
return that variable.var DotNet11Installed: (dnUnknown, dnNotInstalled, dnInstalled); function IsDotNet11Installed(): Boolean; begin; if DotNet11Installed = dnUnknown then begin DotNet11Installed := dnNotInstalled; if RegKeyExists(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322') then DotNet11Installed := dnInstalled; end; Result := DotNet11Installed = dnInstalled; end; function InitializeSetup(): Boolean; begin; DotNet11Installed := dnUnknown; Result := True; end; |
RegKeyExists
may not be the most complex test in the
world, and doesn't really need this sort of protection. But the
principle is sound.
One final note, before I leave the subject of Check functions: Check
functions are only called at install time, never
at uninstall time -- even if they're placed on an [UninstallRun]
entry. This is intended to cover situations where, for example, you have a
Check function that decides there's no need to install a particular
subcomponent. Similarly, you can place the Check function on the
[UninstallRun]
entry as well to indicate to it that there's no
need to run its uninstaller. Note that due to the way that Inno's uninstall
logs work, if the installer is run multiple times (perhaps when upgrading to
several successive versions), then the entry will be executed if the Check
function returned True
on any of those times.
(If you forgot to add the RunOnceId
, then it may even get called
multiple times!)
One common mistake is to think that Check functions are suitable for asking the user a question at uninstall time, such as whether to delete a set of data files or not. They're not suitable for this, as outlined above. If you want to do that, you'll have to do it using pure [Code], not with Check functions.
BeforeInstall
and
AfterInstall
parameters
Almost all "action" entries (such as [Files] and [Registry]) can be given a
BeforeInstall
or AfterInstall
handler (or even
both!). Again, the help file is pretty clear on how these work. But again,
I'll give a brief intro.
Either routine must be a procedure. They may take any number of parameters,
though typically you won't need any. And just like Check functions, they will
not be called if Inno decides (for whatever reason) that the entry in
question should not be installed. Unlike Check functions, however, they are
guaranteed to be called exactly once (if at all), and if the BeforeInstall
routine is called, then the AfterInstall
routine will be as well
(unless the installation failed and aborted on that particular entry).
These are handy for performing minor tasks relating to a particular file. For
example, you may put an AfterInstall
handler on a configuration
[File]
to first copy in a basic template file, and then update it
based on the user's choices in the wizard.
You'll sometimes need to write some complicated logic to obtain the
appropriate value for something (or at least, more complicated than that
permitted by Check
functions and the other standard mechanisms).
Code constants are your friends here. You can use code constants
almost everywhere you can use standard constants -- there are a few
[Setup]
entries that they don't make sense for, and of course
there's no point at all in using a code constant with
ExpandConstant
.
The format is very simple, and explained fairly well in the help file.
You can use either this form: {code:FunctionName}
or
this one: {code:FunctionName|Parameter}
. Note in
particular that there can be only one parameter, which is passed as a string
and does not need quotes around it (in fact, if you put quotes in
then they will be passed into the function as well).
In the [Code] section, you must declare a corresponding function as follows:
function
FunctionName(Default:
String):
String;
|
You can of course change the function and parameter name to whatever you wish, but the rest must stay the same. Note that even if you are calling it using the first form (specifying no parameter), the parameter is still required -- it will simply be assigned an empty string. You're free to either use the parameter or ignore it, as you wish. The function will only be called when Inno needs to obtain the actual value -- so if it already knows that the entry you've used it on should not be installed, then the function will never be called.
Just like Check
functions, order is unimportant here -- you do
not need to declare or define your function earlier than the line that uses
the {code:...}
constant. And also just like Check
functions, it's possible for the function to get called multiple times, even
if it's only being used on a single entry. So it's best to keep things
simple. In fact, typically these functions do little more than return the
contents of a global variable that was calculated earlier.
#include
I've said before that you should try to keep related routines together.
But even doing that doesn't always help, when you have a lot of routines
in the same script file. The answer? Break them out into different files,
of course! This is what the #include
directive is for. And
it's even supported by native Inno, so you don't need to have ISPP installed
to use it.
So what does #include
do? Basically it takes the contents of
the named file and treats it exactly as if you'd copied and pasted the whole
thing at the exact position where the #include
line was.
Thereby including one file within the other one (hence the name).
First of all, of course, you need to create a second file to put things into. This can be called whatever you want, and have any extension you want. I usually use .isi (for Inno Setup Include), but you'll probably find it easier to just keep using .iss (if you use any other extension, you have to keep switching to All files or you won't be able to see them in the Open dialog).
The very first line of the file should be the [Code] section opening. This
is fairly important -- don't even put any comments above this line. Why?
Because [Code] and regular Inno script have different comment styles, and
either could be in effect (it depends on where the #include
is
located). So it's safest to begin with a known state.
Next, put the global variables you want to define here, and finally the
routines. It's important to remember that everything shares a common
namespace, so you can't define a variable called "NameEdit
"
(for example) in both places. For this reason, you may want to put some
sort of prefix (for example, "MyPage_
" for MyPage.isi
)
on the names that are supposed to be "private".
Now you need to put the #include
line into your main script.
Remember that everything in the file gets treated as if it had been pasted
in where the #include
line itself is. The normal rules of
Pascal still apply -- you must declare something before you can use it.
This means that where you put the line is very important. If possible,
you need to put it after everything from the main script that it uses,
but before everything that uses it. You may need to rearrange your functions
a little to achieve this, or insert some forward
references.
The layout that I typically use is as follows (this is by no means the only way to do it, nor possibly even the best way to do it. It's simply the way I do it):
#include
sInitializeWizard
)I put "private" in quotes there because as I said earlier, these functions are not truly private -- they're actually accessible to anything after the #include line itself. Another possible structure goes like this:
#include .ish
files#include .isi
files.ish
files
.isi
files
This structure is a bit more work. It focuses on preventing your main script
from calling anything in the include files (apart from the designated
entrypoint routines) -- which the first didn't. It won't prevent your include
files from calling routines in the main script that they shouldn't (which the
first does). Neither one will protect against one include file calling
something in another (which shouldn't be allowed, if you're maintaining proper
separation) -- however there is a way to test for that. Once you've gotten
everything compiling and working properly, simply reverse the order of all the
#include
lines (don't swap the .ish
es and the
.isi
s, just the lines within each group). If it still compiles
and runs after that, then you can be confident that there's no coupling
between your include files.
For more detailed examples of #include
files in action, see my
ISD article. And for more information on one of the
main drawbacks to include files (and a workaround), see
Debugging with include files.
Having your script divided across multiple files is both good and bad for readability -- it's good for code isolation (keeping functions self-contained) and for keeping related routines together. It's not so good for browsing, as the Inno IDE can (at the moment) only open a single file at a time. (Though you can, of course, use a different editor as your IDE.) And unfortunately, it's absolutely terrible for debugging -- again, because the IDE can only load a single file. And here your options are more limited, since the Inno IDE is the only thing that can do debugging. So what can we do?
Well, you've basically got three options (though there are other variations on these themes):
MsgBox
around the problem area, and then just
run without debugging. Which boxes come up and what they contain can give
you quite a bit of information. Don't forget to remove the calls once
you're done! :)#include
line, and then
paste all that code in its place. Now you can do your debugging. When
you're done, you can cut the code back to the clipboard, uncomment the
include, and paste the code back into the original include file.#include
s), put the
following line:
#expr SaveToFile("debug.iss")
|
debug.iss
will get overwritten. You'll also need to make sure,
before each isolated debugging session, that the #expr
line is
still the very last one in the file. Some editor programs will insert new
sections at the end of the file, thereby putting the #expr
in
the wrong place.
Best practise is to not alter the user's system in any way, until they click
the final Next button (on the wpReady
page). Sometimes it's
tempting to carry out commands in response to the user's selection on custom
wizard pages, but you should fight that urge. The user must always be allowed
to go back and change their mind, or even cancel the entire installation,
without any consequences. As a result, the wizard pages are used simply as
information-gatherers, with action to be performed later. When is the best
time? That depends on what you're doing.
There are two places where it's nice and easy to attach additional actions to
(in addition to the BeforeInstall
and AfterInstall
parameters). Naturally enough, these are
the start of the installation process, and its end. I'll start with a
snippet:
procedure DoPreInstall(); begin // ... end; procedure DoPostInstall(); begin // ... end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssInstall then begin DoPreInstall(); end else if CurStep = ssPostInstall then begin DoPostInstall(); end; end; |
This is your starting point. CurStepChanged
is of course one of
the standard Inno event routines, and can only be defined once in your script
-- so if you've already got one, then you'll need to merge them (most likely
by moving code from your existing procedure into either DoPreInstall
or DoPostInstall
). And speaking of those routines:
DoPreInstall
will be called after the user clicks Next on
the wpReady
page, but before Inno installs any of the [Files]
and other standard script items.DoPostInstall
will be called after Inno has completed
installing all of the [Files], [Registry] entries, and so forth, and also
after all the non-postinstall
[Run] entries, but before the
wpInfoAfter
or wpFinished
pages. However it may
still be before some COM components are registered -- if Inno decides that
a reboot is required, it won't carry out the regserver
until
after the reboot.
I won't go into too much detail here, as there are much better places to find
information. Your first port of call should be Inno's help file --
specifically the "Custom Wizard Pages" topic, and the "Support Functions
Reference" and "Support Classes Reference". From there, you should also look
at two of the example scripts supplied with Inno, namely CodeDlg.iss
and CodeClasses.iss
.
Once you're familiar with the basic idea, you might also find it useful to read my ISD specification (which is by no means an official Inno spec, it just seems to me like a good way to do things).
But just as a quick overview, if you're coming to Inno 5 from having previously used custom pages in Inno 4, you'll find it considerably different.... but better :-) Rather than having to implement all the page-tracking yourself, it's all handled internally by Inno in the same manner as its own standard wizard pages, thereby allowing you far greater flexibility with much more readable code.
The general idea is that you should create all the wizard pages that could
be used by your setup up front, inside the InitializeWizard
handler -- even if most of the time the user will never see that page. This
allows Inno to keep proper "housekeeping" and ensures that pages don't get
mysteriously duplicated, which can happen if you try to follow the Inno4
model of creating the pages as the user reaches them. Instead, Inno provides
the ShouldSkipPage handler, which lets you determine under what conditions
the page should be shown and what conditions it should be skipped.
There are two different "groups" of custom pages provided by Inno. The
first are what I tend to call "template pages", and are created by the
various CreateInput
XXXPage
and
CreateOutput
XXXPage
functions. These are
prestructured for gathering or displaying a specific type of information,
and provide lots of additional properties and methods to help you with that
task. Unless your needs are esoteric, these are what you'll almost always
use when building custom pages. You'll find examples of these in the
CodeDlg.iss
example script.
The second are true custom pages, created by means of
CreateCustomPage
. This gives you a blank slate on which you
can put whatever controls you want, in whatever layout you want. Naturally,
you lose out on the helper functions, but you gain in flexibility. You'll
find examples of this in the CodeClasses.iss
example script.
And that's all I'll say on the topic for the moment. Review the examples, the help, and the ISD page, and that should get you on your way to developing custom pages of your own!
One common mistake people make when they start implementing their own custom pages is to take action the moment the user clicks on the Next button -- perhaps by writing some information to the Registry, perhaps even by beginning an automatic uninstall of the previous version of the application.
Don't do that.
Go back and read again what I said in Hooking into the start
or end of the installation. A user of your setup program should be able
to click their way all the way up to the wpReady
page, then
suddenly change their mind and click Cancel -- and their system should not
have been changed in any way. Only once they pass wpReady
and
agree to start the installation itself should it start to modify things.
The upshot of all this is that while you might be asking all these questions
of the user in your shiny new custom pages, you should simply save the
results until later on. Once the installation has actually begun, then it's
time to pull those answers out and act on them. Perhaps in
CurStepChanged(ssInstall)
or CurStepChanged(ssPostInstall)
,
or even a BeginInstall
/EndInstall
handler somewhere. But not before then.
This article is still a work in progress. I will be gradually extending it, as I get time, as new ideas come to me, and as I receive feedback on what areas you think it would be useful to cover. To submit some feedback, just use the link in the menu to the left (though you'll need to scroll back to the top of the page to see it). Don't be shy -- I welcome feedback of any sort, whether it is a simple comment, a suggestion, or a death threat wrapped around a brick. It's all good. Well, mostly all good :-) You'll also find me hanging around in the Inno support forums. I even answer questions occasionally.
|
webmaster@mirality.co.nz Website copyright © 2002-08 by Mirality Systems. Mirality Systems, MSI, Ultra Enterprises, UE, and their respective logos are trademarks of Gavin Lambert. |