0% found this document useful (0 votes)
542 views58 pages

Delphi Magazine - Aug-97 PDF

Uploaded by

Manny Ortiz
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
542 views58 pages

Delphi Magazine - Aug-97 PDF

Uploaded by

Manny Ortiz
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 58

Issue 24, August 1997 UK £7

Creating NT Services
John Chaytor looks at how to write your
own Windows NT Services

Mik
Mik e’s Eye
Beating The System eO Vie
from rriss rep w
Ann B or
Dave Jewell shows how to ual orland’s ts
C
delve inside Zip files and in N onferen
ashv ce
ille
find what’s there

Class Traps for Delphi


and C++Builder
Cyril Jandia wanted to be in at the birth
and death of his objects, so he came
up with the TClassTrap class

TObjec
t
RIP
Welcome to this electronic sample version of The Delphi Magazine which we hope
you enjoy. If you would like to take out a subscription you can go to the form by
CLICKING HERE FOR ANY COUNTRY or CLICKING HERE FOR USA AND CANADA.
If you would like to find out more about The Delphi Magazine or our new
publication Developers Review please visit our website at www.itecuk.com.

Contents
5 News 43 Implementing Class-Traps
Updates and product information For Delphi And C++Builder
Cyril Jandia wants to be in at the birth
8 Under Construction: and death of his objects so he came up
Delphi 3 Web Modules, Part 1 with a simple technique, encapsulated
Bob Swart begins a 2-part investigation in the TClassTrap class
into the new web modules included
with Delphi 3’s Client/Server Edition 47 Nashville BDC:
A Mike’s Eye View
17 Things You Can Do With Mike Orriss reports back on his
Standard Controls: TMenuItem experiences at Borland’s annual
Brian Long unravels more secrets of conference, held this year in Nashville,
owner draw controls, demonstrating how Tennessee
to spice up your menus with glyphs
50 More News From Nashville
22 Surviving Client/Server: Borland used their conference to make
Credit Payments, Part 2 some key announcements: here’s a
Following on from last month’s case summary
study, Steve Troxell wraps up the
project with more helpful design and 53 Review: ISGMAPI
implementation hints Your apps could have built in email
and fax capabilities at the drop of a
26 Creating NT Services hat with this component from Infinity
Services are a powerful feature of Software and a little help from MS
Windows NT, but how can you write Exchange, reviewed by Peter Hyde
your own? John Chaytor has the answers,
with example programs and Delphi 54 The Delphi Clinic
classes to make it simpler Brian Long with more answers to
your Delphi development problems,
40 Beating The System: Zip Files, including: making 256 colour bitmaps,
handing MsgDialog responses and
Peeking Under The Hood
making your apps iconic
The ubiquitous Zip file is everywhere
nowadays and Dave Jewell shows how to
delve inside and find what’s there 60 Tips & Tricks
Hints and helps from readers, including:
a typed TObject bug, modifying form files
42 One Last Compile... and BDE version incompatibilities
I love my company, and my company
loves me
64 On The Disk
Form a free trial version visit
www.ozemail.com.au/~mtntop

DataSentry Data Maintenance


Logic Process Corporation of Dal-
las, Texas, has released the Data-
Sentry Data Maintenance Utility, a
desktop database maintenance so-
lution for Paradox and dBASE ta-
The Delphi Magazine ISSN: 1365-1013
bles. Extending Database Explorer,
iTec, 9a London Road, BROMLEY
DataSentry offers flexible table
Kent BR1 1BY, United Kingdom?
validation and repair, plus table
Tel: +44 (0)181 249 0354 Fax: +44 (0)181 249 0376 pack, copy, delete, empty, rename,
Email: [email protected] Web: www.itecuk.com and clone functions. All operations
may be performed on any selection
of tables in a single session. The
Modelling Tool Link and a 6 month powerful table repair facility in-

News Bold for Delphi Subscription and


Support Service Program. Volume
licenses are also available. For
cludes a special multi-pass option
to conquer tough repair obstacles
such as auto-increment sequence
Bold OO-RAD more information visit www.bold- corruption.
During the Borland Conference ‘97 soft.com or email Henrik Jondell at The SelfCheck Data Maintenance
BoldSoft announced Bold for [email protected] API is a set of easy-to-use database
Delphi, claimed to be the first OO- maintenance routines which can
RAD development tool, offering de- Delphi2Java be integrated into your own appli-
velopers RAD performance com- New from PowerBBS Computing, cations. All the major functions
bined with true object oriented Delphi2Java converts complete provided in DataSentry are avail-
development. Delphi applications to Java, forms able in the SelfCheck API, which
Bold for Delphi is an OO-RAD and all. It can either produce appli- comes with full source and may be
tool supporting Borland’s Delphi. cations targeted to both lan- deployed royalty-free!
It consists of a number of frame- guages, or simply to move DataSentry costs $89.00 and
works and tools, integrated seam- applications entirely to Java. Sup- bundled with SelfCheck costs
lessly into Delphi’s component port is also included for converting $139.00. An introductory special of
architecture. The tool is driven by Delphi Object Pascal Code, to $99 for the bundle is available. For
the Active Model, an object model C++Builder. Jbuilder support is details contact Logic Process
active at design-time and run-time, planned for a future upgrade. Corporation on +1 214 340 5172,
designed in the Bold Model Editor Delphi2Java is priced at $99, in- fax: +1 214 341 9104, email
or reached through the Bold cluding updates. For more informa- [email protected] or visit
Modelling Tool Links, initially tion visit www.javadelphi.com, or www.logicprocess.com.
available for Rational Rose and SE- email [email protected]
LECT Enterprise. Since the model Dr.Bob’s Wizard Wizard
is active during run-time, code-free Remember Me... The ActiveForm and Merlin COM
features such as dynamically cre- User setup and configuration infor- editions of Bob Swart’s Wizard
ated forms and automatic drag- mation is usually a mixture of Wizard are now available on his
and-drop is available when using strings, booleans checkboxes/ra- website at www.drbob42.com. Bob
the included Model-Aware GUI dio buttons, sometimes floats, has a whole new collection of
controls. Bold for Delphi provides dates, integers and so on. Saving all Delphi and C++Builder Wizards, in-
automatic database creation and these different types for reinstate- cluding Table-2-HTML, Table-2-
modification, and uses fully imple- ment on program restart can be CGI, Source-2-HTML, TableForm,
mented cached updates. painful. SplashForm, ConvertForm, Re-
BoldSoft claim that developers Mountaintop Systems’ Remem- quired , UUCode and HeadConv C
using the product will gain in- ber All Suite aims to cut out the DLL header converter. Registra-
creased productivity since focus pain. TRememPanel, a descendant tion is available at a modest cost.
can be put on the business objects of TPanel, is designed to hold the
instead of the nuts-and-bolts of setup/configuration controls and Sisyphus
implementing object oriented looks after saving and restoring Sisyphus 2 will analyse Paradox or
systems. these settings automatically InterBase databases and Delphi
Bold for Delphi will be available to/from an INI file. Other compo- projects, presenting the data in
in September this year, priced at nents add further sophistication. highly configurable grids or re-
$2,600 for a single developer li- The Remember All suite is priced at ports. With just a few mouse-clicks
cense, which includes one Bold Aus$50, or Aus$80 with source. you decide the type of data, the

August 1997 The Delphi Magazine 5


sort order, filter and layout of the requested data. You Trial versions are available at www.component-
can save multiple versions of projects and databases cafe.com. Contact The Component Cafi on +1 713 621
and then compare them, showing all the changes 3350.
made. For more information, email BMoellenb@
aol.com or download a demo from http://members. GP-Version Change Control
aol.com/bmoellenb GP-Version is a change control system which supports
all file-based programming languages as well as other
Titan SQL Anywhere file types too. It also integrates directly into the Delphi
This is the latest addition to the Titan range of plug-in IDE and allows full change control of source code for
database engine replacements from Reggatta Systems single and multiple developers. The product does not
and supports all versions of Delphi and C++ Builder. differentiate between the source file pairs (eg
Titan products work with Delphi’s data-aware compo- PAS/DFM) found in Delphi projects and treats them as
nents and allow simultaneous use of the BDE. single entities.
Also close to release is Titan ODBC, which will pro- GP-Version allows one touch checking in/out of
vide ODBC connectivity without the need for the BDE. source units directly from the Delphi IDE. In addition,
For more information on Titan products, visit entire projects can be checked in and out with a click of
www.reggatta.com, email [email protected], call +1 the mouse. For more information on GP-Version visit
510 594 0110, or fax +1 510 594 0115. www.qsc.u-net.com

ActiveTEXT Information Please!


The Component Cafi have released its new ActiveTEXT If your company has products or services relevant to
control, available as a 16-bit VBX and 32-bit ActiveX. Delphi developers then we want to hear about them!
The developers claim ActiveTEXT provides power- Our deadline for news is the 8th day of the month
ful text display and editing features in surprisingly light preceding the cover date.
and fast controls. Text is limited only by available Send your information for the attention of the
memory and the control includes extensive character Editor, by email if possible, to [email protected] or
formatting, plus cut, copy, paste and delete. Other fea- failing that by fax to: +44 181 249 0376 or by snail mail
tures include multi-level undo/redo, search and re- if you really must!
place, selection by stream, line or column and
drag-and-drop.

6 The Delphi Magazine Issue 24


Under Construction:
Delphi 3 Web Modules, Part 1
by Bob Swart

T his month, we’ll explore a new


feature included only with the
Client/Server version of Delphi 3:
Application Type

Microsoft Server
Application Object

TISAPIApplication
Request Object

TISAPIRequest
Response Object

TISAPIResponse
DLL (ISAPI)
web modules. If you don’t have the
Client/Server version, this article Netscape Server TISAPIApplication TISAPIRequest TISAPIResponse
may help you decide whether to DLL (NSAPI)
purchase it! Console CGI TCGIApplication TCGIRequest TCGIResponse
Web modules come with a Application
number of new components (found Windows CGI TCGIApplication TWinCGIRequest TWinCGIResponse
on the Internet tab of the Compo- Application
nent Palette) and a new Wizard for
starting a Web Module project
➤ Table 1: Web application types
(which can be found in the
Repository after a File|New).
If we select the Web Server Appli- corresponding objects. Each type
cation option the Wizard asks of application uses a type-specific
which type of Web Server applica- descendant of TWebApplication,
tion we’d like to build (Figure 1). TWebRequest and TWebResponse.
ISAPI/NSAPI DLLs have the com- For an ISAPI or NSAPI application
mon advantage that these pro- client request information is
cesses on the Web Server typically passed to the DLL as a structure
➤ Figure 1
only have to be loaded once and and evaluated by TISAPIApplica-
can remain resident after the first tion, which creates the dispatcher,
load, so they eliminate the time- TISAPIRequest and TISAPIResponse program Project1;
{$APPTYPE GUI}
intensive loading/unloading we get objects. uses
when using CGI and WinCGI web A CGI standalone application is a HTTPApp, CGIApp,
Unit1 in ‘Unit1.pas’ {WebModule1:
applications. However, since the console application that receives TWebModule};
{$R *.RES}
internal logic isn’t much different client request information on stan- begin
Application.Initialize;
(certainly not for the Delphi 3 web dard input and passes the results Application.CreateForm(
module programmer), I decided to back to the server on standard out- TWebModule1, WebModule1);
Application.Run;
build a WinCGI web application put. This data is evaluated by end.
this time, so we can use the Intra- TCGIApplication, which creates the
Bob CGI Debugger version 2.01 dispatcher, TCGIRequest and
➤ Listing 1
(available on my new website at TCGIResponse objects.
www.drbob42.com) to test it on A Win-CGI standalone applica-
our local machine without the tion is a Windows application that form). A web module is just like a
need for a local web server. receives client request informa- new form, however, since it’s
Web server applications extend tion from a configuration settings auto-created in the same way as a
the functionality and capability of (INI) file written by the server and regular form. The main project
existing web servers. The applica- writes the results to a file that the source file contains code to prove
tion receives HTTP request mes- server passes back to the client. that (Listing 1).
sages from the web server, The INI file is evaluated by TCGI- Note that this WinCGI project
performs any actions requested in Application, which creates the uses the HTTPApp and CGIApp units,
those messages and formulates re- dispatcher, TWinCGIRequest and whilst an ISAPI/NSAPI project gen-
sponses that it passes back to the TWinCGIResponse objects. erated this way will use the HTTPApp
web server. Any operation we can and ISAPIApp units. The {$APPTYPE
perform with a (non-visual) Delphi WinCGI GUI} further specifies that it’s a
application can be incorporated After we click OK in the Web Mod- WinCGI application, as opposed
into a web server application. ule Wizard, we get a new project to a standard CGI application
Table 1 shows the four types of with a new empty web module (in- that specifies {$APPTYPE CONSOLE}.
web server applications and the stead of the regular empty new Of course, in both cases the

8 The Delphi Magazine Issue 24


application is non-visual, but the
CGIApp unit uses the APPTYPE com-
piler option to distinguish between
standard CGI and WinCGI applica-
tions. But they offer a quick way to
➤ Figure 2
change a WinCGI application to a
standard CGI application or vice-
versa. We can even switch to an
NSAPI/ISAPI application, by using
the ISAPIApp unit instead of CGIApp.
Other than that, the different
internet protocols are practically
invisible to the Delphi developer,
so just pick the protocol you like
most and join the rest of the article
(you’ll need to read the manual for
tips on how to debug ISAPI/NSAPI
applications).
A web module is actually more
like a data module, in that we can
only drop non-visual components
on it. While we’d put data access
components on data modules, for
web modules we can also drop
components from the Internet tab
➤ Figure 3
(Figure 2), but not the visual
NetManage ActiveX controls of
course. procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
Specifically, we can use one or begin
more of the following new compo- Response.Content := ‘<HTML><BODY>Hello, world</BODY></HTML>’;
end;
nents (from left to right, skipping
the first two components and
➤ Listing 2
the last eight from NetManage):

TWebDispatcher actually merge several web actions WebAction item, we have to select
TPageProducer into one web application, instead the WebActionItem1 in the Web
TQueryTableProducer of having to write a separate web Actions property editor, which will
TDataSetTableProducer application for each action). make the Object Inspector focus
If we click the Add button, the on WebModule.Actions[0] as well.
TWebDispatcher first action item will be created, Then, go to the events page, dou-
This component is already built named WebModule1.Actions[0] of ble click on the event OnAction and
into the web module, but we need type TWebActionItem, but with an write the code shown in Listing 2.
one if we want to use an existing alias of WebActionItem1 (Figure 3). I In this OnAction event handler,
data module as a web module. We say alias because there’s some- we can fill in the Response of our We-
can’t drop a TWebDispatcher com- thing going on behind the scenes bAction, which usually generates a
ponent into a web module as there here: the Object Inspector will use dynamic HTML file. Response is of
should be only one per module. As the WebModule1.Actions[0] name in type TWebResponse, which is an
you would expect, this component the component combobox, while abstract base class for all objects
is the “dispatcher” of actions and the actual name of the component that represent HTTP messages
events. will be WebActionItem1. The form sent in response to an HTTP
The property Actions of type definition in the code editor con- request message.
TWebActionItems contains a list of tains no reference to a WebAc- Our web application automati-
all actions that this web applica- tionItem component, so it should cally creates a TWebResponse object
tion can perform. Each action has a be clear by now that we’re dealing based on the TWebRequest object
number of subproperties: Name, with a sub-component of the web for an incoming HTTP request
PathInfo, Enabled and Default. module, namely Actions[0], that message. The TWebDispatcher for
The PathInfo is the action re- for our convenience is named the application then passes the
quest that is sent to the web server WebActionItem1. TWebResponse object to the TWeb-
and can be used to identify sub- Since it’s the only WebAction ActionItem associated with the
tasks to be performed by this sin- item, it’ll be the default one as well. TWebRequest object, so that the re-
gle web application (so we can To actually write code for this first sponse can be formulated. Of the

10 The Delphi Magazine Issue 24


many properties of the TWebRe- exception dialog pops up, showing the HTML result in output.htm that
sponse, the Content (of type String) exception EFOpenEror. we would expect.
is the most important: here we can Apart from the reason why we The fact that this is indeed a bug
assign whatever value we need to get this exception dialog, it proves can be found in the TWinCGIRe-
return (eg a dynamic HTML page). once again the value of the Intra- quest.Create constructor, which is
Normally, we would also need to Bob local CGI tester: a pop-up ex- defined (in CGIApp.pas) as shown
specify the format of the dynami- ception message on a Web Server in Listing 3.
cally generated output, like would not be seen by anyone (ex- We see that if the ContentFile
content-type: text/html, but this cept the web master if he happens and OutputFile are not specified on
can be specified in another prop- to be near) and would have the the command line they are ob-
erty of TWebResponse, namely Con- same effect as hanging the Web tained from the inifile. But we also
tentType, which has the value Application... Not the best way to see that the FServerData Output-
text/html by default. In our first keep your web space provider File is opened using the fmOpen-
Hello, world web application, we’ve happy! Write or fmShareDenyNone flags.
only defined the dynamic HTML So, what did we do wrong? Well, Which means that if the output file
output page, to consist of a single it turns out that there are two ways doesn’t exist it won’t be created!
line that should display Hello, to execute a WinCGI application. This is usually a problem the first
world. One is by only specifying the inifile time we run the WinCGI applica-
as a command-line argument to the tion on a new web server (unless
IntraBob v2.0 web application and trust the web the output file is removed after
Since we’ve already written one application to read the inifile and each request). The fix is to change
line of code, let’s see if we can get obtain the “form literal” data and the fmOpenWrite to the fmCreate flag
some immediate feedback and test “output file” specification from it. (on lines 410 and 507 in
our Delphi 3 WinCGI web applica- This is the technique that IntraBob CGIApp.pas). The fmCreate will
tion. A few issues back, we wrote uses. However, an earlier way for open the file in fmOpenWrite mode if
IntraBob (the latest version 2.0 is WinCGI applications to execute is it already exists, or just create it
available on my website at by supplying not one but three for writing if it doesn’t.
www.drbob42.com and is compati- command line arguments: the sec- Note that O’Reilley WebSite 2.0
ble with Delphi 2.01, C++Builder ond containing the name of the file works exactly as IntraBob and will
and Delphi 3), a CGI and WinCGI containing the form data and the show the exception (because the
testing application. We can use third containing the filename to output file is not created), while
that to test our project. For this, we write the output to. So, if we go to Microsoft IIS and PWS create the
need an HTML CGI test form, the command line and call pro- output file in the temp directory
defined as follows: ject1.exe again, but this time with beforehand, so they give no
project1.ini as the first argument, problem.
<HTML> any (or a non-existing) file as the Hmmm, a lot of trial and error to
<BODY> second argument and output.htm get even a minimal WinCGI app up
<FORM ACTION="project1.exe" as the third argument, things and running. But that’s usually the
METHOD="POST"> should work again. Well, close but biggest problem: getting started.
<INPUT TYPE=SUBMIT> no cigar, yet. We still get the same
</FORM> exception: cannot open file. Surely, TPageProducer
</BODY> it can’t be the input file (it doesn’t So far, we only wrote one real line
</FORM> even need to read the input). But it of code, to assign a dynamic HTML
turns out to be the output file that string to Response.Content in the
If you know HTML you will notice must already exist (so our web ap- WebModule1WebActionItem1Action
immediately that this form will plication only has to overwrite it). event handler. However, instead of
send no actual data to the web ap- So, if we create an empty file out- writing our own HTML code from
plication, since there is no input put.htm, and call project1.exe pro- scratch here, we can use a more
type other than the Submit button ject1.ini nul output.htm then RAD approach, by dropping a
available. That won’t be a problem, things work fine and we indeed get TPageProducer component into the
however (or at least, it shouldn’t
be!). Loading this HTML form in In-
traBob, we only need to specify in ➤ Listing 3
the CGI Options page that we’re
constructor TWinCGIRequest.Create(IniFileName, ContentFile, OutputFile: string);
dealing with a WinCGI application begin
FIniFile := TIniFile.Create(IniFileName);
(so the options will be written to if ContentFile = ‘’ then
project1.ini, which will be given as ContentFile := FIniFile.ReadString(‘System’, ‘Content File’, ‘’);
if OutputFile = ‘’ then
a command line argument to pro- OutputFile := FIniFile.ReadString(‘System’, ‘Output File’, ‘’);
FClientData := TFileStream.Create(ContentFile, fmOpenRead or fmShareDenyNone);
ject1.exe). If we click on the Submit FServerData := TFileStream.Create(OutputFile, fmOpenWrite or fmShareDenyNone);
button, however, we don’t get the inherited Create;
end;
expected result. Instead, an

12 The Delphi Magazine Issue 24


web module and use it to produce a
➤ Figure 4
set of HTML commands (optionally
based on an input template).
We should also change the Web-
Module1WebActionItem1Action event
handler as shown in Listing 4.
The TPageProducer has two help-
ful properties to either store or di-
rect to a HTML template file. The
HTMLDoc property contains a
StringList that stores the HTML
template directly in the compo-
nent itself, while the HTMLFile prop-
erty contains a filename to the
HTML template. This HTML tem-
plate is used by the PageProducer to
generate the Content string that is
assigned to the Response.Content
string (and returned from the web
server application).
So, we can now click on the Page-
Producer component on the web
module, click the HTMLDoc property
in the Object Inspector and enter
the contents of the HTML template
in the string list editor (Figure 4).
➤ Figure 5
Too bad it doesn’t do HTML syntax
highlighting yet, but it wouldn’t be
hard to write a dedicated property procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
editor for the HTMLDoc TStrings begin
property of the TPageProducer Response.Content := PageProducer1.Content;
end;
component.
Each string is a sequence of one
➤ Listing 4
or more HTML commands or
HTML-transparent tags. An HTML-
transparent tag has the form: procedure TWebModule1.PageProducer1HTMLTag(Sender: TObject; Tag: TTag;
const TagString: String; TagParams: TStrings; var ReplaceText: String);
begin
<#TagName Param1=Value1 if TagString = ‘TAG’ then
ReplaceText := ‘Welcome to Dr.Bob’’s Delphi Clinic’
Param2=Value2 ...> else
ReplaceText := ‘Error: Unknown tag offered...’
end;
The <#TagName may not contain any
spaces. The TagName identifies the
➤ Listing 5
HTML tag for the PageProducer,
which converts the entire tag into a
more meaningful HTML content TagNames defined, we should check If we test the WinCGI project
based on the value of the tagname the TagString argument and for we’ve written so far in IntraBob
and the optional parameter values. each specific TagName replace it again, we get the expected result
This is a transparent tag, since nor- with the required HTML. In this as shown in Figure 5.
mal HTML browsers don’t recog- case, we want to replace the <#TAG> The HTMLDoc property in combi-
nise the #TagName construct and tag, which means we must look for nation with the transparent Tags
hence won’t show anything. a TagString with the name TAG. The and the PageProducer1HTMLTag
The Content method converts optional Tag Parameters can be event handler gives us enough
HTMLDoc into a final HTML string by found in the TagParams argument. power to write a meaningful CGI,
calling the HandleTag method to The initial TagString in combina- WinCGI or ISAPI/NSAPI web appli-
convert each HTML-transparent tion with the TagParams can be used cation. However, there’s more.
tag in HTMLDoc. So, after we’ve filled to define a wide range of tag values Much more...
the HTMLDoc property, we should go to replace.
to the events page of the Object In- We can replace the Tag with any Actions
spector to create an OnHTMLTag content we want and return that For starters, we can put more than
event handler for the PageProducer. content in the ReplaceText string. one action into a CGI web applica-
Assuming there are more possible See Listing 5. tion. We can put a dynamic HTML

14 The Delphi Magazine Issue 24


as environment variables (for stan-
dard CGI applications) or in the INI
file for WinCGI applications.
Now that we have a second Web-
ActionItem, it’s also time to look at
some alternatives to the plain
HTML TPageProducer component:
the TDataSetTableProducer and
TQueryTableProducer components.

TDataSetTableProducer
➤ Figure 6
Usually, we don’t want to put a sim-
ple HTML page on the web, but
rather the contents of a database
or the result of a dynamic query. In
that case, we can use the more so-
phisticated TDataSetTableProducer
and TQueryTableProducer (the Ta-
ble part of their names refers to
HTML table output, not to data-
base tables). These two compo-
nents can be used to produce nice
looking HTML pages, consisting of
records formatted in HTML-tables
that look like DBGrids in the web
browser.
In order to use the DataSetTable-
➤ Figure 7
Producer1 we must first connect to
it from our second WebActionItem.
procedure TWebModule1.WebModule1WebActionItem2Action(Sender: TObject; This is done as shown in Listing 6.
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin The DataSetTableProducer must
Response.Content := DataSetTableProducer1.Content; be connected to a DataSource,
end;
which means it’s now also time to
drop a Table (or Query) on the web
➤ Listing 6
module. Note that we don’t need a
connecting DataSource (which is
form in the main form and let it call To define a new action for our web only needed to connect visual
another action. This can be done module, we need to click the web data-aware controls to datasets).
as follows, for example: module Actions property again. In For this to work, the BDE must be
the Editing WebModule1.Actions installed on the Web Server, of
<FORM ACTION="project1.exe/table" dialog, click Add to get a WebAc- course, including the demo data-
METHOD="post"> tionItem2. Now, we need to set the bases and aliases.
Enabled property of the first action For this example, we can drop a
Now, the ACTION doesn’t consist of to False (since we have two WebAc- TTable on the web module, set the
project1.exe anymore, but has a tions and we only want to test the DatabaseName property of the TTa-
special action URL added to it: /ta- latest one) and we can specify the ble to DBDEMOS, the TableName prop-
ble. The reason that this works is PathInfo of the second WebAc- erty to BIOLIFE.DB and set the
that the Web Server is able to parse tionItem, for example as /table DataSet property of TDataSetTable-
a URI (Uniform Resource Identi- (Figure 6). Producer to Table1 so we’re ready
fier) into subparts. For example: Note that I actually had to dis- to run.
able the first WebActionItem be- In order to specify which col-
http://www.drbob42.com/cgi-bin/ cause IntraBob wasn’t able to split umns are to be used in the DataSet-
project1.exe/table?Name=DrBob the command-line into a URL, TableProducer we need to set the
Query String, Logical Path and Active property of the TTable com-
is parsed into the following parts: Physical Path. Hence, the PathInfo ponent to True. Right after that,
inside the WinCGI application was click the Columns property (type
protocol: http never set and the WebActionItem1 THTMLTableColumns) of the DataSet-
host: www.drbob42.com was always fired from IntraBob TableProducer component and we
ScriptName: /cgi-bin/project1.exe (I’m working on supporting that as end up in a new property editor in
PathInfo: /table well). A real Web Server will be able which we can design the output
Query: ?Name=DrBob to provide this information, either the way we want it (Figure 7).

August 1997 The Delphi Magazine 15


switch quickly from one protocol
to another) that supports multiple
states, tables, queries and more
goodies.
Only for Delphi 3 Client/Server
users, unfortunately, so I’m also
working with Shoreline’s Chad Z
Hower on an article about Port-
cullis, the Internet Application
Gateway from ShoreLine, showing
how to write components compati-
ble with IAG.

Bob Swart (aka Dr.Bob, visit


www.drbob42.com) is a profes-
sional knowledge engineer
technical consultant using Delphi
➤ Figure 8
and C++Builder, freelance techni-
cal author and co-author of The
Most properties (like Align and ucer and QueryTableProducer can be Revolutionary Guide to Delphi 2.
BgColor) seem to work on the entire used, with Tags and replacement Bob is now co-working on Delphi
table instead of individual col- contents, to create customisable Internet Solutions, a new book
umns, at least at design time in the dynamic HTML pages. about Delphi and the internet/
property editor, which is a pity). intranet. In his spare time, Bob
Actually, it turns out that if we want Next Time... likes to watch videos of Star Trek
to specify formatting options for in- Next month, we’ll use the informa- Voyager and Deep Space Nine
dividual fields, we can click on any tion from this month’s column to with his 3 year old son Erik Mark
of these fields in the property edi- write a more complex CGI, WinCGI Pascal and his 8 month old daugh-
tor, go back to the Object Inspec- and ISAPI/NSAPI application (actu- ter Natasha Louise Delphine.
tor, and change them there (a bit ally only one, but we’ll see how to
unnatural: why can’t we do it in the
property editor?).
Anyway, after we close this prop-
erty editor, we’re ready to test the
output of the TDataSetTablePro-
ducer (Figure 8).
Of course, this will look even bet-
ter in a real web browser (rather
than the NetManage HTML control
that is the basis for IntraBob), and
the colours used are probably not
ergonomic, but you should have
the idea by now. There are a lot
more properties and events left un-
explored at this time (such as the
Caption, Header, Footer, RowAttrib-
utes and TableAttributes proper-
ties of the TDataSetTableProducer,
and the TQueryTableProducer that I
haven’t covered at all). But I have
to leave something for next time,
right?
We’ve seen that Delphi 3 web
modules offer a framework for CGI,
WinCGI and ISAPI/NSAPI web appli-
cations that support multiple
actions. The WebModule and WebDis-
patcher are responsible for dis-
patching the actions, while the
PageProducer, DataSetTableProd-

16 The Delphi Magazine Issue 24


Things You Can Do With
Standard Controls: TMenuItem
by Brian Long

W e saw in the last article in this


intermittent series that
there are many native Windows
Menu property points to. However,
more specifically these two menu
items sit on the first menu in Main-
Generally, there are Delphi VCL
ways of achieving what many of
the available flags do. So to keep
controls that support custom dis- Menu1, which is represented by a the TMenuItem component proper-
plays via the owner draw mecha- TMenuItem called Menu1. ties accurate and up to date you
nism. In addition to the button, The API used to set the menus up should only use this API for things
listbox and combobox (as covered for our requirements is ModifyMenu not available in the VCL. For exam-
previously), another control that (see Delphi’s online help file for ple, use the Checked property in-
supports owner drawing is a menu more details) and this takes a stead of the mf_Checked and
item. But, like the TButton class, the number of parameters: mf_Unchecked flags. Similarly, use
TMenuItem component does not sur- the Enabled property instead of the
face this ability to the VCL user. function ModifyMenu(Menu: HMenu; mf_Enabled, mf_Disabled and
However, just as we did with the Position, Flags, IDNewItem: mf_Grayed flags. Use the Break prop-
TButton, we can coax the TMenuItem Word; NewItem: PChar): Bool; erty as opposed to the mf_Menu-
into allowing us to owner draw it Break or mf_MenuBarBreak flags.
and get it to look like whatever our The first parameter is the handle of Also, use a TMenuItem with a cap-
creative minds desire. the menu whose item you wish to tion of a hyphen (or minus sign) in
In addition to supporting owner change in some way. For the two preference to the mf_Separator
drawing, menu items also support menu items in question this will be flag. Generally speaking it is also
automatically displaying a sup- Menu1.Handle. The second parame- advisable to use the Caption prop-
plied bitmap. This is somewhat ter tells Windows which menu item erty to set the text displayed by a
easier to set up than owner draw- on the menu we are referring to. text-only menu item instead of
ing and is fine if you only want a bit- This is specified either as the Win- using the mf_String flag.
map. However it seems that for dows menu command that the Flags which are sensible for use
most applications this is insuffi- menu item will generate when se- are mf_ByCommand and mf_ByPosi-
cient. If there is a need for a bitmap lected (for example MenuItem1.Com- tion (to confirm the intended inter-
in a menu in the first place, there mand) or by its (zero-based) pretation of the second function
will also be a need for text to be dis- position on the menu (so MenuItem1 parameter), mf_Bitmap to have a
played as well. The applications would be at position zero). supplied bitmap drawn for the
comprising Microsoft Office 97 are The third parameter is a bitmap menu item and mf_OwnerDraw for
prime examples of this. Where mask and specifies a number of arbitrary programmer-specified
there is a speed button that acts as things. The various flags that you content in a menu item.
a shortcut for a menu item, the cor- want to specify can be combined The fourth parameter is sup-
responding menu item shows the using the or operator. The resul- posed to be the menu command
speed button’s glyph just to the left tant value firstly dictates whether (again) for the menu item con-
of the usual caption. the second parameter will be a cerned, but in many cases can be
The first project accompanying command or a position. It also de- left as zero if you feel like it. The
this article is called ODMENU.DPR. cides what to do with the menu last parameter is extra information
This shows how to customise a item being modified. used to represent the contents of
menu item in two ways. At design-
time, the form in this project has
two menus. We will focus on the ➤ Listing 2
first one which has two items on it.
procedure TForm1.SetupMenus(const BmpName: String);
These two items have very uninter- begin
Bmp.LoadFromFile(BmpName);
esting captions since they are ini- BmpFileName := BmpName;
tialised and set up for proper { First menu item is set to be a bitmap }
ModifyMenu(Menu1.Handle, MenuItem1.Command, mf_ByCommand or mf_Bitmap,
display at run-time with the code in 0, PChar(Bmp.Handle));
{ Second menu item is set to be an owner draw-menu }
the program. The names of these { So refer to the wm_MeasureItem and wm_DrawItem }
menu items are MenuItem1 and { message handlers for the details }
ModifyMenu(Menu1.Handle, MenuItem2.Command, mf_ByCommand or mf_OwnerDraw,
MenuItem2 and they reside ulti- MenuItem2.Command, PChar(Bmp));
DrawMenuBar(Handle);
mately in a TMainMenu component end;
(called MainMenu1) that the form’s

August 1997 The Delphi Magazine 17


the menu item, in the shape of a proportions. Then, every time the The height of the menu item is ei-
PChar. In the case of a bitmap menu, menu item needs to be displayed ther the bitmap height or the
this must be the bitmap handle. and also when its state changes, it height of the path string, which-
For an owner drawn menu, this is is sent a wm_DrawItem message. Let’s ever is the higher.
any four bytes of additional infor- take these one at a time having as- When the menu item needs to be
mation you wish to be passed to certained that the owner drawn drawn, the code in Listing 4 exe-
the message handlers for the item will display the bitmap along cutes. You can see that we have
wm_MeasureItem and wm_DrawItem with the bitmap’s file name (includ- more checks to ensure we are be-
messages that will need to be serv- ing path). ing called for the right reason and
iced. The code used here passes The wm_MeasureItem message then the drawing logic starts.
the object reference for the target handler is shown in Listing 3. Note Rather than using clumsy native
bitmap object. that the object reference for the GDI calls, the code creates a Delphi
The first menu item, MenuItem1, is bitmap object in question is TCanvas object and uses this to rep-
told to represent a supplied bit- passed as custom information in resent the target device context
map and the second one is told to the ItemData field of the record whose handle is Msg.DrawItem-
be owner drawn. This is done in the structure that accompanies this Struct^.hDC. All drawing opera-
SetupMenus procedure as shown in message and the wm_DrawItem mes- tions on the TCanvas object are
Listing 1, where Bmp is a TBitmap ob- sage. Apart from the initial checks then propagated through to the
ject that loads a specified file. The to verify this is the right handler, appropriate device context.
DrawMenuBar call is made to ensure the rest of the code is a calculation. You should be able to see that
that the form’s menu is updated The width of the menu item is the the code differentiates between
visually as well as just in terms of width of the bitmap plus the width normal state and the menu being
its internal memory structure. of the text string used to represent highlighted (where ods_Selected
I mentioned above that the the bitmap file path. The width of
fourth parameter to ModifyMenu this string is calculated by using
➤ Figure 1
could be left as zero in many cases. the TextWidth method of the bit-
When setting up an owner drawn map’s canvas. When Windows has
menu, this is not the case: the information about the width of all
menu command must be specified. the items, it can determine the wid-
It is usually good practice to est one and make the menu that
specify it in all cases. wide.
The form’s OnCreate handler sets
up the TBitmap object and tells it to
load a particular file (you will need ➤ Listing 2
to change this to have an appropri-
procedure TForm1.FormCreate(Sender: TObject);
ate path). Also, just to show an- begin
other possible (undocumented) Bmp := TBitmap.Create;
{ Move Help menu over to the right }
flag that you can use, some code { Windows 95/NT 4 apps support doing it this way ... }
{$ifndef AlternativeWay}
executes to move the Help menu ModifyMenu(MainMenu1.Handle, 1, mf_ByPosition or mf_Popup or mf_Help,
HelpItem1.Handle, ‘&Help’);
over to the right hand side (see {$else}
Figure 1). This used to be common { ... only Win 3.1x supports this }
HelpItem1.Caption := #8 + HelpItem1.Caption;
practice although it is seen less {$endif}
{ Start off with some bitmap }
these days. Listing 2 shows that SetupMenus(‘c:\delphi\images\splash\16color\athena.bmp’);
there are two ways to approach end;
procedure TForm1.FormDestroy(Sender: TObject);
this problem, one of which uses a begin
Bmp.Free;
ModifyMenu call, employing the end;
mf_Help flag. The other approach,
that of prefixing the caption with a
tab character, seems to work only
in Windows 3.1x. ➤ Listing 3
So you can see that Delphi’s procedure TForm1.WMMeasureItem(var Msg: TWMMeasureItem);
ATHENA.BMP file is going to be begin
with Msg, MeasureItemStruct^ do
used for each menu item and the if (IDCtl = 0) and (CtlType = odt_Menu) and
(ItemID = MenuItem2.Command) then
mechanism for the bitmap menu is with TBitmap(ItemData), Canvas do begin
very clear. But what about the { Width is bitmap width + text width }
ItemWidth := Width + TextWidth(BmpFileName);
owner drawn menu item? { Height is Max(bitmap width, text width) }
if Height > TextHeight(BmpFileName) then
As we found when exploring ItemHeight := Height
owner-drawn buttons, Windows else
ItemHeight := TextHeight(BmpFileName);
will send a wm_MeasureItem message { Yes, we dealt with this message, all right? }
LongBool(Result) := True;
to the owner of a menu item (the end;
end;
form in this case) to identify its

18 The Delphi Magazine Issue 24


will appear in Msg.DrawItem- implemented manually in Delphi 1, property) is what determines the
Struct^.ItemState). Under normal but is a Win32 component that Del- transparency colour. So the code
conditions the menu item back- phi 2 and 3 can use). This will be asks the image list to turn all pixels
ground is filled in with clMenu, text used to generate a new version of of that colour in the original bit-
is drawn clMenuText and the bitmap the bitmap (which will end up in map into the current colour as
is copied normally. If the menu is another TBitmap object called used by a menu.
highlighted, it is filled with clHigh- TransBmp) that has appropriate This new transparent bitmap is
light, the text is drawn in clHigh- transparency. then given to the bitmap menu
lightText and the bitmap is Normally, the colour of the bot- item and the owner drawn item
inverted. Rather that using several tom left pixel of a TBitmap object (as and gives the much better result of
if..then..else blocks, the code returned by the TransparentColor Figure 4.
uses Boolean array typed constants
for brevity.
➤ Figure 2 ➤ Figure 3
The only other code is cosmetic:
both the bitmap and the text are
vertically centred so it looks neat
and tidy no matter whether the bit-
map is taller than the text or vice
versa. The end result is shown in
Figure 2.
If you look carefully, you might
notice that the bitmap menu item
starts drawing slightly further to
the right than the owner drawn
menu item. This is to take account
of the checkmark that might need
➤ Listing 4
to be drawn. If you set a bitmap
menu item’s Checked property to procedure TForm1.WMDrawItem(var Msg: TWMDrawItem);
const
True, a checkmark will obediently Colors: array[False..True] of TColor = (clMenu, clHighlight);
appear. Checkmarks are not auto- TextColors: array[False..True] of TColor = (clMenuText, clHighlightText);
CopyModes: array[False..True] of TColor = (cmSrcCopy, cmNotSrcCopy);
matically supported for owner var
Rect: TRect;
drawn items: if they are needed SourceBmp: TBitmap;
you will have to sort out the draw- begin
with Msg, DrawItemStruct^ do
ing logic yourself. To get the width if (Ctl = 0) and (CtlType = odt_Menu) and
(ItemID = MenuItem2.Command) and (HWndItem = Menu1.Handle) then
that the checkmark would take up with TCanvas.Create do
try
you can use the approach outlined SourceBmp := TBitmap(ItemData);
in Listing 5 { Yes, we dealt with this message, all right? }
LongBool(Result) := True;
This is all fine and dandy but a { Map TCanvas over menu DC }
Handle := hDC;
problem can be seen if the menu { Fill in background with appropriate colour }
colour is changed with Control Brush.Color := Colors[ItemState and ods_Selected <> 0];
FillRect(RCItem);
Panel: see Figure 3. In fact just run- { Centre bitmap vertically, in case shorter than the text }
Rect := Bounds(RCItem.Left, (RCItem.Top + RCItem.Bottom - Bmp.Height)
ning the apps (when compiled with div 2, SourceBmp.Width, SourceBmp.Height);
{ Draw bitmap, normal or inverted as necessary }
Delphi 1) under Windows 3.1x CopyMode := CopyModes[ItemState and ods_Selected <> 0];
shows the same problem, since the CopyRect(Rect, SourceBmp.Canvas, Classes.Rect(0, 0, SourceBmp.Width,
SourceBmp.Height));
menu background is white. { Set up same font as menus use }
Font.Size := 8;
The bitmap images are rather Font.Color := TextColors[ItemState and ods_Selected <> 0];
conspicuous. To fix this we will { Centre text vertically, in case shorter than the bitmap }
TextOut(RCItem.Left + SourceBmp.Width, (RCItem.Top + RCItem.Bottom -
need to trap the message that gets TextHeight(BmpFileName)) div 2, BmpFileName);
finally
broadcast when a system colour is Free
end;
changed (wm_SysColorChange). The end;
code inside it is very simple: in ad-
dition to doing whatever it nor-
mally does, we set up the menus all
over again (see Listing 6). OD- ➤ Listing 5
MENU2.DPR does this. CheckMarkWidth: Word;
To get the bitmaps looking okay, ...
{$ifndef Win32}
the implementation of SetupMenus CheckMarkWidth := LoWord(GetMenuCheckMarkDimensions);
has been extended (see Listing 7). {$else}
if Win32Platform = VER_PLATFORM_WIN32_NT then
In addition to loading the bitmap CheckMarkWidth := LoWord(GetMenuCheckMarkDimensions)
else
into the Bmp object, an image list ob- CheckMarkWidth := GetSystemMetrics(sm_CXMenuCheck);
{$endif}
ject is present (this was

20 The Delphi Magazine Issue 24


One or two finishing comments. Next time we will have a look at
The Office 97 menu system is a re- the TPageControl and TTabControl
implemented non-standard system and see what fun we can have with
which explains why you can drag it them, over and above what the
off the main application window’s VCL offers.
and leave it floating. That also ex-
plains why when you change the
menu colour in Control Panel it er- Brian Long is a UK-based freelance
roneously remains steadfastly Delphi and C++ Builder consultant
grey. However, those points apart, and trainer. He is available for
you should be able to simulate bookings and can be contacted at
those styles of menu by adding [email protected]. Professional en-
➤ Figure 4
speedbutton glyphs onto your quiries can go to consultancy@
menus. blong.com or training@blong .com

➤ Listing 6

TForm1 = class(TForm)
...
procedure WMSysColorChange(var Msg: TWMSysColorChange);
message wm_SysColorChange;
...
end;
...
procedure TForm1.WMSysColorChange(var Msg: TWMSysColorChange);
begin
inherited;
{ When system colour changes, make sure }
{ the bitmap’s transparent area is the }
{ same colour as the menu background }
SetupMenus(BmpFileName)
end;

➤ Listing 7

procedure TForm1.SetupMenus(const BmpName: String);


begin
Bmp.LoadFromFile(BmpName);
BmpFileName := BmpName;
{ Destroy and recreate the image list with the new bitmap in }
if Assigned(ImageList) then begin
ImageList.Free;
ImageList := nil
end;
{$ifdef Win32}
ImageList := TImageList.CreateSize(Bmp.Width, Bmp.Height);
{$else}
ImageList := TImageList.Create(Bmp.Width, Bmp.Height);
{$endif}
{ Add picture and generate mask }
TransBmp.Width := Bmp.Width;
TransBmp.Height := Bmp.Height;
BmpRect := Classes.Rect(0, 0, TransBmp.Width, TransBmp.Height);
TransBmp.Canvas.Brush.Color := clMenu;
TransBmp.Canvas.FillRect(BmpRect);
PicIndex := ImageList.AddMasked(Bmp, Bmp.TransparentColor);
ImageList.Draw(TransBmp.Canvas, 0, 0, PicIndex);
ModifyMenu(Menu1.Handle, MenuItem1.Command, mf_ByCommand or mf_Bitmap,
MenuItem1.Command, PChar(TransBmp.Handle));
ModifyMenu(Menu1.Handle, MenuItem2.Command, mf_ByCommand or mf_OwnerDraw,
MenuItem2.Command, PChar(TransBmp));
DrawMenuBar(Handle);
end;

August 1997 The Delphi Magazine 21


Surviving Client/Server:
Credit Payments, Part 2
by Steve Troxell

L ast month we started looking at


a credit payment processing
system with a few twists. I’ll briefly
methods of payment are entered in
the grid at the bottom.
Now the challenge becomes how
remains in each payment method
“bucket” after allocating payment
to the credits shown in the lower
recap the requirements of the sys- to distribute the payment given in grid. The lower grid lists each of
tem. A customer could have one or the lower grid across the selected the credits to which this payment
more outstanding credits. A single credits in the upper grid. For that is applied and allows the user to
payment could be applied to any we click the Allocate Payment but- enter the portion of each payment
one, some, or all of the customer’s ton and reveal the screen shown in method “bucket” to disburse to
outstanding credits. For example, a Figure 2. The upper grid is read each credit.
$3500 payment is recorded as a sin- only and updates automatically. It As amounts are entered in the
gle payment but could be applied shows the breakdown of the pay- lower grid, the Amount Remaining
to pay off a $1000 credit and a ment by method and how much entries for the payment methods
$2500 credit.
Further, a payment can be com-
➤ Figure 1
posed of different types of cur-
rency: cash, personal checks,
money order, cashier’s check, etc.
Each element of the payment must
be distinguished in the database
and any portion of these elements
may be freely disbursed across any
of the credits being paid off. Con-
tinuing with the example above, a
$3500 payment consists of $900 in
cash, $2100 in the form of one per-
sonal check and $500 in the form of
one cashier’s check. The payment
is applied to the two credits as fol-
lows: the $1000 credit is paid off
with $500 in cash and $500 from the
➤ Listing 1
personal check; the $2500 credit is
paid off with the remaining $400 in
create table Credits
cash, $1600 from the personal (
CreditNo integer not null primary key,
check and the $500 cashier’s Status char(1) default ‘V’ not null,
check. All of this must be tracked CustNo integer not null,
Amount float default 0 not null,
and all of this is considered a single BalanceDue float default 0 not null,
IssueDateTime date default ‘now’ not null
payment. );
Last time, we discussed ways to create table Payments
(
handle the issuance of the credits PaymentNo integer not null primary key,
CustNo integer not null,
and handling the different payment Amount float default 0 not null,
methods (check, cash, etc) in the );
PaymentDateTime date default ‘now’ not null

payment screen. This month we’ll create table PaymentCredits


complete the payment processing (
PaymentNo integer not null,
by covering the two-dimensional CreditNo integer not null,
Amount float not null,
allocation of the payment across BalanceDue float not null,
primary key (PaymentNo, CreditNo)
multiple methods and multiple );
credits. create table PaymentAllocation
(
The main payment screen is PaymentNo integer not null,
shown in Figure 1. The user selects CreditNo integer not null,
PayMethodCode char(2) not null,
one or more credits to apply the Amount float not null,
primary key (PaymentNo, CreditNo, PayMethodCode)
payment to from the grid at the );
top. The totals of the various

22 The Delphi Magazine Issue 24


detail-detail configuration. One
payment record is linked to one or
more credits to be paid, and each
of these is linked to one or more
payment amounts, one for each
payment method applied to that
credit. The table definitions are
shown in Listing 1 (I’ll repeat the
Credits table from last month for
completeness).
By way of illustration, Figures 3
and 4 show the arrangement of
data for the example given at the
beginning of this article.
Now you may think that the
amount of the payment is stored
redundantly but this was done to
➤ Figure 2
facilitate reporting of this informa-
tion. Summing the amounts from
PaymentAllocation to generate the
values we’ve stored in the other ta-
bles would get awkward in SQL, so
we pre-calculate them and make a
little redundancy to simplify the
reporting end of the system.
The BalanceDue in the Payment-
Credits table can be thought of as
redundant with the BalanceDue in
the Credits table. Keep in mind
that the one in Credits always re-
flects the current balance regard-
less of how many payments have
been made. The one in Payment-
Credits always reflects the balance
at the time that particular payment
was made. This was necessary due
➤ Figure 3
to a reporting requirement of the
system. If we joined to Credits to
PAYMENTS: get the balance each time, it would
PAYMENTNO AMOUNT be inaccurate as additional pay-
=========== ============
1 3500 ments are made against the credit.
PAYMENTCREDITS: As you can see from the specifi-
PAYMENTNO CREDITNO AMOUNT BALANCEDUE cation of the user interface and the
=========== =========== ============ ============
1 6 1000 0 layout of the payment tables, it
1 5 2500 0
would be a terrible idea to have the
PAYMENTALLOCATION: payment amounts post directly
PAYMENTNO CREDITNO PAYMETHODCODE AMOUNT
=========== =========== ============= =========== into the database tables via data-
1 6 CS 500 aware controls. That would pro-
1 6 CK 500
1 5 CS 400 duce a lot of unnecessary network
1 5 CK 1600
1 5 TC 500 traffic and database activity, and it
would be extremely difficult to
map the data fields in the dialogs
➤ Figure 4
directly to the table rows and col-
umns. Besides, all the manipula-
and the credits are automatically Amount Remaining values went nega- tion done in the Payment
updated. This screen has been par- tive. For example, in Figure 2 the Allocation dialog is worthless to
tially filled out. In a real system, er- user has over allocated the pay- the database until the user has re-
rors would be generated if the user ment towards the $1000 credit. turned to the main payment dialog
tried to post this screen and the en- To support this concept on the and posted the payment.
tire payment was not allocated out backend, the tables that record the It is more effective to store all
to all the credits or any of the payment information are a master- the data for the payment dialogs in

August 1997 The Delphi Magazine 23


Delphi until the user is done, then dialogs, our concern here is getting grid, we must change the Totals
translate the internal data into it into the database. The full source display on the right. Again, we use
something suitable for the data- code is available on the disk. OnSetEditText to tell us when any
base. In the actual application, we The one thing I would like to grid data has changed, but in this
used an Orpheus grid, which has point out is how the Amount Remain- case we really aren’t concerned
no inherent storage capacity of its ing values are made to automati- with which cell has changed. We
own. So I built a fairly elaborate cally update as the user keys in simply recalculate the total pay-
class structure to contain all the data. For this, we use the OnSet- ment and change the display as
payment information. Here, we’re EditText event handler for shown in Listing 2.
using TStringGrids which do hold TStringGrid. Each time the data in a
their own data, so we don’t need to cell changes, character by charac- Posting The Payment Data
go to great lengths to describe a ter, the OnSetEditText event han- To get the payment data from the
fancy external class structure. It dler fires and gives you an two dialogs into the data tables re-
doesn’t matter how the user infor- opportunity to do something with quires four steps.
mation is stored prior to posting to the cell data just changed. The pa- Firstly, add a single record in the
the database, the point is that we rameters of OnSetEditText tell us Payments table noting the total
do hold it in the Delphi app and which cell changed as well as its amount of the payment and gener-
programmatically post it to the new value. Knowing this we can ating a payment ID number. All the
database. simply recalculate the appropriate data associated with a single pay-
One big advantage to using a totals for that row and column in ment will be marked with the same
data structure to hold the payment the grids. payment ID number.
separate from the user-interface This same technique is used on Secondly, for each credit se-
controls is that it is much easier to the Main Payment Dialog (Figure lected to pay, reduce the balance
provide a “cancel” operation for 1). Whenever the user changes the due on the credit in the Credits ta-
the Payment Allocation dialog and payment breakdown in the lower ble by the amount of the payment.
abandon any changes the user may
have made to the payment alloca-
➤ Listing 2
tion. But we’re going to stick with
our TStringGrids for now. We need procedure TfrmPayment.grdPaymentSetEditText(Sender: TObject; ACol, ARow: Longint;
to keep track of which credits were const Value: string);
var
selected for payment, the payment I: Integer;
begin
amounts broken down by cate- { Update total payment amount }
TotalPaid := 0; { form variable }
gory, and the distribution of all the with grdPayment do begin
payment methods across each of for I := 1 to RowCount - 1 do
if Cells[1, I] <> ‘’ then
the selected credits. TotalPaid := TotalPaid + StrToFloat(Cells[1, I]);
end;
All our payment data is stored UpdateTotals; { local method to update the screen }
within the two grids in the Payment end;

Allocation dialog (Figure 2) except


for the ID numbers of the selected
credits. For that we simply use a
TList in the allocation dialog and ➤ Listing 3
use its pointer fields to hold the ID
create procedure PaymentSave(iCustNo integer, iAmount integer)
numbers of the credits we’ve se- returns (oPaymentNo integer)
lected. If we’ve created an instance as begin
oPaymentNo = gen_id(Gen_PaymentNo, 1);
of TList called Credits, then we insert into Payments (PaymentNo, CustNo, Amount)
values (:oPaymentNo, :iCustNo, :iAmount);
would add the credit ID number end
like this:

Credits.Add(Pointer(aCreditID));
➤ Listing 4
typecasting the LongInt ID number create procedure PaymentCreditSave(iPaymentNo integer,
into a pointer. Now, our list simply iCreditNo integer, iAmount integer)
as
holds a list of integer numbers declare variable NewBalance float;
rather than a list of pointers to begin
select BalanceDue from Credits
allocated memory. where CreditNo = :iCreditNo
into :NewBalance;
NewBalance = NewBalance - :iAmount;
update Credits
OnSetEditText set BalanceDue = :NewBalance
With one exception, I won’t go into where CreditNo = :iCreditNo;
insert into PaymentCredits (PaymentNo, CreditNo,
details about how the data is Amount, BalanceDue)
values (:iPaymentNo, :iCreditNo, :iAmount, :NewBalance);
stored internally in the grids and end
transferred between the two

24 The Delphi Magazine Issue 24


procedure TfrmPayment.btnPostClick(Sender: TObject); ParamByName(‘CreditNo’).AsInteger :=
var LongInt(Credits[C - FixedRows]);
PaymentNo: LongInt; ParamByName(‘PayMethodCode’).AsString :=
Amount, PChar(dmDataModule.
TotalPaidThisCredit: LongInt; PaymentMethodsList.Objects[
C, P: Integer; P - FixedCols]);
begin ParamByName(‘Amount’).AsFloat :=
with dmDataModule.dbDemo do begin Amount;
StartTransaction; Inc(TotalPaidThisCredit, Amount);
try ExecSQL;
{ post the main payment record } end;
with dmDataModule.spPaymentSave do begin end;
ParamByName(‘iCustNo’).AsInteger := CustomerNo; if TotalPaidThisCredit <> 0 then
{note TotalPaid was calculated in Listing 2} with dmDataModule.spPaymentCreditSave do
ParamByName(‘iAmount’).AsFloat := TotalPaid; begin
ExecProc; ParamByName(‘iPaymentNo’).AsInteger :=
PaymentNo := PaymentNo;
ParamByName(‘oPaymentNo’).AsInteger; ParamByName(‘iCreditNo’).AsInteger :=
end; LongInt(Credits[C - FixedRows]);
{ Post the payment amounts at finest granularity } ParamByName(‘iAmount’).AsFloat :=
with frmPaymentAllocation do begin TotalPaidThisCredit;
with grdCredits do begin ExecProc;
{ for each credit selected to pay } end;
for C := FixedRows to RowCount - 1 do begin end;
TotalPaidThisCredit := 0; end;
{ for each payment method for that credit } end;
for P := FixedCols to ColCount - 1 do begin Commit;
Amount := GetCellAmount(Cells[P, C]); except
if Amount <> 0 then Rollback;
with dmDataModule.qryPaymentAllocSave do raise;
begin end;
ParamByName(‘PaymentNo’).AsInteger := end;
PaymentNo; end;

➤ Listing 5
application. Listing 5 shows how the choices made here. In next
we pull all this together. Notice month’s column I’ll begin to look at
Thirdly, for each credit selected that we actually add the Payment- the new database access compo-
to pay, add a single record into the Allocation records before the Pay- nent hierarchy in Delphi 3 and how
PaymentCredits table, noting the mentCredits record in order to give we may derive custom dataset
payment ID, the credit ID, the total us the opportunity to sum up the classes.
amount of payment applied to that payment total for the credit.
credit and the balance remaining
on the credit. Conclusion Steve Troxell is a Senior Software
Lastly.for each different break- Well, that wraps things up for my Engineer with TurboPower Soft-
down of payment (cash, check, little tour around our credit pay- ware. He can be reached by email
etc) against a single credit, add a ment system. My hope is that at [email protected] or on
record to the PaymentAllocation ta- you’ve picked up a few odds and CompuServe at STroxell.
ble noting the payment ID, the ends for the reasoning of some of
credit ID, the code identifying the
payment method, and the amount
of that portion of the payment.
All of these steps should be
wrapped up within a single trans-
action to ensure everything is
posted consistently.
The first step is handled by the
stored procedure shown in Listing
3. We use an InterBase generator to
produce the unique payment ID
and return that to the application
so we can bind all the other pieces
of information under the same ID.
Steps 2 and 3 involve each credit
that is being paid and are handled
by the stored procedure shown in
Listing 4.
Step 4 is a simple insert into the
PaymentAllocation table and is han-
dled by a straight SQL query in the

August 1997 The Delphi Magazine 25


Creating NT Services
by John Chaytor

T his article shows how you can


create services to run under
Windows NT. Services can be use-
have two types of entry points de-
clared with the stdcall directive: a
ServiceMain procedure which is
(the delay period can be altered at
service start-up). It will accept
start, pause, resume and stop re-
ful on both a single workstation PC called on service start-up and a quests. To aid debugging we’ll add
and in a network environment. If ControlHandler procedure which is the capability to write to the event
you look at Settings/Control called to query or change the log (this can be used by standard
Panel/ Services you will be able to status of the service once it is applications also). Once we are
see a list of services defined on running. happy with that I’ll describe the
your machine. Examples of serv- A single program (process) can two classes I developed to encap-
ices that run on my machine are: contain the code for multiple serv- sulate the interface to the SCM
FTP, Gopher and WWW publishing, ices. This is useful if you have serv- which hides the service start-up
telephony, remote access, event ices that need to communicate. By and control mechanism allowing
logging and services supporting putting them in the same process you to concentrate on the
my video card. you avoid the overhead of inter- functionality of your service.
Don’t confuse the term service process communication. However, Delphi 2 does not come with the
with server in the client/server ar- if your service required a special Pascal version of the header file
chitecture. A Win32 service can in- logon-id the service must run in its WinSvc.H. I have supplied my ver-
deed be part of a client/server own process. sion of this, called WINSVCX.PAS,
relationship (SQL Server is an ex- So far so good, but what are the (so it won’t conflict with the offi-
ample of such a service) but it can drawbacks? Services can be diffi- cial version supplied with Delphi
equally be a stand-alone utility. cult to debug. I would recommend 3) on the disk. It only contains the
Two examples of the latter are that you develop the main logic of definitions required for this
automated backup and disk your service as a standard console article.
de-fragment utilities such as or basic GUI app then convert it to
Diskeeper. a service once you are happy with Service Program Structure
So what is so special about a it. Unlike standard console apps, in Listing 1 shows the source for the
service? It is an executable pro- a service you cannot write to the first demo program (on the disk as
gram but it runs under the control console via WriteLn (there may not DEMOSV1.DPR) minus error check-
of the Service Control Manager be any users logged on to the ma- ing and event logging. This pro-
(SCM). It needs to be defined in the chine). You must ensure that the id gram contains a single service.
registry. The SCM controls when that the service runs under (lo- There are special considerations
the service starts and ends, either calsystem by default) has the cor- required for shared service pro-
from information in the registry (in rect access privileges for all the cesses and these are highlighted
response to user requests from the required resources. It may be nec- as they arise.
Control Panel/Services applet) or essary to set up a special userid Although the program is usually
via API calls from applications. It and password specifically for your executed by the SCM it can also be
can be started automatically at sys- service. If you do this, remember to executed at the command line like
tem boot time before any user logs set the password never expires op- any regular console program. We
on. tion to avoid problems in 30 days can use this fact to our advantage.
Services do not need a user to be or so! When the SCM starts the program
logged on in order to execute, but If you access network drives it uses an entry in the registry (see
they can, if required, run under a from the service, ensure that you later) to get the fully qualified path
specific user id account. A service use the UNC paths to access the name for the EXE file: the services
accessing a database may need a drives (eg \\Admin01\C:\Payroll described here do not define a pa-
valid user id to enable it to connect \Files\Wages.DB). You cannot rely rameter. Hence, if the program is
to the DBMS. Services remain run- on mapping drive letters to a net- started without parameters it can
ning when a user logs off. work drive, as these may be assume that it is being started by
Services can have dependency invalid. the SCM. If the program is passed a
information stored in the registry parameter this means that a user
of services that need to be started Getting Started started the program at the com-
before starting the current service. Initially we will create a very sim- mand line. In the demo programs
The SCM will start them in the cor- ple ‘service’ that just beeps peri- provided I use this fact to auto-
rect sequence. odically to show that it is running, matically install or uninstall the
Services are usually linked as to allow us to explore the interac- service by passing a parameter of
console applications. They must tion between the SCM and a service install or uninstall. This idea

26 The Delphi Magazine Issue 24


came from examples in the MSDN Process Start-Up StartServiceCtrlDispatcher within
and avoids the need to use the SC After the SCM starts the process it the timeout period. You’d then
utility or manually edit the regis- waits for the process to call the need to implement a mechanism to
try. See the file SERVICES.TXT on StartServiceCtrlDispatcher func- inform the services that process
the disk for further MSDN tion to register all the services it initialisation has completed before
references. contains. A single service process they are allowed to start.
As well as the main entry point, a (such as DemoSv1) should call this Note that if you accidentally
service program must have two ad- immediately as any service initiali- start the program yourself from
ditional entry points. ServiceMain sation can be done when Service- the command line without passing
is the entry point for the main Main is called by the SCM. A shared parameters, the call to StartServ-
thread of the service. A shared service process should do any iceCtrlDispatcher will timeout as
service process may have more process-wide initialisation first (ie the SCM is not expecting the call.
than one of these entry points or it things that need to be done before No damage will be done though.
may use the single entry point for any service starts) before it calls The program will only be started
all services. I use a single entry StartServiceCtrlDispatcher. How- by the SCM when the first service
point (see DemoSv2). ControlHandler ever, if this initialisation is going to needs to be started. If additional
is the entry point called to update take over 30 seconds the SCM services are to be started in the
or query the running status of the would timeout and assume that an same process the SCM will simply
service. Each service must have its error has occurred. To get round call the relevant ServiceMain entry
own unique entry point. this you would need to create an point for the service being started
extra thread to perform the pro- as specified in the table passed to
cess wide initialisation and call StartServiceCtrlDispatcher.
➤ Listing 1

unit DemoSv1; While not FTerminated do begin


Uses Sleep(BeepDelay);
Windows, SysUtils, Registry, WinSvcX, Demo1Log; if not (FServiceStatus.dwCurrentState =
const SERVICE_PAUSED) then
DemoServiceName = ‘DemoService1’; MessageBeep(0);
DemoServiceDisplayName = ‘Demonstration service 1’; end;
FTerminated: Boolean; if FServiceStatus.dwCurrentState =
FServiceStatus: TServiceStatus; SERVICE_STOP_PENDING then begin
FServicStatusHandle: SERVICE_STATUS_HANDLE; { Do cleanup processing here }
procedure DemoServiceHandler(Code: Integer); StdCall; FServiceStatus.dwCurrentState :=
begin SERVICE_STOPPED;
case code of SetServiceStatus(FServicStatusHandle,
SERVICE_CONTROL_STOP: FServiceStatus);
begin end;
With FServiceStatus do begin end;
dwCurrentState := SERVICE_STOP_PENDING; end else With FServiceStatus do begin
dwWin32ExitCode := 0; dwCurrentState := SERVICE_STOPPED;
dwServiceSpecificExitCode := 0; dwWin32ExitCode := 666;
end; { Set a code to indicate reason for failure }
end; SetServiceStatus(FServicStatusHandle,
SERVICE_CONTROL_PAUSE: FServiceStatus);
FServiceStatus.dwCurrentState := SERVICE_PAUSED; end;
SERVICE_CONTROL_CONTINUE: end;
FServiceStatus.dwCurrentState := SERVICE_RUNNING; end;
end; end;
SetServiceStatus(FServicStatusHandle,FServiceStatus); { Main() entry point }
if FServiceStatus.dwCurrentState = SERVICE_STOP_PENDING var
then Param: ShortString;
FTerminated := True; ServiceEntryTable: PServiceTableEntry;
end; begin
Procedure DemoServiceMain(NumArgs: DWord; Args: PCharArray); FTerminated := False;
StdCall; Param := UpperCase(ParamStr(1));
var if (Param = ‘INSTALL’) or (Param = ‘I’) then
InitialisedOK: Boolean; InstallService
BeepDelay: Integer; else
begin if (Param = ‘UNINSTALL’) or (Param = ‘U’) then
BeepDelay := 1000; UninstallService
FServicStatusHandle := RegisterServiceCtrlHandler( else
DemoServiceName,@DemoServiceHandler); if (Param = ‘VERSION’) or (Param = ‘V’) then
if FServicStatusHandle <> 0 then begin DisplayVersion
FillChar(FServiceStatus,sizeof(TServiceStatus),0); else
With FServiceStatus do begin if Param = ‘’ then begin
dwServiceType := SERVICE_WIN32_OWN_PROCESS; { We should have been called by the SCM,
dwCurrentState := SERVICE_START_PENDING; so connect to it }
dwControlsAccepted := SERVICE_ACCEPT_STOP or ServiceEntryTable :=
SERVICE_ACCEPT_PAUSE_CONTINUE; AllocMem(2*SizeOf(TServiceTableEntry));
end; try
{ Set status to pending before we do our ServiceEntryTable^.lpServiceName:=
initialisation } DemoServiceName;
if SetServiceStatus(FServicStatusHandle, ServiceEntryTable^.lpServiceProc:=
FServiceStatus) then begin @DemoServiceMain;
{ Do initialisation here. If it takes > 1 sec you { The CtrlDispatcher loops round waiting for
should call SetServiceStatus passing wait hints control requests for the service(s) detailed
and checkpoints to show progress is being made } in the ServiceEntryTable array. It will not
{ Simulate time taken to initialise } return until the all services in the process
Sleep(1000); terminate (or an error has occurred) }
InitialisedOK := True; StartServiceCtrlDispatcher(ServiceEntryTable^);
{ We assume initialisation was OK for this demo! } finally
if InitialisedOK then begin FreeMem(ServiceEntryTable);
FServiceStatus.dwCurrentState := SERVICE_RUNNING; end;
if SetServiceStatus(FServicStatusHandle, end else
FServiceStatus) then begin DisplaySyntaxOptions;
{ Main loop of service process } end.

August 1997 The Delphi Magazine 27


The StartServiceCtrlDispatcher dwServiceType SERVICE_WIN32_OWN_PROCESS (there is
function is passed a dispatch table. only one service in this process) or
This is a NULL delimited array of SERVICE_WIN32_SHARE_PROCESS (there is
TServiceTableEntry structures: one more than one services in the process).
for each supported service. This dwCurrentState SERVICE_STOPPED or SERVICE_START_PENDING
structure consists of two fields: a (this is the initial value) or
pointer to a NULL terminated string SERVICE_STOP_PENDING or SERVICE_RUNNING
containing the service name and a or SERVICE_CONTINUE_PENDING or
pointer to its ServiceMain entry SERVICE_PAUSE_PENDING or SERVICE_PAUSED.
point. Note that as you supply the dwControlsAccepted A combination of the flags
address of the entry point this al- SERVICE_ACCEPT_STOP,
lows you to call this function any- SERVICE_ACCEPT_PAUSE_CONTINUE and
thing you wish (its name is not SERVICE_ACCEPT_SHUTDOWN. This lets the
exported). In DemoSv1 it is called SCM know what instructions can be passed to
DemoServiceMain. the service. These can be changed during the
When you call StartServ- life of the service.
iceCtrlDispatcher, the function re- dwWin32ExitCode The two ExitCode fields are used to pass the
mains in a loop waiting to receive return code back to the SCM when service stops.
dwServiceSpecificExitCode
commands from the SCM to be dis-
patched to the service(s) in the dwCheckPoint See Lengthy operations in the article.
process. Due to this, the function dwWaitHint See Lengthy operations in the article.
does not return until all services in
the process have terminated.
➤ Table 1: TServiceStatus fields
If the call to StartService-
CtrlDispatcher succeeds it will
start the required service. To do TServiceStatus = Record
dwServiceType: Integer;
this, the dispatcher creates a dwCurrentState: Integer;
thread and calls the ServiceMain dwControlsAccepted: Integer;
dwWin32ExitCode: Integer;
entry point supplied in the dis- dwServiceSpecificExitCode: Integer;
dwCheckPoint: Integer;
patch table. This is an important dwWaitHint: Integer;
End;
point. We do not need to create
a thread for the service. The
➤ Listing 2
dispatcher does this for us.

Service Start-Up DemoSv1. It will be called from the In the DemoSv1 example it simply
The DemoServiceMain procedure is dispatcher whenever there is a re- goes into a loop and beeps each
passed two parameters. The first is quest to change the status of the time round. Not very exciting!
the number of arguments and the service (eg pause, restart or stop) However, being a service, it will
second is a pointer to an array of or interrogate its current state. stay executing if you log off the ma-
PChars (the arguments). The first The handle returned from Regis- chine, beeping away to itself. While
argument is always the name of the terServiceCtrlHandler will be in its process loop, the service
service being started. This allows a unique for the service. This needs checks two fields: FTerminated, to
single entry point to be common to to be stored away, as it must be see if it should stop processing,
multiple services (more on this used when informing the SCM of and the contents of dwCurrentState
later). Any optional parameters the status of the service. to see if the service has changed its
which follow are the start-up pa- After registering the Handler status to paused.
rameters typed on the Control procedure, the ServiceMain proce- These states are set by the code
Panel Services applet by the user. dure needs to inform the SCM of its in the ServiceHandler. If you re-
This information can be used by current status. This is done via a member, this is called by the SCM
the service in any way you wish. call to SetServiceStatus. This when it needs to query or change
DemoSv1 uses the last parameter to function accepts the service han- the state of the service. If it re-
specify the delay between beeps. dle (returned from RegisterServ- quests that the service be stopped,
In the DemoServiceMain proce- iceCtrlHandler) and a pointer to a paused or resumed the code sim-
dure the first thing that needs to be TServiceStatus structure. Listing 2 ply updates dwCurrentState to indi-
done is to call the RegisterServ- shows the format of this structure cate this. It does not suspend or
iceCtrlHandler function. This and Table 1 shows the meaning of resume the service thread! If you
expects two parameters: a pointer the fields. did this you may pause the service
to the service name and the entry At this point we would start do- in the middle of processing a re-
point of the Handler procedure ing processing specific to our serv- quest. In the examples it is up to
described earlier. This procedure ice (eg waiting on a TCP port, setup the main service to respond to
is called DemoControlHandler in timers for scheduling etc). these requests. For example, if a

28 The Delphi Magazine Issue 24


Operation Initial status Final status the status to SERVICE_STOPPED and
inform the SCM via a call to Set-
Start SERVICE_START_PENDING SERVICE_RUNNING ServiceStatus. If the service fails to

Stop SERVICE_STOP_PENDING SERVICE_STOPPED respond promptly the SCM will


return an error.
Pause SERVICE_PAUSE_PENDING SERVICE_PAUSED If this is the last service to stop
(it is in DemoSv1) the original call
Continue SERVICE_CONTINUE_PENDING SERVICE_RUNNING
to StartServiceCtrlDispatcher re-
turns and the process terminates.
➤ Table 2: Pending states

Service Configuration Data


procedure InstallService;
If you need to store information for
var your service it is recommended
hSCManager: SC_Handle;
hService: SC_Handle; that you store it under the unique
Begin
hSCManager:= OpenSCManager(nil,nil,SC_MANAGER_ALL_ACCESS); key in
If hSCManager <> 0 then
try
hService:= CreateService(hSCManager,DemoServiceName,DemoServiceDisplayName, HKEY_LOCAL_MACHINE\SYSTEM\
SERVICE_ALL_ACCESS,SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL, CurrentControlSet\Services\
PChar(ParamStr(0)),nil,nil,nil,nil,nil);
if hService <> 0 then begin
NameOfService.
WriteLn(‘Service was installed successfully.’);
AddEventDetailsToRegistry;
end else Lengthy Operations
WriteLn(Format(‘Failed to create the service. Error was ‘’%s’’’,
[SysErrorMessage(GetLastError)])); Start, Stop, Pause and Continue re-
finally quests can potentially take some
CloseServiceHandle(hSCManager)
end else considerable time to complete
WriteLn(Format(‘Failed to open Service Control Manager. Error was ‘’%s’’’,
[SysErrorMessage(GetLastError)])); successfully. As the SCM may time-
End;
out before the service has com-
procedure UninstallService;
Var pleted the request a mechanism
hSCManager: SC_Handle;
hService: SC_Handle; has been defined to inform the
Begin SCM that the action is still being
hSCManager := OpenSCManager(nil,nil,SC_MANAGER_ALL_ACCESS);
If hSCManager <> 0 then processed. This is done via the
try
hService := OpenService(hSCManager,DemoServiceName,SERVICE_ALL_ACCESS); dwCheckPoint and dwWaitHint fields
if hService <> 0 then of the TServiceStatus structure. At
try
if DeleteService(hService) then begin the start of the operation the
WriteLn(‘Service was uninstalled successfully.’);
RemoveEventDetailsFromRegistry; status needs to be set to one of the
end else
WriteLn(Format(‘Failed to delete service. Error was ‘’%s’’’, pending states shown in Table 2.
[SysErrorMessage(GetLastError)])); Before setting the status the
finally
CloseServiceHandle(hService); dwWaitHint should be set to the
end else
WriteLn(Format(‘Failed to open service “%s”: Error was ‘’%s’’’, maximum time (in milliseconds)
[DemoServiceName, SysErrorMessage(GetLastError)])); that the SCM should wait before it
finally
CloseServiceHandle(hSCManager) will be updated again with the new
end else
WriteLn(Format(‘Failed to open Service control Manager. Error was ‘’%s’’’, status. This is repeated until the
[SysErrorMessage(GetLastError)]));
End; operation is completed. The
dwCheckPoint field should be incre-
mented to show the progress of
➤ Listing 3
the operation. At the end of the op-
eration the status field should be
service is paused, it should honour passed a request to pause or re- set to the value shown in Table 2.
all requests that it is currently sume the service the routine sim- At this point, the dwCheckPoint field
servicing then refuse to process ply updates the status. The service should be reset to zero. See
any more requests. The examples thread must react to this. When the DemoSv2 for an example of how this
do this by checking the current SCM requests that the service be could be implemented.
service state. stopped, the handler sets the
The code parameter passed to status to stop pending. It then sets Installing A Service
DemoServiceHandler indicates what the FTerminated flag to True. At the Listing 3 shows the calls made to
the SCM is requesting. The handler end of the case statement the Set- automatically register the DemoSv1
consists of a single case statement ServiceStatus call will update the service to Windows. This is done if
specifying each option it is expect- SCM to ‘stop pending.’ The service you pass INSTALL or I as a parame-
ing from the SCM. Regardless of thread checks this field periodi- ter to the program. There are es-
why the SCM called this routine it cally to see if it should terminate. sentially three calls required. First
should always call SetServiceSta- At this point it should do all re- a call is made to OpenSCManager to
tus to update the SCM. If we are quired cleanup. It then needs to set connect to the SCM: you will need

August 1997 The Delphi Magazine 29


administrator privilege for this to hSCManager Handle from OpenSCManager.
be successful. If this was success-
ServiceName Service name.
ful a call is made to CreateService
to create the service. The declara- DisplayName Text that appears on the Control panel services applet.
tion for this API is shown in Listing DwDesiredAccess Level of access required. Need update access.
4, and Table 3 shows the meaning
DwServiceType Indicates if the process runs 1 or more services.
of each parameter.
Once the service entry has been DwStartType When the service starts. At start-up or on demand.
created we then close the handle DwErrorControl What SCM should do it the service fails to start.
by calling CloseServiceHandle.
Note the call to AddEventDetails- BinaryPathName Fully qualified pathname to the EXE file.
ToRegistry: this is covered in the
From ParamStr(0).
section detailing event logging. LpLoadOrderGroup If the service is in a group which defines start order.
LpTagId Order within previous group.
Removing A Service
Listing 3 also shows the calls made Dependencies Names of services to start before this service.
to automatically uninstall the ServiceStartName Id of account to run service in.
DemoSv1 service from Windows.
Password To use for the above.
This is done if you pass UNINSTALL
or U as a parameter to the program.
➤ Table 3: CreateService parameters
This is essentially the reverse of
the above process. A connection is
made to the SCM as before. A call to Function CreateService(hSCManager: TSC_HANDLE; ServiceName, DisplayName: PChar;
OpenService then opens the named dwDesiredAccess, dwServiceType, dwStartType, dwErrorControl: Integer;
BinaryPathName, lpLoadOrderGroup: PChar; lpTagId: PInteger;
service. Once opened it is deleted Dependencies, ServiceStartName, Password: PChar): TSC_Handle; StdCall;
using DeleteService. If the service
is currently running it will be
➤ Listing 4
flagged for deletion. Finally the
connection to the SCM is closed.
Again, notice the call to Re- MessageId=
moveEventDetailsFromRegistry, this Severity=Informational
is covered in the next section. SymbolicName=DEMO1_SERVICE_STARTED
Language=English
The Event Viewer The service started successfully.
If you look at Start/Programs/Ad- .
ministrative Tools (Common)/Event MessageId=
Viewer you should see a log of Severity=Warning
events that have been generated SymbolicName=DEMO1_SERVICE_PAUSED
by device drivers, services, appli- Language=English
cations etc on your system for the The service was paused.
past few days. The Log/System op- .
tion displays events mainly gener- MessageId=
ated by device drivers. It is Severity=Informational
recommended that the application SymbolicName=DEMO1_SERVICE_CONTINUED
log be used for services. Language=English
The service was resumed after being paused for %1 milliseconds.
Writing Events To The Log
Writing events to the log is rela-
➤ Figure 1
tively easy but you have a bit of
work to do before you can call the
APIs. Creating Message Resource compiler generates a C header, a
You will need to create a mes- Event logging requires a special BIN file containing the resource
sage resource containing the mes- kind of resource to support inter- (MSG00001.BIN, for English mes-
sages you intend to use and update nationalisation of event messages. sages) and an RC file (which speci-
the registry to indicate the source To create this resource you will fies the language and source files,
of this resource file (which can be need to use the message compiler in this case MSG00001.BIN). The
in either an EXE or a DLL) before (MC.EXE) which comes with the RC file needs to be compiled to
you can start generating events. SDK. The source text file (with a create a .RES file which, in turn,
The demonstration programs on .MC extension) is fairly straightfor- needs to be linked to the program
the disk update the registry for ward, Figure 1 shows part of the ex- using the $R directive. You will
you. ample for DemoSv1. The message need to convert the header file into

30 The Delphi Magazine Issue 24


var event log. The RegisterEventSource
EventSource: THandle;
Inserts: Array[0..0] Of PChar; API accepts two parameters. The
begin first is the UNC name of the host
EventSource := RegisterEventSource(nil,’DemoSerivce’);
Try computer (nil defaults to the local
Inserts[0] := ‘1000’;
ReportEvent(EventSource, EVENTLOG_WARNING_TYPE,0,Id,nil,1,0,Inserts,nil); machine). The second is the
finally
DeRegisterEventSource(EventSource); source name mentioned earlier.
end; This function returns a handle that
end;
needs to be passed to the following
calls. The ReportEvent API gener-
➤ Listing 5
ates the event record. The declara-
tion for this function is shown in
function ReportEvent(hEventLog: THandle; wType, wCategory: Word; Listing 6.
dwEventID: DWORD; lpUserSid: Pointer; wNumStrings: Word; dwDataSize: DWORD;
Inserts: PCharArray; lpRawData: Pointer): BOOL; stdcall; Table 4 shows the meaning of
each parameter. After generating
the event you need to call DeRegis-
➤ Listing 6
terEventSource to close the handle.
Once the event has been generated
hEventLog The handle returned from RegisterEventSource. it should be present in the event
viewer under the application log.
wType One of the pre-defined constants for information, error or In the DemoSv1 service I always
warning types. write the FServiceStatus record to
WCategory Can be anything meaningful for the service. the event log as ‘raw data’ just to
show how it is done.
dwEventId The ID of one of the messages in the resource created by
MC.EXE. Running DemoSv1
lpUserSid The security identifier for the user. The demos specify nil. If you wish to run DemoSv1 first com-
pile it in Delphi as a console appli-
wNumStrings The number of values to be used to insert into the message. cation (but do not run it from
Delphi). Then execute the pro-
dwDataSize The number of bytes of raw data which can be optionally
added to the event record.
gram in a console window and pass
INSTALL as a parameter. A message
lpStrings Points to an array of PChars which contain the values of the should indicate that it was in-
inserts. stalled OK. Now, go into Settings/
Control Panel/Services. There
lpRawData Pointer to the buffer to be written to the event record.
should be a service called Demon-
stration Service 1 listed with a
➤ Table 4: ReportEvent parameters
start-up type of manual. Its current
status should be blank.
a Pascal unit. In the examples for You then need to create two values To start the service select it
this article I have supplied all the for this key: EventMessageFile is the from the list then click the Start
intermediate files of this process in fully qualified name of the EXE or button (you can pass a delay pe-
case you don’t have the message DLL containing the message re- riod between 500 and 10000 in the
compiler. Note that the messages source, and TypesSupported con- Start-up Parameters field). After a
can contain inserts (%1, %2 etc) tains the bit mask of message types few seconds the SCM should show
which can be specified at run time. supported (info, warning and that its status is now started and it
error). should start beeping. If you look at
Updating The Registry If you fail to add the source name the event viewer an information
You can either link the message re- to the registry the event will still be event should have been created to
source into your service EXE (the logged but there will be no mes- show that the service was started.
demos do this) or into a different sage text displayed for the event. You should now be able to pause
program or DLL. Whichever you In the source on the disk look for and then restart the service using
choose, you must update the regis- the AddEventDetailsToRegistry the Pause and Continue buttons. If
try to let Windows know where the procedures to see how this was you restart the service you should
file is. A new key under done. When the service is Unin- see a message in the event viewer
HKEY_LOCAL_MACHINE\System\ stalled the key needs to be showing the number of millisec-
CurrentControlSet\ deleted. This is done in onds which elapsed while it was
Services\EventLog\Application RemoveEventDetailsFromRegistry. paused. After clicking Stop to stop
needs to be created (this is known the service the status should re-
as the source name). The name of Creating The Event turn to blank. To uninstall the serv-
this key should be unique for the Listing 5 shows the sequence of ice, execute it in a console window
service (use the service name). three calls needed to write to the and pass UNINSTALL as a parameter.

August 1997 The Delphi Magazine 31


That just about covers the ba- issue I have included the file services. If a match is found it cre-
sics as far as services are con- MakeMic.Pas from that article on ates an instance of the TNTService
cerned. I’ll now briefly describe the the disk. derived class (ie a new thread)
classes I have developed to hide which becomes the main worker
these details and describe the TNTServiceController Class thread for that service. The mecha-
extended examples I have pro- The main functions of this class are nism for doing this is similar to the
vided to demonstrate some of the as follows. way CreateForm works (see
features described in this article. It keeps a list of services that it Forms.Pas).
controls. These are added to the DemoSv2 uses a class derived
Encapsulating Services list via the RegisterService method from TNTServiceController called
In Delphi Classes when the program starts. Regis- TNTServiceControllerDemo which
My aim was, as far as possible, to terService accepts a class refer- adds data and synchronisation ob-
hide all the interface requirements ence as its parameter. This class jects that are shared between the
to the SCM inside Delphi classes. reference needs to be for a class services 2b and 2c.
The main problem areas are asso- derived from TNTService. Keeping a
ciated with lengthy operations list of class references in the object TNTService Class
such as start-up and close down. I ensures that the class is able to de- The TNTService class is derived
wanted to be able to simply exe- fer the start-up of services until re- from the TThread class. The main
cute code (such as DoService- quested by the SCM. This keeps the functions of this class are as
Startup) without having to worry number of threads active to the ab- follows.
about co-ordinating updates to the solute minimum (number of It has a ProcessParms virtual
SCM if this was going to take a long started services plus 1). method that is called at the end of
time and may be in danger of tim- It provides a private ServiceMain the constructor to allow the serv-
ing out. If required, an additional method (declared with the stdcall ice to process any parameters en-
thread is created with the respon- directive) and automatically tered on the service start-up
sibility of updating the SCM with makes it callable from Windows by window. The parameters are built
the current state of play. Virtual using MakeMethodInstance. in the ServiceMain method.
functions are used to indicate if a It provides InstallServices and It sets the FreeOnTerminate flag to
thread should be created to per- UninstallServices methods to al- ensure that the TThread object is
form this function. The default low for block or individual install freed when the service ends.
TNTService class always returns and uninstall calls for the regis- It provides a private ControlHan-
False. If an overridden method re- tered services. Configuration op- dler method and automatically
turns true a thread is automatically tions can be supplied by the makes it callable from Windows by
created to update the SCM. particular implementation of the using a MakeMethodInstance. This
I have created two base classes TNTService derived class (TNTServ- method accepts the control re-
to do this, TNTServiceController ice provides default values). De- quests from the SCM for this serv-
and TNTService. Listing 7 shows the pendency information can be ice. This entry point is unique for
type declarations for these two specified for a service. each service.
classes: see SVCClass.Pas on the The Connect method connects to If required, when the Handler is
disk for the implementation the SCM and starts the dispatcher. requested to change the running
details. It supplies the names of all regis- status (eg Pause or Continue) and
The Services.Pas unit contains tered services each having the this may take a long time, it creates
classes derived from the base same ServiceMain entry point (the a second thread to keep the SCM
classes to implement the function- address which is returned from updated with the current status.
ality for the services. Listing 8 MakeMethodInstance). It defines empty virtual methods
shows the DemoSv2 project file to When the dispatcher need to that are called in response to re-
show how a typical service pro- create a service it creates a thread quests from the SCM. Descendant
gram would be implemented using then calls ServiceMain. ServiceMain classes should override these if
these classes. is responsible for starting the re- required.
The first problem you have when quested service via a call to the pri- It provides a LogEvent method to
attempting to implement Windows vate method StartService (which allow the service to easily generate
call-back functions into classes is creates a new thread). After doing events.
the object model used by Delphi: this, ServiceMain ends and the It also has a class function called
methods have an extra hidden in- thread created by the dispatcher ServiceName that returns the serv-
stance pointer (Self) so they can’t terminates. I originally wondered if ice name. This is used in the
normally be passed as call-back this was valid but eventually found TNTServiceController class when it
functions. I covered this in Issue 18 an article in the MSDN that con- needs to reference the service.
in an article called Generic Make- firms that is OK to do this. The class provides configura-
MethodInstance for 16/32 bit appli- The StartService method tion and option information to the
cations. We can use that technique checks the name supplied against TNTServiceController via class
here. In case you don’t have that the names of the registered functions and virtual methods.

32 The Delphi Magazine Issue 24


Start-up Can be Depends Uses DemoSv1. There are slight differ-
Service Description type paused on params ences: After INSTALL or UNINSTALL
on the command line you can sup-
2a Beeper from On Yes – Yes ply a list of individual services to
DemoSv1 demand (elapse ms)
be installed or uninstalled.
2b Monitors Auto Yes – Yes After installing the services you
directory updates (wild card) should see three new services in
the Services applet. These are
2c Query service Auto No 2b No
listed as Demonstration Service
using pipes
2a, 2b and 2c. There are slight dif-
ferences in the configuration for
➤ Table 5: DemoSv2 services
each these services to show how
the various options are imple-
TNTServiceController = class mented for each service. I have im-
private plemented a dependency between
FAvailableServices: TList;
FServiceMainInstance: Pointer; the services 2b and 2c. Also, serv-
function ProcessOption: DWORD;
procedure ServiceMain(NumArgs: DWord; Args: PCharArray); StdCall; ice 2b accepts a parameter entered
procedure StartService(Name: Shortstring; Parms: TStrings);
public by the user in the Services control
constructor Create; virtual; panel applet. Table 5 lists the
destructor Destroy; override;
procedure Connect; specifics for each service.
procedure InstallServices(Names: TStrings);
procedure RegisterService(SvcClass: TNTServiceClass); Service 2b is set to monitor up-
procedure UnInstallServices(Names: TStrings); dates to directory C:\TEMPX (it
end;
TNTService = class(TThread) creates the directory if it doesn’t
private
FController: TNTServiceController; exist). If an update is made to this
FHandlerInstance: Pointer;
FServiceStatus: TServiceStatus; directory it copies all files with the
FServicStatusHandle: SERVICE_STATUS_HANDLE; archive attribute to C:TEMPX\
procedure DoTerminate; override;
function NeedExtnededElapseTime(Option: DWORD): Boolean; virtual; SVBACKUP, then clears the archive
function GetPaused: Boolean;
procedure SetCurrentState(Value: DWORD); attribute.The optional parameter
procedure StartNotificationThread; can be a wild card (defaults to *.*)
procedure TerminateNotificationThread;
protected used to limit the files which are
function AcceptPause: Boolean; virtual;
function AcceptStop: Boolean; virtual; copied. As well as doing this it up-
function CanInteract: Boolean; virtual;
procedure DoHandlerNotification; virtual; dates a log of information in a
procedure DoServiceStartup; virtual; TStrings object. This object is
procedure DoServiceProcessing; virtual; abstract;
procedure DoServiceCloseDown; virtual; stored in the TNTServiceControl-
procedure Execute; override;
procedure Handler(Code: Integer); stdcall; lerDemo object. Service 2c waits for
procedure LogEvent(Severity: DWord; Id: DWord; Inserts: PCharArray; requests from a client application
NumInserts: Integer);
procedure ProcessParms(Parms: TStrings); virtual; (DemoCl2.DPR on the disk) to a
function WantShutdownNotification: Boolean; virtual;
property CurrentState: DWORD read FServiceStatus.dwCurrentState named pipe. DemoCl2 is another
write SetCurrentState;
property Paused: Boolean read GetPaused; console application. Pass Query as
public the command line parameter to list
constructor Create(Parms: TStrings; Controller: TNTServiceController);
virtual; the log details, and pass Reset as
destructor Destroy; override;
class procedure DependentServices(List: TStrings); virtual; the parameter to clear the log
class function ServiceDisplayName: Shortstring; virtual; abstract; details.
class function ServiceName: Shortstring; virtual; abstract;
class function ServiceStartType: DWORD; virtual; Due to the dependency between
property Controller: TNTServiceController read FController;
end; services 2b and 2c the SCM will en-
sure that the services start in the
correct order. Also, if you request
➤ Listing 7
to stop service 2b it will prompt
you to confirm that you also want
Class functions are used whenever ServiceDisplayName and Service- to close service 2c. If you click OK
that value may be required when Name which must be overridden in the SCM will stop both services.
there may not be an instance of the descendant classes. A unique class Note: if you install service 2c and
object (eg ServiceName). needs to be defined for each serv- not 2b the installation will work OK
The Execute method calls the ice. DemoSv2 defines three classes but you will not be able to start
DoServiceStartUp method then derived from TNTService to imple- service 2c. This is because the SCM
calls DoServiceProcessing. Descen- ment the different services which cannot find the details for service
dant classes must put their main are described in Table 5. 2b in the registry.
service processing in this method,
not the execute method. Running DemoSv2 Further Information
The class defines three abstract This program can be compiled and I have supplied a file on the disk
methods: DoServiceProcessing, run in exactly the same way as called SERVICES.TXT that lists

August 1997 The Delphi Magazine 33


some articles in the MSDN, which program DemoSv2;
provide additional information. Uses SysUtils, Classes, Windows, SvcClass, Services, Logging;
var
I: Integer;
One Final Tip... Option: ShortString;
I was caught out for a while when ServiceController: TNTServiceController;
ServiceList: TStrings;
Delphi was failing to compile a begin
service program saying that it was ServiceController := TNTServiceControllerDemo.Create;
try
unable to create the output file. It Option := UpperCase(ParamStr(1));
turned out that the event viewer ServiceList := TStringList.Create;
had a lock on the file. For I := 2 to ParamCount do
ServiceList.Add(UpperCase(ParamStr(I)));
As the resources for the message With ServiceController do
are in the EXE files the event viewer try
RegisterService(TService2a);
holds the file open when it needs to RegisterService(TService2b);
display messages linked to that RegisterService(TService2c);
if Option = ‘’ then
service. Connect
else
if (Option = ‘INSTALL’) or (Option = ‘I’) then
InstallServices(ServiceList)
John Chaytor is a freelance else
programmer who lives and works if (Option = ‘UNINSTALL’) or (Option = ‘U’) then
UninstallServices(ServiceList)
in Brighton, UK, and can be else
contacted via CompuServe as if (Option = ‘VERSION’) or (Option = ‘V’) then
DisplayVersionDetails
100265,3642 else
DisplaySyntaxOptions;
finally
ServiceList.Free;
end;
finally
ServiceController.Free;
end;
end.

➤ Listing 8

34 The Delphi Magazine Issue 24


Beating the System
by Dave Jewell

important caveats I should point discover any ‘gotchas’ in the


out. In general, when writing arti- meantime, then I’ll let you know
cles for programming magazines, I next month!
don’t much mind what people do With the preliminaries out of the
with the code. However, in this par- way, let’s look at the properties
ticular case I’m retaining rights to and methods provided by TZip-
the code because I intend to use it File. In order to use the compo-
as the basis for a shareware com- nent, you obviously need to create
ponent that will allow Delphi an instance of TZipFile, optionally
programs to access ZIP files, de- passing it the fully-qualified name
compress zipped files and perhaps of an existing ZIP file. The specified
compress files as well. That’s quite ZIP file is then examined and the
a bit of work and it isn’t finished various other component proper-
yet, but you can see some hints in ties are set up according to the in-
the code listing such as the pres- formation contained therein. If you
ence of Password and ExtractDir don’t want to specify a ZIP file
properties. If you want to use any name when the component is cre-
of this code for non-commercial ated, then you can pass an empty
purposes, then go right ahead. If string. At any time, you can exam-
you want to use the code as the ba- ine the ZipName property to deter-
sis for a file-find utility like that I’ve mine the ZIP file currently
outlined above, then I’m perfectly associated with the component.
happy with that too. But please To examine the contents of an-
don’t simply take my code, add other ZIP file, simply set the
data decompression routines, and ZipName property to point to the
sell it as your own ZIP component. new file.
If you do that, it’ll be time to phone Perhaps the most important
the lawyers! non-indexed property associated
Also, please bear in mind that al- with the component is FilesCount.

I n this month’s article, I’m going


to describe the internal structure
of those ubiquitous ZIP files and
though for convenience I’ve re-
ferred to TZipFile as a component,
it’s not currently configured as
As the name suggests, this tells the
application how many entries are
contained within the ZIP file. If you
explain how you can write Delphi such. To get it on your Component try and read this property before
programs that make use of this in- Palette, you’d need to add a Regis- specifying a ZIP file name, the com-
formation. Specifically, this month terClasses call and have it derive ponent will raise a EZipErr excep-
I’ll concentrate on the develop- from TComponent instead of TObject. tion with the text No ZIP file
ment of a drop-in Delphi compo- I haven’t bothered doing this be- specified. The ExtractDir and Pass-
nent that can be used to give a neat, cause the class contains a large word properties are reserved for fu-
object-oriented interface to ZIP number of indexed properties and ture use (as mentioned earlier!)
files. Next month, I’ll make use of relatively few non-indexed proper- and don’t do anything in the pres-
this component (and a few more ties. As you’ll appreciate, indexed ent implementation. LowerCase-
bells and whistles) to develop a properties aren’t directly sup- Names determines whether or not
program which can scan your hard ported by the Object Inspector, so I the name of files contained within
disk for files, even searching for can’t see much benefit in making the ZIP should be returned as
them inside any ZIP files it encoun- TZipFile into a design-time compo- lower case names. By default, this
ters. The program will include the nent in the normal sense. option is on. Bear in mind that
ability to launch your favourite ZIP Finally, please bear in mind that pathname information is never
file utility (WinZip, or whatever) this code is ‘work in progress’ and forced to lower case, only the file-
from where you can extract the isn’t guaranteed bug free. In par- name. Finally, the SortStyle and
files you’re interested in. ticular, I haven’t tried porting the ReverseSort properties determine
code to 16-bit Delphi yet. However, the order in which the filenames
Introducing TZipFile... I believe that the present code are logically sorted. SortStyle de-
The code for my ZIP-sniffing com- more than adequately implements faults to sRaw, meaning that the
ponent is given in Listing 1. Before the functionality needed by next files are returned in the same order
doing anything else, there are a few month’s file-searching program. If I that they exist within the ZIP file

August 1997 The Delphi Magazine 35


directory. You can choose any of SRaw Files are sorted as they appear in the ZIP file directory
the values from Table 1 for this
property. SFullName Files are sorted alphabetically by their full name
In conjunction with the above, SFileName Files are sorted alphabetically by file name only
you can also set the ReverseSort
SPathName Files are sorted alphabetically by path name only
property. This will reverse the
‘sense’ of the sort, so that what was SCompressedSize Files are sorted in order of compressed size
the first item will be the last, and so
SOriginalSize Files are sorted in order of original (uncompressed) size
on. As you’ll see when we look at
the code, changing the SortStyle SCompressRatio Files are sorted in order of their compression ratio
property is quite a rapid operation:
SDate Files are sorted in order of modification date and time
I’ve taken some pains to ensure
that this is the case. Re-sorting the
➤ Table 1: SortStyle values
files does not require the ZIP file di-
rectory to be re-read from disk. As
my test vehicle, I have a large ZIP FullName Full name of the entry – pathname and filename,
file (about 3Mb) containing almost eg WOMBAT\SMURF.ICO
3,500 icons. Using my little test pro-
FileName Filename part of the entry, eg. SMURF.ICO
gram, TZipFile will re-sort this file
in around one second on a 200MHz PathName Pathname part of the entry, eg WOMBAT\
Pentium Pro machine. If you factor Encrypted True if this entry is encrypted and requires a
out the time required by the VCL to password
reload the TListBox components
DiskNumber Starting disk number (for disk-spanning archives)
with the re-sorted filename list, the
actual sort time appears to be Crc32 32-bit CRC check for this file
around a quarter of a second. CompressMethod Method used to compress this file
Re-sort performance seems to be
as good (if not better) than WinZip, DateTime Modification date/time for this file
the market leader. CompressedSize Compressed size of the file in bytes
Finally, there are a set of thirteen
OriginalSize Uncompressed size of the file in bytes
indexed properties, each of which
appears as an array property to the CompressMethodName Plain-English description of the compression
calling code. You can access these method
arrays using an index in the range CommentLength Length of file comment in bytes
[0..FilesCount - 1]. It goes with-
CompressionRatio Compression ratio expressed as an Integer
out saying that the supplied index
is always relative to the current
➤ Table 2: Array properties
SortStyle and ReverseSort proper-
ties. In other words, if you access
an array with an index value of five, compression types in any ZIP file haven’t (as yet) provided a mecha-
and then change SortStyle or Re- you’re likely to come across. As far nism for accessing the comment
verseSort, the chances are that an as I’m aware, the first two have data itself; it’s really irrelevant as
index of five will then give you a never been supported in generally far as next month’s file search util-
completely different entry to what available ZIP software, and the last ity is concerned.
you had before. Thus, if you’re one is reserved for use by the The CompressionRatio array
looping through a property array, PKWare Data Compression Li- property is used to return the com-
don’t change the sort configura- brary. For the convenience of the pression ratio for an entry, ex-
tion until you’ve finished the loop, application program, the Compress- pressed as a percentage. I chose to
or you’ll obviously get nonsensical MethodName property returns a plain return this value as an integer for
results. English description of the com- the sake of language independence
It would be tedious to describe pression method. If the associated (I have plans for porting this code
each of these array properties in code offends the internationalisa- to non-Delphi development envi-
detail, so I’ll simply list them in tion purists (!) you can easily put ronments) but if you want the
Table 2. The CompressMethod array the relevant strings into resources. exact floating-point value, you can
property indicates the type of com- The structure of ZIP files allows obviously calculate it by looking at
pression used to compress a file an arbitrary “comment” to be asso- the CompressedSize and Original-
entry. The type of this property is ciated with each stored file. The Size properties for the entry
CompressType, as shown in the code CommentLength property returns the in question. Internally, when the
listing. You will almost certainly length of the comment (in bytes)
never see ResTokenised, ResEn- associated with a particular entry,
➤ Facing page: Listing 1
hancedDeflate or ResPKLibrary but in the code presented here, I

36 The Delphi Magazine Issue 24


unit Zip; end;
interface type
{$A-} PDirEntry = ^DirEntry;
uses WinTypes, WinProcs, SysUtils, Classes, Match; DirEntry = record { Central Directory entry }
type Signature: LongInt; { should be $02014b50 }
EZipErr = class(Exception); CreatorVersion: Word; { version of ZIP that created it }
SortType = (sRaw, sFullName, sFileName, sPathName, ExtractorVersion: Word; { version of ZIP needed for extract }
sCompressedSize, sOriginalSize, sCompressRatio, sDate); GenBits: Word; { general purpose bit flags }
CompressType = (Stored, Shrunk, Reduce1, Reduce2, Reduce3, CompressMethod: Word; { compression method for this file }
Reduce4, Imploded, Res Tokenised, Deflated, DateTime: LongInt; { file modification date/time }
ResEnhancedDeflate, ResPKLibrary); crc32: LongInt; { 32-bit file CRC }
TZipFile = class(TObject) CompressedSize: LongInt; { compressed size of file }
private OriginalSize: LongInt; { uncompressed size of file }
Dir: TList; FileNameLen: Word; { length of filename }
SortMap: TList; ExtraLen: Word; { length of extra info }
fd: Integer; CommentLen: Word; { length of comment stuff }
fSort: SortType; DiskNumStart: Word; { starting disk number }
pTail: Pointer; IFileAttribs: Word; { File attributes }
SelFiles: Integer; XFileAttribs: LongInt; { External file attributes }
fName: String; HeaderPos: LongInt; { offset of local header }
fExtractDir: String; end;
fPassword: String; function GetDirEntrySize(const Entry: DirEntry): Integer;
fLowerCaseNames: Boolean; begin
fReverseSort: Boolean; with Entry do Result := sizeof(DirEntry) + FileNameLen +
procedure LoadDirectory; ExtraLen + CommentLen;
procedure UnloadDirectory; end;
function GetSigOffset(Signature: LongInt): LongInt;
function GetDirectoryEntry(Idx: Integer): Pointer; function IsValidTailPos(fd: Integer; tailPos: LongInt):
function GetFilesCount: Integer; Bool;
procedure SortFiles; var tail: TailRec;
procedure DoSort(L, R: Integer); begin
function GetFullName(Index: Integer): String; { This function is needed to cope with nested ZIP files }
function GetFileName(Index: Integer): String; { Without it, we might accidentally accept a tail marker }
function GetPathName(Index: Integer): String; { inside a nested ZIP rather than the ZIP’s own marker ! }
function GetEncrypted(Index: Integer): Boolean; Result := False;
function GetCompressMethod(Index: Integer): _llseek(fd, tailPos, 0);
CompressType; _lread(fd, @tail, sizeof(TailRec));
function GetCompressMethodName(Index: Integer): String; if tail.Signature = $06054b50 then begin
function GetCompressionRatio(Index: Integer): Integer; _llseek(fd, tail.DirOffset, 0);
function GetDiskNumber(Index: Integer): Integer; _lread(fd, @tail, sizeof(LongInt));
function GetCrc32(Index: Integer): LongInt; Result := tail.Signature = $02014b50;
function GetCompressedSize(Index: Integer): LongInt; end;
function GetOriginalSize(Index: Integer): LongInt; end;
function GetDateTime(Index: Integer): TDateTime; function FindSig(fd: Integer; buff: PChar; len: Integer;
function GetCommentLength(Index: Integer): Word; fPos, Signature: LongInt): integer;
procedure SetZipName(const FileName: String); var
procedure SetSortType(Val: SortType); p: PChar;
procedure SetReverseSort(Val: Boolean); pp: ^LongInt absolute p;
public begin
constructor Create(const FileName: String); Result := -1;
destructor Destroy; override; if len <> 0 then begin
procedure Reset; p := buff;
property FullName [Index: Integer]: String read while len <> 0 do begin
GetFullName; default; if (pp^ = Signature) and
property FileName [Index: Integer]: String IsValidTailPos(fd, fpos + p - buff) then begin
read GetFileName; Result := p - buff;
property PathName [Index: Integer]: String Exit;
read GetPathName; end;
property Encrypted [Index: Integer]: Boolean Inc(p);
read GetEncrypted; Dec(len);
property DiskNumber [Index: Integer]: Integer end;
read GetDiskNumber; end;
property Crc32 [Index: Integer]: LongInt read GetCrc32; end;
property CompressMethod [Index: Integer]: CompressType { These utility routines extract various fields
read GetCompressMethod; from a DirEntry }
property DateTime [Index: Integer]: TDateTime function DirGetFullName(pde: PDirEntry): String;
read GetDateTime; var
property CompressedSize [Index: Integer]: LongInt Idx: Integer;
read GetCompressedSize; begin
property OriginalSize [Index: Integer]: LongInt Result := ‘’;
read GetOriginalSize; if pde <> Nil then with pde^ do begin
property CompressMethodName [Index: Integer]: String {$IFDEF WIN32}
read GetCompressMethodName; SetLength(Result, FileNameLen);
property CommentLength [Index: Integer]: Word {$ELSE}
read GetCommentLength; Result[0] := Chr(FileNameLen);
property CompressionRatio [Index: Integer]: Integer {$ENDIF}
read GetCompressionRatio; Move((PChar(pde) + sizeof(DirEntry))^, Result [1],
published FileNameLen);
property ZipName: String read fName write SetZipName; { Massage UNIX forward slashes to Wintel backslashes }
property SortStyle: SortType for Idx := 1 to Length(Result) do
read fSort write SetSortType; if Result [Idx] = ‘/’ then Result [Idx] := ‘\’;
property ExtractDir: String end;
read fExtractDir write fExtractDir; end;
property Password: String
read fPassword write fPassword; function DirGetCompRatio(pde: PDirEntry): Double;
property ReverseSort: Boolean begin
read fReverseSort write SetReverseSort default False; Result := 0;
property LowerCaseNames: Boolean read fLowerCaseNames if pde <> Nil then with pde^ do
write fLowerCaseNames default True; if OriginalSize <> 0 then
property FilesCount: Integer read GetFilesCount; Result := ((OriginalSize - CompressedSize) * 100) /
end; OriginalSize;
end;
implementation
type constructor TZipFile.Create(const FileName: String);
PTailRec = ^TailRec; begin
TailRec = record { End of central dir: ‘tail’ } fd := -1;
Signature: LongInt; { should be $06054b50 } SortMap := TList.Create;
ThisDisk: Word; { # of this disk } fLowerCaseNames := True;
DirDisk: Word; { # of disk with central dir start } fReverseSort := False;
NumEntries: Word; { # of central dir entries this disk } SetZipName(FileName);
TotEntries: Word; { # of central dir entries total } end;
DirSize: LongInt; { size of the central directory }
DirOffset: LongInt; { offset of c-dir wrt starting disk }
BannerLength: Word; { size of following comment if any } { Continued on page 38... }

August 1997 The Delphi Magazine 37


{ Continued from page 37... } end;
procedure TZipFile.SetZipName(const FileName: String); Result := CompareText(S1, S2);
var end;
tail: TailRec; sDate, sCompressedSize, sOriginalSize, sCompressRatio:
tailPos: LongInt; begin
szName: array [0..255] of Char; if fSort = sDate then begin
begin D1 := FileDateToDateTime(Key1^.DateTime);
UnloadDirectory; D2 := FileDateToDateTime(Key2^.DateTime);
fName := ‘’; fPassword := ‘’; end;
{ If filename is empty, just exit } if fSort = sCompressedSize then begin
if FileName = ‘’ then Exit; D1 := Key1^.CompressedSize;
{ Get filename and make sure it has a proper extension } D2 := Key2^.CompressedSize;
StrPCopy(szName, FileName); end;
if StrPos(szName, ‘.’) = Nil then lstrcat(szName, ‘.zip’); if fSort = sOriginalSize then begin
{ Now try to open the file } D1 := Key1^.OriginalSize;
fd := _lopen(szName, of_Read or of_Share_Deny_Write); D2 := Key2^.OriginalSize;
if fd = -1 then end;
raise EZipErr.Create(‘Cannot open specified file’); if fSort = sCompressRatio then begin
fName := StrPas(szName); D1 := DirGetCompRatio(Key1);
{ OK - it’s there, but is it a valid ZIP file ? } D2 := DirGetCompRatio(Key2);
tailPos := GetSigOffset($06054b50); end;
if tailPos < 0 then if D1 = D2 then
raise EZipErr.Create(‘Not a valid ZIP file’); Result := 0
{ Found the directory tail - ensure no disk spanning } else if D1 > D2 then
_llseek(fd, tailPos, 0); Result := 1
_lread(fd, @tail, sizeof(TailRec)); else
if (tail.ThisDisk <> 0) or (tail.DirDisk <> 0) then Result := -1;
raise EZipErr.Create( end;
‘Disk spanning not yet implemented’); end;
{ Read directory tail and banner into our data structure } end;
GetMem(pTail, sizeof(TailRec) + tail.BannerLength); begin
_llseek(fd, tailPos, 0); repeat
_lread(fd, PChar(pTail), sizeof(TailRec) + I := L; J := R; P := SortMap [(L + R) shr 1];
tail.BannerLength); repeat
{ Now get central directory & ensure all files selected } while SortCompare(SortMap [I], P) < 0 do Inc(I);
LoadDirectory; while SortCompare(SortMap [J], P) > 0 do Dec(J);
end; if I <= J then begin
SortMap.Exchange(I, J);
destructor TZipFile.Destroy; Inc(I);
begin Dec(J);
UnloadDirectory; end;
SortMap.Free; until I > J;
Inherited Destroy; if L < J then DoSort(L, J);
end; L := I;
procedure TZipFile.LoadDirectory; until I >= R;
var end;
p: PChar; procedure TZipFile.SortFiles;
de: DirEntry; var Idx: Integer;
sz, Idx: Integer; begin
function NonBlankEntry: Boolean; { First, clear the sort map }
begin SortMap.Clear;
Result := (de.CompressedSize <> 0) or SortMap.Capacity := FilesCount;
(de.OriginalSize <> 0) or (de.CompressMethod <> 0); { Initialise the sort map for ‘sRaw’ mode }
end; for Idx := 0 to FilesCount - 1 do
begin SortMap.Add(Dir [Idx]);
{ Initialize directory TList } { Now do the actual sort }
Dir := TList.Create; if fSort <> sRaw then
Dir.Capacity := PTailRec(pTail)^.NumEntries; DoSort(0, SortMap.Count - 1);
{ Seek to start of file } end;
_llseek(fd, PTailRec(pTail)^.DirOffset, 0);
{ Read each entry in consecutively } procedure TZipFile.SetSortType(Val: SortType);
for Idx := 0 to PTailRec(pTail)^.NumEntries - 1 do begin begin
_lread(fd, @de, sizeof(de)); fSort := Val;
sz := GetDirEntrySize(de); if Dir <> Nil then
GetMem(p, sz); SortFiles;
Move(de, p^, sizeof(de)); end;
_lread(fd, p + sizeof(de), sz - sizeof(de)); procedure TZipFile.SetReverseSort(Val: Boolean);
{ If this is a blank ‘directory-marker’ record skip it } begin
if NonBlankEntry then Dir.Add(p) else FreeMem(p, sz); fReverseSort := Val;
end; end;
Reset; procedure TZipFile.Reset;
end; var idx: Integer;
function TZipFile.GetDirectoryEntry(Idx: Integer): Pointer; begin
begin SetSortType(fSort);
if Dir = Nil then SelFiles := FilesCount;
raise EZipErr.Create(‘No ZIP file specified’); for idx := 0 to SelFiles - 1 do
if fReverseSort then PDirEntry(GetDirectoryEntry(idx))^.Signature := 1;
Idx := SortMap.Count - 1 - Idx; end;
Result := SortMap [Idx]; procedure TZipFile.UnloadDirectory;
end;
procedure FreeList(var List: TList);
procedure TZipFile.DoSort(L, R: Integer); var
var p: Pointer;
P: Pointer; Idx: Integer;
I, J: Integer; begin
function SortCompare(Key1, Key2: PDirEntry): Integer; if List <> Nil then begin
var for Idx := 0 to List.Count - 1 do begin
D1, D2: Double; p := List.Items [Idx];
S1, S2: String; FreeMem(p, GetDirEntrySize(PDirEntry(p)^));
begin end;
D1 := 0; D2 := 0; Result := 0; { Just to shut compiler up } List.Free;
case fSort of List := Nil;
sFullName, sFileName, sPathName: end;
begin end;
S1 := DirGetFullName(Key1); begin
S2 := DirGetFullName(Key2); FreeList(Dir);
if fSort = sFileName then begin if pTail <> Nil then begin
S1 := ExtractFileName(S1); FreeMem(pTail, sizeof(TailRec) +
S2 := ExtractFileName(S2); PTailRec(pTail)^.BannerLength);
end; pTail := Nil;
if fSort = sPathName then begin end;
S1 := ExtractFilePath(S1);
S2 := ExtractFilePath(S2); { Continued on page 40... }

38 The Delphi Magazine Issue 24


➤ Facing page: Central file header signature 4 bytes (0x02014b50)
Listing 1 continued Version made by 2 bytes
Version needed to extract 2 bytes
General purpose bit flag 2 bytes
SortStyle is set to SCompressRatio, Compression method 2 bytes
the full floating point value is used, Last mod file time 2 bytes
so that the sort order matches Last mod file date 2 bytes
other programs such as WinZip. CRC-32 4 bytes
Compressed size 4 bytes
ZIP File Structure Uncompressed size 4 bytes
Now you understand how to use Filename length 2 bytes
the TZipFile class, it’s time to look Extra field length 2 bytes
at how it works. However, before File comment length 2 bytes
we can do that, you need to get Disk number start 2 bytes
some idea of the internal layout of a Internal file attributes 2 bytes
ZIP file. Most file formats start off External file attributes 4 bytes
with a header which contains Relative offset of local header 4 bytes
pointers to other information Filename variable size
within the file: the icon and bitmap Extra field variable size
files I talked about a couple of File comment variable size
months ago are typical examples.
Surprisingly, ZIP files aren’t like
➤ Table 3: Central directory entry structure
this. Instead, the most important
data structure, the central direc-
tory, is located towards the end of to end up more than 64Kb from the case the ZIP file isn’t kosher, I actu-
the file and is followed by an “End end of the file, some shuffling gets ally search the entire file rather
of Central Directory Record.” It’s performed and the tail and central than the last 64Kb, only giving up if
the End of Central Directory Re- directory are once more moved to the tail cannot be located. As you’ll
cord (normally referred to as the the end of the file. The overall strat- see from the code, the TailRec
“tail” and equivalent to the TailRec egy is to minimise disk-intensive structure contains a field called
data structure in my code) which shuffles most of the time. DirOffset which tells us where the
must be located before the con- With the benefit of hindsight, central directory is located. The
tents of the file can be enumerated. this all seems rather bizarre. A central directory consists of a con-
One of the great things about ZIP fairly obvious question is why on tiguous sequence of directory en-
files is that you can arbitrarily add earth weren’t the first four bytes of tries, of type DirEntry. However,
files to an existing ZIP archive, or the file used as a pointer to the cen- these directory entries are all vari-
remove files just as easily. If the di- tral directory, wherever it might able sized: the size of each entry
rectory was placed at the begin- wind up after each operation? depends on the length of the file
ning of the ZIP file, adding a new file Nowadays, that would be an emi- name, the length of the file com-
would increase the size of the di- nently sensible way of doing ment (if any) and the amount of
rectory, thus necessitating that things, but you’ve got to bear in “extra information” associated
everything else “shuffle down” to- mind that this file format evolved with the file. This extra informa-
wards the bottom of the file. The at a time when some PCs only had tion field is a way of associating ar-
same argument applies to the re- floppy disk drives! Floppy disk bitrary information with each ZIP
moval of entries, causing a “shuffle seek time (the time needed to file entry; it’s used by some ZIP-
up.” By locating the directory to- move the read/write head from compatible software to store oper-
wards the end of the file, this shuf- track to track) was, and still is, hor- ating system dependent informa-
fle is largely avoided. rendously slow. PKWare designed tion which won’t fit into the
Notice that I’ve been careful to the file format in order to minimise ordinary directory entry. The
say “towards the end of the file” the number of large scale seeks re- overall structure of a central direc-
rather than “at the end of the file.” quired in the normal course of tory entry (which is taken from
Again, this is a performance opti- events. In fact, the PKWare applica- PKWare’s application note) is
misation devised by PKWare, the tion note even suggests that they shown in Table 3.
original creators of the ZIP format. were concerned about compatibil-
Suppose you add a few small files ity with non-seekable output How It Works
to a ZIP file: the PKZIP program will devices. Armed with the above informa-
typically increase the size of the di- The bottom line is this: a legal tion, we can examine the various
rectory slightly and write the new ZIP program will contain a “tail” routines in Listing 1. When you cre-
files after the central directory and record within 64Kb of the end of ate a new TZipFile object and asso-
tail. However, if it detects that the the file and we have to search for it, ciate it with a ZIP file, the first
tail and central directory are going backwards from the file end. Just in routine of any interest that gets

August 1997 The Delphi Magazine 39


{ Continued from page 38... } var pde: PDirEntry;
begin
if fd <> -1 then begin Result := False;
_lclose(fd); pde := GetDirectoryEntry(Index);
fd := -1; if pde <> Nil then
end; Result := (pde^.GenBits and 1) <> 0;
end; end;
function TZipFile.GetSigOffset(Signature: LongInt): LongInt; function TZipFile.GetCompressionRatio(Index: Integer):
const Integer;
InBufferSize = 8192; { for sig searching } begin
var Result := Round(DirGetCompRatio(
buff: PChar; GetDirectoryEntry(Index)));
fs, pos: LongInt; end;
bp, bytesread: Integer; function TZipFile.GetCompressedSize(
begin Index: Integer): LongInt;
GetMem(buff, InBufferSize); var pde: PDirEntry;
try begin
fs := _llseek(fd, 0, 2); Result := 0;
if fs <= InBuffersize then pde := GetDirectoryEntry(Index);
pos := 0 if pde <> Nil then
else Result := pde^.CompressedSize;
pos := fs - InBufferSize; end;
_llseek(fd, pos, 0);
{ Get initial buffer content } function TZipFile.GetOriginalSize(Index: Integer): LongInt;
_lread(fd, buff, InBufferSize); var pde: PDirEntry;
bp := FindSig(fd, buff, InBufferSize, pos, Signature); begin
{ This is the main search loop... } Result := 0;
while (bp < 0) and (pos > 0) do begin pde := GetDirectoryEntry(Index);
Move(buff, buff [InBufferSize - 4], 4); if pde <> Nil then
Dec(pos, InBufferSize - 4); Result := pde^.OriginalSize;
if pos < 0 then pos := 0; end;
_llseek(fd, pos, 0); function TZipFile.GetCompressMethod(
bytesRead := _lread(fd, buff, InBufferSize - 4); Index: Integer): CompressType;
if bytesRead < InBufferSize - 4 then var pde: PDirEntry;
Move(buff [InBufferSize - 4], begin
buff [BytesRead], 4); Result := Stored;
if bytesRead > 0 then begin pde := GetDirectoryEntry(Index);
Inc(bytesRead, 4); if pde <> Nil then
bp := Result := CompressType(pde^.CompressMethod);
FindSig(fd, buff, bytesRead, pos, Signature); end;
end; function TZipFile.GetDiskNumber(Index: Integer): Integer;
end; var pde: PDirEntry;
if bp < 0 then begin
GetSigOffset := -1 Result := 1;
else pde := GetDirectoryEntry(Index);
GetSigOffset := pos + bp; if pde <> Nil then
finally Result := pde^.DiskNumStart;
FreeMem(buff, InBufferSize); end;
end;
end; function TZipFile.GetCrc32(Index: Integer): LongInt;
var pde: PDirEntry;
function TZipFile.GetFilesCount: Integer; begin
begin Result := 0;
if Dir = Nil then pde := GetDirectoryEntry(Index);
raise EZipErr.Create(‘No ZIP file specified’); if pde <> Nil then
Result := Dir.Count; Result := pde^.crc32;
end; end;
function TZipFile.GetFileName(Index: Integer): String; function TZipFile.GetCommentLength(Index: Integer): Word;
begin var pde: PDirEntry;
Result := ExtractFileName(GetFullName(Index)); begin
if fLowerCaseNames then Result := 0;
Result := LowerCase(Result); pde := GetDirectoryEntry(Index);
end; if pde <> Nil then
function TZipFile.GetPathName(Index: Integer): String; Result := pde^.CommentLen;
begin end;
Result := ExtractFilePath(GetFullName(Index)); function TZipFile.GetCompressMethodName(
end; Index: Integer): String;
function TZipFile.GetFullName(Index: Integer): String; var typ: CompressType;
begin begin
Result := DirGetFullName(GetDirectoryEntry(Index)); typ := GetCompressMethod(Index);
end; case typ of
function TZipFile.GetDateTime(Index: Integer): TDateTime; Stored: Result := ‘Stored’;
var pde: PDirEntry; Shrunk: Result := ‘Shrunk’;
begin Reduce1..Reduce4: Result := ‘Reduced’;
Result := 0; Imploded: Result := ‘Imploded’;
pde := GetDirectoryEntry(Index); Deflated: Result := ‘Deflated’;
if pde <> Nil then else
Result := FileDateToDateTime(pde^.DateTime); Result := Format(‘Unknown(%d)’, [Ord(typ)]);
end; end;
end;
function TZipFile.GetEncrypted(Index: Integer): Boolean; end.

➤ Listing 1, concluded
should have any sort of user inter- Once the file is open, GetSigOff-
face. It doesn’t prompt for files, it set is called to find the location of
called is TZipFile.SetZipName. This doesn’t put up dialogs to indicate the tail record within the ZIP file.
calls UnloadDirectory to dispose of errors, it is completely faceless. The tail can be identified because
any previous directory context The user interface is the sole re- it contains a special four byte sig-
and then tries to open the file, gen- sponsibility of the calling applica- nature in the first four bytes; more
erating an exception if the file tion. Instead, TZipFile responds to on this in a moment. If no tail can
couldn’t be found. As a philosophi- error conditions by generating ex- be found an exception is raised to
cal aside, let me just say that I don’t ceptions, which can be intercepted indicate that it’s not a valid ZIP file.
believe a low-level ‘file format in- and handled appropriately by the If the tail is found, the complete tail
terface class’ such as TZipFile application itself. record is read into memory and

40 The Delphi Magazine Issue 24


the LoadDirectory method is called
➤ Here’s the
to load the ZIP directory proper.
sample program
The GetSigOffset routine is re-
running: this is
sponsible for scanning backwards
a seriously large
through the ZIP file, looking for the
ZIP file, over
tail signature. It may look a little
3Mb in size
more complicated than you’d
and nearly
expect, that’s because the file is
3,500 entries,
examined in blocks of approxi-
but TZipFile
mately 8Kb and it’s possible that
doesn’t seem
the four byte signature might
to have any
straddle two of these blocks, caus-
problems
ing it to be missed if we simply ex-
digesting it!
amined each block individually.
Within the GetSigOffset routine,
FindSig is called to look for the sig- function, GetDirEntrySize, to calcu- processed central directory infor-
nature within the current buffer. late the total size of each directory mation. This isn’t a very elegant
There is perhaps some argument entry, allowing for all the variable- solution.
for rewriting FindSig in assembler sized information that I’ve previ- In order to solve this problem, I
code to speed up the signature ously mentioned. There’s another introduced an additional TList
search, but in practice it wouldn’t subtle point here: if you look at a variable called SortMap. As with
be worthwhile unless you were ZIP file containing folder names, Dir, SortMap contains a list of point-
dealing with a very large ZIP file you’ll find that it contains an ers to the directory entries. When
that didn’t include the tail within empty, zero-length entry for each a sorting operation takes place,
the last 64Kb of the file. folder name within the archive. I the contents of Dir never change,
If a signature is detected another believe that these entries are used it’s the sort map that gets modi-
routine, IsValidTailPos, is called to to store information relating to the fied. To return to the raw, unsorted
validate the tail position. This is a folders themselves. For example, if state, all that’s necessary is to
very important and subtle point. you want a ZIP file to reconstruct a copy all the entries from Dir into
As you’ll no doubt appreciate, it’s directory tree, it should ideally rec- their corresponding positions in
perfectly legitimate to store one reate each directory with the origi- SortMap.
ZIP file inside another ZIP file, a nal modification date and time All good programmers know
child inside a parent, so to speak. information. However, I felt that that QuickSort is the best general
Because ZIP files are already highly these blank entries were poten- purpose sorting algorithm. If
compressed, a typical ZIP program tially confusing to end-users, and you’re unconvinced, try running
will try to compress the child ZIP so my TZipFile component silently Delphi’s THREADS demo program
file, fail, and default to merely hides them. This is done by the and you’ll soon get the message!
‘storing’ (CompressMethod = Stored) NonBlankEntry routine inside Load- The algorithm I use here is a stan-
the child inside the parent file. This DirEntry. You’ll find that my as- dard recursive QuickSort that op-
means that the enclosing ZIP will sessment of the number of files in erates on the SortMap array.
contain an uncompressed copy of an archive agrees with WinZip, You might wonder why I didn’t
the child. Now imagine what would which also hides blank entries for just call the QuickSort-based Sort
happen if our signature scanning the same reason. method which is now supported
routine picked up the tail signature by TList. There are at least two
of the child instead of the signature Let’s Get Sorted... good reasons. Firstly, as men-
of the parent. I can guarantee you The final thing I want to discuss is tioned earlier, I plan to port this
that this not only can happen, but the operation of the sorting code. code back to 16-bit Delphi, which
sooner or later it will: the voice of When the ZIP directory is read into does not support sorting in TLists
bitter experience! If this problem memory, the Dir variable is set up but only in TStringLists.
goes undetected, then deeply bad to contain a list of pointers to the Secondly, the Sort method in re-
things will occur! That’s the pur- individual directory entries. In cent TLists has been badly imple-
pose of the IsValidTailPos routine. principle, one could sort the direc- mented in an inflexible manner.
It ensures that the detected tail sig- tory entries by merely resorting The application-supplied compari-
nature really is the tail signature of the entries in this TList variable. son function only takes two
the outermost, enclosing ZIP file. However, if we did that, then we parameters, the items to be com-
Once we’ve got the tail, the rest wouldn’t easily be able to reverse pared. This means that without
is easy. LoadDirectory is called to the operation. In order to get back resorting to global variables, or
read the central directory into to the sRaw state, we’d have to re- some unpleasant hack, it’s impos-
memory, one entry at a time, and read the directory information sible for the comparison function
store it in the Dir TList variable. from disk, or perhaps allocate a to access all the information it
This routine calls another small large buffer to hold the entire, un- needs to make the comparison. In

August 1997 The Delphi Magazine 41


the present case, we’d either have (hopefully!) preserving backward can try it out. You can see the
to use separate comparison compatibility with 16-bit Delphi. program running in Figure 1.
functions for each distinct sort See how kind I am to you!
method, or else copy fSort into a
global variable so that it could be Next Month... Dave Jewell is a freelance consult-
accessed by the comparator. That’s it for this month. Next ant/programmer and technical
If Borland had defined TList- month, I’ll present the code for an journalist specialising in system-
SortCompare as a function of object all-singin’, all-dancin’ file find util- level Windows and DOS work. He
rather than a plain vanilla function, ity which will include the ability to is the author of Instant Delphi
then it would have been possible to search inside ZIP files. Programming published by Wrox
make the comparison function a The code for TZipFile is included Press. You can contact Dave as:
method of the TZipFile class. In the on this month’s disk, along with a [email protected],
event, I have rolled the QuickSort simple little test program so you [email protected] or
functionality into the class while [email protected]

One Last Compile...


I love my company, and my company loves me
L ast month we were taken off for our annual ‘Talk to
the Troops’ meeting. This is when senior manage-
ment take us off to a posh hotel somewhere and show
“Imagine!” said Quentin, a spotty graduate recruit
who had argued strongly for Monica. “They still have to
design their input screens using graph paper!”
us lots of slides about how well we’re doing. It’s also a We chortled to ourselves. We may be a pathetic
good opportunity to explain the new mission state- group of individuals with the combined social skills of a
ment, show us the new logo, and tell us that despite the dead llama, but at least we didn’t have graph paper on
fact the company’s doing well, there’s sadly no money our desks.
for a pay rise this year. But, in case we feel bad about “I feel sorry for them,” said Bert, who was a sensitive
this, they remind us that we are the company’s most soul, and who had voted for Phoebe. “They still have to
important asset. To prove this they give us each a free use dumb terminals hooked up to a mainframe.”
hat and a badge. Afterwards they put out some sausage We paused. Bert was right. A life without your own
rolls and some paper plates and while we eat, they scuf- PC on your desk: that was hard to imagine. No Internet
fle around the periphery, occasionally trying to mingle connection. No Jennifer Anniston wallpaper. No
with us. Minesweeper. Worst of all, no Duke Nukem. We looked
It’s deeply embarassing for everybody concerned, at the Cobol people with new eyes. They looked rather
and most people go straight home afterwards and work sad and lost. Not like us, cool cyber-surfers on the on
on their CV. The programmers all wander around look- the cutting edge of technology.
ing mortified and trying to avoid eye contact with any- Quentin snorted. “Have you seen the contractor
body who’s wearing a short skirt (probably in rates for Cobol people at the moment? These Year 2000
marketing) or anybody who seems to be permanently problems are pushing prices through the roof. See that
smiling (definitely in sales). Occasionally program- guy over there, the one in the really horrible sweater?”
mers will recognise colleagues from former projects I looked. They were all wearing horrible sweaters. So
and they will head joyfully for each other, happy in the were we.
knowledge that they can spend the next hour discuss- “Well, he doesn’t even work for us. He’s a contractor.
ing Great Bugs That Nearly Sank The Project or That Somebody told me he earns two thousand pounds a
Hilarious Time We Put Tea In The Coffee Machine. week! Maybe three thousand!”
This year I found myself clutching a glass of warm We looked at the Cobol people again. They looked
lager in the middle of a group of Delphi people. We rather smug now. Maybe life on the cutting edge wasn’t
stood there mournfully, like a group of despondent all it was cracked up to be.
sheep on a snowy hillside. We didn’t have anything to “I hear,” said Bert, “That all our work’s going to be
say to each other, so we talked about whether Quick- outsourced to India. They work for less than a dollar an
Report was better than Piparti (this lasted about three hour. And they write better code.”
minutes). Then we talked about what features we’d like “Hi guys!” said an oily management type who sud-
to see in Delphi 4 (four minutes). We talked about the denly appeared, “how are you all doing?”
new mission statement (eighteen seconds). Then we “We’re fine thanks,” we all chorused. “Great presen-
talked about which of the girls in Friends we’d most like tation. Thanks for the hats. Nice sausage rolls.”
to go out with (twenty minutes). This last conversation “That’s good,” he said. “It’s important to have happy
got a bit heated, so we agreed to talk about something staff. See you next year.”
else. We spotted a group of Cobol programmers over to
our left.

42 The Delphi Magazine Issue 24


Implementing Class-Traps
For Delphi And C++Builder
Or, how to be notified of the birth and death of your objects
by Cyril Jandia

T his article presents a small,


quick ‘n dirty unit allowing you
to trap the creation and destruc-
Another is how to get a reference
to a “forgotten” exception object,
while we are already executing the
we must set our variables explic-
itly to nil after calling Free on
objects (Listing 1).
tion process of Delphi32 and finally part of a try...finally So, we cannot know whether a
C++Builder objects. This is block: remember that at that mo- non-nil value is actually a valid ref-
achieved thanks to the TClassTrap ment SysUtils.ExceptObject is nil, erence to an instance or not (ie
class, which offers a simple inter- this is the so-called “too late” case I pointing to garbage). We need
face for encapsulating this very described. Yet another example is something else. One idea is to
special service. Trapping object when you want to learn more about carry out some kind of “instance
creation and destruction with this (possibly undocumented) objects accounting” with, say, a kind of “of-
class gives us the opportunity to which Delphi itself creates for its ficial account” for a particular
obtain object references at quite own use. Borland C++Builder pres- class. Such an implementation is
unusual moments, when we would ents a great opportunity for this straightforward thanks to Delphi
normally say it is “too soon” or “too kind of learning! Plus there are which provides us with the most
late.” Or, put simpler, using the many cases where there isn’t one useful TList class: see Listing 2.
TClassTrap class is a cheap means line of source to help us. This code keeps references to in-
of hooking calls made to Create and stances of TMyObject in MyObject-
Free throughout our code. Objects Are Everywhere List as they are created and also
TClassTrap carries out its job at In Delphi we use objects all the ensures the list gets rid of the ref-
what is the very beginning (ie the time, even when just placing a erences as the corresponding in-
begin line) of a constructor or de- TButton onto a form. There is one stances to which they point are
structor’s body. I said “cheap” be- thing, however, which is not obvi- freed. So, instead of examining the
cause we don’t need the source ous in many cases, at least in our value of AnObj we can ask MyObject-
code of the classes we are inter- source code. How can we know List for information about any
ested in, nor any kind of explicit which classes actually have in- instances which are still alive.
overloading of what is defined in stances and which do not? As far as This seems to achieve what we
the .dcu (.obj in C++Builder) object Delphi Object Pascal is concerned, were looking for. One possible
files we use. we can only know for sure whether problem is the loss of efficiency in-
Though originally designed for a variable does not contain a refer- troduced by the extra code: in
Delphi 2 and C++Builder 1, ence to an object if its value is nil, some intensive computing tasks
TClassTrap works fine with Delphi 3 that is it does not point to an this overhead may not be accept-
as well, with minor changes to the object. able. But I’m sure you have found
TVmt record’s definition, as I Fine, but if it is not nil we just the real problem with this method:
describe later. don’t know if it points to a valid it’s shouting from the code layout!
As the title points out, what is object or not. If we called Free on It’s related to readability and code
presented applies equally to TOb- the reference, it could point to no maintenance, because object-
ject derived classes written with object, but it is not automatically oriented design principles
C++Builder. To avoid annoying set to nil after the call to Free. So, strongly dissuade us from using
references to “Delphi32 and to be able to use a test against nil such a programming style with
C++Builder,” only the name
“Delphi” will appear in the text
from now. ➤ Listing 1

var
TClassTrap In Use AnObj: TMyObject;
As one example, we can have a ...
if AnObj = nil then
solution to the problem of finding { we know here for sure that AnObj doesn’t point to a TMyObject }
else begin { it’s not nil: let’s assume AnObj points to a TMyObject }
the instantiation order of controls AnObj.Free;
AnObj := nil; { since we plan to test safely against nil later }
created by “foreign” forms (ie end;
where no source code is available).

August 1997 The Delphi Magazine 43


tightly-coupled lines. Rather, we’d this like other languages, but even tor which is what we must override
prefer that TMyObject register itself a little bit more since any Delphi to customise the instance destruc-
in MyObjectList. See Listing 3. class has a VMT. This is because tion process properly. Listing 4
But now we have a new problem. the ultimate ancestor, TObject, shows Delphi 2’s VMT layout, as
We didn’t really want TMyObject’s from which all Delphi classes de- presented in Ray Lischner’s book
implementation to change because rive, already has a VMT with re- Secrets of Delphi 2.
of our need for instance account- served entries for some predefined Note I have modified slightly
ing. Also, this technique is not of virtual methods it defines. Well Ray’s definition of the VMT record
much help if we have no source known to us is the Destroy destruc- layout to support Delphi 3’s.
code for TMyObject.
So then, how about an idea
➤ Listing 2
which may look somewhat crazy at
first: could we hook into Delphi’s unit u_MyObj;
...
own mechanism of object creation var
and destruction? After all, Win- MyObjectList: TList{of TMyObject};
AnObj: TMyObject;
dows itself has in many circum- ...
AnObj := TMyObject.Create;
stances forced us to carry this out try
(see the Windows API reference on if MyObjectList.IndexOf(AnObj) < 0 then MyObjectList.Add(AnObj);
...
Hooking functions). Couldn’t we finally
if MyObjectList.IndexOf(AnObj) >= 0 then
use an analogous technique with MyObjectList.Delete(MyObjectList.IndexOf(AnObj));
AnObj.Free;
Delphi Object Pascal and/or its end;
compiler? ...
initialization
Inside the unit SYSTEM.PAS we MyObjectList := TList.Create;
finalization
find lots of information on object if MyObjectList.Count > 0 then
creation and destruction, includ- { error: some TMyObject’s haven’t be freed!
equally, we would have forget to set to nil somewhere... };
ing the VMT, or Virtual Method MyObjectList.Free;
end;
Table.

Secrets Of The VMT


➤ Listing 3
Strongly typed, compiled object
oriented languages which support constructor TMyObject.Create;
polymorphism often use what Bor- begin
inherited Create;
land Object Pascal people call a ...
if MyObjectList.IndexOf(Self) < 0 then MyObjectList.Add(Self);
Virtual Method Table. This special ...
end;
compiler generated data structure
...
is very handy for implementing destructor TMyObject.Destroy;
var SelfIndex: Integer;
polymorphic classes, that is, begin
classes the instances of which ...
SelfIndex := MyObjectList.IndexOf(Self);
have an actual type known only at if SelfIndex >= 0 then MyObjectList.Delete(SelfIndex);
...
run time. In fact, it seems to be the inherited Destroy;
most straightforward way for im- end;

plementing polymorphism effi-


ciently. Thus statically (ie when
parsing the source code) refer-
ences to polymorphic objects are ➤ Listing 4
of the type of a well-known com- type
mon ancestor, let’s say TVehicle. PVmt = ^TVmt;
TVmt = record
But, dynamically (ie at run time) {$IFDEF VER100} // D3 specific: both fields prepended since Object Pascal 9.x
those references actually point to (D2, BCB)
Vmt: Pointer; // most likely related to use of ClassParent field below(?)
objects of various derived types, IntfTable: Pointer; // for D3’s COM interfaces support stuff
{$ENDIF}
such as TMotorBike, TCar and so on. AutoTable: Pointer;
InitTable: Pointer;
Each of these instances has a hid- TypeInfo: Pointer;
den pointer to the VMT of its actual FieldTable: Pointer;
MethodTable: Pointer;
run time class type: approximately DynMethodTable: Pointer;
ClassName: PShortString;
equal to a TClass value, also known InstanceSize: Cardinal;
as the “metaclass” (more on this ClassParent: Pointer;
{$IFDEF VER100} // D3 specific: field inserted since Object Pascal 9.x (D2, BCB)
later). Note that no matter how SafeCallExceptionMethod: Pointer;
{$ENDIF}
similar two derived class types DefaultHandler: Pointer;
NewInstance: Pointer; // what we’ll hook
sharing a common ancestor can FreeInstance: Pointer;
be, the compiler will generate two Destroy: Pointer; // what we’ll hook
end;
distinct VMTs. Delphi seems to do

44 The Delphi Magazine Issue 24


Normally, this should remain ok dynamic method calls. But that’s still under the control of the com-
with Delphi 2 and C++Builder as another story: see Ray Lischner’s piler generated code. But it would
well since they defined, respec- book for further details. be really cool to have a means of
tively, VER90 and VER93 (Delphi 1 The undocumented Assembly “plugging” our own pointers into
was VER80, that is Object Pascal View in Delphi 32 is very helpful for the two “slots” NewInstance and
version 8 after Borland Pascal 7). a better understanding of all this: FreeInstance represented within
We saw a polymorphic object in- switch it on and off with the follow- the TVmt record. You guessed it, we
stance has an embedded pointer to ing REG_SZ typed key in the do have one, it is called Virtual-
the TClass designating record of its Windows Registry: Protect. This function of the Win32
actual metaclass (ie a TVmt), its API allows a process to modify the
“family” name if you prefer. Well HK_CURRENT_USER\Software\ protection attributes of pages of its
it’s almost true. Delphi objects Borland\Delphi\[2|3].0\ virtual address space. With Vir-
have such a pointer, but this one Debugging\EnableCPU tualAlloc we can modify the pro-
actually points to just past the end tection attributes of the pages in
of the per-class fixed-size TVmt rec- setting it to 1 or 0 respectively. which our classes’ VMTs reside,
ord, instead of pointing to this re- So, how does Delphi manage ob- for instance from PAGE_EXE-
cord’s beginning. The location ject creation and destruction using CUTE_READ to PAGE_READWRITE.
pointed to is where pointers to the two TVmt fields NewInstance and Win32 has the reputation of be-
programmer-defined virtual meth- FreeInstance. After studying the ing a more secure, powerful API
ods are stored, consecutively, us- contexts in which these identifiers than Win16 could ever be, a main
ing four bytes each (they are 32-bit appear and how they are used, we concern of which is to prevent ap-
pointers). So, if we cast a TClass can deduce quite easily which very plications from corrupting each
value to a pointer we get the place specific role they play in the crea- other’s data. It may sound strange
where Delphi’s compiler stored tion and destruction process. First, therefore to read that Win32
the pointers to all the virtual meth- it is important to understand they doesn’t forbid self-modifying code
ods this TClass has either intro- do not handle the entire process for a process. It is not that strange.
duced itself, or inherited from its alone. Thus, when we write some- In fact, self-modifying code is al-
direct ancestor. thing like TMyObject.Create, in addi- lowed for a process because Win32
tion to the code generated by the considers it is the process’s own
Step-By-Step compiler for preparing the access responsibility to decide to change
Virtual Method Call to the metaclass value (TMyObject), page protection attributes if so de-
Calling a virtual method on an there are also two other routines sired, but only for pages belonging
object consists of: involved that are out of direct to its own virtual address. So that a
1. Determining statically (ie at reach: _ClassCreate and _ClassDe- process with default application-
compile time) what one could call stroy. It seems they have to do with level privileges cannot change the
the “virtual index” of the method: the handling of an exception frame protection attributes of another
such a constant index is unique to to be used in case of failure. Only process’s pages. In short, Win32
a virtual method name used then does the code pointed to by assumes every process knows ex-
throughout a whole set of ancestor NewInstance and FreeInstance carry actly what it is doing within its own
and descendant classes linked out the real job, either allocating address space and why, but the
together by direct inheritance. and zeroing the dynamic memory same process cannot decide
2. Getting the 32-bit pointer to chunk required by a fresh class in- changes in another one’s. Thus,
the end of the object’s TClass desig- stance before executing the first when we want, with the help of the
nating record (ie a TVmt) a pointer statement of the class constructor, TClassTrap class to plug “foreign”
embedded in the object itself, at or deallocating an existing class in- pointers into the NewInstance and
offset 0. stance just after the last statement FreeInstance slots of an arbitrary
3. Retrieving the pointer to the of the class destructor has been TVmt record, we are modifying our
virtual method from the array executed. It’s worth noting that own code.
pointed to by 2. using 1. Delphi defines default implementa-
4. Carrying out the call to the tions of these methods, so that, Rewriting NewInstance
code pointed to by 3. with rare exceptions, all classes And FreeInstance
Note that though this scheme use the same code when it is time It is one thing to find a API trick for
seems lengthy, in fact the compiler to New or FreeInstance a chunk of redirecting compiler-generated
generates quite straightforward dynamic memory. pointers to NewInstance and Free-
code for these four steps. So the Instance within an (also)
code remains fast: when compared A Useful Plug compiler-generated TVmt record,
to a static method call, a virtual As just stated, we cannot force but it is another thing to implement
method call overhead is not a rele- Delphi to create or destroy objects properly our own versions of both
vant issue nowadays. Anyway, the another way, at least at the level class functions in order to manage
biggest known cost of this call _ClassCreate and _ClassDestroy are the list of instances for a particular
speed issue is experienced with called, because at this level we are TClass. For the latter task, we have

August 1997 The Delphi Magazine 45


to rely on another trick, a Delphi The first step is a static one, in remarks, and “ChD” for construc-
Object Pascal trick more precisely. other wards it is done at compile tive criticism of early code ver-
What we want is a simple way of re- time of our unit, once for all. The sions. You all three have been of
defining both NewInstance and second is handled at run time when great help. I must also say Ray
FreeInstance class methods and at the programmer will decide to Lischner’s book Secrets of Delphi 2
the same time connect the corre- register a new class-trap for a has inspired me a lot.
sponding TVmt record’s slots of the particular TClass value. Hence the
class to trap to these redefinitions. source code found in the unit
The problem is the phrase: “at the OBJTRAPS.PAS unit on the disk. After two years as a technical
same time.” It is not a good idea. support engineer at Borland
Even more, we simply cannot do it It’s Up To You Now France Support Department for
that way. Listing 5 shows a basic class trap- support contracts, Cyril Jandia,
In fact, we have no other choice ping example. I’m sure you’ll find AKA FLFan, has recently re-
than proceeding in at least two plenty of uses for TClassTrap, be it oriented himself towards training
steps. First, we redefine NewIn- for debugging purposes, for class on Borland products. He’s cur-
stance and FreeInstance in a ge- instance accounting in simulation rently preparing a home for his
neric fashion (see TTrappedObject models, for Delphi experts, or for fiancée Caroline, still too many
in the implementation part of the anything else: Delphi program- miles away, of whom he impa-
ObjTraps unit, on the disk of ming is only limited by our own tiently awaits the arrival in the
course). Then and only then we imagination! capital. Both love England and its
plug the resulting code pointers culture and hope to spend more
into the NewInstance and FreeIn- Acknowledgements time in the country soon!
stance slots of the class to trap (see Very special thanks are due to: Roy
TClassTrap.SetTrapProc and TCla- Nelson of ETT for his support on
ssTrap.SetMagicHooks in the code pre-releases of this article, Hervé
on the disk). for his many good questions and

Recommended Reading
➤ Object-Oriented Software Construction, Bertrand Meyer,
Prentice-Hall, 1988 (French version by InterEditions, Paris,
1990).
➤ Eiffel — The Language, Bertrand Meyer, Prentice-Hall, 1991
(French version by InterEditions, Paris).
➤ Delphi 2 — Programmation Avancée, Dick Lantim, Eyrolles,
1996 (great on Win32’s IPC, the OpenTools API and more).
➤ Delphi 2 Developer’s Guide, Xavier Pacheco and Steve
Teixeira, SAMS, 1996.
➤ The Revolutionary Guide To Delphi 2, various authors, Wrox
Press, 1996.
➤ Secrets of Delphi 2, Ray Lischner, Waite Group Press, 1996
(Valuable insights on the VMT, RTTI, etc and confirms what
we can learn by ourselves from long journeys deep into the
RTL/VCL source, simply a “must have” book!).

➤ Listing 5

unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm) Memo1: TMemo;
private
public
end;
var Form1: TForm1;
implementation
uses ObjTraps;
{$R *.DFM}
procedure MemoTrap(const trap: TClassTrap;
const obj: TObject; op: TObjectOperation);
begin
if op = ooCreate then
MessageBox(0, ‘TMemo created’, ‘’, MB_OK)
else
MessageBox(0, ‘TMemo freed’, ‘’, MB_OK);
end;
initialization
MakeTraps([TMemo], MemoTrap);
end.

46 The Delphi Magazine Issue 24


Nashville BDC: A Mike’s Eye View
by Mike Orriss

EVERY YEAR, Borland organ- of the hotel? A speaker later re- The first real business of the day
ise a Conference somewhere in marked “It’s like living in a terrar- was registering for the conference,
the USA, with lots of technical ium, I keep expecting to see a giant which went very smoothly with no
sessions in a variety of tracks, an eye peering through the glass queue (I was almost disappointed
exhibition of third-party tools, roof.” having got so used to queuing for
books and the like, and usually By the time I had booked into the everything). I was given the usual
a bit of fun too. Your Editor is hotel, found my room and re- Borland bag and retired to a quiet
(as usual!) too busy working on moved the travel dust, it was too corner to explore its contents.
the next magazine to go hop- late to register for the conference First there was a name badge to
ping off around the world on and the rest of the evening was hang around your neck, with a
such jollies, so he asked me to spent in a steak house with Mike name label (coloured blue for
report back on my experiences and John together with another UK Delphi) which followed the time-
of this year’s conference in visitor, Mark Smith, who we honoured tradition of being just
Nashville, Tennessee... collected from the bar next door. too small to read without invading
the other person’s personal space.
Next was a small exhibits guide

T his report is essentially one


Delphi developer’s impres-
sion of the event. It does not at-
booklet, then a (larger) Conference
programme booklet, the Session
outlines (a heavy tome), a T-shirt
tempt to cover all the marketing (amazingly, it fitted). The afore-
razzmatazz that surrounded us nor mentioned Conference CD came
does it mention much about out next, and lastly various adver-
JBuilder, which seemed to be the tising brochures, including a very
main focus this year. The report professional glossy from Borland.
does describe what I found inter- As well as this standard stuff I
esting and hopefully should whet also received a small badge (a pin
your appetite for next year’s event, in American speak) because I at-
which will be back on the West tended last year’s conference, and
Coast. an invitation to an informal “Con-
nections” breakfast (for members
Initial Impressions of Borland’s Connections scheme).
After a fairly uneventful seventeen Having decided to forgo the
hour journey, the usual mixture of various pre-conference tutorials
fatiguing boredom and long through Saturday and Sunday, the
queues, I reached the Opryland conference for me really started at
Hotel at 5:30pm on the Saturday noon when the exhibitors hall
afternoon. I walked into one of the opened. Attendance here served
five lobbies and immediately two purposes: to see what new
bumped into the British Merlin goodies are imminent and also to
team: Mike Scott and the luggage- Sunday meet up with people not seen since
less John Howe (it arrived later Thanks to the six hour time differ- last year. The last point was impor-
that night). ence, Sunday started a bit early, tant: the hotel is so big and without
The hotel itself was very impres- 4:30am in my case. an obvious meeting place that I
sive: five main buildings and the Still, it gave me chance to unpack had seen very few people that I
complex covers 31 acres (not in- properly and get my portable con- recognised.
cluding the Opryland Pleasure nected up to CompuServe, which Overall, the exhibition was not
Park alongside). It felt rather like a proved easy thanks to the data as good as I expected. I did, how-
bigger version of Main Street in connection on the side of the room ever, manage to buy a signed copy
Disneyworld, an impression rein- telephone. Incidentally, the main of Ray Lischner’s new book Hidden
forced by walking over a bridge reason for lugging my portable Paths of Delphi 3 from the author.
inside the hotel and seeing a cruise here was to be able to look at the At 5pm over 2,700 (compared
boat complete with microphoned conference CD to help me see with 2,500 last year) of us were
guide pass underneath! Where else which sessions were worth attend- massed in the hotel’s largest con-
do you see guests queuing to pay ing, time will tell if this reason was ference room for the opening key-
$4 for a boat trip around the inside valid. note presentation given by Del

August 1997 The Delphi Magazine 47


Yocam, Borland’s CEO. This year Next was the keynote presenta- constructed and it would have im-
the theme was Star Wars, with tion by Corel. Here I must confess pacted the functionality added to
Borland coming to the rescue of to playing hookee, preferring to Delphi 3.
data trapped by the evil empire! join a group discussing Delphi For the final session, I attended
Most of the speech related to matters. Marco Cantú’s The Fun Side of
Borland’s Golden Gate initiative Following lunch, I had decided to Delphi, which proved to be very
and the concept of the InfoNet was see Nick Hodge’s Writing Web entertaining. Topics ranged from
introduced. References to Delphi Server Extension DLLs with Delphi the Nothing Form to the ScreenVi-
included the forthcoming Enter- but arriving 10 minutes before it rus component, where we learned
prise edition with links to Entera started I found the room already how to make the screen develop
and we were also told that Delphi full. Instead, I went back to my red spots.
sales have just passed the million room for a shower and to continue Finally, a group of us ended a
mark! this report. long exhausting day at the Jack
Following the keynote, the open- My choice for the next session Daniels Saloon.
ing reception proved just as tiring was Building ActiveX Controls with
as last year’s and jet lag caught up Delphi by Conrad Hermann, which Tuesday
with me at 9pm. I found slightly disappointing: too Another early start as I felt duty
much background explanation and bound to attend the Birds of a
Monday not enough demo. I followed this Feather session at 7am hosted by
Monday started bright and early by attending Nick’s next session Mike Scott and John Howe on
with the Connections breakfast at Building and Deploying Active Merlin (we do have to support UK
7am. This was excellent and I sat at Forms in Delphi. This talk was initiatives!). These ‘B of a F’ ses-
a table of Delphi people. We were excellent, well presented and sions are last minute items where
joined by Rick Le Faivre, Borland’s covered many tips and gotchas. attendees can set up their own
new head of technology, and he Next, it was back to the exhibi- thing with rooms and facilities sup-
was left in no doubt as to the con- tors hall to join the throngs queu- plied by the conference. Unfortu-
tinuing requirement for Delphi 1.5 ing up for pizza and beer. This nately, the session was only really
and BDE16 bug fixes and enhance- proved to be an excellent opportu- publicised thirty minutes after it
ments. He also took note of our nity to get together with people I started so attendance was low, but
request for better two-way commu- had met last year. it was well received. [Look out for a
nications between Borland and de- The first evening event was Meet review of Merlin by Mike Orriss in,
velopers. It will be interesting to the Delphi Development Team, hopefully, the launch issue of Devel-
see if anything does come from hosted by Gary Whizzin. First the opers Review, our new magazine.
this, but I’m not holding my breath. team (or about ten of them any- Editor]
The first session of the day was way) were individually introduced I skipped the 8am session, pre-
the Delphi Product Address. In and the session continued with a ferring a leisurely cup of coffee: I
view of the other presentations good-humoured question and an- was beginning to suffer from
(and the fact that it was repeated swer session with the questioners session overload!
on Tuesday) I was a bit surprised lined up in two fairly long queues at 9:30am saw me at Ray Konopka’s
to see so many people attending the sides of the room. The ques- Advanced Delphi Component Top-
(about 1000). The talk was given by tions that occupied most time ics. Although the presentation was
Lance Devin (Delphi Product Man- inevitably concerned the docu- good, for me it was a mistake be-
ager) and Chuck Jadjewski (Mr mentation. The team acknowl- cause I had seen most of the topics
Delphi since the departure of edged the problem and said it was before. This was followed by the
Anders). Lance showed a nice caused by losing the whole docs daily keynote speech, this time
three tier database application us- team between Delphi 1 and 2 (self- from IBM on the AS400.I decided at
ing four linked portables. After an inflicted?) and the whole structure the last moment that my time
initial mention of Delphi 4, the of the help system has had to be would be better spent elsewhere
presentation was disappointing. re-worked and is an ongoing pro- so I re-visited the exhibitor’s hall.
There was no real news except the cess. New documentation files are Loren Scott of Luxent Software
announcement that Delphi 3.1 is in beta and should be available on (born of Apollo and LightLib) gave
now in beta and the professional the Web shortly. Another topic me a demo of version 2 of
version of the update will include covered in depth was the need for DBPower: this is an impressive
the missing socket components. an official Delphi bug list. Gary package which brings database
For the next session, I just had to Whizzin publicly bestowed owner- desktop facilities right into the IDE,
attend Bob Swart’s Writing Delphi ship of this item to Ben Riga so together with an enhanced
Experts talk else he would never hopefully this should at last come DBGrid.
have forgiven me... The talk was re- to pass. Finally we had a definitive After lunch, Marco Cantu gave a
ceived very well, but I won’t go into statement from Gary that Delphi very interesting talk that com-
details as readers of this magazine 1.5 will not happen: he stated that a pared the C++, Object Pascal and
will be familiar with the topic. business case could not be Java languages. In his opinion, Java

48 The Delphi Magazine Issue 24


is closer to Object Pascal than to like Control Panel, Printers and At 8am I attended the session
C++. His overall summary con- Dial-up Networking) while the right which proved, for me, to be the
cluded that C++ was built for speed panel then displayed a Delphi form! most useful of the entire confer-
and control, Delphi for speed and Now it was time for the Confer- ence. This was given by Bill Fisher,
ease of use and Java for compatibil- ence night out and we all trooped a Borland Technical Support Engi-
ity, sacrificing both speed and across to the Opryland theme neer, on Using the Power of the
power! park. Leaving the hotel for the first Abstract TDataSet which is a topic
Next I saw Eagle Software’s time, the 93 degree temperature where documentation is some-
Raptor demo. Here, Mark Miller and 90 percent humidity came as a what lacking. He showed that
was flying by the seat of his pants bit of a shock. I was with Mike hooking up a new back-end data-
(as usual!): what with a faulty disk Scott, John Howe and David Baer base is done by supplying code for
cable and self-inflicted problems and we ignored the sign saying 29 callback methods and three
caused by continuing product de- “You will get wet” and spent the events and also showed the code
velopment while at the conference, rest of the evening very wet indeed required to hook up to BTrieve. Bill
Raptor was only up and running after going on the rapids ride. Un- did make the point that it is easier
about two minutes before the fortunately, the humidity meant if you know the database before
scheduled start. The demo itself that we didn’t dry out as expected: you start as it made his task far
was very impressive and I’m sure this put a bit of a damper (sorry!) more difficult having to learn
that the product will be very on the evening and we left early. BTrieve as he went along.
successful. For the next session I attended a
For the final session of the day, I Wednesday Vendor’s Showcase: Integrated
went to John Lam’s Fusing 32-bit Yet another 7am start for another Debugging for Delphi with Bounds-
Applications with Explorer and the Birds of a Feather session. This was Checker 5. This I found extremely
Windows95 Shell. This presenta- given by Mike Scott on writing wiz- disappointing. I have owned this
tion made me think more than any ards for Merlin and was much bet- program for a while, but have
other I had seen thus far: it opened ter attended. The Merlin wizard never spent the time to get the best
my eyes to a complete new set of wizard expert (or was it expert wiz- out of the product and I was hop-
programming possibilities. The ard wizard...) makes these things ing to get a few tips and tricks from
demo showed John’s custom so easy to create that I predict that the horse’s mouth. Instead, they
folder in the Explorer left pane (ie we will be overrun with them! turned the session over to an

August 1997 The Delphi Magazine 49


independent reviewer who only popular that finding a free PC could each session). By popular request,
covered the absolute basics and I be problematic. the Star Wars video shown at the
learned nothing. After the customary final day opening keynote was re-shown.
After three solid hours I needed packed lunch, I attended the final This was an incredibly profes-
a break and decided to skip the fi- keynote speech which was given sional production and I hope that
nal morning session. I had been in- by Larry Ellison, Oracle’s founder Borland include it on the final CD
tending to go to Carlton Hewitt’s and CEO, by satellite. This was a for the conference, which we are
Building Business Objects in Delphi very impressive speech which ex- scheduled to receive in the next
and missing it seems to have been pounded his vision of the light- couple of months. The final item of
another mistake. An attendee told weight network PC as a universal the conference was the video shot
me how Carlton used Data Module commodity. He was very persua- throughout the event, showing at-
inheritance and a message passing sive, although the speech was tendees, interspersed with Star
system to keep business objects tinged with a fair bit of Bill Gates Wars out-takes. I was very relieved
and rules truly reusable for differ- paranoia. The speech was followed that I was edited out of this as the
ent projects. I must check the CD to by a question and answer session roving camera caught me totally
see how well that process is de- and he very deftly handled some unawares and I must have looked a
scribed. By the way, bringing the verbal grenades lobbed at him. right prat...!
portable in order to check the CD The closing session followed Well that’s it. I’m sitting in my
didn’t work: I could never find the with all the usual items at such oc- room at 8am the following morn-
time to look at it. There was an- casions: thanks all round, drawing ing, finishing this report ready to
other option though: the Computer of raffle tickets (these having been email it off to the Editor prior to
Lab had about 70 pentiums with previously “purchased” by com- packing and the long trip home...
web access, although this was so pleting speaker evaluation forms at

More News From Nashville


The annual conference is a favourite time for Borland to issue a
flurry of press releases. Here’s a selection of news items...
A New Era Of Corporate Infonets convergence of client/server with Internet and Intranet
Chairman and CEO Del Yocam outlined a new strategy application development technologies and technolo-
to establish Borland as the leading provider of tools gies for decision support and on-line processing.”
and technologies to help customers build “corporate Yocam claims the InfoNet concept addresses several
InfoNets” for accessing and analysing information and key problems facing enterprises, including:
applications across the enterprise and beyond. • Turning raw data into useful business information
As a critical part of its new focus, Yocam said, and knowledge.
Borland plans to combine its object-oriented develop- • Deploying modularized configurable information
ment tools, middleware technologies and open archi- applications across the enterprise, thereby allow-
tecture to allow users to create powerful applications ing end user customization without having to
that provide cross-platform access and analysis of cor- rewrite custom applications.
porate data over Intranets, Extranets and the Internet. • Shifting more business logic and analysis to
For example, Borland plans to bring to market new middle-tier servers, allowing access from both fat
technologies and products that support connectivity and thin clients, increased reliability and security,
to enterprise applications, distributed applications and reduced cost of deployment and maintenance.
management and decision support capabilities.
Construction of corporate Information Networks, or Entera Updated
InfoNets, offering cross-platform access to valuable An update was announced to Borland’s Entera intelli-
information and enterprise-wide applications, repre- gent middleware product which enhances the pro-
sent one the most potent opportunities facing IT or- duct’s support for Delphi, ODBC data connectivity and
ganizations in the late 1990s, according to Yocam. The Gradient/DCE on Windows NT. Borland will also be ex-
new InfoNet strategy is a major extension of the Golden tending Entera 3.2’s platform support with a new port
Gate strategy announced in 1996, which focused on to IBM MVS/Open Edition. There will be a major new
bridging client/server systems and emerging Multi-tier version of Entera later this year.
and Internet/Intranet architectures. The new update to Entera 3.2 is available to all regis-
“We predict that the InfoNet will become an essen- tered Borland Entera support subscribers. To register
tial, cost-effective computing platform for corporate for the beta program for the new Entera 3.2 version for
customers during the next three years,” said Yocam. IBM MVS/Open Edition, contact your local sales rep or
“Corporate IT will experience an increasing the Borland Direct Sales team at +1 408 431 1064.

50 The Delphi Magazine Issue 24


AS/400 Client/Server Developer Program Delphi/400 Client/Server Suite is available now and
There will be a new developer relations and marketing C++Builder/400 Client/Server Suite is scheduled to be
program in conjunction with IBM’s AS/400 Division, de- available this quarter from Borland and authorized
signed to encourage more client/server applications to Borland/400 Business Partners.
be built with Borland’s tools that support the AS/400
series of business computers: the AS/400 Application Jbuilder Java Development Tools
Development Initiative (ADI). The JBuilder family of visual development tools for
This quarter, Borland and IBM’s AS/400 Division will Java are finally scheduled to begin shipping this quar-
be introducing a three-part partnership program ter. JBuilder Standard, JBuilder Professional and
called AS/400 ADI, designed to assist Borland and IBM JBuilder Client/Server Suite feature what Borland claim
AS/400 developers to support, register, and market is the industry’s fastest and easiest JavaBean compo-
their client/server applications on the IBM AS/400 plat- nent creation, a scalable database architecture, robust
form. The main elements of the ADI program are: visual development tools and the ability to produce
• AS/400 ADI Website with technical information. In “100% Pure Java” platform-independent applications,
addition, ADI developers will be able to register and applets and JavaBeans. A demonstration version of
list their applications on the site: creating an elec- JBuilder is publicly available on Borland’s website.
tronic, client/server buyer’s guide for AS/400 cus- As part of Borland’s Golden Gate strategy to support
tomers. Developers interested in registering their major industry standards in all of its tools, JBuilder’s
applications on the ADI Website may send e-mail to open environment will support 100% Pure Java, Java-
[email protected] or visit www.borland.com/ Beans™,JDK™1.1, JFC, AFC, RMI, CORBA, JDBC, ODBC,
borland400/ to receive more information. and all major corporate database servers. It will also
• Access to IBM AS/400 World-Wide Porting Labs. provide developers with a flexible open architecture to
IBM’s AS/400 Division plans to open their world- incorporate third-party tools, add-ins and JavaBean
wide porting centers and selected other sites to ADI components.
developers wishing to port their existing client/ To increase developer productivity by encouraging
server applications. component reuse, JBuilder will feature BeansExpress,
• World-Wide ADI Seminar Series in more than 15 cit- claimed to be the fastest and easiest way to build JDK
ies on client/server development for the AS/400. A 1.1-compliant JavaBean components. With BeansEx-
listing of these seminars will be made available on press, developers can visually create components
the ADI Website. from scratch, or combine a number of existing

August 1997 The Delphi Magazine 51


components into a new JavaBean. BeansExpress will • New Application Browser: combines the features of
also allow developers to easily deploy their JavaBeans a project manager, class browser, file browser, and
and add them to JBuilder’s easy-to-navigate compo- source code editor to make managing, navigating,
nent palette. Each version of JBuilder will include a set and editing source code easy and dynamic.
of award-winning JavaBeans from KL Group and Java- • Powerful graphical debugger and SmartChecker
Soft’s standard AWT components. JBuilder Profes- Compiler: to makes it easier to find and fix bugs in an
sional and Client/Server Suite will also ship with over application’s code, then rebuild/recompile.
100 reusable JavaBeans for GUI design, Internet devel- • Local InterBase server and Borland SQL Tools for
opment and database access, including Grid, Tree, building, managing and deploying scalable data-
Navigator, Image, Checkbox, Choice, Picklist, Field, base server applications.
List, Locator, ButtonBar, StatusBar and many more. JBuilder Standard has an estimated street price of
Source code for these components will be included. $99.95; owners of other Borland tools can purchase
JBuilder will include a new, scalable database archi- JBuilder Professional for $249.95 and owners of com-
tecture called DataExpress. With DataExpress, devel- petitive tools can purchase JBuilder Professional for
opers will be able to use drag-and-drop data-aware $299.95 (all prices are valid for USA only).
components and visual design tools to build database
applications and applets using industry-standard JDBC Visual dBASE 7 Preview
database connectivity. This major upgrade was demonstrated at the BDC and
Productivity features in JBuilder include: is scheduled to ship this autumn. The new 32-bit ver-
• The Borland RAD WorkBench: an IDE similar to that sion will feature support for ActiveX components, a
in other Borland visual development tools, with a new Two-Way object-oriented report designer, en-
tightly-integrated application browser, project hanced visual productivity tools and native high-speed
manager, code editor, HTML viewer, graphical connectivity to dBASE, Paradox, MS Access, MS FoxPro
debugger and compiler. and corporate SQL database servers.
• Pure Java Two-Way-Tools: the Pure Java source
code editor and visual designers are always syn- For More Information...
chronized, with no hidden macros or tags. Check out Borland’s website at www.borland.com for
• Productivity Wizards: to simplify development and updates on these news items, or contact your local
deployment of projects, applications, applets and Borland office or dealer.
JavaBeans.

52 The Delphi Magazine Issue 24


Review:
ISGMAPI 2.0
by Peter Hyde

C ommunications programs of any kind used to be


the bane of our lives. Because they covered the
area where our tidy finite state machine met the Real
World, with all its failings, they invariably resulted in
thorny debugging problems: whether of code, proto-
col, modem init strings or cabling.
Fortunately, the Windows platform has rescued us appear to be an easy way to interrogate them from an
from much of that and, with the plethora of POP3, external program. With such caveats in mind, MAPI
SMTP, serial communications and fax components communications using ISGMAPI will provide a conven-
now available for Delphi, it is relatively easy to build a ient solution for many, but not all, comms applications.
reliable solution for almost any client requirement. The example apps, help files and FAQ will get you off to
But before racing out to communication-enable your a very quick start, with email support also available.
application with any of those components, there is an- An ISGMapi 2.0 trial version can be downloaded from
other viable approach to consider, one which can give Infinity Software Group at http://www.infsoft.com/
you a comms-enhanced application yet which by- isgwww/mapi/mapindex.html. Credit card registration
passes the need for all that paraphernalia. can be made by telephone or online and costs $US110
Personally, I had no use for the “Inbox” on my Win95 for both 16- and 32-bit versions, including source.
desktop until the day a client requested an application
which could send faxes, Internet email and MSMail as Peter Hyde is the author of TCompress for Delphi/
required. But behind the Inbox lurks Microsoft’s vast C++Builder and Development Director of South
(and remarkably complex) MSExchange application Pacific Information Services Ltd. Contact Peter at
which happens to provide all those services and more. [email protected].
The trick, of course, is controlling that functionality
from Delphi, which is where the ISGMAPI component
comes in. There are alternatives, such as wading
through the MAPI (Messaging API) documents and
header files: not for the faint-hearted! ISGMAPI
provides a good level of control and interaction with
MSExchange and other MAPI-compliant services such
as Groupwise or MS Mail, for a reasonable price.
There is something very appealing about using the
same SendMail method to send a fax or email message,
complete with Word document attached, varying only
the URL passed to it. Methods are also provided for
checking and reading incoming messages, composing
messages on the fly and interrogating and using MAPI
Address Books. Good control is provided over how
much of the message user interface is handled by your
application as opposed to by the MAPI service itself.
Better yet, ISGMAPI includes “EForm” capability. An
EForm is an application which transmits and receives
its data via MSExchange. Data management and trans-
mission (and EForm execution at the client) is handled
by MSExchange/ISGMAPI. The read/write routines are
like those used to access INI files.
In surrendering much of your message management
to a MAPI service like MSExchange, some compro-
mises are involved. For example, your application may
queue 1,000 faxes for transmission by MSExchange,
but it won’t get direct feedback on how the actual
transmissions fared. Instead, the user must look at the
message logs kept within MSExchange: there doesn’t

August 1997 The Delphi Magazine 53


Edited by Brian Long
The Delphi Problems with your Delphi project?

CLINIC Just email Brian Long, our Delphi


Clinic Editor, on [email protected]
or write/fax us at The Delphi Magazine

Making A 256 Colour Bitmap Delphi 3 has rather more palette one of the button OnClick handlers
support built into the VCL and so from the Capture.Dpr project on

Q I am writing a part of my ap-


plication where I need to
capture a (known) area of the
to avoid doing repetitive heap
management calls, it defines a new
type that specifies 256 palette en-
the disk. This has an image control
called Img and three buttons on it.
The first button captures the top
screen and save it to a .BMP file. tries (see Listing 2). left portion of the screen (as big an
This would be fine, I can write the Your code that fails to save the area as the image started its life
API code to do this, but the screen palette probably looks a bit like with). The other two buttons save
is in 256 colour. When I check the Listing 3. It should be extended to the image to a BMP file and load a
bitmap file it only has 16 colours look more like Listing 4 which is BMP file into the image (to verify
stored in it. What do I need to do in
addition to just saving the bitmap
to get all the right colours saved
➤ Figure 1
into the file?

A What you are missing is a


colour palette. Just creating
a TBitmap object, writing onto its
canvas and calling the SaveToFile
method will make you a 16 colour
bitmap. To get a 256 colour bitmap
you need to manufacture a palette
and assign it to the bitmap’s Pal-
ette property. Fortunately, Win-
dows already has a palette set up
internally that the other applica-
tions are making use of. Since you
are capturing the screen then this
will be the palette you want.
➤ Listing 1
On the other hand, if you were
programmatically manufacturing
PPaletteEntry = ^TPaletteEntry;
an arbitrary bitmap, then you TPaletteEntry = packed record
might need to manufacture a cus- peRed: Byte;
peGreen: Byte;
tom palette with custom colour peBlue: Byte;
values in it. peFlags: Byte;
In Delphi, a palette is repre- end;
PLogPalette = ^TLogPalette;
sented by a TLogPalette (see List- TLogPalette = packed record
ing 1). This is a data structure that palVersion: Word;
palNumEntries: Word;
can hold a variable number of palPalEntry: array[0..0] of TPaletteEntry;
TPaletteEntry records. Since the end;
actual number to be used may
vary, the array has been declared
with just one element.
➤ Listing 2
To use a TLogPalette structure,
you typically declare a variable PMaxLogPalette = ^TMaxLogPalette;
that points to it (ie a PLogPalette). TMaxLogPalette = packed record
You can then allocate sufficient palVersion: Word;
palNumEntries: Word;
heap space for the fixed fields and palPalEntry: array [Byte] of TPaletteEntry;
also for as many palette entries as end;
are required.

54 The Delphi Magazine Issue 24


that the palette was saved cor- No button and got a nasty shock. If take a user-action of closing the
rectly). Figure 1 shows the pro- the user presses neither button form with Alt-F4 or whatever (ie
gram after having captured a but closes the dialog with the dia- no button pressing) to be a cancel-
corner of my 256 colour desktop, log’s system menu, then neither lation, so the MessageDlg routine re-
saved it and reloaded it from a BMP mrYes nor mrNo is returned by Mes- turns mrCancel under those
file. sageDlg. What should I do to trap circumstances. Therefore you
To accompany the Capture.Dpr for this possibility? could modify your code as shown
project, a Capture3.Dpr project is in Listing 6. Alternatively you
also supplied. This uses Delphi 3’s
new picture save and picture load
dialogs and also uses TMaxLogPal-
A I guess that you are using
code like that in Listing 5. It
is standard Windows practice to
could embrace that third option
and differentiate between No and
Cancel. No answers “No” to the
ette instead of PLogPalette to
avoid memory management
➤ Listing 3
operations.
procedure TForm1.Button1Click(Sender: TObject);
Background var
R: TRect;
Palette Information C: TCanvas;
When you are running Windows in begin
16 colour mode, there are 4 bits re- R := Img.BoundsRect;
C := TCanvas.Create;
quired to represent the total C.Handle := GetDC(HWnd_Desktop);
number of colours and so 16 colour try
Img.Canvas.CopyRect(R, C, R);
mode is also referred to as 4-bit finally
mode. Each pixel in the bitmap ReleaseDC(HWnd_Desktop, C.Handle);
C.Free;
takes 4 bits to indicate which of the end;
16 colours it is. These 16 colours end;
are used by the video system to in-
dex into the full range of colours
available. 256 colour mode uses 8
➤ Listing 4
bits to represent each of the 256
colours and again indexes into a
procedure TForm1.Button1Click(Sender: TObject);
colour table in the video system. const
Therefore 256 colour mode is often NumColors = 256;
var
referred to as 8-bit mode. A palette R: TRect;
can be used to specify alternative C: TCanvas;
target colours for the index values LP: PLogPalette;
Size: Integer;
to refer to in both 4-bit and 8-bit begin
mode. R := Img.BoundsRect;
C := TCanvas.Create;
If you are working in high colour C.Handle := GetDC(HWnd_Desktop);
modes such as 16-bit or 24-bit, then try
Img.Canvas.CopyRect(R, C, R);
the issue of palettes becomes Size := SizeOf(TLogPalette) +
somewhat redundant. In 16-bit col- (Pred(NumColors) * SizeOf(TPaletteEntry));
our mode (not to be confused with LP := AllocMem(Size);
try
16 colour mode, which is 4-bit), LP^.palVersion := $300;
each pixel uses 16 bits to represent LP^.palNumEntries := NumColors;
GetSystemPaletteEntries(C.Handle, 0, NumColors, LP^.palPalEntry);
the colour. Rather than an index, Img.Picture.Bitmap.Palette := CreatePalette(LP^);
this value is made up of three sets finally
FreeMem(LP, Size);
of five bits (and a spare) that define end
the colour in terms of how much finally
red, green and blue go to make it ReleaseDC(HWnd_Desktop, C.Handle);
C.Free;
up. In 24-bit, there are three sets of end;
eight bits to define colours even end;

more accurately. So palettes to de-


fine the target colours are mostly
irrelevant: the pixel values them-
selves define the colours. ➤ Listing 5

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);


MessageDlg Surprises begin
if MessageDlg(‘Save changes before leaving?’,

Q I regularly use MessageDlg


with OK and Cancel buttons.
Recently I used one with a Yes and a
mtInformation, [mbYes, mbNo], 0) = mrNo then
Exit;
{ Do saving ... }
end;

56 The Delphi Magazine Issue 24


question being asked but allows property to True when working To make the application start off
the execution flow to continue un- with Oracle and Informix. This is a minimised, you set the main form’s
molested, whereas Cancel stops 32-bit BDE setting which defaults WindowState property to wsMini-
the current execution flow. See to False. mized. This causes a Delphi 1 app to
Listing 7 for an example of this. Luckily there are two relatively start life with the main form
painless workarounds. One would iconised on the task bar or desk-
Localised be to set ENABLE BCD back to False. top. In the case of a Delphi 3 app, it
Database Headaches The other involves issuing this is the Application window that
query: starts off minimised on the desk-

Q I have a problem concerning


Personal Oracle 7.3 on Win-
dows 95 and using BDE 3.5. If I try to
ALTER SESSION
SET NLS_NUMERIC_CHARACTERS = ’.,’
top. That is more like what would
be anticipated by someone who
knows about the innards of the
write data into an integer field in VCL since when any Delphi appli-
my database and have a German after the database has been cation is running, minimising the
version of Personal Oracle in- opened but before you open any of main form causes the Application
stalled I get an exception saying the datasets. You need to ensure window to be iconised on the task
that the number format is not valid. the alias’s SQLPASSTHRU MODE is set to bar, not the main form’s window.
Dates and strings work without either SHARED AUTOCOMMIT or SHARED When a Delphi 2 app starts with its
problem. Also the English version NOAUTOCOMMIT so that the datasets main form set to minimise it gets
of Personal Oracle does not exhibit will actually benefit from this things confused and leaves the
this problem. I tried all kinds of dif- query statement. main form looking like a minimised
ferent BDE settings to no avail. MDI child above the task bar. The
What must I do to fix the problem? Iconic Applications fix for this was discussed in The
Delphi Clinic in Issue 9. When the

A This is quite a well known


problem for anybody who
uses a comma as a decimal separa-
Q I am trying to start up an ap-
plication with it always mini-
mised on the task bar (Win95 or NT
fix is applied, Delphi 2 and 3 act
alike.
So, a Delphi 1 app needs to pre-
tor as opposed to a period (or full 4) or minimised on the desktop vent the main form being restored
stop). The BDE Configuration (WinNT 3.51 or Win 3.1x). I am un- and Delphi 2 and 3 apps need to
app’s Number page has a likely look- able to come up with a satisfactory prevent the Application window
ing setting called DECIMALSEPARATOR, solution. being restored. Of course what
but unfortunately it is ignored by normally happens when the Appli-
Delphi. In fact all the settings on
the last three pages are ignored by
Delphi: they are principally meant
A In theory this is easy. When
someone tries to restore
your iconised form, Windows
cation window is restored is that it
restores all the other windows (in-
cluding the minimised main form)
to be used for QBE execution and sends it a message. If the form re- so 32-bit apps still really need to
since Delphi does not inherently turns zero, the form doesn’t re- prevent the main form being re-
support QBE files, the settings are store. That should be it. However, stored. This business with the
not used. things are often not that clear cut main form is easily dealt with using
The problem occurs primarily when talking to Delphi forms at the a message handling method to re-
when you set the ENABLE BCD API level. spond to wm_QueryOpen messages.
To make the application’s sys-
tem menu look sensible, during ap-
➤ Listing 6
plication startup we can delete the
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean); inappropriate menu items such as
begin Move, Size, Restore, Minimize and
if MessageDlg(‘Save changes before leaving?’,
mtInformation, [mbYes, mbNo], 0) <> mrYes then Maximize, leaving the only relevant
Exit; one: Close. Of course in Delphi 1
{ Do saving ... }
end;
this is the main form’s system
menu.
In Delphi 2 and 3 it will be the Ap-
plication’s. This can also be done
➤ Listing 7
in a message handler. Windows
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
sends a wm_NCCreate message when
begin it is manufacturing the non-client
case MessageDlg(‘Save changes before leaving?’, area of the form (including borders
mtInformation, mbYesNoCancel, 0) of
{ Save data if user wishes } and system menu). We can trap
mrYes: { Do saving ... }; this message and follow this de-
{ Abandon form closure }
mrCancel: CanClose := False;
fault functionality with the appro-
end; priate API calls to tidy up the
end; system menu.

August 1997 The Delphi Magazine 57


The IconStay.Dpr project on the disk implements all
➤ Listing 8
these requirements using conditional compilation to
work in all three versions of the product. type
Listing 8 shows the main form unit, although it omits TForm1 = class(TForm)
the code required to fix the Delphi 3 bug. private
procedure WMQueryOpen(var Msg: TWMQueryOpen);
message wm_QueryOpen;
Acknowledgements procedure WMNCCreate(var Msg: TWMNCCreate);
message wm_NCCreate;
Thanks to Steve Axtell and Mike Scott for help with end;
parts of this month’s column. ...
procedure TForm1.WMQueryOpen(var Msg: TWMQueryOpen);
begin
On our Web site: Msg.Result := 0;
end;

http://www.itecuk.com procedure TForm1.WMNCCreate(var Msg: TWMNCCreate);


var SysMenu: HMenu;
Don’t forget to visit our Web site regularly to keep begin
up to date. Here’s some of what you can find: inherited;
{ Delphi 1 generally shows the Application window
➤ Updated program and data files for TDMAid, when iconised. However if the app starts with the
main form minimised then the icon is that of the
the Article Index Database. main form. It seems that Delphi 2 and 3+ make
things more consistent by using the Application
➤ TDMaid Online for immediate access! window regardless. Therefore, some conditional
➤ The Delphi Magazine Book Review Database. compilation is required }
SysMenu := GetSystemMenu(
➤ Is your companion disk dead? The source and {$ifndef Windows}
Application.{$endif}Handle, False);
example files from the articles for the last few { Get rid of irrelevant system menu entries }
issues are here for download.* DeleteMenu(SysMenu, sc_Size, mf_ByCommand);
DeleteMenu(SysMenu, sc_Move, mf_ByCommand);
➤ Details of what’s in the next issue. DeleteMenu(SysMenu, sc_Minimize, mf_ByCommand);
DeleteMenu(SysMenu, sc_Maximize, mf_ByCommand);
➤ Back issues: contents and availability. DeleteMenu(SysMenu, sc_Restore, mf_ByCommand);
{ Get rid of the separator item that remains }
➤ Sample articles from back issues. DeleteMenu(SysMenu, 0, mf_ByPosition);
{ Refresh the system menu }
➤ Links to other great Delphi sites. DrawMenuBar(Handle);
end;
* Do also contact us so we can send you a new disk.

58 The Delphi Magazine Issue 24


above technique let me exorcise the component and
restore things to normal. In this case, Delphi wouldn’t
let me save the modified DFM until I’d also loaded the
PAS file for a supporting unit, a datamodule unit.

Contributed by Brandon Smith, [email protected]

BDE Version Incompatibilities


It appears that there is at least one incompatibility be-
tween versions 3 and 4 of the Borland Database Engine
Delphi 2/3 Typed TObject Bug (BDE) that will break programs which create tables on
I was notified of this as a Delphi 2 bug by a friend. Using the fly. It seems that BDE 4 does not allow dBase tables
to have more than one primary index. What I’d had
type before was along these lines:
TInternalError = type TObject;
with IndexDefs do begin {set up table, define fields}
he got an internal error URW337. I get the same error in clear;
Delphi 2. I made the following form to test it in Delphi 3: add(‘ID’,’ID’, [ixUnique, ixPrimary]);
add(‘SET’, ‘SET’, [ixPrimary]);
unit TypeBugU; end;
interface
uses What I had to do to make my program work was set the
Windows, Messages, SysUtils, Classes, Graphics, index options of additional indexes to nil:
Controls, Forms, Dialogs;
type with IndexDefs do begin
TForm1 = class(TForm) clear;
end; add(‘ID’,’ID’, [ixUnique, ixPrimary]);
{ this is the only line I added } add(‘SET’, ‘SET’, []);
TInternalError = type TObject; end;
var Form1: TForm1;
implementation This incompatibility will cause production programs
{$R *.DFM} compiled under Delphi2 to throw invalid index descrip-
end. tor errors if you try to run them with BDE 4 loaded. I
don’t really miss not putting ixPrimary in for the secon-
The first attempt to compile this gives: Fatal Error: (0): dary indexes, but if I remember correctly, I couldn’t
Internal error: L1086. On subsequent compilations I get: compile it under Delphi 2 without specifying ixPrimary.
Fatal Erorr: ...: Unit SysUtils was compiled with a different In any case, if your Delphi 2 code creates tables on the
version of System. I have to close down the project to re- fly and uses secondary indexes as shown above, you’ll
set Delphi so that it no longer belives I have a modified have to recompile it to test for the BDE version and take
version of System. appropriate action.

Contributed by Hallvard Vassbotn, Contributed by Brandon Smith, [email protected]


[email protected]
More Delimiter Tools
Modifying Form Files While reading Dave Jewell’s article about the delimited
Once in a while you will find yourself having dropped a string class in Issue 23, I thought I’d share how I store
component on one of your forms which manages to Window size and position information in INI files or
mess up everything so bad you can’t even load your Registry entries.
project anymore. Double clicking on a *.DFM file in the First I’ll illustrate the final notation, then I’ll go into
explorer brings up Delphi with the form in the text edi- the functions used to support it. To store a TRec in a
tor. You can modify this file and save it to make TRegistry entry:
changes: most often to remove an offending compo-
nent. Do pay attention, however, as one could easily writeSTring(fWinName,
make a royal mess of things. DblQuoteStr(TRectToCommaDelimStr(fFormRect)));
This happened to me recently with the promotional
copy of xxxxx [Just to avoid the attentions of any lawyers and in an INI file:
out there! Editor]. I dropped a component on a form and
upon compilation, presto! a modal dialog box with an writeString(SectionName, fWinName,
inactive OK button and no way to exit except CTL-ALT- DblQuoteStr(TRectToCommaDelimStr(fFormRect)));
DEL! And worse, the same thing happened when trying
to reload the project or the *.PAS file by itself. The To retrieve the TRect from a TRegistry entry:

60 The Delphi Magazine Issue 24


try Some of the functions used to support this conven-
fFormRect := ient notation date back to Turbo Pascal 3 days. Though
CommaDelimStrToTrect(readString(fWinName)); I’ve done some updating, I haven’t beefed them up to
except handle the length that a Delphi 2 or 3 string could have.
fFormrect := CommaDelimStrToTrect(cEmptyRect) I suppose I should be setting these up as classes to be
end; in the true spirit of Object Pascal, but it seems to me
that there is still a place for plain old functions.
and from an INI file: The first function is used to build a comma delimited
string out of a TRect. The only caution for using some-
fFormRect := CommaDelimStrToTrect(readString( thing like this is that the programmer needs to be
SectionName, fWinName, cEmptyRect)); aware of where the data in the TRect came from. If it
came from ClientRect, for example, then r.right is the
The constant cEmptyRect is set to 0,0,0,0 and is used to width, not the right edge. See Listing 1.
make sure I’ve got something in the TRect when I re- I found I had to wrap TRectToCommaDelimStr in
trieve from storage that might not exist. Since a real DblQuoteStr in order to get it written out to INI files cor-
window will always have something other than zero for rectly. However, ReadString from an INI file ignores the
right or bottom, I look at that to find out whether or not double quotes, whereas ReadString from TRegistry
I got a valid value from the registry or INI file. brings in the double quotes. Rather than having differ-
ent ways of writing to INI files and the registry, I modi-
fied the converse function to take that into account. Do
➤ Listing 1
I hear someone say why not modify TRectToCommaDe-
function TrectToCommaDelimStr(const r: trect): string; limStr? Since I already had DblQuoteStr in my function
begin library (for what reason I can’t recall), I follow the rule I
result := IntToStr(r.left)+’,’+IntToSTr(r.top)+’,’+
IntToStr(r.right)+’,’+IntToStr(r.bottom); learned when I thought Charles Moore’s FORTH was
end;
function DblQuoteStr(const s : string): string;
the best of all possible languages: if you find yourself
begin coding the same sequence more than
result := ‘"’+s+’"’
end; twice, you need to turn it into a function or proce-
dure. The downside of that practice is a whole slew of
little functions and some rather bulky function units.
On the other hand, Delphi is pretty good about ignoring
➤ Listing 2
the ones you don’t use and letting you give them mean-
function CommaDelimStrToTrect(const s: string): trect; ingful names. See Listing 2.
var
tmp, wrkstr : string; The symbol stripping function has seen a lot of use in
begin my work. One of these days I’ll set up some conditional
if pos(‘"’, s) <> 0 then
{strip the double quotes, if present} directives in here to make sure I don’t truncate a Delphi
tmp := stripsymbol(s, ‘"’)
else
tmp := s;
wrkstr := get_Word_UpTo(tmp, ‘,’); ➤ Listing 4
result.left := strToInt(wrkstr);
wrkstr := get_nth_word(tmp, 2, [‘,’]);
result.top := StrToInt(wrkstr); TYPE
wrkstr := get_nth_word(tmp, 3, [‘,’]); charset = Set of Char;
result.right := StrToInt(wrkstr); FUNCTION get_nth_word(wrkstr : STRING; nth_word : BYTE;
wrkstr := get_nth_word(tmp, 4, [‘,’]); Delim : charset) : STRING;
result.bottom := strToInt(wrkstr); VAR
end; i, ndx, start_pt, end_pt : BYTE;
function StripSymbol(const s, sym: string): string; found : BOOLEAN;
{takes out any occurrences of symbol} BEGIN
begin start_pt := 1;
result := s; end_pt := length(wrkstr);
while pos(sym,result) <> 0 do IF LENGTH(wrkstr) = 0 THEN BEGIN
result := copy(result, 1, pos(sym,result)-1) + result := ‘’; { whoops, empty string}
copy(result, pos(sym, result)+1, 255); EXIT;
end; END;
ndx := 1;
FOR i := 1 TO nth_word DO BEGIN
found := FALSE;
REPEAT { find first delimiter }
IF wrkstr[ndx] in delim THEN
➤ Listing 3 INC(ndx)
ELSE BEGIN
start_pt := ndx;
FUNCTION get_word_upto(wrkstr : STRING; delim : CHAR) : found := TRUE;
STRING; END;
VAR UNTIL found;
i : INTEGER; found := FALSE;
found : BOOLEAN; REPEAT { find end of this word }
BEGIN IF (ndx > LENGTH(wrkstr)) OR (wrkstr[ndx] in delim)
found := FALSE; THEN BEGIN
i := 1; end_pt := ndx - 1;
REPEAT found := TRUE;
INC(i); END ELSE
IF wrkstr[i] = delim THEN found := TRUE; INC(ndx);
IF i > LENGTH(wrkstr) THEN found := TRUE; UNTIL found;
UNTIL found; END;
result := COPY(wrkstr, 1, i - 1); result := COPY(wrkstr, start_pt, end_pt - start_pt + 1);
END; END;

62 The Delphi Magazine Issue 24


2 or 3 string. I’ve often briefly considered doing this with files with 2 or even 1 character extensions. The
function and the next two in assembler, but I hardly see code in Listing 5 is how we deal with this case (in addi-
the point given the speed of Delphi and most machines tion I’ve included a snippet of code which we use to
it runs on. Besides, what if one of these days Borland change a file’s extension).
makes Delphi cross platform? In any case, I certainly
make no claims that these are the most elegant ways to Dr Pete Frizzell, [email protected]
do the job. Suggestions for improvements are always
welcome.
➤ Listing 5
The get_word_upto function, Listing 3, is only useful
when you know you want a substring of unknown function ReturnJustFileName( const s : string ) : string;
var
length from the beginning of a string, a fairly frequent sExtension : string;
occurrence. I could have just as easily used the Next sFullFileName : string;
begin
function, but since I do have a modicum of concern sFullFileName := ExtractFileName(s);
sExtension := ExtractFileExt(sFullFileName);
about performance, this one is a better choice where I Result := Copy(sFullFileName, 1,
Length(sFullFileName)-Length(sExtension) );
used it. end;
In REXX the function WORD(string,n) returns the nth function ReplaceFileExt( const sFullFileName : string;
const NewExt : string ):string;
symbol in the string. In REXX, any non-alphanumeric is var sFileExt : string;
considered a delimiter. The Pascal version in Listing 4 begin
sFileExt := ExtractFileExt(sFullFileName);
requires the programmer specify the delimiters as a Result := Copy(sFullFileName, 1, Length(sFullFileName) -
Length(sFileExt) ) + ‘.’ + NewExt;
set. This version also assumes a short string that end;
doesn’t have more than 255 words in it, though that’d
be easy enough to change.

Brandon Smith, [email protected] A New Hand At The Helm...


I’m delighted to report that from now on
Just The Extension Please Mike Orriss will be editing the Tips & Tricks
Issue 21 contains a tip from Tom Corcoran which pro- column, in addition to his sterling work on
vides code to extract a filename without an extension. collating and testing our disk material. So,
This works fine so long as you can assume that the ex- please send all your Tips direct to Mike by
tension is 3 characters long (4 counting the period). email at [email protected]
Unfortunately for some of our applications we deal

August 1997 The Delphi Magazine 63


On The Disk...
A run-down of what’s included on the free disk with this issue
Collated and tested by Mike Orriss

➤ RTREG32
The RT Registration Control is a li-
brary unit (DCU for Delphi, other-
wise DLL) containing functions to
protect shareware and other soft-
ware against unauthorised use:
files are unlocked with a unique
registration key. You can save en-
crypted information about users
somewhere in the system and a
count and/or date lock in order to
make an evaluation copy. This
means shareware can be marketed
electronically through the internet
because the same shareware ver-
sion can be turned into a registered
version. Another highlight is a sup-
port for leased/rented software
through linking the registration
key with the next payment date.

➤ ORANET23
This contains two documents put
together by George Pujol as a guide
➤ Some of the options available
to BDE/Oracle connectivity. TCP/IP, and Named Pipes proto-
for the Animated Tray Icon
The ORA7316.DOC file describes cols in the Windows NT version. 16
all the necessary steps for connect- and 32-bit ODBC drivers have also
ing to an Oracle Workgroup Server been successfully tested for the
Version 7.1, 7.2, or 7.3 for NetWare Oracle 7.3 version. All of the the
or Windows NT using the Borland information is current as of July
Database Engine and Oracle 1st, 1997.
SQL*Net version 2.1, 2.2, or 2.3 us-
ing 16-bit drivers. ➤ PGTRAY3X
The ORA7332.DOC file contains The Animated TrayIcon VCL is a
similar information but using 32-bit component you can use in Delphi 1,
drivers. This configuration works 2, and 3 as well as C++ Builder 1 to
for the SPX and TCP/IP protocols in add icons to the Windows 95 or NT
the NetWare version and the SPX, 4 system tray.

➤ Left and Right:


ZProfiler in
action

64 The Delphi Magazine Issue 24


shell’s taskbar window is a well-
known example. Though appbars
are usually docked on an edge of
the user’s screen, they can also
float. To make room for an appbar
that’s docked, the Windows 95/NT
shell shrinks the size of the
screen’s work area.

➤ Important Note!
Always, but always make a backup
copy of your Delphi 1 or 2 compo-
nent library file before you take the
plunge and install any new compo-
nent into Delphi! Sometimes com-
ponent installation can trash your
component library and you will
then be left with an inoperative
Delphi will have to re-add all your
extra components.
➤ Some of AppBar’s properties, which make it easy to have exactly
➤ Send In Your Code...
the style you want!
If you have some carefully crafted
routines, why not send them in for
➤ Z_PROF ➤ APPBAR11 evaluation by mail or (zipped up)
ZProfiler is a utility component The TAppBar class is a TForm derived by email to our Disk Editor, Mike
that enables the assessment of exe- class for Delphi 3 that lets your Orriss, at [email protected].
cution times of code fragments form to behave like an Application We do consider all submissions for
with a 0.01 microsecond resolu- Desktop Toolbar(appbar short), use on future disks.
tion. It is based on the CPU clock of which is a window that attaches it-
Pentium computers. self to an edge of your screen. The

The source code for the articles in this issue can be downloaded from our
website at www.itecuk.com. The extra bonus files described here are available
on the disk for this issue when you subscribe.

August 1997 The Delphi Magazine 65


Subscribe To The Delphi Magazine
And You Will:
➤ Get expert help
with your projects Issue
20, Ap
ril 199

Delph
7

from some of the i 3 Dis


cover UK £7

most experienced ed Brian


Client Lo
first of ng brings u

Delphi developers Using /Server Cac


Neil M
cC
File St
databas lements sh
e apps
reams ing
ows ho
h
tw
take an o articles w the
what D in-depth loo hich
elphi 3
has to
s

k at
offer
by cach w

around Revie
Dave
Je
w: As
compo well takes
nents th
ing rare to speed up

ync Pr
ly-upd

from Tu is suite of se
rboPow
ofessi
rial co
sl
ated d uggish
ata

onal
ListBo er out mm
for a te unications
Brian
Lo
x e s Have st drive
ListBox ng reveals Powe
➤ Develop better r!
contro the hid
l den dep
th s of th
e hum
ble
INCLU Abo
DED O Mike O ve: with the

applications more
N THIS rr TB
DISK
ARE R MONTH create iss on this m ag compone
power
EGISTR 'S ful Regi onth's disk yo nts from
COMP
ONEN
TS, A
Y EDIT
ING Beatin stry m
anipul u too can
BUILD
ER, M
N SQL
QUER
Dave g The ation to
EYES
OUSE
, PLU
S MO
-CHASI
N G
Y
Je
active well shows
System ols
RE... button h
s to in ow to create

quickly using the


clude o
Under n your nifty Explore
Bob S Const CoolBar
s
r-style
FULL D wart ex ructio
CONTE K
IS
NTS
in you
r applic plains how
ations to
n
and co implement
PAGE
64
Surviv mponen multi-th
ing Cl
best technical
ts readin
g
Steve
loads o
Troxell ient /Serve
f user- has all the in r
defined format
functio ion
ns to yo you need to
ur Inte ad
rbase ap d

material available
ps

The Delphi Magazine is produced by Delphi developers for


Delphi developers. Delphi is the best Windows development
environment, our aim is to help you make the most of it:
➤ Published monthly, each issue includes regular columns on building
components and experts, client/server development, system-level issues,
with The Delphi Clinic to solve your problems and Tips & Tricks too.
➤ Plus there are in-depth feature articles. Topics we've covered so far
include: performance optimisation, resource files, callbacks, file handling,
screen capture, custom database logins, typecasting, moving from other
languages, custom clipboard formats, display updating and drag & drop.
➤ And in case that isn't enough there's product and book reviews and
news. With each issue we include a free disk containing all the source
and example files plus shareware/freeware components and tools as well.

There's more information and a subscription form overleaf


Call today on +44 (0)181 249 0354 or fax us on +44 (0)181 249 0376
For more information visit our web site at http://www.itecuk.com
Surviving
The Delphi
Edited by Brian Long Surviving Client/Server:
Problems with your Delphi project?
Client/Server Getting Started With SQL Part 1
CLINIC Just email Brian Long, our Delphi Clinic
Editor, on [email protected]
or write/fax us at The Delphi Magazine

Written by Steve
by Steve Troxell

Service Not Good Enough Combo Woes OnClick handler with a new D elphi incorporates some very
useful database components
make sure you’ve selected Local
Server, enter the path and filename
databases. A database is a
collection of tables; in Paradox

Q The Delphi online help and


documentation on the
subject of DDE says that a Delphi
Q The TDBLookupCombo.OnClick
event doesn’t fire if I click on
the drop down list. However
method, saving the old one first.
The new handler calls the old one,
then calls the previously uncalled
OnClick handler.
The Delphi Clinic Troxell, this column that make developing database
applications a breeze. And because
Delphi ships with a local version of
Borland’s InterBase database
for the EMPLOYEE.GDB database,
enter SYSDBA for the user name, and
enter masterkey for the password
(make sure you enter the password
each .DB file represents a table, in
InterBase each .GDB file represents
a database and the tables are
managed internally.
DDE server application’s service does work in this way for server, a great number of applica- in lower case). This is the default Second, let’s take a look at the

Edited by author,
OnClick

provides insights and


name, or application name, will the other combo box components. Microsoft Products tion developers now have an easy, system administrator login for syntax of a SQL statement. A
match that of the project without What’s wrong with it? And Floating Point inexpensive means of exploring the InterBase databases. typical SQL statement might be:
the extension. Can this be altered world of Client/Server technology Using the ISQL program is
(ReportSmith does not conform to
this rule)? A
The TDBLookupCombo is not a
real combo box, but is made Q
I have a DLL written in a
Microsoft C compiler which
right at their fingertips. For these
reasons, many fledgling Delphi
simple: type in the SQL statement
you want to execute in the SQL
SELECT name, address
FROM customers;

A Well, ReportSmith is not


written in Delphi (perhaps it
wouldn’t be so big and slow if it
up from, among other things, an
edit box and a list box. This is also
why a TDBLookupCombo on a form
with a FormStyle of fsStayOnTop
exports a function returning a
Double. When I try and call it, I don’t
get the values I expect. Is there a
compatibility problem between
consultant and practical help on your
programmers may be introduced
to Structured Query Language
(SQL) for the first time. This article
is intended to introduce you to the
Statement window and click the Run
button to execute the statement.
The results of your statement will
appear in the ISQL Output window.
SQL itself is case-insensitive, but in
this article SQL keywords are
shown in all uppercase and table
were), but that’s not the point. doesn’t show its drop down part at Borland and Microsoft products? principal SQL data access Once you run an SQL statement, it names, column names, etc. are
Indeed this rule can be altered. The all: the list box used by the VCL Finally, you can add functions to open and close the

trainer Brian Long, Client/Server projects.


statements: SELECT, INSERT, disappears from the SQL Statement shown in all lower case. SQL gener-

T&ipsTricks
DDE components make use of a
component called DDEMgr, of the un-
documented component type
stays behind the stay-on-top form.
Although the list box part of this
fake combo does have an OnClick shows
tables
That’s
orproblem
latesup
A by scanning
exactly through
Close method
thewith
is, and
what the
accordingly.
tablesfunctions
the array, calling the Open
it usually This method encapsu-
access, providing
that access to any unit
UPDATE and DELETE. These are the
workhorses of SQL and by the end
of this article you’ll be able to
window. If you want to run it again
(and perhaps make small changes
to it), you can retrieve any
ally requires a semi-colon at the
end of each statement, but in
certain tools (such as ISQL) it’s
TDDEMgr, which has a property event handler, it is only used to which
return uses the
floating Table
point unit and
values. Forallowing easy access to effectively use SQL to accomplish a previous SQL statement by clicking optional.
called AppName, used to define the
DDE service. Fortunately, DDEMgr is
declared in the interface section of
the DDEMan unit, so something like:
determine when to hide the
list box.
We can improve the situation by
extending the functionality of this point
the tables.
example,
will of
havescanning
So, there’s
in a Delphi
perform
The
a Visual finalapplication
Basic
troublethrough
DLL tonoreturn
part of Listing 1 shows an example

need for
table.
getting a function

actions on referred
value (hereafter
long table names and your can
a floating
all your tables
to with relative ease.
the Clinic answers Topics have included
wide variety of tasks. We’ll
continue to expand our knowledge
of SQL in the next issue.
All of the examples in this article
the Previous button.

SQL Preliminaries
Before we get started, let’s look at
Third, you can break an SQL
statement across multiple lines by
pressing RETURN anywhere you can
legally place a space in the
listbox’s OnClick event and causing as a float for brevity), as will an use the sample InterBase database a few of the ground rules for work- statement.
DDEMgr.AppName :=
’NewDDEServiceName’;Control Arrays

in your server form’swhen


One of the (very)

handler should do the trick.


it to also call the TDBComboBox’s On-

using Delphi
OnCreate
In Delphi
Click in a new component. Listing
few features
1 shows I miss
how. The
is the ability
TNewDBLookup
Arrays. The following techniquelist
own component
from Visualfor
constructor
to handle
searches throughControl
is until
a simple
its
solution
it finds a
Excel or Microsoft C application.

Basic options
Contributed
Regardless
(CompuServe

256
floats onColour
of by

the NDP
whatMark
are used,100627,422)
Mamone of Newcastle, UK
compiler
Borland prod-
ucts cause functions to return
Bitmaps
(Numeric Data
readers' “How do an introduction to
EMPLOYEE.GDB that ships with
Delphi. You may find it helpful to
connect to this database through
the Windows Interactive SQL
(ISQL) program that ships with
ing with SQL. First, three new
terms: the familiar structures of
file, record, and field are called
table, row, and column in relational
Finally, you can enclose literal
strings with single quotes or
double quotes. We’ll use single
quotes here.

I...” and “Why SQL, choosing TTable


and makes yourTPopupGrid
code easier(ato class
use. based on a In Issueor280x87
Processor, of The Delphi Magazine, Stefan Boether
co-processor) Delphi and try the examples as you
➤ Listing 1
Say your program needs a number
TDBLookupList ), thenofreplaces
tables open,
access through a variety of units. How can you achieve
its for stack.showed how to read a bitmap in a resource into a
TBitmap, using LoadBitmap. However, this is only valid
read about them. It’s also handy to Introducing The Column...
be able to experiment as ideas
unit Newcomb;
interface
this easily and efficiently? if Components[Loop] isfor
Create a unit called Tables, for want of awith Components[Loop]
better name,
16 colour
TPopupGrid
as TPopupGrid
frequently
thenbitmaps. Questions appear reasonably
do begin
on CompuServe about how to do it for 256
come to you while reading the text.
This database is located in the D elphi users seem to have settled down into several groups: firstly
occasional programmers or first-time programmers ( attracted by

or TQuery, plus stored


uses FOldGridClick := OnClick;

won't it” queries


and define
SysUtils, WinTypes, WinProcs, an array
Messages, of TTable pointers for
Classes, however
OnClick colours. The problem is the palette. LoadBitmap will
:= NewGridClick; \IBLOCAL\EXAMPLES directory if Delphi’s ease of use), secondly professional full-time developers doing
manyDialogs,
Graphics, Controls, Forms, tables you haveDBLookup;
StdCtrls, defined. As shown in Listing 1,
Break; work for a 256 colour image, but it will ignore the you installed Delphi with the general Windows application building (enthralled by Delphi’s amazing
type end;
firstly define constants for the totalend;
TNewDBLookup = class(TDBLookupCombo)
number of tables palette that has been stored with the image. default directories. We will be productivity) and thirdly those putting together Client/Server systems
private and one for each table (better than procedure
a number) and the The code in Listing
TNewDBLookup.NewGridClick(Sender: TObject);2 copies the bitmap from the adding information in this (impressed by Delphi’s robustness and power). This column is aimed
FOldGridClick: TNotifyEvent;
type holding pointers to each table.var FOnClick: TNotifyEvent; resource, using various Windows API routines, into a at the third group, but especially those who may be dipping a toe into
database so you may want to make

procedures and triggers.


protected begin

every month.
Now, weTObject);
procedure NewGridClick(Sender: define a procedure to set up the pointers, memory stream and then loads the contents of the
if Assigned(FOldGridClick) then a copy of the EMPLOYEE.GDB file the waters of Client/Server for the first time.
public pointing to the table controls on the FOldGridClick(Self);
form – see the stream into the bitmap. The only clever part is noticing and work with the copy only. Steve is involved in developing a variety of Client/Server systems in
constructor Create(AOwner: TComponent); override; FOnClick := OnClick;
end; procedure TfrmTables.InitialiseTables in Listing 1. that a bitmap resource is the same as a bitmap file, but his work at TurboPower Software, using different server databases, and
if Assigned(FOnClick) then
procedure Register; FOnClick(Self); without a TBitmapFileHeader record at the front. To Using ISQL we are looking forward to learning from his experience over the months.
implementation end; allow the stream read to work, we need to put a fake To use Windows ISQL, start the As well as SQL – an essential part of the Client/Server developer’s skill
➤ Listing 1 TComponent);
constructor TNewDBLookup.Create(AOwner: procedure Register;
var Loop: Word;
TBitmapFileHeader record at the front of the stream. ISQL program from the Delphi set – Steve plans to cover a variety of other topics and also provide lots
begin
begin
const
RegisterComponents(’Samples’, [TNewDBLookup]); program group. From the File of hints and helps along the way. If there are things which you need
inherited Create(AOwner); end;
MAX_TABLES = do
for Loop := 0 to Pred(ComponentCount) 4; {Number of Tables} menu, select Connect to Database. some help with, why not drop us a line and let us know!
end.
{Constants for the array of tables} Contributed by Brian Long (CompuServe 76004,3437) In the Database Connect dialog box,
TABLE_BACKUP = 1;
TABLE_RECOVERY = 2;
TABLE_REUSE = 3;
56 TABLE_SUMMARY = 4; The Delphi Magazine Issue 6 12 The Delphi Magazine Issue 3
type ➤ Listing 2
{for handling tables without having controls
on-screen}
TableData = Array[1..MAX_TABLES] of TTable; type
var TResBitmap = class(TBitmap)
Data : TableData; public
procedure TfrmTables.InitialiseTables; constructor Create(ID: PChar);
var end;
ptr : Integer; constructor TResBitmap.Create(ID: PChar);
begin var
{Setup the Table Pointers ready for use} HResInfo: THandle;
for ptr := 1 to MAX_TABLES do begin
{Point to the component on the form}
case ptr of
TABLE_BACKUP : Data[ptr] := tblBACKUP;
BMF: TBitmapFileHeader;
MemHandle: THandle;
Stream: TMemoryStream;
ResPtr: PByte;
Under Construction:
TABLE_RECOVERY : Data[ptr] := tblRECOVERY;

Property Editors
ResSize: Longint;
TABLE_REUSE : Data[ptr] := tblREUSE;

Under Construction
TABLE_SUMMARY : Data[ptr] := tblSUMMARY; begin
end; inherited Create;
end; BMF.bfType := $4D42;
end; HResInfo := FindResource(HInstance, ID, RT_Bitmap); by Bob Swart
{ ...add code to open/close tables as required... } ResSize := SizeofResource(HInstance, HResInfo);
MemHandle := LoadResource(HInstance, HResInfo);
{ scanning through a table... } ResPtr := LockResource(MemHandle);
D elphi offers a Tools API, There’s actually much more to property editors already exist in

Bob Swart, known to many as “Dr.Bob”, writes


begin
with Data[TABLE_BACKUP] do begin
Stream := TMemoryStream.Create; which allows us to extend the property editors than meets the Delphi. To do that, start a new
Stream.SetSize(ResSize + SizeOf(BMF));
First; Stream.Write(BMF, SizeOf(BMF)); functionality of the Delphi IDE eye and we’ve only scratched the project, add uses DsgnIntf; to the
repeat Stream.Write(ResPtr^, ResSize); itself. There are four different surface. Let’s look deeper and see implementation section, compile,
{ Do something, assuming there is at least one FreeResource(MemHandle);
record in the table} Stream.Seek(0, 0);
Tools API interfaces: for Experts if we can do even more. To do that, open the browser and search for
Next; LoadFromStream(Stream); (see Issue 3), Version Control we need to check out the one Tools TPropertyEditor – see Figure 2.
until Data[TABLE_BACKUP].Eof;

this monthly column on how to build your own


Stream.Free; Systems, Component Editors and API source file which defines the
end; end;
end. Property Editors. They offer us behaviour of the property editors
➤ Figure 1
the functionality we need to add for which you do need to write
new IDE features or enhance the code: DSGNINTF.PAS (in directory
existing ones! \DELPHI\SOURCE\VCL). This file
58 The Delphi Magazine Issue 5

Delphi components and experts, providing


contains not only the definition for
Property Editors the base class TPropertyEditor, but
Property editors are, like Experts also numerous derived property
and Version Control Systems, in editors for the properties of the
this sense extensions of the Delphi VCL components of Delphi itself.
IDE. That may sound very difficult Most of these property editors are
or complex, but is in fact very easy.
You can even write a property
editor without knowing it – for
enumerated types, for example.
available for our use as well, like
the TEnumProperty we used for the
TRang enumerated type in the first
example.
practical insights from his own development
Remember the color property of a

Tips & Tricks


TForm? When you want to enter a
value, you get a drop-down list
showing all possible choices.
That’s an enumerated type prop-
Existing Property Editors
Before we actually take a look at a
property editor from the inside,
let’s first examine what kinds of
experience into what many have found to be
an extraordinarily productive area of Delphi
erty editor, a very easy one, and we
can make one with only a few lines
➤ Listing 1

Readers' hints on getting


of code.
As an example, I’ve picked the Type
Starfleet rank overview (as a TRang = (cadet, midshipman, chief, ensign,
confirmed Trekkie what do you junior_lieutenant, lieutenant,

development. Topics covered so far include


lieutenant_commander, commander,
expect!). Or, in Object Pascal, the captain, commodore, admiral);

the best out of Delphi, enumerated type which is shown in


Listing 1.
In this case, like all other enu-
merated types, we must make sure
➤ Listing 2

from Windows API insights


that the user cannot make a mis-
take, by typing “commonder”
instead of “commander” for exam-
ple. We need to offer a list of limited
unit Officer;
interface
uses Classes;
Type
TRang = (cadet, midshipman, chief, ensign, junior_lieutenant, lieutenant,
component building basics, encapsulating DLLs,
choices: a drop-down list. Well, lieutenant_commander, commander, captain, commodore, admiral);

extending components, adding built-in help,


TOfficer = class(TComponent)
that’s exactly what the user gets

to customising the VCL.


private
when s/he drops the TOfficer FRang: TRang;
component shown in Listing 2 onto published
property Rang: TRang read FRang write FRang;
a form (see Figure 1). end;
So, we haven’t done anything procedure Register;

custom events, property editors and building


implementation
special but our first personal procedure Register;
property editor is up and running! begin
RegisterComponents(’Dr.Bob’, [TOfficer]);
And who knows, it might even be end;
your second or third, now that you end.
think about it...

February 1996 The Delphi Magazine 17 data-aware components.

Beating the System:


Explorer-Style Active Buttons
by Dave Jewell

Beating The System


L ast month I promised

scribe the development of an

Internet Explorer

control. I designed this button to


style
to de-

button

Dave Jewell helps make


Plus there's much more...
Of course there are also in-depth feature articles,
complement the Coolbar code that

I developed a couple of months

back. Since then, I’ve taken the

plunge and installed Office97 and

it’s now obvious that Microsoft are


sense of the system-level
reviews of Delphi add-ons and books, plus an
using the same combination of

Coolbars and active button con-

trols throughout all their new

Office applications, setting a new

standard for user interface design.


issues which are so
So What’s An Active Button?
When I use the term active button
➤ Figure 1: Here’s a view of the TExplorerButton control being used

update on news in the Delphi world, in every


rarely discussed in
in a demo application: the right-most of the four buttons has an
control or just active button, I’m not
attached popup menu which you can see being used
referring to ActiveX or anything of

issue of The Delphi Magazine. Our team of


that nature. Rather, I’m using my

own home-grown terminology for make this control respond as much colour one. Similarly, when a but-

the way in which Microsoft’s re- like the Microsoft equivalent as ton is disabled, the glyph is drawn

cently developed controls recog-

nise that the mouse is moving over


possible.

Just to show you where we’re


with a greyed-out appearance and

the button’s caption text (if any) is


programming books,
them (without pressing a mouse

button) and change their appear-

ance to indicate that they can be


going, take a look at Figure 1, which

shows a small demo program that

I knocked up. This demo uses the


drawn

greyed-out
in a three-dimensional,

manner.

pointing out that the caption and


It’s worth

authors are some of the most knowledgeable


clicked. This ‘sit up and beg’ behav-

iour is an excellent idea because it


Coolbar code from a

months ago, with one of the Cool-


couple of glyphs are optional: you can have

both, you can have a button with a


with practical advice on
provides

back to
immediate

novice users,
visual feed-

showing
bar bands containing a TPanel com-
ponent. Inside this panel are four of
caption only, or you can just have

the glyph. You can even have a


and experienced Delphi developers around.
topics such as converting
them what parts of a user interface our new Explorer style buttons. button with neither caption nor

will respond to mouse clicks. As with Microsoft’s buttons, glyph, but it doesn’t look terribly

TExplorerButton

Readers from over 40 countries are already


Because of Delphi 3’s ability to can display itself exciting!

convert VCL components into in one of four different ways: active The Explorer button supports a

ActiveX controls, and because of (when the mouse is over the but- number of different event types,

projects to 32-bit, how


the introduction of Microsoft’s ton), inactive (the button’s normal with which you can associate an

Visual Basic 5.0 CCE (Control Crea- ‘idle’ state), disabled, or pressed event handler in the normal Delphi

benefiting from their insights – why not join


tion Edition) development system, down (when the mouse is actually way. In addition to standard events

you can create custom ActiveX clicked over the button). In Figure such as OnClick and OnMouseMove,
controls more easily than ever 1, the fourth button is pressed there are a couple of custom events

OnMouseEnter On-

to identify CPUs and


before. I anticipate an explosion of down and displaying a drop-down defined ( and

new controls over the next twelve menu. MouseExit) which allow you to do

them? Fill in the subscription form below and


to eighteen months and it’s there- As you move the mouse over a something unusual when the

fore critical to make your new con- button, it automatically toggles mouse starts moving over a button

trol as easy to use and intuitive as from the inactive to active state. or leaves a button. These events

drives, disk formatting


possible. Making it ‘active’ goes a This is indicated by drawing a obviously correspond to the time

long way towards achieving this. At raised border around the button when the button flips from its inac-

send it off today, or just telephone/email us.


the same time, it’s important to and changing the displayed glyph tive to active state (and vice versa).

make your new controls consistent (Borland’s term for small bitmaps By utilising these events, you might

with industry leaders such as displayed inside button controls) (for example) change some aspect

and creating/using DLLS.


Microsoft. I’ve endeavoured to from a monochrome bitmap to a of the main window while the

8 The Delphi Magazine Issue 20

FA
XN
Yes! Please enter my subscription to The Delphi Magazine OW
Please complete and fax or post to: The Delphi Magazine, 9a London Road, BROMLEY !
Kent BR1 1BY. Tel: +44 (0)181 249 0354. Fax: +44 (0)181 249 0376. Email: [email protected]
Name (Mr/Ms)
Position Company
Address
Town
County/State Post/Zip code Country
Telephone Fax Email
ddg
Subscription: 12 ISSUES INCLUDING FREE COMPANION DISKS, please tick one:
United Kingdom £75 Europe £80 USA/Canada £90 Rest of the World £95

Please debit my VISA / MasterCard / American Express account Card Number:


Expiry Date: / Cardholder name: Signature:
I enclose a Sterling cheque drawn on a United Kingdom bank, or a Sterling Eurocheque, for £
made payable to iTec (We'll send you a receipt. Sorry, NO purchase orders! Please do not send payment in other currencies)

Please tick here if you do NOT wish to receive information on relevant products and services from other companies

Copyright © iTec 1997


CHECK OUR WEB PAGE AT http://www.itecuk.com All trademarks acknowledged
Subscribing To The Delphi Magazine
From The United States & Canada
If you live in the USA or Canada you can subscribe from that issue, plus extra shareware and freeware
directly with our North American Agent in US components and tools.
dollars. Here’s how. We think you’ll agree that this represents excel-
We believe the best way to stay at the cutting edge lent value for money and we are sure you won’t want
of Delphi development is to read The Delphi to miss a single issue. Your copies will be airmailed
Magazine! You will receive 12 issues a year, with to you direct from the publishers in England, so
in-depth technical articles, regular columns, Tips & you’ll be sure to get them FAST.
Tricks, the Delphi Clinic, news, book and product Simply fill out the subscription form below and
reviews. We aim to make you more productive and mail it as indicated. If you prefer, you can subscribe
help you enjoy your Delphi development even with a credit card by CompuServe™email by send-
more. ing the information on the form in an email message
As well as all this we include a free disk with each to 70602,1215. Or, just call us with your credit card
issue, which contains all the code and example files details on (802) 244 7820.

The Delphi Magazine Subscription Request


Please complete this form and mail to:
The Delphi Magazine (USA), RR1 Box 6020, WATERBURY CENTER, Vermont 05677 AB97
Payment must be included, we’ll send a receipt to you. Sorry, NO purchase orders!
Name (Mr/Ms) ___________________________________________________________________________________
Position ____________________________________ Company __________________________________________
Address_________________________________________________________________________________________
____________________________________________ City/Town _________________________________________
State _______________________________________ Zip ________________________________________________
Tel _________________________________________ Fax________________________________________________
Email_______________________________________

USA & Canada Subscription US$140: 12 issues including FREE disks*


Payment Method (please tick):
Please debit my Visa Mastercard American Express card (tick as appropriate) by US$140.00
Card Number: _______________________________________ Expiration date: Month:_____ Year:_____
Name on Card:____________________________________ Signature:_________________________________

I enclose a check for US$140 payable to ‘The Delphi Magazine (USA)’ Note: please send
Sorry, we can’t accept payment by bank transfer payment only in
US dollars
We will start your subscription with the next issue

Please tick here if you do not wish to receive information on relevant products and services from other companies.

*Note: for subscriptions to other countries please contact the publishers direct:
iTec, 9a London Road, BROMLEY, Kent BR1 1BY, United Kingdom
Tel: +44 (0)181 249 0354 Fax: +44 (0)181 249 0376 Email: [email protected]

You might also like