Sie sind auf Seite 1von 44

March 2001, Volume 7, Number 3

Cover Art By: Arthur Dugoni

ON THE COVER

25

Better UI Design: Part II Robert Leahey


Last month, Robert Leahey made his case for the proper treatment of
end users. Now he puts his ideas into practice by implementing and
demonstrating a message queue class.

Columns & Rows

dbExpress Bill Todd


Bill Todd introduces Borlands new, small, fast set of components for
accessing SQL databases from Kylix (and soon Delphi 6) and even
explains how to deploy a dbExpress application.

34

FEATURES
9

In Development

Hunting for Bugs: Part II Cary Jensen, Ph.D.


Cary Jensen continues his exploration of Delphis integrated debugger.
The focus is on breakpoints, from breakpoint groups to address, data,
and module load breakpoints.

REVIEWS

Sound + Vision

Working with Waveforms: Part II Alan C. Moore, Ph.D.


Alan Moore concludes his two-part series on the tricky world of
Windows sound. Last month covered waveform recording and double
buffering. This month, its playback time.

19

Dynamic Delphi

ISAPI Development: Part II Shiv Kumar


Shiv Kumar continues his series by building a larger application that
generates a form that allows users to enter data, validates and posts
information to a database, and much more.

1 March 2001 Delphi Informant Magazine

At Your Fingertips

Drives, Files, etc. Bruno Sonnino


This month, all of Bruno Sonninos tips work together to gather
information about drives and files, and then display them in a
ListBox, complete with appropriate icons.

38
14

Greater Delphi

InstallShield Professional - Windows Installer


Edition 2.0
Product Review by Bill Todd

42

Learn Object Pascal with Delphi


Book Review by Alan C. Moore, Ph.D.

DEPARTMENTS
2
43

Delphi Tools
File | New by Alan C. Moore, Ph.D.

Delphi
T O O L S
New Products
and Solutions

Book Picks
Securing Windows
NT/2000 Servers
Stefan Norberg
OReilly

ISBN: 1-56592-768-0
Cover Price: US$29.95
(199 pages)

Understanding the Linux Kernel


Daniel P. Bovet and Marco Cesati
OReilly

ISBN: 0-596-00002-2
Cover Price: US$39.95
(684 pages)

2 March 2001 Delphi Informant Magazine

The Imaging Source Releases TX Text Control 8


The Imaging Source, LLC
announced TX Text Control 8,
a royalty-free word processing
component in reusable form.
TX Text Control is licensed
on a per-developer basis. An
unlimited number of copies of
programs created with TX Text
Control can be distributed without any license fees. The per
developer model also means
that you may install your copy of
TX Text Control on more than
one computer as long as you
are the only one who uses it.
The TX Text Control ActiveX
is the ideal tool for applications
written in Delphi and Visual
Basic. The ActiveX is included
in the Standard and Professional
versions. It is fully compatible
with Internet Explorer, and
enables you to let your applications run in a browser. Browserbased applications can be created
as ActiveX Documents in Visual
Basic, or directly in HTML
using VBScript.
While the ActiveX also works
with Visual C++, the C++ Class
Library (only available in the Professional Version) offers ease of
use, and is designed especially for
Microsoft Visual C++ and MFC.
The class library seamlessly integrates into C++ projects, eliminating the need to call DLL
messages or OCX properties.
The class library directly
uses TX Text Controls core
components. Classes include the
CTextControl base class, with
member functions for all of TX
Text Controls features, including headers and footers, tables,
macro elds, and images; and
CTXDoc and CTXView classes,
with full support for MFCs
document/view architecture,
separate classes for button bar,
ruler, and status bar and readyto-use document handling, and
printing and print preview.
Other TX Text Control features are a step-by-step guide on
how to integrate the TX Text
Control Class Library into an
MFC project, a Scribble-like
tutorial, a reference manual with
overview chapters and class reference, a class library for Visual

C++, HTML, RTF, Word 6


to 2000, ANSI, and Unicode
le formats, TIFF, BMP, JPEG,
WMF image formats, tables,
headers, footers, bullets, numbered lists, text ow around
embedded images, OLE objects
and controls, undo and redo.
A time-unlimited evaluation
copy of TX Text Control 8,

including a library of sample


programs, is available for download.
The Imaging Source, LLC
Price: Professional version, US$949; Standard version, US$439; upgrade to Standard,
US$249; upgrade to Professional, US$499.
Contact: (877) 898-2875
Web Site: http://www.textcontrol.com

Signsoft Announces VisIt 2.0


Signsoft announced VisIt,
a collection of exible components for 3D graphics and animation based on the OpenGL
standard. It includes components for Borland Delphi and
Borland C++Builder, as well as
special solutions for software
developers.
VisIt 2.0 is based on the Microsoft OpenGL library. With these
components you can design 3D
applications with Delphi and
C++Builder. The contact with
the interface of the library is
reduced to a minimum but it
is possible to access all OpenGL
commands and options. These
components simplify many
repetitive tasks in one functionality.
This components collection
consists of components for cameras, camera controlling, lights,
materials, textures, text, predened objects, and more. Components handle complex scenes.
You may also save, load, and
print scenes. You can use all
the functionality by dragging the

corresponding component into


your design form. For example,
it can be used for developing
multimedia applications or CAD
programs. With the help of
OpenGL hardware accelerators
you can speed the graphic performance up to real time.
Features include more than 30
VCL components, an improved
base class for the components,
acceleration by the use of arrays
and the automatic use of display lists, rendering of 3D functions, rotation objects, object
clipping, rendering of faces
(landscapes) from points, timer
components for a given frame
rate, transformations of complex objects, control of the
transformation order by properties, scene management, and
import of DXF les.
Signsoft GmbH
Price: US$111.21; upgrade from VisIt 1.x
is US$66.72.
Contact: info@signsoft.com
Web Site: http://www.signsoft.com/
international/visit

Delphi
T O O L S
New Products
and Solutions

Book Picks
Red Hat Linux 6
Harold Davis
Peachpit

Wise Solutions Releases Wise for Windows Installer 3


Wise Solutions released Wise
for Windows Installer 3, a
setup authoring tool for Windows Installer in Professional
and Standard editions. The
Professional edition includes
tools such as the patent-pending Debugger for Windows
Installer, as well as InstallTailor, a tool to customize
any Windows Installer package. The Standard edition is
an easy-to-use tool that helps
developers create installations
for Windows Installer.
Both the Professional and
Standard editions feature full
support for creating Windows
Installer packages for any
32-bit Windows platform,
merge modules, transforms,
software patches, and project
les. Wise for Windows
Installer 3 also provides complete control for editing all
standard and custom Windows
Installer tables.
A language pack add-on that

simplies the process of localizing end-user installations into


non-English languages is also
available. The language pack
contains translated resources
for 20 languages, in addition
to the ve languages that are
included in the Professional
and Standard editions of Wise

for Windows Installer 3.


Wise Solutions
Price: Wise for Windows Installer 3 Standard,
US$449; Wise for Windows Installer 3
Professional, US$899; Wise for Windows
Installer Language Pack, US$795.
Contact: (800) 554-8565
Web Site: http://www.wisesolutions.com

DbCAD dev: OCX and ActiveX Components for GIS, MAPPING, GPS, and CAD Applications
ISBN: 0-201-35437-3
Cover Price: US$29.99
(311 pages, CD-ROM)

Applying COM+
Gregory Brill
New Riders

ISBN: 0-7357-0978-5
Cover Price: US$49.99
(466 pages)

3 March 2001 Delphi Informant Magazine

ABACO srl announced


DbCAD dev, a raster/vector/
database technology developed
in Italy in the form of standard
OCX and ActiveX components. DbCAD dev provides
one or more graphic windows
with zoom, pan, and overview
commands, using graphic solutions to build GIS, MAPPING, GPS, and CAD
applications. DbCAD dev
works in standard Windows
development environments
such as Delphi, Visual Basic,
Visual C++, PowerBuilder, and
more.
DbCAD dev supports les
in many raster and vector standard formats, such as TIF,
BMP, RLC, and many other
raster formats, plus AutoCAD
(DWG/DXF), ESRI (Shape
File SHP), and other vector
formats. It is also possible to
add new drivers that allow
extending the supported raster
and vector formats. DbCAD
dev can calibrate the graphic
window with a double precision coordinate system to over-

lay graphic raster and vector


les using transparency effects
or to load only le portions.
Linking vector entities with
an external database is simple:
the DbCAD dev database
graphic engine is based on
standard formats such as
dBASE III, Microsoft Access
MDB, or Buffered DbCAD
dev structure and many others
available as DbCAD dev complementary drivers. DbCAD
dev also allows for a custom
graphic database driver for
managing other database formats. The DbCAD dev functions also include the ability to
import, export, create, select,
and edit all the 2D vector entities such as lines, polylines,
polygons, arcs, blocks, and
text, including their properties.
Complex and lled polygons
are available with various hatch
styles and transparency effects,
and operations such as union,
intersection, etc.
DbCAD dev also allows spatial analysis and query, the display and overlay of real-time

vector entity animation for


GPS applications, and is supported by a set of print functions.
DbCAD dev complementary
drivers include a TIFF and GIF
driver, which supports compressed 24-bit TIFF and GIF
les format; a JPG driver that
supports JPG les format; an
advanced SHAPEFILE driver
that manages Shape le format
(SHP); an MrSID driver that
manages MrSID les format;
an ER Mapper driver that manages ER Mapper les format;
an ORACLE vector database
driver; and an SDE vector database driver.
DbCAD dev standard includes
a preview of DbCAD dev Internet Server, a solution based on
ISAPI server and Java client for
the development and distribution of GIS, MAPPING, GPS,
and CAD applications.
ABACO srl
Price: Contact for pricing information.
Contact: info@dbcad.com
Web Site: http://www.dbcad.com

Delphi
T O O L S
New Products
and Solutions

Book Picks
JavaScript for the World
Wide Web, 3rd Edition
Tom Negrino and Dori Smith
Peachpit

Excel Softwares WinTranslator Adds SQL Reengineering


Excel Software extended the
reengineering capabilities of WinTranslator to generate WinA&D
data models from SQL code.
Database designers can generate
logical and physical data models
from a SQL schema for popular
RDBMS products, including
Oracle, SQL Server, DB2,
Sybase, Informix, and InterBase.
Rich data models can represent
tables, views, constraints, assertions, triggers, indexes, procedures, and other SQL elements.
WinA&D is a comprehensive
tool for system analysis, requirement specications, software
design, and code generation.
Popular modeling notations and
supported methods include
object-oriented analysis and
design with UML, structured
analysis and design using
Yourdon/DeMarco and Information Engineering (Martin) style
data models for developing information systems. WinTranslator is
a reengineering tool for WinA&D

that generates UML class models


for Delphi, C++, and Java, and
structure charts from C, Pascal,
Basic, and Fortran.
WinTranslator processes an
input SQL script le containing
statements like CREATE TABLE,
CREATE TRIGGER, etc. and
produces a dictionary entry list
that can be imported into
WinA&D. The new WinA&D
3.2 software automatically generates data models from the dictionary information. Within minutes
designers can view graphic data
models to understand, communicate, or enhance their database
systems. Data models can be
edited within WinA&D and used
to generate new SQL schemas
for a selected RDBMS. Data
models can be integrated with
other WinA&D models such as
process models that show information ow or state models that
show different modes of operation
and event transitions. HTML
reports can easily be generated to

Extended Systems Announces XTNDConnect RPM


ISBN: 0-201-35463-2
Cover Price: US$17.99
(292 pages)

The Wireless Application Protocol


Steve Mann and Scott Sbihli
Wiley

ISBN: 0-471-39992-2
Cover Price: US$29.99
(210 pages)

4 March 2001 Delphi Informant Magazine

Extended Systems released


XTNDConnect RPM (Remote
Procedure Middleware), a
middle-tier server for developing
wireless and wired applications
that provides real-time access
to enterprise data and server
processes. XTNDConnect RPM
supports Palm Computing,
Windows-powered Pocket PC,
and Windows platforms.
XTNDConnect RPM allows
developers to extend simple or
complex enterprise applications
such as MRP systems to wireless
mobile devices through easy-touse multi-tier server architecture.
The mobile device becomes a
thin-client to the server, allowing
complex processes to be executed
on the server and then send processed data to the mobile client
application.
With real-time access, business
decisions may be made in
the eld and updates to the
enterprise server are possible.
XTNDConnect RPM minimizes
the limitations associated with
mobile applications. XTNDConnect RPM allows large enterprise databases to be accessed and

processed by a mobile device,


allowing the mobile worker to
fully exploit the application
anywhere, anytime. XTNDConnect RPM complements
Extended Systems mobile synchronization and management
software, XTNDConnect Server.
XTNDConnect RPM is
designed for wireless network
environments. XTNDConnect
RPM provides compression and
message queuing technology.
Features include programmable
process-oriented middleware;
scaling from stand-alone to
enterprise; support for native
development tools on server
and mobile devices like Delphi,
C++Builder, MobileBuilder, and
Code Warrior; and support for
any database server through an
ODBC or ADO interface.
Extended Systems
Price: With support for up to 1,000
concurrent connections, XTNDConnect RPM
starts at US$525 for one concurrent connection; five concurrent connections is US$425
per connection.
Contact: (800) 235-7576
Web Site: http://www.extendedsystems.com

communicate models, specications, and requirements to anyone


with a Web browser.
WinTranslator has a selection
option for SQL-99, Oracle, SQL
Server, DB2, Sybase, Informix, or
InterBase dialects of SQL. Vendorspecic SQL statements in other
dialects can also be supported with
a conguration option that captures design data from the SQL
script. Some RDBMS products
support multiple schemas within
a database. Each schema is automatically mapped to a different
namespace within the WinA&D
design environment.
WinTranslator runs on Windows 95, 98, NT, or 2000.
Excel Software
Price: US$495
Contact: info@excelsoftware.com
Web Site: http://www.excelsoftware.com

EC Software Releases EC
Software Help Suite
EC Software released EC Software Help Suite (EHS) for Delphi
3, 4, and 5. EHS covers every help
related task in a Delphi application and completes the help functions that Delphi provides.
EHS implements a full-featured
context-sensitive help system.
Without a single code line it provides a right-click Whats This?
menu item for every control and
adds a Whats This? button
to the main form, similar to
the help function of Microsoft
Word. It also implements rightclick help for menu items and
gives you full control over the
1 key. EHS builds a bridge
to HTML HELP and it HTML
HELP-enables your applications;
it also supports HTML HELP
pop-up topics, as well as mixedmode systems, a combination of
HTML HELP and WinHelp.
EC Software Help Suite comes
with full source code. A demonstration application that illustrates the features is included.
EC Software
Price: Free
Contact: info@ec-software.com
Web Site: http://www.ec-software.com/
comppage.htm

Columns & Rows


SQL Databases / Cross-platform / Kylix / Delphi 6

By Bill Todd

dbExpress
SQL Database Access for Kylix and Delphi 6

bExpress is Borlands new database access technology that will make its first appearance in Kylix (Delphi for Linux) and Delphi 6. Although its been widely referred to as
a replacement for the Borland Database Engine (BDE), this is not entirely true.
dbExpress is a new set of components for accessing
SQL databases. You wont be able to access Paradox
and dBASE tables using the dbExpress drivers supplied by Borland because those database management systems are built into the BDE. If you want to
use Paradox as your database, youll have to continue
to use the BDE. However, as youll see later in this
article, this may not be the case for dBASE users.
[Note: Everything in this article applies to both
the Windows and Linux platforms unless otherwise
noted. Since this article is based on pre-release versions of dbExpress, some features may change in
the nal version.]
Borland designed dbExpress with the following
goals in mind:
 Make it small.
 Make it fast.
 Make it cross-platform.
 Avoid the installation and conguration
problems of the BDE.
 Make it easy for anyone to write dbExpress
drivers.
Windows File

Linux File

Description

DBEXPINT.DLL
(116KB)

LIBSQLIB.SO

The dbExpress InterBase driver

MIDAS.DLL
(270KB)

MIDAS.SO

The ClientDataSet support DLL

GDS32.DLL
(339KB)

LIBGDS.SO

The InterBase client library

Figure 1: Files deployed with a Delphi InterBase application.

5 March 2001 Delphi Informant Magazine

Every goal has been achieved. To give you an


idea of whats involved in deploying a dbExpress
application, lets look at a Delphi application
that uses InterBase as its database. To deploy
this application on a Windows computer that
has never had any Borland database software
installed, you must distribute the three les
shown in the Windows File column of Figure 1,
in addition to your EXE.
First, you must deploy the client library supplied
by the database vendor for accessing their database.
In the case of InterBase, this is a single DLL,
GDS32.DLL. The number and size of the le(s)
in the database vendors client may be different
for other databases. Next, you must distribute the
dbExpress driver for the database youre using. For
InterBase on Windows, this is DBEXPINT.
If your application lets users interactively edit data,
youll also need to deploy MIDAS.DLL for reasons
that will be explained shortly. You dont have to
create any Windows registry entries, create any INI
les, or register any COM servers to deploy an
application. Its really that easy.
This means you can put the application and
supporting DLLs in a directory on the le server
and have all users share them without changing
anything on the users PC. It also means you
can put all of the les on a CD-ROM, and
run your application from there without installing anything on the users PC. Note that you
would still have to install the vendor library,
GDS32.DLL, if you were using the BDE, so the
total size of the two les that replace the BDE is

Columns & Rows


about three percent of the size of the BDE. The sizes of the Linux
library les were not available at the time of this writing, but they
should be similar to the Windows les.
While you can put DLLs that you distribute with your application in the same directory as your EXE, you may not want to
because the DLLs can be shared by any number of Delphi applications. Instead of putting the DLLs in the working directory, you
can put them in any directory where Windows will nd them.
This includes the Windows \System or \System32 directory, the
\Windows directory, or any other directory on the search path.
Since all of the connection information is contained in the properties
of the dbExpress components, you have complete control over how
accessible dbExpress is. If you want to give users the ability to change
some or all of the connection parameters, you can store them in the
registry or an INI le on a Windows machine, or in a text le on a
Linux machine, and give users a way to edit the values. When your
program starts, just read the values from their storage locations and
set the properties of the dbExpress components.
One word of warning is in order at this point. The previous description
deals with deploying only the client application. This example assumes
that the database server, in this case InterBase, is already installed somewhere. How difcult it is to install the database software depends on the
database you use, and that topic is outside the scope of this article.

Working with dbExpress


dbExpress drivers are very small because they provide very limited
functionality. A dbExpress driver implements ve interfaces that
support fetching metadata, executing SQL statements and stored
procedures, and returning a read-only unidirectional cursor to any
result set returned by a SQL statement or stored procedure.
All other data access functionality is provided by connecting
a MIDAS ClientDataSet and DataSetProvider to the dbExpress
dataset components. If youre a Delphi Professional user, dont
worry; the ClientDataSet and DataSetProvider components will
be included in the Professional version of any future Delphi or
C++Builder product that includes dbExpress.
If youve worked with the ClientDataSet and DataSetProvider
components in the Enterprise version of Delphi, you know
what a great architecture they provide. ClientDataSet components
hold the records returned by your query or stored procedure in
memory. They:
 provide very high concurrency by minimizing the time transactions are open;
 allow you to instantly create indices for fast searching, or to
change the sort order of your data;
 allow indexing of calculated elds;
 support maintained aggregates to allow you to include the sum,
min, max, count, or average of any eld, with grouping by
another eld or elds in your datasets;
support
powerful lters that use SQL WHERE syntax;

 let you selectively undo changes;
 allow you to write code to make join query result sets updateable;
 provide temporary in-memory tables;
 support nested datasets for master-detail relationships;
 let you load data from, or save data to, XML les;
 allow single-user at-le applications without using additional
database software; and
 support briefcase model applications.
6 March 2001 Delphi Informant Magazine

Figure 2: The dbExpress Connection Editor.

dbExpress drivers are planned for InterBase, MySQL, Oracle, DB2,


and ODBC. However, some of these drivers may not be available
in the initial release of Kylix. Since the interfaces that dbExpress
drivers must implement are documented in the online help, other
database vendors will almost certainly provide dbExpress drivers for
their products. There are also already third-party dBASE engines
available, so it seems likely that one or more of those vendors will
develop dbExpress drivers, making it possible for dBASE table users
to switch from the BDE to dbExpress if they wish.

The dbExpress Components


dbExpress consists of a SQLConnection component, several dataset
components, a SQLMonitor component, and a SQLClientDataSet
component. The SQLConnection component provides a database
connection for any number of dataset components. To connect to a
database, begin by dropping a SQLConnection component on a form
or data module.
The next step is to dene a connection to a specic database. There
are three ways to do this. You can use an existing named connection,
create a new named connection, or put the connection parameters in
the Params property of the SQLConnection component. To use an
existing named connection, just set the ConnectionName property.
To create a new named connection, double-click the SQLConnection
component to open the dbExpress Connection Editor (see Figure 2), and
create a named connection. The Connections list box on the left shows
any connections that have already been dened. The Driver drop-down
list lets you lter the connection names, so only the connections for the
driver you select are shown. The Connection Settings grid on the right
shows the connection settings for the selected connection name. All of
the connections you create here are stored in the CONNECTIONS.INI
le on Windows, and in the dbxconnections.conf le on Linux.
Figure 3 shows the connections le with entries for a MySQL connection and an InterBase connection. After creating a named connection,
you can assign it to the SQLConnection components ConnectionName
property. The only problem with named connections is that youll have
to distribute a connections le with your application, or locate an existing connections le on the target computer and add your connection to
it. A shared connection le also means you run the risk of having your
connection names conict with those of other applications.

Columns & Rows

Figure 5: The
SQLConnection
component
Params
property.

youre working, SQLConnection provides the GetTableNames,


GetFieldNames, and GetIndexNames methods.
Figure 3: The connections file.

A better solution is to begin by


setting the DriverName property of
the SQLConnection component. The
drop-down list for the DriverName
property lists all of the drivers
installed on your system. The driver
information is contained in the
DRIVERS.INI le on Windows, and
in dbxdrivers.conf on Linux. Setting
the DriverName property will also
set the LibraryName and VendorLib
properties using information from the
drivers le. LibraryName contains the
name of the dbExpress driver library
le, and VendorLib contains the name
of the database vendors client library
le. These les will be DLLs on the
Windows platform, and shared object
libraries (.so les) on Linux. Figure 4
shows the Kylix Object Inspector with
DriverName set.
Next, enter the same connection
Figure 4: Setting the
parameters shown in the dbExpress
DriverName property.
Connection Editor in the Params
property of the SQLConnection component, as shown in Figure 5. This way the connection information
is contained entirely within your application. If you want application users to be able to change any connection parameters, you
can store them in the applications INI le. Of course, youll also
need to provide a way to edit the INI le, either within your application, or with a separate conguration editor program. This makes
each application totally self-contained. Once you have set up your
connection, set the Active property to True to ensure that you can
connect to the database.
The SQLConnection component also provides the StartTransaction,
Commit, and Rollback methods for explicit transaction control. If
you need to execute SQL statements that dont return a result
set, you can do so through the SQLConnection component using
the Execute or ExecuteDirect methods. No dataset component is
required. Execute will handle SQL statements that return a result
set, but its easier to use a dataset component to work with result
sets. If you need access to the metadata of the database with which
7 March 2001 Delphi Informant Magazine

dbExpress provides four dataset components: SQLDataSet,


SQLQuery, SQLStoredProc, and SQLTable. SQLDataSet is the component of choice for any new application you write. By setting its
CommandType property, you can use it to execute SQL statements,
call a stored procedure, or access all of the rows and columns in a
table. The other three dataset components are provided mainly to
make it easy to convert BDE applications to dbExpress.
Using SQLDataSet is very similar to using the ADODataSet component in ADO-Express. Just drop it on a form or data module,
and set the SQLConnection property to the SQLConnection component you want to use. Then, set the CommandType property to
ctQuery, ctStoredProc, or ctTable. Most often youll use the default
value of ctQuery. Next, set the CommandText property. The value
of CommandText depends on the value of CommandType. If
CommandType is ctQuery, CommandText contains the SQL statement you want to execute. If CommandType is ctStoredProc,
CommandText is the name of the stored procedure. If CommandType
is ctTable, CommandText is the name of the table. You use the Params
property to supply parameters for a parameterized query or a stored
procedure, and the DataSource property to link the SQLDataSet to
another dataset component in a master-detail relationship.
Here, however, the similarity between the SQLDataSet component
and other dataset components you may have used ends, because
SQLDataSet is a read-only dataset and provides only a unidirectional
cursor. If this is the only access you need, to print a report for
example, you can use the SQLDataSet by itself or with a DataSource
component, depending on the requirements of your reporting tool. If
you need to scroll back and forth through the records or edit the data,
you must use a DataSetProvider and ClientDataSet from the MIDAS
page of the Component palette.

The MIDAS Touch


If youve worked with MIDAS before, youll nd using a ClientDataSet
and DataSetProvider with a SQLDataSet no different than using them
with a TQuery. If you have not used these MIDAS components before,
the following is a brief summary of how to use them.
Begin by dropping a DataSetProvider on your form or data module,
and set its DataSet property to the SQLDataSet component. Next, add
a ClientDataSet and set its Provider property to the DataSetProvider.
Figure 6 shows a data module with all four components.
When you open the ClientDataSet, it will open the SQLDataSet,
fetch the result set returned by the SQL statement, and load the
records into memory. If you want to access the data through

Columns & Rows


the stored procedure or table, when a stored procedure or table
name is required. SchemaPattern lets you provide a SQL pattern
that will lter the result set. For example, if SchemaType is stTables
and SchemaPattern is 'EMP%', the dataset will only contain tables
that start with EMP.

Figure 6: A data module with a SQLConnection, SQLDataSet,


DataSetProvider, and ClientDataSet.

The last dbExpress component, SQLMonitor, is provided to help


you debug your application. SQLMonitor monitors all of the SQL
statements passed between a SQLConnection component and the
database server to which its connected. You can set its TraceFlags
property to specify which statements you want to monitor. The
SQL statements can be logged to a le, or you can write event
handlers to process them any way you wish. Unlike the BDEs
SQL Monitor that throws everything into the same list, with
SQLMonitor you can trace each SQLConnection component in
its own log.

Conclusion
the user interface, add a DataSource component and set its
DataSet property to the ClientDataSet, and then connect your
data-aware controls to the DataSource. As you add, delete,
and modify records, the changes are stored in memory in the
ClientDataSet components Delta property. When you call the
ClientDataSet components ApplyUpdates method, all of the accumulated changes are sent to the DataSetProvider, which generates
SQL statements to update the tables in the database.
The trick here is that the ClientDataSet holds all of the records
returned by the query in memory, so you need to use traditional
client-server architecture for your application, where the user must
enter selection criteria that allows you to fetch a reasonably small
result set. This may sound restrictive at rst, but once you explore
the powerful features of the ClientDataSet component, youll never
want to write a database application any other way. Besides, holding records in memory is not as big a limitation as it sounds,
when you consider that 10,000 records that are 100 bytes long will
only consume 1MB of memory and 100,000 records will consume
10MB. While you would not want to load that many records into a
ClientDataSet, in most situations you certainly can if necessary.

dbExpress seems to have it all. Its small, fast, easy to congure


and deploy, allows you to change databases without changing
your application, and works on both the Windows and Linux
platforms. Drivers are relatively easy to write, and the interfaces
are documented in the online help database, so vendors should
have no problem providing drivers for their products. If necessary,
you could even write your own. Borland will provide the source
code for dbExpress drivers that support OpenSource database
engines such as InterBase. You can refer to code from a working
dbExpress driver if you need to write one.
Since nothing needs to be installed, congured, or registered on
the users machine, dbExpress applications can easily be run from a
le server or CD-ROM. The control you get from all the database
connection information contained within the properties of the
dbExpress components is a big step forward. It gives you complete
control of what, if anything, the user can change. The Borland
engineering team really deserves applause for dbExpress.

dbExpress provides a shortcut via the SQLClientDataSet component.


This component is a ClientDataSet with a built-in DataSetProvider
and SQLDataSet. Just connect it to a SQLConnection component,
set the DataSet, CommandType, and CommandText properties, and
youre ready to go. You can also link two SQLClientDataSet
components in a master-detail relationship using the MasterSource
and MasterFields properties of the detail SQLClientDataSet. However, its more efcient to use separate SQLDataSet, DataSource,
DataSetProvider, and ClientDataSet components linked to include
the detail dataset as a dataset eld in the master dataset, as you would
in a MIDAS application.
If you need more detailed metadata information than the methods of SQLConnection provide, use the SetSchemaInfo method
of a SQLDataSet component. SetSchemaInfo takes three parameters: SchemaType, SchemaObject, and SchemaPattern. SchemaType
can be set to stNone, stTables, stSysTables, stProcedures, stColumns,
stProcedureParams, or stIndexes. This parameter indicates the type
of information the SQLDataSet will contain when its opened.
The schema type is set to stNone when youre retrieving data from
a table using a SQL statement or stored procedure. Each of the
other schema types creates a dataset with a structure appropriate
for the information being returned. SchemaObject is the name of
8 March 2001 Delphi Informant Magazine

Bill Todd is president of The Database Group, Inc., a database consulting and
development firm based near Phoenix. He is co-author of four database programming books, author of more than 60 articles, a contributing editor to Delphi
Informant Magazine, and a member of Team Borland, providing technical support
on Borland Internet newsgroups. He is a frequent speaker at Borland Developer
Conferences in the US and Europe. Bill is also a nationally-known trainer, and has
taught Delphi programming classes across the country and overseas. Bill can be
reached at bill@dbginc.com.

In Development
Debugging / Delphi 5

By Cary Jensen, Ph.D.

Hunting for Bugs


Part II: Get More from Your Breakpoints

orrecting errors in code is a necessary but all-too-often frustrating and timeconsuming task faced by software developers. Fortunately for Delphi developers,
Delphis integrated debugger includes a wealth of features that greatly assist bug searchand-destroy missions.
Last month, Part I of this series introduced you to
a number of the support tools provided by Delphis
debugger. This month, well take an in-depth look
at the workhorse of code debugging breakpoints.

can produce. In fact, some of the most valuable


breakpoints are those that trigger without loading the
integrated debugger. Non-breaking breakpoints are
described in detail later in this article.

A breakpoint is a marker associated with a compiled


line of code, or an event. When integrated debugging is enabled, and the application is being run
in the IDE, the debugger evaluates the breakpoint
when the line of source code to which it is attached
is about to be executed or the event occurs.

There are four types of breakpoints: source breakpoints, address breakpoints, data breakpoints, and
Module load breakpoints. Each is described in the
following sections.

Source Breakpoints

While most developers use breakpoints regularly, few


leverage the full potential of this important tool.
Specically, most Delphi developers consider a breakpoint to be a signal to the integrated debugger to
load, temporarily halting execution of the code. In
fact, loading the integrated debugger is only one of
a number of alternative behaviors that breakpoints

Source breakpoints are the most common type of


breakpoint. A source breakpoint is attached to a
specic line of compiled code. Note that some lines
of code do not compile, due to compiler optimizations. For example, the compiler will not compile
a statement that assigns a value to a local variable
that is never used. Attaching a breakpoint to such a
line results in a breakpoint that will never trigger.
After compiling a project, lines that are compiled are
identied by a blue diamond in the left editor gutter,
as shown in Figure 1. Note that in this gure the
line that assigns the value of 0 to the local variable
i is not marked with a blue diamond. The compiler
has ignored this line, since the value assigned to the
variable is never used. If you set a breakpoint on a
line that is removed due to compiler optimizations,
the red dot that normally denotes a breakpoint will
appear with a diagonal line across it, indicating that
this breakpoint is invalid.

Figure 1: Blue diamonds indicate which lines are compiled, and to which source
breakpoints can be attached.
9 March 2001 Delphi Informant Magazine

There are a number of ways to add a source breakpoint to your code. The easiest is to click the gutter
of the code editor to the immediate left of the
line to which you want to attach a breakpoint.

In Development
The dened breakpoint appears in the editor as a large red dot by
default (in place of the smaller blue diamond). Also, the entire line is
highlighted in red, or whatever alternative color youve dened using
the Color page of the Editor Options dialog box.

Figure 2: The Add


Source Breakpoint
dialog box (also
known as the
Source Breakpoint
Properties dialog
box).

The other three techniques for creating source breakpoints include


selecting Run | Add Breakpoint | Source Breakpoint from Delphis main
menu, right-clicking in the Breakpoint List window and selecting Add
Source Breakpoint, or by pressing Caaa from the Breakpoint List
window. You display the Breakpoint List window by selecting View |
Debug Windows | Breakpoints from Delphis main menu or by pressing
CAb.
Once a breakpoint has been placed, you control its behavior using
the dialog box shown in Figure 2. This dialog box is known by
two names, depending on how its displayed. When you see it as
a result of adding a breakpoint, it uses the title Add Source Breakpoint. If you display it by right-clicking a breakpoint and selecting
to view its properties, its named the Source Breakpoint Properties
dialog box. Its the same dialog box, and will be referred to as
the Source Breakpoint Properties dialog box for the remainder of
this article.
The Source Breakpoint Properties dialog box is automatically
displayed when you use any of the three breakpoint-adding techniques described in the preceding paragraph. There are two additional ways this valuable dialog box can be displayed. You can
right-click a breakpoint in the Breakpoint List window and select
Properties. And nally, the easiest way is to right-click on the red
dot that identies the breakpoint in the editors gutter, and select
Breakpoint properties.
By default, a newly added source breakpoint is an unconditional
breakpoint. When the integrated debugger encounters an unconditional breakpoint, the debugger is loaded and execution pauses
immediately, before executing the line of code on which the breakpoint appears. This type of breakpoint is the one most familiar to the
majority of Delphi developers.
Source breakpoints can be customized in a number of interesting
ways. Using the Source Breakpoint Properties dialog box, you can
make the debugger load based on a condition (a Boolean expression)
or pass count, as well as declare the breakpoint to be a member of a
breakpoint group. The Source Breakpoint Properties dialog box also
permits you to dene specic information that can be written to the
event log when the breakpoint is encountered, react to the breakpoint
without loading the debugger, or enable or disable other breakpoints
as a result of processing this breakpoint.
It is these customizations that supply the full power of breakpoints.
Therefore, each of the Source Breakpoint Properties dialog box
options is discussed separately in the following sections.

Filename and Line Number


The Filename and Line number text boxes of the Source Breakpoint
Properties dialog box display the unit and line number to which the
breakpoint is attached. Normally, this is set for you and doesnt need
to be changed.
When you display the Source Breakpoint Properties dialog box
from the Breakpoint List window, you have an opportunity to
change the Filename and Line number text boxes. This is useful
when you want to clone an existing breakpoint, placing the newly
10 March 2001 Delphi Informant Magazine

created copy on another line in the same unit or in a different


unit. To do this, view the Source Breakpoint Properties dialog
box for the breakpoint you want to clone from the Breakpoint
List window, and then change the Filename and/or Line number
text boxes, as well as any other breakpoint properties you want to
modify. Before accepting this dialog box, however, enable the Keep
existing Breakpoint checkbox in order to copy the newly created
breakpoint to the new line (this checkbox is available only when
the dialog box is called from the Breakpoint List context menu
and does not appear in Figure 2). Leaving Keep existing Breakpoint
unchecked removes the previously placed breakpoint, replacing it
with one on the newly dened line.

Defining Conditions
You can use the Condition text box to enter an expression to control
whether the breakpoint will trigger. It can be any valid Boolean
expression, including functions, constants, variables, operators, and
so forth. When the Boolean expression evaluates to True, the breakpoint triggers; otherwise, it does not.
The only limitation to the conditional expression is that all symbols used within the expression must be within scope of the
breakpoint. For example, the value of a local variable of a method
can be used in the condition of a source breakpoint, but only
if that breakpoint appears within the method in which the local
variable is declared. Likewise, a function can only be used in the
expression if the breakpoint is placed within a unit that uses the
functions unit. For example, you can use AnsiCompareText in the
Condition text box, but only if the unit in which the breakpoint
appears is declared to use the SysUtils unit (the unit in which
AnsiCompareText is declared).

Using a Pass Count


The Pass count text box of the Source Breakpoint Properties dialog
box permits you to instruct the debugger to trigger the breakpoint
conditionally, based on the number of times the breakpoint has been
encountered. For example, imagine that your application contains
a subroutine that you assume will execute only once. One way to
verify this assumption is to place a breakpoint in that code segment
and set Pass count to 2. This breakpoint will only trigger if it is
encountered a second time.
Breakpoints that employ pass counts are also very useful within looping
control structures, where you may want to halt execution periodically
to inspect the value of expressions. For example, in a loop that executes
1,000 times, you may want to set Pass count to 100, providing yourself
with 10 chances to inspect the value of variables within the loop.
After setting a Pass count, you can monitor how many times it has
been encountered using the Breakpoint List window. For example,
the window shown in Figure 3 depicts a breakpoint whose pass

In Development

Figure 3 (Top): You can see the current pass count, as well as the pass count condition, from
the Breakpoint List window. Figure 4 (Bottom): The Breakpoint List window includes breakpoint
group membership.

count is set to 4. Furthermore, it shows that the breakpoint has


been encountered twice. The breakpoint will trigger automatically
if this breakpoint is encountered two more times.
Once a breakpoint that employs a pass count condition has been
triggered, it resets its counted passes to 0. For example, if you set
a breakpoints pass count to 100, its pass count will reset to 0
after the breakpoint triggers, meaning that the breakpoint must be
encountered another 100 times before it triggers again.

Breakpoint Groups
Breakpoints can be organized into groups. You designate a breakpoint
as a member of a breakpoint group when you want to enable or disable all breakpoints associated with the group at run time. Enabling
or disabling breakpoint groups is discussed later in this article.
To assign a breakpoint to a group, enter the group name in the
Group combobox of the Source Breakpoint Properties dialog box.
Alternatively, if you want to assign the breakpoint to a group to
which you have previously assigned a breakpoint, select that group
name from the Group combobox.
The group to which a particular breakpoint has been assigned
appears in the Breakpoint List window, as shown in Figure 4. This
window lists ve breakpoints. Three of these belong to one of two
different groups: click or wiggle.

Breakpoint Actions
Some of the more interesting things you can do with breakpoints
can be controlled with a breakpoints action properties. To display
the breakpoint action properties, click the Advanced button on the
Source Breakpoint Properties dialog box. This expands the dialog
box, as illustrated in Figure 5.

Normally, non-breaking breakpoints that control exceptions are


placed in pairs, with one breakpoint instructing the debugger
to ignore exceptions and a corresponding breakpoint enabling
exception handling once more. For example, you may have a
particular module that explicitly raises exceptions for the purpose
of displaying error messages to the end user. If you do not want
these exceptions to cause the integrated debugger to load during
debugging, you can place one exception at the entrance to the
module and another at its exit. Both of these breakpoints would
be non-breaking, with the rst breakpoint disabling subsequent
exceptions and the second enabling them. Together, these breakpoints prevent exceptions raised within the module from loading
the integrated debugger, while having no effect on any other code
within your application.
Another powerful action of a non-breaking breakpoint involves messages written to the event log. (Conguring the event log was
discussed in last months article.) To write a static message to the
event log upon encountering your breakpoint, enter the text of the
message in the Log message text box. If you want the value of
an expression written to the event log, enter that expression into
the Eval expression text box. Like with the Condition text box
of the Source Breakpoint Properties dialog
box, you can use any
symbol in this expression, so long as any
variables, constants,
functions, and so forth
are within scope of the
breakpoint.

You use the Break checkbox to control whether the breakpoint


invokes the integrated debugger. When Break is checked (the
default), the debugger is loaded when the breakpoint triggers
(subject to the Condition and Pass count values for the breakpoint).
When Break isnt checked, execution doesnt stop, and the debugger isnt loaded.

Unlike the Condition


text box, however,
which can hold only
Boolean expressions,
the Eval expression text
box can contain any
type of expression.

Initially, disabling Break may seem like disabling the breakpoint


itself. However, breakpoints that dont break still trigger, so any
other actions dened for the breakpoint still take place. Its these
non-breaking breakpoints that offer some of the more interesting
debugging options in Delphi.

Whenever you enter


an expression into the
Eval expression text
box, you can enable
or disable the writing

11 March 2001 Delphi Informant Magazine

One of the actions that a nonbreaking breakpoint may perform


is the disabling or enabling of subsequent exceptions. As you learned
from last months article, you
can instruct Delphis debugger to
ignore some or all language exceptions. This feature is controlled
from the Language Exceptions
page of the Debugger Options
dialog box. This technique, however, disables some or all exceptions for the entire project. By
comparison, using breakpoints
you can disable exceptions for specic portions of your application.

Figure 5: Click the Advanced button


on the Source Breakpoint Properties
dialog box to view the advanced breakpoint properties.

In Development
tion line, or right-click
the instruction line and
select Toggle breakpoint.
A breakpoint appears
as a large red dot in
the Disassembly pane
gutter, as shown in the
Figure 6.

Figure 6: An address breakpoint appearing in the CPU window.

of that expression to the event log using the Log result checkbox.
This checkbox, which is enabled by default, is useful when you
want to execute one or more functions that produce side effects
from within the expression, but dont actually need the value
of the resulting expression to be written to the event log. For
example, you may execute a function in the expression that serves
to destroy some temporary les created by some other part of your
application, ignoring any value returned by that function.
The last two comboboxes permit you to enable or disable entire
groups of breakpoints within your application. To disable a group
of breakpoints, enter the name of the group whose breakpoints
you want to disable in the Disable group combobox. Alternatively,
to enable a group of breakpoints, enter the name of that group
into the Enable group combobox. A single breakpoint can both
enable one group and disable another group when triggered.
Controlling which breakpoints are enabled is a powerful action for
a non-breaking breakpoint. For example, imagine the example presented earlier where a code segment should execute once, and once
only. If you determine that the code is executing more than once, you
might consider placing a number of breaking breakpoints within that
code segment, assigning them all to the same group. Initially, these
breakpoints could be disabled, since these are expected to be executed
once, at a minimum. You then place one non-breaking breakpoint
at the rst line of the code segment, setting its Pass count to 2 and
using its action to enable the breakpoint group. When the code
segment is executed the second time, the breakpoints in the group
become enabled, permitting you to use the debugger to determine
what conditions exist that permitted the code to execute again.
As mentioned earlier, there are four types of breakpoints, with the
source breakpoint being the most commonly employed. The three
remaining breakpoint types are described in the following sections.

Address Breakpoints

To modify characteristics of an address breakpoint, right-click the


gutter for the instruction line on which an
address breakpoint has
been assigned and select
Breakpoint Properties.
(You can also display
an address breakpoints
properties from the
Breakpoint List
window.) Delphi
responds by displaying
the Address Breakpoint
Properties dialog box
shown in Figure 7. As
you can see from this
gure, the Address
Breakpoint Properties
dialog box contains
essentially the same
properties as the Source
Breakpoint Properties
dialog box.

Figure 7: The Address Breakpoint


Properties dialog box.

Data Breakpoints
Data breakpoints are
those that trigger when
a particular memory
address is written to.
The memory address
Figure 8: The Add Data Breakpoint
dialog box.
can be represented
either as a hexadecimal
number, or as the variable used to refer to that memory address. Unlike other breakpoint types, which can persist from one Delphi session to the
next, data breakpoints last only for the current Delphi session.
(Conguring Delphi to persist breakpoints is described in the
nal section of this article.)
To add a data breakpoint, invoke the debugger (by either raising an
exception or triggering a breaking breakpoint). Then select Run | Add
Breakpoint | Data Breakpoint. Delphi responds by displaying the Add
Data Breakpoint dialog box shown in Figure 8.

Address breakpoints permit you to dene a breakpoint that will trigger when an instruction at a particular memory address is executed.
There are two ways to add address breakpoints, and both of them
require the debugger to be loaded. The rst technique involves the
Disassembly pane of the CPU window, which is the top-left pane
shown in Figure 6. Normally, you display the CPU window by
selecting View | Debug Windows | CPU.

At Address, enter the memory address of the data. Use Length to


dene the length of the data beginning at that address. The other
controls of this dialog box work as described in the preceding sections
on the Source Breakpoint Properties dialog box.

To add an address breakpoint, click in the left gutter of the Disassembly pane in the CPU window on the line associated with
the instruction address, press Cb with your cursor on the instruc-

To add a data breakpoint based on a variable name, add the variable


into the Watches window. (You display the Watches window by
selecting View | Debug Windows | Watches from Delphis main menu.)

12 March 2001 Delphi Informant Magazine

In Development
Figure 9: The Add
Module dialog box.

Enter the name of the module in the Module Name text box and
click OK. This is the only way to set a Module load breakpoint on a
module that isnt already loaded.
You can also create a Module load breakpoint from the Modules
window, but this is only useful if the project is in the debugger and
the module has already been loaded. With the debugger loaded, open
the Modules window by selecting View | Debug Windows | Modules
to view the window shown in Figure 10. Then, in the Module pane,
right-click the module you want to break on, and select Break On
Load. Modules that will trigger a Module load breakpoint appear in
the Module pane with a red dot to their left, as shown in Figure
10. To remove a Module load breakpoint, right-click the specic
module and select Break On Load again. (You can also add a Module
load breakpoint from the Modules window by right-clicking in the
Module pane and selecting Add Module.)

Persisting Breakpoints across Delphi Sessions


Figure 10: Adding a Module load breakpoint using the Module
pane of the Modules window.

Then, right-click on the watch entry and select the Break on change
option. You can modify the breakpoint properties of data breakpoints
by right-clicking on the breakpoint in the Breakpoint List window
and selecting Properties.

Module Load Breakpoints


Module load breakpoints are triggered when a specied module is
loaded into your application. For example, if during debugging you
want to be notied that a particular DLL (or run-time package,
ActiveX control, etc.) is being loaded, you can set a Module load
breakpoint for that DLL, causing the breakpoint to trigger when that
module is being loaded. Unlike the other breakpoints, Module load
breakpoints never appear in the Breakpoint List window.
To add a Module load breakpoint, select Run | Add Breakpoints |
Module Load Breakpoint. Delphi responds by displaying the dialog box
shown in Figure 9.

13 March 2001 Delphi Informant Magazine

To save source, address, and Module load breakpoints from one


Delphi editing session to another, you must enable the Project Desktop
checkbox in the Autosave Options group on the Preferences page of the
Environment Options dialog box. To display this dialog box, select
Tools | Environment Options.

Conclusion
Breakpoints are the most commonly used feature of Delphis integrated debugger. Unlike their name suggests, however, not all breakpoints break. In fact, as shown in this article, some of the most
valuable breakpoints you can set trigger without invoking the integrated debugger.

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


development company. Cary is co-author of 17 books, including Oracle JDeveloper
[Oracle Press, 1998], JBuilder Essentials [Osborne/McGraw-Hill, 1998], and
Delphi in Depth [Osborne/McGraw-Hill, 1996]. He is a Contributing Editor of
Delphi Informant Magazine, and an internationally respected trainer of Delphi and
Java. For more information, visit http://www.jensendatasystems.com, or e-mail
Cary at cjensen@compuserve.com.

Sound + Vision

Waveform Recording and Playback / Windows Multimedia API / Delphi 2-5

By Alan C. Moore, Ph.D.

Working with Waveforms


Part II: Playback

ast month, we examined the process of recording sounds to memory. Certain structures that are needed to record and play sounds were also discussed. Now its playback time, so well be examining the WaveOut functions (all of which begin waveOut...),
just as we examined the WaveIn functions.
Just as with the WaveIn functions, the WaveOut
functions generally return a code indicating success
or failure. And as you would expect, the functions
must be called in a specic order. Figure 1 describes
many of the WaveOut functions in the order in
which theyre usually called.
The WaveOut functions also use many of the same
parameters as the WaveIn functions. Heres a short
description of the parameters:
 hWaveOut: A UINT identifying the waveform
output device.
 lpCaps: The address of a WAVEOUTCAPS
structure to be lled with the capability
information of the device specied by
hWaveOut.
 uSize: An integer (UINT) indicating the size
of the previous parameter. If that parameter
is a WAVEOUTCAPS structure, you should
use SizeOf(WAVEOUTCAPS) to get this value.
 lpText: A PCHAR pointing to a text buffer that
holds an error message.
 lphWaveOut: A (PHWaveOut) handle identifying the open waveform audio output device.
This handle is set by waveOutOpen, and
should be saved in a variable that can be used
by other waveform output functions when they
are called.

14 March 2001 Delphi Informant Magazine







lpWaveOutHdr: Address of a WAVEHDR


structure that identies the buffer to be prepared or used.
uDeviceID: An integer identifying the waveform audio output device to open. It can be
a device identier, the handle of an already
open waveform audio output device, or the
WAVE_MAPPER constant.
lpFormatEx: The address of a
WAVEFORMATEX structure identifying the
requested format for playing back waveform
audio data (not all sound cards support all
formats).
dwCallback: The address of a xed callback function, an event handle, or the handle of a window
or thread that will be called during waveform
audio playback to process messages related to
playback progress. If no callback mechanism is
required, this value can be set to zero.
dwInstance: User-instance data passed to the
callback mechanism. If youre using the
window callback mechanism, this parameter
isnt used and should be set to zero.
dwFlags: Flags for opening the device,
including various constants indicating the
type of callback mechanism, if any, to
use (CALLBACK_FUNCTION,
CALLBACK_WINDOW, etc.).

Sound + Vision
Function

Parameters

Description

waveOutGetNumDevs
Returns UINT

None

Returns number of output devices.

waveOutGetDevCaps
Returns MMRESULT

hWaveOut;
lpCaps; uSize

Queries the waveform output device, hWaveOut, determining its


capabilities and placing them in the lpCaps structure.

waveOutOpen
Returns MMRESULT

lphWaveOut; uDeviceID;
lpFormatEx; dwCallback;
dwInstance; dwFlags

Opens a waveform audio output device identified by uDeviceID


for playback. If successful, the parameter lphWaveOut will contain
a handle for the device. This handle will be used later by other
WaveOut functions.

waveOutPause
Returns MMRESULT

hWaveOut

Pauses playback on a given waveform audio output device,


saving the current position.

waveOutPrepareHeader
Returns MMRESULT

hWaveOut;
lpWaveOutHdr;
uSize

Prepares a temporary block buffer for waveform audio


output. Before you call this function you must initialize
several of the WAVEHDR structures members, particularly
lpData, dwBufferLength, and dwFlags. The lpData field should
point to a valid PCHAR for which memory has been set; the
dwFlags field must be set to zero.

waveOutWrite
Returns MMRESULT

lpWaveOutHdr; uSize

Sends a block of audio data to a waveform audio output device.

waveOutRestart
Returns MMRESULT

hWaveOut

After a pause, restarts audio output (playback) process


on the waveform audio output device, hWaveOut.

waveOutStop
Returns MMRESULT

hWaveOut

Stops waveform audio output (playback).

waveOutReset
Returns MMRESULT

hWaveOut

Stops output on the given waveform audio output device,


resetting the current position to zero.

waveOutUnprepareHeader
Returns MMRESULT

hWaveOut;
lpWaveOutHdr;
uSize

Cleans up the preparation performed by the waveOutPrepareHeader


function. After passing a buffer with the waveOutAddBuffer function,
you must wait until the driver is finished with the buffer before calling
this function.

waveOutClose
Returns MMRESULT

hWaveOut

Closes the given waveform audio output device, hWaveOut,


the handle of which is no longer valid if the function succeeds.

waveOutGetErrorText
Returns MMRESULT

lpText; uSize

This function returns a textual description of the error identified


by the given error number, mmrError. The error string is
placed in the lpText parameter.

Figure 1: WaveOut functions from the Microsoft Win32 application programming interface (API). The functions are declared for Delphi
in mmsystem.pas.

So, how do all of these functions work together? Again, the


process is similar to that of recording sounds. Begin by calling the
waveOutGetNumDevs function in the FormCreate method,
and store the result in FNumOutputDevices. Then call the
waveOutGetDevCaps function to nd a waveform output device
to meet your specications (see Listing One beginning on page
16). Having found an appropriate device, initialize the elds of
the FormatStruc record and call the waveOutOpen function to open
the device. Indicate that you want to use a callback function, and
provide it with the name of that function. Then call waveOutPause
so playing wont begin until you prepare the temporary buffers that
will be used to process the sound and send the rst data blocks.
Now call the waveOutPrepareHeader function, followed by the
waveOutWrite function (there is no waveOutAddBuffer function).
Since youre using a double-buffering system, you must do this
twice. Finally, youre ready to start playback. Call waveOutRestart,
because you called waveOutPause earlier. Once you call this function, the double-buffering process itself begins. (The entire demonstration application is available for download; see end of article
for details.)
15 March 2001 Delphi Informant Magazine

Double Buffering in Waveform Output


As soon as a block buffer has been lled with audio data, an
output message is sent to Windows. That message is handled
by the waveOutProc callback procedure, which responds to the
WOM_DONE case by calling the ContinuePlaying method if
the special ag, WaveOutStatus, is equal to wosPlaying. Otherwise
it sends a WaveOut_Playing message that is handled by the
DefaultHandler method.
In the ContinuePlaying method, the data is transferred from
the main memory buffer, MainSoundBuffer (the TMemoryStream
where it was recorded), to the temporary block buffers to send
to the sound card. Most of the action takes place in the
LoadNextDataBlock method. This process continues until you
reach the end of the memory buffer. Then, WaveOutStatus is
set to wosDonePlaying and, as in the recording process examined
last month, control is transferred to the DefaultHandler method,
which calls CloseDownPlayback.
Its just as important to close things properly with waveform
output as it is with waveform input. Call the following functions

Sound + Vision
here. You can nd details of those in the Windows Multimedia Help File.
Or better yet, read Chapter 3 of my book, The Tomes of Delphi: Win32
Multimedia API [Wordware, ISBN: 1-55622-666-7]. That chapter is
devoted entirely to waveform audio and includes all of the functions.
The les referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103AM.

Figure 2: The Talk Back! application, after a playback session,


with input properties displayed.

in the CloseDownPlayback method. First, call waveOutReset to stop


the playback process; then call waveOutUnprepareHeader for each
of the two buffers. Only then can you safely call waveOutClose to
shut down your playback device.
The one function that hasnt been discussed is waveOutGetErrorText.
You may use this function any time you receive an error code in the
MMRESULT variable to get error text.
Be sure to take a good look at the DefaultHandler method. As in
the waveform input examples discussed last month, the text on
the form is updated according to the WaveOut messages received.
Figure 2 shows the application after a particular playback session.
It instructs the user how to begin another recording session.

Showing Device Properties


Use a ListView component to display input and output device
properties, as shown in Figure 2. You laid the groundwork in the
FormCreate method for iterating through the various input and output
devices by determining the number of devices with these two statements:
FNumInputDevices := waveInGetNumDevs;
FNumOutputDevices := waveOutGetNumDevs;

Now you can use these variables to iterate through the devices while using
waveInGetDevCaps and waveOutGetDevCaps to determine the properties
of each device. Store those properties in the records, FWaveInCaps and
FWaveOutCaps, accessing the various elds of those records to display
specic device properties. The code for displaying input and output
properties is shown in Listing Two (beginning on page 17).

Conclusion
You have come to the end of this exploration of working with waveform
input and output in memory using the double-buffering technique. The
explanations and examples presented here should be sufcient to get you
started working with waveform sound in memory.
There are many other details concerning the structures and functions
discussed here. There are also other waveform functions not discussed
16 March 2001 Delphi Informant Magazine

Alan Moore is a Professor of Music at Kentucky State University, specializing in


music composition and music theory. He has been developing education-related
applications with the Borland languages for more than 15 years. He is the author
of The Tomes of Delphi: Win32 Multimedia API [Wordware Publishing, 2000]
and co-author (with John Penman) of an upcoming book in the Tomes series
on Communications APIs. He has also published a number of articles in various
technical journals. Using Delphi, he specializes in writing custom components
and implementing multimedia capabilities in applications, particularly sound and
music. You can reach Alan on the Internet at acmdoc@aol.com.

Begin Listing One Playing sounds from memory


procedure TForm1.btnStartPlaybackClick(Sender: TObject);
begin
if not OpenSoundPlayingDevice then begin
ShowMessage('Could not open Sound Playing Device');
Exit;
end;
if not PlaySounds(ErrorMsg) then
ShowMessage(ErrorMsg);
end;
function TForm1.GetHighResOutputDevice(
var DeviceToUse: Integer): Boolean;
var
j: Integer;
begin
Result := False;
// Iterate through output devices.
for j := 0 to (FNumOutputDevices-1) do begin
CurrentMMResult := waveOutGetDevCaps(
j, @FWaveOutCaps, SizeOf(TWaveOutCaps));
if CurrentMMResult=MMSYSERR_NOERROR then
DeviceToUse := j;
if ((FWaveOutCaps.dwFormats and
(1 shl WAVE_FORMAT_4S16))=
(1 shl WAVE_FORMAT_4S16)) then begin
Result := True;
Break;
end;
end;
end;
function TForm1.OpenSoundPlayingDevice : Boolean;
var
FormatStruc : tWAVEFORMATEX;
DevNum : Integer;
begin
Result := False;
DevNum := 0;
if not GetHighResOutputDevice(DevNum) then

Sound + Vision
begin
FormatStruc.nChannels := FWaveOutCaps.wChannels;
FormatStruc.nSamplesPerSec := 22050;
end
else
begin
FormatStruc.nChannels := 2;
FormatStruc.nSamplesPerSec := 44100;
end;
FormatStruc.wFormatTag := WAVE_FORMAT_PCM;
FormatStruc.wBitsPerSample := 8;
FormatStruc.nBlockAlign := Round((FormatStruc.nChannels *
FormatStruc.wBitsPerSample) / 8);
FormatStruc.nAvgBytesPerSec :=
FormatStruc.nSamplesPerSec * FormatStruc.nBlockAlign;
FormatStruc.cbSize := 0;
CurrentMMResult := waveOutOpen(@FOutputHandle, DevNum,
(@FormatStruc), DWORD(@WaveOutProc),
DWORD(MainFormHandle), DWORD(callback_function));
Result := (CurrentMMResult=MMSYSERR_NOERROR);
end;
procedure TForm1.LoadNextDataBlock(
var PlaybackLength: DWORD; var TempHdr: PWAVEHdr);
var
TempBuffer: array[0..32767] of Byte;
begin
if FPlaybackBufferPosition<FMainBufferSize then
with TempHdr^ do begin
PlaybackLength :=
FMainBufferSize - FPlaybackBufferPosition;
if PlaybackLength>FBufferBlockSize then
PlaybackLength := FBufferBlockSize;
MainSoundBuffer.Read(TempBuffer, PlaybackLength);
Move(TempBuffer, lpData^, PlaybackLength);
FPlaybackBufferPosition := MainSoundBuffer.Position;
end
else
WaveOutStatus := wosDonePlaying;
end;
function TForm1.PlaySounds(var ResultMsg: string): Boolean;
var
j: Integer;
CurrentLength: Cardinal;
begin
Result := False;
CurrentLength := 0;
MainSoundBuffer.Position := 0;
FPlaybackBufferPosition := 0;
// Make sure you're at the start and not already playing.
if (WaveOutStatus=wosClosed) then begin
WaveOutStatus := wosPlaying;
waveOutPause(FOutputHandle);
// Iterate through two buffers.
for j := 0 to 1 do begin
LoadNextDataBlock(CurrentLength,
FWaveHeaderArray[j]);
FWaveHeaderArray[j].dwBufferLength := CurrentLength;
CurrentMMResult := WaveOutPrepareHeader(
FOutputHandle, PWaveHdr(FWaveHeaderArray[j]),
SizeOf(WAVEHdr));
if CurrentMMResult<>MMSYSERR_NOERROR then begin
ResultMsg := 'Could not prepare header';
Exit;
end;
CurrentMMResult := WaveOutWrite(FOutputHandle,
FWaveHeaderArray[j], SizeOf(WaveHdr));
if CurrentMMResult<>MMSYSERR_NOERROR then begin
WaveOutStatus := wosError;
WaveOutGetErrorText(CurrentMMResult,
PChar(ResultMsg), MsgLength);
Exit;
end;
end;
WaveOutRestart(FOutputHandle);
Result := True;

17 March 2001 Delphi Informant Magazine

end;
end;
function TForm1.ContinuePlaying(WaveHdrPtr: Integer;
var ResultMsg: string): Boolean;
var
j : Integer;
CurrentLength : Cardinal;
TempWavHdr : PWaveHdr;
begin
Result := False;
TempWavHdr := PWaveHdr(WaveHdrPtr);
CurrentLength := 0;
if WaveOutStatus = wosPlaying then
begin
LoadNextDataBlock(
CurrentLength, PWaveHdr(TempWavHdr));
TempWavHdr.dwBufferLength := CurrentLength;
CurrentMMResult := WaveOutPrepareHeader(
FOutputHandle, TempWavHdr, SizeOf(WAVEHdr));
CurrentMMResult := WaveOutWrite(FOutputHandle,
TempWavHdr, SizeOf(WaveHdr));
Result := CurrentMMResult=MMSYSERR_NOERROR;
if not Result then Exit;
end
else
begin
// Iterate through two buffers.
for j := 0 to (MaxBuffers-1) do
if ((FWaveHeaderArray[j].dwFlags and
(1 shl WHdr_InQueue))=(1 shl WHdr_InQueue)) then
begin
Result := True;
Exit;
end;
end;
end;
procedure TForm1.CloseDownPlayback;
var
j: Integer;
begin
if (WaveOutStatus = wosClosed) then Exit;
Label1.Caption := ClosingDownPlaybackMsg;
Label1.Invalidate;
// Always call waveOutReset before waveOutClose.
CurrentMMResult := WaveOutReset(FOutputHandle);
for j := 0 to 1 do
CurrentMMResult :=WaveOutUnprepareHeader(
FOutputHandle, FWaveHeaderArray[j], SizeOf(WAVEHdr));
CurrentMMResult := WaveOutClose(FOutputHandle);
WaveOutStatus := wosClosed;
end;
end;

End Listing One


Begin Listing Two Showing input and output
properties
procedure TForm1.btnShowWaveInPropertiesClick(
Sender: TObject);
var
k: Integer;
j: Integer;
Node, FormatNode: TTreeNode;
begin
TreeView1.Items.Clear;
for j := (FNumInputDevices-1) downto 0 do begin
waveInGetDevCaps(j, @FWaveInCaps, SizeOf(TWaveInCaps));
Node := TreeView1.Items.AddChildFirst(
nil, 'Device ' + IntToStr(J+1));
TreeView1.Items.AddChild(Node, 'Manufacturer ID: ' +
IntToStr(FWaveInCaps.wMid));
TreeView1.Items.AddChild(Node, 'Product ID: ' +
IntToStr(FWaveInCaps.wPid));
TreeView1.Items.AddChild(Node, 'Driver Version: ' +

Sound + Vision
IntToStr(hi(FWaveInCaps.vDriverVersion)) +
'.' + IntToStr(lo(FWaveInCaps.vDriverVersion)));
TreeView1.Items.AddChild(Node, 'Product Name: ' +
FWaveInCaps.szPname);
FormatNode := TreeView1.Items.AddChild(
Node, 'Formats Supported: ');
for k := 0 to (WaveFormatTotal-1) do
if (FWaveInCaps.dwFormats and
(1 shl (k+1))=(1 shl (k+1))) then
TreeView1.Items.AddChild(
FormatNode, WaveFormatArray[k]);
FormatNode := TreeView1.Items.AddChild(
Node, 'Functions Supported: ');
TreeView1.Items.AddChild(Node, 'Channels Supported: ' +
IntToStr(FWaveInCaps.wChannels));
end;
end;
procedure TForm1.btnShowWaveOutPropertiesClick(
Sender: TObject);
var
k: Integer;
j: Integer;
Node, FormatNode: TTreeNode;
begin
TreeView1.Items.Clear;
for j := (FNumOutputDevices-1) downto 0 do begin
waveOutGetDevCaps(j, @FWaveOutCaps,
SizeOf(TWaveOutCaps));
Node := TreeView1.Items.AddChildFirst(
nil, 'Device ' + IntToStr(J+1));
TreeView1.Items.AddChild(Node, 'Manufacturer ID: ' +
IntToStr(FWaveOutCaps.wMid));
TreeView1.Items.AddChild(Node, 'Product ID: ' +
IntToStr(FWaveOutCaps.wPid));
TreeView1.Items.AddChild(Node, 'Driver Version: ' +
IntToStr(hi(FWaveOutCaps.vDriverVersion)) +
'.' + IntToStr(lo(FWaveOutCaps.vDriverVersion)));
TreeView1.Items.AddChild(Node, 'Product Name: ' +
FWaveOutCaps.szPname);
FormatNode := TreeView1.Items.AddChild(
Node, 'Formats Supported: ');
for k := 0 to (WaveFormatTotal-1) do
if (FWaveOutCaps.dwFormats and
(1 shl (k+1))=(1 shl (k+1))) then
TreeView1.Items.AddChild(
FormatNode, WaveFormatArray[k]);
FormatNode := TreeView1.Items.AddChild(
Node, 'Functions Supported: ');
for k := 0 to WaveFunctionTotal-1 do
if (FWaveOutCaps.dwSupport and
(1 shl (k+1))=(1 shl (k+1))) then
TreeView1.Items.AddChild(
FormatNode, WaveFunctionArray[k]);
TreeView1.Items.AddChild(Node, 'Channels Supported: ' +
IntToStr(FWaveOutCaps.wChannels));
end;
end;

End Listing Two

18 March 2001 Delphi Informant Magazine

Dynamic Delphi

ISAPI / NSAPI / CGI / Web Development / Delphi 3-5

By Shiv Kumar

ISAPI Development
Part II: Using Forms to Dynamically Generate Web Pages

ast month in Part I, you constructed your first ISAPI application with Delphi. You also
learned how to deal with Web browser requests and responses to and from your ISAPI
application. In Part II, youll build a larger ISAPI application by having Web site owners
add a link to their site from your Web site. Instead of manually making these additions in
your database, youll learn how to automate this process by:





Generating a form, allowing users to enter data.


Validating and posting information to a database.
Displaying information stored in a database in
a dynamically generated Web page.

Creating the HTML Form


To generate an HTML form dynamically from
your ISAPI application, youll need to create a
Web server application named AddLink, and add
an action to it named AddLinkPage. The URL
to this page will be http://ServerName/scripts/
AddLink.dll/AddLinkPage, where ServerName is
the name of your PC or Web server, or the domain
name of your Web site. Please note the word
scripts in the URL. Remember that all ISAPI and
CGI applications need to be in the scripts folder
(cgi-bin on some Web servers) of the Web server
(C:\inetpub\scripts), or any folder with access rights
similar to that of the scripts folder.
Start Delphi, and select File | New. Then select
Web Server Application from the New tab of the
New Items dialog box. At the next dialog box,
accept the default option, ISAPI/NSAPI Dynamic

Figure 1: The Actions editor with the AddLinkPage action.

19 March 2001 Delphi Informant Magazine

Link Library (there are CGI and Win-CGI options


as well). You should then see a new project with
a Web Module and its unit.

At this point, go into the Project Options dialog


box (Project | Options) and set the Output directory
on the Directories/Conditionals tab to the scripts
folder of your Web server, e.g. C:\inetpub\scripts.
Now, when your application is compiled, the DLL
will be generated in the correct folder. Next, save
the project. (A Web Module is similar to a Data
Module, in that you place the Data Access components and any other components in it.)
To add actions to your Web application, either
click on the ellipsis for the Actions property in
the Object Inspector, or double-click on the Web
Module to display the Action editor (in Delphi 5,
dont double-click in the TreeView section).
Once the Actions editor is active, add an action
to the Web Module by clicking on the Add New
speedbutton. In the Object Inspector, ll in the
PathInfo property as /AddLinkPage, and set the
Name property to waAddLinkPage (see Figure 1).
Select the Events tab of the Object Inspector and
generate the OnAction event for this action item.
In the event handler, type in the code shown in
Listing One on page 23. Please note that you
may want to use the variable NewLine in your
Web applications to make the generated HTML
more readable and easier to debug. Declare it as
a constant in the interface section of the unit as:

Dynamic Delphi
const
NewLine = #13#10;

Instead of NewLine, you may prefer crlf because its easier to type.
Also, remember that you dont need to have a new line (you only
do this to format the generated HTML); the browser will render
the page in exactly the same way, with or without the new line.
Take a closer look at the forms actions:
'<form action="' + Request.ScriptName +
'/PostLinkInfo" method="post"' + NewLine +
' title="Enter the information required ' +
'to Add a link to your Web Site.">'

The Request objects ScriptName property returns the name and path
of the ISAPI DLL. In this case, its /scripts/AddLink.dll. Therefore, the value of the action attribute of the form is:
/scripts/AddLink.dll/PostLinkInfo

This action tells the browser what to do when the form is submitted by the Submit button. It becomes obvious then that you need
a new action in your project with the PathInfo property set to
/PostLinkInfo. In other words, when this form is submitted, the
Web Brokers dispatcher will look for an action whose PathInfo
property is /PostLinkInfo, and re its OnAction event. This
is where you write code to validate and post the information
received.
The Method attribute generally can be either POST or GET. (The
difference between the two will be explored later.) The Title attribute
produces the hint or tool tip for the form in the browser.
For now, return to the OnAction event shown in Listing One.
Notice youre sending back (by using the Response objects Content
property) the HTML required for a form. The Content property is a
string that is sent back via the Web server to the requesting browser.
For this example, visitors (who are interested in adding a link to
their site from your Web site) need to ll in the following elds:
 The URL to their site.
 The links title to appear on your Web site.
 A short description of their Web site.
 The Web site owners rst name.
 The Web site owners last name.
 The Web site owners e-mail address.
By looking at the form in Figure 2, youll notice that the URL eld
already has the text http://. This is done when the form is generated,
by entering http:// in the value attribute as follows:
'<td><font face="Tahoma"><input type="text" ' +
'name="txtLinkURL"' + NewLine + ' style="height: ' +
'22px; width: 274px" value="http://"></font></td>' +
'</tr>' + NewLine +

Figure 2: The dynamically generated data entry screen in a


browser.
http://ServerName/scripts/AddLink.dll/AddLinkPage

However, before you can use this URL, your Web server and HTTP
service must be started. You also need to compile your application
and make sure that AddLink.dll exists in your Web servers scripts
folder. Remember to replace ServerName with your PCs name, IP
address (127.0.0.1), or domain name. After youve completed these
tasks, you can type in the URL and see the form (see Figure 2).
Tip: If you set the Default property of AddLinkPage to True, you
can simply type in http://ServerName/scripts/AddLink.dll and
get the same form. (There can be only one default action in an
ISAPI/CGI project.)

Validating and Posting to a Database


Now that you have a form that allows you to capture data, you need
to retrieve this data, validate it, and post it to a database. The database
parts of this project are like any other Delphi database application.
First, create an action item that corresponds to the forms action:
<FORM action="' + Request.ScriptName +
'/PostLinkInfo" method="post">

This evaluates to /scripts/AddLink.dll/PostLinkInfo.


Set the PathInfo property to /PostLinkInfo, and the Name property
to waPostLinkInfo. Then write an event handler for the OnAction
event of this action item. Before you do all of this, a database needs
to be in place.
For this example, youll use an Access database. Access is ne for small
ISAPI/CGI applications, but ideally you should use an RDBMS. The
name of the table in the Access .mdb le is LINKS. Its structure is
shown in Figure 3.

Notice also that each eld on the form (recognized by the <input>
tag) has a unique name such as txtLinkURL. Later, youll use these
names to extract the values for each of the required elds in order to
post this information to a database.

Create an ODBC DSN for this .mdb le called WEBSITE_ODBC.


(Those of you who would rather use InterBase or SQL Server can do
so, paying attention to the required changes.)

So far, you have an action item in your ISAPI DLL that will generate
a form. To call this action, you would enter the following URL in
your Web browsers address eld:

Now, add Data Access components and set a few properties. Drop
Database, Query, and Session components on the Web Module. Set the
AliasName property of the database component to WEBSITE_ODBC,

20 March 2001 Delphi Informant Magazine

Dynamic Delphi
the DatabaseName property of the database component to
DELPHILINKS, the DatabaseName of Query1 to DELPHILINKS, and
the Login property of the Database component to False. Finally, set the
Params property of the Database component to:
username=admin
password=

Setting the LogInPrompt property to False and setting the parameters


above will allow your ISAPI or CGI application to log in to the
database non-interactively. ISAPI applications run in the process
space of the Web server. In the case of IIS, this is a service. By default,
service applications dont interact with the users desktop, and you
wont even see the log-in dialog box on the server.
You also need to set the AutoSessionName of the Session component
to True. Remember that an ISAPI DLL runs in the Web server
process. Each time a request is made to the ISAPI DLL, a new
thread is spawned, i.e. an instance of the Web Module is created
in this thread. To be able to connect to a database multiple times
using the same instance, you need to provide a unique session
name for each connection. The
Session component handles that
Key Field Name
Type
when you set the AutoSessionYes LinkURL
Text
Name property to True, although
Title
Text
dont use this in your CGI appliDescription
Text
cations. The Web server creates
DateAdded
Text
a new instance of your CGI
OwnerFirstName Text
application for each request, so
OwnerLastName Text
a unique session name is not
EmailAddress
Text
required. Once youve completed Figure 3: The structure of the
these steps, your Web Module
LINKS table in the Access .mdb
should look like Figure 4.
file.

Processing the Forms Data


Every actions OnAction event makes two objects available to you
the Request and Response objects. So far youve used the Request
objects ScriptName property.
The Request.ScriptName property evaluates to /scripts/
Its good practice to use the ScriptName property, and
not to hard-code the scripts name. Later, if you change the name
of the script or path of the scripts folder, you wont have to modify
your code.

AddLink.DLL.

Also note that the Request and Response objects are actually public
properties of the Web Module, which means theyre accessible
throughout the module even in your own methods.
The data of the form is available in the ContentFields property
of the Request object because the POST method has been used
(method=post) Request because it comes to this event as a request
made by the browser via the Web server. If you use the GET method
in the form, then the same information would be made available to
you in the QueryFields property of the Request object.
To make data validation easier, rst create two custom Exception
objects in the type section that can be raised if any part of the data
is invalid:
type
EInsufficientInfo = class(Exception);
EInvalidURL = class(Exception);
...

21 March 2001 Delphi Informant Magazine

Figure 4: The Web Module with Data Access components.

The ContentFields and QueryFields properties are TStrings descendants. The forms data is made available to you in the form of
name=value pairs, so the data entered in the URL eld looks like
txtLinkURL=http://www.matlus.com. Therefore, you can use the
Values method of the ContentFields object to get the value for a given
eld name. The code in this action items OnAction event can be seen
in Listing Two beginning on page 23.
Notice that the error information is sent back to the browser
in the form of HTML, so the user is informed of the error(s).
This also comes in handy while debugging, because youll see
all errors in the browser even a key-violation error, or some
other database-related error. As mentioned earlier, the database
programming aspect is like that of any other Delphi database
application.
Now, look at the following statement:
Response.SendRedirect(
Request.ScriptName + '/ShowDelphiLinks');

If the data was posted successfully to the table, youll want to show
a list of links in your database. This line will redirect the browser to
the action in your ISAPI DLL to make that happen. You have yet to
create that action and the code for it, but youll do that soon.
The code for the URLIsValid function is:
function TWebMod1.URLIsValid(URL: string): Boolean;
begin
try
// If URL isn't found, an exception will be raised.
IdHTTP1.Get(URL);
Result := True;
except
Result := False;
end;
end;

Use the new Indy (originally Winshoes) idHTTP component to help


with validating the URL. You can download the Indy suite of components from http://www.nevrona.com/Indy/. I personally prefer to
use the Indy components, even though theyre standard in Delphi 6.
However, you can use any HTTP component you wish by making the
appropriate changes to the URLIsValid function.
Before you create the waShowDelphiLinks action item, review the differences between the GET and POST methods. The POST method

Dynamic Delphi
Recompiling Your Project
If you need to recompile your project for any reason as an ISAPI DLL,
you must stop and restart the WWW service, and then recompile
your application. (This isnt necessary for a CGI application). If youre
using Internet Information Server (IIS), you can uncheck the Cache
ISAPI Applications checkbox in the Internet Service Manager options.
Be sure to turn it on in your production Web server machine. IIS has
the ISAPI DLL in use, and therefore wont allow you to overwrite the
file until it is unloaded from memory. This is accomplished by either
shutting down the service (closing the program), or selecting an option
in the Web server to unload the DLL after each use.
To release the lock on a DLL, Personal Web Server (PWS) must be
manually stopped using the command prompt. By default, Pws.exe
resides in the folder C:\Windows\System\Inetsrv. To stop PWS, type
the following command:
Windows\System\inetsrv\pws.exe /stop

Figure 5: The default page of the ISAPI application.

You can then restart PWS manually from the command prompt, thus:
Windows\System\inetsrv\pws.exe /start

has been used in this article. If you use the GET method, the URL
changes when you hit the Submit button. The browser will add to the
URL path the information as parameters to send back to the server.
The URL would look like:

Once PWS is stopped, this will release the ISAPI DLL. Recompile the
DLL, if necessary. Once PWS restarts, it will again lock the DLL when
its instantiated by your browser.

http://ServerName/scripts/AddLink.dll/
PostLinkInfo?txtLinkURL=http://mywebsite.com&txtTitle=
My+Site+Title&txtDescription=My&Sites&description

For IIS 4.0. At the run line, type net stop iisadmin /y, and then
press J. This will stop all services while recompiling your DLL.

Your application accesses the data from the Request objects QueryFields
property, rather than its ContentFields property. It extracts the values
from Request.QueryFields in the same manner as Request.ContentFields,
because both properties are TStrings descendants.
The advantage of the GET method is that it doesnt require a form
to send information back to your application. You can simply append
the value you need to send to the end of the URL. In the POST
method, the information is captured from the form and sent as a
separate stream. Of course, you may nd that the POST method is
advantageous because the information doesnt appear in the address
box of the browser. Additionally, the POST method can handle much
greater volumes of information. The GET method has a limit to the
amount of data that can be sent in this way. Also, the URL needs to
be encoded. The browser does this automatically for you when the
GET method is used in a form.
Tip: The HTTPApp.pas unit contains two functions, named
HTTPEncode and HTTPDecode, that will come in handy for future
ISAPI/CGI work.

Displaying Database Information on the Web


You can display database information on the Web in two ways. The
simpler way is to use the TQueryTableProducer component that comes
with Delphi. The second way is to do the work manually.
To display the information manually (in this case, the links to other
sites from your database), create a new action item called
waShowDelphiLinks, and set its PathInfo property to
/ShowDelphiLinks. Then, drop another Query component on the
Web Module, and set its DatabaseName property to DELPHILINKS,
and its SQL property to:
22 March 2001 Delphi Informant Magazine

After recompiling your DLL, type net start w3svc from the run
line. Your services will be up again. This application uses the Services
applet to stop/start the WWW service.
For IIS 5.0.The command line is simply iisreset, or you can use the
Service administrative tool to stop-start the WWW service.
Shiv Kumar

SELECT * FROM LINKS


ORDER BY Title

Now, enter the code in Listing Three (on page 24), in the OnAction
event. Notice this fragment in Listing Three:
'<img src="/Images/DelphiLinksHead.gif">' + NewLine +
'<a href="' + Request.ScriptName + '/AddLinkPage">' +
'<img alt="Add a link to your Web site from here" ' +
'border="0" src="' + '/Images/AddLink.gif"></a>' +
NewLine + '<p/><hr/>' + NewLine + '<table>' + NewLine;

Two images are referenced here with the <img> tags. These images
should be in the Images subfolder of your Web servers root folder.
You can call this subfolder anything you like, but be sure to
change the code to match your subfolders name. [Note: Its good
practice to store your images in a separate folder, rather than in
the root folder. They are easier to manage this way.]
The second image in the previous code is hyperlinked. This link
will direct users to the data entry screen. Also notice the line
with the Format procedure. This line of code results in a string
like this:

Dynamic Delphi
<a href="http://www.matlus.com">
<b>The Delphi Apostle</b></a>

In the browser, this code will render The Delphi Apostle as


hyperlinked text. When you click on the hyperlink, the browser
will direct you to the URL specified in the HREF attribute of
the anchor <a> tag. The URL for this page is http://ServerName/
scripts/AddLink.dll/ShowDelphiLinks. If you have data in your
table, the generated Web page should look like Figure 5. To have
users hit this page by default, set the Default property of this action item
to True. This sets all other action item Default properties to False.

Conclusion

There you have it; an ISAPI application where users can enter data
into a form, send the information back to your database, and have
it appear on your Web site in a dynamically generated HTML page.
In the next part of this series, youll learn how to extract images from
a database table, shrink images (thumbnails) on the fly to reduce the
required bandwidth, and increase the speed of your Web site.
The files referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103SK.

Shiv Kumar works in R&D at DataSource Inc. He develops new product ideas,
prototypes them, and leads the implementation effort. Shiv, an ex-VB programmer,
has used Delphi exclusively since Delphi 1. His hobbies include electronics and
photography, and he loves to ride his CBR 1100XX at every opportunity. You can
contact Shiv at shiv@matlus.com.

Begin Listing One OnAction event of waAddLinkPage


procedure TWebMod1.WebMod1waAddLinkPageAction(
Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
Response.Content := '<html>'+NewLine+'<head>'+NewLine +
' <title>Add a Link to your Web Site</title>' +
NewLine + '</head>' + NewLine + '<body>' + NewLine +
'<h2>Please Enter the Required information to be ' +
'able to link your Site from here.</h2>' + NewLine +
'<form action="' + Request.ScriptName +
'/PostLinkInfo" method="post"' + NewLine +
' title="Enter the information required to Add a ' +
'link to your Web Site.">' + NewLine + '<table>' +
NewLine + '<tr>' + NewLine + '<td><font face=' +
'"Tahoma">URL to your Web Site</font></td>' + NewLine +
'<td><font face="Tahoma"><input type="text" ' +
'name="txtLinkURL"' + NewLine + ' style="height: ' +
'22px; width: 274px" value="http:// "></font></td>' +
'</tr>' + NewLine + '<tr>' + NewLine + '<td><font ' +
'face="Tahoma">Title to be Shown</font></td>' +
NewLine + '<td><font face="Tahoma"><input type=' +
'"text" name="txtTitle"' + NewLine + ' style=' +
'"height: 22px; width: 274px"></font></td></tr>' +
NewLine + '<tr>' + NewLine + '<td><font face=' +
'"Tahoma">Description to be shown</font></td>' +
NewLine + '<td><textarea name="txtDescription" style='+
'"height: 80px; width: 274px"></textarea></td></tr>' +
NewLine + '<tr>' + NewLine + '<td><font face=' +
'"Tahoma">Owner''s First Name</font></td>' + NewLine +
'<td><font face="Tahoma"><input type="text" name=' +
'"txtOwnerFirstName"' + NewLine + ' style="height: ' +
'22px; width: 274px"></font></td></tr>' + NewLine +
'<tr>' + NewLine + '<td><font face="Tahoma">Owner''s'+
' Last Name</font></td>' + NewLine + '<td><font face='+
'"Tahoma"><input type="text" name="txtOwnerLastName"' +
NewLine + ' style="height: 22px; width: 274px">' +
'</font></td></tr>' + NewLine + '<tr>' + NewLine +
'<td><font face="Tahoma">Email Address</font></td>' +
NewLine + '<td><font face="Tahoma"><input type="text"'+
' name="txtEmailAddress"' + NewLine + ' style=' +
'"height: 22px; width: 274px"></font></td></tr>' +
NewLine + '<tr>' + NewLine + '<td><font' + NewLine +
' face="Tahoma"><input type="submit" name=' +
'"btnSubmit" value="Submit"></font></td>' + NewLine +
'<td><font' + NewLine + ' face="Tahoma"><input type='+
'"reset" value="Reset"></font></td></tr>' + NewLine +
'</table>' + NewLine + '</form>' + NewLine +
'<P><h4>The server will attempt to validate the URL' +
' you are attempting to submit. If you get an error' +
NewLine + ' due to an invalid URL. Please refresh' +
' the page before attempting to re-sumbit.' + NewLine +
' if everything goes fine, you should be re-directed' +
' to the Links Page.' + NewLine + ' You should be' +
' able to see the link you just added.' + NewLine +
'<P>Thank you for taking the time to link your web' +
' site from here.</h4>' + NewLine + '</body>' +
NewLine + '</html>';
end;

End Listing One


Begin Listing Two Parsing, validating, and posting
form data

procedure TWebMod1.WebMod1waPostLinkInfoAction(
Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
{ A custom Exception object called EInsufficientInfo will

23 March 2001 Delphi Informant Magazine

Dynamic Delphi
be raised if any part of the data is invalid. In this
case, invalid = null. }
try
{ Validate data entered by the user. }
if Request.ContentFields.Values['txtLinkURL'] = '' then
raise EInsufficientInfo.Create(
'Please Enter the URL of your Web Site');
if Request.ContentFields.Values['txtTitle'] = '' then
raise EInsufficientInfo.Create('Please Enter the' +
' Title you want Displayed for your Web Site');
if Request.ContentFields.Values[
'txtDescription'] = '' then
raise EInsufficientInfo.Create('Please Enter the' +
' Description you want Displayed for your Web Site');
if Request.ContentFields.Values[
'txtOwnerFirstName'] = '' then
raise EInsufficientInfo.Create(
'Please Enter Your First Name.');
if Request.ContentFields.Values[
'txtOwnerLastName'] = '' then
raise EInsufficientInfo.Create(
'Please Enter Your Last Name');
if Request.ContentFields.Values[
'txtEmailAddress'] = '' then
raise EInsufficientInfo.Create(
'Please Enter your contact Email Address');
{ Validate the URL that the user has entered as well.
To do that, use an HTTP component in the URLIsValid
function. Warn the user if the URL is invalid. }
if not URLIsValid(Request.ContentFields.Values[
'txtLinkURL']) then
raise EInvalidURL.Create('The URL you submitted is' +
' not a valid URL.' + #13#10 + 'Please make sure ' +
'you type a valid URL before submitting');
{ If data and URL are valid, post it to the database. }
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('INSERT INTO LINKS');
Query1.SQL.Add('(LinkURL, Title, Description,
DateAdded, OwnerFirstName, OwnerLastName,
EmailAddress)');
Query1.SQL.Add('Values(:LinkURL, :Title, :Description,
:DateAdded, :OwnerFirstName, :OwnerLastName,
:EmailAddress)');
Query1.ParamByName('LinkURL').AsString :=
Request.ContentFields.Values['txtLinkURL'];
Query1.ParamByName('Title').AsString :=
Request.ContentFields.Values['txtTitle'];
Query1.ParamByName('Description').AsString :=
Request.ContentFields.Values['txtDescription'];
Query1.ParamByName('DateAdded').AsDateTime := Now;
Query1.ParamByName('OwnerFirstName').AsString :=
Request.ContentFields.Values['txtOwnerFirstName'];
Query1.ParamByName('OwnerLastName').AsString :=
Request.ContentFields.Values['txtOwnerLastName'];
Query1.ParamByName('EmailAddress').AsString :=
Request.ContentFields.Values['txtEmailAddress'];
Query1.ExecSQL;
{ If everything is fine, show the user the Links page,
so he can see the link he just added. }
Response.SendRedirect(
Request.ScriptName + '/ShowDelphiLinks');
except
on E : Exception do
Response.Content := Format('<html><head><title>' +
'Error Posting Inforamtion</title>' + '</head>' +
'<body><H1>%s</H1></body></html>', [E.Message]);
end;
end;

End Listing Two

24 March 2001 Delphi Informant Magazine

Begin Listing Three Links page from the database


procedure TWebMod1.WebMod1waShowDelphiLinksAction(
Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
with Query2 do begin
Open;
Response.Content := '<html>' + NewLine + ' <head>' +
NewLine + <title>Delphi Links People have added' +
'</title>' + NewLine + ' </head>' + NewLine +
'<body>' + NewLine +
{ The image is in a subfolder of your Web server's
root folder called Images,
e.g. C:\inetpub\wwwroot\images\ }
'<img src="/Images/DelphiLinksHead.gif">' + NewLine +
'<a href="' + Request.ScriptName + '/AddLinkPage">' +
'<img alt="Add a link to your Web site from here" ' +
'border="0" src="' + '/Images/AddLink.gif"></a>' +
NewLine + '<p/><hr/>' + NewLine + '<table>' +NewLine;
while not Eof do begin
Response.Content := Response.Content + Format(
'<tr>%s<td><table><tr><td><a href="%s"><b>%s</b>' +
'</A></td>%s' + '<td><b>Posted By:</b>%s</td>' +
'%s<td><b>Date Posted:</b>%s</td></tr></table>%s' +
'<b>Description:</b><br>%s%s</td>%s</tr><tr>' +
'<td><hr/></td></tr>', [NewLine, FieldByName(
'LinkURL').AsString, FieldByName('Title').AsString,
NewLine, FieldByName('OwnerFirstName').AsString +
' ' + FieldByName('OwnerLastName').AsString,
NewLine, FieldByName('DateAdded').AsString,
NewLine, FieldByName('Description').AsString,
NewLine, NewLine]);
Next;
end; { while not Eof }
Close;
Response.Content := Response.Content + '</table>' +
NewLine + '</body>' + NewLine + '</html>';
end;
end;

End Listing Three

Greater Delphi

User Interface Issues / Message Handling / Class Design / Delphi 4, 5

By Robert Leahey

Better UI Design
Part II: Building a Message Queue Class

ast month, in this space, we looked at some of the common problems in bad
user interface (UI) design. We indicated that, while the primary goal of good user
interaction design is to make the users more productive, there are common UI problems
that hamper our users ability to be productive.
This month, we examine a possible solution to
one of the more ubiquitous of bad UI design problems, that of excessive message dialog boxes. The
constant barrage of needless confirmation dialog
boxes, status messages, and unhandled exceptions
is a major source of disruption for users.
Do you have things to tell your users? Sure. Do you
have things to ask them? Of course. But by popping up a message every time we have something
to say or ask, we are interrupting the users and
forcing them to deal with our priorities instead of
theirs. Thats rude. Would you want to work with
someone capable of this exchange?
Your files were checked in successfully. OK?
The file was renamed. OK?
The user cancelled the action. OK?
You really screwed up this time and your hard disk
will be reformatted when you click OK. OK?

Figure 1: The
bane of our
existence.

Enough! Its not okay (see


Figures 1 and 2). Stop interrupting me; let me work!
This scenario is the bane
of touch typists everywhere:

Figure 2: The future of software? Not if we can


help it.
25 March 2001 Delphi Informant Magazine

Youre typing along, not looking at your screen,


when suddenly theres a ding. You stop typing and
look at the screen. Nothing is there, but youre
missing the last few characters you typed. What
happened? The ding meant that a message dialog
box appeared while you were typing, but its not
there now. Oh no, that spacebar you hit must
have closed the dialog box with the default button!
What did you just OK?! OK that your disk is
full? OK to send that flame e-mail to your boss?
OK to transfer your life savings to my checking
account? What?! Well, youll never know because
the dialog box was shown at the convenience of
the programmer rather than yours.
The challenge is to create a means by which we
can still provide necessary information to and ask
questions of our users, but allow them to deal with
these interruptions at their convenience instead of
ours. We need to be able to store a list of messages
that are accumulated as the users interact with our
software. We need to know how hot some of these
messages are, e.g. has an exception been raised, or are
we simply telling them that a process is complete? We
need to provide a means for users to access this list,
and finally, we need to fire some sort of event when
users read a message so that we programmers can
take appropriate action when that message is read.
This is obviously a simplified list of requirements
due to our limited space, but its typical of messaging needs. With these needs in mind, let us
examine some classes that can serve as a starting
point for meeting the challenge before us. (The
code referenced in this article and a demonstration
project are available for download; see the end of
this article for details.)

Greater Delphi
unit buiMessageQueue;
interface
uses
SysUtils, Windows, Classes, buiParenting, buiTypes,
buiMessages, buiMessageListInterface;
type
TbuiMessageQueue = class (TbuiParenting)
private
FOnOutOfSynch: TNotifyEvent;
FOutOfSynch: Boolean;
FRemoveWhenRead: Boolean;
function GetContinueProcess: Boolean;
function GetHasUnreadMessages: Boolean;
function GetHighestUnreadPriority: TbuiMessagePriority;
function GetMessageCount: Integer;
function GetMessages(aIndex: Integer): TbuiMessage;
function GetUnreadMessageCount: Integer;
function MessageOfTypeExists(
aMessageClass: TbuiMessageClass): Boolean;
procedure SetOutOfSynch(aOutOfSynch: Boolean);
protected
function AllowNewMessageOfType(
aMessageClass: TbuiMessageClass): Boolean; virtual;
procedure DoOnOutOfSynch;
public
constructor Create;
class function ParentedClass: Boolean; override;
procedure AddMessage(aMessageClass: TbuiMessageClass;
const aCaption, aText, aCategory: string;
aAllowProcess: Boolean;
aPriority: TbuiMessagePriority;
aReadEvent: TbuiReadMessageEvent); virtual;
procedure MessageRead(aIndex: Integer); virtual;
procedure PopulateMessageList(
aMessageList: IbuiMessageList); virtual;
procedure RemoveReadMessages; virtual;
property ContinueProcess: Boolean
read GetContinueProcess;
property HasUnreadMessages: Boolean
read GetHasUnreadMessages;
property HighestUnreadPriority: TbuiMessagePriority
read GetHighestUnreadPriority;
property MessageCount: Integer read GetMessageCount;
property Messages[aIndex: Integer]: TbuiMessage
read GetMessages; default;
property OnOutOfSynch: TNotifyEvent
read FOnOutOfSynch write FOnOutOfSynch;
property OutOfSynch: Boolean read FOutOfSynch;
property RemoveWhenRead: Boolean
read FRemoveWhenRead write FRemoveWhenRead;
property UnreadMessageCount: Integer
read GetUnreadMessageCount;
end;

Figure 3: The interface section of buiMessageQueue.pas.

Classes Overview
The following classes make up this messaging queue system. Note:
For the code presented in this series of articles, Ive adopted the prex
of bui for Better UI.
 TbuiParenting (declared in buiParenting.pas) is the ancestor for
the queue and message classes. It introduces behaviors specic to
parent/child relationships.
 TbuiMessageQueue (declared in buiMessageQueue.pas) is the
queue class. It manages a list of messages, allowing the programmer to add and retrieve them for users.
 TbuiMessage (declared in buiMessages.pas) is a class representing a message that is sent to the queue. Instances of this class
26 March 2001 Delphi Informant Magazine

constructor TbuiMessageQueue.Create;
begin
inherited Create;
FOutOfSynch := True;
FRemoveWhenRead := True;
end;
class function TbuiMessageQueue.ParentedClass: Boolean;
begin
Result := False;
end;

Figure 4: The TbuiMessageQueue constructor and ParentedClass


method.




are created by the queue object when its AddMessage method


is called.
IbuiMessageList (declared in buiMessageListInterface.pas) is an
interface that describes a class whose implementation will allow
the queue to ll its list with messages for the users.
TbuiTreeView (declared in buiControls.pas) is a component that
is a custom implementation of the IbuiMessageList interface
intended as an example of how it can be implemented.

TbuiMessageQueue Class
Central to our discussion is the class that manages the messages
we want to send to users TbuiMessageQueue. The code for the
interface section of the buiMessageQueue unit is shown in Figure 3.
First, lets look at the public section of TbuiMessageQueue. The
constructor is simple, initializing two elds. ParentedClass is a class
function introduced in TbuiParenting and overridden here to return
a value of False, indicating that this class doesnt have a parent. The
implementations of these two methods are shown in Figure 4.
The next four methods are involved in managing the messages.
AddMessage is called when we need to send a message to the
queue. It has several parameters, but theyre fairly straightforward:
aMessageClass is the type of message were sending. Well discuss
TbuiMessage momentarily, but for now its enough to say that when
we call AddMessage, we pass in the class type of the TbuiMessage
descendant we want the message queue to create. The aCaption,
aText, and aCategory parameters are strings that make up the
message. aAllowProcess is a Boolean value that allows us to
specify whether we want the application to continue accepting
input from the users until they have read the message. If
the queue contains a message for which aAllowProcess is False
and unread, TbuiMessageQueue.ContinueProcess will be False. The
aPriority parameter allows you to set the relative importance of your
message in ve degrees dened by the TbuiMessagePriority type.
Finally, the aReadEvent parameter allows you to specify an event
handler that will be called when the user reads this message.
You can call MessageRead when your users have read a message
from an implementation of IbuiMessageList. Calling MessageRead
will cause the queue object to re the event handler you specied
in AddMessage. The aIndex parameter is the index of the message
thats been read. This value will be provided when the queue object
populates your IbuiMessageList implementation.
The PopulateMessageList method is called to retrieve the messages that
are stored in the queue. You can pass in any implementation of the
IbuiMessageList interface, and the queue object will call its methods
to ll it with the messages.

Greater Delphi
Finally, you can call RemoveReadMessages at any time to force the
queue object to delete any messages that have been read from its list.
To see how these four methods are implemented, see Listing One
(beginning on page 30).
Next, well look at the TbuiMessageQueue class properties (again, see
Figure 3). ContinueProcess is a Boolean property that indicates if any
of the messages owned by the queue need to be read before the
users can continue. By reading this property, you can indicate to your
users visually when theres a message that requires their immediate
attention.
Note that, while it may seem that this is no different than stopping users progress with a pop-up message dialog box, there is,
in fact, a difference. The message dialog box still grabs users
attention with a jarring pop, but with the message queue method
we can provide our users with a much more subtle indicator that
we need their attention. By using a method such as a ashing icon
on the status bar, or by turning the border of their workspace a
different color, we allow the users to discover the message and
take the initiative to read it themselves. This is admittedly a
slight difference, but its the type of difference that can make an
application more enjoyable to use.
HasUnreadMessages indicates whether there are unread messages in
the queue. HighestUnreadPriority polls the queued messages for the
message with the highest priority that has not been read. You can use
this information to alter your applications appearance in some way to
indicate to users the relative importance of the messages waiting for
them. MessageCount is a count of the messages currently stored in the
queue both read and unread.
The indexed Messages property allows you access to the queued messages. OnOutOfSynch is an event that is red anytime the message

function TbuiMessageQueue.AllowNewMessageOfType(
aMessageClass: TbuiMessageClass): Boolean;
begin
Result := True;
if not (aMessageClass.AllowDuplicates) and
MessageOfTypeExists(aMessageClass) then
Result := False;
end;
procedure TbuiMessageQueue.DoOnOutOfSynch;
begin
if Assigned(FOnOutOfSynch) then
FOnOutOfSynch(Self);
end;
function TbuiMessageQueue.MessageOfTypeExists(
aMessageClass: TbuiMessageClass): Boolean;
var
liIndex: Integer;
begin
Result := False;
liIndex := 0;
while not(Result) and (liIndex < MessageCount) do begin
if (Messages[liIndex].ClassType = aMessageClass) then
Result := True
else
Inc(liIndex);
end;
end;

queue changes in such a way that it would be out of sync with any
populated IbuiMessageList implementations. This allows you to call
PopulateMessageList again when the queue changes. OutOfSynch is
the Boolean representation of the synched state of the queue and a
populated list. Set the RemoveWhenRead property to indicate whether
the queue object should delete any messages that have been read.
Finally, the UnreadMessageCount property returns a count of the
unread messages in the queue. Listing Two (on page 31) shows
the implementation of the read-and-write access methods of these
properties.
There are three other methods of TbuiMessageQueue that we
need to examine: MessageOfTypeExists in the private section,
and AllowNewMessageOfType and DoOnOutOfSynch in the
protected section.
MessageOfTypeExists simply takes a parameter of a TbuiMessageClass
type and determines whether a message of this class exists in its list.
This method is used by AllowNewMessageOfType, which determines
whether a new message of a given TbuiMessageClass can be added.
If the given TbuiMessages class method, AllowDuplicates, returns
False and there is already a message of that class type in the queue,
no more will be added.
Finally, DoOnOutOfSynch is a dispatch method to re the
OnOutOfSynch event. Figure 5 shows the implementation of
these methods.
unit buiMessages;
interface
uses
SysUtils, Windows, buiParenting, buiTypes;
type
TbuiReadMessageEvent = procedure(Sender: TObject;
const aMessageText: string;
var aRead: Boolean) of object;
TbuiMessage = class(TbuiParenting)
private
FCaption: string;
FCategory: string;
FContinueProcess: Boolean;
FMessageText: string;
FOnReadMessage: TbuiReadMessageEvent;
FPriority: TbuiMessagePriority;
FRead: Boolean;
public
constructor Create;
class function AllowDuplicates: Boolean; virtual;
procedure DoReadMessage(aSender: TObject);
property Caption: string read FCaption write FCaption;
property Category: string
read FCategory write FCategory;
property ContinueProcess: Boolean
read FContinueProcess write FContinueProcess;
property MessageText: string
read FMessageText write FMessageText;
property OnReadMessage: TbuiReadMessageEvent
read FOnReadMessage write FOnReadMessage;
property Priority: TbuiMessagePriority
read FPriority write FPriority;
property Read: Boolean read FRead write FRead;
end;
TbuiMessageClass = class of TbuiMessage;

Figure 5: Implementing MessageOfTypeExists,


AllowNewMessageOfType, and DoOnOutOfSynch.
27 March 2001 Delphi Informant Magazine

Figure 6: The interface section of buiMessages.pas.

Greater Delphi
TbuiParenting Class
As ancestor to TbuiMessageQueue and TbuiMessage, TbuiParenting
provides some basic functionality for managing parent/child relationships. Listing Three (beginning on page 31) shows the entire
buiParenting.pas unit.

constructor TbuiMessage.Create;
begin
inherited Create;
FRead := False;
FPriority := mpNormal;
end;

TbuiMessage is also a descendant of TbuiParenting. This class and


its descendants are created by, and managed by, TbuiMessageQueue.
Figure 6 shows the interface to the buiMessages.pas unit.

class function TbuiMessage.AllowDuplicates: Boolean;


begin
Result := True;
end;

Again, well examine the public section of TbuiMessage rst. The constructor initializes two elds while the class method AllowDuplicates is
introduced. AllowDuplicates allows you to create a message class that
allows only one instance within the queue object. To do so, you would
create a descendant that would override AllowDuplicates to return False.
Figure 7 shows the implementation of these two methods.
DoReadMessage is the dispatch method for the OnReadMessage event.
Figure 8 shows its implementation.
Next, well take a look at the properties of TbuiMessage.
Caption is a string that describes the message to the users.
When TbuiMessageQueue.PopulateMessageList is called, it passes the
Caption property to the IbuiMessageList implementation. In the
case of TbuiTreeView, which implements IbuiMessageList, Caption is
used as the text for the tree nodes.
The Category property is similar in usage to the Caption property,
but it can be used by IbuiMessageList implementations to categorize messages. With TbuiTreeView, we create parent nodes by
Category and add message nodes beneath them. ContinueProcess
is a Boolean property that indicates whether we want our application to allow processing until this message has been read. This
property is accessed by TbuiMessageQueue.GetContinueProcess as
discussed before.
The MessageText string property is the actual text of the message.
OnReadMessage is an event that will re when the users read this
message. Calling TbuiMessageQueue.MessageRead will cause the specied event handler to re. Priority is of type TbuiMessagePriority as
dened in buiTypes.pas. Heres the declaration:
TbuiMessagePriority = (mpLowest, mpLow, mpNormal,
mpHigh, mpHighest);

And lastly, Read. This Boolean indicates whether this message has
been read by the users.

IbuiMessageList Interface
The IbuiMessageList interface is provided as a way for
TbuiMessageQueue to populate some sort of list with the messages
it contains. (Note: Delphis interfaces are outside the scope of
this article. Search Delphi online help for Object Interfaces to
learn more.) Sufce it to say that any object that implements a
dened interface can be used where that interface is expected. For
our purposes, we have dened an interface that provides a way
for TbuiMessageQueue to add messages to a list. Its up to the
implementing class to dene what should be done with the messages it receives. Figure 9 shows the buiMessageListInterface unit.
TbuiMessageQueue.PopulateMessageList (see Figure 10) is the method
that accepts IbuiMessageList implementations.

28 March 2001 Delphi Informant Magazine

procedure TbuiMessage.DoReadMessage(aSender: TObject);


var
lbRead: Boolean;
begin
lbRead := True;
if Assigned(FOnReadMessage) then
FOnReadMessage(aSender, MessageText, lbRead);
Read := lbRead;
end;

Figure 7 (Top): The TbuiMessage class constructor and


AllowDuplicates method. Figure 8 (Bottom): Implementing
DoReadMessage.

Notice that this method takes an IbuiMessageList implementation,


calls its ClearList method to clear its previous list, and then iterates
through its queue of messages, calling AddMessage for each. The class
implementing IbuiMessageList is responsible for what to do with the
messages, so lets take a look at an example.

TbuiTreeView Class
The TbuiTreeView component is provided as an example implementation of the IbuiMessageList interface. Obviously, you can create
your own implementations, but this component serves as an excellent
example of how an implementation might be realized. The code for
TbuiTreeView from the buiControls unit can be seen in Listing Four
(beginning on page 32).
TbuiTreeView is fairly self evident, so well forego direct examination
of the component in favor of seeing it in action.

The Message Queue Class at Work


All this code is pointless unless we see the classes in their native
habitat. What follows is an overview of how these classes were used in
a sample project to test their effectiveness.
Lets review for a moment, in case your eyes have glazed over
from all the code samples. Striving for better user interaction,
our goal is to make our users more productive. As a result, weve
determined that one of the great hindrances of modern software
design to user productivity is the overabundance of messages we
send to our users. With that in mind, these classes have been
designed to allow us to create an application in which its possible
to display no message dialog boxes.
Thus, for the purposes of testing and demonstrating these classes, I
built a small text editor (see Figure 11) that sends messages to users
via the message queue object. The main form owns and creates an
instance of the TbuiMessageQueue class. Near the bottom of the form
is an instance of TbuiTreeView to contain the messages. Also visible is
an instance of TbuiPopupMenu, another IbuiMessageList implementation which, while not covered here, is in the source code for this
demonstration application.

Greater Delphi
Implementing an event handler for the message queue objects
OnOutOfSynch event (which will re every time we add
or remove a message from the queue), we call
TbuiMessageQueue.PopulateMessageList for our IbuiMessageList
implementing controls. This way, every time a message is added to
the queue, the lists are updated. In addition, we check the queues
HighestUnreadPriority property so we can alter the appearance of the
application as higher priority messages arrive in the queue.
If you look at Figure 11 again, youll notice a red border around the
text workspace and a red ag on a button in the lower-left corner of
the window. These visual cues indicate to the user that a very high
priority message is in the queue an access violation in this case.
Because of the severity of this message, I wanted to limit the users
ability to proceed until he or she had read this message. To that
end, most of the event handlers in this application check the value
of the queue objects ContinueProcess property before executing their
code. With an access violation unread in the queue, the value would
be False, and the application would visually indicate that the users
should check the messages by ashing the red border.
In the tree views OnChange event, we call the queue objects MessageRead
method. Part of the declaration of the IbuiMessageLists AddMessage
method is the aMessageIndex parameter. By storing this index in our tree
unit buiMessageListInterface;
interface
uses
SysUtils, Windows, buiTypes;
type
IbuiMessageList = interface (IUnknown)
['{ 1B464763-A224-11D4-9EC9-F04C51C10100 }']
procedure AddMessage(const aCategory, aCaption,
aMessageText: string; aPriority: TbuiMessagePriority;
aRead: Boolean; aMessageIndex: Integer); stdcall;
procedure ClearList; stdcall;
end;

nodes Data property, we can pass that value back to the queue in the
MessageRead method. Obviously, this is a supercial look at the use of
these classes, so its recommended that you download the demonstration
and examine it more closely.

Now Wait Just a Minute


Theres an important point that might be lost if I dont belabor it. If
you think you can use these classes to simply shunt all your messages
into the queue and let your users wade through them, then Ive allowed
you to miss the point. Given two types of messages, expected and
unexpected, good UI design would require that you use these classes
only for the unexpected. If we seek to rid our applications of message
dialog boxes, then any messages that can be planned for should be
designed into the application workspace. For instance, if you know its
possible for your users to create an invalid state within your application,
your workspace should visually indicate when its in that state.
Heres a chance for you to use one of Delphis greatest strengths
exceptions. Some exceptions can be planned for, and by using
robust exception handling, you can design your application to visually indicate when those exceptions have been raised. Then, by adding
TbuiMessageQueue to your toolkit, you can provide a means to handle
unexpected exceptions and messages, still without showing a message
dialog box.
If youve used Delphis Java sibling, JBuilder, youve seen a wonderful
example of visual cues. Because JBuilder is constantly parsing the
code, it will often detect errors in the form of incomplete statements
(while youre typing). In a bit of UI design that ironically looks
a great deal like TbuiMessageQueue plus TbuiTreeView, JBuilders
IDE updates its Structure View with error messages until you type
something it recognizes (see Figure 12).
A good example of the advantages of such visual cues would be
an access violation. An access violation is obviously unplanned, and
when one occurs, it normally leaves your application in an unusable
state a broken tool. It should be obvious when a tool is broken. If
its not apparent to the users even though you might tell them its
broken its likely they will click OK and continue using it.

implementation
end.

procedure TbuiMessageQueue.PopulateMessageList(
aMessageList: IbuiMessageList);
var
liIndex: Integer;
lMessage: TbuiMessage;
begin
if (aMessageList <> nil) then begin
RemoveReadMessages;
aMessageList.ClearList;
for liIndex := 0 to MessageCount - 1 do begin
lMessage := Messages[liIndex];
aMessageList.AddMessage(lMessage.Category,
lMessage.Caption, lMessage.MessageText,
lMessage.Priority, lMessage.Read, liIndex);
end;
SetOutOfSynch(False);
end;
end;

Figure 9 (Top): buiMessageListInterface.pas. Figure 10 (Bottom):


TbuiMessageQueue.PopulateMessageList accepts IbuiMessageList
implementations.
29 March 2001 Delphi Informant Magazine

Figure 11: The message queue demonstration application.

Greater Delphi
box. Can you nd it? Were the ones whore supposed to be good
at nding the solutions to tough problems, so do your users a favor
by making them more productive. Find a better option than the
ubiquitous message dialog box.
Next month: If its worth asking, its worth the program remembering; in other words, better user option persistence.
The les referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103RL.

Figure 12: Errors listed in JBuilders Structure View.

If an access violation (or some other incapacitating exception) occurs,


your application workspace should, in no uncertain terms, continue
to indicate that all is not right with the world, and that the user is
taking a risk should he or she decide to continue.

Counting the Cost of Better UI Design


Excellence is never attained for free, and improving the usability of
your application is no exception. As we discussed last month, there
are costs to better user interaction design, and we must decide if we
are willing to pay them.
With regard to the costs of eliminating message dialog boxes, it will
take a good bit of planning to enumerate in advance the set of
possible messages and exceptions. Then it will take a great deal of
acumen in workspace design to gure out a viable way to display
the resulting state of your application as a result of these messages.
As weve discussed, TbuiMessageQueue, or your adaptation of it, can
help with this, but if youre going to build ground-breaking, advanced
software thats a pleasure to use, it will take some extra time.
Another cost of implementing this philosophy surrounds the area of
conrmation messages. Typically we see conrmation dialog boxes
displayed in lieu of a sophisticated Undo system. If we ask the users if
they really want to do some action, then its off our shoulders if they
cant get back to where they were. Right? Well, if were to eliminate
conrmation messages and just do what they ask without questioning
them, then were going to have to work a little harder and provide
a robust Undo system.
These are important costs to consider. However, as I said last month,
if you choose not to pursue better UI design and your competitors
do, how much will that cost you?

So What Have We Accomplished?


Weve created a set of classes that allow us to create applications in
which no message dialog boxes are presented, thus allowing users to
keep their focus on their task, and to read our messages when theyre
ready, rather than when its convenient for us. Opponents of this
method can always point out some case in which a message dialog
box must be presented, objecting: Sometimes you have no choice.
To them I would say that there is nothing inherently wrong with
presenting a message dialog box. Good UI design, however, argues
against an excessive number of them. That being said, it could be
argued that theres always some better option than a message dialog
30 March 2001 Delphi Informant Magazine

Robert is Director of Product Management - ReportBuilder for Digital Metaphors


Corp. He graduated with a degree in Music Theory from the University of North
Texas, but has been writing code since his Apple II+ and AppleBasic days. He has
been programming in Object Pascal since Delphi 1 and currently resides in Texas
with his wife and daughters.

Begin Listing One Implementing AddMessage,


MessageRead, PopulateMessageList,
RemoveReadMessages
procedure TbuiMessageQueue.AddMessage(
aMessageClass: TbuiMessageClass; const aCaption, aText,
aCategory: string; aAllowProcess: Boolean;
aPriority: TbuiMessagePriority;
aReadEvent: TbuiReadMessageEvent);
var
lMessage: TbuiMessage;
begin
if (aMessageClass <> nil) and
AllowNewMessageOfType(aMessageClass) then begin
lMessage := aMessageClass.Create;
lMessage.Caption := aCaption;
lMessage.Category := aCategory;
lMessage.MessageText := aText;
lMessage.ContinueProcess := aAllowProcess;
lMessage.Priority := aPriority;
lMessage.OnReadMessage := aReadEvent;
lMessage.Parent := Self;
SetOutOfSynch(True);
end;
end;
procedure TbuiMessageQueue.MessageRead(aIndex: Integer);
var
lMessage: TbuiMessage;
begin
if (aIndex < MessageCount) then begin
lMessage := Messages[aIndex];
lMessage.DoReadMessage(Self);
end;
end;
procedure TbuiMessageQueue.PopulateMessageList(
aMessageList: IbuiMessageList);
var
liIndex: Integer;
lMessage: TbuiMessage;
begin
if (aMessageList <> nil) then begin
RemoveReadMessages;
aMessageList.ClearList;
for liIndex := 0 to MessageCount - 1 do begin
lMessage := Messages[liIndex];
aMessageList.AddMessage(lMessage.Category,

Greater Delphi
lMessage.Caption, lMessage.MessageText,
lMessage.Priority, lMessage.Read, liIndex);
end;
SetOutOfSynch(False);
end;
end;
procedure TbuiMessageQueue.RemoveReadMessages;
var
liIndex: Integer;
lMessage: TbuiMessage;
begin
for liIndex := MessageCount - 1 downto 0 do begin
lMessage := Messages[liIndex];
if (lMessage.Read) then
lMessage.Free;
end;
end;

End Listing One


Begin Listing Two TbuiMessageQueue propertys
access methods
function TbuiMessageQueue.GetContinueProcess: Boolean;
var
liIndex: Integer;
begin
Result := True;
liIndex := 0;
while (Result) and (liIndex < MessageCount) do begin
if not (Messages[liIndex].Read) and
not (Messages[liIndex].ContinueProcess) then
Result := False
else
Inc(liIndex);
end;
end;
function TbuiMessageQueue.GetHasUnreadMessages: Boolean;
var
liIndex: Integer;
begin
Result := False;
liIndex := 0;
while not(Result) and (liIndex < MessageCount) do begin
if not(Messages[liIndex].Read) then
Result := True
else
Inc(liIndex);
end;
end;
function TbuiMessageQueue.GetHighestUnreadPriority:
TbuiMessagePriority;
var
liIndex: Integer;
begin
Result := mpLowest;
for liIndex := 0 to MessageCount - 1 do
if not(Messages[liIndex].Read) and
((Ord(Messages[liIndex].Priority)>Ord(Result))) then
Result := Messages[liIndex].Priority;
end;
function TbuiMessageQueue.GetMessageCount: Integer;
begin
Result := ChildCount;
end;
function TbuiMessageQueue.GetMessages(aIndex: Integer):
TbuiMessage;
begin
Result := TbuiMessage(Children[aIndex]);
end;
function TbuiMessageQueue.GetUnreadMessageCount: Integer;

31 March 2001 Delphi Informant Magazine

var
liIndex: Integer;
begin
Result := 0;
for liIndex := 0 to MessageCount - 1 do
if not (Messages[liIndex].Read) then
Inc(Result);
end;
procedure TbuiMessageQueue.SetOutOfSynch(
aOutOfSynch: Boolean);
begin
FOutOfSynch := aOutOfSynch;
if (FOutOfSynch) then
DoOnOutOfSynch;
end;

End Listing Two


Begin Listing Three buiParenting.pas
unit buiParenting;
interface
uses
SysUtils, Windows, Classes;
type
TbuiParenting = class (TObject)
private
FChildren: TList;
FFreeingChildren: Boolean;
FParent: TbuiParenting;
procedure SetParent(aValue: TbuiParenting);
protected
procedure FreeChildren;
function GetChildCount: Integer;
function GetChildren(aIndex: Integer): TbuiParenting;
public
constructor Create;
destructor Destroy; override;
class function ParentedClass: Boolean; virtual;
procedure AddChild(aChild: TbuiParenting); virtual;
function RemoveChild(aChild: TbuiParenting): Integer;
virtual;
property ChildCount: Integer read GetChildCount;
property Children[aIndex: Integer]: TbuiParenting
read GetChildren;
property FreeingChildren: Boolean
read FFreeingChildren;
property Parent: TbuiParenting
read FParent write SetParent;
end;
implementation
constructor TbuiParenting.Create;
begin
inherited Create;
FChildren := TList.Create;
FParent := nil;
end;
destructor TbuiParenting.Destroy;
begin
SetParent(nil);
FreeChildren;
FChildren.Free;
inherited Destroy;
end;
procedure TbuiParenting.AddChild(aChild: TbuiParenting);
begin
FChildren.Add(aChild);
end;

Greater Delphi
procedure TbuiParenting.FreeChildren;
var
liIndex: Integer;
begin
FFreeingChildren := True;
try
for liIndex := (FChildren.Count - 1) downto 0 do
TObject(FChildren[liIndex]).Free;
finally
FChildren.Clear;
FFreeingChildren := False;
end;
end;
function TbuiParenting.GetChildCount: Integer;
begin
Result := FChildren.Count;
end;
function TbuiParenting.GetChildren(aIndex: Integer):
TbuiParenting;
begin
Result := TbuiParenting(FChildren[aIndex]);
end;
class function TbuiParenting.ParentedClass: Boolean;
begin
Result := True;
end;
function TbuiParenting.RemoveChild(aChild: TbuiParenting):
Integer;
begin
Result := FChildren.Remove(aChild);
end;
procedure TbuiParenting.SetParent(aValue: TbuiParenting);
begin
if (ParentedClass) and (FParent <> aValue) then begin
if (FParent <> nil) then FParent.RemoveChild(Self);
FParent := aValue;
if (FParent <> nil) then FParent.AddChild(Self);
end;
end;
end.

End Listing Three


Begin Listing Four buiControls.pas
unit buiControls;
interface
uses
SysUtils, Windows, Classes, Menus, Comctrls,
buiMessageListInterface, buiTypes;
type
TbuiAddCategoryNodeEvent = procedure (Sender: TObject;
aNode: TTreeNode) of object;
TbuiAddMessageNodeEvent = procedure (Sender: TObject;
aNode: TTreeNode; aRead: Boolean;
aPriority: TbuiMessagePriority) of object;
TbuiTreeView = class (TTreeView, IbuiMessageList)
private
FMessageTexts: TStrings;
FOnAddCategoryNode: TbuiAddCategoryNodeEvent;
FOnAddMessageNode: TbuiAddMessageNodeEvent;
protected
procedure DoOnAddCategoryNode(Sender: TObject;
aNode: TTreeNode);
procedure DoOnAddMessageNode(Sender: TObject;
aNode: TTreeNode; aRead: Boolean;

32 March 2001 Delphi Informant Magazine

aPriority: TbuiMessagePriority);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddMessageText(aMessageIndex: Integer;
const aMessageText: string);
function FindNodeByCaption(const aCaption: string):
TTreeNode;
procedure ClearList; stdcall;
procedure AddMessage(const aCategory, aCaption,
aMessageText: string; aPriority: TbuiMessagePriority;
aRead: Boolean; aMessageIndex: Integer); stdcall;
published
property OnAddCategoryNode: TbuiAddCategoryNodeEvent
read FOnAddCategoryNode write FOnAddCategoryNode;
property OnAddMessageNode: TbuiAddMessageNodeEvent
read FOnAddMessageNode write FOnAddMessageNode;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Better UI', [TbuiTreeView]);
end;
constructor TbuiTreeView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FMessageTexts := TStringList.Create;
end;
estructor TbuiTreeView.Destroy;
begin
FMessageTexts.Free;
inherited Destroy;
end;
procedure TbuiTreeView.AddMessage(const aCategory,
aCaption, aMessageText: string;
aPriority: TbuiMessagePriority; aRead: Boolean;
aMessageIndex: Integer);
var
lNode: TTreeNode;
lMessageNode: TTreeNode;
lsCategory: string;
begin
if not(aRead) then
lsCategory := aCategory
else
lsCategory := 'Read: ' + aCategory;
lNode := FindNodeByCaption(lsCategory);
if (lNode = nil) then begin
if (aRead) then
lNode := Items.AddChildObject(
nil, lsCategory, Pointer(-1))
else
lNode := Items.AddChildObjectFirst(
nil, lsCategory, Pointer(-1));
DoOnAddCategoryNode(Self, lNode);
end;
lMessageNode := Items.AddChildObject(
lNode, aCaption, Pointer(aMessageIndex));
DoOnAddMessageNode(Self, lMessageNode, aRead, aPriority);
AddMessageText(aMessageIndex, aMessageText);
end;
procedure TbuiTreeView.AddMessageText(
aMessageIndex: Integer; const aMessageText: string);
begin
while (FMessageTexts.Count <= aMessageIndex) do
FMessageTexts.Add('');
FMessageTexts[aMessageIndex] := aMessageText;
end;
procedure TbuiTreeView.ClearList;
begin

Greater Delphi
Items.Clear;
FMessageTexts.Clear;
end;
procedure TbuiTreeView.DoOnAddCategoryNode(Sender: TObject;
aNode: TTreeNode);
begin
if Assigned(FOnAddCategoryNode) then
FOnAddCategoryNode(Sender, aNode);
end;
procedure TbuiTreeView.DoOnAddMessageNode(Sender: TObject;
aNode: TTreeNode; aRead: Boolean;
aPriority: TbuiMessagePriority);
begin
if Assigned(FOnAddMessageNode) then
FOnAddMessageNode(Sender, aNode, aRead, aPriority);
end;
function TbuiTreeView.FindNodeByCaption(
const aCaption: string): TTreeNode;
var
lNode: TTreeNode;
begin
Result := nil;
lNode := Items.GetFirstNode;
while (lNode <> nil) and (lNode.Text <> aCaption) do
lNode := lNode.GetNext;
if (lNode <> nil) then
Result := lNode;
end;
end.

End Listing Four

33 March 2001 Delphi Informant Magazine

At Your Fingertips

Drive and File Information / System Icons / ListBox Component

By Bruno Sonnino

Drives, Files, etc.


Drive and File Info in a ListBox with Icons

ometimes you need information about your systems installed drives and files, for
example, which drives are installed. This article shows you how to retrieve this
information and place it in a ListBox. It also demonstrates how to retrieve and use the
system image list, which is used to cache the icons used in Windows.

Detecting Installed Drives


To get the drive type, you must use the Windows API function GetDriveType. Its declared
like this:
function GetDriveType(lpRootPathName:
PChar): UINT;
stdcall;

It expects one parameter, lpRootPathName, a


PChar containing the root directory of the drive
to get information. It returns the drive type
using one of the return codes shown in Figure 1.
Once the drive type is determined, you can
decide what to do. One application of this function is to know if the current program is run
Return Value

Description

The drive type cannot be determined.

The root directory does not exist.

DRIVE_REMOVABLE

The disk can be removed from the drive.

DRIVE_FIXED

The disk cannot be removed from the drive.

DRIVE_REMOTE

The drive is a remote (network) drive.

DRIVE_CDROM

The drive is a CD-ROM drive.

DRIVE_RAMDISK

The drive is a RAM disk.

Figure 1: Return codes for GetDriveType.

34 March 2001 Delphi Informant Magazine

from a CD-ROM. Figure 2 shows the code to


detect if the program is run from a CD-ROM.
Another use for this function is to detect all
drives installed on the system (even mapped network drives) and to show their types. To do
this, you must cycle through all of the possible
drive letters, and test the GetDriveType return
code. The code in Figure 3 adds all valid drives
and their types to a ListBox. Figure 4 shows an
example result.

Extracting File Information


Windows Explorer is capable of displaying a lot of
information about a le, e.g. its display name, icon
(both large and small), type name, and attributes.
To retrieve this information yourself, you could use
the Windows API function, SHGetFileInfo, dened
in ShellApi.pas. It is declared like this:
function SHGetFileInfo(pszPath: PAnsiChar;
dwFileAttributes: DWORD; var psfi: TSHFileInfo;
cbFileInfo, uFlags: UINT): DWORD; stdcall;

The rst parameter, pszPath, is the path to


the le. The second parameter, dwFileAttributes,
describes the le attributes; its used only if
SHGFI_USEFILEATTRIBUTES is included in
the uFlags parameter. The third parameter, ps, is
a TSHFileInfo structure that will hold the return
information. The fourth parameter, cbFileInfo, is
the size of the TSHFileInfo structure. Its initialized

At Your Fingertips
var
CurrDrive : string;
begin
// Get executable drive.
CurrDrive := ExtractFileDrive(ParamStr(0));
// Check if executable drive is CD-ROM.
if GetDriveType(PChar(CurrDrive+'\')) <> DRIVE_CDROM then
MessageDlg('This program isn't run from a CD-ROM',
mtError, [mbOk], 0);

Figure 4: A
systems installed
drives.

Figure 2: Detecting if the program is run from a CD-ROM.


var
Drive : Char;
begin
// Cycle for all possible drives.
for Drive := 'A' to 'Z' do
// Get drive type; if it's valid, add it to ListBox.
case GetDriveType(PChar(Drive+':\')) of
DRIVE_REMOVABLE :
Listbox1.Items.Add(Drive + '
Removable');
DRIVE_FIXED :
Listbox1.Items.Add(Drive + '
Fixed');
DRIVE_REMOTE :
Listbox1.Items.Add(Drive + '
Network drive');
DRIVE_CDROM :
Listbox1.Items.Add(Drive + '
CD-ROM');
DRIVE_RAMDISK :
Listbox1.Items.Add(Drive + '
RAM Disk');
end;

Attribute

Description

SHGFI_ATTRIBUTES

Retrieves the file attribute flags,


in dwAttributes.

SHGFI_DISPLAYNAME

Retrieves the display name for the


file, in szDisplayName.

SHGFI_EXETYPE

Returns the type of the executable


file if pszPath identifies an
executable file.

SHGFI_ICON

Retrieves the handle of the icon


that represents the file, in hIcon,
and the index of the icon within
the system image list, in iIcon.

SHGFI_ICONLOCATION

Retrieves the name of the file


that contains the icon representing
the file, in szDisplayName.

SHGFI_LARGEICON

Retrieves the files large icon.

SHGFI_LINKOVERLAY

Adds the link overlay to the


files icon.

SHGFI_OPENICON

Retrieves the files open icon.

SHGFI_PIDL

Indicates that pszPath is the


address of an ITEMIDLIST
structure, not a path name.

SHGFI_SELECTED

Retrieves the icon in selected state.

SHGFI_SHELLICONSIZE

Retrieves a shell-sized icon.

SHGFI_SMALLICON

Retrieves the files small icon.

SHGFI_SYSICONINDEX

Retrieves the index of the icon within the system image list, in iIcon.

SHGFI_TYPENAME

Retrieves the files type, in


szTypeName.

SHGFI_USEFILEATTRIBUTES

Indicates that the function should


use the dwFileAttributes parameter.

Figure 3: Detecting installed drives and their types.

to SizeOf(TSHFileInfo). The last parameter, uFlags, is a ag combination that determines the information to be retrieved.
The TSHFileInfo structure will hold the information requested about
the le, and the values it will contain depend on the uFlags parameter.
This structure is dened like this:
_SHFILEINFOA = record
hIcon: HICON;
// File icon handle.
iIcon: Integer;
// File icon index.
dwAttributes: DWORD; // File attributes.
// Display name.
szDisplayName: array [0..MAX_PATH-1] of AnsiChar;
szTypeName: array [0..79] of AnsiChar; // File type.
end;
TSHFileInfo = TSHFileInfoA;

The uFlags parameter must contain a Boolean value, or some combination of the ags shown in Figure 5. The members of the
TSHFileInfo structure will be lled with valid data, depending on
the uFlags combination.
For example, if we want to retrieve the normal icon and display name
for a le, we can use a call like this:
SHGetFileInfo(PChar(Filename), 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_DISPLAYNAME or SHGFI_ICON);

The last parameter tells the function to retrieve the display name,
copying it to szDisplayName; and the les icon, putting its handle
in the hIcon member of the return structure. The code shown in
Figure 6 retrieves the large and small icons, displaying them in two
Image components on the form. Figure 7 shows a sample result.
Notice that SHGetFileInfo is called twice, once for each icon. To
check if the icon is valid, test the return code from SHGetFileInfo.
35 March 2001 Delphi Informant Magazine

Figure 5: Valid attributes for uFlags.

When we request a le icon, the return code should be an ImageList handle (more on this in the next tip), or 0, if the le is
not valid.

Working with the System Image List


Windows caches all used icons in a system image list, so it can retrieve
them faster. When you use SHGetFileInfo to retrieve the le icon, the
return value is a handle to the system image list. This handle can be
assigned to an ImageList component to display an icon. You must
be careful when using this handle: it doesnt point to a copy

At Your Fingertips
procedure TForm1.Button1Click(Sender: TObject);
var
ShFileInfo : TSHFileInfo;
SysListHandle : THandle;
begin
// Retrieve small icon, type name, and icon location
// in the system image list.
SysListHandle := SHGetFileInfo(PChar(Edit1.Text), 0,
ShFileInfo, SizeOf(TSHFileInfo), SHGFI_DISPLAYNAME or
SHGFI_SMALLICON or SHGFI_ICON or SHGFI_TYPENAME or
SHGFI_ICONLOCATION);
// Assign retrieved icon to the small icon Image.
Image1.Picture.Icon.Handle := ShFileInfo.hIcon;
// If SysListHandle is <> 0, it's a valid file.
if SysListHandle <> 0 then
begin
// Assign display name label.
Label3.Caption := string(ShFileInfo.szDisplayName);
// Assign icon index label.
Label5.Caption := IntToStr(ShFileInfo.iIcon);
// Assign type name label.
Label7.Caption := string(ShFileInfo.szTypeName);
// 2nd call to SHGetFileInfo to retrieve normal icon.
SHGetFileInfo(PChar(Edit1.Text), 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_ICON);
// Assign retrieved icon to large icon TImage.
Image2.Picture.Icon.Handle := ShFileInfo.hIcon;
end
else
begin
// Handle invalid filename.
Label3.Caption := 'Not a valid filename';
Label5.Caption := '';
Label7.Caption := '';
end;
end;

procedure TForm1.FormCreate(Sender: TObject);


var
ShFileInfo : TSHFileInfo;
i : Integer;
begin
// Retrieve small icons.
ImageList1.Handle := SHGetFileInfo('', 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_SMALLICON);
// Retrieve large icons.
ImageList2.Handle := SHGetFileInfo('', 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_ICON);
// Add a ListView item for each icon.
for i := 0 to ImageList2.Count-1 do
with ListView1.Items.Add do
ImageIndex := i
end;

Figure 8: Retrieving small and large icon system image lists.

Figure 6: Retrieving small and large icons.

of the system image


list, but to the ImageList component. When
assigning the handle to
an ImageList component, you must leave its
SharedImages property
set to True, so the
images arent destroyed
when the ImageList
component is
destroyed.
The code in Figure 8
Figure 7: Sample extracted icons.
shows how to retrieve
the small and large
icon system image lists, assigning them to two ImageList components and displaying them in a ListView (see Figures 9 and 10).
The ListView Style property must be set to vsIcon or vsSmallIcon
to display the icons.

Figure 9: Large icons ImageList.

Figure 10: Small icons ImageList.

Drawing in a ListBox

width and height). When using lbOwnerDrawFixed, the items height


is given by the ItemHeight property. Then you must draw the item in
the OnDrawItem event handler.

Usually, a ListBox component displays only alphanumeric data, but


it can also show other kinds of information, such as bitmaps and
icons. To draw them you must allow custom drawing by setting
the ListBox components Style property to lbOwnerDrawFixed or
lbOwnerDrawVariable.

Now lets use all of the ideas discussed so far this month. One
thing, however, that wasnt mentioned regarding the SHGetFileInfo
function, is that you can also use it to retrieve drive icons. Well use
this capability to place the icon and display name in a ListBox.

If Style is lbOwnerDrawVariable, the OnMeasureItem event is red


every time a ListBox item must be drawn (and you must set the items

We wont store the icons ourselves. Instead, we will use an ImageList that will share images with the system image list of small

36 March 2001 Delphi Informant Magazine

At Your Fingertips
procedure TForm1.FormCreate(Sender: TObject);
const
DriveTypes : array[0..4] of string = ('Removable',
'Fixed Disk', 'Network Drive', 'CD-ROM','Ram Disk');
var
Drive : Char;
ShFileInfo : TSHFileInfo;
DriveType : Integer;
begin
// Get icon width and height.
FIconWidth := GetSystemMetrics(SM_CXSMICON);
Listbox1.ItemHeight := GetSystemMetrics(SM_CXSMICON) + 4;
// Assign system image list to ImageList component.
ImageList1.Handle := SHGetFileInfo('', 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_SYSICONINDEX or
SHGFI_SMALLICON or SHGFI_ICON);
for Drive := 'A' to 'Z' do begin
// Get display name and icon index for drive.
SHGetFileInfo(PChar(Drive+':\'), 0, ShFileInfo,
SizeOf(TSHFileInfo), SHGFI_SYSICONINDEX or
SHGFI_DISPLAYNAME);
// Get drive type. If it's valid, add drive and
// icon index to the ListBox.
DriveType := GetDriveType(PChar(Drive+':\'));
if DriveType in [DRIVE_REMOVABLE..DRIVE_RAMDISK] then
Listbox1.Items.AddObject(
string(ShFileInfo.szDisplayName) + '
' +
DriveTypes[DriveType-DRIVE_REMOVABLE],
TObject(ShFileInfo.iIcon));
end;
end;

Figure 11: Preparing to display drive information in a ListBox.


procedure TForm1.ListBox1DrawItem(Control: TWinControl;
Index: Integer; Rect: TRect; State: TOwnerDrawState);
begin
with Listbox1 do begin
// Draw the icon from the ImageList.
ImageList1.Draw(Canvas, Rect.Left+2, Rect.Top+2,
Integer(Items.Objects[Index]));
// Draw the text.
Canvas.TextOut(Rect.Left+10+FIconWidth,
Rect.Top+2, Items[Index]);
end;
end;

Figure 12: The OnDrawItem event handler.

Figure 13:
Sample ListBox with
installed
drives and
icons.

The rst parameter, Control, is the control that must be drawn.


The second parameter, Index, is the index of the item to draw. The
third parameter, Rect, holds the coordinates of the rectangle in the
controls canvas. The last parameter, State, is the state of the item.
If its selected, focused, or checked, you can change your drawing
depending on this parameter.
Well draw the current icon and text in the OnDrawItem event
handler, as shown in Figure 12. We get the icon from the Objects
property, and the text from the ListBox items text (using its index).
The position to draw the item is retrieved from the Rect parameter
and FIconWidth variable. Figure 13 shows a ListBox with the installed
drives and their respective icons.

Conclusion
Getting information about installed drives and disk les is easy, using a
couple of Windows API functions. As a bonus, with the SHGetFileInfo
function, you can also access the system image list and use its icons, in
much the same way you use an ImageList with your own images. One
use of all this information is to create a ListBox that shows the installed
drives, drawing their icons, display names, and types.
The les referenced in this article are available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\MAR\
DI200103BS.

icons. We initialize the ListBox items in the forms OnCreate event


handler, as shown in Figure 11.
Initially, we get the small icon dimensions with the
GetSystemMetrics function, so we can initialize the ListBox
ItemHeight property. We also store the icon width in a variable
named FIconWidth, so well know where to draw the text. Then
we retrieve the system image list handle, assigning it to the
Handle property of the ImageList component, so the images are
shared. Finally, we cycle through all possible drives, getting their
display names and small icons. If the drive is a valid drive, we add
its display name, type, and icon index to the ListBox. We must
typecast the index to a TObject, because the AddObject method
expects a TObject. This typecast presents no problem, because a
TObject pointer and an integer are the same size.
The actual drawing occurs in the OnDrawItem event handler. Its type
is TDrawItemEvent, declared like this:
TDrawItemEvent = procedure(Control: TWinControl;
Index: Integer; Rect: TRect; State: TOwnerDrawState)
of object;

37 March 2001 Delphi Informant Magazine

A Brazilian, Bruno Sonnino has been developing with Delphi since its first
version in 1995. He has written the books 365 Delphi Tips and Developing
Applications in Delphi 5, published in Portuguese. He can be reached at
sonnino@netmogi.com.br.

New & Used


By Bill Todd

InstallShield Professional Windows Installer Edition 2.0


A Powerful Tool for Building Complex Windows Setups

nstallShield Professional - Windows Installer Edition 2.0 is the latest tool from
InstallShield Corp. for building installations that use Microsofts Windows Installer
service. Windows Installer offers two ways to create an installation: manually through a
series of views, or the Project Wizard can get you started.
Figure 1 shows the Windows Installer window
immediately after startup. The Windows Installer
user interface mimics Outlook in its single window
and its organization in a series of views within the
window. If you dont like the tree view shown in
the left-hand panel, you can display an Outlook
bar (called a viewbar in the Windows Installer
manual) in addition to, or in place of, the tree view.
To start a new project, click Create a new project in
the center panel; the right panel will change to offer
four options. The rst, Blank Merge Module Project,

allows you to create your own merge module if you


are supplying components that other developers will
include in their installation projects. One example
is the BDE merge module that Borland supplies
to allow those distributing a Delphi application to
include the BDE in the applications installation. The
second option, Blank Setup Project, lets you create a
new project manually. Import Visual Basic Project lets
you import a VB project as the basis for installing
that VB application. The nal choice is Project Wizard.

Using the Project Wizard


The Project Wizard starts with a list of the steps the
wizard will take you through. Clicking Next brings
you to the rst screen of the wizard, which prompts
you to create a new project and supply its name, or
open an existing project. Next the wizard prompts
you for the application information, including version number, company name, and technical support contact information. This is the information
that appears in the Add/Remove Programs Control
Panel applet in Windows 2000 and Windows ME.
The next page of the wizard presents a list of
languages your installation can run in, and lets you
check the ones you want. Clicking Next takes you
to the Application Features page of the wizard.

Figure 1: The main screen: creating a new project.

38 March 2001 Delphi Informant Magazine

If you are new to Windows Installer, its a good


idea to familiarize yourself with the architecture
you must use in describing your application to
the Windows Installer service. Applications consist of features. A feature is an element of your
application that the user can choose to install
as part of the setup process. Features may be

New & Used


marked as required, in which case they will always be installed.
An accounting system product might have features like System
Manager, Accounts Receivable, Accounts Payable, General Ledger,
Payroll, and Tutorial. A feature is made up of one or more components. Components are les or collections of les that must be
installed together. The same component can be included in more
than one feature. This ensures that the component is installed in
case any feature needs it.
The Windows Installer Project Wizard presents a tree view with
Features as the root node, and three default features already
dened. You probably wont want to use the default features as
they stand, but you can easily right-click a feature and change
its name. You can also delete unwanted features by selecting the
feature and clicking the Delete button. You can add a new feature
by clicking the Add button. The next page in the wizard is where
you dene your components. Again, you can rename or delete
the default components or add new ones. On this page you must
also enter the folder where you want each component installed.
Moving to the next page in the wizard brings you to one of
the most critical steps in building your installation, associating
components with features. Figure 2 shows the association page
with the features in a tree view on the left, and the components in
a listbox on the right. Simply select a feature, select a component,
and click Add to add the component to the feature.
You identify all of the les that comprise each component on the next
page of the wizard. You should identify one le in each component
as the key le. The key le is the le that Windows Installer looks
for to determine whether the component has been installed. The key
le is also the le for which a shortcut will be created if you want
one for a component. This takes place on the next page of the wizard.
Clicking Next from the Shortcuts page takes you to the Registry
Data page where you can specify a .REG le for each component.
The registry le will be merged into the registry on the destination
machine as part of the installation. Next is the Dialogs page where
you select which of the standard dialogs you want displayed during
installation. Unfortunately, the wizard doesnt provide a way to enter
the path to your license agreement le if you choose to include the
License Agreement dialog box. Therefore, youll have to edit your
project after leaving the wizard to add the path. The nal page of the
wizard asks if you want to save the project settings, or save the project
settings and build a release.
Although you can use the Project Wizard to build the shell of your
installation, it falls short of providing the features most users will
need in several areas. First, you cannot specify where your project
will be saved. The project is saved in My Documents\MySetups
(or the directory you specify in Tools | Options) whether or not
thats what you want. I nd this very inconvenient, because I always
want everything connected with a project in subdirectories under
the project directory. That way I have the source code, executable,
database, correspondence, documentation, help les, and installation
all in one place. This approach prevents les from accidentally being
deleted or moved, and simplies delivering the project to a client, or
archiving the project to a CD.
The second problem with the wizard is that theres no way to
specify the media for which you want the release built. Finally,
the wizard does not provide a way to include one or more merge
modules in your project. If you need to distribute the BDE,
MDAC, DAO, or any components using a merge module, you
must add the merge modules outside of the wizard. Since this
39 March 2001 Delphi Informant Magazine

Figure 2: Associating components with features.

version of Windows Installer is targeted at users who need to build


sophisticated installations, its likely that most of their installation
projects will need to be modied outside the wizard anyway. The
inability to specify merge modules and distribution media is a
minor inconvenience at worst. However, not being able to specify
the directory where the project will be saved is annoying. If you
want to use the wizard to build the basic structure of your installation, but need to locate the installation somewhere other than
your local MySetups folder, you will have to resave the project in
the correct location after you nish the wizard, then use Windows
Explorer to delete the original copy from MySetups.

Building Your Setup Manually


Because there are many options that arent available in the wizard, you
may nd it just as easy to choose Blank Setup Project to start your new
project. If you do, you will see the screen shown in Figure 3. The
tree view on the left lets you move between all of Windows Installers
views, and identies the six major steps in building your installation
project with the numbers one through six circled in blue. When you
choose General Information under step one, a second tree view is added
to the right of the rst. The Project Properties node in this second tree
lets you enter the setup authors name and comments, and choose the
languages in which the setup will run. If you want your setup to run
in languages other than English, you must purchase language packs
for the other languages.
The Summary Information Stream lets you enter information that
will be displayed on the Summary tab of the Properties dialog box
for your installation le. This is the dialog box that appears when
you right-click the installation le in Explorer and choose Properties
from the context menu. In the Windows 2000 view, enter all of the
information thats displayed in the Windows 2000 or Windows ME
Add/Remove Programs Control Panel applet. Under Product Properties
you can enter the product name, version number, and application
type for the application youre installing, as well as generate GUIDs
for the Product Code and Upgrade Code. These values are used by
Windows Installer to identify a specic product and upgrade. You
can also specify install conditions in this view. For example, if your
product were a Windows NT service, you would want to add a
condition to allow installation only on systems running Windows NT
or Windows 2000. You can specify the destination folder, the default
folder in which your software will be installed. The last view is String
tables. Here you can edit any of the strings that may be displayed by
Windows Installer during your installation.

New & Used


The second and last option under step one is Features. Here you
add the features that comprise your application and specify their
destination directory, whether they will be installed by default,
and whether this feature is required. You can also specify release
ags that allow you to specify which releases of your product will
include this feature. Conditions can also be imposed at the feature
level to control whether the feature can be installed.
Step two, Specify Application Data, begins with the Files choice
that displays the screen shown in Figure 4. This view mimics two
copies of Windows Explorer stacked one above the other. The top
view shows the directory structure on your computer; the bottom
shows the directory structure on the destination computer. Initially the only entry in the destination computers directory tree
is Destination Computer. However, by right-clicking Destination
Computer and choosing Show Predefined Folder, you can add the
default installation directory, as shown in Figure 4, or any of the
standard Windows directories to the tree. You can also create your
own folders from the context menu. Then its just a matter of
dragging les or folders from the source computers folders to
the destination computers folders. Note that you do not have
to create components to contain the les and then associate the
components with features. Just select the feature whose les you
want to add from the drop-down list at the top of the screen, then
add the les. The necessary components are created automatically
following the Windows Installer rules for which les must be
in separate components. One very nice feature is that each time
you add les from a new source directory, you have the option
of creating a path variable for the directory. If you create path
variables, any of the source les will be moved to a new location.
All you have to do is choose Path Variables in the tree view and
change the path for the appropriate variable.
At rst it appears that there is no way to conrm which features youve
associated with les. However, the information is all there via the
context menus. First, right-click Destination Computer and choose Show
Components to add the components that were created to the directory
tree for the destination computer. If you dont like the automatically
generated component names, just right-click the components and
choose Rename. To see which features a component is associated with,
or to associate a component with additional features, right-click the
component, choose Properties, then click the Features tab. You can also
work with components, features, and les at any time by jumping to
the bottom of the tree and choosing Setup Design view under Advanced
Views. In this view, shown in Figure 5, a second tree view is added
to the display. This clearly shows the components associated with each
feature, and the les associated with each component. You can easily
add les to, or remove them from components, and add components
to features to get the structure you need.
The next option in step two is Merge Modules. Here you can add
any merge modules that must be included with your application. By
default, the list of merge modules shows over forty modules that are
included with Windows Installer. If you need to work with other
merge modules, choose Tools | Options, click the File Locations tab,
and go to Merge Module Locations at the bottom of the page. This
is a comma-separated list of paths. Just add the paths to any other
folders that contain merge modules, and they will appear in the
merge module list. To nish step two, choose Dependencies and use
one of the automatic scan options to scan your application and make
sure youve included all of the required les in your setup.

40 March 2001 Delphi Informant Magazine

Figure 3 (Top): Creating a setup manually.


Figure 4 (Bottom): Associating files with features.

Congure the Target System is the third step in building your


installation. Here you choose which shortcuts will be created, which
registry entries will be created, which ODBC drivers, DSNs, and
translators will be included in the installation, and which INI
les will be created or modied. The fourth step is Design the
User Interface. Under this step you can congure the dialog boxes
and billboards that will be displayed when the installation is run.
Windows Installer includes a dialog box editor you can use to create
custom dialog boxes, and edit existing dialog boxes.
The fth step, Dene Sequences and Actions, is where you get
to customize your installation. Sequences allows you to see the
sequence of actions that occur as your installation is running. You
can add, delete, and reorder the actions in a sequence. Choosing
Actions/Scripts lets you create your own custom actions. Actions
can run an EXE, call a function in a DLL, run VBScript, run
JavaScript, or run a script written in the Windows Installer scripting language. This is the same scripting language used by Windows
Installer Professional 5.x/6.x, so current Windows Installer Professional users will feel at home with the language. A custom action
can also set a property or directory, or launch another Windows
Installer installation package. The easiest way to build a custom
action is to right-click Custom Actions and launch the Custom Action
Wizard, which steps you through the process. Once you have created your custom actions, return to Sequences and insert your
actions into the appropriate sequence at the appropriate location.

New & Used

InstallShield Professional - Windows Installer Edition 2.0 will save


time building complex Windows Installer setups, either manually
or with its Project Wizard. Its familiar Outlook-like user interface
provides easy access to the products features.
InstallShield Software Corp.
900 National Parkway, Ste. 125
Schaumburg, IL 60173-5108

Figure 5: Setup Design view shows the components associated


with each feature, and the files associated with each component.

The last step in creating your installation project is Prepare for


Distribution. Before you can distribute your setup, you must
create one or more releases. To begin, click Releases in the tree
view, then right-click Releases in the center panel of the window.
You can choose either New Product Configuration or Release Wizard,
but the Release Wizard is the way to go. The wizard is comprehensive, and guides you through every option you may want to set.
If you assigned release ags to your features, this is where you
can specify which features are included in each release. You can
also choose the languages for this release, the media type, whether
you want the distribution les compressed, the location where the
release les will be created, and whether to create an installation
EXE. By default, Windows Installer creates a SETUP.EXE and
includes the Windows Installer versions for Windows 9x and NT
4. If you know that your application will only be installed on
Windows 2000 or Windows ME, both of which have the Windows
Installer built in, you can create just the Windows installer database le. Once you have built your setup, clicking Distribute in the
tree view lets you send your setup les to any drive or directory on
your PC or network, or you can use FTP to distribute the les.

Phone: (800) 374-4353


E-Mail: sales@installshield.com
Web Site: http://www.installshield.com
Price: US$999; upgrade from InstallShield for Windows Installer,
US$299; upgrade from InstallShield Professional 3.0 or 5.0, US$799.

from the Help menu. This is critical for those new to Windows
Installer. All new users will want to read the introductory sections in
the Windows Installer help to get an understanding of the purpose of
Windows Installer, how it works, and how installations are organized.

Conclusion
If you need a powerful tool to build complex Windows Installer
setups, you wont go wrong with InstallShield Professional Windows Installer Edition 2.0. The Outlook-like user interface
will be familiar to most users and provides easy access to the products vast array of features. If you dont need the sophistication of
InstallShield Professional - Windows Installer Edition 2.0, take a
look at InstallShield Express at http://www.installshield.com.

Other Features
Windows Installer includes a debugger that lets you set breakpoints
and step through the user interface sequence of actions. If you use
a source code version control system that supports Microsofts SCC
interface, Windows Installer will integrate with it to analyze the
difference between versions and create updates. Supported source
control systems include Visual SourceSafe, Rational ClearCase, and
PVCS. If you build a single le setup for Web distribution, Windows
Installer can protect it with a password or digital signature. One
of the most intriguing features of Windows Installer is that it is
an Automation server. This lets you write Automation clients that
use Windows Installer to create or modify setups. An editor is also
included which lets you edit the Windows Installer tables that cannot
be maintained through the user interface.

Documentation
The documentation is excellent. It includes a Getting Started manual,
extensive online help, and an online tutorial. Be sure to read the
manuals nal chapter, Frequently Asked Questions, which shows
you how to accomplish important tasks that arent easy to nd in
the user interface. You can also take advantage of the 30 days of free
technical support that comes with the product. The documentation
also includes the Windows Installer SDK help le, which is accessible
41 March 2001 Delphi Informant Magazine

Bill Todd is president of The Database Group, Inc., a database consulting and
development firm based near Phoenix. He is co-author of four database programming books, author of more than 60 articles, a contributing editor to Delphi
Informant Magazine, and a member of Team Borland, providing technical support
on Borland Internet newsgroups. He is a frequent speaker at Borland Developer
Conferences in the US and Europe. Bill is also a nationally-known trainer and has
taught Delphi programming classes across the country and overseas. Bill can be
reached at bill@dbginc.com.

TextFile

Learn Object Pascal with Delphi


This book is rather unique. Its hardly
an introduction to Delphi as we understand
this wonderful Windows development tool.
Theres minimal discussion of the IDE, virtually nothing on the Visual Component
Library, and hardly any discussion of the
many built-in functions. What the book does
present is the underlying language of Delphi,
Object Pascal. While a number of other books
allocate a chapter or two to Object Pascal,
the entire contents of Learn Object Pascal with
Delphi are devoted to this single topic.
Warren Racheles Learn Object Pascal
with Delphi takes an interesting and unique
approach to separating the visual development aspect of Delphi (with which it does
not deal) from its underlying language: All
of the code examples are built as console
applications. Rachele does a nice job of building a template to use for this purpose. For
that reason, much of the code can also be run
under Borland Pascal 7.
The author carefully explains the syntax of
Object Pascal. The simple and straightforward
code examples, the logical ow of the explanations, and the authors natural style make these
topics extremely accessible. The questions at
the end of each chapter provide an opportunity for the new developer to test his or her
knowledge before moving to the next topic.
The opening chapters provide a wonderful
introduction to the core language features,
including tokens and separators, variables and

42 March 2001 Delphi Informant Magazine

constants, and basic input and output. Chapters 5 and 6 focus on program ow, and
include cogent discussions of the case statement, if statements, sets, and various types
of loops. Chapter 7 introduces procedures
and functions, but unfortunately does not discuss very many of the built-in procedures
and functions. Most of these have been a
part of Turbo Pascal from the early days. The
discussion about working with strings would
have been enhanced by including copy, pos,
and the anachronistic concat routine. I recommend that if Rachele decides to write a
new edition, he consider adding a chapter
on built-in procedures and functions or incorporating a discussion of these in appropriate
chapters. This would round out an otherwise
excellent introduction to Object Pascal.
Chapters 9 and 10 introduce the
languages important data collections, arrays,
and records. As in previous chapters, the
author introduces these topics systematically,
with easy-to-follow descriptions, and builds
on that foundation by providing important
warnings and helpful examples. The nal two
chapters broach the more advanced topics of
object-oriented programming, pointers, and
le handling. The presentation on pointers
includes examples of building linked lists,
and the use of dynamic variables. This
extremely clear and accessible presentation of
these more advanced Object Pascal topics is
one of the highlights of this book.

Learn Object Pascal with Delphi is a solid and


generally complete introduction to Object
Pascal. Its best suited to new Delphi developers, whether they are completely new programmers or moving to Delphi from another
visual language. What a great gift for that
Visual Basic programmer friend who still
believes that Pascal is a difcult language.
Alan C. Moore, Ph.D.
Learn Object Pascal with Delphi by Warren
Rachele, Wordware Publishing, Inc.,
ISBN: 1-55622-719-1
Cover Price: US$49.95
(358 pages, CD-ROM)

File | New
Directions / Commentary

Interview with David Intersimone

s Vice President of Developer Relations, David Intersimone (or David I for short) is responsible for building full-service
developer programs offering corporations, government agencies, third-party software developers, consultants, and
end-users the support they need to be successful with Borland products. This brings him into contact with professional
programmers, user groups, the technical press, book authors, and corporate customers around the world.
Delphi Informant: Youre arguably the best known certainly the most
recognizable person at Borland. How did you come to join Borland,
and what are some of your more memorable experiences there?
David I: Ive been here over 15 years now. A lot has happened over
the years too much to cover in just one answer, or even in an
article. Maybe Ill write a book someday. But Ill try to cover a
few highlights.
Before I joined Borland in 1985, I worked for Softsel Computer
Products (now called Merisel). I met Philippe Kahn at Comdex Las
Vegas in 1983. He came by the Softsel booth where I was working
and we started talking. He said he had a Pascal compiler, but was
not looking for a distributor. I told him about an internal project
I was building using Microsoft Pascal. He gave me the CP/M and
PC/DOS versions of Turbo Pascal 1.0 and went on his way. I kept
track of what Borland was doing for the next two years. A coworker at Softsel, Spencer Leyton, joined Borland and convinced
me to come up to Scotts Valley and interview with Philippe. My job
interview was out on a sailboat in Monterey Bay. I started working
for Borland, looking for new products for the company. My next
job was Director of R&D for Language products. During my time
in R&D we brought out Turbo Pascal for the Macintosh, Turbo
Pascal 4 and 5, Turbo C 1.5 and 2, Turbo Assembler and Debugger,
Turbo Basic, and Turbo Prolog.
For the past 9 years, Ive been chief evangelist for our developer
products. I have a wonderful job traveling around the world, talking
to programmers, meeting customers, launching products, speaking at
conferences, writing articles, and doing everything else I can to help
our company.
What are some of the memorable experiences Ive had? Its so hard
to single a few out. Being part of the changing face of programming,
improving the process of programming, and making the job easier for
programmers are some of my best memories. Its great to be able to
work with the best developer tool smiths on the planet.
43 March 2001 Delphi Informant Magazine

DI: Youve been involved in Borland conferences for a long time. Is


one conference most memorable to you?
David I: Ive been fortunate to serve as host for our Borland conferences around the world, but Im only one part of a whole team.
Three of us, Andrea Ginsberg, Christine Ellis, and myself work with
many other employees and an outside advisory board to put the
conference together.
All of the conferences are memorable. Of course our 1999 conference
in Philadelphia was especially memorable for me. I was presented
with the rst annual Borland lifetime achievement award by our
CEO Dale Fuller. My family was in the audience that night and
got to share in the warm reception the attendees gave me. Lifetime
achievement awards are often given to someone after they nish their
service. I still have many more years to go in helping our company.
What makes all of our conferences so memorable? My favorite part is
to see the attendees, the speakers, the advisory board, and the staff all
working together to make our conference a true learning and sharing
experience. There is a spirit that ows through the center of each
convention. Most importantly, attendees keep coming back year after
year to learn more about the products they use, and to learn about
new products and technologies.
DI: I see some encouraging signs regarding the companys health:
Borland nally had a quarter in the black, and the mood at this years
conference was as positive as Ive seen in a long time. Whats your
perspective?
David I: The whole company has been working for six quarters
since Dale Fuller arrived to turn Borland around. Weve restored
the company to protability, been cash-ow positive for the past four
quarters, and we have over $240 million cash reserves on hand to fuel
new growth. We are paying close attention to operational efciency,
focusing on execution, and spending a lot of time looking at business
expansion opportunities.

File | New
Having all of our products available on Linux is one of those business
opportunities. Continuing to support and take advantage of new
Microsoft Windows capabilities also helps fuel our growth. Being
very aggressive with the free JBuilder Foundation Edition to seed the
market, and seeing the benets with sales of JBuilder and Borland
AppServer have helped our business.

and component builders to a brieng in March. Since then weve


also traveled around the world, meeting and brieng thousands of
partners to get them ready to support Project Kylix. While its up
to each outside company to make their own business decisions and
announcements, Im sure there will be massive support for Project
Kylix when it ships.

This is a great time to be a developer. Programming, in the 1980s


and early 1990s, had become another common laborer job. With
the growth of the Internet, and the rush for every business and
industry to get to the Internet, programming has become the it
job again. The demand for programmers, the demand for tools,
and the shortness of time to market have reinforced the need for
RAD tools, for component architectures, for distributed computing
infrastructures, and for faster ways to build, deploy, and manage
Web applications. Its fun to be a programmer again. I think that
added to the spirit at our conference this past summer.

DI: Borland has recently launched a very aggressive marketing


campaign to win the hearts and minds of VB developers, including
remarkably a copy of Delphi polybagged with Visual Basic
Programmers Journal. Many in the Delphi community feel such
efforts are long overdue. Please share any plans for future Delphi
marketing efforts.

DI: Many of us are excited about the Kylix Project and what Borlands support for Linux development will mean to the entire community. What are some of your observations and predictions on how
this will affect the company?
David I: Our lawyers have asked me to include the following
Forward-looking statements.... We cant make predictions about
the future. However, Project Kylix is affecting our company today.
Weve been showing the product at trade shows and conferences
where weve never been before. Weve been interviewed and written up in Linux magazines and Web sites. Project Kylix has
already won a few awards. Kylix Beta won the Best Corporate
Product award at Interact + IT 2000 in Melbourne, Australia.
Kylix Beta also won the Nikkei-Byte Editors Choice award.
Nikkei-Byte is one of the most popular developer magazines in
Japan, with a circulation of 80,000.

David I: As I mentioned earlier, were inviting Visual Basic developers


to take a look at Delphi as the way to get to Linux. This is just one
effort. Were also reaching out to the many Delphi developers who
havent upgraded to Delphi 5. I agree with the Delphi communitys
requests for us to do more marketing. I also mentioned that weve
been focused on keeping expenses under control while were turning
the company around. Now that weve returned to protability, were
able to do more things. Stay tuned for more marketing activities in
the coming months.
DI: Are there any areas we havent touched upon that youd like to
share with readers?
David I: I just want to say Thank you to all of our customers.
I appreciate all of the nice comments that theyve made about our
company, our products, and personally to me. We have to continue
to work smart and hard to deserve your business and support. We
also appreciate all of the things that Delphi Informant Magazine
does to help our customers and Delphi. It takes everyone working
together to help our community to continue to prosper.

We are excited about Project Kylix and support for Linux across our
entire product line. Project Kylix will bring an instant army of
skilled application developers for Linux. It will allow the millions of
Delphi, C++Builder, and Visual Basic developers to develop desktop,
database, and Web applications. It will bring to Linux the hundreds
of thousands of applications and components built with Delphi.

If you love Delphi, go and tell others to try it and see what theyre
missing. If each of you can convince 50 or 100 developers to
switch to Delphi and/or Kylix, well be even more successful as a
company, and well be able to provide more products, features, services, and support to you. Keep the feedback coming. Go Delphi!
Go Borland!

Using Delphi 5 today, and Project Kylix tomorrow, is the fastest


route for Visual Basic developers to get to Linux. Delphis environment is so similar to Visual Basic that the learning curve for developers will be much less than moving to another language. You can
develop for Windows today to make your code ready for Linux
tomorrow. Developers will be able to build applications that run on
both Windows and Linux.

Alan C. Moore, Ph.D.

We added coverage of Linux for the rst time at our conference this
past summer. You will denitely see more coverage of Linux at our
conference in Long Beach, July 21-25, 2001 [for more information
go to http://www.borland.com/conf2001].
DI: Borland held a Kylix Kickoff event to which you invited
third-party developers, Project JEDI representatives, and others. I was
delighted to see this early involvement of these folks who contribute
greatly to the community. What results can we expect in terms of
early third-party support for Kylix?
David I: Weve been working with hundreds of third-party companies since early 2000. We invited many of our supporting tool
44 March 2001 Delphi Informant Magazine

Alan Moore is a Professor of Music at Kentucky State University, specializing in music


composition and music theory. He has been developing education-related applications
with the Borland languages for more than 15 years. He is the author of The Tomes
of Delphi: Win32 Multimedia API [Wordware Publishing, 2000] and co-author (with
John Penman) of an upcoming book in the Tomes series on Communications APIs. He
has also published a number of articles in various technical journals. Using Delphi, he
specializes in writing custom components and implementing multimedia capabilities
in applications, particularly sound and music. You can reach Alan on the Internet at
acmdoc@aol.com.

Das könnte Ihnen auch gefallen