Delphi Informant Magazine (1995-2001)

Download as pdf or txt
Download as pdf or txt
You are on page 1of 53

July 1997, Volume 3, Number 7

Internet Delphi
Creating an SMTP E-Mail Client Program

Cover Art By: Tom McKeith

ON THE COVER
6 Internet Delphi: Part I — Gregory Lee 38 Sights & Sounds
Adding Internet e-mail capabilities — for automated registration, O p t i m i z i n g G r a p h i c s — Peter Dove and Don Peer
program support, even database reporting — could wake up your Speed is a primary concern in graphics programming, and this ongoing
next ho-hum application. This series of articles tracks the development of example project is no exception. This month, the authors weigh several
a Delphi e-mail program, beginning with Simple Mail Transfer Protocol. optimization methods to increase speed by 50 percent.

FEATURES 43 On the Net


10 Informant Spotlight N e t C h e c k : P a r t I I — John Penman
W h a t ’ s N e w w i t h E x p e r t s ? — Ray Lischner If you develop for the Internet or intranets, you need a network debugging
You might not know that Delphi 3’s Open Tools API sports some tool — and Mr Penman has just the thing. This month, he adds Echo
enhancements. Discover how project creators and module creators, when processing and packet tracing to May’s NetCheck tool.
combined, let you create experts and wizards with ease.
49 At Your Fingertips
19 Delphi at Work D i s p l a y i n g S h o r t e n e d P a t h n a m e s — Robert Vivrette
A u t o m a t e d A c c e s s — Ian Davies Suppose you want to retain the right and left portions of a too-long path-
First Word, then Excel, and now Access. In this third installment, Mr name, and eliminate characters from the middle. The remedies for this
Davies shows how Automation may be ideal for manipulating Access and other puzzlers are at hand this month.
systems that include more than a database.

23 Greater Delphi
I n t e r B a s e I n d e x e s — Bill Todd
InterBase uses indexes more flexibly than do most databases. Learn how
careful index creation can yield the best possible performance. REVIEWS
51 AdHocery for Delphi
27 Columns & Rows Product Review by Bill Todd
T h e P a r a d o x F i l e s : P a r t I V — Dan Ehrmann
Although the Paradox file format has extensive features for validity
checks and referential integrity, Delphi doesn’t support some, but goes
others one better. Here’s how it all shakes out.

32 DBNavigator DEPARTMENTS
C a c h e d U p d a t e s : P a r t I I I — Cary Jensen, Ph.D. 2 Delphi Tools
In previous installments, you learned the advantages of cached updates.
Now Dr Jensen explains the use of two event properties for those times 5 Newslines
when you want complete control. 53 File | New by Richard Wagner

1 July 1997 Delphi Informant


Aurorasoft Releases New Visual Toolbar for Delphi
Delphi Aurorasoft of Danville, CA allows developers to create allows users to drag-and-drop
T O O L S has released Visual Toolbar toolbars for any type of appli- between a customized float-
for Delphi, a component that cation. ing window and a stationary
New Products Visual Toolbar, along with position. It retains size, posi-
and Solutions its Visual Toolbar Editor, tion, and visible state auto-
matically, and users can cus-
tomize its floating toolbar
windows.
Toolbars can also be
developed by creating
up to 10 buttons at
once, dragging and
dropping buttons onto
the toolbar, setting the
toolbar size and position
visually, and setting
other properties through
a design interface.

Price: US$79
Contact: Aurorasoft, P.O. Box
DemoShield Ships ActiveX 104, Danville, CA 94526-0104
DemoShield Corp. has released
Demo-X, an ActiveX version of its Phone: (800) 987-2426 or
DemoShield product. (510) 939-3788
Demo-X contains tools for creating
drag-and-drop interactive buttons, Fax: (510) 939-3779
screen shots, hot spots, and animated E-Mail: [email protected]
transitions — without scripting.
Demo-X is available free from Web Site: http://www.aurora-
http://www.demoshield.com. soft.com

Adapta Software Launches AdaptAccounts 6.0


Adapta Software Inc. of tions with Windows 95 and cations, and include transac-
Victoria, Canada has Windows NT environments tion importing facilities and
shipped AdaptAccounts 6.0, using Delphi. validation of externally
a family of modules that can Adapta’s line of modular loaded data.
be installed together or sepa- applications includes
rately. This version com- System Manager, General
bines Adapta’s integrated Ledger and Financial Price: The General Accounting pack
accounting database applica- Reporter, Accounts (includes System Manager, General
Receivable, Accounts Ledger and Financial Reporter, Accounts
Payable, Inventory, Sales, Receivable, and Accounts Payable)
Purchasing, Job ranges from US$1,495 to US$4,195.
Costing, Bill of The Accounting/Distribution pack
Materials, and (includes the Inventory, Sales,
Payroll. Purchasing, and general accounting
All modules modules) ranges from US$2,695 to
are available US$7,695. Typical individual module
in multi-user prices range from US$495 to
or single-user US$1,295. Existing users of
versions, AdaptAccounts 5.0 or 5.7 can upgrade
with source for 40 percent plus US$75 per module.
code avail- Contact: Adapta Software Inc.,
able. Adapt- 4608 Cliffwood Place, Victoria, BC,
Accounts’ Canada V8Y 1B5
modules can Phone: (250) 658-8484
also be inte- Fax: (250) 658-2108
grated with E-Mail: [email protected]
other appli- Web Site: http://www.adapta.com

2 July 1997 Delphi Informant


Amzi! Releases Intelligent Components
Delphi Amzi! Inc. of Stow, MA
T O O L S has released Amzi! Prolog +
Logic Server 4.0, a C++
New Products application that integrates
and Solutions Prolog’s catch/throw mech-
anism to the catch/throw
mechanisms in Delphi,
C++, and Java. Because it’s
designed as a C++ applica-
tion, it allows other pro-
grams to create multiple
instances of the Logic
Server running in the same
or separate threads.
Amzi! Prolog + Logic Server
allows the execution of mul-
tiple Prolog engines for
applications such as Web
server-side applications, tele-
phony systems, and Java-
based and database server
components. It also supports
Unicode character sets, users to write server-side and servers for e-mail, FTP,
allowing the creation of applications for the Web, news, HTML, and more.
Prolog components that can making it possible to Version 4.0 has an
Delphi 3 Superbible
Paul Thurrott, Gary Brent, Richard reason over and speak any embed all manner of expert enhanced IDE, including
Bagdazian, & Steve Tendon language. systems, advisors, problem improved project support.
Waite Group Press The Amzi! Prolog engine solvers, and intelligent Console versions of the
is a native Unicode applica- components on Web pages. command-line tools are
tion similar to Java and NT This interface consists of a available for building Amzi!
4.0. The Prolog source code C program, the Prolog Prolog components while
can be written in Unicode framework, and a library compiling and linking the
or ASCII, allowing predi- that provides an interface main application. The latest
cates and variables, as well between the Prolog script development environments
as strings to be represented and CGI/HTTP protocols. from Borland and Microsoft
in the 16-bit Unicode char- The Java Class allows Prolog are also supported.
acter set. components to be embedded
ISBN: 1-57169-027-1 Support of Internet and in Java programs and applets. Price: Personal edition, US$298;
Price: US$54.99 Web-based applications has They include support for Professional edition US$598.
(1,312 pages, CD-ROM) been expanded with three exception handling. Contact: Amzi! Inc., 40 Samuel
Phone: (800) 368-9369
new components, the CGI The Sockets Logic Server Prescott Dr., Stow, MA 01775
interface, the Java Class, Extension allows users to Phone: (508) 897-7332
and the Sockets Logic write clients and/or servers in Fax: (508) 897-2784
Server Extension. Prolog that communicate E-Mail: [email protected]
The CGI interface allows with other Internet clients Web Site: http://www.amzi.com

Pretty Objects Computers, Inc. Announces Polyglot 2.24


Pretty Objects Computers, tions in other languages by Price: US$200 for first license;
Inc. of Outremont, Quebec exporting all character US$100 for second to fifth license;
has announced Polyglot 2.24, strings to a table, and re- US$50 for additional licenses.
an internationalization expert importing them. Contact: Pretty Objects Computers,
for Delphi 2 that enables Polyglot also manages ele- Inc., 5158 Hutchison, Outremont,
users to create multilingual ments that are less visible, Quebec, Canada H2V 4A9
applications. such as the user’s Help file, Phone: (514) 990-7026
Polyglot allows users to date formats, numbers, and Fax: (514) 990-7026
determine what needs to be common Windows dialog E-Mail: [email protected]
changed to present applica- boxes. Web Site: http://www.prettyobjects.com

3 July 1997 Delphi Informant


Standard

Client/Server
Professional
Delphi Comparing Delphi 3 Versions
T O O L S Features
Visual drag-and-drop RAD X X X
New Products 32-bit, optimizing, native-code compiler X X X
and Solutions Royalty-free, stand-alone EXEs and reusable DLLs X X X
Packages compiler technology for EXEs X X X
Interfaces for native COM and ActiveX support X X X
Access to Win32 API, ActiveX, multi-threading, OLE, COM, DCOM, ISAPI, NSAPI X X X
Creates multi-threaded Windows 95/NT applications X X X
Professional IDE with Editor and Debugger X X X
Object-oriented, extensible component and application architecture X X X
Object repository for storing and reusing forms, data modules, and experts X X X
Visual form inheritance and form linking X X X
Suite of Windows 95 common controls X X X
Visual component Library with over 100 drag-and-drop reusable components X X X
Create and use OLE automation controllers and servers X X X
Visual components creation for making component templates X X X
CodeTemplates Wizard X X X
CodeCompletion Wizard X X X
CodeParameter Wizard X X X
ToolTip Expression Evaluation X X X
DLL debugging X X X
Multiple database engine support X X X
Native drivers for MS Access, FoxPro, Paradox, and dBASE X X X
Data-aware components X X X
Separate business rules from application code with Data Module Objects X X X
Hidden Paths of Delphi 3
Ray Lischner Database Explorer for managing tables, aliases, and indices X X X
Informant Press Integrated reporting X X X
Delphi 1 for 16-bit Windows 3.1 applications X X X
One-step ActiveX creation for maximum reusability (100 percent compiled
high-performance ActiveX controls with no run-time redistributables) X X
One-step ActiveForm creation to Web-enable applications X X
Live graphs and charting X X
Additional 30 VCL components X X
VCL source code and printed manual X X
ODBC connectivity X X
Maintain data integrity with scalable Data Dictionary X X
Develop and test SQL applications with Local InterBase X X
Cached updates X X
Internet Solutions Pack X X
ISBN: 0-9657366-0-1 InstallShield Express X X
Price: US$39.99
(300 pages, CD-ROM)
Open Tools API X X
Phone: (800) 884-6367 or Printed documentation X X
(916) 686-6610 SQL Links native drivers, with unlimited deployment license for Oracle,
Sybase, Informix, MS SQL Server, InterBase, and DB2 X
SQL Database Explorer X
SQL Monitor X
Visual Query Builder X
Develop and test multi-user SQL applications with InterBase (4-user license) X
Decision Cube Crosstabs for multi-dimensional data analysis X
Remote DataBroker X
ConstrainBroker X
Business ObjectBroker X
WebServer X
Support for Netscape NSAPI and Microsoft ISAPI with WebBridge X
WebModules for information publishing X
WebDispatch for responding to Web client requests X
WebDeploy X
Integrated Intersolv PVCS Version Manager X
CASE Tool Expert X
Data Pump Expert X

4 July 1997 Delphi Informant


News Borland Announces Spin-Off of Open Environment Consulting Group
Scotts Valley, CA — Borland
has announced NetNumina
Numina include pre- and
post-sale technical support,
tier technology knowledge to
Web-deployed, distributed
L I N E Solutions Inc., a spin-off of training, custom develop- object solutions.
its Open Environment ment projects, and place- Borland acquired the Open
July 1997 Division. Located in Boston, ment and management of Environment Corp. in
MA, NetNumina has been contract personnel at cus- November 1996. Borland is
providing consulting services tomer sites. currently preparing Entera
on behalf of Borland to The principals of 4.0 (previously developed by
Open Environment and NetNumina were previously Open Environment) for
Borland customers, as well as the consulting arm of the release, and is expanding the
servicing new customers, Open Environment Entera development team by
since April. Division. They produced adding engineers.
Under the terms of the over 150 multi-tier applica- Secondarily, Entera is an
agreement, consulting ser- tions at Fortune 1000 com- enabling technology within
vices provided to Borland panies. NetNumina is cur- Delphi, Borland C++Builder,
and its customers by Net- rently extending its multi- JBuilder, and IntraBuilder.

Borland Improves Support for Corporate Developers


Scotts Valley, CA — Increasing tions to database servers, and Extended Developer Assist
its focus on corporate IT, usability of Borland products offers priority hotline ser-
Borland announced a new in a workstation network or vices during service hours
developer support system. This client/server environment, on on Borland workstation
Apple Computer’s new support structure includes a per incident basis. products, including extend-
Worldwide Vice President
Joins Borland several programs differentiated Priority Developer Assist is ed products such as
Borland has appointed John by levels of support, rather an annual developer support Delphi/400. Questions con-
Floisand, 52, to vice president of
US Sales. Floisand has over 25 than by product lines. Borland service with priority hotline cerning the installation, pro-
years of sales experience, most hopes this unified process will assistance, during service gramming, connections to
recently with Apple Computer, Inc.
where he was responsible for speed response times and hours, on Borland workstation database servers, and usabili-
worldwide sales. reduce the number of support products. The service covers ty for Borland products in a
Floisand plans to use his experi-
ence in sales, customer service, contracts needed. questions concerning installa- workstation, network, or
operations and support to help Since May, Borland has been tion, programming, connec- client/server environment
build Borland’s direct support
business. This involves improving taking orders for its new sup- tions to database servers, and are covered for a predefined
relationships with VARs and sys- port contracts. Borland has, at usability of Borland products number of 15 incidents or
tems integrators to extend support
for corporate customers. the same time, continued to in a workstation, network, or 12 months.
Additionally, Floisand plans to honor all existing contracts. client/server environment for For more information on
improve the segmentation of
Borland’s product range, packag- The new support programs a predefined number of 15 these programs, call Borland
ing, prices, distribution, and sup- apply to customers in the US incidents or 12 months. at (408) 431-1064.
port to better serve customers.
During Floisand’s 11 years at and Canada, and are scheduled
Apple Computer, he held various to be in operation this month. Droege’s 1997 Developers Competition
management positions, including:
senior vice president of Worldwide They include: Installation Durham, NC — Droege Humanity, Sunshares,
Sales; president of Apple Pacific; Assist, offering customer sup- Computing Services, Inc. has Rainbow House, and others.
vice president of Sales, Customer
Services, Operations and Support; port via telephone on Borland announced the 1997 This year developers can
and director of UK Sales. workstation software prod- Developers Competition, to select from special categories,
ucts; and Primary Assist, pro- be held October 7 to 9 in including Internet, object-
viding per-minute telephone Durham, NC. oriented, RAD, GUI, and
support on local installation In the past six years, this client/server.
and product usability. competition has attracted one- The results are judged by
Developers can choose or two-developer teams from industry experts, and the win-
among several programs, 16 countries. Contestants ners split US$10,000 in cash.
including Developer Incident build a typical business appli- Typically, another
Assist, Priority Developer cation for a charity, and may US$500,000 of sponsor-
Assist, and Extended use any product on any plat- donated software products is
Developer Assist. form. Past events have pro- also distributed among the
Developer Incident Assist duced applications for a Child contestants.
provides phone support for Protection Team, the Duke For more information,
questions concerning installa- Primate Center, the American visit http://www2.inter-
tion, programming, connec- Dance Festival, Habitat for path.net/devcomp.

5 July 1997 Delphi Informant


On the Cover
Delphi 2 / SMTP / Winsock

By Gregory Lee

Internet Delphi: Part I


Creating an SMTP E-Mail Client Program

E -mail is being used in some ingenious and non-traditional ways that go


beyond sending a message to a friend or business acquaintance. Automated
product registrations, program support, even database reporting can be tied into
the global e-mail distribution system. Adding Internet e-mail capabilities could be
just the thing to turn your next ho-hum application into an exciting and useful
product.

Over the course of the next few months, we’ll understand what TCP/IP is, or how it works,
follow the development of a fully functional to use Winsock. What you do need is the
Internet e-mail program written entirely with- WINSOCK.DLL and a basic knowledge of
in Delphi. Although we’ll focus on traditional the functions available. In the Delphi Finger
e-mail functions, you can adapt the underly- article, I touched on some of the more com-
ing code to just about any application. monly used functions. If you want to under-
stand the low-level stuff, the Finger article is
Getting Started a good place to start. To keep things simple
If you haven’t used Delphi to write an here, however, we won’t discuss the Winsock
Internet application before, you may want to interface much more.
do a little homework. A few good books are
available that focus on programming for the Pick a Protocol
Internet, but ones that help you do it in Internet e-mail is governed by two basic
Delphi are few and far between. The August protocols:
1996 issue of Delphi Informant contains an 1) The Simple Mail Transfer Protocol
article I wrote that describes a Finger pro- (SMTP) lays out the rules for sending
gram written in Delphi. Truth be told, there’s messages, from the e-mail clients’ point of
not a lot to implementing the Finger proto- view.
col itself, and the bulk of that article is really 2) The Post Office Protocol (POP) defines
about Winsock. the process for retrieving messages.

Winsock is the Windows version of the origi- In this installment, we’ll focus exclusively on
nal Berkeley sockets interface. The sockets the implementation of SMTP. POP is a little
interface was developed to provide a simple more involved, but we’ll get around to it.
API for network applications based on the You’ll be able to apply a lot of what you learn
TCP/IP network protocol. You don’t need to here when we get to POP.

6 July 1997 Delphi Informant


On the Cover
example, in the e-mail address [email protected], the host
name is goodnet.com. If you want an explanation of how the
host name is translated into an IP address, you can pore
through the Windows Socket Specification (or get a copy of
the Finger article mentioned earlier). From our perspective,
the process itself is less important than the result.

The host name gets us most of the way there, but we still
need to know where on the host system we can find the
SMTP mail server. RFC 821 tells us that the mail server will
be listening for calls at port number 25. You can think of
Internet port numbers as the extension numbers in a tele-
phone system. Most established protocols have a fixed port
number where clients and servers can count on hooking up.
These fixed port assignments are often referred to as “well-
Figure 1: The ftp site ds.internic.net/rfc contains an index to all
RFC documents, as well as the documents themselves.
known ports,” and the “well-known port” for SMTP happens
to be port 25.

With the host name and port number now in place, we’re
ready to establish the connection. Unfortunately, the connec-
tion itself doesn’t happen instantaneously. Depending on the
route the connection takes and the amount of network traffic,
it may take a few seconds. Sooner or later though, we should
get a notification message indicating a successful connection.

Welcome to the Machine


According to RFC 821, when we connect to the SMTP serv-
er, it will send us a greeting. Again, Winsock sends a notifica-
tion message to indicate that the data has arrived. We’ll use
the Winsock recv function to retrieve the greeting message.

The greeting could contain just about anything, but according


to SMTP, we can count on one constant element: the first three
characters in the greeting message will be 220. Now we could
check the first three characters of every message we get during
an SMTP session for these three magical characters, but it’s
more efficient to create something called a state machine, so we
Figure 2: A typical SMTP e-mail client session.
only look for the 220 prefix when appropriate. The state
If you want a complete description of SMTP, you should read machine is also handy because it lets the program remember
RFC 821, “Simple Mail Transfer Protocol” by J. Postel (see how far along our session has progressed at any given time. Are
Figure 1). RFC stands for Request For Comment, and virtu- we waiting for the greeting? Have we sent the response? Are we
ally every Internet standard is documented somewhere in an waiting for the server to acknowledge something we’ve sent?
RFC file. You can find this and other RFC documents at
ftp://ds.internic.net/rfc. A simple state machine can be implemented with one global
variable (the current state), and a case statement that executes
Connecting on Cue whatever code is appropriate, given the current state. At each
In a typical e-mail session, users enter their address and the stage in the protocol, our program will proceed by sending
address of the person to whom they’re sending the message. something new to the server, then waiting for a response.
They will then enter the subject and body of the message (see When the response or acknowledgment message is received,
Figure 2). When everything is ready to go, they’ll click the the state machine is bumped to the next level.
Send button. This is our application’s cue to initiate the
SMTP conversation. In our SMTP Email Client program, an SMTP state machine
is implemented using the global variable State and the case
Before we can connect to the SMTP server, we must know statement inside the SmtpEngine procedure. To make the code
where to find it. Part of the server’s location can be taken a little clearer, we’ve also defined a new type for the State vari-
directly from the e-mail address of the person to whom the able. Appropriately enough, we’ve called this new type TState
message is being sent. In a typical e-mail address, everything (see Figure 3). With the new type established in this way, the
to the right of the @ sign is referred to as the host name. For function of the code in SmtpEngine is fairly obvious.

7 July 1997 Delphi Informant


On the Cover
TState = (Inactive, Connected, HelloSent, FromSent, where user@host is the Internet e-mail address of our
ToSent, DataStart, SendingData, DataEnd, intended receiver. If there are multiple receiver addresses,
QuitSent, UnChanged);
we simply wait for a reply and send another RCPT
Figure 3: The definition of the TState variable type. TO:<user@host> message with the next address. Once all
the receiver lines are sent, the State variable becomes
A Warm HELO ToSent, so the state machine will be ready to move forward
Initially, our State is Inactive. Once we’ve connected to the
when a response arrives.
SMTP server, our State changes to Connected. The Connected
case handles this state by scanning incoming messages for the
The ToSent case looks for another 250 and, once we get it,
220 prefix we’re expecting. When we get the response we’re
we’re nearly ready to send the text of our message.
looking for, we can proceed to the next step, which is to send
the server a warm HELO. No, that’s not a typo. RFC 821
Are We There Yet?
indicates that we must respond to the server’s greeting by
Before we can actually send the text of the message, we
sending a line back to the server with the keyword HELO, fol-
must give the SMTP server a little warning. The keyword
lowed by the name of the host system. Once we’ve sent it,
used to accomplish this is DATA. After the DATA message
State is bumped to HelloSent.
has been sent, the State variable is set to DataStart, and
incoming messages are filtered through the DataStart case.
The next time we receive a message from the server, it will fil-
ter through our state machine, and the new message will be
This time we’re looking for a return message of 354. Once
handled by the code in the HelloSent case. This code checks
we get it, we can start sending the text of our e-mail. Sort
for a reply: 250. Again, SMTP doesn’t dictate exactly what
of. According to RFC 822, “Standard for the format of
the server will send us, but it does require that the first three
ARPA Internet text messages” by D. Crocker, Internet e-
characters be 250. As soon as we receive that sequence, we
mail messages must conform to some additional rules.
can proceed to the next step.
Most of these rules have to do with something called head-
ers. Message headers are those things at the top of every
Return to Sender
Internet e-mail you get; they tell you when and where the
One of the basic requirements of any e-mail system is that you
message originated, who it was sent to, and how it came to
let the e-mail server know where a particular message is com-
your system. Some of that stuff is tacked on along the way,
ing from and where it’s headed. The need for the receiver’s
but a few things must be included from the beginning.
address should be obvious; however, the need for the sender’s
may not be so clear. The simple answer is that the SMTP serv-
er needs a return address in case things go bad. For example, Specifically, we need to include a From header, a To header,
what if the user you’re sending this e-mail to just moved to a Date header, and a Message-ID header. There are a lot of
another system? Typically, the host system will notify you by other, optional, header lines such as Subject, Cc, Bcc,
return mail, if an e-mail you’ve sent is undeliverable. Reply-To, as well as an undefined number of Extension
headers. Extension headers are easy to spot, because they all
The format of the line we send to the server indicating the start with a leading X-. Among other things, they’re often
return address is: used to identify the e-mail program and version that gener-
ated the message.
MAIL FROM:<user@host>
We already have most of the information needed for these
where user@host is our Internet e-mail address. After this headers, so it’s just a matter of building some strings and shov-
line is sent, we set the global state variable to FromSent, so ing them into the queue. Using the Delphi functions Time
the state machine will know what to look for as a and DateTimeToString, we can build the Date header easily
response. enough, but where does this Message-ID header come from?

The code in the FromSent case will look for the appropriate The Message-ID header uniquely identifies a specific piece
reply code, which, in this case is another 250. If you haven’t of e-mail. There’s no way to be positive a message identifier
bought into the importance of this state-machine scheme by we generate will be unique. However, we can greatly
now, the fact that we’re getting a second 250 reply message increase the odds by using something like our e-mail address
should close the deal. Up until this point, we could have got- and the current date and time. After all, what are the odds
ten away with using the reply number to indicate our next that somebody else is going to use our e-mail address to
move; but if we’re getting the same reply to MAIL FROM identify a piece of their e-mail? If you combine that with
and HELO, that approach would clearly be inadequate. the current time, even we would have to try pretty hard to
create another message with exactly the same identifier.
Once the 250 reply is detected, we can send the destina-
tion address. The format of this line is: After we place all the headers into the pipeline, a blank line is
sent after them to identify the end of the header section.
RCPT TO:<user@host> Now we’re ready to send the text of the message.

8 July 1997 Delphi Informant


On the Cover
Make It So To make things even more confusing, you may also see this
The text of the message is sent one line at a time. At this point, error message if you have one or more conflicting versions
we don’t even have to wait for the server to respond after each of WINSOCK.DLL and/or WSOCK32.DLL on your sys-
line. The only problem we have to be even remotely concerned tem. This often happens when one version of the DLL
about is the possibility that a line of the message will start with exists in your \Windows\System directory and is used to
the period character. That’s a potential cause for alarm, because establish your local TCP/IP network connection, and
that happens to be the signal we use to tell the mail server that another version is used by the dialer that was provided
we’ve reached the end of the message text. To send a line of text with your Internet account.
that starts with a period, we must quote the period character by
preceding it with yet another period character. Simple enough. Different versions of Winsock and WSock32 are generally
not compatible, so the golden rule is: Whatever
Each of the lines are sent, and the state machine remains in the WINSOCK.DLL or WSOCK32.DLL is used to establish
SendingData mode until we reach the end of the text. Then we your Internet connection must also be the first and only
send the termination signal — a line containing a single period version available to your Internet client program. Typically,
character — and advance the State variable to DataEnd. If all the first hint that this is a problem occurs when you see:
goes well, the mail server responds to our signal with another 250
reply code. Valid name, no data record of requested type

To finish, we use the message QUIT, and advance the State vari- for a host name that you know is a valid registered domain.
able again to QuitSent. The mail server reacts by sending us a 221
reply code, then closes the connection. All we have to do now is
close our end of the connection, and let the user know the e-mail Conclusion
has been sent. To turn this into a full-featured mail program, we’ll probably
want to log the message at this point, or at least record some-
Your Mileage May Vary where the fact that a message has been sent. And then there’s
The most common error message you’re likely to encounter with receiving messages — but we’ll get to that next time. D
the sample program is:

Valid name, no data record of requested type The files referenced in this article are available on the Delphi
Informant Works CD located in INFORM\97\JUL\DI9707GL.
This message is a direct pass-through from Winsock. This usually
indicates that one of two name lookups has failed.

When we’re attempting to connect with the SMTP server, among


the first low-level tasks we must complete is to translate the given Gregory Lee is a programmer with over 15 years of experience writing applications
host name into an Internet IP address. Every registered Internet and development tools. He is currently the president of Software Avenue, Inc.,
domain has at least one unique IP address. which has just released a C++Builder Edition of their Delphi development tool,
Internet Developer’s Kit. Greg can be reached by e-mail at 76455.3236@compu-
The Internet uses the IP address for identification and routing. serve.com.
The host name is essentially a convenient alias for this address.
The mechanism used to store and report the relationship between
host names and IP addresses is called the Domain Name System
(DNS). If the DNS lookup for a host name fails, Winsock will
give the error code for:

Valid name, no data record of requested type

You may also see this message on a LAN where the ‘HOST-
NAMES’ file has not been set up properly, or does not contain
an entry for the host name you’ve given.

After the host name has been resolved, a second name lookup
may be used to translate the service name ‘smtp’ into its associat-
ed “well-known” port number. The translation between common
Internet service names and their “well-known” port numbers is
handled by way of another (much smaller) lookup table. If this
translation fails, the Winsock error code again indicates:

Valid name, no data record of requested type

9 July 1997 Delphi Informant


Informant Spotlight
Delphi 3

By Ray Lischner

What’s New with Experts?


A Look at Project and Module Creators

D elphi 3 has many new, exciting features — packages, interfaces, ActiveX


support, and more. What you might not know is that Delphi’s Open Tools
API also sports some enhancements. This article examines two of the major new
features for writing experts and wizards: project creators and module creators.

The Open Tools API lacks formal documen- Jump Right In


tation, so this article refers to specific source First, let’s look at how project and module
files for the Open Tools API. These source creators work, using an expert-creation
files are in the \Source\Toolsapi directory, wizard as an example. The wizard creates a
provided you have Delphi 3 Professional or skeleton for an expert, based on a few tidbits
higher. If you’re not familiar with the Open of information from the user. Specifically, it
Tools API and writing experts in Delphi, creates a library project to create and register
several books provide solid information: the wizard. It also creates a unit that declares
Hidden Paths of Delphi 3 [Informant Press, the expert interface class. Figure 1 shows the
1997] and Secrets of Delphi 2 [Waite Group expert-creation wizard at work. It prompts
Press, 1996] by Ray Lischner, Delphi the user for the kind of expert to create and
Component Design [Addison-Wesley an expert name, then uses this information
Developers Press, 1997] by Danny Thorpe, to create its files.
and The Revolutionary Guide to Delphi 2
[WROX Press, 1996] by Paul Hinks, et al. The first step is the same for any expert:
declare the expert interface class. Listing One
Start by looking at the TIToolServices class in (beginning on page 14) shows the Expert
ToolIntf.pas, where you will see several new unit, which declares the TExpertCreator class.
methods. This article discusses the The expert-creation wizard is a project
ModuleCreate and ProjectCreate methods and expert, so it defines an author, comment,
their related classes, TIModuleCreator and repository page, and so on. The Execute
TIProjectCreator (which you can find in method calls RunExpert, which is in the
EditIntf.pas). ExptDlg unit, shown in Listing Two (begin-
ning on page 15). RunExpert opens the main
form to get the expert style and name from
the user. If the user clicks the OK button, the
expert employs the user’s information to
instantiate a TExpertInfo object, which it
passes to CreateExpert in the ExptGen unit.

The interesting part of the expert is the


ExptGen unit (see Listing Three, beginning
on page 15). This unit declares the project
creator class, TProjectCreator, and a module
creator, TModuleCreator. The project creator
creates the library source file. Notice that
most of its methods return empty strings,
Figure 1: The Expert Creation Wizard. which tell Delphi to use its default settings.

10 July 1997 Delphi Informant


Informant Spotlight
For example, GetFileName must Flag Literal Description
return the filename for the project’s
cpCustom Custom project (empty project file).
source file. An empty string, howev-
cpApplication Application (project starts with default program declaration).
er, tells Delphi to use its default —
cpLibrary DLL (project starts with default library declaration).
Project1.dpr — in the current direc-
cpCanShowSource Delphi displays the project source file.
tory. The two most interesting meth-
cpExisting The project source file already exists.
ods are NewProjectSource and
Figure 2: The flags for ProjectCreate.
NewDefaultModule.
to use existing files, or to create new ones. Note that Delphi
NewProjectSource returns the source code for the project’s .DPR doesn’t actually create a file on disk when the project creator
file. In this case, the source code is relatively short, so this func- runs. Instead, it creates files in memory. When the user saves the
tion calls Format to produce the result string. The parameters for project, Delphi saves its in-memory buffers to disk. This is the
Format are the project name and the expert interface class. same behavior as when Delphi creates its default project. Delphi
Delphi supplies the project name as an argument to assigns the name Project1 to the project and Unit1 to the blank
NewProjectSource. Your expert supplies the class name, from the form, but doesn’t create any files on disk. This gives the user the
TExpertInfo object. You must ensure that the source code is cor- greatest degree of flexibility to rename the project or units when
rect, or your expert’s user might be upset when he or she is saving files, or to abandon the project without saving anything.
unable to compile the resulting project.
To use a project creator, you first derive a class from the interface
After Delphi creates the project source file, it calls class, TIProjectCreator, overriding all its methods. The expert cre-
NewDefaultModule, which creates the sole unit for the ates an instance of your project creator class, and passes that
project. As you might expect, given the topic of this article, object as the first argument to TIToolServices.ProjectCreate. When
NewDefaultModule uses a module creator for the expert your expert calls ProjectCreate, Delphi calls back to your project
interface unit. creator object to learn the project’s name, source file, resources,
and so on. The declaration for the ProjectCreate method of
The module creator, TModuleCreator, has a more difficult job TIToolServices is:
than the project creator. Just like TProjectCreator, most of the
methods of TModuleCreator return empty strings. As you might function ProjectCreate(ProjectCreator: TIProjectCreator;
have already guessed, that tells Delphi to use its defaults for the CreateFlags: TCreateProjectFlags): TIModuleInterface;

filename, and so on. NewModuleSource, however, is complex. It


returns the complete source code for the module file — in this If you don’t need to specify the project’s source file and
case, the expert interface unit. You can use the same Format tech- resources, you can let Delphi create a default project by pass-
nique that you used in the project creator, but it’s hard to work ing a nil pointer for the ProjectCreator argument. In this case,
with. Instead, the module creator writes its text to a stream. the CreateFlags argument tells Delphi what kind of project to
create. Figure 2 lists the project creation flags; except for
One of the advantages of using a stream is that you can define a cpCanShowSource, the flags have meaning only if
custom class — in this case, TTextStream. With a custom stream ProjectCreator is nil. When ProjectCreator is nil, you must
class, you can define methods that work the way you want. The choose one of cpCustom, cpApplication, or cpLibrary to specify
TTextStream class defines methods that make it easier to write to what kind of project you want Delphi to create.
the stream one line at a time.
The cpApplication project is Delphi’s default application and
To summarize, the CreateExpert procedure does the real default main form. The cpLibrary project is Delphi’s default DLL
work of creating the project. CreateExpert constructs an project. The cpCustom flag is for any other kind of project. For a
instance of TProjectCreator and passes that instance to cpCustom project, Delphi starts with an empty file, which is not
ToolServices.ProjectCreate. Delphi calls the methods of the pro- what you usually want your expert to do. Instead, when using
ject creator to obtain information about the project, and uses cpCustom, your expert can create the project source file explicitly,
that information to create and open a new project. The project and use the cpExisting flag to tell Delphi that the file exists.
creator adds units to the project in the NewDefaultModule
method. The sample expert has one unit, so NewDefaultModule If you set the cpExisting flag (and ProjectCreator is nil) and the
creates one module creator instance and passes that object to project source file doesn’t exist, Delphi will create a default
ToolServices.ModuleCreate. Delphi calls back to the module cre- project file, according to the cpApplication (default project file,
ator to obtain information about the unit file, and creates the but with no forms), cpLibrary (default DLL project), or
unit’s source file accordingly. Now it’s time to look more closely cpCustom (empty source file) flag.
at the creator classes, and understand what they do.
Regardless of whether ProjectCreator is nil, you can use the
Project Creators cpCanShowSource flag when you call ProjectCreate. This flag
A project creator puts you in control when creating a new pro- tells Delphi to display the project source file in its Code
ject. The project creator instructs Delphi how to create the pro- Editor. If you exclude cpCanShowSource, Delphi hides the
ject’s source (.DPR) and resource (.RES) files. It can tell Delphi project source file. For an application with forms, you

11 July 1997 Delphi Informant


would typically omit cpCanShowSource; but for projects NewProjectResource Procedure
such as a library, you would most likely want to include this procedure NewProjectResource(Module:
flag. ProjectCreate returns a module interface object for the TIModuleInterface);
new project file. You use the module interface to further Delphi calls the NewProjectResource procedure after
modify the project file. No matter how your expert uses the NewDefaultModule to report to your expert about the project’s
interface, it must call Free to release the interface. resource file. If Existing returns True, and the project resource file
exists, Delphi doesn’t call NewProjectResource. If Existing returns
TIProjectCreator Class False, or if the resource file doesn’t exist, the Module argument is
The first argument to ProjectCreate is the project creator the module interface for the new resource file. You can call its
object. If you want to create a default project, pass a nil GetProjectResource method to obtain a resource file interface.
pointer. If you want to define the resource, project source
file, filename, or other aspects of the project, you must When Delphi calls NewProjectResource, the resource file is empty.
define a project creator. The following sections describe the Remember that when you save a project, and you haven’t speci-
functions and procedures of TIProjectCreator. Remember to fied an application icon, Delphi supplies a default MAINICON
override all of these, because TIProjectCreator declares them resource. In other words, if you’re satisfied with the default appli-
as virtual, abstract methods. (You can find the declaration cation icon, you can write NewProjectResource, which will do
for TIProjectCreator in the EditIntf.pas file.) nothing except free the module interface.

Existing Function One important aspect of a module interface object is that


function Existing: Boolean; Delphi creates a single TIModuleInterface object for each file,
Override the Existing function to return True if the project and shares that object with all the experts that need it. Delphi
files exist, or False if your expert is creating new files. If you uses reference counting to make sure it doesn’t free a module
define Existing to return False, Delphi assumes the files interface object while an expert still retains a reference to it.
don’t exist, and calls NewProjectSource, NewDefaultModule, When your expert finishes using any module interface object, it
and NewProjectResource. If one or more files exist, define must free the object reference so Delphi can keep an accurate
Existing to return True. Delphi looks for the files; if a file reference count. Thus, even if NewProjectResource doesn’t use its
exists, Delphi doesn’t call the corresponding function. That Module argument to add any resources, it must call Module.Free
is, if the .DPR file exists, Delphi doesn’t call to release the object reference.
NewProjectSource. If the .RES file exists, Delphi doesn’t call
NewProjectResource. Delphi doesn’t look for any unit files — NewProjectSource Function
if Existing returns False, Delphi doesn’t call function NewProjectSource(const
NewDefaultModule. ProjectName: string): string;
Delphi calls the NewProjectSource function to obtain the con-
GetFileName Function tents of the project’s source (.DPR) file. If Existing returns
function GetFileName: string; True, and the project source file exists, Delphi doesn’t call
Override the GetFileName function to return the full path to NewProjectSource. Otherwise, Delphi calls NewProjectSource,
the project’s .DPR file. If the file doesn’t exist, Delphi doesn’t and passes the name of the project as the ProjectName argu-
create the file yet, but marks the project as modified, so the ment. Typically, your expert would use the name in the source
user can choose to save the file, or abandon the new project file’s program or library statement. This function must return
without creating any files. The GetFileName function can the full contents of the file as a single string.
also return an empty string, in which case Delphi uses a
default project name (e.g. Project1) in the current directory. Module Creators
A module creator is similar to a project creator, but they differ
in many of their details. The biggest difference is that a project
GetFileSystem Function
creator creates a project, while a module creator creates a mod-
function GetFileSystem: string;
ule — that is, a unit source file and possibly a form or data
Override the GetFileSystem function to return the name of a
module. The methods of TModuleCreator work slightly differ-
registered file system that will store the project’s source and
ently than the corresponding methods of TProjectCreator, so
resource files. In most cases, your project creator will return
you can’t always apply to module creators what you learned
an empty string, telling Delphi to use the default file system.
from project creators.
NewDefaultModule Procedure
To use a module creator, you must derive a class from
procedure NewDefaultModule;
TIModuleCreator, and override its methods. Your expert creates
Delphi calls the NewDefaultModule procedure after it calls
an instance of your derived module-creator class, and passes that
NewProjectSource. You can use NewDefaultModule to add
object to the tool services ModuleCreate function. The declara-
units and forms to the project. If Existing returns True,
tion for the ModuleCreate function is:
Delphi doesn’t call NewDefaultModule. If you want a project
that doesn’t have any units or forms, write NewDefaultModule function ModuleCreate(ModuleCreator: TIModuleCreator;
so it returns without doing anything. CreateFlags: TCreateModuleFlags): TIModuleInterface;

12 July 1997 Delphi Informant


Informant Spotlight
The ModuleCreate function creates Flag Literal Description
a new unit, form, or data module, cmAddToProject Add the new module to the currently open project.
in a manner that your expert cmMainForm Make the new module the main form. Requires
defines. You define how to create cmAddToProject. Ignored if there is no form.
the module by deriving a class cmMarkModified Mark the module as modified so Delphi will prompt the user
from TIModuleCreator. Your to save it before closing the file.
expert creates an instance of your cmShowForm Show the form in the form designer (ignored if there is
derived, concrete class. This cre- no form).
ator object returns the module cmShowSource Show the unit in the Code Editor.
name, file system, and so on. cmUnNamed Mark the module as unnamed. When the user closes or saves
the file, Delphi will prompt the user for a filename.
Delphi calls back to your module
creator object after it creates a Figure 3: The significant flags for ModuleCreate.
form, passing a form interface object as an argument. call to TIToolServices.ModuleCreate, as previously discussed. This
section describes the TIModuleCreator class in depth.
ToolIntf.pas lists several flags in the TCreateModuleFlags type.
The ModuleCreate function heeds only some of the flags in its Existing Function
second argument. The other flags are there for CreateModule function Existing: Boolean;
and CreateModuleEx. Figure 3 describes the flags that matter Override the Existing function to return True if the module
to ModuleCreate. source file exists, or False if your expert will create a new file. If
Existing returns True, the file must exist, or Delphi raises an
When your expert calls ModuleCreate, Delphi calls back to your EFOpenError exception. Note that this behavior is different
module creator object, calling GetAncestorName, GetFormName, from that of a project expert, which checks whether the file
GetFileName, Existing, GetFileSystem, NewModuleSource, and exists, but continues regardless.)
FormCreated (in that order). Delphi creates the unit and form
files and returns a TIModuleInterface object. Most form experts
Note that Delphi ignores the cmExisting flag in the call to
can free the module interface without doing anything else with it.
TIToolServices.ModuleCreate. If the module files exist, make sure
Existing returns True.
If you return an empty string from GetFileName, make
sure you include the cmUnNamed flag when calling
FormCreated Procedure
ModuleCreate. Delphi generates a default name, but the
procedure FormCreated(Form: TIFormInterface);
user probably wants to choose a different name when sav-
Delphi calls the FormCreated procedure after it has created the
ing the file. The cmUnNamed flag tells Delphi that the cur-
form. If the source file (as returned from NewModuleSource)
rent name is just a placeholder, so Delphi prompts the user
doesn’t contain a {$R *.DFM} directive, Delphi doesn’t call
for a real filename when it saves the file.
FormCreated. If the unit source file contains a {$R *.DFM}
directive, Delphi creates the form, and passes the form interface
If Existing returns False, make sure you include the
to FormCreated. Typically, your expert would use the form inter-
cmMarkModified flag. This tells Delphi to mark the new files
face to set the form’s properties, add components, and so on.
as modified, forcing the user to save them before closing the
files or project. If you omit cmMarkModified, Delphi will let
the user close the project without saving the new unit or form GetAncestorName Function
files. In most cases, a form expert will use the cmShowSource function GetAncestorName: string;
and cmShowForm flags. Without them, Delphi creates the new Override the GetAncestorName function to return the name of
files, but doesn’t show the files to the user. A form expert’s job the ancestor form. If your new form doesn’t use form inheri-
is to create new units and forms, and the user probably wants tance, return an empty string or Form. To create a data module,
to see the newly created source code and form. use DataModule as the ancestor name. If your module creator is
creating a source unit without a form, return an empty string.
A project expert, on the other hand, might omit cmShowSource
and cmShowForm for certain units. Perhaps the project expert GetFileName Function
creates an application with several forms, including an About function GetFileName: string;
dialog box. The About box is less important than the applica- Override the GetFileName function to return the path to the
tion’s main form. The project expert can use cmShowForm and unit’s source (.PAS) file. Your module creator can also return
cmShowSource when creating the main form, but omit these an empty string, in which case Delphi chooses an appropriate
flags when creating the About box. This avoids cluttering the default filename, e.g. Unit1.pas in the current directory.
screen with too many forms. Delphi extracts the unit name from the filename by stripping
the drive, directory, and extension.
TIModuleCreator Class
Find the declaration of the TIModuleCreator class in the EditIntf If Existing returns True, the file named by GetFileName
unit. To use this class, derive a class from TIModuleCreator, over- must exist. If the file doesn’t exist, Delphi will raise an
riding all its methods. Your expert uses your derived class in a EFOpenError exception.

13 July 1997 Delphi Informant


Informant Spotlight
GetFileSystem Function The files referenced in this article are available on the Delphi
function GetFileSystem: string; Informant Works CD located in INFORM\97\JUL\DI9707RL
Override the GetFileSystem function to return the name of the
file system where you want Delphi to store the form and This article was adapted from Hidden Paths of Delphi 3
source file. You must name a registered file system or return [Informant Press, 1997], Ray Lischner’s latest book covering
an empty string to use the default file system. undocumented aspects of Delphi. Hidden Paths of Delphi 3
reveals the hitherto undocumented Open Tools API.
GetFormName Function Lischner’s first Delphi book, Secrets of Delphi 2 [Waite Group
function GetFormName: string; Press, 1996], continues to be pertinent for Delphi 3.
Override the GetFormName function to return the name of
the unit’s form. Return an empty string if your module cre-
ator is not creating a form, or if you want to use a default
form name, e.g. Form1. Another way to obtain a form name Ray Lischner is a contributor to several Delphi periodicals, and is a familiar figure
is by calling TIToolServices.GetNewModuleAndClassName. on the Delphi Usenet newsgroups. He is the founder and president of Tempest
Software, which specializes in consulting and training for object-oriented lan-
If you define GetFormName so it doesn’t return an empty guages, components, and tools. He also teaches Computer Science at Oregon State
string, you must ensure the form name is a valid University, is the president of the Corvallis chapter of the Software Association of
Oregon, and serves on the board of directors for the Pacific Northwest Software
Delphi/Pascal identifier, and is unique in the project. Your Quality Conference. You can reach him at [email protected].
expert can test whether a form name is unique by comparing
it with the names of all the other forms in the current project.

NewModuleSource Function
Begin Listing One — The Expert Unit
function NewModuleSource(UnitIdent, FormIdent,
unit Expert;
AncestorIdent: string): string;
{ Expert-creation wizard. }
Delphi calls the NewModuleSource function to obtain the
source code for the new module. The UnitIdent argument is interface

the name of the new unit, FormIdent is the name of the form,
uses Windows, Classes, SysUtils, Forms,
and AncestorIdent is the name of the ancestor form. Your Dialogs, ExptIntf, ToolIntf;
module creator must return the contents of the unit’s .PAS
file as a string. type
TExpertCreator = class(TIExpert)
public
If you’re defining a form or data module, make sure the procedure Execute; override;
unit’s source code contains a proper declaration of the form’s function GetAuthor: string; override;
function GetComment: string; override;
class, and a reference to the form description (.DFM) file. If
function GetGlyph: HICON; override;
you want to create a plain unit without a form, feel free to function GetIDString: string; override;
ignore the FormIdent and AncestorIdent arguments. function GetMenuText: string; override;
function GetName: string; override;
function GetPage: string; override;
Remember to preface the form and ancestor names with function GetState: TExpertState; override;
“T” to turn the names into type names. If your form does- function GetStyle: TExpertStyle; override;
end;
n’t use form inheritance, the ancestor name is Form. This
makes your job easier when creating the source code for the implementation
form; you can use the same code to generate the source
uses ExptDlg;
string for all situations.
resourcestring
Conclusion sAuthor = 'Tempest Software';
sComment = 'Create an expert';
Project and form experts are ideal solutions for defining sName = 'Expert Wizard';
standardized projects, units, and forms. Templates in the sPage = 'Projects';
Object Repository are static, and offer little flexibility.
procedure TExpertCreator.Execute;
Experts, on the other hand, have the full power of Delphi begin
to ask questions of the user and create customized projects, RunExpert;
units, and forms. Delphi 3 makes your job easier with pro- end;

ject and module creators. function TExpertCreator.GetAuthor: string;


begin
Result := sAuthor;
Project creators give your expert access to Delphi’s default
end;
application and library projects; or your expert can take
control, and define the project’s source and resource files. function TExpertCreator.GetComment: string;
begin
Module creators give you control for individual units and Result := sComment;
forms. Combine the two, and you can create experts and end;
wizards with ease. D

14 July 1997 Delphi Informant


Informant Spotlight

{ Run the expert when the user invokes it. }


{ Use the application's icon as the expert's icon. If you procedure RunExpert;
define a package for this expert, make sure it has var
the MAINICON resource. } Dlg: TMainDlg;
function TExpertCreator.GetGlyph: HICON; Info: TExpertInfo;
begin begin
Result := LoadIcon(hInstance, 'MAINICON'); Dlg := TMainDlg.Create(Application);
end; try
if Dlg.ShowModal = mrOK then
function TExpertCreator.GetIDString: string; begin
begin Info := Dlg.GetExpertInfo;
Result := 'Tempest Software.ExpertCreator'; try
end; CreateExpert(Info);
finally
function TExpertCreator.GetName: string; Info.Free;
begin end;
Result := sName; end;
end; finally
Dlg.Free;
function TExpertCreator.GetPage: string; end;
begin end;
Result := sPage;
end; { Enable the OK button only when the user has selected
an expert style and entered a name. }
function TExpertCreator.GetStyle: TExpertStyle; procedure TMainDlg.EnableOkButton;
begin begin
Result := esProject; OkButton.Enabled := (ExpertStyle.ItemIndex >= 0) and
end; (ExpertName.Text <> '');
end;
function TExpertCreator.GetMenuText: string;
begin
procedure TMainDlg.ExpertStyleClick(Sender: TObject);
Result := '';
begin
end;
EnableOkButton;
end;
function TExpertCreator.GetState: TExpertState;
begin
procedure TMainDlg.ExpertNameChange(Sender: TObject);
Result := [];
begin
end;
EnableOkButton;
end;
end.

{ Create a TExpertInfo object to store the information


End Listing One the user supplied. }
function TMainDlg.GetExpertInfo: TExpertInfo;
Begin Listing Two — The ExptDlg Unit begin
Result := TExpertInfo.Create(
unit ExptDlg;
TExpertStyle(ExpertStyle.ItemIndex),ExpertName.Text);
{ Expert-creation wizard. }
end;

interface
end.

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, End Listing Two
Forms, Dialogs, StdCtrls, ExtCtrls, ExptGen;
Begin Listing Three — The ExptGen Unit
type
TMainDlg = class(TForm) unit ExptGen;
ExpertStyle: TRadioGroup; { Expert-creation wizard. }
Label1: TLabel;
ExpertName: TEdit; interface
OkButton: TButton;
Button1: TButton; uses ToolIntf, ExptIntf;
procedure ExpertStyleClick(Sender: TObject);
procedure ExpertNameChange(Sender: TObject); type
private TExpertInfo = class
{ Private declarations } private
procedure EnableOkButton; fStyle: TExpertStyle;
function GetExpertInfo: TExpertInfo; fName: string;
public function GetClassName: string;
{ Public declarations } public
end; constructor Create(Style: TExpertStyle; Name: string);
property Style: TExpertStyle read fStyle;
procedure RunExpert; property Name: string read fName;
property ClsName: string read GetClassName;
implementation end;

uses ToolIntf, ExptIntf; procedure CreateExpert(Info: TExpertInfo);

{$R *.DFM} implementation

15 July 1997 Delphi Informant


Informant Spotlight

for I := 1 to Length(Name) do
uses SysUtils, Classes, EditIntf, TypInfo; if Name[I] in ['a'..'z','A'..'Z','_','0'..'9'] then
Result := Result + Name[I];
resourcestring end;
sInvalidStyle = 'Invalid expert style, %d';
sMainLogic = ' { Fill in the main logic here. }'; { TProjectCreator class. Remember the expert info object. }
sMainIcon1 = '{ In a package, you must explicitly add the'; constructor TProjectCreator.Create(Info: TExpertInfo);
sMainIcon2 = ' MAINICON resource to the package. In a DLL,'; begin
sMainIcon3 = ' set the icon in the project options. }'; inherited Create;
sAuthor = 'Author'; fInfo := Info;
sOrganization = 'Organization'; end;
sFormPage = 'Forms';
sProjectPage = 'Projects'; const
CRLF=#13#10;

type
{ Create and return contents of project's source file. }
TProjectCreator = class(TIProjectCreator)
function TProjectCreator.NewProjectSource(
private
const ProjectName: string): string;
fInfo: TExpertInfo;
begin
public
Result := Format(
constructor Create(Info: TExpertInfo);
'library %s;' + CRLF + CRLF +
function Existing: Boolean; override;
'uses ShareMem, Forms, ExptIntf, ToolIntf;' +CRLF+CRLF+
function GetFileName: string; override;
'{$R *.RES}' + CRLF + CRLF+
function GetFileSystem: string; override;
'function InitExpert(ToolSvc: TIToolServices;' + CRLF +
function NewProjectSource(
' RegProc: TExpertRegisterProc;' + CRLF +
const ProjectName: string): string; override;
' var Terminate: TExpertTerminateProc):' + CRLF +
procedure NewDefaultModule; override; ' Boolean; stdcall;' + CRLF +
procedure NewProjectResource( 'begin' + CRLF +
Module: TIModuleInterface); override; ' Result := True; { return False for error }' + CRLF +
property Info: TExpertInfo read fInfo; ' ToolServices := ToolSvc;'+CRLF+
end; ' Application.Handle := ToolSvc.GetParentHandle;'+CRLF+
TModuleCreator = class(TIModuleCreator) ' RegProc(%s.Create)' + CRLF + 'end;' + CRLF + CRLF +
private 'exports InitExpert name ExpertEntryPoint resident;' +
fInfo: TExpertInfo; CRLF + CRLF + 'begin' + CRLF + 'end.' + CRLF
public , [ProjectName, Info.ClsName]);
constructor Create(Info: TExpertInfo); end;
function Existing: Boolean; override;
function GetAncestorName: string; override; function TProjectCreator.Existing: Boolean;
function GetFileName: string; override; begin
function GetFileSystem: string; override; Result := False { New files. }
function GetFormName: string; override; end;
function NewModuleSource(UnitIdent, FormIdent,
AncestorIdent: string): string; override; function TProjectCreator.GetFileName: string;
procedure FormCreated(Form: TIFormInterface); override; begin
property Info: TExpertInfo read fInfo; Result := '' { Default filename. }
end; end;

function TProjectCreator.GetFileSystem: string;


{ Create the expert. } begin
procedure CreateExpert(Info: TExpertInfo); Result := '' { Default file system. }
var end;
ProjectCreator: TProjectCreator;
begin { Add the expert interface to the project. }
ProjectCreator := TProjectCreator.Create(Info); procedure TProjectCreator.NewDefaultModule;
try var
ToolServices.ProjectCreate(ProjectCreator, []); ModuleCreator: TModuleCreator;
finally begin
ProjectCreator.Free; ModuleCreator := TModuleCreator.Create(Info);
end; try
end; with ToolServices.ModuleCreate(ModuleCreator,
[cmAddToProject,cmShowSource,cmUnNamed,
cmMarkModified]) do
{ TExpertInfo class } { Free the module interface. }
constructor TExpertInfo.Create(Style: TExpertStyle; Free;
Name: string); finally
begin ModuleCreator.Free;
inherited Create; end;
fStyle := Style; end;
fName := Name;
end; { Default resources. Remember to free the
module interface. }
{ Build a class name from the expert name by removing all procedure TProjectCreator.NewProjectResource(
non-alphanumeric characters and prepending 'T' for Type. } Module: TIModuleInterface);
function TExpertInfo.GetClassName: string; begin
var Module.Free;
I: Integer; end;
begin
Result := 'T'; { TModuleCreator class. Remember the expert info object. }

16 July 1997 Delphi Informant


Informant Spotlight
constructor TModuleCreator.Create(Info: TExpertInfo); WriteLn(sMainLogic);
begin WriteLn('end;');
inherited Create; WriteLn('');
fInfo := Info; FormatLn('function %s.GetAuthor: string;', [ClsName]);
end; WriteLn('begin');
if Info.Style in [esForm, esProject] then
{ The TTextStream class inherits from TStringStream. FormatLn(' Result := ''%s'';', [sAuthor])
It makes it a little easier to write lines of text else
to a stream. } WriteLn(' Result := '''';');
type WriteLn('end;');
TTextStream = class(TStringStream) NewLine;
public FormatLn('function %s.GetComment: string;', [ClsName]);
constructor CreateEmpty; WriteLn('begin');
procedure FormatLn(Fmt: string; Args: array of const); if Info.Style in [esForm, esProject] then
procedure WriteLn(Line: string); FormatLn(' Result := ''%s'';', [Info.Name])
procedure NewLine; else
end; WriteLn(' Result := '''';');
WriteLn('end;');
constructor TTextStream.CreateEmpty; NewLine;
begin if Info.Style in [esForm, esProject] then
inherited Create(''); begin
end; WriteLn(sMainIcon1);
WriteLn(sMainIcon2);
procedure TTextStream.NewLine; WriteLn(sMainIcon3);
begin end;
WriteString(CRLF) FormatLn('function %s.GetGlyph: HICON;', [ClsName]);
end; WriteLn('begin');
if Info.Style in [esForm, esProject] then
procedure TTextStream.FormatLn(Fmt: string; WriteLn(' Result := LoadIcon(hInstance, ''MAINICON'');')
Args: array of const); else
begin WriteLn(' Result := 0;');
WriteString(Format(Fmt, Args)); WriteLn('end;');
NewLine; NewLine;
end; FormatLn('function %s.GetIDString: string;', [ClsName]);
WriteLn('begin');
procedure TTextStream.WriteLn(Line: string); FormatLn(' Result := ''%s.%s'';', [sOrganization, Info.Name]);
begin WriteLn('end;');
WriteString(Line); NewLine;
NewLine; FormatLn('function %s.GetMenuText: string;', [ClsName]);
end; WriteLn('begin');
if Info.Style in [esStandard] then
{ Return a string containing all source code for module. Use FormatLn(' Result := ''%s'';', [Info.Name])
a TTextStream to keep this function manageable. } else
function TModuleCreator.NewModuleSource( WriteLn(' Result := '''';');
UnitIdent, FormIdent, AncestorIdent: string): string; WriteLn('end;');
var NewLine;
Stream: TTextStream; FormatLn('function %s.GetName: string;', [ClsName]);
ClsName: string; WriteLn('begin');
begin FormatLn(' Result := ''%s'';', [Info.Name]);
ClsName := Info.ClsName; WriteLn('end;');
Stream := TTextStream.CreateEmpty; NewLine;
with Stream do FormatLn('function %s.GetPage: string;', [ClsName]);
try WriteLn('begin');
FormatLn('unit %s;', [UnitIdent]); if Info.Style in [esForm] then
NewLine; FormatLn(' Result := ''%s'';', [sFormPage])
WriteLn('interface'); else if Info.Style in [esProject] then
NewLine; FormatLn(' Result := ''%s'';', [sProjectPage])
WriteLn('uses Windows, ToolIntf, ExptIntf;'); else
NewLine; WriteLn(' Result := '''';');
WriteLn('type'); WriteLn('end;');
FormatLn(' %s = class(TIExpert)', [ClsName]); NewLine;
WriteLn(' public'); FormatLn('function %s.GetState: TExpertState;', [ClsName]);
WriteLn(' procedure Execute; override;'); WriteLn('begin');
WriteLn(' function GetAuthor: string; override;'); if Info.Style in [esStandard] then
WriteLn(' function GetComment: string; override;'); WriteLn(' Result := [esEnabled];')
WriteLn(' function GetGlyph: HICON; override;'); else
WriteLn(' function GetIDString: string; override;'); WriteLn(' Result := [];');
WriteLn(' function GetMenuText: string; override;'); WriteLn('end;');
WriteLn(' function GetName: string; override;'); NewLine;
WriteLn(' function GetPage: string; override;'); FormatLn('function %s.GetStyle: TExpertStyle;', [ClsName]);
WriteLn(' function GetState: TExpertState; override;'); WriteLn('begin');
WriteLn(' function GetStyle: TExpertStyle; override;'); FormatLn(' Result := %s;',
WriteLn(' end;'); [GetEnumName(TypeInfo(TExpertStyle), Ord(Info.Style))]);
NewLine; WriteLn('end;');
WriteLn('implementation'); NewLine;
NewLine; WriteLn('end.');
FormatLn('procedure %s.Execute;', [ClsName]);
WriteLn('begin'); Result := DataString;
if Info.Style in [esForm, esProject, esStandard] then finally

17 July 1997 Delphi Informant


Informant Spotlight

Free;
end;
end;

function TModuleCreator.Existing: Boolean;


begin
Result := False; { Create new files. }
end;

function TModuleCreator.GetAncestorName: string;


begin
Result := ''; { Not using inheritance. }
end;

function TModuleCreator.GetFileName: string;


begin
Result := ''; { Use the default filename. }
end;

function TModuleCreator.GetFileSystem: string;


begin
Result := ''; { Use the default file system. }
end;

function TModuleCreator.GetFormName: string;


begin
Result := ''; { Not a form. }
end;

procedure TModuleCreator.FormCreated(
Form: TIFormInterface);
begin
{ This unit has no form, so Delphi should never call
this method. If you define a module creator that
defines a form, remember to free the form interface,
as shown below. }
Form.Free;
end;

end.

End Listing Three

18 July 1997 Delphi Informant


Delphi at Work
Delphi 2 / Object Pascal / Microsoft Access / Visual Basic for Applications

By Ian Davies

Automated Access
Creating Automation Clients: Part III

I n this third and final installment of the Automation series, we’ll cover the use of
Microsoft Access as an Automation Server. (In the May issue of Delphi Informant I
discussed using Word as an Automation server; last month I covered using Excel.)

Access version 2 for Windows 3.1 uses Access where Acc is declared as a Variant elsewhere
Basic as its underlying programming language, in the program. The instance data of the
but versions 7 and 8 for Windows 95 (sup- Automation object is stored in Acc, and it is
plied with the Professional Editions of Office through this that you gain access to its
95 and Office 97, respectively) use Visual underlying functionality. For example, to
Basic for Applications (VBA). Because Access open an existing database, you would call
version 2 cannot act as an Automation server, I the OpenCurrentDatabase method of the
will concentrate on the use of VBA for the Application object, as follows:
remainder of this article.
Acc.OpenCurrentDatabase(

Access as an Automation Server filepath := '...path and filename of database...');

Access exposes the Application object, which


can be used for Automation. It can be used in Similarly, the NewCurrentDatabase method
the following way: will create a new, empty database (the filepath
variable is implied in this case):
Acc := CreateOLEObject('Access.Application');
Acc.NewCurrentDatabase(
'...path and filename of new database...');

Each open database has an associated DoCmd


object that exposes various properties and
methods specific to that database. For exam-
ple, to open a pre-defined query in the cur-
rent database, use the OpenQuery method of
the DoCmd object:

Acc.DoCmd.OpenQuery('Sales by Category');

Similarly, the OpenReport method will open


and print the report specified by the argument
it is passed.

Figure 1 shows a Delphi form, in design mode,


that demonstrates the principles discussed

19 July 1997 Delphi Informant


Delphi at Work
Automation. This is achieved by calling the RunMacro method
of the DoCmd object, passing the name of the macro as a
string parameter. For example:

Acc.DoCmd.RunMacro('Macro1');

This means that processes not available, nor appropriate to be


performed via Automation, can still be used. They are imple-
mented directly in Access and controlled from Delphi.
Figure 1: An Automation example.
Data Access Objects
here. The program uses the Northwind sample database that Technically speaking, Data Access Objects (DAO) is a COM
ships with all versions of Access. (Component Object Model) interface to the JET database
engine (Microsoft’s counterpart to the Borland Database
One important point concerning the Northwind database is that Engine). DAO to a Delphi programmer is, in some respects,
of its splash screen: When the database is opened — normally or similar to Access, insofar as it’s an Automation server. However,
through Automation — it displays a splash screen. If you were it’s typically used for a different purpose. In general, DAO is
using Access directly, the splash screen must be cleared before any used for manipulating data, and Access is used for presenting it.
progress can be made, because it’s shown modally. When control- DAO is more efficient than Access at getting data stored in an
ling Access using Automation, however, this isn’t necessary, Access database (a .MDB file ), because it comprises — and
because the commands directly reference the underlying object, therefore loads into memory — only the functionality necessary
so your application proceeds as if the splash screen wasn’t there. for manipulating the data, not any visual functions such as
reporting, graphical querying, table manipulation, etc.
(It’s possible to prevent the splash screen from appearing by
sending a + keystroke, which simulates holding down S,
DAO exposes the dbEngine object as its top-level object used
before the database is opened. However, because the splash
in Automation as follows:
screen doesn’t inhibit the functionality of Access when used
through Automation, and considering that the databases you’re dbEngine := CreateOLEObject('DAO.dbEngine');
likely to use probably won’t have a splash screen, I won’t delve
into the complexity of sending keystrokes to other applications.) where dbEngine has been declared as a Variant. The dbEngine
object can be used to control various functions, such as imple-
The Open Query and Print Report buttons implement the previ- menting transaction control, creating new databases, opening
ous two examples. I chose to use the “Ten Most Expensive existing databases, setting access privileges, and returning
Products” query in the example, because this demonstrates a details of error messages. In the following example, we are
nice feature of Access — its implementation of SQL that makes using the OpenDatabase method to open an existing database,
generating statistics of this kind very simple. The Export Data the instance details of which are stored in the dbs variable
button executes a pre-defined query, exports the data returned (which has previously been declared as a Variant):
by that query in dBASE format, and finally opens and displays
the table using Delphi’s Table and DataSource components. dbs := dbEngine.OpenDatabase(
The export is performed using Access’ TransferDatabase '... path and filename of database...');

method, and is called with various parameters describing the


type of transfer to be carried out, the format of the export, the Having established a reference to a particular database, we can
path of the destination table, the type of the source object, the now create what is known in the Microsoft world as a recordset.
name of the source object, and the destination filename: You can think of a recordset as a collection of records in a data-
base, such as a table, part of a table, a query, or part of a query.
Acc.DoCmd.TransferDatabase(acExport, 'dBase 5.0', This object is also stored in a Variant and can be used as follows:
'C:\Program Files\Borland\Delphi 2.0\Demos\Data\', acQuery,
'Ten Most Expensive Products', 'qry.dbf');
rst := dbs.OpenRecordset('Ten Most Expensive Products');

All the methods used here, together with their respective para-
Rather than a specific pre-defined object in a database,
meters, are fully documented in Access’ online Help, and need
recordsets can take as a parameter a SQL query statement.
only slight modifications to get them to work via Automation
For example:
from Delphi.
rst := dbs.OpenRecordset('Select * From Employees',
When closing the application, the instance of Access also needs dbOpenSnapshot);
to be closed. This is performed by calling the Quit method of
the Application object. As previously stated, DAO is useful for manipulating data,
rather than displaying it. The previous examples demon-
Using Access, macros that perform some local function can be strated how queries can be executed, but there was no way
stored within the database and executed remotely using for the results to be viewed. The functionality discussed

20 July 1997 Delphi Informant


Delphi at Work
of each field, and post the changes using the Update
method. Similarly, to search for a particular value in a data-
base, you would set the active index and call the Seek
method. The steps involved are identical to those you
would use if you were using a database native to Delphi,
but the syntax is different. After these differences are over-
come, DAO is an ideal way to get to existing data stored in
an Access database.

Licensing
It’s appropriate to mention the licensing implications of
using third-party products as Automation servers. In gener-
al, you have no rights to distribute any part of the
Automation server with your application unless permission
Figure 2: A demonstration form that uses DAO to access data. is obtained from the author of the server software (which
will typically involve a licensing fee). However, if you used
procedure TQueryForm.FillStringGrid(Data: Variant); Visual Basic or Visual C++ (or another similar Microsoft
var development tool) as the development language, you have a
NumRows, NumColumns, l, M: Integer;
begin
royalty-free license to distribute the Microsoft JET database
{ Move to the last record in the recordset to ensure the engine and its associated DAO with your application.
RecordCount method references all the records. }
Data.MoveLast;
Data.MoveFirst; Unfortunately, this doesn’t extend to developers using non-
Microsoft tools, such as Delphi. If you know your clients
{ Retrieve the number of fields and records, then have the rights to use a product that can be used as an
set the size of the string grid. }
NumRows := Data.RecordCount; Automation server, you are permitted to make full use of its
NumColumns := Data.Fields.Count; capabilities, provided the number of system users doesn’t
exceed the number of licenses of the Automation server.
StringGrid1.RowCount := NumRows + 1;
StringGrid1.ColCount := NumColumns; Different license agreements have different restrictions, so
it’s always wise to check with the author of the Automation
{ Add Captions to first row of string grid. }
for l := 0 to NumColumns - 1 do
server before implementing any system based on it.
StringGrid1.Cells[l, 0] := Data.Fields[l].Name;
Conclusion
{ Cycle through each cell in the recordset placing its
value in the appropriate place in the StringGrid. }
Access (and its associated file format) is an extremely popu-
for l := 0 to NumRows - 1 do begin lar database management system, largely because it’s part of
for M := 0 to NumColumns - 1 do the Microsoft Office Professional suite. Using Automation is
if Data.Fields[M].Value <> Null then
StringGrid1.Cells[M, l + 1] := Data.Fields[M].Value
only one way to get at data in its databases; various third-
else party add-ons are available for Delphi that offer native
StringGrid1.Cells[M, l + 1] := ''; access to the data without the overhead of using an
{ Move to the next record in the recordset. }
Data.MoveNext; Automation server. However, Automation is the ideal solu-
end; { for } tion if you’re interested in manipulating systems that exist in
end; Access that include more than a database.
Figure 3: A function to display the contents of a dataset in a
string grid. Referring generally to office suites, you can choose the com-
ponent of the suite that best suits a particular sub-task and
here has been implemented in the example shown in Figure 2. call (what are likely to be) pre-defined functions to produce
It includes the two queries already mentioned, plus a func- an extremely rapid solution. For example, Excel provides
tion that returns the results and displays them in a string many complex functions that would be pointless and diffi-
grid (see Figure 3), a facility that performs an indexed cult to recreate in Delphi. Using Automation, they are only
search on a particular table, and an example of inserting a a function call away, yet, at all times, control is maintained
new record with some sample data into a database table. A by your application. An ideal application of Automation is if
more complex example using DAO would involve having your client wants to maintain control over the content of,
multiple databases and multiple recordsets within those for example, printed reports, to make changes at will, yet
databases open at once, possibly stored in a Variant array. still be able to access them via a program pre-written in
Delphi (this was described in detail in Part I of this series).
If you’ve used Delphi’s standard facilities for accessing This removes the need to rebuild reports using a specialized
tables and queries, using DAO will be reasonably familiar reporting tool for what could only be a minor amendment.
to you. To add a record to a database, for example, you Furthermore, if implementing a new system, the creation of
would call the Add method of the recordset, set the values the reports (or conversion of existing ones) could be carried

21 July 1997 Delphi Informant


Delphi at Work
out by the system users, while development of the code to
access them could proceed in parallel — only a small inter-
face routine would be required to bridge the gap.

Although I have referred specifically to Microsoft Office and


its components in this series, the principles discussed can be
applied to any Automation server. Even if you aren’t interest-
ed in implementing such a system in a practical situation,
you should at least have an idea of how complex Automation
servers are designed, should you ever decide to create your
own in Delphi. D

The files referenced in this article are available on the Delphi


Informant Works CD located in INFORM\97 \JUL\DI9707ID.

Ian Davies is a developer of 16- and 32-bit applications for the Inland Revenue
in the UK. He began Windows programming using Visual Basic about four years
ago, but has seen the light and is now a devout Delphi addict. Current interests
include Internet and intranet development, inter-application communication, and
sometimes a combination of the two. Ian can be contacted via e-mail at
[email protected].

22 July 1997 Delphi Informant


Greater Delphi
InterBase / Object Pascal / SQL

By Bill Todd

InterBase Indexes
Inside InterBase: Part II

I n Part I of this series (presented in last month’s issue), we explored creating and
using InterBase triggers and generators. In this installment, we continue looking
inside InterBase, focusing on the use of its indexes.

Why Indexes? index is to scan it sequentially — you


The reasons to create indexes in an InterBase might as well scan the table.
database are the same as for any other:
Indexes provide a fast way to find specific Creating Indexes
rows in a table, and the means to enforce Creating indexes has advantages and disad-
uniqueness. Because any index is a sorted list vantages. For the best performance from
— unlike the table data itself — the database InterBase, you must understand the type of
management system (DBMS) can find any indexes to create and when to create them.
value quickly. The ability to enforce unique- By indexing all the columns you may use to
ness stems directly from the ability to find a select records, you’ll garner the best perfor-
value quickly; each time you add a row to the mance locating rows. However, indexes
table, the DBMS must first search for the hurt performance when you are inserting,
unique value to see if it already exists. modifying, or deleting rows. This is because
the indexes must be updated each time you
You can create an index on a single column, insert or delete a row, or modify an indexed
or on multiple columns in a table. InterBase column in a row.
allows a maximum of 16 columns in an
index. By maintaining a sorted list of the val- Indexes also consume disk space. However,
ues in the column(s) being indexed, an index this is usually a minor consideration because
lets you find rows fast. Therefore, you can disk space is relatively inexpensive compared
use a multi-column index to search for a to users’ lost time with a slow application.
value or values in the first n columns.
Therefore, you should create an index under
For example, assume you have an index on an these three conditions:
Items table that includes the 1) The column(s) are used frequently in the
CustomerNumber, OrderNumber, and WHERE clause of a query.
PartNumber columns. A DBMS can use this 2) The column(s) are used frequently to join
index to find rows by: tables in a query.
■ customer number, 3) The column(s) are used frequently in the
■ customer number and part number, or ORDER BY clause of a query.
■ customer number, part number, and
order number. With InterBase, including the same column
in multiple indexes is a bad idea. For exam-
However, the index is useless if you need to ple, if you have three columns to index, you’ll
find a row by part number alone; the index need six indexes to include all the different
is sorted first by customer number. The combinations of column order. However,
only way to find a part number in the InterBase can use multiple, single-column

23 July 1997 Delphi Informant


Greater Delphi
Database Statistics window. In the Database Statistics win-
dow, select View | Database Analysis to display the statistics
for all the tables and indexes in the database. To find the
information for a particular index, select Search from the
main menu, and search for the index name. Figure 1 shows
the statistics for the CUSTNAMEX and CUSTREGION
indexes. The table in Figure 2 explains these statistics.

InterBase automatically creates an index whenever you define a


primary key, foreign key, or unique constraint. You can create
additional indexes using the SQL CREATE INDEX statement.
Its syntax is:

CREATE [UNIQUE] [ASCENDING | DESCENDING] INDEX IndexName


ON TableName (column1, column2, ...)

For example, this statement creates a unique index on the


Figure 1: The InterBase Server Manager’s Database Statistics window.
Items table:
indexes when a query includes selection criteria for both
CREATE UNIQUE INDEX ITEMX
columns. So you’ll get better overall performance creating a ON ITEMS (CustomerNumber, OrderNumber, PartNumber)
single-column index on each of the three columns, than by
creating six, three-column indexes. You can abbreviate ASCENDING as ASC and DESCENDING
as DESC. The index’s name must be unique within the database.
However, this isn’t true if you use the columns in only one You can create multiple indexes on the same column. So if
order. For example, if you always select rows from the you need to order records both ways, you may want to create
Items table by specifying a customer number, an order an ascending and a descending index on a date column. You
number, and a part number, you’ll get the best perfor- cannot create a unique index if the column already contains
mance by creating a single index on all three columns. duplicate values.
The same is true if you always order a query’s results by
the same columns. If the ORDER BY clause exactly Modifying Indexes
matches the fields and order of an existing index, To permanently remove an index, use the SQL DROP
InterBase will use the index to order the result set, instead INDEX command. For example, this command permanently
of retrieving the requested rows and sorting them. removes the index named ITEMX from the database:

Additionally, you’ll get better performance using single-col- DROP INDEX ITEMX
umn indexes if a query’s WHERE clause includes the OR
operator to connect selection criteria on two columns. This You can also temporarily deactivate an index using the
is because InterBase indexes are used for moving through ALTER INDEX command. For example, these commands
records in order, and as bitmaps. In an OR operation, deactivate the ITEMX index, then reactivate it:
InterBase will use single-column indexes to evaluate the
selection conditions, then combine them to retrieve the ALTER INDEX ITEMX INACTIVE
ALTER INDEX ITEMX ACTIVE
required rows. (Yes, bitmapped indexes existed long before
FoxPro started using them.)
Setting the index to ACTIVE rebuilds the index; on a large
table, this may take some time. One case where you’ll want
The database page size also plays a role in index perfor-
to deactivate indexes is when importing a large number of
mance. A larger page size means each index page
holds more entries. That, in turn, means fewer Item Definition
pages must be read from disk to search the index.
Depth Number of levels in the index tree.
The page size also affects the depth of the index
leaf buckets Number of leaf pages in the index.
tree. If an index is more than four levels deep,
nodes Total number of pages in the index.
consider increasing the page size. If the index
Average data length Average length of each index entry in bytes.
depth on data that changes frequently is less than
total dup Total number of rows in the index with
three levels, consider decreasing the page size. duplicate values.
However, don’t decrease the page size to the point max dup Number of rows for the value that has the
that one record will not fit on a page. most duplicate entries in the index.
Fill distribution Histogram showing the number of pages in
To determine the number of levels in the index, each of the percent fill ranges.
use the InterBase Server Manager. Select Tasks | Figure 2: Explaining the numbers in the Database Statistics window shown
Database Statistics from the menu to open the in Figure 1.

24 July 1997 Delphi Informant


Greater Delphi
records into a table. Deactivating the indexes on the table,
importing the data, then rebuilding the indexes by activating
them is faster than incurring the overhead of updating the
indexes for each new row as it’s imported.

Although indexes normally require no maintenance on


your part, some attention is occasionally necessary to
ensure optimum performance. InterBase indexes use a bal-
anced B-tree structure; over time, they can become unbal-
anced if many records are added and deleted. Deactivating
and reactivating the indexes will rebuild them, and correct
this problem.

Selectivity
For each index, InterBase also generates a statistic referred to as
selectivity. Selectivity indicates the number of unique values in
the index, in relation to the number of rows in the table. The Figure 3: The Basic ISQL Set Options dialog box.
query optimizer uses this number to determine if an index
should be used to process a particular query, or if scanning the
table will be faster. Adding and/or deleting large numbers of
records could change the index’s selectivity.

The selectivity of the index is calculated when the index is built.


It is not updated as rows are added and deleted from the table.
Although selectivity is re-computed any time the index is
rebuilt, you don’t have to rebuild the index to update the selec-
tivity. You can update the selectivity using the SET STATIS-
TICS command:

SET STATISTICS ITEMX

None of these steps are necessary if you periodically back up


and restore your database. A backup and restore rebuilds all
indexes, and thus recalculates their selectivity. Additionally, a
backup and restore performs a sweep to remove outdated
record versions, and re-packs the database so the pages are uni- Figure 4: Displaying the query’s plan.
formly filled.
Plan box; ISQL will display the query plan for each query you
When you use a SQL SELECT statement to retrieve records execute. Figure 4 shows the result of the following query:
from an InterBase database, the optimizer examines your
SELECT E.FIRST_NAME, E.LAST_NAME, E.PHONE_EXT, D.DEPARTMENT
query and formulates an execution plan. To determine which FROM EMPLOYEE E, DEPARTMENT D
indexes to use, and how to use them to provide the best per- WHERE E.DEPT_NO = D.DEPT_NO
formance, the optimizer looks at:
■ the size of each table involved in the query, The results window contains the query, plan, and query
■ the available indexes, result, respectively. In this example, the plan is:
■ the selectivity of the indexes,
■ the contents of the WHERE clause, and PLAN JOIN (D NATURAL, E INDEX (RDB$FOREIGN8))
■ the contents of the ORDER BY clause.
This shows the optimizer will perform the natural join between
A Definite Plan the Employee and Department tables using the index,
Although the InterBase optimizer is very good, it’s always possi- RDB$FOREIGN8. The index has a system-generated name
ble it won’t use an index when you think it should. If you experi- because it was created automatically when the foreign key on
ence poor performance from a query, you should examine the DEPT_NO field in the Employee table was defined.
whether the query plan created by the optimizer appears to make
optimal use of available indexes. To override the plan created by the optimizer, use the PLAN
clause of the SQL statement (see Figure 5). An InterBase exten-
To see the optimizer’s plan for executing a query, start ISQL sion to standard SQL, the PLAN clause lets you specify your
and select Session | Basic Settings to display the Basic ISQL Set own plan instead of letting the optimizer create one. You
Options dialog box (see Figure 3). Check the Display Query should use this capability sparingly. First, it’s unlikely you’ll

25 July 1997 Delphi Informant


Greater Delphi

Figure 5: Specifying a plan.

find a case where you can create a better plan than the optimiz-
er. Second, one of the great benefits of dynamic query optimiza-
tion is that the query plan may change over time, as the charac-
teristics of the tables change. For example, if an index’s selectivi-
ty drops dramatically, the optimizer will recognize this, and stop
using the index. If you add an index to a table, the optimizer
will detect the new index automatically, and use it when appro-
priate. On the other hand, if you specify a plan in your SQL
statement, the plan will never change — no matter what hap-
pens to the database — unless you change it.

Conclusion
You will get the best possible performance from your
InterBase application by carefully creating indexes on fields,
or combinations of fields, that you use to select and/or order
rows. InterBase is more flexible in its use of indexes than
most databases, in that it can effectively use multiple single-
column indexes to select rows, based on values in multiple
columns. Therefore, creating single-column indexes is gener-
ally better than creating multi-column indexes, unless you’re
sure you’ll always select and/or order by the same columns
in the same order.

Next month, we’ll look at managing security for your


InterBase databases. D

Bill Todd is President of The Database Group, Inc., a database consulting and development
firm based near Phoenix, AZ. He is a Contributing Editor of Delphi Informant; co-author of
Delphi: A Developer’s Guide [M&T Books, 1995], Delphi 2: A Developer’s Guide [M&T
Books, 1996], and Creating Paradox for Windows Applications [New Riders Publishing,
1994]; and a member of Team Borland providing technical support on CompuServe. He is
also a nationally known-trainer, and has been a speaker at every Borland Developers
Conference and the Borland Conference in London. He can be reached on CompuServe at
71333,2146, on the Internet at [email protected], or at (602) 802-0178.

26 July 1997 Delphi Informant


Columns & Rows
Paradox / BDE / Delphi

By Dan Ehrmann

The Paradox Files: Part IV


Validity Checks and Referential Integrity

D evelopers use the Paradox file format every day, yet the Delphi documenta-
tion offers little information about it. To help fill that gap, the first two articles
in this series explored the internals of Paradox .DB files: table structure, BDE
record and block management, field types, and record size calculation. The third
article examined primary and secondary indexes. In this, the fourth article of the
series, we’ll examine validity checks and referential integrity.

What Is a Validity Check? This file has an undocumented binary for-


A validity check, abbreviated as ValCheck, is a mat, so you can’t modify it directly.
data formatting requirement that you define
for a field, and that’s enforced for you by the The Paradox file format supports the fol-
BDE whenever you add or modify the value lowing types of ValChecks:
in that field. ■ required value (the field cannot be left
blank);
ValChecks are defined and modified from the ■ minimum value in a field;
Restructure dialog box of the Database ■ maximum value in a field;
Desktop. When this dialog box first opens, ■ default value in a field when a record is
the panel on the right side allows you to set inserted; and
ValChecks for each field (see Figure 1). ■ a format mask that can also control
ValChecks are saved in the TableName.VAL allowable values in a field.
file, a member of the table’s family of files.
Figure 2 lists each Paradox field type, and
the ValChecks that are valid for that type.

ValChecks have been available in the


Paradox file format since its earliest ver-
sions. A former limit of 64 ValChecks per
table was removed with the advent of Level
7 tables. (For more information on Paradox
table “levels,” see the “The Paradox Files:
Part I,” in the April 1997 Delphi
Informant.)

ValChecks provide a number of benefits,


because they’re defined and enforced at the
database level, rather than within your
application. This allows you to maintain
Figure 1: The Database Desktop’s Restructure dialog box. them in one place, and make them available

27 July 1997 Delphi Informant


Columns & Rows

Field Type ValChecks


to every application The Default ValCheck
that uses the same Use this option to specify a starting value for the field when a
Alpha All valid
set of tables. As new record is inserted. You can then overwrite the default
Number All valid
you’ll see, however, value with any other valid entry. The Default ValCheck is
Money (Currency) All valid
Delphi provides fully supported by Delphi. Delphi 3 includes a DefaultValue
Short All valid
only limited sup- property for many of the TField-derived objects, which can
Long Integer All valid
port for ValChecks, be used in place of the ValCheck.
BCD All valid
reducing their ben-
Date All valid
efit in your applica- Special keywords. For Date fields, you can enter the keyword
Time All valid
tions. Today in the Default ValCheck to place the current system
Timestamp All valid
date into the field. You can also enter this keyword in the
Autoincrement Minimum only
Logical Required and
The Minimum Minimum or Maximum ValChecks, to set the current date as
Default only
and Maximum the lower or upper limit. (This option has been supported
Memo Required only
ValChecks since the file format’s earliest versions.)
The purpose of
Formatted Memo Required only
ValChecks is self- For Time and Timestamp fields, you can enter the keyword
Graphic Required only
evident. You can Now in the Default ValCheck to place the current system time
OLE Required only
define one to set a or date/time in the field. You can also enter this keyword into
Binary Required only
lower or upper the Minimum or Maximum ValChecks, to set the current
Byte Required only
boundary, or define time or date/time as the lower or upper limit. (This option
Figure 2: Field types and valid ValChecks.
them both to set a was added in Level 7.)
range. The bound-
ary value itself is a valid entry (i.e. with a Minimum ValCheck The Required ValCheck
defined, allowable values are equal to or greater than the This ValCheck specifies that a value is required in the field
defined value.) Obviously, the Maximum ValCheck must be before the record can be posted. If you post without a value,
larger than the Minimum. you’ll receive the following error message:

When you use Delphi’s field editor to add static field EDatabaseError
Field <name> must have a value.
objects to your form, Delphi doesn’t pick the underlying
ValChecks for those field types that have MinValue and
Delphi’s static field objects include a Required property. When
MaxValue properties. This is an unfortunate omission on
you instantiate one of these objects, and if the DataSet con-
Borland’s part.
nection is active, Delphi will set the Required property to
True, if there is a Required ValCheck. You can also set this
When you specify these ValChecks with an Alpha field, the
BDE gets the sort order from the table language. (If you don’t property manually without the underlying ValCheck being
specify a custom table language, each table is defined with a set, but you must then write code in the OnValidate event, to
default language driver that you specify in the BDE enforce the property.
Configuration program.) When you define a Minimum
ValCheck for an Autoincrement field, the BDE uses this The Picture ValCheck
value to seed the counter. New records increment from the Pictures are format strings that control which values can be
specified value. entered in a field, and how those values should be displayed.
Figure 3 shows the characters in Paradox’s picture “language,”
Delphi and the BDE do not test these ValChecks until you while Figure 4 shows some sample pictures.
post the new or updated record. If one of them is violated,
Character Meaning
you’ll receive the following error:
@ Any character
EDBEngineError # Any number (0-9)
Minimum (or Maximum) validity check failed.
? Any letter (uppercase or lowercase)
Field: <name>.
& Any letter (converted to uppercase)
~ Any letter (converted to lowercase)
The minimum or maximum value isn’t shown, and there’s no
! Any character (letters converted to uppercase)
easy way to obtain it. If you use the MinValue and MaxValue
[ ] Bracket-optional entries
properties instead, you’ll receive the following error when the
{ } Group-required entries
value limits aren’t met:
*<num> Specify a number of repetitions of a group
or character
EDatabaseError
X is not a valid value for field <name>. * Specify any number of repetitions
The allowed range is <min> to <max>. ; Literal escape character
Other chars Treated as literals
As you can see, the second error is more informative for the user. Figure 3: The Paradox picture language.

28 July 1997 Delphi Informant


Columns & Rows
Picture Meaning 1) A change should be cascaded through the database. For
example, if you change a vendor ID or delete a vendor, you
###-##-#### US Social Security Number
might choose to cascade this change through the Stock table.
*3#-*2#-*4# US Social Security Number
2) A change should be prohibited if records depend on the
#&#-&#& Canadian postal code
value being updated or deleted. For example, you might
red,green,blue,yellow One of four listed colors.
stop the user from deleting a vendor if products sold by
red,b{rown,l{ack,ue}} One of four listed colors; grouping
ensures the first matching entry that vendor are still within their warranty period.
isn’t filled. 3) A change should cause linked values to be set to null or
!*@ Anything, but capitalize the first blank. For example, if you decide to no longer support
character if it’s a letter. a particular type of packaging, all stock items that use
##/##/#### Date with a four-digit year. the packaging method might have this field set to null.
!*{ !,@} Capitalize the first letter in each
word (“proper case”). The Paradox file format doesn’t support all these outcomes.
Figure 4: Picture examples. In fact, only the following limited options are available:
■ For update operations, you can elect to cascade or prohib-
Delphi provides no support for Paradox’s Picture ValCheck
it the change. Paradox does not support changing to null
on data entry or posting. Instead, Delphi provides properties
for update operations.
with some of the same capabilities, specific to each field type:
■ For delete operations, only prohibit is supported. Paradox
■ TFloatField, TCurrencyField, TSmallIntField,
tables do not support “cascaded delete” or “change to null.”
TIntegerField, TBCDField, and TAutoIncrementField
This means that you must manually delete linked rows
objects provide the DisplayFormat and EditFormat proper-
from the foreign-key table before you can delete the match-
ties. (EditFormat is ignored for Autoincrement fields.)
ing row from the primary-key table. (This operation will
■ TDateField, TTimeField, and TTimeStampField objects
succeed, because there will then be no dependent rows.)
provide the DisplayFormat and EditMask properties.
■ TStringField objects use only the EditMask property, because
This limited support for RI is one of the more serious limita-
the displayed value is the same as the entered value.
tions in the Paradox file format. It’s unfortunate that Borland
■ TBooleanField objects use the DisplayValues property to
doesn’t offer more.
control how True and False are represented in data-
bound controls. Defining RI
RI is defined from the foreign-key table back to the primary-
Referential Integrity key table. For example, an RI relationship between the Stock
When one or more fields in a table refer to the primary and Vendor tables is defined from the Stock table, referring
key of another table, those fields are said to be a foreign back to the primary key of the Vendor table.
key, i.e. a key “imported” from another table. Foreign keys
are necessary to enforce data consistency among the vari- To set up RI, use the Restructure dialog box in the Database
ous tables in the database. The Referential Integrity (RI) Desktop. To display the appropriate panel, click on the Table
rule states that when two sets of fields in two tables are in properties drop-down list in the top right corner. To define an RI
a primary key/foreign key relationship, every foreign key rule, click the Define button to see the dialog box shown in
value in one table must match an existing primary key Figure 5. On the left side, select the field or fields that define the
value in the other table. foreign key. On the right side, select the table whose primary key
matches this foreign key. You can also define how the RI rela-
For example, a typical Stock table might contain fields for tionship should behave for updates. When you close the dialog
VendorID, StockType, and PackagingType. Each of these box, you’ll be prompted to enter a name for the RI relationship;
fields is likely to be a foreign key to a supporting table this name follows the same rules as the field and index names
containing the valid vendors, stock types, and packaging described in earlier articles.
types. RI ensures that only entries in each of these
supporting tables can be used to populate the
appropriate fields in the Stock table.

Certain types of operations on a database might


cause existing foreign-key data to become invalid.
For example, if a vendor goes out of business, what
happens to the stock items supplied by that vendor?
And if your company decides to exit from a specific
line of business, what happens to all the items with
that stock type?

For update and delete operations, the RI rule defines


three possible outcomes: Figure 5: The Database Desktop’s Referential Integrity dialog box.

29 July 1997 Delphi Informant


Columns & Rows
Strict RI. In Figure 5, notice the Strict referential integrity
check box. This setting is a holdover from the days when
Paradox for DOS and Paradox for Windows applications
coexisted on networks. Paradox for DOS didn’t support the
RI features added to the Paradox file format with Level 4.
When this box was checked, the table could be accessed only
by Paradox for Windows, to protect against the possibility
that a Paradox for DOS application would subvert RI. This
setting is ignored by the BDE.

How Does RI Work?


RI information is saved in the .VAL files for both tables.
The BDE first places a maintained secondary index on the
field or fields comprising the foreign key. This index allows
Figure 6: The Database Desktop’s Table Lookup dialog box.
the BDE to quickly sort and filter the table by these fields,
and to manage the foreign-key side of the relationship. It No additional information is available from the EDBEngineError
also allows Delphi to create a one-to-many relationship object, or from the TDBError objects in the BDE error stack.
within the primary-key table as the master, and the foreign-
key table as the linked detail. When you’re prompted to The Table Lookup “ValCheck”
specify the name of the RI relationship, you’re actually In its earliest days, the Paradox file format didn’t support RI at all.
specifying the name of this secondary index. Instead, Paradox provided a Table Lookup feature that worked in
a similar fashion. A Table Lookup can be established on any field
For the foreign-key table, the BDE stores the name of the in a table, linking that field to the single-field primary key of
field or fields comprising the foreign key, the name of the another table. (There’s no support for composite primary keys.) A
table whose primary key forms the other end of the rela- value entered in that field must exist in the lookup table.
tionship, and the name of the index used to manage the
link. It also stores the prohibit or cascade setting for the Table Lookup information is also stored in the .VAL file. To
update rule. configure a Table Lookup, use the Restructure dialog box in
the Database Desktop. Click the Table properties drop-down
For the primary key table, the BDE stores the name of the list in the top-right corner to display the appropriate panel.
dependent foreign-key table, and the name of the index used Click the Define button to see the dialog box shown in
to manage the link. This information allows the BDE, Figure 6. On the left side, specify the single field on which
Database Explorer, and Database Desktop to list the tables to perform a lookup. On the right side, select the lookup
that depend on this table. A data-modeling tool could use this table, whose primary key must match the field already
information to trace the RI relationships in either direction. selected. Two additional options can be set:
■ A Lookup access parameter is designed for table view in
RI and Delphi the Database Desktop and Paradox itself, and for native
Because its database operations are performed by the BDE, Paradox forms. When set to Help and fill, this option pro-
Delphi fully supports Paradox’s implementation of RI. If you vided a Cs hot key to display the lookup table
try to insert a value into a field that has an RI relationship to in a dialog box, so that a value could be selected. When
another table, and the inserted value isn’t in the other table, set to Fill no help, this option only validated the entry,
you’ll receive the following error message: and didn’t provide the hot key or pop-up dialog box.
■ A Lookup type parameter controls which fields are filled
EDBEngineError
Master record missing. when a lookup is performed. When set to Just current
field, only the lookup field itself is filled. When set to
Unfortunately, Delphi doesn’t tell you which field contains All corresponding fields, the lookup field and any addi-
the errant value, nor does it move you to that field once tional fields with the same name and type are filled.
you’ve cleared the error dialog box.
Table Lookup and Delphi
If you try to change the primary-key value in the other If you have a Table Lookup defined, Delphi will validate
table, or delete the record with that value — and if there the lookup value on record-post (not on field-depart, as the
are records using that value — you’ll receive the following Database Desktop and Paradox do.) If the value isn’t in the
error message: lookup table, you’ll receive the following error message:

EDBEngineError EDBEngineError
Master has detail records. Cannot delete or modify. Field value out of lookup table range.

Again, Delphi doesn’t tell you the name of the table where Delphi doesn’t tell you which field has the bad value. Also,
the detail records reside. (There could be more than one.) Delphi doesn’t provide native support for the Lookup access
30 July 1997 Delphi Informant
Columns & Rows
and Lookup type options described previously. There is no sources. This catalog will support complex constraints pro-
automated way to display the lookup table, nor to fill in grammed in SQL and host languages. These constraints
additional values based on matching field names. will serve as business rules that function across different
data sources, and for any application using that catalog.
In general, don’t bother setting up table lookups unless With this feature, Borland implements the theory behind
you plan to manipulate data with the Database Desktop or Paradox’s ValChecks in a far more powerful and flexible
Paradox itself. The RI features described previously pro- way that’s independent of file formats.
vide the same validation as an entered value in a lookup
table. To display an actual lookup list, use a DBLookupComboBox Next Time
component instead of a TDBEdit component. The ListSource The next article in this series will examine the Paradox file
and ListField (Delphi 3) or LookupSource and LookupField format’s encryption and security mechanisms, and how the
(Delphi 1 and 2) properties allow you to bind this compo- BDE manages passwords for Paradox tables. It will also dis-
nent to a lookup table. You even have explicit control over cuss table-language options that allow you to define tables
the fields displayed in the drop-down list, as well as the with different character sets and sort orders. D
size of the list.

Should You Use ValChecks and RI with Delphi? Dan Ehrmann is the founder and President of Kallista, Inc., a database and
Although the Picture ValCheck exists in the Paradox file Internet consulting firm based in Chicago. He is the author of two books on
format, Delphi ignores it, and newer, better-integrated fea- Paradox, and is a member of Team Borland and Corel’s CTech. Dan was the
Chairman of the Advisory Board for Borland’s first Paradox conference, which
tures have replaced its functionality. The situation for evolved into the current BDC. He has worked with the Paradox file format for
Table Lookup is similar; Delphi and the BDE support it, more than 10 years. Dan can be reached via e-mail at [email protected].
but RI and built-in components have replaced it.

While Delphi and the BDE support the Minimum and


Maximum ValChecks, you can also set minimum and maxi-
mum properties at the TField level. When you do this,
Delphi provides more-informative error messages. For this
reason, many developers don’t use these ValChecks, either.

Delphi fully supports the Default and Required ValChecks.


It provides analogs in the DefaultExpression and Required
properties for TField objects, so you can set these proper-
ties in either place. However, as I noted previously, if you
set the TField.Required property to True, and don’t set the
underlying Required ValCheck, you must write your own
code in the OnValidate event to trap for a blank field.

RI is an essential part of any database definition. It links


normalized tables to ensure that the data in each table
remains consistent and fully synchronized. It’s unfortunate
that the Paradox file format doesn’t support all RI options,
but the available support shouldn’t be ignored.

And in the Future?


Borland is adding advanced database features to Delphi
and the BDE. For example, Delphi 3 supports the follow-
ing new properties on TField-descendent objects:
■ CustomConstraint, to specify and test application-specific

constraints imposed on a field’s value, using SQL-based


search expressions. For example: Value >= 0 AND Value
<= 100.
■ ConstraintErrorMessage, to specify a custom error mes-

sage when the constraint is violated.


■ ImportedConstraint, a read-only property that holds
server-based constraints.

Borland also intends to enhance a BDE-based, centralized


database catalog that can draw from heterogeneous data

31 July 1997 Delphi Informant


DBNavigator
Delphi 2 / Delphi 3

By Cary Jensen, Ph.D.

Cached Updates: Part III


The OnUpdateRecord and OnUpdateError Event Handlers

In the past two issues, this column has explained the use of cached updates in
Delphi 2 and 3. This third and final installment of the series will look at event
handlers related to cached updates: OnUpdateRecord and OnUpdateError.

A Quick Recap You might remember that there are four pri-
Update caching is a mechanism by which all mary advantages to using cached updates:
changes made to a DataSet are stored locally, ■ a decrease in network traffic,
then applied simultaneously using the ■ an increase in performance,
ApplyUpdates method of a DataSet or ■ more user interface options, and
Database component. Cached updates are ■ greater programmatic control over the
enabled by setting a DataSet’s CachedUpdates posting of individual records.
property to True. The cached edits are applied
only specifically to the corresponding This month’s DBNavigator will concentrate
DataSet; closing a table, or setting on the final advantage, programmatic control
CachedUpdates to False without actually over applying updates.
applying the updates, cancels all cached edits.
Using Cache-Related Event Handlers
When updates are being cached, you can For those instances where you want complete
offer editing options that would otherwise be control over the application of cached
unavailable. For example, you can permit the updates, the DataSet class provides two event
user to view all edited records in the cache, properties: OnUpdateRecord and
including those that have been deleted. Also, OnUpdateError. From OnUpdateRecord, you
you can display both the original and new replace Delphi’s attempt to update the records
field values for records that have been modi- with your own; in other words, you supply
fied. Finally, you can permit the user to revert completely customized update behavior.
any cached edit, restoring the record to its OnUpdateError is triggered if an exception is
original state. raised during the attempt to update a record,
whether the exception originated from
Last month, we looked at the UpdateSQL Delphi’s own update process, or from
component. This component can be used OnUpdateRecord.
with a DataSet to allow a user to edit read-
only DataSets, such as SELECT queries that Using OnUpdateRecord. As mentioned, if
make use of the DISTINCT keyword. The you need to define custom update behavior,
UpdateSQL component permits you to define you assign an event handler to the
up to three SQL queries — one each for OnUpdateRecord event property. However,
delete, insert, and modify updates. These SQL although this series’ preceding two articles
statements can be quickly and easily con- demonstrated how to turn on cached updates
structed using the UpdateSQL Editor, a com- with an open table, the technique can’t be
ponent editor for UpdateSQL components. used with OnUpdateRecord. Specifically, if

32 July 1997 Delphi Informant


DBNavigator
Value Description cally, the updates are being applied to the current record of
ukModify Current record has been modified. this DataSet. Because DataSet controls which record is cur-
ukInsert Current record is newly inserted. rent when OnUpdateRecord is executing, it’s extremely
ukDelete Current record is scheduled for deletion. important not to perform any record navigation during the
execution of OnUpdateRecord. (This same caution applies
Figure 1: The values of UpdateKind and their meanings.
to OnUpdateError.)
Value Description
uaFail Record couldn’t be updated; raise an
The second parameter, UpdateKind, identifies the type of
exception and abort updating. (This is update that needs to be applied. This parameter has three possi-
the default value.) ble values, as shown in Figure 1.
uaAbort Same as uaFail, except that a silent
exception is raised, meaning that no The third parameter, UpdateAction, informs the Query com-
error message is displayed to the user. ponent of what you’ve done with the current record. There
uaSkip Current record was not updated. are five valid values for this property, as shown in Figure 2.
Continue applying updates.
The default value of UpdateAction is uaFail. This value causes
uaRetry Not meaningful in an OnUpdateRecord
event. Used for OnUpdateError to make the entire update to fail. Therefore, if you are successful in
another attempt at updating a record updating the current record, it’s imperative that you assign a
whose previous attempt generated value of uaApplied.
an exception.
uaApplied The event handler successfully updated The use of the OnUpdateRecord event handler is demonstrat-
the record. ed in the CACHE5.DPR project, shown in Figure 3. The
Figure 2: The values of UpdateAction and their meanings. DataSet being cached in this example is a Query. In addition,
a Table component appears on this form. This table points to
the same table as the Query, and is used to perform the
updates for individual records when the cached update is
applied. Also, the Query’s RequestLive parameter doesn’t need
to be set to True, because the updates will occur via the
OnUpdateRecord event handler, which edits the Table, not the
Query. In fact, leaving the Query’s RequestLive property set to
False ensures that updates are performed only within a
cached-update mode.

Figure 4 shows the OnUpdateRecord event handler assigned


to the Query. It includes a case statement to test the value of
UpdateKind. If the current record is being inserted, and a
Figure 3: The CACHE5 project demonstrates the use of new record is added to the table, then the fields of the Query
OnUpdateRecord and OnUpdateError.
are assigned to the fields of the table, and the record is post-
you open a table, then set its CachedUpdates property to ed. If the record has been modified, then the existing record
True, code associated with OnUpdateRecord won’t be exe- is located in the table, and its fields are updated. If the
cuted. However, if you set CachedUpdates to True at design record has been deleted, again the corresponding record is
time, or before opening a table (either by setting its Active located in the table and deleted. If the actions of this event
property to True, or by calling the table’s Open method), handler don’t raise an exception, the final statement sets the
the table’s OnUpdateRecord event handler will execute UpdateAction parameter to uaApplied. If an exception is
properly. This behavior is not a bug, but rather an artifact raised, the UpdateAction’s default value of uaFail remains
of how OnUpdateRecord is implemented. unchanged, causing the update to fail.

In the following example, a Query component will be cached, Using OnUpdateError. If you create an OnUpdateError event
rather than a Table component. With a Query component, handler, it will be executed if the attempt to update a particular
OnUpdateRecord is triggered whether it is set before or after record fails. This is true whether the update is being performed
the activation of the Query. Consequently, the following by the default DataSet behavior, an UpdateSQL object, or code
example can be similar to the preceding examples. attached to OnUpdateRecord. Unlike OnUpdateRecord, which
doesn’t work properly with Table components under all condi-
Here’s the declaration for the OnUpdateRecord event handler: tions, OnUpdateError works properly for any DataSet.

procedure(DataSet: TDataSet; UpdateKind: TUpdateKind; The following is the declaration for the OnUpdateError
var UpdateAction: TUpdateAction)
event handler:
The first parameter, DataSet, identifies the DataSet com- procedure(DataSet: TDataSet; E: EDatabaseError;
ponent to which updates are being applied. More specifi- UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);

33 July 1997 Delphi Informant


DBNavigator
procedure TForm1.Query1UpdateRecord(DataSet: TDataSet; procedure TForm1.Query1UpdateError(DataSet: TDataSet;
UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction); E: EDatabaseError; UpdateKind: TUpdateKind;
var var UpdateAction: TUpdateAction);
i: Integer; var
begin BdeError: PChar;
// Post pending cached edits. NewVal: string;
if DataSet.State in [dsEdit,dsInsert] then begin
DataSet.Post; GetMem(BdeError, 1024);
// Update the record. try
if UpdateKind = ukInsert then BDE.DbiGetErrorString(DBIERR_KEYVIOL,BdeError);
begin // Test if a key violation message is in the exception.
Table1.Insert; // If a key violation occurred, permit the user
for i := 0 to Table1.FieldDefs.Count - 1 do // to enter a new key.
if DataSet.Fields[i].Value <> Null then if E.message = copy(StrPas(BdeError),1,
Table1.Fields[i].Value := DataSet.Fields[i].Value; Length(E.message)) then
try begin
Table1.Post; NewVal := DataSet.Fields[0].NewValue;
UpdateAction := uaApplied; if InputQuery('Key violation',
except 'Enter new key',NewVal) then
Table1.Cancel; begin
raise; DataSet.Edit;
end; DataSet.Fields[0].Value := NewVal;
end DataSet.Fields[0].NewValue := NewVal;
else UpdateAction := uaRetry;
// Not an insert. Locate the existing record. end
if Table1.Locate('CustNo', else
DataSet.Fields[0].OldValue,[]) then // Use uaAbort since the user has clicked Cancel.
case UpdateKind of // No error message is necessary.
ukModify: UpdateAction := uaAbort;
begin end
Table1.Edit; else
for i := 0 to Table1.FieldDefs.Count - 1 do ; // The default UpdateAction is uaFail.
if Table1.Fields[i].Value <> // Permit the update to fail.
DataSet.Fields[i].Value then finally
Table1.Fields[i].Value := FreeMem(BdeError);
DataSet.Fields[i].Value; end;
try end;
Table1.Post;
UpdateAction := uaApplied; Figure 5: The OnUpdateError event handler of the CACHE5 project.
except;
Table1.Cancel; attempt to post the current record again, either through its
raise; internal code, or by calling your OnUpdateRecord event han-
end;
dler again.
end;
ukDelete:
begin The use of OnUpdateError is demonstrated in the CACHE5
Table1.Delete; project (see Figure 5). Here the event handler is looking for a
UpdateAction := uaApplied; specific type of error: a key violation. If this error is encoun-
end;
tered, the user can assign another key value, after which the
end;
end; update to the record is retried.
Figure 4: The OnUpdateRecord event handler assigned to the
Within this event handler, the invalid key value is stored in
Query.
the variable NewVal. This value is then displayed in an
As with OnUpdateRecord, OnUpdateError is passed parameters InputQuery dialog box, which allows the user to enter a cor-
for a DataSet, an UpdateKind, and an UpdateAction. These rect key value.
parameters contain the same information as the correspond-
ing parameters for OnUpdateRecord. In addition, the E para- Once the value is entered, the DataSet for which the event
meter points to the exception raised during the failure. You handler is executing is placed into the dsEdit state using the
use this exception to determine how to handle the error. Edit method, and the new value is assigned to both the Value
and NewValue properties of the key field. The
Unlike OnUpdateRecord, where it makes no sense to use the OnUpdateRecord event handler is then re-triggered by setting
UpdateAction value of uaRetry, uaRetry plays an important role the UpdateAction parameter to uaRetry.
in handling update errors. Specifically, in addition to aborting
the update or skipping the current record, you can also make If you want to permit a user to edit changes to a record
any changes necessary to the record, then attempt to update it being posted during a cached update, there are two reasons
again. Setting UpdateAction to uaRetry causes the DataSet to for caution:

34 July 1997 Delphi Informant


DBNavigator
■ If the user happens to leave the workstation during the Actually, the call to Apply encapsulates calls to SetParams,
update, a transaction may be left open, awaiting the which performs the run-time binding of the query parame-
user’s return. ters, followed by ExecSQL. The only time you would want to
■ With the preceding code example, I’ve found that if the use SetParams and ExecSQL is when a particular query
user enters a second invalid key in response to an initial includes parameters in addition to the defaults.
invalid key, the record can’t be posted, and an exception
will be raised when a valid key is entered. This behavior For example, to perform a ukDelete action on an orders
may be due to a bug in the cached-updates feature. table without deleting records that have shipped within the
last month, you could include — in the DELETE query —
Consequently, the best approach may be to use OnUpdateError a parameter used in the WHERE clause to test the ship-
to copy any invalid record to a temporary table for manual ping-date field. Then, when applying the updates, you
processing by an administrator, setting the UpdateAction para- would begin by calling SetParams to perform the default
meter to uaSkip when the record has been copied. run-time parameter binding, then assign an appropriate
date value to your date parameter, and finally call ExecSQL.
Alternatively, you can either validate and post the updated record
from within OnUpdateError, or use an UpdateSQL object to If the SQL statements you assign to your UpdateSQL compo-
update the record instead of an OnUpdateRecord event handler. nents don’t use aliases (DatabaseNames), you’ll also need to assign
Regardless, you should rigorously test any OnUpdateError event UpdateSQL’s DataSet property before calling Apply or SetParams.
handler if you plan to use it for correcting invalid records. DataSet is a run-time property that points to the DataSet affected
by the SQL statements. This permits the UpdateSQL compo-
Executing UpdateSQL Queries from OnRecordUpdate nent to use the Database being used by the DataSet.
In the preceding example of the OnUpdateRecord event han-
dler, Table methods such as Locate, Insert, and Delete were used The use of multiple UpdateSQL components is demonstrated
to perform the updates of the cached edits. Furthermore, you in the project CACHE6, shown in Figure 6. This project
learned that the UpdateObject property of a DataSet is ignored includes a read-only query that joins two tables. These tables,
if a procedure is assigned to OnUpdateRecord. This doesn’t CUSTOLY1.DB and RESERVA1.DB, are copied from the
mean, however, that you can’t use an UpdateSQL object with CUSTOLY.DB and RESERVAT.DB tables supplied with
an OnUpdateRecord event handler. In fact, it’s possible, and in Delphi, from within the OnCreate event handler for the
some cases highly desirable, to assign one or more UpdateSQL main form.
components from your code to OnUpdateRecord.
Also from within the OnCreate event handler, Query1 is
The most common reason for using UpdateSQL compo- assigned to the DataSet properties of both UpdateSQL
nents from within OnUpdateRecord is that the DataSet is a components. The following SQL statements are associated
read-only query that contains a join between two or more with Query1:
tables. A single UpdateSQL component can’t be used to
update both tables; you need one UpdateSQL component SELECT d.ResNo, d.EventNo, d.CustNo, d.NumTickets,
d.Amt_Paid, d.Pay_Method, d.Card_No, d.Card_Exp,
for each table that needs updating. d.Pay_Notes, d.Purge_Date, d.Paid,
d1.Last_Name, d1.First_Name, d1.VIP_Status,
When two or more UpdateSQL components are in use d1.Address1, d1.Address2, d1.City, d1."State/Prov",
d1.Post_Code, d1.Country, d1.Phone, d1.Fax,
(unlike when they’re used singly), don’t use the UpdateObject d1.EMail, d1.Remarks
property of the DataSet. Instead, call methods of the FROM "Reserva1.db" d, "Custoly1.db" d1
TUpdateSQL class to execute the queries associated with the WHERE (d1.CustNo = d.CustNo)

various UpdateSQL components.

The most common technique is to call the UpdateSQL


method Apply, which has the following declaration:

procedure Apply(UpdateKind: TUpdateKind);

When you call Apply, it executes the UpdateSQL statement


associated with the type of update. For example, if you pass
this method a parameter of ukDelete, the DeleteSQL state-
ment of the specified UpdateSQL component is executed.

The other alternative is to call SetParams, followed by ExecSQL.


The following are the declarations of these two methods:
procedure SetParams(UpdateKind: TUpdateKind);
procedure ExecSQL(UpdateKind: TUpdateKind); Figure 6: The CACHE6 project demonstrates the use of two
UpdateSQL components to update the result set of a join.

35 July 1997 Delphi Informant


DBNavigator
procedure TForm1.FormCreate(Sender: TObject); UPDATE "Custoly1.db"
var SET "Custoly1.db"."Last_Name" = :"Last_Name",
OldTable: TTable; "Custoly1.db"."First_Name" = :"First_Name",
NewTable: TTable; "Custoly1.db"."VIP_Status" = :"VIP_Status",
begin "Custoly1.db"."Address1" = :"Address1",
OldTable := TTable.Create(Self); "Custoly1.db"."Address2" = :"Address2",
NewTable := TTable.Create(Self); "Custoly1.db"."City" = :"City",
try "Custoly1.db"."State/Prov" = :"State/Prov",
OldTable.DatabaseName := 'DBDEMOS'; "Custoly1.db"."Post_Code" = :"Post_Code",
OldTable.TableName := 'CUSTOLY.DB'; "Custoly1.db"."Country" = :"Country",
NewTable.Tablename := 'CUSTOLY1.DB'; "Custoly1.db"."Phone" = :"Phone",
NewTable.DatabaseName := 'DBDEMOS'; "Custoly1.db"."Fax" = :"Fax",
NewTable.BatchMove(OldTable,batCopy); "Custoly1.db"."EMail" = :"EMail"
NewTable.AddIndex('','CustNo',[ixPrimary, ixUnique]); WHERE "Custoly1.db"."CustNo" = :"OLD_CustNo"
OldTable.TableName := 'RESERVAT.DB';
NewTable.Tablename := 'RESERVA1.DB'; Figure 9: The ModifySQL property of UpdateSQL2.
NewTable.BatchMove(OldTable,batCopy);
NewTable.AddIndex('',' ResNo',[ixPrimary, ixUnique]);
Query1.DatabaseName := 'DBDEMOS'; INSERT INTO "Custoly1.db"
Query1.Open; ("Custoly1.db"."Last_Name", "Custoly1.db"."First_Name",
UpdateSQL1.DataSet := Query1; "Custoly1.db"."VIP_Status", "Custoly1.db"."Address1",
UpdateSQL2.DataSet := Query1; "Custoly1.db"."Address2", "Custoly1.db"."City",
finally "Custoly1.db"."State/Prov", "Custoly1.db"."Post_Code",
OldTable.Free; "Custoly1.db"."Country", "Custoly1.db"."Phone",
NewTable.Free; "Custoly1.db"."Fax", "Custoly1.db"."EMail")
end; VALUES
end; (:"Last_Name", :"First_Name", :"VIP_Status", :"Address1",
:"Address2", :"City", :"State/Prov", :"Post_Code",
Figure 7: The OnCreate event handler for the CACHE6 project’s
:"Country", :"Phone", :"Fax", :"Email")
main form.
Figure 10: The InsertSQL property of UpdateSQL2.
UPDATE "Reserva1.db"
SET EventNo = :EventNo,
CustNo = :CustNo, autoincrement fields can’t be modified. Your SQL statements
NumTickets = :NumTickets,
Amt_Paid = :Amt_Paid,
must take this into account.
Pay_Method = :Pay_Method,
Card_No = :Card_No, The ModifySQL property of UpdateSQL1 contains the
Card_Exp = :Card_Exp,
Purge_Date = :Purge_Date, SQL statements shown in Figure 8. Here’s the InsertSQL
Paid = :Paid property from the same component:
WHERE ResNo = :OLD_ResNo
INSERT INTO "Reserva1.db"
Figure 8: The ModifySQL property of UpdateSql1. (EventNo, CustNo, NumTickets, Amt_Paid, Pay_Method,
Card_No, Card_Exp, Purge_Date, Paid)
The code in Figure 7 is the OnCreate event handler for the VALUES
(:EventNo, :CustNo, :NumTickets, :Amt_Paid, :Pay_Method,
CACHE6 project’s main form. When the cached updates are :Card_No, :Card_Exp, :Purge_Date, :Paid)
applied, the OnUpdateRecord event handler calls the Apply
methods of the UpdateSQL1 and UpdateSQL2 components. The DeleteSQL property of UpdateSQL1 is shown here:
Here’s the OnUpdateRecord event handler for the Query: DELETE FROM "Reserva1.db"
WHERE ResNo = :OLD_ResNo
procedure TForm1.Query1UpdateRecord(DataSet: TDataSet;
UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
begin Let’s now consider the SQL statements from UpdateSQL2. The
UpdateSQL1.Apply(UpdateKind); ModifySQL property is shown in Figure 9. Finally, Figure 10 is
UpdateSQL2.Apply(UpdateKind); the InsertSQL property from UpdateSQL2.
UpdateAction := uaApplied;
end;
Conclusion
As you can see, the event handler is very simple. Cached updates greatly increase the number of options you
have when it comes to editing records. In addition, they can
Probably the hardest part of using UpdateSQL components generally improve your application’s performance. In its
from within OnUpdateRecord is ensuring the queries associat- simplest case — that of editing a single table — cached
ed with the component make sense. While this may seem updates are easy to employ. Even when your caching needs
obvious, it can be much harder than you think. In CACHE6, are complex, however, Delphi’s cached-update capabilities
for instance, the UpdateSQL2 component, which is used to provide the tools necessary to get the job done right. D
update the CUSTOLY1.DB table, does not include a
DeleteSQL query. As you can imagine, the fact that a reserva-
tion is deleted for a customer doesn’t mean that the user also The files referenced in this article are available on the Delphi
wants to delete the customer. Likewise, the memo fields and Informant Works CD located in INFORM\97\JUL\DI9707CJ.

36 July 1997 Delphi Informant


DBNavigator

Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database


development company. He is author of more than a dozen books, including
Delphi In Depth [Osborne/McGraw-Hill, 1996]. Cary is also a Contributing Editor
of Delphi Informant, as well as a member of the Delphi Advisory Board for the
1997 Borland Developers Conference. For information concerning Jensen Data
Systems’ Delphi consulting and training services, visit the Jensen Data Systems
Web site at http://gramercy.ios.com/~jdsi. You can also reach Jensen Data
Systems at (281) 359-3311, or via e-mail at [email protected].

37 July 1997 Delphi Informant


Sights & Sounds
Delphi 2 / Object Pascal

By Peter Dove and Don Peer

Optimizing Graphics
Delphi Graphics Programming: Part V

S peed is a primary concern in graphics programming, and optimization is


one of the most studied areas of games programming. This month, we’ll
discuss several methods of optimization, and apply them to our example pro-
gram (see Figure 1).

To keep TGMP as object-oriented as possi- Multiplication and Division


ble, we’ll exclude some of the more “exotic” The first optimization method we’ll cover is a
optimization methods. Our goal is to quick way to divide and multiply integers.
increase the speed that TGMP rotates the Using the shl and shr operators enables you
Shaded Textured Cube, at its default posi- to shift integers bitwise, to the left or right. If
tion, by 50 percent. So first, we’ll optimize you bit-shift an integer 1 to the left, you mul-
the DIB class, then TGMP. tiply its value by 2 (binary is base 2). If you
bit-shift an integer 1 to the right, you divide
Note that optimization involves knowing where its value by 2.
to optimize. A function called only once dur-
ing an object’s life is probably not worth opti- Bit-shifting is quick — it only takes one clock
mizing. However, a method that’s called cycle of the processor to perform a bit-shift.
repeatedly is worthy of attention. An extreme Multiplication and division operations will
example is a deeply nested loop. If the inner take many times longer depending on the
loop contains inefficiencies, they’re magnified processor you use. Here’s an unoptimized line
in exact relation to the number of times the of code, and its optimized equivalent using a
loop is executed. bit-shift operation in place of conventional
multiplication (from the DrawHorizontalLine
procedure in DIB16.PAS):
{ Previous statement. }
BasePointer := Pointer(FScanWidthArray[Y] +
(X * 2));

{ Optimized statement. }
BasePointer := Pointer(FScanWidthArray[Y] +
(X shl 1));

Two Timing
One of the DIB class’ main tasks is to clear
the backpage. Unfortunately, the backpage is
cleared only one pixel at a time. Because a
pixel is 16 bits of data, and the largest inte-
ger size accessible in Delphi 2 is 32 bits, this
begs the question, “How can we clear the
Figure 1: The sample application — times six. backpage in 32-bit hits rather than in 16-bit

38 July 1997 Delphi Informant


Sights & Sounds
{ Private section of the class. }
procedure TDIB16bit.ClearBackPage(Color : Word);
FScanWidthArray : array [0..800] of Integer;
var
X, XColor : Integer;
{ Place at the end of the Create constructor. }
BasePointer : ^Integer;
for X := 0 to 800 do
begin
FScanWidthArray[x] :=
{ Loop through bitmap, setting every pixel
(FScanWidth * X) + Integer(FPointerToBitmap);
to Color parameter. }
BasePointer := FPointerToBitmap;
This new optimized line in the SetPixel procedure now uses
XColor := Color and (Color shr 16);
the lookup table:
// Optimization: Shift division.
// Replace div 2 with shr 1. { Using a lookup table and bit-shifting to increase speed. }
for X := 0 to (((FScanWidth shr 1) * BasePointer := Pointer(FScanWidthArray[Y] + (X shl 1));
(FBHeader.bmiHeader.biHeight))-1) shr 1 do
begin
BasePointer^ := XColor;
You probably noticed we also included another optimiza-
{ Inc increments a pointer by the size of the type that tion, converting:
the pointer points to; in other words, 4 bytes. }
Inc(BasePointer); (X * 2)
end;
end;
to:
Figure 2: The new, optimized ClearBackPage procedure.
(X shl 1)
hits?” Theoretically, this would cut the operation time in
half. Incidentally, the ClearBackPage procedure is also a Now let’s optimize TGMP. The optimized code for
good candidate for optimization; it’s called numerous DIB16.PAS is available for download (see end of the article
times and consumes large chunks of program time. for details).

So how do we do it? Recall in Part IV, we talked a lot about Reciprocals


bit-shifting. Figure 2 shows the fully-commented There are still many ways of optimizing the code to obtain
ClearBackPage procedure with all optimizations in place. more speed. In this vein, we’ll further optimize the GMP
Note that BasePointer is now a pointer to a 32-bit integer unit and the TGMP class.
value. To write two pixels at a time, we move the Word Color
into the top 16 and bottom 16 bits of a 32-bit integer. The The floating-point processor doesn’t take the same amount of
new XColor variable is 32-bit, so we perform an and opera- time for all calculations. For example, floating-point division
tion on Color, with Color shifted to the left 16 bits. We takes much longer than floating-point multiplication. Thus,
incorporated bit-shifting optimization methods with the bit- we should favor the use of floating-point multiplication
shifting, replacing the two division operations. Also, we are wherever possible. A good way to do this is by using recipro-
“blitting” 32 bits at a time, instead of 16 bits. cals. The reciprocal of a number is 1 divided by that number.
For example, the reciprocal of 31 is 1/31, or 0.032258.
Lookup Tables
Lookup tables offer a quick way to pre-calculate multiple vari- Before optimization, the RemoveBackfacesAndShade procedure
ables and place them into an array. Thus, the program only continued the following:
has to view a place in memory for a result, rather than calcu- R := Round(((255 - GetRValue(AnObject.Color)) / 31) *
late it. In this section, we’ll implement lookup tables in the Intensity) + GetRValue(AnObject.Color);
G := Round(((255 - GetGValue(AnObject.Color)) / 31) *
SetPixel and DrawHorizontalLine procedures of the DIB class.
Intensity) + GetGValue(AnObject.Color);
B := Round(((255 - GetBValue(AnObject.Color)) / 31) *
This unoptimized code from SetPixel is called every time a Intensity) + GetBValue(AnObject.Color);

pixel is drawn in the


texture-mapping modes: Here’s the optimized code which uses reciprocals:

Integer(BasePointer) := R := Round(((255 - GetRValue(AnObject.Color)) * 0.032258) *


Integer(FPointerToBitmap) + Intensity) + GetRValue(AnObject.Color);
(Y * FScanWidth) + (X * 2); G := Round(((255 - GetGValue(AnObject.Color)) * 0.032258) *
Intensity) + GetGValue(AnObject.Color);
B := Round(((255 - GetBValue(AnObject.Color)) * 0.032258) *
Also consider the following calculation. It only returns the Intensity) + GetBValue(AnObject.Color);
beginning of the DIB class, then the Y offset into it:
Fixed-Point Math
Integer(FPointerToBitmap) + (Y * FScanWidth) Integer math is much faster than floating-point math (espe-
cially when it comes to division), but how can we use
An array of pre-calculated Y positions can do a better job. integer math when we need to handle decimal values? We
We easily implement the arrays by adding this code to the can simulate floating-point precision with integer math,
DIB class: using a number of bits to represent the fraction.

39 July 1997 Delphi Informant


Sights & Sounds

{ Unoptimized code without floating-point division. }


point number (by shifting left eight positions), you would take
if RenderMode = rmSolidTexture then TextXIncr and TextYIncr, then shift right eight bits to normalize
begin it to a fixed-point number.
for Y := 0 to 479 do begin
if YBuckets[Y].StartX = -16000 then
continue; To reconvert a fixed-point number into a normal integer,
Length := (YBuckets[Y].EndX - YBuckets[Y].StartX) + 1; simply reshift it right by the precision you selected (in this
TextXIncr := ((TextureBuckets[Y].EndPosition.X —
TextureBuckets[Y].StartPosition.X)) /
case, eight bits). Of course, the problem with fixed-point
Length ; math is that the highest value available is the highest value
TextYIncr := ((TextureBuckets[Y].EndPosition.Y — (32), minus precision (24 bits in this case).
TextureBuckets[Y].StartPosition.Y)) /
Length ;
TextX := TextureBuckets[Y].StartPosition.X; Let’s look at an integer multiplication example that
TextY := TextureBuckets[Y].StartPosition.Y; assumes eight bits of precision. Let’s say we have a variable,
for I:=YBuckets[Y].StartX to YBuckets[Y].EndX do begin
if I < 0 then
X, with a value of 55; and a variable, Y, with a value of 10.
begin First we convert each into an 8-bit fixed-point value using
TextX := TextX + TextXIncr; the shl operator:
TextY := TextY + TextYIncr;
Continue;
end; 55 shl 8 = 14080
if I > Width then
begin
TextX := TextX + TextXIncr; and:
TextY := TextY + TextYIncr;
Continue;
10 shl 8 = 2560
end;
{ Use the FDib SetPixel method instead of the
Windows GDI SetPixel. } Next, we multiply:
FDib.SetPixel(I, Y, FCurrentBitmap[Round(TextX),
Round(TextY)]);
TextX := TextX + TextXIncr; 14080 * 2560 = 36044800
TextY := TextY + TextYIncr;
end;
end;
Then we normalize:
end; { if RenderMode = rmSolidTexture ...}
36044800 shr 8 = 140800
Figure 3: From the unoptimized RenderYBuckets procedure.

For example, let’s say you select 16 bits for the precision; you Last, we obtain the conventional integer value:
have just placed a decimal point in the middle of the 32-bit
integer. Using eight bits of a 32-bit integer to represent the 140800 shr 8 = 550
fraction provides a precision of 1/256.
which we can check using good ol’ multiplication:
Converting a normal integer value into a fixed-point value
is the easiest way to perform this optimization. The code in 10 * 55 = 550
Figure 3 is from the RenderYBuckets procedure. The Length
variable is a normal integer. To convert it (or any normal Now let’s look at division. This formula, (2 / 10) * 20 = 4, for
integer) into a fixed-point integer, shift the bits left by the example, just can’t be done with integer math. First, let’s eval-
number of bits of precision (e.g. eight) you have decided to uate the expression in the parentheses. Sixteen bits of preci-
use. You can see this effect with these two statements: sion is needed to ensure this example is correct. Convert 2 to
a fixed-point value:
TextXIncr := ((TextureBuckets[Y].EndPosition.X -
TextureBuckets[Y].StartPosition.X) shl 8) div Length ;
TextYIncr := ((TextureBuckets[Y].EndPosition.Y -
2 shl 16 = 131070
TextureBuckets[Y].StartPosition.Y) shl 8) div Length ;
The divisor, however, doesn’t need to be converted:
The start position is subtracted from the end position, and
the result shifted eight bits to the left. The divisor, Length, 131070 div 10 = 13107
remains untouched, i.e. it was not shifted. If you converted
length into a fixed point, the equation would suffer from Now we convert 20 into a fixed-point value:
incorrect scaling — the value would be too low. The oppo-
site occurs when using fixed-point multiplication — you 20 shl 16 = 1310700
must downscale the answer by eight bits.
The final equation is:
The results, TextXIncr and TextYIncr, are correctly formatted,
fixed-point numbers. If length were converted into a fixed- 13107 * 1310700 = 17179344900

40 July 1997 Delphi Informant


Sights & Sounds
if RenderMode = rmSolidTexture then with Object3D.PolyStore[P] do begin
begin if Z <> 0 then
for Y := 0 to Height do begin begin
if YBuckets[Y].StartX = -16000 then for I := 0 to NumberPoints - 1 do begin
continue; NewX :=
Length := (YBuckets[Y].EndX - YBuckets[Y].StartX) + 1; Point[I].X * PreCalCos - Point[I].Y * PreCalSin;
NewY :=
{ Floating-point to fixed-point. } Point[I].X * PreCalSin + Point[I].Y * PreCalCos;
TextXIncr := ((TextureBuckets[Y].EndPosition.X - Point[I].X := NewX;
TextureBuckets[Y].StartPosition.X) Point[I].y := NewY;
shl 8) div Length ; end;
TextYIncr := ((TextureBuckets[Y].EndPosition.Y - end;
TextureBuckets[Y].StartPosition.Y) end; { with }
shl 8) div Length ;
Figure 5: The Delphi with statement optimizes your code by
{ Turns TextX and TextY into fixed-point resolving a pointer (to Object3D.PolyStore[P] in this case) once
integers with 8 bits of precision. } for an entire block of code.
TextX := TextureBuckets[Y].StartPosition.X shl 8;
TextY := TextureBuckets[Y].StartPosition.Y shl 8;
for I:=YBuckets[Y].StartX to YBuckets[Y].EndX do begin Speed, speed, speed. Our original goal was to improve the
if I < 0 then engine’s speed by 50 percent. Did we achieve it? On one
begin
TextX := TextX + TextXIncr;
machine, the code from Part IV ran at 10 frames per second
TextY := TextY + TextYIncr; (fps) with fully-lighted texturing. The new, optimized code
Continue; runs at 14 fps, so we’re not quite there yet. However, many
end;
if I > Width then
optimizations remain. For now, we’ll leave the final fps up to
begin you; look at the different optimization sections provided and
TextX := TextX + TextXIncr; see if you can apply them to other portions of TGMP. And
TextY := TextY + TextYIncr;
don’t worry, we’ll continue to implement optimizations in
Continue;
end; our next article.

{ To turn a fixed point integer to a normal integer,


It’s in the cards. Note that the code will only run as fast as
shift the bits right by the same amount shifted
left when turning them into fixed point integer. } your graphics card can run. On a system with a Matrox
Mystique graphics card, the fully-lighted textured cube ran
{ Uses the FDib SetPixel method instead of at an amazing 30 fps. The commercial version of TGMP
the Windows GDI SetPixel. }
FDib.SetPixel(I, Y, FCurrentBitmap[TextX shr 8,
(due for release soon), ran at over 70 fps. Optimization
TextY shr 8]); software has shown most of the overhead in TGMP is con-
sumed by the blitting-to-screen process. Thus, the faster
{ Addition of fixed point numbers is the same as
normal integers and it is the same with
your card, the faster the cube will spin. This can also be
subtraction of fixed point numbers. } proven by reducing the window size in ARTICLE5.EXE
TextX := TextX + TextXIncr; while it’s running. Each time you reduce the window size,
TextY := TextY + TextYIncr; the fps will increase proportionately.
end; { for }
end; { for }
end; { if RenderMode = rmSolidTexture } From our readers. A reader, Fred Mitchell, found that the
Figure 4: From the fully-optimized RenderYBuckets procedure.
CrossProduct function was wrong, and we have corrected it
accordingly. The RemoveBackfacesAndShade method was
Now correct the multiplication overflow: also altered to ensure it consistently refers to the new
PolyWorld array instead of sometimes referring to the
shr 16 = 262140 PolyStore or PolyWorld array. This was an oversight in the
upgrade to the World Coordinate System (from Part IV).
Next, convert it into a conventional number by shifting right
16 bits: Our Fifth Application
Essentially, our fifth application is the same as that pre-
262140 shr 16 = 4 sented in Part IV, with two major additions:
1) We added code to the Timer1.Timer event that analyzes
The fully-optimized code from the RenderYBuckets procedure, the frames per second, running a type constant counter
with additional comments, is shown in Figure 4. within the event. When the counter reaches 50, it evalu-
ates the time elapsed since it arrived at 50. The rest is
More on Optimization simple math. The result is displayed in the title bar to
Optimizing with with. Delphi supports other optimiza- indicate any speed improvements you may want to make.
tions, such as with its with statement (see Figure 5). Here, 2) The SetLightSourcePosition procedure in TGMP configures
the with statement has the compiler save a pointer to the light vector’s exact orientation. The method accepts
Object3D.Polystore[P], so the pointer doesn’t have to be two parameters: the position of the light in space, and its
evaluated each time through the for loop. direction, i.e. a point in space to which the light “looks.”

41 July 1997 Delphi Informant


Sights & Sounds

The complete source for our fifth application developed


with the TGMP component is available in ARTICLE5.PAS.

Conclusion
Aside from adding the camera coordinate system, next
month we’ll add more optimizations (so you can compare
those you devised with the ones we implemented), allow
for embedding sprites into the 3D engine, and change the
system to cope with displaying more than one object at a
time. Finally, we’ll provide the ability to display and build
the scene at design time. See you then. D

The files referenced in this article are available on the Delphi


Informant Works CD located in INFORM\97 \JUL\DI9707DP.

Peter Dove is a partner in Graphical Magick Productions, specialists in graphics,


training, and component development. He can be reached via the Internet at
[email protected].

Don Peer is a Technical Associate with Greenway Group Holdings Inc. (GGHI). He
can be reached via the Internet at [email protected].

42 July 1997 Delphi Informant


On the Net
Delphi 2 / Object Pascal / Internet / Intranets

By John Penman

NetCheck: Part II
Completing the 32-Bit Network Debugging Tool

I n the whirls of the Internet and intranets, developers must ensure their net-
work programs remain robust. A network debugging tool, therefore, is
essential for any developer building Internet and/or intranet applications.

In Part I of this series (presented in the May and preclude user interaction. Because Trace
Delphi Informant), we began examining descends from Sonar, it inherits this not-
NetCheck, a simple network debugging too-serious, but inconvenient, problem.
application that uses three, non-visual
Delphi components, each encapsulating a To overcome the effects of blocking, we’ll
well-known debugging service: use the TThread class to add multi-tasking
1) Sonar, a wrapper for the ping application; capability to Trace and Sonar. However,
2) EchoC, an echo client wrapper for the we’ll take a different approach when han-
Echo service, similar to ping; and dling the Echo service. To work asynchro-
3) Trace, which encapsulates the TraceRoute nously, EchoC uses the Winsock function,
service. Trace maps the route of the pack- WSAAsyncSelect. This permits you to work
ets between the sending and receiving with NetCheck while processing the back-
machines. This mapping can help find any ground, without using threads.
bottlenecks or “breaks” in the network.
Full of Echoes
In May, we implemented Sonar in NetCheck. The Echo service is used to verify the con-
This month, we’re extending the utility by nection between the target host and client
adding the EchoC and Trace components machine, and that the server is operating at
(see Figure 1). the application level. In contrast, Sonar and
other ping applications test the connection
Kicking Out the Jams only at the target machine’s network inter-
Recall that Sonar operates in blocking
face; this doesn’t indicate the target machine’s
mode, causing the user interface to “freeze”
operating system is functioning. The down-
side of using the Echo service is that an echo
server must be running on the target host.
Ping applications, including Sonar, do not
require such a server.

When the host responds positively to an


echo request using the Echo service, you
know the server is working with a viable
connection. (EchoS.pas is the source to
implement the EchoS component, used by
the echo server program, EchoServe.exe.
Because EchoS is similar to EchoC, we won’t
Figure 1: NetCheck showing Sonar, EchoC, and Trace at cover it in detail. EchoS.pas is available for
design time. download; see end of article for details.)

43 July 1997 Delphi Informant


On the Net
The operation of the echo
client is simple: It sends a
test message at predeter-
mined intervals to the
echo server, on port seven
(IPPORT_ECHO). The
server then reflects the test
message back to the client.
If the echo server doesn’t
respond, either the net-
work interface or target
machine’s operating system
is down, or both. Another
possibility is that the echo Figure 3: The controls on the Echo page allow you to change
server program hasn’t start- the default values before attempting an echo.
ed. When this happens,
Sonar can be used to verify The Echo service can use the Transmission Control Protocol
Figure 2: The Object Inspector the connection at the tar- (TCP), or User Datagram Protocol (UDP) to send messages.
showing EchoC’s published prop- get machine’s network TCP, a streaming protocol, guarantees reliable delivery of data,
erties. interface, to narrow the using a virtual circuit between the sender and receiver
number of possible causes. machines across the network. UDP, a connectionless protocol,
does not require a virtual circuit. Unlike TCP, UDP contains
The EchoC Component no error checking, and consequently, has lower overhead.
The TEchoC class descends from the TComponent class to
form the basis of the EchoC control (see Listing Four TCP is analogous to using registered snail mail to send infor-
beginning on page xx). The TEchoC.Create constructor mation safely, requiring the recipient to acknowledge receipt.
includes the CheckWS function to initialize WinSock.DLL UDP is similar to using regular snail mail; the recipient doesn’t
before use. If the .DLL isn’t available, EchoC posts an acknowledge receipt. In this case, EchoC only uses UDP; how-
error message, and closes NetCheck. ever, you can easily implement the TCP version of the Echo
service. (TCP is partially implemented in the EchoC and
EchoS components. I leave this enhancement to you.)
Two methods, Start and Stop, are declared in TEchoC ’s
public section:
Processing Echoes
public
The GetHost method configures a socket to transmit mes-
{ Public declarations } sages and receive message echoes. A socket is an endpoint
procedure Start; in a communication link, used to send data and listen for
procedure Stop;
incoming data.
The Start procedure calls GetHost to resolve a given host After GetHost creates the socket, Start calls AllocateHwnd
name before beginning the echo process. The Stop procedure to create the FWnd and FTWnd handles to invisible win-
halts the echo process. dows for the EchoEvent and TimerEvent event procedures,
respectively. When the socket receives notification that
You control EchoC’s behavior through its published proper- data is ready to read or send, the EchoEvent procedure
ties, PortNo, Interval, and NoEchoes. They allow you to speci- responds. To make triggering of EchoEvent possible,
fy a port, the time lapse between transmissions, and the WSAAsyncSelect is called through StartAsyncSelect to put
number of times to send a message, respectively. Although the socket, FSocketNo, into non-blocking mode. EchoC is
the Echo service uses the standard IPPORT_ECHO port to now ready to work asynchronously.
transmit messages, you can change it to any port number.
However, you must also change the echo server’s port; other- After sending a message, the socket waits in the background
wise, communication is not possible. for an echo reply. When the socket receives the data (from the
target host), Winsock sends an FD_READ notification. This
You can alter the values of published properties at design triggers EchoEvent to call GetData to read the data. EchoC
time (see Figure 2) or run time. For flexibility, these prop- then posts the echo reply to memEchoMsg, a Memo control on
erties are available through appropriate controls on the NetCheck’s Echo page. After an interval of Interval seconds,
Echo page in NetCheck (see Figure 3). For example, to Windows sends a WM_TIMER message. This triggers the
change the interval between transmissions from 0 to 999 TimerEvent method to call SetData to send another message.
seconds, use the UpDown component for Interval (secs). In
the same way, you can indicate the number of messages to Each time Windows triggers TimerEvent, TimerEvent com-
send in No of echoes. pares FWriteCount — a counter that is incremented with

44 July 1997 Delphi Informant


On the Net
Tracing Packets
Trace checks the connectivity and the route between two
machines, tracking any bottlenecks or “breaks” between client
and server. Trace can also be used to determine if a routing
problem is causing a network application failure. Although you
can’t solve any network problems that cause the packets to dis-
appear en route, you can obtain evidence of a routing problem.

Like all TraceRoute programs, Trace uses the Time To Live


mechanism (TTL) in the Internet Protocol (IP). TTL indi-
cates the number of hops a packet can travel before expiring.
Recall from Part I in May that a hop is a link between any
two machines on a path over the network. Twenty hops may
exist between the sending and target machines.
Figure 4: EchoC in action.
Let’s say a packet’s TTL is 32, and the target host is 33 hops
from the sending machine. This packet will expire before
reaching its destination. However, if a packet’s TTL is greater
than 32, it will probably reach that destination. All
TraceRoute (a.k.a. hopcheck) programs use this principle to
map the route between the sender and receiver.

When you start a trace, the initial TTL of an ICMP packet


is one. The first machine on the route receives it, and decre-
ments the packet’s TTL. When the router sees the packet’s
TTL is zero, it returns an error message, indicating the pack-
et’s TTL has expired.

Next, note the first machine’s address and send another packet
Figure 5: The Echo Server program responding to echo requests
with a TTL of two. The first machine decrements the TTL by
from NetCheck. one, then forwards the packet to the next machine when it
sees the TTL is non-zero. When the second machine receives
each transmission — with FNoEchoes. If FWriteCount the packet, it decrements the packet’s TTL by one. After see-
exceeds FNoEchoes, TimerEvent calls the following code in ing this packet’s TTL is zero, the second machine returns an
TimerEvent to halt transmission: error message. Again, note the address of the second machine
and increase the TTL by one to three. Continue this cycle of
if FWriteCount > FNoEchoes then
begin
sending the packet with increasing TTL until it reaches the
KillTimer(FTWnd,1); destination host, or dies because of a network problem.
DeallocateHWND(FTWnd);
FDone := True;
OnDoneEvent;
Using the TTL mechanism isn’t foolproof, because the
Exit; routes can vary between each packet sent. In spite of this,
end; TTL is a useful tool.

OnDoneEvent then posts a message to the Stop method to The Trace Component
halt the echo process. The Trace component was created by deriving the TTrace class
from the TSonar class. Therefore, Trace inherits TSonar’s Create
To use the EchoC component, select the Echo page in constructor. This method checks the status of the ICMP and
NetCheck and enter the target host’s name or IP address in Winsock DLLs. Create sets the TTL to a default value of 128,
Host name or IP address. Then, alter the default settings for a reasonable value to cover most routes (see Figure 6).
the port number, interval, and number of echoes. Finally,
enter the test message in the Edit control, edEchoTestMsg. TTrace uses TSonar’s GetHost method to obtain the address
of the target machine to trace. Like the Sonar component,
To start the echo process, click the Echo button. The Trace must initialize the TIPOptions and TICMPEchoReply
exchange of data immediately appears in the memEchoMsg records before starting a trace. (For more information on
Memo control (see Figure 4). Figure 5 shows the Echo this, refer to Part I.)
Server program responding to echo requests from NetCheck.
You can halt transmission at any time by clicking the Stop The heart of the Trace component is the DoTrace method.
button, which activates the Stop method. The asynchronous First, DoTrace obtains the addresses of the IcmpCreateFile,
nature of EchoC provides the freedom to cease the process. IcmpCloseHandle, and IcmpSendEcho functions, exported

45 July 1997 Delphi Informant


On the Net
type
TTrace = class(TSonar)
private
{ Private declarations }
FTimeToLive : Byte;
FIPFound, FHostFound : string;
procedure ResolveHost;
protected
{ Protected declarations }
pEchoReply : pIcmpEchoReply;
procedure DoTrace; virtual;
procedure Stats; override;
public
{ Public declarations }
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
procedure Trace;
published
{ Published declarations } Figure 7: The Trace page, after tracing a route between
property TimeToLive : Byte
www.informant.com and the sender.
read FTimeToLive write FTimeToLive default 128;
end;
method, which in turn, calls GetHost to obtain the target
Figure 6: The TTrace class. host’s address. Trace then calls DoTrace to send the packets.
Each time the Trace component receives data from a machine,
by the ICMP.DLL. Then, pEchoReply and FIPOptions are it posts a message to the memTraceMsg Memo control,
initialized, with the FIPOptions.TTL field set to 1. through the component’s Msg property (see Figure 7).
Before starting the IcmpSendEcho function, DoTrace creates a
You can press the Abort button to cancel the trace at any time.
handle for IcmpCreateFile. As with the calls to the ICMP and
However, Trace operates in blocking mode. This causes
Winsock DLLs, DoTrace checks the result of the
NetCheck’s user interface to be unresponsive, preventing you
IcmpCreateFile function. If IcmpCreateFile returns a value of
from using Abort. To enable the use of the Abort button, we
INVALID_HANDLE_VALUE, DoTrace aborts with an error
must put Trace to work in non-blocking mode. This is where
message, and exits to NetCheck.
threads can help.
A while loop executes the IcmpSendEcho function until the
control variable, Finished, is set to True by the following code:
Using Threads
We aren’t implementing true non-blocking versions of Sonar
if (pEchoReply.Status = IP_SUCCESS) or and Trace. The intent is to add multi-threading capability
(FIPOptions.TTL > FTimeToLive) then without changing these components. Sonar and Trace remain
Finished := True
blocking in nature; they’re placed on a thread separate from
else
Inc(FIPOptions.TTL); the primary thread, which is usually the user interface. Thus,
NetCheck’s interface can be used to perform other tasks. For
After the IcmpSendEcho function is executed, the code example, we can exercise Trace’s capabilities while simultane-
examines the pEchoReply.Status field. If it contains an ously using EchoC.
IP_TTL_EXPIRED_TRANSIT value, the packet’s TTL has
expired. Next, the application checks that the address returned Adding threading capability to Sonar and Trace without
by IcmpSendEcho is valid, then calls ResolveHost to resolve the compromising their integrity, however, involves breaking a
name of the machine that dispatched the error message. Then cardinal rule: I don’t use the Synchronize method of the
TTL is increased by 1, provided the value of FIPOptions.TTL TThread class. Any messages the components send to update
is less than that of FTimeToLive (set at design time). NetCheck’s Memo and ProgressBar components should be
done through Synchronize. I didn’t use the Synchronize
If pEchoReply.Status contains the value method because it would have required me to “hardwire” the
IP_REQ_TIMED_OUT, a time out has occurred, perhaps locations of these controls (i.e. Memo, Edit, etc.) from with-
because of heavy network traffic. When the ICMP packet in the Sonar and Trace components. Therefore, be careful
finally reaches the target host, IcmpSendEcho returns a value of when using Sonar and Trace components in non-blocking
IP_SUCCESS, then calls ResolveHost to determine the host’s mode. (In testing these components, I didn’t encounter any
name. Then, Stats is called to post the number of hops to problems by not using Synchronize.)
reach the host, and target the host’s name. Finally the Finished
flag is set to True to terminate the while loop. To add multi-threading capability to Trace, use Delphi’s New
Items dialog box to create a new class, TTraceThrd, in the
Mapping a Path TraceThrd unit (see Figure 8). The Create constructor initializes a
In NetCheck, select the Trace tab. First, enter the target host’s private copy of the TTrace class in FTrace. To make this
name or IP address, then click the Trace button to begin the TTraceThrd class available to NetCheck, declare Tracer in the
trace. The Trace button calls the Trace component’s Trace interface section of the TraceThrd unit, and add TraceThrd to

46 July 1997 Delphi Informant


On the Net
type program is available on the Web from http://www.tcp.chem.-
TTraceThrd = class(TThread) tue.nl/~tgtcmv and http://www.dephi32.com/apps.
private
{ Private declarations }
FTrace : TTrace;
protected The files referenced in this article are available on the Delphi
procedure Execute; override;
public
Informant Works CD located in INFORM\97\JUL\DI9707JP.
constructor Create(TTracer : TTrace; Name : string);
end;

Figure 8: The TTraceThrd class enables Trace to be non-blocking.

procedure TpdMain.bbtnTraceClick(Sender: TObject);


begin
with Trace1 do begin John Penman is the owner of Craiglockhart Software, which specializes in provid-
if Mode = Blocking then ing Internet and intranet software solutions. John can be reached on the Internet
begin at [email protected].
HostName := edTraceHost.Text;
Trace;
end
else
begin
TimeToLive := OldTimeToLive;
Tracer := TTraceThrd.Create(Trace1,
edTraceHost.Text);
end; Begin Listing Four — The TEchoC Class
end; type
end; CharArray = array[0..MaxBufferSize] of char;
Figure 9: Attach this code to the Trace button. TConditions = (Success, Failure, None);
TTransport = (TCP, UDP);
TEchoC = class(TComponent)
the uses clause in Main.pas. Next, add the code in Figure 9 to the private
Trace button. (These techniques are applicable to Sonar as well.) { Private declarations }
FParent : TComponent;
FStatus : TConditions;
Now when you need to abort a trace, click the Abort button. FVersion, FVersionDate, FComponentName,
It simply resets Trace’s TimeToLive property to 1, halting the FDeveloper : string;
FStatusWS, FOkay : Boolean;
trace completely. Before starting Trace in non-blocking FProgress : Integer;
mode, set the Mode property to Non blocking in the Fh_addr : pChar;
rgTraceMode RadioGroup control. FMsgBuf : CharArray;
function CheckWS : Boolean;
protected
Conclusion { Protected declarations }
FNoSent, FNoRecv, FNoEchoes, FMin, FMax, FAve,
With these enhancements, NetCheck is a basic debugging
FwMsg, FRTTSum : Word;
tool that can be used as-is, or extended to include more fea- FHostName, FHostIP : string;
tures. For example, you may want to include the option to FOnRecv, FOnNewData, FOnProgress,
FOnDone : TNotifyEvent;
select a TCP or UDP for the Echo service. Additionally, you FSocket : TSocket;
may want to enhance the interface by adding a pick list of FHwnd : THandle;
favorite hosts that could be stored in the Windows 95 or FSockAddr, FSockAddrIn : TSockAddrIn;
FAddress : DWord;
Windows NT 4.0 registry. FHost : PHostent;
FSocketNo : TSocket;
We covered the internals of a Delphi application for network FProtocol : PProtoEnt;
FService : PServent;
debugging. However, network debugging techniques is a large FReadCount, FWriteCount : Integer;
topic, far beyond the scope of this article. To help you, a list of FTransport : TTransport;
references is included here. (I particularly recommend Chapter FWnd, FTWnd: HWND;
FMessage, FMsg, FTestMsg : string;
13 in Windows Sockets Network Programming.) D FInterval, FEchoPortNo : Integer;
FDone : Boolean;
References procedure StartAsyncSelect;
procedure EchoEvent(var Mess : TMessage);
Chapman, Davis, Building Internet Applications with Delphi 2 message SOCK_EVENT;
[QUE, 1996]. procedure TimerEvent(var Mess : TMessage);
message WM_TIMER;
Dumas, Arthur, Programming Winsock [SAMS, 1995]. procedure OnRecvEvent;
Quinn, Bob, and David Shute, Windows Sockets Network procedure OnNewDataEvent;
Programming [Addison-Wesley, 1996]. procedure OnProgressEvent;
procedure OnDoneEvent;
Stevens, W. Richard, UNIX Network Programming [Prentice procedure SetUpAddress;
Hall, 1990]. procedure SetUpAddr;
Taylor, Don, et al., KickAss Delphi Programming, Chapters 4 procedure GetHost;
procedure SetPortNo(ReqdPort : Integer);
and 5 [Coriolis Group, 1996]. function GetPortNo : Integer;
Verbruggen, Martien. Source code for the demonstration ping

47 July 1997 Delphi Informant


On the Net
function GetMessage : string;
procedure SetMessage(ReqdMsg : string);
function GetData : string;
procedure SetData(DataReqd : string);
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
public
{ Public declarations }
procedure Start;
procedure Stop;
property Status : TConditions
read FStatus write FStatus default Success;
property Transport : TTransport
read FTransport write FTransport default UDP;
property Msg : string read FTestMsg write FTestMsg;
property StatusMsg : string read FMsg write FMsg;
published
{ Published declarations }
property Done : Boolean
read FDone write FDone default FALSE;
property Interval : Integer
read FInterval write FInterval default 1;
property PortNo : Integer
read FEchoPortNo write FEchoPortNo
default IPPORT_ECHO;
property HostName : string
read FHostName write FHostName;
property IPAddress : string read FHostIP write FHostIP;
property NoEchoes : Word
read FNoEchoes write FNoEchoes default 5;
property OnRecv : TNotifyEvent
read FOnRecv write FOnRecv;
property OnNewData : TNotifyEvent
read FOnNewData write FOnNewData;
property OnProgress : TNotifyEvent
read FOnProgress write FOnProgress;
property OnDone : TNotifyEvent
read FOnDone write FOnDone;
end;

End Listing Four

48 July 1997 Delphi Informant


At Your Fingertips
Delphi / Object Pascal

By Robert Vivrette

Displaying Shortened Pathnames


... and Other Brief Treats

T here are times when you may need to display a long pathname in a short
space; for example, the Panel caption in Figure 1. This panel has been
placed on the form with its Align property set to alClient. As a result, the space
available for the caption changes when the form is resized. When the width of
the form is reduced, the caption is truncated on the left and right.
Suppose, however, that you want to retain the DrawTextEx API call. We won’t be using the
right and left portions of the path, and elimi- function to actually draw text, but to modify
nate characters from the middle. The Win32 the string we send.
API provides this capability with the
The first few parameters of DrawTextEx are
the Canvas’ Handle (a Windows Device
Context), the string to display, the length of
that string (-1 calculates it for us), and the rec-
tangle defining the text area. Then comes a
combination of several flags, only three of
which interest us here. The first is
Figure 1: What will happen to the pathname when the form DT_PATH_ELLIPSIS, which eliminates some
is resized? characters in the string and replaces them with
procedure TForm1.FormResize(Sender: TObject); three dots (an ellipsis). The second is
var DT_MODIFYSTRING, which modifies the
B : array[0..255] of Char;
passed string so that we get an altered string
R : TRect;
begin back after the function returns. The last is
StrCopy(B,'C:\Program Files\Borland\Delphi 3.0\' + DT_CALCRECT, which is used to calculate
'Images\Buttons\ZoomIn.bmp');
the rectangle occupied by the text. We’re not
R := ClientRect;
InflateRect(R,-10,-10); interested in this value, but DT_CALCRECT
DrawTextEx(Canvas.Handle,B,-1,R, has the side effect of telling DrawTextEx not to
DT_PATH_ELLIPSIS or DT_MODIFYSTRING or DT_CALCRECT,nil);
Panel1.Caption := B;
draw the text. If we left out this flag, the path
end; would be displayed on the Panel Canvas, sepa-
Figure 2: The pathname-shortening technique. rate from the Caption. The result? The func-
tion modifies the passed string, making it a
shortened form of the original.

Figure 2 demonstrates the technique, and


Figure 3 shows the result. The InflateRect call
simply reduces the size of the rectangular area,
so the text has a bit of a “margin.” I use
StrCopy to copy from a constant, so that we
start with a fresh, undisturbed copy of the
string each time. (Remember — it’s modify-
Figure 3: The shortened pathname. ing the return value.)

49 July 1997 Delphi Informant


At Your Fingertips
Speeding TList Enumerated Types
Memory Have you ever created a set type and wanted to access the
Allocation actual names of each element? Granted, it doesn’t happen
Here’s a tip for often, but it’s nice to know how when you need to. For
fans of the example, if you wanted to access the names of a TPenStyle
Delphi TList set, you might be inclined to do something like this:
object. In look-
ing through case ThePenStyle of
psSolid : ShowMessage('psSolid');
Delphi’s online psDash : ShowMessage('psDash');
Help, you may psDot : ShowMessage('psDot');
have noticed psDashDot : ShowMessage('psDashDot');
psDashDotDot : ShowMessage('psDashDotDot');
Figure 4: The element’s string value is that a TList has psClear : ShowMessage('psClear');
added to a list box. a Capacity psInsideFrame : ShowMessage('psInsideFrame');
property. This property end;

isn’t used to set the maxi-


mum size of the list, but However, there’s a better way. Delphi’s RTTI (Run-Time
to give Delphi an idea of Type Information) can obtain this information for you:
how many items the list
procedure TForm1.FormCreate(Sender: TObject);
will contain. var
a : Integer;
As you may already begin
for a := Ord(Low(TPenStyle)) to Ord(High(TPenStyle)) do
know, when you add ListBox1.Items.Add(GetEnumName(TypeInfo(TPenStyle),a));
items to a TList, the list end;
dynamically allocates
memory for the new This cycles through the elements of TPenStyle. You get the first
items. If you add an item and last elements with Low and High, respectively, then use Ord
to the list when there’s to get the element’s ordinal number. Then we use GetEnumName
no free space, Delphi to get the name of the enumerated type. GetEnumName wants
requests a chunk of the type information record for the type as the first parameter;
memory for several more we get this value by calling TypeInfo. The result is the string value
items (between four and of the element, and we add this value to a list box (see Figure 4).
16, depending on how
many are currently in the Navigating in Close Quarters
list). But what if you Sometimes, database applications must access Paradox data
need to add, say, 50,000 files (*.DB) in the currently logged directory. The developer
items? Delphi would has several options, the best of which is to set up a database
request little chunks of alias that gives access to the data file. A second option would
Figure 5: This syntax tells Delphi memory over 12,500 be to enter a fully qualified path into the Database property.
to look right under its nose. Of course the disadvantage is that if the application is moved
times.
to another machine, the database file might not reside in that
Instead, you can estimate how many items the list will location and the application wouldn’t be able to find it.
have, then set its Capacity property accordingly. Afterward,
if you didn’t add as many as you thought, you can reset Wouldn’t it be nice if you could tell Delphi to look for the
Capacity to reflect the real number. The result is that you database in the same directory as the application regardless of
will have one memory allocation at the start, none during where it is? Well, you can! All you need to do is put a period
the adding of the items, and one small deallocation after- and a backslash in the DatabaseName property of a Table
ward. If the starting allocation isn’t sufficient, Delphi will component (see Figure 5). When you access the TableName
property, you’ll see the database files in the currently logged
generate a memory exception at the outset.
directory. Even if you move the application elsewhere, it will
still be able to find database files in its own directory. D
Look at the following pseudo-code to see how this might be
done in practice:

ListOfBooks.Clear; Robert Vivrette is a contract programmer for Pacific Gas & Electric, and Technical
ListOfBooks.Capacity := 50000;
repeat
Editor for Delphi Informant. He has worked as a game designer and computer
blah ... blah ... consultant, and has experience in a number of programming languages. He can
ListOfBooks.Items.Add(TheNewBook); be reached via e-mail at [email protected].
blah ... blah ...
until DoneAddingBooks;
ListOfBooks.Capacity := ListOfBooks.Count;

50 July 1997 Delphi Informant


New & Used

By Bill Todd

AdHocery for Delphi


The Key to Seizing Query Control

N eed to provide ad-hoc query capabilities for your users? AdHocery from
Nevrona Designs not only makes providing such capability easy, it also
provides control of the user interface. With AdHocery, your users can see only
the data they want, either on screen or in reports.

Understanding AdHocery using any combination of Company, City,


The easiest way to understand AdHocery is to State, and Country. To provide the ad-hoc
examine a sample program that uses it. Figure 1 query capability, start by placing an
shows a form with a Query, DataSource, and AdHocSource component — one of the five
DBGrid connected to display the result set of a AdHocery components — on the form.
query. The SQL statement is: Next, set the AdHocQuery component’s
BaseQuery property to Query1, to connect it
SELECT CustNo, Company, City, State,
Country, Addr1, Addr2
to the Query component on the form.
FROM Customer Double-click AdHocSource to display its
Fields Editor, as shown in Figure 2. Now
The goal of this project is to let the user right-click, select Add Fields from the menu,
select the records returned by the query, and add the Company, City, State, and
Country fields.

Now drag the four fields from the Fields Editor


window, and drop them onto your form.
AdHocery automatically creates four DBEdit
components with labels, then sets their
DataSource properties to the AdHocSource
component, and their DataFields property to
their respective fields. The Fields Editor also
lets you specify the comparison operator for
each field. By default, the comparison operator
is =, but you can specify other conditions, such
Figure 1: A sample ad-hoc query form. as <, <=, >, or >=. You can also use the Fields
Editor to add multiple instances of a field, so
you can specify ranges in the query. To allow
the user to enter a range for a numeric field,
you would add the field twice in the Fields
Editor — specifying >= as the comparison
operator for one instance, and <= for the sec-
ond. This allows you to drop two instances of
the field on the form — one for the minimum
value, and one for the maximum.

If the operation is:

Figure 2: The AdHocSource Fields Editor. = ('..' Enabled)

51 July 1997 Delphi Informant


New & Used
component works with the
DBEdit components to allow you
to build a complex query condi-
tion using AND, OR, and
grouping (parentheses). Nevrona Designs’ AdHocery 1.0
offers unsurpassed control of
ad-hoc query potential, offering
The AdHocGrid component also powerful interfaces for both the
lets users build complex queries, as developer and the user.

shown in Figure 4. The grid Nevrona Designs


approach resembles query-by- 1930 S. Alma School Rd., Ste. B214
Mesa, AZ 85210-3041
example in many respects, and
enables users to build complex Phone: (888) 776-4765;
queries in an intuitive format. A (602) 491-5492
Fax: (602) 530-4823
particularly nice feature of E-Mail: [email protected]
Figure 3: The AdHocery SQL dialog.
AdHocGrid is the ability to specify Web Site: http://www.nevrona.-
logical AND and OR operations com/designs
Price: US$149
between cells. This makes building
lists, such as:

City = Honolulu OR Miami

(the condition in Figure 4), very easy for users. Another


nice feature of the AdHocTreeView and AdHocGrid com-
ponents is that you can change the text on the context
menus that appear when the component is right-clicked, to
suit your taste and the sophistication of your users.

AdHocery includes one more component, AdHocLookupGrid,


which, like a multi-select list box, lets users easily specify a
Figure 4: Operations between forms can be specified.
list of values to match in one field of a query. For example,
the user can specify a “starts with” value for that field by you could present a list of states, and let users select only
adding two periods to the end of the value. Behind the scenes, those that interest them. AdHocery also provides a power-
AdHocery changes the operator from = to LIKE, and replaces ful developer interface — via public methods and events
the .. with %. To give users the full power of the SQL LIKE — that lets you control every aspect of the query-genera-
operator, change the operation to LIKE in the Fields Editor. tion process in code.
Now users can enter the SQL % wildcard anywhere in the
search value. Conclusion
AdHocery is a great tool. It gives you the ability to let the
Finally, drop two buttons on the form, and set their captions users of your programs construct complex and powerful
to Show SQL and Apply, respectively. Add the following lines queries easily, with a choice of user interface. This allows
of code to the button’s OnClick event handlers: you to exploit query generation in a way most useful to
the users of each program you write. AdHocery is the best
// The Show SQL button’s OnClick event handler. ad-hoc query tool I’ve seen — it will certainly remain in
AdHocSource1.ShowSQL;
my toolbox. D
// The Apply button’s OnClick event handler.
AdHocSource1.ExecuteSQL;

Bill Todd is President of The Database Group, Inc., a database consulting and
If you compile and run this program, you can enter any com- development firm based near Phoenix, AZ. He is a Contributing Editor of Delphi
bination of values in the DBEdit components, then click the Informant; co-author of Delphi 2: A Developer’s Guide [M&T Books, 1996],
Apply button, and see the result of your query in the DBGrid. Delphi: A Developer’s Guide [M&T Books, 1995], Creating Paradox for Windows
To see the SQL generated by AdHocery, click the Show SQL Applications [New Riders Publishing, 1994], and Paradox for Windows Power
button, and you’ll see a display similar to Figure 3. Programming [QUE, 1995]; and a member of Team Borland providing technical
support on CompuServe. He has also been a speaker at every Borland Developers
Conference. He can be reached on CompuServe at 71333,2146, on the Internet
While this example provides basic query capability, it doesn’t at [email protected], or at (602) 802-0178.
support logical AND and OR operations, or compound con-
ditions other than “and-ing” between fields. To make more
complex queries easy, AdHocery provides two other compo-
nents: AdHocTreeView and AdHocGrid. The AdHocTreeView

52 July 1997 Delphi Informant


File | New
Directions / Commentary

Delphi on the Web

S oftware development need not be an isolated process. Macho programmers may continue to insist on
coding every line themselves, but the rest of us can take advantage of the Web as a resource for com-
ponents, tips, techniques, and other technical information. The Web offers a plethora of Delphi sites. This
month, I’ve given “gold stars” to the top five Delphi-related sites on the Web.

Delphi Super Page The Delphi Deli The Delphi Information Connection
Maintained by Robert Czerwinski, this If the two previous sites provide the “meat This site, maintained by
is my favorite site to visit when looking and potatoes” for Delphi developers (com- David and Susan Bernard, deserves special
for components and other Delphi ponents), The Delphi Deli, maintained by mention. It’s an “all-around site,” contain-
resources. It has the largest library of Sylvia Lutnes, strives to provide a full ing not only a wealth of components, but
VCL components I’ve seen, and can be menu. Yes, it offers a component library, also links and other resources. Its design is
quickly searched using a variety of cri- but it also has information on Delphi mail- notable, featuring a graphic Table of
teria (e.g. 16-bit and/or 32-bit, free- ing lists, book reviews, FAQs, a chat room, Contents in the form of Delphi’s Object
ware and/or shareware, etc.). Com- and more. This is a well-rounded site, par- Inspector. Unfortunately, it looks as
ponent descriptions, while not as ticularly for those folks who want to do though the site hasn’t been updated since
extensive as Torry’s Delphi Pages or The more than just download the latest VCL. late 1996; hopefully it will be revived
Delphi Deli, are more than adequate. soon, before it becomes outdated.
Finally, Robert does a terrific job of The Delphi EXchange
updating the site regularly. Maintained by Brad Choate, this site offers While each of these “gold star” sites
a large collection of VCLs, as well as addi- deserve special merit, there are a vast
Torry’s Delphi Pages tional resources such as Delphi news and array of other Delphi-related Web sites
Running neck and neck with the Delphi announcements, volunteer Delphi experts that also prove helpful to developers (see
Super Page, this site is administered by (DEXperts), and programming tips. The table below). You owe it to yourself to
Maxim Peresada and Victor Gvozdev. Delphi EXchange also features perhaps the check ’em out. D
Not only is its library extensive, it also most exhaustive list of Delphi-related Web
provides helpful comments — by a dog links I’ve seen. When searching for compo- — Richard Wagner
named Torry — about many of the nents, you can take advantage of its power-
components; awards the best compo- ful search engine, or drill down by category,
nents with a special “Torry’s Top” using an outline view of its file library. My Richard Wagner is Contributing Editor to
award; and notes the most popular one “wish” for this site is better file descrip- Delphi Informant and Chief Technology
downloads. Another nice feature is a tions, e.g. is a file freeware? Does it include Officer of Acadia Software in the Boston,
page that lets you know what’s new on source code? This shortcoming aside, The MA area. He welcomes your comments at
other Delphi sites. Delphi EXchange is a source I visit often. [email protected].
(all URLs begin with http://) Number of Searching File Additional Links to Other
Components Descriptions Resources Delphi Sites
Delphi Super Page Excellent Excellent Good Fair Very good
sunsite.icm.edu.pl/delphi/
Torry’s Delphi Pages Very good Listing only Excellent Very good Very good
carbohyd.siobc.ras.ru/torry/
The Delphi Deli Good Good Excellent Excellent Good
www.intermid.com/delphi/
The Delphi EXchange Very good Excellent Fair Good Excellent
www.delphiexchange.com
The Delphi Information Connection Good Very good Very good Very good Good
www.delphi32.com
The Delphi Temple Fair Listing only Good Average Good
simtel.coast.net/~jkeller/
The Delphi Companion N/A N/A N/A Very good Very good
www.xs4all.nl/~dgb/delphi.html
Delphi Source Good N/A Fair Good Very good
www.doit.com/delphi/
Delphi Station Limited Listing only Fair Good Very good
www.technosoftinc.com/delphi.shtml

53 July 1997 Delphi Informant

You might also like