You are on page 1of 35

January 2002, Volume 8 Number 1

Cover Art By: Arthur A. Dugoni Jr.

ON THE COVER
5

Columns & Rows

The BLOB Bill Todd


Bill Todd delivers a comprehensive guide to working with InterBases
all-purpose BLOB data type, from manipulating bitmaps and JPEGs,
to working with WAVs and AVIs, to using BLOBs in queries.

FEATURES
12

Sound+Vision

Delphi Does DirectShow: Part I Jon Webb


Jon Webb introduces DirectShow technology and then demonstrates how
to playback video (AVI, MPEG, WMV), and audio (WAV, MP3, WMA) files
with a sample Delphi application.

16

On the Net

20

The API Calls

A WebSnap Online Survey: Part I Corbin Dunn


The first of a two-part series, Borland QA Engineer Corbin Dunn explains
how WebSnap changes the way Web development is done in Delphi, and
demonstrates with a sample application.
Windows API Error Handling: Part I Marcel van Brakel
Marcel van Brackel begins a two-part series that exposes the details of
Windows error handling, describing how to retrieve messages for Win32
error codes, retrieve messages from a specific DLL, and more.
1 January 2002 Delphi Informant Magazine

REVIEWS
24

Help & Manual

28

Async Professional ActiveX

31

Code Co-op

Product Review by Bill Todd


Product Review by Warren Rachele
Product Review by Robert Leahey

DEPARTMENTS
2
34

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

Delphi
T O O L S

New Products
and Solutions

Book Picks
Delphi Developers
Guide to XML
Keith Wood
Wordware Publishing

ISBN: 1-55622-812-0
Cover Price: US$59.95
(530 pages, CD-ROM)
www.wordware.com

Special Edition Using SOAP


John Paul Mueller
QUE

ISBN: 0-7897-2566-5
Cover Price: US$39.99
(436 pages)
www.quepublishing.com

2 January 2002 Delphi Informant Magazine

Wise Solutions Releases Wise Installation System 9


Wise Solutions, Inc., a global
provider of software installation
solutions for developers and
system administrators, announced
the availability of both the Standard and Professional Editions of
Wise Installation System 9. The
Standard Edition is an easy-to-use,
script-based installation tool that
quickly builds basic and reliable
installs for Windows platforms.
It includes an Installation Expert,
Script Editor, integrated debugger,
custom dialog editor, and much
more. The Professional Edition
satisfies software developers wide
range of advanced installation
requirements with additional
features such as Web deployment
and Windows CE installs.
Wise Installation System 9
includes application self-repair,
previously found only in Windows Installer technology. It
automatically recovers broken or
missing files, registry keys, or
components.
Wise Installation System 9
provides powerful capabilities for professional developers, including Web-based
installs, automated Web-based

application updating, and


smart-patching technology for
simplified installation upgrades.
Developers can build robust
installs within minutes, using
either the point-and-click
Installation Expert, or WiseScript, a powerful scripting
language designed specifically
for installation routines. This
product also makes it easy to
keep up with the latest thirdparty run times, with built-in
support for popular technologies such as Visual Basic,

MDAC, BDE, and Crystal


Reports.
Both editions of Wise Installation System 9 are available
for sale directly from Wise
and through select resellers. A
complete demonstration version
can be downloaded from http://
www.wisesolutions.com/wis.htm.
Wise Solutions, Inc.
Price: Standard Edition, US$449; Professional
Edition, US$899
Contact: info@wisesolutions.com
Web Site: http://www.wisesolutions.com

SkyLine Tools Imaging Releases ImageLib Corporate Suite 6


SkyLine Tools Imaging, a
provider of imaging tools in the
Delphi and C++Builder market,
announced ImageLib Corporate
Suite 6 for Delphi 5 and 6 and
C++Builder 4 and 5. The Suite
boasts TWAIN handling so that
scan speeds will match the highest
speeds of scanners. SkyLines Save
Ahead technology uses multithreads to simultaneously scan
and save documents on multipage or Automatic Document
Feeding scanners.
SkyLine also continues to offer its
SkyDocImage, an end-user document imaging package supplied
with source in Delphi, as part
of the suite. This package enables
programmers to immediately get
their application up and running
so they can demo it for prospective
clients. Use the SkyDocImage as is
out of the box, or tweak it for a
more customized application.
The suites anti-aliasing technology increases the readability
of displayed images by adding

several shades of gray to the


jagged edges of characters to
produce clear and smooth text.
The user no longer must zoom
in on certain areas of an image
to read the words. The Magnifying Glass helps users scan an
image in its original size rather
than requiring an overall zoom
in of the image, making it convenient to read one page without
adjusting the image onscreen.
Annotation capabilities allow
users to mark up images onscreen
and save them separately or burn
them into the document. The
user can add arrows, underline
text, highlight areas, add post-it
notes, and time-date stamps all
with a mouse-click. Annotations
in the Document Imaging component are flicker free, so readers
wont see a blacked-out word,
name, or salary when downloading an image.
ImageLib Corporate Suite 6
has a new resizable, stand-alone
Thumbnail Manager component,

with the latest Windows look and


feel, and can be used in several
components. The 20 file formats
supported by the suite include
PCD (Photo CD), TIFF CCITT
3 and 4, Multipage TIFFs,
Packbits, TIFF LZW, AVI, WAV,
MID, Animated GIF, Transparent
GIF, Progressive JPG, and PNG.
Equipped with many image correction, manipulation, and deformation tools, the suite offers 40
filters that allow easy handling
of color image files. Correction
effects, such as brightening, contrast, color reduction, hue and saturation, edge detection, inverting,
sharpening, and rotation, provide
vast improvement on dull, difficult-to-read images. In addition,
the Zoom, Pan, and Scroll features
offer precise control when moving
across large images.
SkyLine Tools Imaging
Price: US$599
Contact: (800) 404-3832
Web Site: http://www.imagelib.com

Delphi
T O O L S

New Products
and Solutions

Book Picks
Learn Pascal in Three Days,
Third Edition
Sam A. Abolrous
Wordware Publishing

ISBN: 1-55622-805-8
Cover Price: US$29.95
(324 pages, CD-ROM)
www.wordware.com

WML & WMLScript:


A Beginners Guide
Kris Jamsa
Osborne/McGraw-Hill

ISBN: 0-07-219294-1
Cover Price: US$29.99
(466 pages)
www.osborne.com

3 January 2002 Delphi Informant Magazine

Wise Solutions Introduces Wise Package Studio 3


Wise Solutions, Inc.
announced the availability of
Wise Package Studio 3, a
process-oriented solution
designed specifically for system
administrators working on
repackaging projects of any size
or complexity. Repackaging
or customizing a vendorsupplied installation has
become a necessary step for
administrators to ensure
reliability, to streamline and
standardize installations, and
to take advantage of new
operating system features and
technologies.
Wise Package Studios
SetupCapture combines
capturing technology with
Wise Solutions exclusive
SmartMonitor technology to
significantly improve the
quality and consistency of
repackaged installations.
SetupCapture provides detailed
capture reports for a full audit
trail, and uses customizable
exclusion lists to ensure accurate captures.
Package Studio also provides
three levels of package validation and conflict management
to detect and resolve software
conflicts between applications
before theyre deployed. In
addition to simple package

validation, the product provides a wizard-based approach


to resolving individual conflicts
and a rules-based approach
that automatically resolves
conflicts based on predefined
conflict resolution rules.
Package Studio is the only
product that provides both
Windows Installer and scriptbased installation technologies
with full support for installation creation and editing with
either technology. It creates a
single-file, self-contained executable, or .msi package, which
can be distributed through
any of the leading software
distribution systems. Package
Studio provides tight integration with distribution tools
such as Microsoft SMS to allow
seamless package hand-offs to
these distribution systems.
Wise Package Studio 3 is
available in three editions. The
Enterprise Edition promotes a
collaborative approach utilizing
a workbench interface, which
streamlines the workflow process and promotes hand-offs
between group members, task
documentation, project tracking, and user security. The Professional Edition is designed
for application integration
teams that deploy software at

a department or division level,


while the Standard Edition
provides a simple, cost-effective way to repackage applications. For companies migrating
to Windows 2000 or XP, all
editions of Package Studio
provide the ability to migrate
installations to Windows
Installer (.msi) technology,
taking advantage of benefits
such as self-healing, installon-demand, and installation
rollback, even if the existing
installation doesnt support
.msi standards.
All three editions of Wise
Package Studio 3 are available
for sale directly from Wise,
and the Standard and Professional Editions are also available through select resellers.
Complete evaluation versions
(30-day use limit) of the
Standard and Professional Editions are downloadable at
http://www.wisesolutions.com/
wps.htm. Special upgrade pricing is available for owners of
other Wise Solutions products.
Wise Solutions, Inc.
Price: Standard, US$1,299;
Professional, US$3,299; Enterprise,
contact Wise for price.
Contact: info@wisesolutions.com
Web Site: http://www.wisesolutions.com

Greatis Software Announces WinDowse 4.2


Greatis Softwares WinDowse 4.2 is a convenient
tool for obtaining necessary

technical information about


any window. Place a mouse
cursor on a window, and this
advanced windows
analyzer will show
all parameters of the
window and window
class, including
information from
the Window, Class,
Parents, Children,
Digger, and Graphics tabs.
All parameters can
be shown in hexadecimal, decimal,
or binary formats.
Upon activation,
WinDowse displays
a continuous
readout as the user
moves the mouse
around the screen

switching between separate


or nested windows. The continuous readout can be frozen
by a click of the mouse, and the
data for that window studied in
detail at any time. WinDowse
also allows results to be copied
directly to the Clipboard. Each
field of the analysis is supplied
with context-sensitive help.
WinDowse 4.2 is free software, although full source code
for Delphi can be purchased for
US$19.95. WinDowse is available for download from http://
www.greatis.com/wdsetup.exe.
Greatis Software
Price: Free
Contact: b-team@greatis.com
Web Site: http://www.greatis.com /
windowse.htm

Delphi
T O O L S

New Products
and Solutions

Book Picks
Advanced Delphi Developers
Guide to ADO
Alex Fedorov and
Natalia Elmanova, Ph.D.
Wordware Publishing

ISBN: 1-55622-758-2
Cover Price: US$59.95
(643 pages, CD-ROM)
www.wordware.com

Bluetooth Revealed
Brent A. Miller and
Chatschik Bisdikian
Prentice Hall PTR

ISBN: 0-13-090294-2
Cover Price: US$44.99
(303 pages)
www.phptr.com

4 January 2002 Delphi Informant Magazine

Borland Kylix 2 Delivers First Web Services Solution for Linux


Borland Software Corp.
announced Borland Kylix 2
for the rapid development of ebusiness applications for Linux.
Kylix 2 enables companies
to rapidly build and deploy
applications that simplify ebusiness integration with Web
Services across diverse platforms
between customers, suppliers,
business partners, and employees worldwide.
According to a recent survey
by Evans Data, 70 percent
of Linux developers believe
Web Services now represent
the future of Internet applications. Kylix 2 leverages Web
Services and XML technologies
to expand the e-business capabilities of Linux and Apache.
By combining Kylix 2 with Borland Delphi 6, the first Web
Services development environment for the Windows platform, companies can develop
cross-platform enterprise-class
applications that integrate with
Web Services industry standards, SOAP, XML, and WSDL.
Kylix 2 features BizSnap, WebSnap, and DataSnap to form
a fully integrated set of visual
tools with advanced compiler
technology and re-usable soft-

ware components. BizSnap Web


Services RAD development
platform simplifies next generation e-business integration by
easily creating XML/SOAP
Web Services and connections;
WebSnap transforms Apache
Web servers into powerful
enterprise-class database-driven
Web application servers; and
DataSnap allows customers
to build high-performance,
Web Service-enabled database
middleware that scales and
interoperates with any e-business solution, such as Oracle,
Informix, IBM DB2, MySQL,

MicroOLAP Technologies LLC Announces MySQLDAC 1.8


MicroOLAP Technologies
LLC released MySQL Direct
Access Components (MySQLDAC) 1.8, a library of native
VCL components for direct
access to the MySQL database
server from Borland Delphi and
C++Builder. The main advantage of MySQLDAC 1.8 is that
it doesnt require BDE/ODBC
on the client side.
This product allows you to
simplify distribution, administration, and support of your
applications and reduces system
requirements. It supports all
MySQL specific features, such
as Show, Transaction, Ping,
Kill Process, and Check and
Repair Table, and simplifies the
development of client/server
applications. In addition, you
can insert, update, delete and
refresh easily, and theres support of BLOB fields, which

speeds up retrieving records


from a database. The TDBImageEx component for JPEG
images support is included with
source for free.
The interface of MySQLDAC
1.8 is BDE-compatible, so it
allows for easier migration of
existing projects from the
BDE/ODBC scheme to the
native one. It supports MySQL
3.23.xx stable versions for all
existing platforms.
MicroOLAP Technologies distributes MySQLDAC packages
for Borland Delphi 46 and
C++Builder 5. A trial version is
available for free download.
MicroOLAP Technologies LLC
Price: Single-user license, US$49.95;
single-user license with source, US$79.95.
Contact: info@microolap.com
Web Site: http://www.microolap.com/
mysqldac.htm

PostgreSQL/Red Hat Database, and Borland InterBase,


through industry standard Web
Services and XML, DCOM, or
CORBA. With these components, Kylix 2 users can easily
exchange and manipulate XML
documents, integrate applications across the Internet, and
take advantage of additional
functionality from Web Service
platforms such as .NET and
BizTalk from Microsoft and
ONE from Sun Microsystems.
Kylix 2 is available in three
editions: Kylix 2 Enterprise,
Kylix 2 Professional, and Kylix
2 Open. For new users, Kylix
2 Enterprise edition is available
for US$1,999 and Kylix 2 Professional edition for US$249.
Special upgrade pricing is available for Kylix customers, as well
as special pricing for Delphi
6 users. Kylix 2 Enterprise
and Professional editions are
available directly from Borland
at http://shop.borland.com and
through authorized resellers. A
60-day free trial download of
Kylix 2 Enterprise is available at
http://www.borland.com/
downloads/. Kylix 2 Open edition for open source software
(general public license) developers will be made available
during the fourth quarter for
free download from http://
www.borland.com/downloads/.
Borland Software Corp.
Price: US$299 to US$1,999 for new users
Contact: (800) 632-2864
Web Site: http://www.borland.com/kylix

Columns & Rows


InterBase / Binary Data / Images / Sounds / Text / Delphi 2-6

By Bill Todd

The BLOB
Working with InterBases Binary Data Type

ne of the more useful features of most modern databases is their ability to store
binary data, i.e. data of any type, format, or size. Its useful because its versatile; you
can use a binary field to hold anything from a WordPerfect document to an MP3 file.
InterBase was actually the first database to feature
such a field type, and refers to it as a BLOB. Heres
how its defined in the InterBase documentation:
InterBase provides the binary large object (BLOB)
data type to store data that cannot easily be stored
in one of the standard SQL data types. A BLOB is
used to store very large data objects of indeterminate and variable size, such as bitmapped graphics
images, vector drawings, sound files, video segments, chapter or book-length documents, or any
other kind of multimedia information.
The basic tools for getting a lot of BLOB data into
and out of a database table are the LoadFromFile
and SaveToFile methods of TBlobField. If the only
thing you want to do with a BLOB is load an external file containing the BLOB data into a database
table, or save the BLOB data in a database table to
an external file, these are the only methods youll
need. However, you can do a lot more with BLOBs
using native Delphi components. This article takes
a look at techniques for getting BLOB data into
and out of fields in database records, memory, files,
and Delphi components.
The sample application that accompanies this
article uses an InterBase database, the InterBase
Express components, and a ClientDataSet, but

CREATE TABLE "Blobs"(


"Blob_ID" INTEGER NOT NULL,
"Blob_Type" "BLOB_TYPE_DOMAIN",
"Text_Data" BLOB SUB_TYPE TEXT SEGMENT SIZE 80,
"Binary_Data" BLOB SUB_TYPE 0 SEGMENT SIZE 512,
PRIMARY KEY("Blob_ID"));

Figure 1: The Blobs table in the BLOB2.GDP database.


5 January 2002 Delphi Informant Magazine

the same methods and techniques will work with


most of the Delphi dataset components (see end
of article for download details). The InterBase
database contains just two tables. The first is
Blobs, and its structure is shown in Figure 1. The
BLOB_TYPE_DOMAIN is a VarChar(32) used
to hold a text description of the type of BLOB
in the record. The second table, Blob_Type,
contains a list of the valid BLOB types and is
used to validate the value of the Blob_Type field
in the Blobs table. Validation of the Blob_Type
field is done by the Before Insert and Before
Update triggers. The value for the Blob_ID field
is supplied by a generator.

Working with Bitmaps


LoadFromFile and SaveToFile are methods of the
TBlobField class. TBlobField is the field object class
used for BLOB fields whether you instantiate the
field objects at design time using the Fields Editor,
or let Delphi create the field objects dynamically
at run time. Figure 2 shows the code from the Load
File button of the Bitmap form in the sample application (see Figure 3). This code begins by setting
the InitialDir property of the TOpenDialog named
OpenBmpDialog, and then calls its Execute method
to display the dialog box. If the user chooses a file
and clicks the OK button, the code checks to see if
the ClientDataSet, cdsBlob, is in Browse mode. If it
is, the ClientDataSets Edit method is called. Next,
the cdsBlobBinary_Data field objects LoadFromFile
method is called to load the contents of the selected
file into the Binary_Data field of the current record.
This code works only because the field objects
for the cdsBlob ClientDataSet were instantiated
at design time using the Fields Editor, so that the

Columns & Rows


procedure TfrmBmp.btnLoadFileClick(Sender: TObject);
begin
with OpenBmpDlg do begin
InitialDir := ExtractFilePath(
Application.ExeName + gFileDir);
if not Execute then Exit;
end;
if dmMain.cdsBlob.State = dsBrowse then
dmMain.cdsBlob.Edit;
dmMain.cdsBlobBinary_Data.LoadFromFile(
OpenBmpDlg.FileName);
end;

Figure 2: Code for the Load File button of the Bitmap form.

procedure TfrmBmp.SetImageSize;
begin
if dbiBmp.Picture.Height > dbiBmp.Picture.Width then
begin
if dbiBmp.Picture.Height > 0 then begin
dbiBmp.Height := pnlBmp.Height - 2;
dbiBmp.Width := Trunc(dbiBmp.Height *
(dbiBmp.Picture.Width / dbiBmp.Picture.Height));
dbiBmp.Left := (pnlBmp.Width-dbiBmp.Width) div 2;
dbiBmp.Top := 1;
end;
end
else
if dbiBmp.Picture.Width > 0 then
begin
dbiBmp.Width := pnlBmp.Width - 2;
dbiBmp.Height := Trunc(dbiBmp.Width *
(dbiBmp.Picture.Height / dbiBmp.Picture.Width));
dbiBmp.Top := (pnlBmp.Height-dbiBmp.Height) div 2;
dbiBmp.Left := 1;
end;
end;

Figure 4: The SetImageSize method.


procedure TfrmBmp.btnCopyClipboardClick(Sender: TObject);
begin
dbiBmp.CopyToClipboard;
end;
procedure TfrmBmp.btnPasteClick(Sender: TObject);
begin
dbiBmp.PasteFromClipboard;
end;

Figure 3: The Bitmap form.

TBlobField field object instance named cdsBlobBinary_Data could


be used to call its LoadFromFile method. If you try to use:
dmMain.cdsBlob.FieldByName('Binary_Data').
LoadFromFile(OpenBmpDlg.FileName;

you will get an Undeclared identifier: LoadFromFile error. This


happens because the FieldByName method returns an object reference
of type TField, and TField doesnt have a LoadfromFile method. If you
want to use FieldByName or the Fields array property to access the
field object, youll have to cast the reference to TBlobField as shown
in this statement:

Figure 5: The OnClick event handlers of the Copy and Paste


buttons.
procedure TfrmBmp.btnSaveJpegClick(Sender: TObject);
var
JpegImage: TJpegImage;
begin
JpegImage := TJpegImage.Create;
try
JpegImage.Assign(dbiBmp.Picture.Bitmap);
JpegImage.SaveToFile(ExtractFilePath(
Application.ExeName) + 'Files\Bitmap.jpg');
finally
JpegImage.Free;
end;
end;

Figure 6: Saving a bitmap image in JPEG format.


dmMain.cdsBlob.TBlobField(FieldByName('Binary_Data'
)).LoadFromFile(OpenBmpDlg.FileName;

The Save File button on the Bitmap form uses code thats almost
identical. The only difference is that it uses a TSaveDialog and calls
the SaveToFile method.
Windows bitmaps are the easiest type of graphic to work with in
Delphi, because they can be displayed using the DBImage data-aware
component. The only trick to displaying bitmaps is to decide how
you want to handle the size of the image in relation to the size of
the DBImage component. There are two choices. First, you can show
the bitmap actual size by setting the Stretch property of the DBImage
to False. If the bitmap is larger than the DBImage component, youll
only see part of the image. This is likely to be unsatisfactory since
theres no way to add scroll bars to the DBImage to let users move
to other areas of the image.
6 January 2002 Delphi Informant Magazine

In the sample database used to write this article, all of the images
were produced by a digital camera and are 1,600 pixels wide
by 1,200 pixels high. To fit the entire image into the DBImage
component, a second option is used. The Stretch property is set
to True, which causes the image to be resized to fit the DBImage
components size. The problem with this technique is that the
picture will appear to be distorted if the aspect ratio, the ratio of
width to height, of the DBImage doesnt match the aspect ratio of
the bitmap.
Solving the aspect ratio problem is easy if all of the images have
the same orientation. All you have to do is make sure you set
the Width and Height properties of the DBImage to match the
aspect ratio of your bitmaps at design time. The sample database
contains bitmaps in both portrait and landscape orientation. To
solve the problem, the SetImageSize method, shown in Figure 4, is

Columns & Rows


procedure TfrmBmp.btnInsertQueryClick(Sender: TObject);
var
BinFile: TFileStream;
Buff: string;
begin
with dmMain.ibsqlMisc do begin
// Create file stream.
BinFile := TFileStream.Create(ExtractFilePath(
Application.ExeName) + gFileDir + '\' +
'BallastControl.bmp',
fmOpenRead or fmShareDenyWrite);
try
// Allocate memory for the buffer.
SetLength(Buff, BinFile.Size);
// Read file into buffer.
BinFile.Read(Buff[1], BinFile.Size);
// Assign SQL statement to query.
dmMain.ibsqlMisc.SQL.Add(
'INSERT INTO Blobs (Blob_Type, Binary_Data) ' +
'VALUES (:BlobType, :BlobData)');
dmMain.ibtranMisc.StartTransaction;
try
// Set query parameter.
dmMain.ibsqlMisc.Params.ByName(
'BlobType').AsString := btBmp;
dmMain.ibsqlMisc.Params.ByName(
'BlobData').AsString := Buff;
// Execute query.
dmMain.ibsqlMisc.ExecQuery;
finally
dmMain.ibtranMisc.Commit;
end;
finally
BinFile.Free;
end;
end;
dmMain.cdsBlob.Refresh;
end;

Figure 7: The Insert Query buttons OnClick event handler.

procedure TfrmJpeg.btnViewClick(Sender: TObject);


begin
LoadImageFromField(imgJpeg, dmMain.cdsBlobBinary_Data);
SetImageSize;
end;
procedure TfrmJpeg.LoadImageFromField(Image: TImage;
ImageField: TBlobField);
var
MemStrm: TMemoryStream;
Jpg: TJPEGImage;
begin
if ImageField.IsNull then begin
Image.Picture.Assign(nil);
Exit;
end;
Jpg := TJPEGImage.Create;
try
MemStrm := TMemoryStream.Create;
try
ImageField.SaveToStream(MemStrm);
MemStrm.Seek(0, soFromBeginning);
with Jpg do begin
PixelFormat := jf24Bit;
Scale := jsFullSize;
Grayscale := False;
Performance := jpBestQuality;
ProgressiveDisplay := True;
ProgressiveEncoding := True;
LoadFromStream(MemStrm);
end;
Image.Picture.Assign(Jpg)
finally
MemStrm.Free;
end;
finally
Jpg.Free;
end;
end;

Figure 9: Methods that display a JPEG from a table in an Image


component.

method sets the width of the DBImage to the width of the panel
minus two, and calculates the height to maintain the aspect ratio.
To get a bitmap thats displayed in a DBImage into the Clipboard, call
the CopyToClipboard method of the DBImage component. To paste
a bitmap from the Clipboard into the DBImage, call the DBImages
PasteFromClipboard method. The code from the Bitmap forms Copy
and Paste buttons OnClick event handlers is shown in Figure 5.

Figure 8: The JPEG form.

called from the AfterScroll event handler of the cdsBlob ClientDataSet. The DBImage has a Picture property of type TPicture
thats a reference to the TPicture object that holds the bitmap.
SetImageSize uses the Picture.Height and Picture.Width properties
of the DBImage to determine if the image orientation is portrait
or landscape. If the orientation is portrait, the method sets the
height of the DBImage to the height of the panel that contains
it minus two pixels, and calculates the width of the DBImage
to preserve the aspect ratio. If the orientation is landscape, the
7 January 2002 Delphi Informant Magazine

One of the disadvantages of the Windows bitmap image format


is that no compression is used, so the files are large. If you need
to save a bitmap to a file and you want to minimize the file size,
you can convert it to the JPEG format. While JPEG compression
can reduce the file size dramatically, it uses a lossy compression
algorithm so some image data will be lost. The code from the Save
As JPEG buttons OnClick event handler is shown in Figure 6.
This method begins by creating an instance of the TJpegImage class.
The code converts the bitmap thats currently displayed in the DBImage component by calling the Assign method of the JpegImage object,
and passing the Bitmap property of the Picture object referenced by the
Picture property of the DBImage component as the parameter. This
converts the bitmap to a JPEG. Next, the JpegImage objects SaveToFile
method is called to save the image to the BITMAP.JPG file. Finally, the
JpegImage object is freed. You can reduce the compression and increase
the quality of the JPEG image by changing the value of the JpegImage
objects CompressionQuality property as described in the online Help.

Columns & Rows


procedure TfrmJpeg.btnCopyClipboardClick(Sender: TObject);
begin
Clipboard.Open;
Clipboard.Assign(imgJpeg.Picture);
Clipboard.Close;
end;
procedure TfrmJpeg.btnPasteClipboardClick(Sender: TObject);
begin
imgJpeg.Picture.Bitmap.Assign(Clipboard);
end;

Figure 10: The Copy and Paste buttons OnClick event handlers
from the JPEG form.

procedure TfrmJpeg.btnSaveAsBmpClick(Sender: TObject);


var
BmpImage: TBitmap;
begin
BmpImage := TBitmap.CREATE;
try
BmpImage.Assign(imgJpeg.Picture.Graphic);
BmpImage.SaveToFile(ExtractFilePath(
Application.ExeName) + 'Files\Jpeg.bmp');
finally
BmpImage.Free
end;

Figure 11: Converting a JPEG to a bitmap.

procedure TfrmJpeg.btnSaveBmpToDbClick(Sender: TObject);


var
BmpImage: TBitmap;
Bs:
TStream;
begin
BmpImage := TBitmap.Create;
try
BmpImage.Assign(imgJpeg.Picture.Graphic);
with dmMain.cdsBlob do begin
Insert;
dmMain.cdsBlobBLOB_TYPE.AsString := btBmp;
Bs := CreateBlobStream(
dmMain.cdsBlobBinary_Data, bmWrite);
try
BmpImage.SaveToStream(Bs);
finally
Bs.Free;
end;
Post;
end;
finally
BmpImage.Free;
end;
end;

Figure 12: Using a BlobStream to load a BLOB into a field in


a table.

Using Blobs in Queries


To include BLOB data in an INSERT or UPDATE query, you must
use a parameter for the BLOB data. Figure 7 shows the code from the
OnClick event handler of the Insert Query button on the Bitmap form.
This code loads a bitmap file into memory, then uses an INSERT
query to add a new record, which includes the bitmap, to the table.
The method begins by creating a FileStream object that opens the
BallastControl.bmp file for reading. A string variable named Buff is
used to hold the bitmap in memory. The next step is to set the size
of Buff to the size of the file by calling SetLength, and passing Buff
8 January 2002 Delphi Informant Magazine

Figure 13: The Other Blobs form.

as the first parameter and BinFile.Size as the second parameter. The


next line calls the FileStreams Read method to read the entire file into
the string. Note that you cannot pass the string variable Buff itself as
the first parameter to the Read method because this parameter is an
untyped pointer to a location in memory. Instead, you must pass the
first element of the string array, which is a pointer to the beginning of
the block of memory where the strings data is stored.
The data module in the sample application contains an IBSQL
component, ibsqlMisc, thats used to insert the new record. The
SQL statement:
INSERT INTO Blobs (Blob_Type, Binary_Data)
VALUES (:BlobType, :BlobData)

is assigned to the SQL property if the IBSQL component and the


values are assigned to the two parameters in the SQL statement. Note
that the BlobData parameter is assigned using its AsString property
from the string variable Buff that contains the bitmap. Finally, the
code starts a transaction, executes the query, commits the transaction,
and frees the FileStream. Updating a BLOB field in an existing record
is done exactly the same way.
It may seem strange to use a string variable to hold the BLOB, but
its the only option available for the IBSQL and IBDataSet components because their parameter object doesnt have an AsBlob property.
If youre using the BDE dataset components or any other dataset
components that use the TParam object for parameters, youll have the
option of using the AsBlob property. AsBlob expects to be assigned a
pointer to an untyped string of bytes, so you can use a PChar to hold
the BLOB and assign the PChar to the parameter using:
Query1.ParamByName('BlobData').AsBlob :=
SomePCharThatContainsTheBlob;

Working with JPEG Images


Working with JPEG images is very similar to working with bitmaps,
but there are important differences. The first of these is that the
DBImage component cannot display a JPEG. Instead, you must use a
TImage and move the JPEG from the database field to the Image component in code. Figure 8 shows the JPEG form from the sample application. Figure 9 shows the code from the View JPEG buttons OnClick
event handler and the LoadImageFromField method that gets the JPEG
image from the field and displays it in the Image component.

Columns & Rows


procedure TfrmOther.btnPlaySoundClick(Sender: TObject);
var
Bs: TStream;
Ms: TMemoryStream;
begin
Bs := dmMain.cdsBlob.CreateBlobStream(
dmMain.cdsBlobBinary_Data, bmRead);
try
Ms := TMemoryStream.Create;
try
// Copy the WAV file to memory.
Ms.CopyFrom(Bs, Bs.Size);
// Play it, but if there's an error,
// raise an exception.
Win32Check(PlaySound(
Ms.Memory, 0, SND_SYNC or SND_MEMORY));
finally
Ms.Free
end;
finally
Bs.Free
end;
end;

Figure 14: Playing a sound.


procedure TfrmOther.MediaPlayerClick(Sender: TObject;
Button: TMPBtnType; var DoDefault: Boolean);
begin
MediaPlayer.Close;
dmMain.cdsBlobBinary_Data.SaveToFile(ExtractFilePath(
Application.ExeName) + 'Files\Temp.avi');
MediaPlayer.FileName := ExtractFilePath(
Application.ExeName) + 'Files\Temp.avi';
MediaPlayer.Open;
end;

Figure 15: The MediaPlayers OnClick event handler.

LoadImageFromField takes two parameters. The first is the Image


component that will be used to display the JPEG image. The second
is the BlobField field object for the field that contains the JPEG image.
The method begins by checking to see if the field contains any data.
If not, it sets the Picture property of the Image component to nil to
remove whatever is currently being displayed, and exits. If the field
contains data, instances of the TJPEGImage object and MemoryStream
object are created. Next, the BlobField objects SaveToStream method
is called to copy the JPEG image from the field to the MemoryStream.
The code in the with block sets the properties of the JPEGImage
object and calls its LoadFromStream method to load the JPEG image
into the JPEGImage object from the MemoryStream. Finally, the
Image components Picture objects Assign method is called to assign
the JPEGImage component to the Picture property. This causes the
JPEG image to appear in the Image component. If you wanted to
automatically display the JPEG images in a table, you could call this
method from the datasets AfterScroll event handler.
Using the Clipboard with an Image component is different from
using the Clipboard with a DBImage, as shown in Figure 10. Since
the Image component has no Clipboard methods, the Delphi Clipboard object is used to copy an image out of, or paste an image into,
the Image component.
Figure 11 shows how to convert a JPEG image to a Windows bitmap
and save it to a file. A Bitmap object is created and its Assign method
is called to convert the JPEG in the Image component to a bitmap
and load it into the Bitmap object. Saving the bitmap to a file is
accomplished by calling the Bitmap objects SaveToFile method.
9 January 2002 Delphi Informant Magazine

procedure TfrmOther.btnSaveArrayClick(Sender: TObject);


var
A: array[1..10] of Integer;
I: Integer;
Bs: TStream;
begin
// Fill the array with the numbers 1 through 10.
for I := 1 to 10 do
A[I] := I;
// Insert a new record if the ClientDataSet
// is in Browse mode.
if dmMain.cdsBlob.State = dsBrowse then begin
dmMain.cdsBlob.Insert;
dmMain.cdsBlobBlob_Type.AsString := btBinary;
end;
// Create a BlobStream.
Bs := dmMain.cdsBlob.CreateBlobStream(
dmMain.cdsBlobBinary_Data, bmReadWrite);
try
// Write the array to the BlobStream.
Bs.Write(A, SizeOf(A));
finally
Bs.Free;
end;
dmMain.cdsBlob.Post;
end;

Figure 16: Saving an array to a BLOB field.

The Save BMP To DB buttons OnClick event handler is shown in


Figure 12. This code shows how to use a BlobStream to load an
image from any object that has a SaveToStream method. This
example creates a Bitmap object, then calls its Assign method to load
the JPEG into the Bitmap object as a bitmap. The method then
inserts a new record into the ClientDataSet, assigns a value to the
Blob_Type field, creates a BlobStream object, and calls the Bitmap
objects SaveToStream method to load the bitmap into the field.
Finally, the BlobStream is freed, the record is posted, and the Bitmap
is freed. Note that you must free the BlobStream before you post the
record or the field will be null.
In this specific case, where the image was in a Bitmap object, it
would have been easier to call the BlobField s Assign method to get
the bitmap into the field. However, if the bitmap or JPEG was in
a MemoryStream, FileStream, or some other location where Assign
couldnt be used, you would need to use a BlobStream:
dmMain.cdsBlobBinary_Date.Assign(BmpImage);

Working with WAV and AVI Files


Getting a WAV or AVI file into the table is no different than
loading a JPEG. Just use the BlobField objects LoadFromFile
method. The question is: What do you do with the data once
its in the database? The Other Blobs form, shown in Figure 13,
contains a Play Sound button whose OnClick event handler is
shown in Figure 14. You can play a WAV file using the Windows
API PlaySound function, but first you have to load the WAV data
into memory.
The code in Figure 14 begins by creating a BlobStream. It does this
by calling the ClientDataSets CreateBlobStream method and passing the field object for the field as the first parameter. The second
parameter is the mode: bmRead, bmWrite, or bmReadWrite. Next,
a MemoryStream is created and the BLOB data is copied into the
MemoryStream, Ms, by calling the MemoryStreams Copy method.
Copy takes two parameters, the source of the data, in this case the

Columns & Rows


procedure TfrmOther.btnShowArrayClick(Sender: TObject);
var
A: array[1..10] of Integer;
Bs: TStream;
I: Integer;
S: string;
begin
// Don't try it if the Blob_Type is not Binary.
if dmMain.cdsBlobBlob_Type.AsString <> btBinary then
Exit;
// Post the record if not in Browse mode.
if dmMain.cdsBlob.State <> dsBrowse then
dmMain.cdsBlob.Post;
// Create the BlobStream.
Bs := dmMain.cdsBlob.CreateBlobStream(
dmMain.cdsBlobBinary_Data, bmRead);
try
// Read the data from the BlobStream into the array.
I := Bs.Read(A, Bs.Size);
finally
Bs.Free;
end;
// Display the values.
for I := 1 to 10 do
S := S + IntToStr(A[I]) + '-';
ShowMessage(S);
end;

Figure 17: The Show Array buttons OnClick event handler.

BlobStream, and the number of bytes to copy. The Size property


of the BlobStream is passed as the size parameter so that the entire
contents of the BLOB field are copied to the MemoryStream.
The WAV data is played by calling the Windows API function
PlaySound. PlaySound is passed as a parameter to the Win32Check
procedure, which checks the value returned by PlaySound and raises
an exception if the returned value indicates an error.
PlaySound takes three parameters. The first indicates the source
of the sound and can be a file name, resource name, or pointer
to the location in memory where the sound is located. If the first
parameter indicates a resource, the second parameter is the handle
of the executable that contains the resource; otherwise, the second
parameter is nil. The third parameter consists of one or more
constants logically ordered together to identify the source of the
sound and the action to take. In Figure 14, the third parameter is
SND_SYNC or SND_MEMORY to indicate that the first parameter is a pointer to a memory location that contains the beginning
of the sound and that the PlaySound function shouldnt return until
the sound has finished playing. Finally, both the MemoryStream and
BlobStream are freed.
You can play an AVI file using Delphis MediaPlayer component;
however, the MediaPlayer can only use data in a disk file. Figure 15
shows the code from the OnClick event handler of the Other Blobs
form. This method closes the MediaPlayer, saves the contents of
the Binary_Data field in the current record of the ClientDataSet to
disk using the BlobField objects SaveToFile method, then sets the
FileName property of the MediaPlayer to the file. The user can now
use the MediaPlayers buttons to play the AVI.

Working with Memory Structures


The Other Blobs form also contains two buttons labeled Save Array
and Show Array. The first saves an integer array to a BLOB field,
and the second retrieves the array and displays its values. Figure 16
shows the code from the OnClick event handler of the Save Array
button. This method fills a 10-element integer array with the values
10 January 2002 Delphi Informant Magazine

procedure GetMemoLineCol(Memo: TCustomMemo;


var MemoLine, MemoCol: Integer);
begin
with Memo do begin
// Get the line number of the line that contains
// the cursor.
MemoLine := SendMessage(
Handle, EM_LINEFROMCHAR, SelStart, 0);
// Get the offset of the cursor in the line.
MemoCol := SelStart - SendMessage(
Handle, EM_LINEINDEX, MemoLine, 0) + 1;
end;
end;

Figure 18: The GetMemoLineCol procedure.

procedure MemoCursorTo(Memo: TCustomMemo;


MemoLine, MemoCol: Integer);
begin
with Memo do
SelStart := SendMessage(Handle,
EM_LINEINDEX, MemoLine, 0) + MemoCol - 1;
end;

Figure 19: The MemoCursorTo procedure.

1 through 10. If the ClientDataSet is in Browse mode, a new record


is inserted. Next, a BlobStream is created for the Binary_Data field,
and the array is written to the BlobStream by calling the BlobStreams Write method and passing the address of the array as the
first parameter and the size of the array as the second.
Figure 17 shows the code from the Show Array buttons OnClick
event handler. Here, a BlobStream is created and its Read method is
called to read the data from the BLOB field into the integer array.

Working with Text


Delphi provides many ways to easily get text into, or out of, a
memo field in a table. The DBMemo component makes it easy for
users to enter and edit text. The BlobField objects LoadFromFile
and SaveToFile methods make it easy to get text from, or export it
to, a file. The contents of a memo field can be assigned to a string
variable and any of Delphis string manipulation routines used
to modify the text. If you want to work with the contents of a
memo field in code as an array of lines, assign the field to the Text
property of a StringList.
The Text form in the sample application demonstrates the
LoadFromFile and SaveToFile methods of the BlobField object, as
well as the CopyToClipboard and PasteFromClipboard methods of
the DBMemo component. However, there are some features that
DBMemo doesnt provide. For example, theres no way to get
the cursor position as a line number and column number. The
GetMemoLineCol procedure shown in Figure 18 shows how
to do this using the Windows API. This procedure takes the
Memo component as its first parameter. Note that the parameter
is of type TCustomMemo so you can pass either a Memo or a
DBMemo. The second and third parameters are var parameters
used to return the line and column numbers.
The procedure begins by calling the Windows API function SendMessage
and passing the Memo components Handle property as the first parameter. The second parameter is the constant EM_LINEFROMCHAR,
which tells the Memo component that it should return the number of
the line that contains a particular character position. The third param-

Columns & Rows


eter is the Memo components SelStart property, which supplies the
cursor position as an offset from the beginning of the memo. The fourth
parameter is not used.
The column that the cursor is in is computed by subtracting the
offset of the beginning of the line, returned by SendMessage using
the EM_LINEINDEX constant from the cursor position contained
in the SelStart property plus one. This procedure is called by the
UpdateCursorPos method in the Text form, which displays the current
line and column number in the status bar. UpdateCursorPos is called
from the forms OnCreate event handler and from the DBMemos
OnKeyDown and OnMouseDown event handlers.
The MemoCursorTo procedure in Figure 19 lets you set the cursor
position to a specified line and column. This procedure positions
the Memo components cursor by assigning a value to its SelStart
property. SelStart must be specified as an offset from the beginning
of the text. The line number is converted to an offset by calling
SendMessage with the EM_LINEINDEX constant, just as in the
GetMemoLineCol procedure in Figure 16, and adding the column
number minus one to it.

Conclusion
Working with database BLOB fields in Delphi is both easy and
flexible, once you understand the tools that are available and how
to use them. You can store any string of bytes in a BLOB field
regardless of what the bytes represent. They could be a picture,
a Word document, sound, a movie, an array, a Pascal record, or
anything else that can be stored in a binary file on a computer. The
sample application that accompanies this article doesnt include a
database because of the size of the graphic BLOBs (the test database
for this article was 24MB). Instead, a backup of an empty database
is included in the file BLOB2.GBK. Restore this backup using
IBConsole or GBAK to create an empty database to use with the
sample application.
The sample application referenced in this article is available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2002\JAN\DI200201BT.

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 80 articles, a contributing editor to Delphi
Informant Magazine, and a member of Team B, providing technical support on
the Borland Internet newsgroups. Todd is a nationally known trainer, has taught
Delphi programming classes across the country and overseas, and is a frequent
speaker at Borland Developer Conferences in the United States and Europe. Readers can reach him at bill@dbginc.com.

11 January 2002 Delphi Informant Magazine

Sound+Vision
DirectShow / DirectX / COM / Delphi 3-6

By Jon Webb

Delphi Does DirectShow


Part I: Audio/video Playback

irectShow is Microsofts multimedia architecture and part of the DirectX API. It allows us
to record and play multimedia files and streams, and to control multimedia devices such
as TV tuners and DVD players. This article will introduce the concepts used in DirectShow,
and demonstrate how to build a DirectShow audio/video player application.
Weve all seen (and sometimes cursed) Delphis
built-in TMediaPlayer control. Its based on the
MCI API, which was developed for 16-bit Windows. The MCI API and its sister API, Video
for Windows, werent sufficiently extensible to
cope with emerging multimedia technologies.
DirectShow is the successor to these two APIs,
and gives us much greater flexibility.
You may have used DirectShow before without even
knowing it. Mostly because of the re-branding frenzy
of Microsoft marketing, the same technology has
been known as Quartz, Active Movie, DirectX Media
and now DirectShow. To make things difficult for
us developers, evidence of these multiple personalities
can be found throughout the library.

Its All in the Graph


The central concept of DirectShow is the filter graph.
A filter graph is a collection of plug-in modules, called
filters, that can be connected to each other with pins
(more about pins in a bit).

Most vendors of modern multimedia hardware


supply DirectShow-compatible WDM drivers to
interface with their particular product. DirectShow is designed to be hardware independent.
Your application speaks to all hardware using the
common DirectShow API.

The name filter can be a bit misleading, as not all


filters actually filter anything. Each filter performs
a specific task, such as reading data from a file, converting data from one format to another, controlling a piece of hardware, displaying video on the
screen, and so on. For instance, a TV application
might have a graph containing a tuner filter for
channel selection, audio and video capture filters
that digitize the audio and video signals, and audio
and video renderer filters that play audio through
the sound card and display video on-screen. Figure
1 shows an example of a filter graph.

DirectShow makes heavy use of COM. To understand this article, you should be familiar with using
COM interfaces. You wont need to create any
interfaces yourself, but if you arent comfortable using
COM you may want to look up COM Interfaces
in the Delphi online help, review chapter 44 in the

The programmer can configure these filters


using the (often numerous) COM interfaces
that the filters publish, or sometimes display
filter property pages. DirectShow comes with
many standard filters to perform common tasks,
such as splitting AVI data into audio and video

Figure 1: Example of a filter graph.


12 January 2002 Delphi Informant Magazine

Delphi manual, check out Eric Harmons excellent Delphi COM Programming, or read Alessandro
Federicis Introduction to COM in the September
2001 issue of Delphi Informant Magazine.

Sound+Vision
streams. The DirectShow SDK documentation describes the COM
interfaces of each standard filter in detail. The SDK also describes
many interfaces to which third-party drivers should adhere.

the Internet (see end of article for details). Put the headers in a
folder of their own, and add that folder to the library path. There
arent any components to install.

Filters have pins that connect them to other filters, much like the
pins of components in an electronic circuit. In some cases, the
pins actually do represent hardware pins. For example, a sound
card filter typically has pins that represent the physical connections (line-in, microphone, CD audio, and so on). Pins carry data
in one or more formats, such as PCM audio at various bit rates,
or compressed video with various dimensions. When two filters
are connected, they can automatically negotiate which format
will be exchanged. Alternatively, the programmer can choose a
specific format to connect the two filters, as long as both filters
understand that format, of course.

If you are running Windows 2000, you may notice that Delphi keeps
stopping at a breakpoint in the kernel. This breakpoint was accidentally
left in by the Microsoft programmers, and youll need the IDE Expert
called Win2kDebugBreakpointsFix to circumvent this problem.

The DirectShow system provides the programmer with tools to


simplify the process of building a graph. In fact, DirectShow can
sometimes automatically build a graph for you, and let you modify it
by adding, deleting, reconnecting, and configuring filters.

Its also a good idea to install a recent version of Windows Media


Player. DirectShow contains filters to use audio and video codecs, but it
doesnt contain any codecs itself. Instead, it relies on other applications,
such as WMP or your own application, to install the required codecs.

Once a graph has been constructed it can be run, stopped, paused,


interrogated, and controlled in many ways. The application can
call COM interface methods of the graph, and use filters and pins
to modify or query their functionality.

Our Audio/Video Player Application

The DirectShow SDK


The DirectShow SDK, available from the Microsoft site, contains
numerous examples (in C++, VB, and compiled form), documentation, and tools. The documentation is also available online
on the MSDN site, but you will probably need to refer to it
frequently. It makes sense to download the SDK instead of reading it online. (See the Resources section at the end of this article
for this and all other URLs.)
One of the tools in the SDK is called GraphEdit. Its a handy
application that allows you to explore the filters you have at
your disposal. Using GraphEdit you can insert filters, call up the
property pages that a filter exposes, draw connections between
pins by simply dragging arrows and when youre satisfied with
the result actually run, pause, and stop the graph. Though it
doesnt provide access to all functionality of DirectShow, its a very
good way of learning how filters work and interact.
DirectShow can build a filter graph for a particular media file
automatically. GraphEdit exposes this functionality through the
Render Media File option from the File menu. For instance, rendering the Speed Is Delphi video file (speedis.avi) located in the
\Delphi5\Demos\Coolstuff folder will produce the filter graph
shown in Figure 1.
The leftmost filter is called a File Source filter, and provides data
from the file. That data is split by the AVI Splitter filter into a
video and an audio stream. The Default DirectSound Device
filter plays the audio stream using the default sound card. The
AVI Decompressor filter decodes the video stream (using the Intel
Indeo Video codec), and passes it to the Video Renderer filter to
be displayed in a window. Right-click on the filters and pins to
examine their properties.

Preparing Delphi
To use DirectShow in Delphi, youll need the JEDI DirectShow
headers. These translated C++ headers can be downloaded from
13 January 2002 Delphi Informant Magazine

Of course youll also need the DirectShow binaries themselves.


Theyre installed as a part of DirectX, which can be downloaded
from the Microsoft site. I recommend using the latest version
8.0a for maximum stability, but make sure your hardware
drivers are compatible with the version you are installing. Once
installed, DirectX cannot be uninstalled.

The application described here can play video files (AVI, MPEG,
WMV), audio files (WAV, MP3, WMA), and filter graph files
(GRF) created by GraphEdit. It can also use a URL as a source,
such as an MP3 file on a Web server. The end-user needs the
relevant codecs installed, of course.
Because we let DirectShow do most of the work for us, the application
is deceptively simple. It contains only a Menu component for opening
a file or a URL, a Panel to display the video stream, a Panel to contain
some start/stop/pause speedbuttons, and an OpenDialog component
to allow the user to select a file to open.
The event handlers for the Open | File and Open | URL menu
items both call a method named OpenFileOrUrl, which takes the
file name or URL as its only parameter. This is by far the most
important part of the application; its in this method that the filter
graph is built.

Building the Graph


In its most simple form, a filter graph is just a COM object
with CLSID_FilterGraph. There are many ways to construct
a graph. DirectShow provides us with helper interfaces called
Graph Builders to build different kinds of graphs, such as graphs
for capturing audio and video, but in this article well use the
standard IFilterGraph interface, which is published by every filter
graph object.
The filter graph object publishes dozens of interfaces that let us
construct and control various aspects of audio and video playback.
In this application we will be using the interfaces listed in Figure 2.
To construct the graph object, well use Delphis CreateComObject
function:
FilterGraph :=
CreateComObject(CLSID_FilterGraph) as IFilterGraph;

CreateComObject creates the object, which will be accessed using early


binding, and returns an IUnknown interface. We use the as keyword
to query the IUnknown interface for an IFilterGraph interface. Well
use this technique frequently in our DirectShow applications.

Sound+Vision
Interface

Usage

IFilterGraph

Refer to the filter graph itself. Access other


interfaces of the filter graph.
Add filters to the graph based on a media file
or URL. Play, pause, and stop the graph.
Position and size the video display.
Rewind the media file to the beginning.
Receive notification messages.

IMediaControl
IVideoWindow
IMediaSeeking
IMediaEventEx

Figure 2: Interfaces used in the sample application.

if Supports(FilterGraph, IID_IVideoWindow,
VideoWindow) then
with VideoWindow do begin
// Make our video panel the owner.
put_Owner(pnlVideo.Handle);
// Make the video window a child window.
put_WindowStyle(WS_CHILD);
// Make it fit into our video panel.
SetWindowPosition(0, 0, pnlVideo.ClientWidth,
pnlVideo.ClientHeight);
end;

Figure 3: Displaying a supported video in a panel.

Well need the IMediaControl interface to run, stop, and pause the
graph. Remember, the IFilterGraph interface was provided by the
filter graph object, so we can use it to query for the IMediaControl
interface, and any other interface we might need:
MediaControl := FilterGraph as IMediaControl;

The IMediaControl interface lets us render (open and display) any


supported multimedia file or URL using the RenderFile method:
DSCheck(MediaControl.RenderFile(Location));

When the RenderFile method is called, DirectShow will open the


file, examine its contents, and construct a complete graph for us. If
required, it will even include decompression filters for audio and video.
This is similar to using the File | Render Media File option in GraphEdit.
The RenderFile method, like many DirectShow functions, returns a
result code. Because we often need to check the result codes, we can
write a generic procedure called DSCheck to test for errors, get an error
text using the AMGetErrorText function, and raise an exception.

Displaying Video
If the media file were rendering contains a video stream, the
graph will publish an IVideoWindow interface so we can control
the appearance of the video display. We can use Delphis Supports
function to test whether the IVideoWindow interface is supported.
The code snippet in Figure 3 checks whether the interface is available. If so, it makes the video window a child of the panel on our
form, and sets the coordinates and dimensions so that the video
fits into the panel. When the panel is resized, its OnResize event
calls SetWindowPosition again to adjust the video dimensions.

Starting, Stopping, and Seeking


The IMediaControl interface has methods to run, pause, and
stop the graph. Stopping the graph doesnt actually rewind to the
beginning, so we need to take care of that ourselves. This can be
done with the IMediaSeeking interface, which contains methods
for retrieving setting the current position in the stream. Well use
the SetPositions method to reset the current position to zero:
with FilterGraph as IMediaSeeking do begin
GetStopPosition(StopPosition);
CurrentPosition := 0;
SetPositions(CurrentPosition,
AM_SEEKING_AbsolutePositioning, StopPosition,
AM_SEEKING_AbsolutePositioning);
end;

We could easily use the IMediaSeeking interface together with a


scrollbar to show the current position within the media file, or
even to allow the user to jump to an arbitrary position. Not all
14 January 2002 Delphi Informant Magazine

streams support seeking in this way, so wed need test for this
using the CheckCapabilities method. For the sake of brevity, Ill
leave it up to you to add that feature to the application.
After running, stopping, or pausing the graph we call UpdateButtons to enable or disable our buttons depending on the graph state.
We can find out whether the graph is running, paused, or stopped
with the GetState method on the IMediaControl interface:
var GraphState: TFilter_State;
...
MediaControl.GetState(500, GraphState);
sbPlay.Enabled := GraphState in [State_Stopped,
State_Paused];
sbPause.Enabled := GraphState = State_Running;
sbStop.Enabled := GraphState in [State_Running,
State_Paused];

The first parameter of GetState is a timeout value. If the graph


is in transition between two states when GetState is called, the
method will wait up to the specified number of milliseconds
before returning.

Handling Events
When we reach the end of the file, we need to update the buttons
and rewind the file to the beginning. We need to be notified when
the graph has finished playing our media file. To this end, the filter
graph has an IMediaEventEx interface with which we can register
for notification messages using the SetNotifyWindow method:
const WM_GRAPHNOTIFY = WM_USER + 1;
...
with FilterGraph as IMediaEventEx do
SetNotifyWindow(Handle, WM_GRAPHNOTIFY, 0);

After calling SetNotifyWindow, our window will receive a


WM_GRAPHNOTIFY message whenever the filter graph has one
or more events for us. Using a standard message handler we can
receive such messages:
procedure HandleGraphNotifyMessage(var Msg: TMessage);
message WM_GRAPHNOTIFY;

If you need to brush up on defining custom windows messages,


look up Message Handling in Delphis online help.
In our message handler, we repeatedly call the GetEvent function
of the IMediaEventEx interface to retrieve and handle the actual
DirectShow events. When the graph finishes playing the file,
GetEvent will return the EC_COMPLETE event code, and we
stop the graph. If an error occurs, we update our buttons based on

Sound+Vision
if Supports(FilterGraph, IMediaEventEx, MediaEventEx) then
with MediaEventEx do begin
repeat
// Get the event.
hr := GetEvent(EventCode, Param1, Param2, 0);
if Succeeded(hr) then
begin
// Check the event code.
if EventCode = EC_COMPLETE then
// End of file; click on Stop.
sbStop.Click
else
if EventCode in [EC_PAUSED, EC_ERRORABORT] then
UpdateButtons;
// Free the event parameters.
FreeEventParams(EventCode, Param1, Param2);
end; // if succeeded.
until not Succeeded(hr);
end; // with MediaEventEx.

Figure 4: Handling DirectShow events.

Conclusion
This article has introduced DirectShow and has demonstrated a very
simple DirectShow application (see Figure 5). With a minimum
amount of code we can play back many different media sources.
In Part II well tackle a more advanced project using DirectShow.
Well learn how to build a filter graph ourselves, enumerate
devices and filters, display filter property pages, and capture still
images from a Webcam. See you then.

Resources
Download the Win2kDebugBreakpointsFix IDE expert

mentioned in the Preparing Delphi article from Borlands


CodeCentral site at http://codecentral.borland.com. Click
on Find and search for article ID 15804. You may need to
remove the HexUtils unit from the uses clause and modify the
.ini file to get it to compile and work.
The DirectShow binaries are available from the Microsoft
Web site at http://www.microsoft.com/directx/homeuser/
downloads/default.asp.
The DirectShow SDK is available from the Microsoft Developer Network site at http://msdn.microsoft.com/directx. Click
on Downloads in the table of contents on the left, then on
DirectX Partial SDK download.
You can find the excellent JEDI DirectX headers at
http://www.delphi-jedi.org/DelphiGraphics.
Eric Harmon has written a very good book about using COM
in Delphi. Its called Delphi COM Programming. Search
http://www.newriders.com for Delphi for more info.

The project referenced in this article is available on the Delphi Informant Magazine Complete Works CD located in INFORM\2002\JAN\
DI200201JW.

Figure 5: The finished application.

the graph state (see Figure 4). Dont confuse DirectShow events
with Delphi events; they have nothing in common.
In this example, we werent interested in the event parameters,
but DirectShow allocates them for us anyway. The call to
FreeEventParams is very important. It frees resources allocated
when GetEvent was called. Refer to the DirectShow SDK documentation for a list of event codes.

15 January 2002 Delphi Informant Magazine

Jon is an Englishman living in Holland. He works as a senior software


specialist for MarketResponse, a leading Dutch market research agency. At
age twelve he was writing games for Atari 600 XL and Commodore 64, building SQL database systems on Unix at fourteen, and developing FidoNet BBS
systems at seventeen. Hes been using Borland products since Turbo Pascal 3.
He no longer needs a TV, VCR, or CD player, but does it all with Delphi and a
PC full of gadgets. Jon can be reached at jon@jonwebb.net.

On the Net
WebSnap / HTML / InterBase / Delphi 6 Enterprise

By Corbin Dunn

A WebSnap Online Survey


Part I: Creating the Application and Data Module

his is the first part of a two-part article series that demonstrates how to create an
online survey using the new WebSnap capabilities of Delphi 6 Enterprise edition.
This months article introduces WebSnap by comparing it to the Web Broker technology available in previous versions of Delphi, explains how to build the sample InterBase database, and begins to construct the survey application itself.
Traditional Delphi Web Broker applications commonly involve embedding HTML directly into the
Pascal source code. A developer also will use PageProducer tags and transparent tags to generate the
desired result in response to an action. This works
well, but has some significant drawbacks.
Any changes to the Web page (such as a new phone
number) might require a complete recompilation
of the project to apply the update. If the project is
an ISAPI DLL, the process requires that the DLL
be unloaded from memory in order to replace it.
Depending on the version of Internet Information
Server (IIS), this might require completely rebooting the computer.
Another drawback is that with Web Broker, the
Delphi developer and Web developer are often the
same person. Therefore, theres no easy way to have
a Delphi developer create the applications business
logic while a Web developer creates the Web-based
user interface.
WebSnap changes the way Web development is
done in Delphi. First of all, it separates the Web
interface from the application by keeping a separate
HTML file associated with each Web page. Unlike
Web Broker, WebSnap allows you to have multiple
Web modules, with each one typically representing
an actual page in the final Web application. WebSnap also gives you the power of server-side scripting to access Delphi components, thus eliminating
any need to have HTML embedded in the Delphi
application. That means youre free to pass the
HTML design off to a Web developer who doesnt
know anything about Delphi.
16 January 2002 Delphi Informant Magazine

Introduction to WebSnap
The basic concept of WebSnap is to use adapter
components as a scriptable interface into your
application. You can use server-side scripting
embedded in HTML to access the adapter components, and build complex HTML results from any
kind of data. An adapter exposes data through
adapter fields, and allows updates through HTML
forms and the use of adapter actions.
You should use TAdapter if you want to write all
the logic to access and update data from your
application. TPagedAdapter is the same as a TAdapter,
but goes further by allowing your data to be spread
across multiple Web pages similar to how search
engines have multiple page buttons at the bottom
of the results. TDataSetAdapter is incredibly powerful
and can give you scriptable access to a TDataSet
instantly. It even includes a set of default database
actions, such as DeleteRow, Insert, NextRow,
FirstRow, etc. Finally, the TLoginFormAdapter is
a specialized adapter containing default fields for
the login name, password, and next page. It
interacts with some of the other standard WebSnap
components (WebUserList, EndUserSessionAdapter,
and SessionService) to provide instant login control
to a Web site. Once you have an adapter, you can
write custom server-side script to interface with it, or
you can use an AdapterPageProducer component to
create script for you automatically.
As previously mentioned, WebSnap introduces
the concept of multiple Web modules in a single
project. In previous versions of Delphi, you had to
put all your logic inside a single Web module. You
typically would use actions to return a dynamic

On the Net
/* Table: SURVEY, Owner: SYSDBA */
CREATE TABLE "SURVEY" (
"SURVEY_ID"
INTEGER NOT NULL,
"DESCRIPTION" VARCHAR(255) NOT NULL,
CONSTRAINT "SURVEYS_PK" PRIMARY KEY ("SURVEY_ID")
);
/* Table: SURVEY_DATA, Owner: SYSDBA */
CREATE TABLE "SURVEY_DATA" (
"SURVEY_DATA_ID" INTEGER NOT NULL,
"SURVEY_ID"
INTEGER NOT NULL,
"DESCRIPTION"
VARCHAR(255) NOT NULL,
"VOTES"
INTEGER DEFAULT 0 NOT NULL,
CONSTRAINT "SURVEY_DATA_PK"
PRIMARY KEY ("SURVEY_DATA_ID")
);
CREATE GENERATOR "GEN_SURVEY_ID";
CREATE GENERATOR "GEN_SURVEY_DATA_ID";
SET TERM ^ ;

Figure 1: Main page of the example survey application.

Web page to the end user. With WebSnap, each page in the Web
application typically will be in a separate Web module, and will
perform some specific logic.
The best way to start learning WebSnap is through a sample application. Its common to see Web sites with quick information polls. Creating a simple voting site, as seen in Figure 1, is easy with WebSnap.

Create the Database


The first task is to create a database to store the surveys and their
results. InterBase is an excellent database server, so I created the
tables, generators, triggers, and procedures with the SQL script
shown in Figure 2.
The SURVEY table will be used to uniquely identify a survey. Each
option in a survey will be represented by a SURVEY_DATA row, consisting of a unique key, description of that option, the number of votes for
that option, and a foreign key named SURVEY_ID as a reference to the
master table SURVEY. The triggers use generators to create new IDs for
each table automatically. You should be able to use IBConsole to create a
new database by executing the above SQL statements.

Creating the WebSnap Application and Web Data Module


Run Delphi and select File | New | Other. From the WebSnap tab,
choose WebSnap Application. For ease of debugging, make it a Web
App Debugger executable, and give it a CoClass Name of WebVote.
Change the Page Name to MainPage, and click OK.
Select File | Save All, and save the units and project using the following names: MainPg.pas, MainFrm.pas, and WebVote.dpr. Notice
that a file named MainPg.html is also created and associated with
MainPg.pas. By default, all Web modules will have an associated
HTML page in a separate file.
Now we want to create a Web data module to hold DataSet components and adapters that the application uses. Web data modules are
typically used to hold components that multiple Web page modules will share. Select File | New | Other, and choose Web Snap Data
17 January 2002 Delphi Informant Magazine

CREATE PROCEDURE "ADD_VOTE" ("SURVEY_DATA_ID" INTEGER)


AS
DECLARE VARIABLE CURRENT_VOTE INTEGER;
BEGIN
SELECT VOTES
FROM SURVEY_DATA
WHERE SURVEY_DATA_ID = :SURVEY_DATA_ID
INTO CURRENT_VOTE;
CURRENT_VOTE = CURRENT_VOTE + 1;
UPDATE SURVEY_DATA SET VOTES = :CURRENT_VOTE
WHERE SURVEY_DATA_ID = :SURVEY_DATA_ID;
END ^
CREATE TRIGGER "SURV_BEFORE_IN" FOR "SURVEY"
ACTIVE BEFORE INSERT POSITION 0 AS
BEGIN
NEW.SURVEY_ID = GEN_ID(GEN_SURVEY_ID, 1);
END ^
CREATE TRIGGER "SURV_DATA_BEFORE_IN" FOR "SURVEY_DATA"
ACTIVE BEFORE INSERT POSITION 0 AS
BEGIN
NEW.SURVEY_DATA_ID = GEN_ID(GEN_SURVEY_DATA_ID, 1);
END ^
SET TERM ; ^

Figure 2: CREATE statements to construct the example InterBase


database.

from the WebSnap tab. In the Module Options dialog box,


click OK to accept the default properties. Select File | Save to save the
Web data module as WebDataMod.pas. Select the newly created Web
data module, and set the Name to WebData in the Object Inspector.
Module

From the InterBase tab on the Component palette, drop an IBDatabase component and make the following changes with the Object
Inspector:
Set the Name to MainDatabase.
Set the DatabaseName to be the newly created InterBase database.
Prepend the DatabaseName with localhost to ensure the connection is done remotely. If you dont do this, your application
may not work as an ISAPI DLL.
Add the following parameters to allow an automatic login:
user_name=sysdba and password=masterkey.
Set LoginPrompt to False.
Next, add an IBTransaction component to the Web data module, and
make the following changes:

On the Net
Double-click on the tblSurvey component to bring up the Fields
Editor. Right-click on the Fields Editor and select Add all fields.
Select the SURVEY_ID field and, in the Object Inspector, expend
the ProviderFlags property. Set pfInKey to True. This provider flag
is what WebSnap uses to determine which field in a TDataSet is
the primary key, in order to do updates and locates on a particular
row. In the Events for tblSurvey, add the following code to the
AfterPost event to allow a refresh of the data after posting:
procedure TWebData.tblSurveyAfterPost(DataSet: TDataSet);
begin
DataSet.Close;
DataSet.Open;
end;

From the Data Access tab, add a DataSource, and make the following changes:
Set the Name to be dtsrcSurvey.
Set the DataSet to be tblSurvey.
Figure 3: Linking SURVEY_IDs using the Field Link Designer.

procedure TWebData.AddVoteActionExecute(Sender: TObject;


Params: TStrings);
begin
try
// SurveyDataId will contain the survey option we are
// voting for. SurveyDataId.ActionValue could be nil
// if there were no HTML input element associated with
// it from the form.
if SurveyDataId.ActionValue = nil then
raise Exception.Create('Internal error: no ' +
'SurveyDataId.ActionValue passed to application');
if SurveyId.ActionValue = nil then
raise Exception.Create('Internal error: no ' +
'SurveyId.ActionValue passed to the application');
// ActionValue's can have multiple Values sent from
// the Web page. An example of where this would happen
// is from a multi-select list box. You can use
// ActionValue.ValueCount to iterate through all the
// values. We only access the first value, since the
// underlying HTML is not a multi-select list box.
strdprcAddVote.ParamByName('SURVEY_DATA_ID').
AsInteger := SurveyDataId.ActionValue.Values[0];
strdprcAddVote.ExecProc;
// Redirect to the results page, upon success.
Response.SendRedirect(Request.InternalScriptName +
'/' + ResultsPage.Name + '?SURVEY_ID=' +
SurveyId.ActionValue.Values[0]);
except
// Capture all exceptions and add them to the
// adapter to be displayed in the HTML page.
on E: Exception do
AddVoteAdapter.Errors.AddError(E);
end;
end;

Figure 4: AddVoteAction OnExecute code.

Set the Name to MainTransaction.


Set the MainDatabase components DefaultTransaction to be
MainTransaction.
Double-click on the MainTransaction component, and select
Snapshot for the Transaction Properties.

Drop down an IBTable component and make the following changes:


Set the Name to be tblSurvey.
Set the Database to be MainDatabase.
Set the TableName to be SURVEY.
18 January 2002 Delphi Informant Magazine

From the InterBase tab, add another IBTable component, and make
the following changes:
Set the Name to tblSurveyData.
Set the Database to MainDatabase.
Set the TableName to SURVEY_DATA.
Set the MasterSource to dtsrcSurvey.
Double-click on the MasterFields property in the Object Inspector
and link the SURVEY_IDs as shown in Figure 3. This sets up the
master-detail relationship.
In the Events, set the AfterPost event to tblSurveyAfterPost.
Add all the fields for the tblSurveyData component, and set the
pfInKey ProviderFlag for the SURVEY_DATA_ID field, similar to how
it was done for tblSurvey.
At this point, weve set up a traditional master-detail relationship
using the IBExpress components. Now we need to use adapters to
interface with the data from server-side script. From the WebSnap tab,
drop a DataSetAdapter component and make the following changes:
Set the Name to SurveyAdapter.
Set the DataSet to be tblSurvey.
Add another DataSetAdapter with the following changes:
Set the Name to SurveyDataAdapter.
Set the DataSet to tblSurveyData.
Set the MasterAdapter to SurveyAdapter.
The MasterAdapter property allows WebSnap to realize a masterdetail relationship is taking place. The property also hides proper
state information into the Web page.
The DataSetAdapter components add a lot of functionality thats
available from server-side script automatically. You can access all
the fields in the database, and you can access standard DataSet
actions such as Apply, Delete, and Edit. Ill cover this next month
in Part II, when we start writing some script.
The stored procedure, ADD_VOTE, was created earlier in order to
add a vote for a given option. We want to expose this with a scriptable
interface. To do that, add an IBStoredProc component from the InterBase tab to the Web data module and make the following changes:
Set the Name to strdprcAddVote.
Set the Database to MainDatabase.
Set the StoredProcName to ADD_VOTE.

On the Net
To expose the stored procedure to server-side scripting, well use
a traditional adapter. Add an adapter from the WebSnap tab to
the Web data module and change its name to AddVoteAdapter.
Select AddVoteAdapter and double-click on the Data value field to
bring up the Fields Editor. Whenever a user votes, we need to pass
information back to the Web application. With WebSnap, this
information is passed via an adapters AdapterField. Right-click on
one of the Fields Editor panes and select New Component. Doubleclick on AdapterField. In the Fields Editor, select AdapterField
and, with the Object Inspector, change the name to SurveyDataId.
SurveyDataId will be the survey option for which someone is
voting. Add another AdapterField and name it SurveyId. SurveyId
will be the survey for which the user is adding a vote, and it will
be used to display the current results after the user votes.
We now need a way to execute some code whenever the user
wants to vote. Select AddVoteAdapter on the Web data module, and
double-click on Actions in the Object Inspector. Right-click in one
of the action-editor panes and select Add New Component. Doubleclick the default AdapterAction in the dialog box that appears. In
the Object Inspector, change the name of the AdapterAction1 to
AddVoteAction. Double-click on the OnExecute event and add the
code seen in Figure 4.
All AdapterFields have an ActionValue property that represents the
data sent from a Web page. The ActionValue property will be nil if
nothing is sent from the page, so its important to always check it
for nil before using it.
We will add a little more code to the Web data module. Select
WebData and double-click on the OnDeactivate event. Add the
following code to commit the InterBase transaction:
procedure TWebData.WebDataModuleDeactivate(
Sender: TObject);
begin
if MainTransaction.InTransaction then
MainTransaction.Commit;
end;

Whenever you need to initialize a Web module (or Web data


module), do it in the OnActivate event. The corresponding
OnDeactivate event fires whenever a request is finished and is
about to be sent to the browser.

Conclusion
Thats all we have room for in this article. In Part II of this article
series, well see how to add script to the main page, and get into the
implementation details of creating the survey pages. See you then.
The project and database referenced in this article are available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2002\JAN\DI200201CD.

Corbin Dunn has worked for Borland Software Corporation for three years. He
started in Developer Support and is now a quality-assurance engineer for the
rapid-application-development product group, where hes currently working on
WebSnap. Readers may reach him at cdunn@borland.com.

19 January 2002 Delphi Informant Magazine

The API Calls


Windows API / Error Handling / Delphi 2-6

By Marcel van Brakel

Windows API Error Handling


Part I: Detecting and Understanding Windows Errors

n an ideal world, your programs would work flawlessly every time. Unfortunately
however, no matter how hard you try, any piece of software you develop will never
be error-free. You can minimize the number of errors your code causes, but you
cannot always ensure code you use performs its task without errors. Even if you could,
there will always be a class of errors that cannot be prevented, only detected. One of
the most prevalent examples is in the area of network programming, where all kinds
of errors can occur seemingly at random.
This two-part article series is all about detecting
and handling errors generated in the Windows
API. Although COM is sometimes considered

part of the Windows API, I blissfully ignore


COM error handling in this series; the topic is
itself worthy of a series.

Retrieving Error Information


C

Pascal

Description

VOID

N/A

BOOL
DWORD

Boolean
Longword

By definition, a void function doesnt return a value


(its a procedure), and will never fail. These are rare
in the Windows API, e.g. GlobalMemoryStatus and
ExitProcess.
Returns True for success, False if it fails.
Most functions that return a DWORD value return
0 or -1 to signal failure, and any other value to
indicate success. A few return an error code as the
function result, and instead return 0 when the
function succeeds. An example of the latter is
LoadModule.
The function returns a handle to an object. Most
functions that return a handle return 0 when the
function fails and a non-zero value if it succeeds.
However, some functions return the constant
INVALID_HANDLE_VALUE (defined as $FFFFFFFF) to
indicate failure. Be sure to read the documentation
carefully to determine which is used, or you might
run into some bugs that are difficult to find. One of
the functions that exhibits this behavior is
CreateFile.
The function returns a pointer. In case of success,
the pointer will be non-nil. In case of failure, its nil.

HANDLE

LPVOID

THandle

Pointer

Figure 1: Common API function return types.


20 January 2002 Delphi Informant Magazine

There are two common ways of signaling errors:


return codes and exceptions. In the last few
years, the use of exceptions has become more
widespread and is often the preferred method
of signaling an error. Unfortunately, the Windows API doesnt use exceptions. Instead, it uses
return codes in combination with something
called the last-error code.
Whenever you call a Windows API function
that, for some reason, cannot handle the
requested task, the function indicates the failure
by its result. There are a variety of return types;
the most common are shown in Figure 1.
Whenever a Windows API function fails, it
indicates its condition by using one of the return
types from Figure 1. Most functions also provide
you with the reason of failure, by setting whats
known as the last-error code. The last-error code
is a 32-bit value associated with a thread; each
thread has its own last-error code. Because of
this arrangement, you can call Windows API
functions from different threads safely, without
having to worry about one thread overwriting

The API Calls


Bits

Contents

Description

31-30

Severity

29

Customer Flag

28
27-16

Reserved
Facility

Severity indicates the kind of error.


Possible values are: * 00 success,
* 01 Informational, * 10 Warning,
* 11 Error.
This bit is clear for all Windowsdefined error codes. Custom error
codes defined by an application
should set this bit.
Reserved.
Facility indicates which part of
the system generated the error
code. Examples are FACILITY_RPC
for the RPC subsystem, and
FACILITY_SECURITY for the
security subsystem. A complete
list of all possible values is
in WinError.pas.
Status Code is the code that
actually defines the specific error,
relative to its severity and facility.

15-0

Status Code

Figure 2: Structure of error codes.

the last-error code of another. The actual numerical value of the


last-error code is usually of no interest, and youll probably never
bother with it. Instead, you compare the code with one of the
many predefined error codes from WinError.pas. Still, this series
wouldnt be complete if I didnt show you how the error codes are
structured (see Figure 2). Furthermore, youll need to know this
taxonomy of error codes when you start defining your own error
codes.
The WinError.pas file to which I refer is available for download
(see end of article for details). The file is a translation of the
WinError.h header file from Microsoft, which defines all the base
error codes used by the Windows API. Most of these error codes
are also in Windows.pas, but not all of them. Note that this
file does not define the complete set of error codes used by the
Windows API. These are scattered throughout multiple include
files, usually with names ending in error or err.
I should warn you, however, that Windows doesnt strictly follow
this structure for its base error codes, although it does for most subsystems. But that shouldnt prevent you from following it when you
define your own error codes. (See Setting the Last-error Code in
Part II.) The error-code constants are usually prefixed with a string
indicating the subsystem that defines the error code. For example,
the base error codes are prefixed with ERROR_, error codes from
the Windows Sockets API are prefixed with WSAE, and error codes
from COM are prefixed with E, CO_E, or OLE_E. Figure 3 shows
a few example codes from WinError.pas.
You can see a complete listing of error codes in the Win32 API
| Reference section of the Platform SDK documentation, or by
looking at the WinError.pas unit. Once a Windows API function
fails, you can retrieve the last-error code by calling the Windows
API function, GetLastError:
function GetLastError: DWORD; stdcall;

You should call this function immediately after the failed API call.
If you call any other Windows API function after a failed call, you
run the risk that the error code will be overwritten by one of those
21 January 2002 Delphi Informant Magazine

// MessageId: ERROR_INVALID_FUNCTION
// MessageText: Incorrect function.
ERROR_INVALID_FUNCTION = 1; // dderror
{$EXTERNALSYM ERROR_INVALID_FUNCTION}
// MessageId: ERROR_FILE_NOT_FOUND
// MessageText:
// The system cannot find the file specified.
ERROR_FILE_NOT_FOUND = 2;
{$EXTERNALSYM ERROR_FILE_NOT_FOUND}
// MessageId: ERROR_PATH_NOT_FOUND
// MessageText:
// The system cannot find the path specified.
ERROR_PATH_NOT_FOUND = 3;
{$EXTERNALSYM ERROR_PATH_NOT_FOUND}

Figure 3: A few error code constant definitions.

calls. Its also important to note that a successful Windows API


call usually does not reset the last-error code to 0 (although some
do), so you should not rely on the last-error code having a valid
value after a successful API call.
Figure 4 demonstrates how you would call GetLastError. This code
uses the FindFirstFile, FindNextFile, and FindClose Windows API
functions to enumerate the contents of the D drive. The details
arent relevant here, so suffice it to say that the FindNextFile
function returns False upon failure. When that happens, one of
two things has occurred. Either there were no more files in the
directory, or an error has occurred. To find out which, we call
GetLastError. If it returns ERROR_NO_MORE_FILES, there are
no more files left to enumerate. If it returns any other code, that
usually means a real error has transpired.
I have to mention one oddity about the use of the last-error code
in the Windows API. Aside from using the last error to indicate
the cause of failure, some Windows API functions actually use
it to indicate the reason of success. You might find this rather
strange, and it takes a little getting used to, considering its called
the last-error code. However, there is logic behind this absurdity,
and that is that its actually an effective way of avoiding some
signaling mechanisms that are awkward to code.
One of the more common functions that exhibits this behavior is CreateFile. When you call this function, you can specify
CREATE_ALWAYS as one of its parameters. That indicates
that the function should open the file if it exists, or create it if it
doesnt. When it succeeds, the function returns a handle to the
opened or created file. To determine whether the function created
the file or opened an existing one, you can call GetLastError.
GetLastError will return ERROR_ALREADY_EXISTS if the file
already existed, and 0 if it had to be created. You will actually
find similar behavior with most of the Windows API functions
that deal with creating (optionally) named kernel objects, such as
events, mutexes, and file-mapping objects.

Error Messages
In addition to the raw, numerical error codes, Windows also
provides error messages in text form for each of the standard
Windows error codes. These textual messages are stored as
resources, MESSAGETABLE resources to be precise, in system
DLLs. These MESSAGETABLE resources are a general-purpose
resource type, similar to the STRINGTABLE resource type. They
store a list of strings, each of which is associated with a numerical

The API Calls


value, the message ID, that uniquely identifies the string.
Additionally, where appropriate, a single message-table resource
allows you to store the message texts in multiple languages,
thereby allowing on-the-fly localization.
One of the more powerful features of MESSAGETABLE resources
that distinguish these from STRINGTABLE resources is that they
allow you to embed escape sequences into the message text, which
can be replaced at run time to customize the messages. You can
retrieve the error message associated with an error code by using
the FormatMessage function:
function FormatMessage(dwFlags: DWORD; lpSource: Pointer;
dwMessageId: DWORD; dwLanguageId: DWORD; lpBuffer: PChar;
nSize: DWORD; Arguments: Pointer): DWORD; stdcall;

FormatMessage has two distinct purposes that can be applied


at the same time. First, the function allows you to retrieve
message strings from any message DLL, given a handle to the DLL
and a message ID (and, optionally, a language). Second,
it allows you to apply some formatting to a message string,
such as replacing escape sequences and altering line breaks. How
the function behaves is specified using the dwFlags parameter. The
combination of flags you specify largely determines the purpose of
the remaining parameters. Fortunately, Microsoft made it relatively
easy to use the function to retrieve the message text for error codes
through the special FORMAT_MESSAGE_FROM_SYSTEM flag.
In this series, Ill demonstrate how to use the FormatMessage
function for this purpose only. A more in-depth discussion of this
function, and MESSAGETABLE resources in general, will be in
a future article. One word of caution: The message text retrieved
using FormatMessage often is terminated with a carriage-return/
linefeed pair. In the examples that follow, I leave these in place.
However, you may want to remove them in your own applications. The ErrorLookup project accompanying this series shows
one possible way of doing that (available for download; see end of
article for details).

Example 1: Retrieving Messages for Win32 Error Codes


The first example (shown in Figure 5) demonstrates the most
common way of using FormatMessage. First, we force an error to
occur by attempting to open a nonexistent file. This will cause
the CreateFile call to fail, and GetLastError to return
ERROR_FILE_NOT_FOUND. On failure, we call
FormatMessage to retrieve the message text associated with this
error code: The system cannot find the file specified.
The most interesting pieces of this code are the flags
we supply to the dwFlags parameter. First, The
FORMAT_MESSAGE_FROM_SYSTEM flag tells the function
to search the system DLLs for the message text identified by the
dwMessageID parameter. Specifying this flag causes FormatMessage
to ignore the lpSource parameter (the function determines which
DLLs to search in this case), so we simply pass nil. The message
ID for message texts associated with an error code is identical to
the value of the error code, which is why we can pass the result of
GetLastError directly as the value of the dwMessageID parameter.
Second, the FORMAT_MESSAGE_ALLOCATE_BUFFER flag
tells FormatMessage to allocate the memory required for storing the
message itself, and to return a pointer to this buffer as the output
of the lpBuffer parameter. We must free this buffer when were done
with the message text, using the LocalFree Windows API function.
22 January 2002 Delphi Informant Magazine

procedure TForm1.Button1Click(Sender: TObject);


var
Data: TWin32FindData;
SearchHandle: THandle;
begin
FillChar(Data, SizeOf(Data), 0);
SearchHandle := FindFirstFile(PChar('d:\*.*'), Data);
if SearchHandle <> INVALID_HANDLE_VALUE then
begin
repeat
Memo1.Lines.Add(Data.cFileName);
until not FindNextFile(SearchHandle, Data);
if GetLastError <> ERROR_NO_MORE_FILES then
ShowMessage('Unexpected error');
Windows.FindClose(SearchHandle);
end
else
ShowMessage(
'FindFirstFile failed, GetLastError returned' +
IntToStr(GetLastError));
end;

Figure 4: Using GetLastError.

procedure TForm1.Button2Click(Sender: TObject);


const
Flags = FORMAT_MESSAGE_FROM_SYSTEM or
FORMAT_MESSAGE_ALLOCATE_BUFFER;
var
F: THandle;
Msg: PChar;
begin
F := CreateFile('12345.657', 0, 0, nil,
OPEN_EXISTING, 0, 0);
if F = INVALID_HANDLE_VALUE then
begin
if FormatMessage(Flags, nil, GetLastError,
0, @Msg, 0, nil) > 0 then
begin
ShowMessage(Msg);
LocalFree(Cardinal(Msg));
end
else
ShowMessage('FormatMessage failed');
end;
...
end;

Figure 5: Retrieving messages for Win32 error codes.

Note that the Delphi Runtime Library (RTL) supplies the


SysErrorMessage function that essentially does the same as the code in
Figure 5, except it doesnt rely on the FormatMessage function to allocate the buffer, but instead supplies its own. You may want to have a
look at it for an example of how to do that. Furthermore, its recommended you use SysErrorMessage as opposed to calling FormatMessage
directly. This is especially important for cross-platform development
because the retrieval of error messages is implemented differently
on other platforms. The good news is that by using SysErrorMessage,
those implementation details are hidden from you and handled automatically. SysErrorMessage is very easy to use. It takes only a single
parameter, an error code, and returns the message text as the function
result. For more details, look it up in Delphi help.

Conclusion
Error detection and handling play an extremely important role
when interfacing with the Windows API. This month, Ive shown
you how to detect error conditions after calling a Windows API
function, and how to retrieve a message text describing the error.
In Part II, Ill conclude the series with a look at retrieving mes-

The API Calls


sages from a specific message DLL, setting the last-error code,
error modes, and Delphis approach to error handling.
Accompanying this series are two projects: ErrorDemo and
ErrorLookup. The first one contains all the code shown in this
series; the second contains an IDE wizard that allows you to look
up the message text associated with any Windows API error code.
See the included readme.txt for installation details.
The projects and files referenced in this article are available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2002\JAN\DI200201MB.

Marcel van Brakel is an independent contractor specializing in Windows systemlevel development using Delphi. Hes a contributing member of Project JEDI
(http://www.delphi-jedi.org) and is the founder of the JEDI Code Library. You can
write to him at brakelm@chello.nl, or visit members.chello.nl/m.vanbrakel2.

23 January 2002 Delphi Informant Magazine

New & Used


By Bill Todd

Help & Manual


Stand-alone Tool Makes Creating Help Manuals Easy

f you need a help-authoring tool that does it all, Help & Manual is the tool for you. Its
not only easy to use, it also creates 16- and 32-bit Windows help, HTML help, an HTML
file, an RTF file, and a printed manual from a single source.
Figure 1 shows Help & Manuals main form with
a help project open. The main form is divided into
three panels. The upper, left panel shows the table
of contents for your help file. Below the table of
contents is the pop-up topics panel. Pop-up topics
in Help & Manual are any topics you dont want
included in the table of contents. To the right of the
contents and pop-up topics panels is a tabbed notebook with tabs for Help text and Topic options.
The Help text tab contains the editor where you
write your help topics.
When you create a new project in Help & Manual,
the wizard shown in Figure 2 appears. The radio
buttons let you choose to create a new help project,
import an existing Windows help project, decompile and import an existing Windows help file, or
import an RTF file created in Microsoft Word.

If you create a new help project, the Help &


Manuals main form will appear as shown in Figure
3. The Help & Manual project is stored in a single
file, and Help & Manual even offers an option to
compress this file if you are short of disk space.

Help Topics
If you dont like the default topics that appear in
the table of contents, just delete the ones you dont
want, or rename and rearrange them to suit your
needs. You can change the name of any chapter
or topic in the table of contents by right-clicking
and choosing Edit from the context menu, or by
double-clicking on the topic or chapter name. To
reorganize the chapters and topics, just drag and
drop. To add a chapter or topic, right-click on an
existing chapter or topic and choose Add Before or
Add After from the pop-up menu.
I like to outline my help files before I start writing
the topic text, and the Help & Manual table-ofcontents panel is an excellent tool for building an
outline of your help file. Once you have the topics
and chapters organized the way you want them, its
a simple matter to move through the topics and add
the text. Another advantage of this approach is that
having all the topics listed in the table of contents
before you start writing makes it a snap to create
links as you go. You also can right-click topics on
the table of contents and choose Mark Item from the
pop-up menu. This causes the topic to appear in bold
in the table of contents, and provides a convenient
way to mark topics to which you must return later.

Figure 1: The main form in Help & Manual.


24 January 2002 Delphi Informant Magazine

Writing your help topics is very easy thanks to Help


& Manuals WYSIWYG editor. When you add a new
topic, Help & Manual prompts you for a caption, and

New & Used


non-scrolling region and edit the text at any time. To create a link to
another topic, just highlight the text you want to use as the link and
then drag and drop it on the topic to which you want it to link in
the table of contents. You also can create a link by highlighting text
and clicking the link button on the toolbar to display a dialog box
that lets you choose the topic to which you want to link. Although
this works fine, using drag and drop is so much faster and easier that
I cannot imagine creating a link any other way. Help & Manual also
lets you test your links at design time. Just double-click on any link
to go to the topic to which it points.

Figure 2: The New Help & Manual project wizard.

Figure 3: The main form with a new project loaded.

Figure 4: The Topic options tab.

the caption you enter appears in both the table of contents and the
non-scrolling region at the top of the help topic. This is much easier
than most help-authoring tools, which require you to define a nonscrolling region if you want one. Help & Manual correctly assumes
that the vast majority of help topics will have a non-scrolling region
at the top and puts it in for you automatically. You can click in the
25 January 2002 Delphi Informant Magazine

Adding graphics to a topic is easy as well. Just click the graphic


button in the toolbar to display the open file dialog box, and
choose the bitmap file you want to add to your topic. If you
need to add a screen shot, Help & Manual includes an integrated
screen-capture utility. You can use it to capture any part of the
screen and add the image directly to the current help topic at the
insertion point. One of the things I particularly like about Help
& Manual is that graphics appear in your help topics at design
time exactly the way they will appear in the final help file. This
is true regardless of whether you embed the graphics in the help
project, or link separate image files to the topic.
The Topic options tab (see Figure 4) lets you set the options and
keywords for your topic. The Topic ID field will be filled already
because Help & Manual automatically generates a topic ID from
the caption you assigned to your topic. You can change the topic
ID if you wish, and Help & Manual will ask if you want it to
update all references to the topic automatically. You should choose
this option always, so links to the topic will be maintained. The
only exception is if you change the topic ID of the topic you
know has no links. The Keywords memo control lets you enter the
keywords you want to appear in the index of your help file. Enter
one keyword per line. If you want to create
second-level keywords, enter the main keyword first and then, on a new line, enter the
main keyword again followed by a comma
and the second-level keyword. The A-Keywords memo control lets you enter keywords
you want to use for searching but do not
want to appear in the index.
Below the A-Keywords memo control are
six check boxes used to control the type of
output in which this topic will be included.
By default, Help, Manual, and HTML are
checked to indicate the topic should appear
in Windows help, printed manuals, HTML
files, and HTML help files. The Demo,
User1, and User2 check boxes provide three
more categories you can use in any way you
choose. When you generate any kind of
output, you will have the option to include
any combination of the six categories. This
lets you build different help files with different content from the same source.
The last item you can change on the Topic options tab is the help
context number. Help & Manual generates this number automatically, so there is no reason for you to enter it manually. You can set
the starting number and increment value as part of your project
options. This lets you choose a range of numbers that will not conflict with another help file that also is part of your project.

New & Used

Figure 5: The Browse tab.

Figure 6: The Make dialog box.

Browse Sequences

one page on A4 paper in portrait mode, or any other combination of


paper size and layout you can think of.

Another great feature of Help & Manual is that it automatically creates the browse sequences for all of your topics based on their order
in the table of contents. This means that most of the time, you wont
have to spend your time creating browse sequences to allow users to
move through your topics in a logical order. If you do need to create
your own browse sequences, its very easy to do. Choose Options |
Project | Winhelp Options and click on the Browse tab shown in Figure
5. Click the New button to add a browse sequence and then add
topics from the Available topics list to this sequence by double-clicking
them, or by dragging and dropping them. Help & Manual creates
a browse sequence automatically for all topics you dont include in
browse sequences you define. You can test your browse sequences
at design time using the forward and back buttons on the Help &
Manual toolbar. Being able to test links and browse sequences without compiling your help file can save a lot of time.
Help & Manual provides syntax highlighting for Pascal, C++, Visual
Basic, and SQL. Just paste or type the code into your help topic, highlight it, click the drop-down Syntax Highlight button on the toolbar, and
choose the correct language.
To create any type of file from your Help & Manual project, choose File |
Make Help File to display the Make dialog box shown in Figure 6.
The five buttons at the left side of the dialog box let you choose the
type of output you want to produce. Selecting a button also changes the
options that are available in the dialog box. To create a Windows help
file, you must have the Microsoft Windows help compiler installed. To
create an HTML help file you must have the Microsoft HTML help
compiler installed. Both compilers are available free from the Microsoft
Web site, and the Help & Manual help tells you exactly where to get
them. The check boxes at the right edge of the dialog box let you specify
which topics will be included in or excluded from the file youre creating.

Printing a Manual
To print a hard copy of the manual, just choose File | Print Manual.
I was really impressed with the printed manual Help & Manual
generated. It was formatted nicely, and included a complete table of
contents and index.
The only thing you would have to add to a Help & Manual hard
copy is a cover page. Every aspect of a printed manual is configurable.
You can specify anything you want for the page-header and pagefooter text, including where you want the page number to appear.
The print-layout options are impressive, also. You can print your
manual with two pages side by side on legal paper in landscape mode,
26 January 2002 Delphi Informant Magazine

The Manual Options dialog box is a five-page, tabbed notebook that


lets you configure everything from whether you will print a table of
contents, to the numbering style you will use for a level-nine chapter
heading. You can even print just the odd or even pages, so you can
duplex print your manual on a printer that does not have built-in
duplex printing support. If, for some reason, you dont want to use
the printed manual Help & Manual has generated, just create an
RTF file, open it in Word, and make whatever changes you need.
When you create a new Help & Manual project, you can use any other
project as a template. Doing so imports the table of contents and all
layout and formatting information from the template project. Help &
Manual includes eight layouts. A Help & Manual layout is basically a
style in word-processing terms. A layout specifies the font, paragraph
formatting, character set, and tab settings. The first two layouts,
Header Text and Topic Text Default, cannot be renamed, but can be
changed. The remaining six layouts are available for you to name and
set up in any way you choose. You can apply layouts as youre writing
text, using the Edit menu or the 3 through 0 keys.
Help & Manuals documentation is excellent. One of the options
you can configure in Help & Manual is whether to use the Windows
help version of the online help file or the HTML help version. The
source project for the Help & Manual help file is included, also. So,
if you want a printed manual, all you have to do is open the project
and choose File | Print Manual. The help file is very well organized and
leads you, step-by-step, through creating a project, creating the help
topics, and producing all the various output types. Its very unusual,
in my experience, to find a help file thats designed to be read like a
manual. But this one is, and it makes learning to use Help & Manual
very easy. From the time I first installed Help & Manual, it took me
less than a day to learn the product and produce a complete help
system for a small project.

Importing a Delphi Project


One of the most intriguing features of Help & Manual is its ability
to import a Delphi project. Choosing Import a Delphi Project from
the Tools menu starts a wizard that lets you choose the Delphi project you want to import. Next, the wizard provides a list of component classes that will not be included in the topic outline. You can
add any class to or remove any class from this list. Theres also an
option to exclude classes whose names have not been changed. In
the next step, the wizard creates a tree view that consists of all the
forms in the project and all the visual components on each form.

New & Used

Help & Manual lets you create all the documentation for your project
from a single source. The built-in WYSIWYG editor makes creating
and organizing help topics easy, and, once the writing is done, you
can produce Windows help, HTML help, and a printed manual with
just a few mouse clicks. Being able to import a Delphi project to
build your table of contents and list of topics is the icing on the cake.
EC Software
Suedtirolerstrasse 1 b
5201 Seekirchen
Austria
Phone: +43-6212-7838
Fax: +43-6212-4724
E-Mail: office@ec-software.com
Web Site: http://www.ec-software.com
Price: single-user license, US$199; additional-user license, US$69.

At this point, you can delete any components you do not want
included as topics in your help file. Here, you also get to choose
from three options that control how the topics will be added to
your help project. The first option adds all the topics to the table of
contents. The second option adds all the topics to the list of pop-up
topics. The third option adds the forms to the table of contents and
the components on the forms to the pop-up topics.
You also get to choose how your topic IDs will be generated.
The first option will create the topic IDs from each components
HelpContext property if a value has been assigned to HelpContext.
The second option uses FormName.ComponentName as the topic
ID. Importing your Delphi project is an easy way to get a relevant
table of contents to start with. But also, by creating a topic for
every visible component of every form, importing your Delphi
project helps you ensure nothing is skipped.

Conclusion
Help & Manual is an outstanding product. It is a stand-alone
help-authoring tool, so no particular word processor is required. Its
WYSIWYG topic editor lets you see exactly what your help topic is
going to look like in the finished help file. Help & Manual is very
easy to learn and very easy to use. I can create a help system faster
using Help & Manual than with any other product Ive tried. If you
hate writing documentation (or even if you dont) Help & Manuals
ability to create Windows help, HTML help, an HTML file, an RTF
file, and a printed manual from a single source really makes documentation easy. Being able to import a Delphi project to build your
table of contents and list of topics is the icing on the cake.

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 80 articles, a contributing editor to Delphi Informant
Magazine, and a member of Team B, which provides technical support on the
Borland Internet newsgroups. Todd is a nationally known trainer, has taught Delphi
programming classes across the country and overseas, and is a frequent speaker at
Borland Developer Conferences in the United States and Europe. Readers may reach
him at bill@dbginc.com.
27 January 2002 Delphi Informant Magazine

New & Used


By Warren Rachele

Async Professional ActiveX


Package Suitable for Delphi, VB, and Visual C++

dding serial communications capabilities to an application is never a simple matter.


Often, the project requires specialized knowledge that might be outside an individual programmers expertise. For years, Delphi and C++Builder developers have
relied upon the power, flexibility, and reliability of the Async Professional serial communications library from TurboPower Software. Now, to bring that power to another
segment of the development community, TurboPower has brought Async Professional
ActiveX (APAX) to market.
The highly regarded Async Professional (APRO)
libraries have brought the intricacies of Internet and serial communications programming
to numerous programmers, to allow them to
concentrate on the capabilities of their programs
rather than on how to perform some low-level task,
such as addressing the Universal Asynchronous
Receiver-Transmitter. Because most readers of this
publication will be familiar with the scope of the
TurboPower libraries, this article will concentrate
on the highlights of the ActiveX product and what
sets it apart from the component libraries.

APAX
APAX incorporates the functionality of the APRO
libraries into an ActiveX package to make it suitable
for use in environments that support that interface,

such as Delphi, Visual Basic, and Visual C++. The


control gives programmers the tools to integrate serial
communications capabilities into an application,
whether on the scale of direct, serial port communications between two machines, or a large-scale
connectivity project requiring a high level of complex
communications. After installation of the APAX control, users of APRO might be mystified by the lack
of choices in the VCL. True to ActiveX form, only a
single control appears on the ActiveX tab of the VCL
or the Components dialog box in VB. This compares
with the four control-filled tabs that APRO installs.
(The packaging of an abstract component into an
ActiveX control often hides the majority of the power
behind its interface.)
Developers should not confuse the ActiveX packaging
with a light edition of the library. All the capabilities you need for communications are in the ActiveX
control. You can use the APAX control to create
serial communications applications, or communicate
via Winsock and TCP/IP. The powerful data-triggering functions are present in this version of the library
and give the programmer complete control over the
communications session. In addition to the powerful
code you are adding to the program, you gain a
simplified interface as well.

Putting APAX to Use

Figure 1: The APAX control in its default state.


28 January 2002 Delphi Informant Magazine

The ActiveX control installs smoothly. ActiveX


controls are registered with Windows and become
available to all of your development tools concurrently. After installation, I was able to use APAX from
Delphi, VB, Visual C++, and the Microsoft Office

New & Used

Figure 3: Developers
can also use the
APAX control invisibly.

Figure 4: Setting
up a data trigger
to capture the text
string available.
Figure 2: Communicating using the APAX control.

suite. The APAX controls interface will appear in the same format in
any of these environments. As shown in Figure 1, the control drops onto
your form, ready and willing to communicate.
Those who arent ActiveX developers may be surprised to find a
functional object inserted into the project. Users of the Async
component libraries will be accustomed to installing and configuring each element of what we see here: the Terminal object, the
Dialer object, etc. What do you give up by using this prefabricated
object over the component parts? Nothing.
Straightforward serial communications are at the core of APAX. Figure 2
shows a simple communications program built using the APAX control.
The majority of the controls settings were modified using the Properties
dialog box and did not require coding. The toolbar has been turned off,
so the user will go to the provided buttons to control the program. The
modem-status lights have been left on and track the communications
status of the device automatically without any coding. Though the APAX
control displays a full terminal interface by default and is fully featured in
this configuration, its not necessary to use it this way.
Figure 3 shows the placement of the control so it will be transparent when the application is executed. In this configuration, the
application can address an instance of the object without the use
of the visible interface.
The standard controls that gather and display the Winsock port call
the APAX methods for their communication needs, but do not use
the terminal, toolbar, or status bar. This invisible use of the APAX
control gives the developer the flexibility to implement his or her user
interface via the standard controls, while using the rich communications library that the control supplies.

Data Triggers
The quality and reliability of the TurboPower Async libraries is usually reason enough to recommend the product in either form, but
one outstanding feature truly sets it apart from products that compet29 January 2002 Delphi Informant Magazine

ing vendors offer: the data-trigger mechanism. Popular with longtime


users of the APRO libraries, the data trigger also is available to users
of the APAX control. The feature enables programmers to review data
from the incoming data stream based on actions associated with the
data-trigger mechanism. For example, Figure 4 shows a data-trigger
value being set for the text available.
APAX will watch the data stream of a communications session and
will trigger the OnDataTrigger event if it encounters a string. For
this example, we will simply display a message box that indicates
the string was encountered (see Figure 5). Rather than the developer
having to write his or her own screen scraper, the APAX data trigger

Async Professional ActiveX incorporates the functions necessary to


add serial communications capabilities to an application. The function
list includes Winsock operation, serial port management and logging,
terminal emulation, TAPI, and file transfer protocols. Functions scale
from the simplest machine-to-machine connections up to large-scale
connectivity all in a portable ActiveX control.
TurboPower Software Company
15 North Nevada Ave.
Colorado Springs, CO 80903-1708
Phone: International sales, (719) 471-3898;
in the US and Canada, (800) 333-4160
Fax: (719) 471-9091
E-Mail: orders@turbopower.com
Web Site: http://www.turbopower.com
Price: US$349

New & Used


has encountered the string we specified and has fired the event. Once
the event triggers, the data stream is passed through to the application unchanged so the communications session isnt affected.
The example is simple and doesnt stretch the capabilities of the
mechanism. APAX enables the developer to control the packet size
(how many characters to look for), whether to make the trigger
case-sensitive, and even a timeout parameter that terminates the
data trigger if the condition is not met within a specified time.
Data triggers can persist and can be re-enabled once theyve fired.
If the data youre watching for cannot be defined easily, the datatrigger mechanism supports the use of traditional DOS wild card
characters in the trigger string. A question mark (?) in the string
will substitute for a single character, whereas an asterisk (*) accepts
any number of characters between the defined starting and ending
characters. For example, a data-trigger string of C*T will trap
CAT or COOT. The escape character \is also supported, so your
data trigger can trap asterisks and question marks.
The data-trigger mechanism of the APAX control gives the
developer nearly infinite flexibility to automate communications
sessions for the user. The data-trigger mechanism can be programmed by the developer before delivery, or can be modified
on-the-fly by the user. Either way, the use of the data triggers is
limited only by the imaginations of the developer and user.

Standard Features
APAX supports the full range of features a developer expects to
find when considering a component or library. APAX completely
controls the byte stream, regardless of the port through which it
travels. The developer can choose to let the control manage most
of the details, and APAX is happy to do so. For more technically
demanding applications, APAX has an interface to all the lowlevel capabilities necessary to control the data flow into or out of
an application. A well-designed dispatch-logging service is built
into APAX, and nothing is more useful than a byte-by-byte log
file from which you can examine the data flow when debugging a
serial communications application. To round out the basic group
of features, the APAX control provides a customizable terminal
interface and a full range of file transfer protocols.
Although components provided by the compiler vendor may
supersede this feature, APAX also supports communications via
Winsock. Supplying an IP address and a port number turns the
terminal window of the control into a capable telnet application.
APAX can act as either a client or server in this mode for either
making calls or listening for them. Another Windows-based
standard is supported in Telephony Application Programming
Interface (TAPI). The TAPI DLLs are intended to simplify com-

30 January 2002 Delphi Informant Magazine

Figure 5: In this example program, the data trigger has fired


the OnDataTrigger event.

munications programming within the Windows environment,


and, naturally, APAX makes good use of it by offering a clean
interface to this API.

Conclusion
TurboPower Software extends its winning streak with APAX.
TurboPowers years of asynchronous communications expertise make
this package a definite winner. While most Delphi developers will
opt for the Async Professional libraries to take advantage of the flexibility of numerous separate components, Object Pascal developers
should not dismiss APAX. For the programmer who wants to add
serial communications to an application without the learning curve
of APRO, the ActiveX control is ideal. This control also should
make an impact on other developer camps that rely on the ActiveX
package. The control is an outstanding performer and comes with
TurboPowers usual excellent and thorough documentation. Usage
examples are provided for Delphi, VB, and Visual C++.

Warren Rachele is chief architect of The Hunter Group, a software-development


company in Evergreen, CO that specializes in database-management software.
He has written two books, Learn Object Pascal with Delphi and The Tomes of
Delphi: Win32 Database Developers Guide (both from Wordware Publishing).
Rachele also teaches programming, hardware architecture, data communications,
and database management at colleges in Colorado. Readers may contact him at
wrachele@earthlink.net.

New & Used


By Robert Leahey

Code Co-op
Affordable, Server-less Version Control

pend any time at all on Borlands delphi.thirdparty-tools newsgroup, and you will
see that two of the most frequently asked questions regard what reporting tool to
use, and what version control software (VCS) to use. While it should be fairly obvious
what I would suggest for the former, for the latter I can now make a new suggestion.
Code Co-op by Reliable Software is a version control package that deserves some
mention in these discussions, and in the following paragraphs Ill tell you why.
Code Co-op saw its inception when two developers living in different countries (one in the
United States and the other in Poland) needed
to do some collaborative development. Lacking
a common server needed by standard VCS packages, these two developers created a system that
kept a code repository on each of their machines
and used e-mail scripts to keep the source code
in synch. That was the birth of Code Co-op,
and while the product has grown since then, the
basis of the software is still a server-less VCS.

Version Control for Newbies


Just in case youre new to the idea of VCSes, Ill
give a quick overview here. A VCS is basically a
source code repository that stores a copy of your
code and logs the changes you make to it. The
major benefits of this include having a stored
copy of your code as a backup, having a history
of changes to the code, so you can return to
a previous version of a file or files, and if
youre in a team environment having some
ability to manage source code collisions (which
can happen if two developers make changes to
the same file at the same time).
There are two approaches to managing collisions: optimistic and pessimistic. The pessimistic
approach attempts to avoid collisions by locking
31 January 2002 Delphi Informant Magazine

all files under the VCSs control. These files


become read-only on your computer, and you
need to check them out from the VCS to alter
them. This has the effect of reserving a file and
preventing anyone else from checking that file
out while youre making changes to it. When used
correctly this prevents collisions entirely, but it
can be inconvenient for those who dont want to
wait to make their changes. An example of pessimistic locking is Visual Source Safe by Microsoft.
The optimistic approach has no locking scheme,
instead allowing any user to make changes to
any file at any time. The result is that the inconvenience of locking files is removed but there
can often be collisions. In this case, when two
or more developers attempt to check in their
changes to the same file, the first one to do so
wins; his or her changes are checked in with
no problems, but the following developers all
have issues in which their changes to the source
files must be merged with the changes from the
previous developer. An example of the optimistic
approach is Borlands TeamSource.
Other common functionality of VCSes includes
benchmarks, differencing, e-mail services, comments,
and integration into your development environment.
Benchmarks are the ability to define an indication at a

New & Used


and IDE integration. It offers a sort of hybrid optimistic/pessimistic locking scheme (Ill explain that later), and offers some handy
features for managing teams of developers. Well take a closer look
at these features, but first we need to discuss the method used for
code synchronization.

Figure 1: Code Co-ops primary interface.

given position in the progress of the files. This is called by many different
names in various VCS packages: cutting a label, setting a bookmark,
adding a version, etc. Its a feature often used when you want to
indicate a definitive point in your development, such as when you reach
a release point. This benchmark is the code that was in version 2.3 of
our product. The VCS package should allow you to retrieve all the files
in a project as they were at the time of that benchmark. Thus you can get
the files as they were for version 2.3 and try to figure out how you broke
version 2.4.
Differencing is an important part of version control. This is the ability
to display the differences between two versions of a file. Many packages
provide e-mail services to send notifications to your team members
when changes have been made to the source code.
Another common feature is the ability to add comments when
checking in or adding files to a project. This can be helpful when
reading over a large number of check-ins to find a specific one. More
advanced VCS products provide the ability to add those comments
to your source code files automatically as they are checked in.
Finally, integration into your IDE can be an important convenience. With the ability to check files in and out, add comments,
add and remove files, etc. directly from your IDE, you are saved
the inconveniences of closing a file you want to change, leaving
the IDE to launch your VCS, checking out the file, reopening it
in the IDE, making your changes, and so on. Instead, you can just
check out a file you have open right from your IDE.

What makes Code Co-op stand apart is the fact that all synchronization and notification is handled via e-mail scripts. When you
check in a set of changes, Code Co-op compares the previous version of the file and the new version, then sends a script to all other
members of the project, containing only the information required
to duplicate the changes in the other members files. Likewise,
when a set of changes are received by a member, an acknowledgement script is generated. Because theres no centralized server,
Code Co-op cannot have true pessimistic locking; on the occasion
when two (or more) project members attempt to check-in changes
to the same file, the first script received wins, while the other
scripts are rejected, requiring that project members correct the
collision.
In spite of this de facto optimistic scheme, Code Co-op requires
you to check out files before working on them. This is an
annoyance, as it in essence saddles users with the worst of both
approaches. Another annoyance for me was the barrage of scripts
flying back and forth as files are managed. Your e-mail client will
essentially become analogous to a server messaging hub using
Code Co-op, but this is understandable given the nature of the
product and I became used to the flurry. Plus, I got in the habit
of having Code Co-ops Dispatcher handle all messages before I
began checking e-mail. Usually this resulted in not seeing the messages, because the Dispatcher would clean them up after it finished
processing its scripts.

Standard VCS Features


Concerning the features I mentioned earlier, Code Co-op does
a fine job of providing most of what you would expect from a
VCS package. With regard to benchmarks, every check-in within
Code Co-op creates a new benchmark. You also have the option
of adding a label as a marker for significant progress points. The
product will display a comprehensive history of all steps taken as
a result of scripts sent and received. Its easy to select a set of past
scripts and tell Code Co-op to re-send them, or to select a benchmark or label to retrieve an earlier version of the project.

What about Code Co-op?


Now that youre familiar with VCS packages in general, lets look at
Reliable Softwares Code Co-op specifically and see how it stacks up.
The primary interface is compact (see Figure 1) and very responsive. The executable launches with almost no start-up time, making
it seem like the application is always running. Once the system is
properly set up, it behaves rather quietly, sending and receiving its
scripts and informing you via a system tray icon when you need
to attend to something. On the downside, the documentation is
scarce and a little obtuse; the terminology can be obscure, and getting the system set up in the first place took some work. It leaves a
bit to be desired from a user interaction design standpoint, as well.
However, I have no major complaints, and my few interaction
gripes dont stop me from being satisfied with the product.
Code Co-op offers many of the standard features you would expect
from a VCS, including benchmarks, differencing, e-mail services,
32 January 2002 Delphi Informant Magazine

Code Co-op is an affordable, server-less VCS that enables collaboration


from virtually any location by keeping projects synchronized via scripts.
These scripts can be exchanged by LAN, e-mail, or portable media and
allow your team members to have the latest files whether theyre
remote, or on site.
Reliable Software
1011 Boren Avenue, PMB 206
Seattle, WA 98104
Phone: (206) 361-6679
Fax: (206) 367-6085
E-Mail: support@relisoft.com
Web Site: http://www.relisoft.com
Price: US$95 per seat. A 31-day trial version is available for download.

New & Used


Code Co-ops differencing is adequate, offering
a color-coded view that displays changes made
to a source file (see Figure 2). This is good
given that if your script is rejected as a result of
a collision, youll need that differencing capability to show where the collisions occurred.
You can create IDE integration for Code
Co-op, making it possible to perform most
of the common actions from within your
IDE rather than from Code Co-op itself. I
created a simple integration expert which
quickly had me checking in, checking out,
and adding and removing files from Code
Co-op projects all from within my IDE.
This is a very nice feature of the product.
Code Co-op also claims to be Microsoft
SCC API compliant, which is less of an
issue for Delphi developers, but could come
in handy if you also work in Visual Studio,
HomeSite, Cold Fusion, or other packages.
I cant attest to how well Code Co-op implements SCC compliance because I was unable Figure 2: Code Co-op differencing.
to integrate Code Co-op into my Allaire
HomeSite environment, even with several rounds of e-mail supfound it to be small, powerful, reliable, and unobtrusive. It also
port. And Code Co-op currently offers no support for automated
requires minimal intercession from the user. I heartily suggest it
source commenting.
as a dark-horse competitor in the world of VCSes especially if
your work involves remote team members, or if you need to keep
Some of the other convenient features include team management
files at home synchronized with files at work.
options and the Code Co-op Dispatcher. For team management,
you have the ability to vote in a new administrator, if necessary, and With its command-line options, creating an expert for Delphi IDE
the ability to change your project membership to observer. If, for
integration is simple, making Code Co-op an excellent option for
instance, you were going on vacation, becoming an observer would
Delphi developers. And given its price and its features, it delivers
lessen the amount of e-mail being sent to you. You could then
quite a bang for the buck. Moreover, because Reliable Software
change your status back to full membership when you returned.
offers a 31-day trial edition of Code Co-op, you cant go wrong by
The Dispatcher is a system tray application that can be set up to
trying it out.
automatically send and retrieve e-mail scripts on a timed basis. This
feature helps make Code Co-op a little more transparent.
Although Code Co-op is billed as a server-less VCS, it is equally at
home on a LAN, or in a hybrid configuration. The former would
allow you to use the product in a conventional team development
setting, where the script messages would be invisible, and the latter
allows you to have a combination of local and remote developers.

Conclusion
Given some of the annoyances Ive mentioned, it may seem as
though Im unimpressed with Code Co-op. On the contrary, I

33 January 2002 Delphi Informant Magazine

Until recently, Robert was Director of Product Management-ReportBuilder for


Digital Metaphors Corp. He now provides custom creative solutions (including
report designs) via his company, Thoughtsmithy. 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. He can
be reached via his sporadically maintained Web site at http://www.leahey.com.

File | New
Directions / Commentary

Interview with Cary Jensen

orn in California, Cary Jensen grew up in San Mateo, on the San Francisco peninsula just north of Silicon Valley.
He attended college at California State University at Hayward, where he studied experimental psychology and
statistics. While there, he developed an interest in computers software interface issues, in particular. After graduation, he accepted a research fellowship at prestigious Rice University in Houston, Texas, where he received a masters
degree and doctorate of philosophy in human factors psychology, specializing in human-computer interaction.
Jensen and his wife, Loy Anderson, started Jensen
Data Systems, Inc. immediately after completing
their degrees in 1988. Anderson also has a Ph.D.
in human factors psychology from Rice. Their
company specializes in database-application
development, consulting, and training. Because
Jensen is based in Houston, many of his early
database applications focused on the oil-andgas, medical, and aerospace industries. However, he also has worked for
companies specializing in insurance, law, manufacturing, banking, and
finance.
Most people know Jensen from his many books. In their first year,
Jensen and Anderson wrote several training courses, which led to their
first book deal. Since then, the two have written 18 books, including
their latest, Building Kylix Applications [Osborne/McGraw-Hill], which
became available last July. Jensen also has written more than 50 software
training courses, and more than 150 magazine articles.
During the past eight years, Jensen has earned an international reputation as a speaker and trainer, first of Paradox and then Delphi.
He wrote material and was principle trainer for the Borland/
Softbite Delphi World Tours, Borland Developer Days, and the
Delphi Development Seminars. Hes a regular speaker at software
conferences and workshops in North America and Europe, and he
presented at his 11th consecutive Borland Conference in July. He
continues to write for Delphi Informant Magazine, offer training
seminars, develop software, and provide consulting services.
Delphi Informant: Youve worked as a developer and a trainer for
many years, specializing in database applications. Please share with
us some of your experiences, going back to the days you were
working with Paradox.
Jensen: I had a profound realization in our first year of business.
I wrote a natural-gas transportation tracking system called
GasTrack, and, several months after its deployment, the client
asked me to upgrade the system. It was then that I saw how
the software had changed peoples lives. It showed me that, when
done right, software can help people be more productive, with less
work. The employees enjoyed their jobs more, and the company
was more profitable.
DI: Each new Delphi version has introduced new features and
enhancements, beginning with 32-bit support in Delphi 2. Besides
this obvious one, what do you feel was the most important feature
added to Delphi since the original version?
34 January 2002 Delphi Informant Magazine

Jensen: Theres no question: Support for interfaces is the most


important feature thats been added to Delphi. They provide you
with a whole new level of flexibility in software development. For a
good example of this, take a look at the WebSnap stuff in Delphi 6.
Interfaces abound.
DI: If you could add any feature or capability of your choice to the
next version of Delphi, what would it be?
Jensen: I think Delphi needs a serious reporting tool, but much of
the slack there has been taken up by third-party developers. Another
nice capability would be macro programming in the editor.
DI: As well as being an expert in Delphi, youve worked with other
programming languages. Please share with us some of your experiences with those. Do you have a favorite?
Jensen: Ive coded in maybe a dozen languages, including a number
of statistical programming languages. During the past decade, I have
focused on Paradox (though very little of that nowadays), Object
Pascal, and Java. I love the sheer beauty of Java and its powerful,
object-oriented approach to almost everything, but Id have to say
Object Pascal is still my favorite.
DI: Weve seen some encouraging developments at Borland in the
past year, with the company showing more profitability. From the
training programs you do, how do you gauge the perspective of the
average developer concerning the company and its health?
Jensen: Developers know the quality of Borland tools and have
confidence Borland is going to be around for a long time. However,
many developers tell me they have a hard time convincing management of this, and many say they wish they didnt have to continually
defend Borland. Im not a marketing person, so I dont have a solution to offer, but its a problem.
On the other hand, a number of on-site classes Ive been contracted to
deliver lately have been for developers new to Delphi. This is a very
good sign. It suggests real growth in the size of the Delphi community.
DI: If you could give the company one bit of advice and have it
implemented fully, what would it be?
Jensen: Give Kylix away for free to high schools and colleges. Companies are influenced by the tools their employees know how to use.
A good example of this can be seen in Germany, where Delphi is king.
Until recently, Pascal was the primary language taught in colleges.

File | New
DI: Youve been on the Borland Conference advisory board for a while.
Whats that like? What advice can you give the would-be presenter at a
future conference?
Jensen: Its hard work evaluating the many excellent abstracts and trying
to put together a great program. But its also fun, and the advisory board
always consists of talented and interesting people.
My advice to people who want to speak at BorCon is this: Demonstrate
your ability to write and speak in public. Write articles for magazines
and user-group newsletters. Speak at user-group meetings and regional
conferences. Show the board you can effectively communicate your
knowledge. Being an expert is one thing, but being able to share that
information with others requires different skills.
DI: There was tremendous excitement about Kylix when Borland
announced a Delphi version for Linux development. Now that Kylix is
out, what do you think? Is it all you had hoped it would be? What
reactions do you see from developers?
Jensen: Kylix is incredible. Its everything Borland set out to accomplish
and more. The reaction from developers who are using it is very positive.
Its a great tool. However, until recently, it was too expensive for many to
buy just to kick the tires. Borland really needs to find a way to get Kylix
into the hands of as many people as possible.
DI: Youve worked a lot with Kylix yourself, including training sessions
and your new book. Please share some of your personal experiences,
including what you liked most and least.
Jensen: If you know Delphi, youve got a great start on building Linux
applications with Kylix. You will have to learn a lot about Linux, but
most of your development skills will transfer nicely. I believe Kylix will
grow like JBuilder. Initially, JBuilder had a nice, small following. During
the years, its feature set has expanded, and its now the number one Java
tool. Have you seen JBuilder 5? It rocks.
DI: Lets talk about Linux and the ongoing war between it and Microsoft
Windows. How do you view this? Do you see Linux making significant
strides in the next year or two, and do you see this happening in areas
other than Web development?
Jensen: In the short run, one of the biggest areas where Linux is going to
make a splash is in dedicated systems. A lot of people are not interested
in paying for the Windows license when the user never interacts directly
with the OS. Long-term, workstation development will depend on other
factors, such as how GNOME matures. Itll be interesting.
DI: You are a well-respected author. What advice would you give to the
aspiring technical writer?
Jensen: Dont get me started (laughing). I could talk about writing for
days. I love to write, but its hard work harder than most people
realize. Anybody who wants to write should enjoy the process and be
prepared to take the necessary time to do it right. Thats number one.
Number two is this: Have an agreement with a publisher before you
write. Dont write a book and then shop it around. Find your publisher, agree on the scope and nature of the book, and then write.
DI: How do you view the future of computing in the next five years?
Weve examined the Windows/Linux issue, but there are others. Do
35 January 2002 Delphi Informant Magazine

you see any major developments in database programming? Do you


see other developments on the horizon for which we need to prepare?
Jensen: Five years is an eternity in this industry, but I suspect well see
a continuation of the migration toward distributed and loosely coupled
systems. I think well see less and less reliance on the operating system,
and much more on the network. This will be good for Delphi.
DI: Lets talk a little more about your other interests. How have you
enjoyed the training sessions youve been doing? Tell us about some
of the other interesting things youve done in the last year and your
plans for the near future.
Jensen: I enjoy communicating. The live, interactive experience of
leading a seminar is fun and rewarding. Its also a great learning
experience. The questions attendees raise during my seminars force
me to examine issues I might not otherwise have considered.
The future holds much of the same, for the time being; Im starting
to look toward next springs schedule. Also, I offer extensive options
for on-site training, as well as consulting services. Readers can learn
about these by visiting http://www.JensenDataSystems.com. Im also
considering several book offers.
DI: How frequently do you travel? Do you have a favorite destination? Would you like to share any weird travel stories with readers?
Jensen: I travel a lot. I put in about 100,000 miles a year. Fortunately,
Loy is usually with me, and I really do like airports and hotels. But I
do miss my home in Houston after a while on the road.
My favorite cities are San Francisco, New York, London, and Amsterdam, and many others come in a very close second. Ive got a lot of
weird travel stories, as you might imagine, but the strangest is when
I saw one of my suitcases lying on the tarmac at Londons Gatwick
Airport as I was transferring to a flight to Copenhagen. Cars and
lorries had to swerve to miss it. Surprisingly, it arrived in Copenhagen
on my flight, though it was a bit banged up from having fallen off
the baggage-transfer vehicle.
DI: To conclude, are there any areas Ive not touched upon that you
would like to share with readers?
Jensen: The Delphi community is great, and Im proud to be a member
of it. There are just so many people who willingly share their techniques
and information. From the Web site operators to the many conference
speakers, from the authors to the talented developers in this community
as a group, Delphi people are wonderful.
Alan C. Moore, Ph.D.

Alan Moore is Professor of Music at Kentucky State University, where he teaches music
theory and humanities. He was named Distinguished Professor for 2001-2002. He
has developed 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 also has 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.

You might also like