To begin with (PCQ, Mar 96) we wrote a application
PCQSETUP.EXE that could read instructions from an INI file and install your application
files from distribution disks to the target machine. Lets recap the features of PCQSETUP.
It can
different directory on the target machine.
that call your application(s).
Engine (BDE), it can create a BDE alias that your application may require on the target
machine.
install process.
COMPRESS.EXE applications.
The only shortcoming with PCQSETUP was that you had to
write the INI file manually. An easy enough task for small applications with a limited
number of files. However, for a large application with scores of files, the task of
creating the distribution disks and creating an .INI file could prove daunting. So in our
next episode of RAD (Apr 96) we wrote the PCQEXP to give you an easy visual interface to
create an INI file for use with PCQSETUP.
What we plan to do this month is to write PCQSETUP, our
install utility, as a Delphi component. As a component, every time you need to write an
install application, you can drop the component on to a form, customize it to accommodate
any special install requirements and have your install program ready within minutes. You
can even develop the component further and distribute to others and make some money.
Now component creation is not a subject that we can pretend
to teach through these columns. It''s too vast and it is very technical. In this column we
will only discuss how to write the setup utility as a component. You will be able to
benefit from what follows only if you have gone through the Component Writer''s Guide that
ships with Delphi and have attempted creating simple components. Fortunately, the
component that we will create is a relatively simple one and we will need to focus on a
very narrow area of component creation.
If you aren''t geared up for component writing, but are a
Delphi programmer, you could still take advantage of this month’s column by
downloading the component from PCQ Online and making use of it. If you are not a Delphi
programmer, I am afraid this episode of RAD will be of little use to you.
Advantages Of Writing
PCQSETUP As A Component
Applications distributed as executables are written for end
users. Components, on the other hand, are written for developers. An application cannot be
customized, but a component can. It makes a lot of sense to write PCQSETUP as a component
since it is a utility that targets developers with a need to distribute applications.
Developers write applications that vary extensively in size and hue. It is unlikely that a
standard install routine will meet all their requirements.
Currently, PCQSETUP does what it is designed to do and
nothing more. (Nothing less either, we hope.) It provides just the core functionality
common to most install routines. But these often need to perform additional tasks such as
displaying an opening welcome screen, confirming that adequate diskspace is available, or
unzipping files compressed with algorithms other than what PCQSETUP can handle. It does
not make sense to add these features to PCQSETUP and in the process inflate its code size.
Not all the features will be used in all install routines. However, as a developer you
will want to retain the option to customize the install process to suite your special
requirement.
Before the advent of software components, it was not
possible to customize any application unless you had its code. With components, even when
you do not have access to the source code you can customize an application to do precisely
what you want and the way you want it. This is possible because the component writer
selectively exposes the internals of the application through properties and events.
Properties are internal variables that the component publishes''. By publishing an internal
variable, the component lets the user set its value at design time. The values that you
assign to a component property is internally stored in a private field or variable of the
component.
Properties, or ‘published’ variables, are
distinct from public component variables. The public component variables are also
accessible to the developer, but only at run time.
A component user can customize a component by either
assigning values to, or changing the values of its properties. For example, the component
that we will create will have a property called IniFileName. Since this is a property, the
developer can specify the name of the .INI file that the setup program must read at design
time. Contrast this with the PCQSETUP application, where the name of the
.INI file is hard
coded into the application. An alternative implementation, using a public variable, would
have been to prompt the user for the name of the .INI file at run time. I won’t seek
you opinion on how elegant a solution that would be.
Events provide an even more important mechanism for
customizing components. The application developer can customize the working of components
by writing event handling code. An event serves as a mechanism that links an occurrence
within the code of the component to the code written by the application developer. Events
may be triggered by the some action on the part of the user. Such events are referred to
as external events. For example, if the user clicks the mouse button, it may trigger the
OnClick event of a component. Events may also be triggered by code within the components,
in which case they are referred to as internal events.
In the component that we are about to write we would be
dealing only with internal events. For example, the code within the component will trigger
an event OnFileCopy after copying a file to the target machine.
Whenever an event occurs, the code within the components
looks for the event handler. The event handler is written by the developer. If the
component code finds an event handler, it executes the code within it before getting on
with its job. You can look upon events as methods in reverse. Methods represent component
code that developers can call from the code that they write. An event handler, on the
other hand, is a block of developer’s code that the component code calls in response
to an internal or external triggering.
The PcqSetup Component
The setup component that we are about to write is a non
visual component so we will derive it from the TComponent class of Delphi. It will have
just one property–IniFileName. The component user will assign to it the name of the
.INI file that the setup application should read instructions from. The
.INI file, as we
have already discussed, can be created either manually or by using
PCQEXP.
To allow for customization
the component will have four events
The OnBeginInstall event will occur before the setup
program starts reading the setup .INI file. Typically an event for this event will check
on disk space availability, or display a copyright agreement. The OnFileCopied event will
occur every time the setup program finishes copying a file to the target machine. An event
handler for this event maybe used to unzip/explode a file compressed with a program other
than COMPRESS.EXE. The OnMissingFile event will occur anytime the setup program does not
find a file that it is required to install. The component has a built in default behavior
to handle this contingency–It informs users about the missing file and lets them
specify a new source directory for the install files. An event handler will let you
override the default handling. Finally, the OnFinishedInstall event will occur after the
installation is completed. On completion of installation you may like to run the
application that they have just installed. You could write an event handler for this event
to do just that. However, you would need to keep track of the directory to which the main
application file was installed, since the user could have changed the target directory.
For this you would also need to write an event handler for the OnCopyFile event that would
store the directory to which the main application file was copied. This event handler
receives the name of the file just copied and the directory to which it was copied.
Events--Getting
Technical
We have seen that an event is a mechanism to link the code
within a component to the code written by an application programmer. We will now see how
this is done.
In Delphi, events are implemented in a manner similar to
properties. In fact, events are properties too. An event property differs from a data
property in one respect. The data properties, and the private variable that they
represent, have a data type -- an Integer or String. In the case of event properties the
private variables that they publish are invariably method pointers. In other words rather
than pointing to data these pointer point to code that the developer has written.
A developer can customize a component by changing the value
of a data property. Similarly, a developer can also customize a component by write event
handling code for event properties.
When you define an event you go through the following steps
lot worse than it actually is. We said earlier that events are method pointers. Now a
method call may involve passing parameters. The number of parameters and the parameter
types could vary from method to method. Before we can declare a pointer to a method we
need to define the type of method it points to.
method your event points to you are ready to declare the event.
user input, or you can trigger it on state change. In our install component, all the
events are triggered by state changes - Installation starts, File is copied etc.
made from one central point in the component rather than from several places. The usual
practice is to declare a virtual method in your component that calls the user’s event
handler and provides any default handling. If you do that, another developer can create a
new component by inheriting from your component and change its default handling of an
event by overriding the virtual method in your component.
Let''s look at some code snippets. I am sure it will lighten
things up and clear the theoretical cobwebs! First, we will see how we implement the
IniFileName property. Here are the relevant portions from the class definition.
In the private section of the component class,
TleInst, we
declare an object field FIniFileName of type String. In the published section we declare
the property IniFileName of type String. The ‘read FIniFileName’ in the
declaration specifies that to read the property value the component directly access the
internal storage field. However, when you assign a value to the property a private method
is used to write the value to the storage field. This mechanism enables the component to
update its status.
A look at the implementation of the private method should
illustrate this Implementation.
Before writing the new property value to the storage field,
we check if the value has changed. If it has we ensure that the INI file actually exists.
If it does, we free the existing TIniFile Object, extract the file path from the INI
filename and create a new TIniFile object. Only after updating the component status do we
assign the new name value to the storage field.
Implementing Events
We now study code snippets to implement event properties.
We will first present the relevant parts of the interface section to let you get the
complete picture, and then focus on parts of the code.
Recall the steps in defining events. Step one is to define
the event handler types
The event handler for the OnBeginInstall event receives a
single Boolean parameter–Cancel of type var. We define a pointer to the method as
TBeginInstallEvent. Since the parameter is of type var the user can change its value. The
default event handling code within the component examines the value of Cancel after
executing the event handler. If the value has been set to false the component aborts the
installation by raising an exception. For example, if the event handler finds that the
existing diskspace is inadequate for the installation it can set the Cancel parameter to
True.
We define event handler types for the other two events in a
similar fashion. (We will let you figure out the explanation.) For the OnFinished we use
the default event handler type which is of type TNotifyEvent. The event handler in this
case just receives a pointer to the component that originates the event. (You may like to
look up the on line help for more on the TNotifyEvent)
Moving on to step two for defining an event -- declare the
events as properties with corresponding storage fields. The event properties are declared
as follows in the published part of the class definition
Notice how we declare the event properties as of
TBeginInstallEvent, TMissingFileEvent, etc. The properties have their corresponding
internal storage fields that store pointers. These are declared in the private section as
Step three of defining an event is to setup a mechanism to
trigger the event. We centralize calls to the event handlers through virtual methods
defined in the protected part of the class definition as follows
Wherever in the code we need to trigger an event we place a
call to the virtual method corresponding to the event. This code snippet from the Execute
method of the component illustrates this
Before, the component reads the INI file, it triggers the
OnBeginInstall event with a call the BeginInstall method. The call is made with the Cancel
parameter set to False.
The final step in defining events is to setup the call to
the event handler. The BeginInstall method performs the task of looking for, and where
assigned, executing the event handling code. Have a look at its code.
The code checks to see if an event handler has been
assigned to the OnBeginInstall event. If it has, it calls the assigned event handler with
the Cancel parameter set to False. After executing the event handler it examines the value
of the Cancel parameter. If it has been set to False the default handling is to raise an
exception.
We will now see how the other three events are triggered
and called.
OnMissingFile and
OnFileCopied Events
The OnMissingFile and OnFileCopied events are triggered in
the InstallFile method of the component. Here is the code for the InstallFile method.
OnMissing and OnFileCopied
Events
The OnMissingFile event is of TMissingFileEvent type and
takes the following parameters
If you go back to the declaration of TMissingFile event you
will notice that the source and cancel parameters are of type var. The event handler can
allow the user to either point to a different source directory or cancel the installation.
Here is the code for the MissingFile method.
Notice how the MissingFile method calls the install file
method again if the user says -- Retry. This nesting can get chancy and easily blow your
stack. We keep track of the number of retries with the Inc(FailedCopy) part of the code.
After two bad retries the InstallFile method raises an exception, nevertheless.
Let us now look at the code for the FileCopied method.
The event handler for this event is called with the name of
the file that has just been installed and the directory in which it has been installed.
This lets the developer write code in the event handler to say unzip the file if it has
been compressed using an algorithm that the install program cannot handle.
The OnFinishedInstall Event
The OnFinishedInstall event is triggered with this call in
the Execute method of the component.
Conclusion
I am afraid we have space for just this much and no more.
We have barely managed to focus on creating events in components. We haven’t even had
time for an overview of component creation. As mentioned at the start, the complete source
code for the component is available for download from the DEV forum of PCQ Online as
STUPCOMP.ZIP. Those of you who do not have access to PCQ Online need not despair –
you will be able to get the complete source code on the Summer 96 CD-ROM to be distributed
free along with copies of the June issue of PC Quest.
With this episode we conclude the three part series in
which we have created a suite of applications to relieve you of your installation
headaches. We hope you have enjoyed the series and found it useful. We would love to hear
from you in either case. The feedback will allow us to better tune and focus the
forthcoming episodes.
The virtual method that makes the actual call is also
straightforward.
procedure TleInst.Execute; var . . . begin . . . {Finished copying files. Set up program group} CreatePgmGroup (IniFile); Wait(500); CreateAlias; FinishedInstall; ViewDoc; . . . end; |
procedure TleInst.FinishedInstall; begin if Assigned(FOnFinishedInstall) then FOnFinishedInstall(Self); end; |
procedure TleInst.FileCopied(FileName, Destination: TFileName); begin if Assigned(FOnFileCopied) then FOnFileCopied(FileName, Destination); end; |
procedure TleInst.MissingFile(FileName, DestName: TFileName; var Source: TFileName; var Cancel: Boolean); var DiskNo: LongInt; begin Inc(FailedCopy); {Check for planned disk changes here} DiskNo := IniFile.ReadInteger(‘Disk Change’, FileName, 0); if DiskNo <> 0 then begin if not InputQuery(‘Setup requires Disk ’ + IntToStr(DiskNo), ‘SourcePath’, Source) then raise Exception.Create(UserCancel); if Copy(Source, Length(Source), 1) <> ‘\’ then Source := Source + ‘\’; InstallFile(FileName, DestName); Exit; end ; {No disk change was planned here} if Assigned(FOnMissingFile) then begin FOnMissingFile(FileName, DestName, Source, Cancel); if not Cancel then InstallFile(FileName, DestName) else raise Exception.Create(UserCancel); end else if InputQuery(File + Source + FileName + is missing. Retry?, `Source'', Source) then begin if Copy(Source, Length(Source), 1) <> \ then Source := Source + `\''; InstallFile(FileName, DestName); end else raise Exception.Create(FileReadError + FileName); end; |
OnBeginInstall OnFileCopied OnMissingFile OnFinishedInstall |
type TleInst = class(TComponent) private . . . FIniFileName: String; procedure InstallFile(FileName, DestName: TFileName); . . . protected . . . published property IniFileName: String read FIniFileName write SetIniFileName; . . . end; |
.. procedure TleInst.SetIniFileName(FileName: String);begin if FileName <> then begin if not FileExists(FileName) then raise Exception.Create(‘INI file ‘ + FileName + ‘ not found!’); if IniFile <> nil then IniFile.Free; SourcePath := ExtractFilePath(Application.ExeName); IniFile := TIniFile. Create(SourcePath + FileName); end; FIniFileName := FileName;end; |
type TBeginInstallEvent = procedure (var Cancel: Boolean) of object; TMissingFileEvent = procedure( FileName, Destination: TFileName; var Source: TFileName; var Cancel: Boolean) of object; TFileCopiedEvent = procedure (FileName, Destination: TFileName) of object; |
type TleInst = class(TComponent) private . . . FOnBeginInstall: TBeginInstallEvent; FOnFileCopied: TFileCopiedEvent; FOnMissingFile: TMissingFileEvent; FOnFinishedInstall: TNotifyEvent; . . . protected procedure BeginInstall(var Cancel: Boolean); virtual; procedure MissingFile(FileName, DestName: TFileName; var Source: TFileName; var Cancel: Boolean); virtual; procedure FileCopied(FileName, Destination: TFileName); virtual; procedure FinishedInstall; virtual; public . . . published . . . property OnBeginInstall: TBeginInstallEvent read FOnBeginInstall write FOnBeginInstall; property OnMissingFile: TMissingFileEvent read FOnMissingFile write FOnMissingFile; property OnFileCopied: TFileCopiedEvent read FOnFileCopied write FOnFileCopied; property OnFinishedInstall: TNotifyEvent read FOnFinishedInstall write FOnFinishedInstall; end; type TBeginInstallEvent = procedure (var Cancel: Boolean) of object; TMissingFileEvent = procedure( FileName, Destination: TFileName; var Source: TFileName; var Cancel: Boolean) of object; TFileCopiedEvent = procedure (FileName, Destination: TFileName) of object; |
property OnBeginInstall: TBeginInstallEvent read FOnBeginInstall write FOnBeginInstall; property OnMissingFile: TMissingFileEvent read FOnMissingFile write FOnMissingFile; property OnFileCopied: TFileCopiedEvent read FOnFileCopied write FOnFileCopied; property OnFinishedInstall: TNotifyEvent read FOnFinishedInstall write FOnFinishedInstall; |
FOnBeginInstall: TBeginInstallEvent; FOnFileCopied: TFileCopiedEvent; FOnMissingFile: TMissingFileEvent; FOnFinishedInstall: TNotifyEvent; |
procedure BeginInstall(var Cancel: Boolean); virtual; procedure MissingFile(FileName, DestName: TFileName; var Source: TFileName; var Cancel: Boolean); virtual; procedure FileCopied(FileName, Destination: TFileName); virtual; procedure FinishedInstall; virtual; |
procedure TleInst.Execute; var Group: TGroup; I, J: Integer; DestDir: TFileName; Cancel: Boolean; begin try {The installation starts} Cancel := False; BeginInstall(Cancel); TotFiles := 0; FileCount := 0; IniFile.ReadSection(PgmSection, Groups); . . . |
procedure TleInst.BeginInstall(var Cancel: Boolean); begin if Assigned(FOnBeginInstall) then begin FOnBeginInstall(Cancel); if Cancel then raise Exception.Create(UserCancel); end; end; |
procedure TleInst.InstallFile(FileName, DestName: TFileName); var SrcFile : Integer; DestFile : Integer; RetCode : Longint; OpenFileBuf : TOFStruct; SrcFileName, DestFileName : array< 0..255 > of Char; Cancel: Boolean; begin if not FileExists(SourcePath + FileName) then begin {We don’t want the program going into the weeds with no stack space!!} if FailedCopy > 2 then raise Exception.Create(FileReadError + FileName); Cancel := False; MissingFile(FileName, DestName, SourcePath, Cancel); Exit; end; StrPCopy( SrcFileName, SourcePath + FileName ); if GetExpandedName(SrcFileName, DestFileName) < 0 then StrPCopy(DestFileName, DestName + ‘\’ + FileName) else StrPCopy(DestFileName, DestName + ‘\’ + ExtractFileName(StrPas(DestFileName))); SrcFile := LZOpenFile( SrcFileName, OpenFileBuf, of_Read ); try DestFile := LZOpenFile( DestFileName, OpenFileBuf, of_Create ); try RetCode := LZCopy( SrcFile, DestFile ); case RetCode of LZERROR_UNKNOWNALG: raise Exception.Create(‘The source file was compressed with an unknown algorithm’); LZERROR_WRITE: raise Exception.Create(‘The destination drive is full!’); else if RetCode < 0 then raise Exception.Create(‘Could not copy file!’); end; finally LZClose(DestFile); end; finally LZClose(SrcFile); end; FileCopied(FileName, DestName); end; |
FileName - The name of the file being installed DestName - The target directory SourcePath - The source directory for the file Cancel - A boolean |