You are on page 1of 49

September 2001, Volume 7, Number 9

Cover Art By: Arthur Dugoni

ON THE COVER
5

OP Tech

Two-tier Database Development Bill Todd


Bill Todd shows you how to build and deploy a two-tier client/server
application using dbExpress, one of the major new features in Delphi
6, and the sample InterBase database that ships with Delphi.

FEATURES
10 On Language
Console Applications: Part II Mike Edenfield
In Part II of his console programming series, Mike Edenfield shares
several advanced features that console applications offer programmers, including screen buffers, view windows, and console modes.
16 In Development
Custom Key Bindings Cary Jensen, Ph.D.
Cary Jensen introduces custom key bindings, a little-known feature of
the Delphi Open Tools API that permits you to add custom keystroke
combinations to the Code editor. And it works for Kylix too!
21 Columns & Rows
Using the XML Features of SQL Server 2000: Part II
Alex Fedorov
This month, Alex Fedorov describes how Delphi interacts with the
Microsoft XML Parser, before moving on to the main topic of how to
query SQL Server 2000 for XML data from Delphi.
25 Kylix Tech
Kylix Program Launcher Brian Burton
Brian Burton demonstrates a pure Kylix alternative to using shell
scripts to launch a Linux application that requires shared libraries,
without requiring technical finesse from the user.

28

Greater Delphi

Introduction to COM Alessandro Federici


Alessandro Federici explains the fundamentals of COM and the reasons its important from its underlying principles to a concrete
example you can download and examine for yourself.
1 September 2001 Delphi Informant Magazine

32

Sound+Vision

DirectDraw Delphi Brian Phillips


Microsofts DirectDraw is a proven technology, but requires some
know-how to make it perform as advertised. Brian Phillips shares
several techniques and details a sample implementation.

REVIEWS
37

ExpressBars Suite 4
Product Review by Ron Loewy

40

SQL Tester 1.2


Product Review by Bill Todd

43

Building Delphi 6 Applications


Book Review by Tom Lisjac

44

Building B2B Applications with XML


Book Review by David Ringstrom

44

Special Edition Using XHTML


Book Review by Robert Leahey

45

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

45

SQL in a Nutshell
Book Review by Christopher R. Shaw

DEPARTMENTS
2
46
48

Delphi Tools
Best Practices by Clay Shannon
File | New by Alan C. Moore, Ph.D.

Delphi
T O O L S

New Products
and Solutions

ModelMaker Tools Releases ModelMaker 6.0 for Delphi


ModelMaker Tools announced
the release of ModelMaker 6.0, a
two-way productivity and UMLstyle CASE modeling tool.
ModelMaker 6.0 generates and
reverse-engineers native Delphi
source code.
ModelMaker 6.0 features
extensions in the code model,
such as method overload support
and custom property access speciers, which give near-100 percent compatibility with Delphi
source code. A new code
importer eliminates virtually all
import restrictions. Code generation allows more customization
such as assigning or maintaining
a custom code order and highly
simplied generation and import
of in-source documentation.
The Code Template palette simplies creating and applying
snippets of related code. The
Creational wizard automatically
inserts and maintains code to
initialize and destroy class elds
and assign property default

values. Enhancement of the


ModelMaker Tools API enables
creating plug-ins for reporting,
inserting code fragments, etc.
Existing interfaces have been
enriched and new ones, such
as access to the Unit Code
Explorer, have been added.
Improvements in the GUI and
other functions improve overview, navigation, and restructur-

ing capabilities.
Check the ModelMaker Web site
for a free fully functional demonstration of ModelMaker 6.0.
ModelMaker Tools
Price: Single-user license, US$269;
10-user license, US$995; site license,
US$1,995.
Contact: info@modelmakertools.com
Web Site: http://www.modelmakertools.com

NAG Software Solutions Announces Support for Delphi 6


NAG Software Solutions
is shipping NAG Components
for Delphi 2.1, a component
suite with complete support for
Delphi 6.
NAG Components for Delphi
2.1 is a cross-platform suite of
more than 70 native general-purpose components for Delphi. The
product includes visual controls
aimed at enhancing the look and
feel of desktop and data-driven
applications, as well as a variety
of system-level components that
enable developers to interact with
the end users system.
The visual controls implement
dynamic borders with over 10
predened styles, as well as
custom gradient or bitmap-based
background lls. The non-visual,
system-level components enable

2 September 2001 Delphi Informant Magazine

developers to access a number of


critical settings and system-wide
parameters on the end-users operating system, such as locale,
regional settings, desktop, screen
saver, memory, processor type, and
system paths. NAG Components
for Delphi 2.1 also includes components that encapsulate specic
peripherals such as the screen, keyboard, and mouse for retrieving
low-level details directly from the
corresponding device drivers.
NAG Components for Delphi
2.1 supports Windows operating
systems and has enhanced property editors and dedicated components for creating custom
screen savers, putting applications in the system tray, making
applications run automatically at
startup, tuning up forms, gain-

ing access to open windows and


processes, and modifying the
default look of hints.
Developers can obtain a free,
fully functional trial copy of
NAG Components for Delphi
2.1 from NAG Software Solutions Web site or from the
Delphi 6 Companion CD.
Customers using a licensed version of NAG Components for
Delphi 2.x have received a free
update, while customers using
older versions need to upgrade
to version 2.1 to get support for
Delphi 6.
NAG Software Solutions
Price: Standard, US$89.95; Professional,
US$104.95.
Contact: sales@nagsoftware.com
Web Site: http://www.nagsoftware.com

Delphi
T O O L S

New Products
and Solutions

Eldean AB Announces ESS-Model


Eldean AB announced ESSModel, an object model reverseengine tool for professional
developers.
A lot of development time
is wasted nding out the object
design or how classes are related

in full-scale projects. ESSModel is a reverse engine for


Delphi/Kylix and Java les.
ESS-Model source code is visualized in automatically generated UML-standard static Class
Diagrams.

A free trial version is available


at the Eldean Software Web
site. It features automatic generation of UML-standard class
diagrams from your source, fast
installation, fast execution, a
Delphi/Kylix (.dpr, .pas) and
Java (.class, .java) parser, visibility ltering, drag-and-drop user
interface, easy integration into
Delphi/Kylix, full control from
the command-line, and diagram
auto-layout.
The registered version of ESSModel offers all the features of
the free version, as well as Javadoc-style HTML documentation, XMI export features, and
no limit on input les.
ESS-Model is available in a
Windows version; a Linux version is planned.
Eldean AB
Price: Trial version, free; complete version, US$49.
Contact: essmodel@eldean.se
Web Site: http://www.essmodel.com

Greatis Software Releases Greatis Form Skin 1.0


Greatis Software released Greatis Form Skin 1.0, transparent
window components for Delphi
and C++Builder.
The Form Skin pack contains
components that allow developers to create windows with
transparent areas and with arbitrary shapes.
Form Skin contains three
components. TCustomFormSkin
is a base parent component
for all skin components; it
denes all methods, properties,
and events needed to create
skinned forms. TSimpleFormSkin
is a component that publishes
some properties and events
derived from TCustomFormSkin.
TBitmapFormSkin allows you to
create form skin by bitmap.
You can use TCustomFormSkin
to create your own skin components. Just derive your component from TCustomFormSkin and
override any virtual methods.
TCustomFormSkin and
TSimpleFormSkin feature simple
controlling of system area (caption and borders), transparency,
simple controlling of controls
transparency, form auto-size by
3 September 2001 Delphi Informant Magazine

skin size, moving window by


client area for non-caption forms,
simple dening form transparency rules, and simple dening of
any form areas as system areas,
such as caption, caption buttons,
resize borders, etc.
TBitmapFormSkin offers Skin
and TransparentColor properties so you can dene form
shapes and Skin images at
design time.
A demonstration kit is
available from the Greatis

Software Web site. It contains


a compiled EXE demonstration, printable documentation
in PDF format, and trial
versions of all Form Skin
components.
Greatis Software
Price: Single-user license, US$29.95;
10-user license, US$149.95; site license,
US$299.95.
Contact: b-team@greatis.com
Web Site: http://www.greatis.com/
formskin.htm

Delphi
T O O L S

New Products
and Solutions

Julian Ziersch Software Introduces WPTools Version 3.1


Julian Ziersch Software introduced WPTools 3.1, a word
processing component developed in Object Pascal for
Delphi and C++Builder.
WPTools 3.1 offers WYSIWYG layout view, ready-to-use

dialog boxes and GUI controls,


table and graphic handling,
and header and footer functionality. The integrated RTFengine supports a variety of
font and paragraph attributes,
including hyperlinks, decimal
tabs, and justied text. It
reads and
writes RTF,
HTML, and
ANSI les,
and can be
attached to
any database.
WPTools
3.1 offers
features such
as paragraph
styles and
hover effects

for links and elds. You can


display pop-up hints for links
and elds, and forms and/or
contracts are supported via special input elds. WPTools 3.1
has the ability to read HTML
with the tags displayed as
objects.
Use WPTools 3.1 with wPDF
to export documents created
in WPTools as PDF les for
automatically generated outlines and links.
A demonstration version of
WPTools 3.1 is available at the
Web site.
Julian Ziersch Software
Price: Contact Julian Ziersch Software for pricing information.
Contact: info@wptools.de
Web Site: http://www.wptools.com

TurboPower Ships SysTools for Kylix


TurboPower Software Co. is
shipping SysTools for Kylix, a version of its productivity library for
Linux programmers using Borland Kylix (Delphi for Linux).
SysTools for Kylix has over 800
optimized, time-tested routines for
handling common programming
operations such as string manipulation, date/time math, high-precision calculations, and sorting. The
library also offers specialized routines for nancial and statistical
calculations; generating, viewing,
and printing a variety of barcode

formats; and routines for accessing


Linux operating system services.
SysTools for Kylix provides
programming constructions the
compiler does not. For example,
SysTools for Kylix has a set of
reusable container classes, including trees, queues, and stacks, that
can be added to projects for
sophisticated data management.
SysTools for Kylix also includes
improved streams for creating
and accessing data in disk les.
SysTools for Kylix ships with
complete source code. It also

includes printed documentation,


examples, and online help. No
royalties are charged when using
SysTools for Kylix in compiled
Linux applications.
TurboPower Software Co.
Price: SysTools for Kylix, US$249; upgrade
from SysTools 3 for Windows, US$149;
upgrade from other TurboPower products,
US$199. SysTools Platform Bundle (Windows
and Linux editions), US$349. Shipping and
handling charges not included.
Contact: (800) 333-4160
Web Site: http://www.turbopower.com

SkyLine Tools Imaging Announces Barcode Recognition Suite 2.0


SkyLine Tools Imaging
announced Barcode Recognition
Suite 2.0. The Barcode Recognition toolkit comes in versions
compatible with Delphi, Visual
Basic, Visual C++, and Borland
C++Builder as a COM object.
Barcode Recognition Suite
2.0 offers position recognition,
making it unnecessary to
identify the position of the
barcode. The suite will automatically detect where the barcode is on the page and read
it from that location. Recognition of multiple barcodes on a
page is also possible.
The proprietary algorithm is
based on the fuzzy logic approach
in image recognition, solving the
problem of identifying barcode
4 September 2001 Delphi Informant Magazine

type. It will decode a barcode


value in a fraction of a second.
The user need only dene the
graphical image of the barcode
as a graphical le (BMP, TIFF,
JPEG, or any other supported
format) or as a DIB handle, or
HBitmap, or HDC. The barcode
module will do the rest.

A free trial version of Barcode


Recognition Suite 2.0 is available
at the SkyLine Tools Imaging
Web site.
SkyLine Tools Imaging
Price: US$1,999.
Contact: sales@imagelib.com
Web Site: http://www.imagelib.com

OP Tech

Database / Two-tier / Client/Server / dbExpress / Delphi 6

By Bill Todd

Two-tier Database Development


dbExpress Makes It Easy in Delphi 6

bExpress is one of the major new features in Delphi 6. This article takes a look
at how to build and deploy a two-tier client/server application using dbExpress
and the sample InterBase database that ships with Delphi. [For an introduction to
dbExpress, its place vis--vis Delphi 6 and Kylix, deployment, and similar issues, see
Bill Todds article dbExpress in the March 2001 Delphi Informant Magazine.]
If you have built applications using the MIDAS
components in a prior version of Delphi, youll
nd yourself on familiar ground when building
your rst dbExpress application. If you havent
used MIDAS, your rst dbExpress application
will involve a lot of new concepts. The best way
to get through the basics is to walk through building the sample application that accompanies this
article (see end of article for download details).

Creating the Application


The rst step in building a typical two-tiered
client/server application using dbExpress is to
create a new project and add a data module. Figure
1 shows the nished data module for the sample
application. Figure 2 shows the applications main

Figure 1: The example projects data module.

5 September 2001 Delphi Informant Magazine

form. The main form contains a page control


with three tabs labeled Employee Sales, Customers, and Schema. The Employee Sales tab shows
the Employees table and the Sales table from the
sample InterBase database in a master-detail relationship. The combo box at the top of the form
lets you select a department. The application then
displays the employees in the selected department
and their sales records. The Customers tab lets you
view and edit the data in the customer table. The
Schema tab lets you view information about the
database schema.
To build this application, start by dropping a
SQLConnection component on the data module.
Then type EmpConn as the name for the component. Next, select the DriverName property in the
Object Inspector and choose InterBase from the
drop-down list. Once you have selected a driver,
you can go to the Params property and open the
property editor (shown in Figure 3), where you
can set all the properties of the driver.
In this example, I entered the path to the database,
the user name and password, and changed the
transaction isolation level to Snapshot. You can
easily make any of these parameters congurable
by loading their values from an INI le or from
the Windows registry. The application described

OP Tech
procedure TEmpDm.LoadIniSettings;
var
Ini: TIniFile;
begin
Ini := TIniFile.Create(ExtractFilePath(
Application.ExeName) + 'Emp.ini');
try
EmpConn.Params.Values['Database'] :=
Ini.ReadString('Database', 'Database', '');
finally
Ini.Free;
end;
end;

Figure 4: Loading the database path from an INI file.

Figure 2: The example projects main form.

property to the SQL statement you want to use. In this case the
SQL statement is:
SELECT * FROM EMPLOYEE
WHERE DEPT_NO = :DEPT_NO

Although you dont need to change it, take note of the CommandType
property. This property controls what you can enter in CommandText.
When set to its default value of ctQuery, CommandText holds
the SQL statement you want to execute. When CommandType is
ctTable, CommandText provides a drop-down list of table names;
when CommandType is ctStoredProc, CommandText contains a
drop-down list of stored procedure names. Add a DataSource
component and connect it to the EmpDs SQLDataSet and name
it EmpLinkSrc. Add a second SQLDataSet component and set its
name to SalesDs. Then set its SQLConnection property and its
CommandText property to:
Figure 3: The SQLConnection components Params property.

SELECT * FROM SALES


WHERE SALES_REP = :EMP_NO

in this article loads the database path from the EMP.INI le by


calling the LoadIniSettings method, shown in Figure 4, from the data
modules OnCreate event handler.

Set the SalesDs DataSource property to the EmpLinkSrc DataSource component. This will cause the SalesDs SQLDataSet to get
the value of its :EMP_NO parameter from the current record in
EmpDs, and also cause the Sales records to be embedded in the
Employee dataset as a nested dataset eld. Next, add a DataSetProvider
from the Data Access page of the Component palette, and set its
DataSet property to the Employee SQLDataSet, EmpDs.

Go to the DataSnap page of the Component palette, and add a


LocalConnection component to the data module. This is a new
component added in Delphi 6 for building two-tier applications
where the DataSetProvider and the ClientDataSet are in the same
application. While putting a ClientDataSet and its provider in the
same application is nothing new, in prior versions of Delphi there
were limitations caused by lack of a connection component. These
include a lack of access to the connection components methods,
events, and properties, particularly the AppServer property. The
LocalConnection component is optional, but including it gives
you access to its properties, methods, and events, and allows
you to write an application that you can convert to multi-tier
architecture with fewer changes later.
Next, return to the dbExpress tab on the Component palette
and add a SQLDataSet component to the data module. While
dbExpress includes three other dataset components, SQLTable,
SQLQuery, and SQLStoredProc, these components are provided
as analogs to the BDE dataset components to make conversion of
BDE applications to dbExpress easy. The SQLDataSet component
can do everything the other three dataset components can do,
so theres no reason to use anything else in a new application.
Begin by setting the SQLDataSets SQLConnection property to the
EmpConn connection component. Next, set the CommandText
6 September 2001 Delphi Informant Magazine

The nal two components you need to supply data to the


Employee Sales tab on the main form are a ClientDataSet and a
DataSource. Set the DataSources DataSet property to connect it
to the ClientDataSet. Set the RemoteConnection property of the
ClientDataSet to the LocalConnection component, then set its
ProviderName property to the DataSetProvider for the Employee
DataSet, EmpProv. Name the ClientDataSet EmpCds.
The DataSetProvider and ClientDataSet components are required,
because all the dbExpress dataset components provide a unidirectional read-only cursor, i.e. you cannot move backward through the
records and you cannot make changes using dbExpress components.
To be able to browse both backward and forward through records
and insert, delete, and update records, you need the ClientDataSet
component to buffer and edit the records supplied by the dbExpress
dataset component, and the DataSetProvider to generate the SQL
statements to insert, delete, and update records in the database.
The components added so far provide everything necessary to
display information from the Employees table in the top grid

OP Tech
procedure TMainForm.LoadDeptCombo;
begin
with EmpDm.DeptDs do begin
Open;
while not EOF do begin
DeptCombo.Items.Add(FieldByName('DEPT_NO').AsString +
' - ' + FieldByName('DEPARTMENT').AsString);
Next;
end;
Close;
end;
end;

Figure 5: The LoadDeptCombo method.

on the main form. To display the records from the Sales table,
add another ClientDataSet and DataSource to the data module,
naming them SalesCds and SalesSrc respectively. Select the
Employee ClientDataSet, double-click it to open the Fields editor,
then right-click and choose Add All Fields. Note the last eld in the
Fields editor is named SalesDs, the same name as the SQLDataSet
component for the Sales table. This is the nested dataset eld that
contains the Sales records linked to each record in the Employees
table. Now select the SalesDs ClientDataSet, click its DataSetField
property, and click the arrow to open the drop-down list. The
only entry in the list will be EmpCdsSalesDs, assuming that you
used the component names shown in Figure 1. The last step is to
set the DataSet property of the DataSource component to connect
it to the Sales ClientDataSet.
This application uses typical client/server architecture by forcing
the user to provide some selection criteria thats used to fetch a
small set of records. In this case, the selection criteria is provided
by choosing a department from the combo box at the top of the
form. The combo box is loaded with the department numbers and
names when the application starts. The data module in Figure 1
contains a SQLDataSet component named DeptDs thats used to
load the combo box.
Figure 5 shows the LoadDeptCombo method thats called from
the main forms OnCreate event handler. This code opens the
department SQLDataSet and uses a while loop to iterate through
the records and load the combo box. Note that no Provider
or ClientDataSet component is required in this case, because
the dataset is only being read and traversed from beginning to
end. Because this SQLDataSet isnt being used with a Provider
and ClientDataSet, its NoMetaData property is set to True. This
improves performance, because metadata information required for
updates isnt retrieved from the server.
When the application starts, the main forms OnCreate event
handler calls a custom method that opens both the employee
and sales ClientDataSets. Since no values have been assigned to
the parameters in the SQL statements, no records are returned,
and both grids are empty. When the user chooses a department
from the combo box, the combo boxs OnChange event handler,
shown in Figure 6, executes. This code closes the Employee
ClientDataSet, extracts the department number from the rst
three characters of the combo boxs Text property, and assigns
it to the ClientDataSets DEPT_NO parameter. When the
ClientDataSet is opened, it sends this new parameter value
through the provider to the SQLDataSet for the Employee table,
opens the SQLDataSet, retrieves the records returned by the
query, and closes the SQLDataSet.
7 September 2001 Delphi Informant Magazine

procedure TMainForm.DeptComboChange(Sender: TObject);


begin
with EmpDm.EmpCds do begin
Close;
Params.ParamByName('DEPT_NO').AsString :=
Copy(DeptCombo.Text, 1, 3);
Open;
end;
end;

Figure 6: The department combo boxs OnChange event handler.

If youve never worked with MIDAS, this may seem like a


complex process. Remember, however, that these components
are also designed to work in multi-tier applications where the
ClientDataSet is in a client program running on one computer,
and the DataSetProvider and SQLDataSet are in an application
server program running on a different computer.
If all you need to do is select records from a single table and edit
them, you can use a single SQLClientDataSet component from the
dbExpress page of the Component palette, instead of a SQLDataSet,
DataSetProvider, and ClientDataSet. The Customers tab on the main
form uses this technique. The SQLClientDataSet is a new component in Delphi 6 that includes a ClientDataSet, DataSetProvider,
and ClientDataSet in a single component. Just set its DBConnection
property to your SQLConnection component, add a SQL SELECT
statement to its CommandText property, and youre ready to go. In the
sample application, the SELECT statement is:
SELECT * FROM CUSTOMER
WHERE STATE_PROVINCE = :STATE_PROVINCE
ORDER BY CUSTOMER

Now add a DataSource component and connect it to the


SQLClientDataSet. Then connect the DBGrid on the Customers
page to the DataSource, and youre nished. The SQLClientDataSet
has MasterSource and MasterFields properties that can be used to
create a master-detail relationship. However, this is very inefcient.
For master-detail relationships, it is much better to use separate
SQLDataSet, DataSetProvider, and ClientDataSet components.
The Customer table works just like the Employee Sales tab.
Choosing a state from the combo box causes its OnChange event
handler to close the SQLClientDataSet, assign the state to its
:STATE_PROVINCE parameter, and open the SQLClientDataSet.

How It Works
If youve worked with MIDAS before, you can skip this section. If
you havent, heres a brief explanation of how the provide/resolve
architecture implemented by the dbExpress components works.
When you open a ClientDataSet, it sends a request to its provider
requesting data. The provider, in turn, opens the dataset its connected to and retrieves all the rows supplied by that data set. The
provider stores the data in a variant in a proprietary format and
sends the data packet to the ClientDataSet. The ClientDataSet
receives the data packet from the provider, extracts the data, and
loads it into memory.
When you edit the ClientDataSet data, the changes are stored
in memory and a property named Delta. To update the database
you must call the ClientDataSets ApplyUpdates method. In the

OP Tech
Getting the Database Schema
Figure 7 shows the Schema tab of the applications main form.
The Schema Type combo box at top of the form lets you choose
to view Tables, System Tables, or Stored Procedures in the top grid.
The choices in the Schema Details combo box vary depending on
whether youre viewing schema information for tables or stored
procedures. For tables or system tables, you can view information
on columns or indices. If youre viewing stored procedures, the
Schema Details combo box contains a single choice, Procedure
Parameters. Schema information is retrieved using the SQLDataSet
component and its SetSchemaInfo method.

Figure 7: The Schema tab of the sample applications main form.

sample application, this is done in the ClientDataSets AfterPost


event handler. Calling ApplyUpdates sends the Delta property back
to the provider. The provider starts a transaction, looks at each
of the changes recorded in the Delta data packet, creates and
executes SQL statements to update the database, then commits
the transaction.
This may seem like a complex architecture, but it has tremendous
advantages, such as:
1) The provider and its data set can be in one application, while
the ClientDataSet is in a different application running on
a different computer. This makes building multi-tier applications easy.
2) Transactions are very short-lived, thus reducing concurrency
problems on the database server.
3) You can sort the ClientDataSets data in any order by setting
its IndexFieldNames property.
4) If youre holding a large amount of data in memory in a
ClientDataSet, you can create in-memory indices for lightning-fast searching and sorting, without the overhead of
maintaining these indices on the database server.
5) You can view a subset of the records in the ClientDataSet by
setting its Filter property using SQL WHERE syntax.
6) You can save the ClientDataSets data to your local hard
drive; disconnect from the network; insert, delete, and update
records; then reconnect to the network and apply those
updates to the database. This makes creating applications for
eld sales and service personnel a snap.
7) You can sort the ClientDataSet on calculated elds.
8) You can dene maintained aggregates (i.e. elds that compute
the sum, average, minimum, maximum, or count of all the
records or groups of records) in the ClientDataSet.
9) You can use the ProviderFlags property of the field object
of the dataset connected to the DataSetProvider to control
which fields are used to locate records to be updated, and
which fields will actually be updated. This allows you to
easily update the fields from one table in the result set of
a join query while ignoring the fields from the other table
or tables.
10) You can save the ClientDataSets data in XML format, and
load XML data into a client data set.
These are just a few of the advantages of the dbExpress architecture.
If youve never worked with the ClientDataSet and DataSetProvider
components, it will be well worth your while to spend as much time
as you can to learn about all of their capabilities.
8 September 2001 Delphi Informant Magazine

Figure 8 illustrates the use of the data modules UpdateSchema


method, which is called from the event handler of the Schema
Type combo box. The SchemaType parameter thats passed to this
method is actually the ItemIndex property of the combo box.
The code starts by closing the SchemaCds ClientDataSet thats
connected through a provider to the SchemaDs SQLDataSet.
The SchemaDs SQLDataSets SQLConnection property is set to
the EmpConn SQLConnection component, but its CommandText
property is null.
The case statement in Figure 8 determines whether the user
chose Stored Procedures, System Tables, or Tables in the Schema Type
combo box by comparing the SchemaType parameter to three constants, StoredProcs, SysTables, and Tables, declared in the interface
section of the data modules unit. In each case, the code calls the
SQLDataSets SetSchemaInfo method, passing the constant that
matches the users choice as the rst parameter.
The second parameter of SetSchemaInfo is SchemaObject, and is used
only when getting information about the columns or indices of a
table, or the parameters of a stored procedure. The third parameter,
SchemaPattern, lets you lter the result set using the same syntax as
the SQL LIKE operator. For example, you could pass 'CUST%' to
get all the tables that start with CUST. Calling SetSchemaInfo tells
the SQLDataSet to fetch the requested schema information when
its opened, and ignore the value of its CommandText property. If
youve retrieved schema information and want the SQLDataSet to
execute its CommandText the next time its open, call SetSchemaInfo
and pass stNone as the rst parameter.
After calling SetSchemaInfo, the UpdateSchema method calls the
main forms LoadSchemaDetail method, and passes either sdtTable
or sdtProcedure to indicate whether the user is viewing schema
information for tables or stored procedures. The sdtTable and
sdtProcedure constants are members of an enumeration declared in
the interface section of the main forms unit.
Figure 9 shows the LoadSchemaDetail method. This method clears
the Schema Details combo box and adds columns and indices as
choices if the user is viewing schema information for tables, or
procedure parameters if the user is viewing schema information
for stored procedures. The parameters passed to Items.Add are
constants declared earlier in the unit.
Figure 10 shows the Schema Detail combo boxs OnChange event
handler. This event handler simply calls the data modules
UpdateSchemaDetail method, passing a parameter that identies
whether the user chose columns, indices, or procedure parameters.
The UpdateSchemaDetail method calls the SchemaDetDs SQLDataSets
SetSchemaInfo method and passes the constant stColumns, stIndexes,

OP Tech
procedure TEmpDm.UpdateSchema(SchemaType: Integer);
begin
with SchemaDs do begin
SchemaCds.Close;
case SchemaType of
StoredProcs:
begin
SetSchemaInfo(stProcedures, '', '');
MainForm.LoadSchemaDetail(sdtProcedure);
end;
SysTables:
begin
SetSchemaInfo(stSysTables, '', '');
MainForm.LoadSchemaDetail(sdtTable);
end;
Tables:
begin
SetSchemaInfo(stTables, '', '');
MainForm.LoadSchemaDetail(sdtTable);
end;
end; // case
SchemaCds.Open;
end; // with
end;

Figure 8: This procedure is called by the OnChange event handler for the Schema Type combo box.

or stProcedureParams that corresponds to the choice the user


made. The second parameter, SchemaObject, is set to the value of
the TABLE_NAME or PROC_NAME eld from the SchemaDs
SQLDataSet. This causes the lower grid on the Schema tab of the main
form to display information about the columns or indices of the table,
or information about the parameters of the stored procedure selected
in the upper grid. The UpdateSchemaDetail method is also called from
the AfterScroll event handler of the SchemaDs SQLDataSet, so the
information displayed in the details grid will update as you scroll from
record to record in the upper grid. As you can see, SetSchemaInfo
makes it very easy to get any schema information you may need in
your application.

Deploying Your Application


There are two ways you can deploy an application that uses dbExpress. The rst is to deploy MIDAS.DLL and the driver DLL for
your database with your application. To nd the name of the driver
DLL for your database, just look at the LibraryName property of
your SQLConnection component. This property is lled in automatically when you set the DriverName property. In the case of
InterBase, the driver is DBEXPINT.DLL. MIDAS.DLL is about
270KB in size and DBEXPINT.DLL is 116KB. Neither of these
DLLs needs to be registered with Windows, and no registry entries
are required. Just copy the DLLs into the folder that holds your
applications EXE le, or to any folder on the path, and youre done.
If you dont want to distribute these two DLLs, you can compile
the code they contain directly into your executable. All you have
to do is add three units, DbExpInt, MidsLib, and Crtl, to the
uses clause in one of your applications units, and all the required
dbExpress code will be compiled into your EXE.

Conclusion
dbExpress makes it easy to create and deploy applications that
use SQL database servers. Since all of the database connection
information is contained in the properties of the SQLConnection
component, you have complete control over whether this information is embedded in your application and stored in the registry, an
9 September 2001 Delphi Informant Magazine

procedure TMainForm.LoadSchemaDetail(
DetailType: TSchemaDetailType);
begin
with SchemaDetCombo do begin
Items.Clear;
case DetailType of
sdtTable:
begin
Items.Add(Columns);
Items.Add(Indices);
end;
sdtProcedure: Items.Add(ProcParams);
end;
end;
end;

Figure 9: The LoadSchemaDetail method.

procedure TMainForm.SchemaDetComboChange(Sender: TObject);


begin
case SchemaCombo.ItemIndex of
StoredProcs:
EmpDm.UpdateSchemaDetail(ProcedureParams);
SysTables:
EmpDm.UpdateSchemaDetail(SchemaDetCombo.ItemIndex);
Tables:
EmpDm.UpdateSchemaDetail(SchemaDetCombo.ItemIndex);
end;
end;

Figure 10: The OnChange event handler for the Schema


Details combo box.

INI le, a text le, or somewhere else. This also lets you provide
your own user interface for changing these properties.
dbExpress also makes writing an application that supports multiple
database servers easy. All you have to do is supply the driver DLL
for the database server your client is using and set the DriverName,
LibraryName, and VendorLib properties of the SQLConnection
component to the correct values for that database server. dbExpress
is small, fast, and easy to deploy, making it an excellent tool for
writing database applications.
The sample project referenced in this article is available on the Delphi
Informant Magazine Complete Works CD located in INFORM\2001\
SEP\DI200109BT.

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 Borland, providing
technical support on the Borland Internet newsgroups. Bill is also a nationally
known trainer and has taught Delphi programming classes across the country
and overseas, and is a frequent speaker at Borland Developer Conferences in
the US and Europe. Bill can be reached at bill@dbginc.com.

On Language

Console Applications / Input/Output / Delphi 2-6

By Mike Edenfield

Console Applications
Part II: Advanced I/O

ast month, in Part I of this series, we covered the basics of writing applications that
make use of the Windows console (character-mode) subsystem. This subsystem
user interface allows you to write command-line utilities and applications that look
and behave like applications written for the MS-DOS operating system.
This is only the beginning of what console applications can do, however. Far from being restricted
to old, pre-Windows programming techniques, the

function ReadConsoleOutput(hConsoleOutput: THandle;


lpBuffer: Pointer; dwBufferSize, dwBufferCoord: TCoord;
var lpReadRegion: TSmallRect): BOOL; stdcall;
function WriteConsoleOutput(hConsoleOutput: THandle;
lpBuffer: Pointer; dwBufferSize, dwBufferCoord: TCoord;
var lpWriteRegion: TSmallRect): BOOL; stdcall;
function FillConsoleOutputCharacter(
hConsoleOutput: THandle; cCharacter: Char;
nLength: DWORD; dwWriteCoord: TCoord;
var lpNumberOfCharsWritten: DWORD): BOOL; stdcall;
function FillConsoleOutputAttribute(
hConsoleOutput: THandle; wAttribute: Word;
nLength: DWORD; dwWriteCoord: TCoord;
var lpNumberOfAttrsWritten: DWORD): BOOL; stdcall;
function ReadConsoleOutputCharacter(
hConsoleOutput: THandle; lpCharacter: PAnsiChar;
nLength: DWORD; dwReadCoord: TCoord;
var lpNumberOfCharsRead: DWORD): BOOL; stdcall;
function ReadConsoleOutputAttribute(
hConsoleOutput: THandle; lpAttribute: Pointer;
nLength: DWORD; dwReadCoord: TCoord;
var lpNumberOfAttrsRead: DWORD): BOOL; stdcall;
function WriteConsoleOutputCharacter(
hConsoleOutput: THandle; lpCharacter: PChar;
nLength: DWORD; dwWriteCoord: TCoord;
var lpNumberOfCharsWritten: DWORD): BOOL; stdcall;
function WriteConsoleOutputAttribute(
hConsoleOutput: THandle; lpAttribute: Pointer;
nLength: DWORD; dwWriteCoord: TCoord;
var lpNumberOfAttrsWritten: DWORD): BOOL; stdcall;

Figure 1: Low-level console output functions.

10 September 2001 Delphi Informant Magazine

Windows console-mode API allows you to take


advantage of all the features of the Windows protected-mode environment, as well as advanced features of the console window that go far beyond
what was possible in MS-DOS.

Low-level Output
The console windows output consists of a collection of character cells. Each cell has a character
and an attribute associated with it. Internally, Windows treats the output as a two-dimensional array
of CHAR_INFO records. Several low-level output
functions exist that allow you to act on a range of
these cells at once (see Figure 1).
The first two functions act on a rectangular region
of the screen. The destination buffer pointer is filled
with an array of CHAR_INFO structures containing the cells at the requested region. The remaining
functions act on a continuous series of these characters, returning either an array of characters or
attributes. Wrapping is automatic at the end of the
line. If the bottom of the screen is reached, the
functions cease processing and return the number
of characters written in the last var parameter.
Use of these functions is fairly straightforward. The
sample programs in Listings One and Two (beginning on page 14) demonstrate a common use of
the FillConsoleOutputCharacter function: clearing
the screen. This is done by simply filling the entire
screen, starting at 0,0, and proceeding for (height *

On Language

Until now, weve been working under a few assumptions. All of our
programs act as if:
 there were already a single console present, and we had to use it;
 there were only one screen of output available; and
 the output screen was only as big as the console window.

If your application already has a console, you must call FreeConsole


before trying to allocate a new one. Calling FreeConsole with no
console present has no negative effects, so its good practice to always
call it rst. AllocConsole creates a new console window, attaches the
standard handles to it, and displays it onscreen. Note that calling
FreeConsole from a child process will return control of the console
to the parent process, allowing it to continue to process input. (This
is the effect you get when you run a windowed application from a
command-line prompt: the windowed application releases the console
immediately, and your prompt returns.)

These three assumptions are the defaults for new console applications,
but by no means are they the only options. Processes are free to
detach themselves from their parents console and create their own,
as well as create multiple output buffers and various-sized windows
into those output buffers. Taking advantage of these features requires
an understanding of the three different Windows objects involved in
displaying text to a console.

Contained within a console are its input and output buffers. Each
console has a single input buffer that handles all input events. However,
theres no such limit to the number of output buffers, which are referred
to as screen buffers. There are two-dimensional grids of characters
representing whats to be displayed on the console. A single screen buffer,
the same size as the console window, is created automatically. The API
calls to manipulate screen buffers are shown in Figure 3.

The console itself is an abstract object created by Windows, either


in response to a console application being run, or at the request of
the application. It contains several I/O buffers, and a visible window
representing the program interface. The API calls that deal directly
with the console are shown in Figure 2.

You can create as many screen buffers as you want, but only one of
them can be active at any given time. The various console output
functions all take a handle to a screen buffer, which doesnt have to
be the active screen buffer. The standard output handle retrieved from
GetStdHandle, however, will always refer to whichever console buffer
is active at the time its called.

width) characters with a space character. This technique is preferred


over others because its fast and compatible with both Windows
NT/2000 and Windows 95/98.

Consoles, Screen Buffers, and View Windows

Unfortunately, the GetConsoleWindow function only works in


Windows 2000. It allows you to get a legitimate window handle
to the console window, and use it with API calls such as
ShowWindow. In earlier operating systems, you could get this
information via EnumWindows or similar functions. To assist you
in nding the consoles window handle, you can retrieve (or
change) the title bar for the console window with the appropriate
functions. If you dont change it, the console window takes on the
name of the last process to attach to it (such as the EXE name
of a DOS command).

function GetConsoleWindow: THandle; stdcall;


function AllocConsole: BOOL; stdcall;
function FreeConsole: BOOL; stdcall;
function SetConsoleTitle(lpConsoleTitle: PChar):
BOOL; stdcall;
function GetConsoleTitle(lpConsoleTitle: PChar;
nSize: DWORD): DWORD; stdcall;

Figure 2: These API calls deal directly with the console.

function CreateConsoleScreenBuffer(
dwDesiredAccess, dwShareMode: DWORD;
lpSecurityAttributes: PSecurityAttributes;
dwFlags: DWORD; lpScreenBufferData: Pointer):
THandle; stdcall;
function GetConsoleScreenBufferInfo(
hConsoleOutput: THandle;
var lpConsoleScreenBufferInfo: TConsoleScreenBufferInfo):
BOOL; stdcall;
function SetConsoleActiveScreenBuffer(
hConsoleOutput: THandle): BOOL; stdcall;
function SetConsoleScreenBufferSize(
hConsoleOutput: THandle; dwSize: TCoord): BOOL; stdcall;
function ScrollConsoleScreenBuffer(hConsoleOutput: THandle;
const lpScrollRectangle: TSmallRect;
lpClipRectangle: PSmallRect; dwDestinationOrigin: TCoord;
var lpFill: TCharInfo): BOOL; stdcall;

Figure 3: API calls for manipulating screen buffers.


11 September 2001 Delphi Informant Magazine

Its interesting to note how changing the active screen buffer


affects calls to the library functions, Write and Writeln. These
functions act on a global textle-type variable called Output by
default. This variable is attached to the standard output when
your application rst executes, and never changes. Keep this in
mind if you change the active screen buffer, as your output from
Write or Writeln may no longer be visible until you switch active
buffers. Note in Listing One the function WriteToScreen, which
is used in place of Write or Writeln, always writes to the current
active buffer.
Associated with each screen buffer is a view window. This shouldnt
be confused with the console window itself, although its very closely
related. The view window of a screen buffer is the portion of the
screen buffer visible onscreen. To work with the view windows, use
these functions:
function GetLargestConsoleWindowSize(
hConsoleOutput: THandle): TCoord; stdcall;
function SetConsoleWindowInfo(hConsoleOutput: THandle;
bAbsolute: BOOL; const lpConsoleWindow: TSmallRect):
BOOL; stdcall;

This SetConsoleWindowInfo function changes the size and position


of the view window. This can be used to easily scroll a console
window by simply moving the view window down the screen
buffer as needed. The maximum window size is determined by
the size of the screen buffer attached to it, and the size of the
font the user has chosen for the console itself. Windows provides
the GetLargestConsoleWindowSize function that takes into account
all these factors, and determines the maximum size of a console
window given its current characteristics. According to Microsofts
documentation, attempting to set a window size larger than this,
or which extends beyond its screen buffer, results in an error.
However, the actual behavior of the SetConsoleWindowInfo function
is a bit different. There are also discrepancies between Windows
95/98 and Windows NT/2000 console-mode behavior. Listing One
demonstrates two examples of these differences.

On Language
In Part I, we discussed two levels of input and output processing
available to consoles. Microsoft terms these as high-level and
low-level access to the console. Not surprisingly, there are different sets of console-mode options affecting the high- and low-level
I/O functions. Theyre set or cleared by passing a bit mask to the
SetConsoleMode function, which can contain any combination of
high- and low-level I/O mode options.

Figure 4: The example ScreenBuffers program at run time.

Attempting to scroll beyond the bottom of the screen buffer in


NT/2000 actually causes the view window to shrink. This confusing
behavior occurs when you attempt to set the view window position
via relative coordinates, and the bottom coordinate is beyond the
bottom of the screen buffer. In this case, the top coordinate is
adjusted as requested, but the bottom coordinate is left the same,
effectively changing the number of lines in the view window. To
avoid this problem, use the function GetConsoleScreenBufferInfo, and
either calculate absolute coordinates for the new view window instead
of relative ones, or verify that your relative coordinates wont cause
this shrinking behavior before setting them.
This odd behavior doesnt occur in Windows 95/98. In fact, Windows 95/98 programs cannot change the size of their output buffers view window under any circumstances. Attempting to change
the size of a view window will render the attached output buffer
useless: no output will be displayed as long as the view window
size differs from the size of the actual console window. To most
clearly see this behavior, run Listing One under Windows 95/98,
then comment out indicated sections and run it again. As long as
the second screen buffer is a different size than the rst, it will
appear empty. In contrast, running the application under Windows
NT/2000 will result in the dimensions of the console window
changing as the active screen buffer changes, and the text in all
buffers will always be visible.
Notice theres no {$APPTYPE CONSOLE} directive in Listing
One because its not needed; the console is created by a call to AllocConsole. (The run-time result of Listing One is shown in Figure 4.)
This is done primarily for demonstration purposes. In general, if you
know youll always have a console while your program is running,
youll get better performance by letting Windows create it automatically. The API call will be used more effectively in later installments
of this series, when we create console windows for normal GUI
applications.

Console Modes
Much of the behavior of the console window depends on what mode
Windows has put the console in. For most situations, the default
settings for the console mode will be sufcient. However, changing
these modes allows you a very intimate level of control over the
behavior of the console. Changing and querying the console modes is
accomplished via two API calls:
function GetConsoleMode(hConsoleHandle: THandle;
var lpMode: DWORD): BOOL; stdcall;
function SetConsoleMode(hConsoleHandle: THandle;
dwMode: DWORD): BOOL; stdcall;

12 September 2001 Delphi Informant Magazine

For high-level input and output, the following console modes are
available:
 ENABLE_LINE_INPUT Input is processed by your application one line at a time.
 ENABLE_ECHO_INPUT Characters are echoed back to the
screen as entered.
 ENABLE_PROCESSED_INPUT Windows handles editing
and control characters internally.
 ENABLE_PROCESSED_OUTPUT Windows handles control sequences automatically.
 ENABLE_WRAP_AT_EOL_OUTPUT Lines wrap automatically at the edge of the screen buffer.
By default, all ve of these options are turned on. This mode of operation
is sometimes called cooked mode, because Windows performs most
of the editing and control-code handling for you. Your ReadFile calls
will block until a carriage return is entered, and Windows will correctly
process and echo back any pressed editing or control keys. If you want
to turn off the three input modes, keep in mind that ECHO_INPUT
mode is disabled automatically if you disable LINE_INPUT. Also,
attempts to turn off WRAP_AT_EOL_OUTPUT mode under Windows 95/98 are silently ignored.
Low-level console output, using functions such as WriteConsoleOutputCharacter
or FillConsoleOutputAttribute, isnt affected by any input or output
modes. This low-level output is always in a very raw, direct form.
However, the ReadConsoleInput API and related low-level input functions are affected by these three input modes:
 ENABLE_MOUSE_INPUT determines if MOUSE_EVENT
input events are reported to your application.
 ENABLE_WINDOW_INPUT determines if
WINDOW_BUFFER_SIZE_EVENT input events are reported
to your application.
 ENABLE_PROCESSED_INPUT automatically handles CC.
Processed input mode here simply means that Windows automatically calls the control handler when CC is pressed. By default,
processed and mouse input are on, but window input is off. Note
that no matter what console mode you are in, youll always receive
KEY_EVENT, FOCUS_EVENT, and MENU_EVENT messages.
In practice, youll rarely need to change these modes. While turning
off the default input modes for high-level input gives you much
ner control over console input, you can achieve the same effect
simply by using the low-level input functions. And, as we are about
to see, its a simple task to override the default CC handler to
do what you want.

Console Control Handlers


One of the key elements often mentioned when discussing console
input modes is the consoles control handler. This handler is a function that Windows calls when certain system-level actions take place
that could affect the console. Windows installs an empty handler that
simply calls ExitProcess when the console is created. To install your
own, use this API call:

On Language
ag isnt specied, any child processes of a console process belong
to the same group. Console applications can then send signals to a
specied process group, using the following API call:
function GenerateConsoleCtrlEvent(dwCtrlEvent: DWORD;
dwProcessGroupId: DWORD): BOOL; stdcall;

This function will send the specied signal to the console window
associated with the calling process. Any other applications that are
both attached to that console and part of the given process group will
receive this signal. This can be used by a parent process to control the
behavior of multiple child processes at once.
Figure 5: The control-handlers example program at run time.

function SetConsoleCtrlHandler(
HandlerRoutine: TFNHandlerRoutine; Add: BOOL):
BOOL; stdcall;

The HandlerRoutine is a pointer to a function that accepts a single


DWORD parameter, and returns a BOOL value, called with the stdcall
calling convention. The function adds or removes (depending on the
value of the Add parameter) your function from the list of registered
handlers. In Windows NT/2000, you can also add and remove NULL
as a HandlerRoutine. This special handler is designed specically to
ignore CC; otherwise it passes control to the default handler.
Each handler is called, in the reverse order they were registered,
whenever a system event occurs. The list of events your handler may
receive in its single DWORD parameter are:
 CTRL_C_EVENT User pressed CC.
 CTRL_BREAK_EVENT User pressed Cak.
 CTRL_CLOSE_EVENT User attempted to close the console
window.
 CTRL_LOGOFF_EVENT User attempted to log off the
system.
 CTRL_SHUTDOWN_EVENT User attempted to shut
down windows.
Each handler has the opportunity to handle the system events on its
own, or pass them along the chain. If the control handler believes it
has successfully handled the event, it should return True. This will
abort the processing of the event. Otherwise, it should return False to
pass control to the next handler in the chain.

The sample program in Listing Two only deals with the more basic
behavior of signal handlers. (Its shown at run time in Figure 5.) We
simply install a pair of control handlers and have them demonstrate
the effect of the return values on Windows behavior. As you can see by
the behavior of this sample, if none of the other handlers return True
to indicate theyve handled a given signal, the internal default handler
is still executed last. Its behavior is to simply kill the process whenever
any of the control signals are received. If this isnt the behavior you
want, you must make sure to install a control handler that returns True
as soon as possible after your program begins executing.

Conclusion
In this installment of the console programming series, we went over
the advanced features console applications provide to programmers.
Weve now covered nearly every aspect of console programming and
the console APIs, and observed how to take very low-level control of
the console and its associated buffers.
However, console programming is most effective when used with
the rest of the Windows API. In the nal article in this series,
next month, well examine how to use other common Windows
programming techniques in conjunction with console applications.
In particular, well examine how to use a console within a program
thread, create a message queue that a GUI-less application can use,
use consoles and graphical windows in the same application, and
replace the standard input and output handles with handles of our
own, including other le handles and anonymous pipes, to perform
I/O redirection from within a Windows application.
The sample programs referenced in this article are available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2001\SEP\DI200109ME.

The last three events, the close, logoff, and shutdown messages,
exhibit special behavior depending on the results of the handler
functions. If all of the handler functions return False, the process
will exit as soon as the last handler is done. However, if any of the
handler functions return True, Windows will instead prompt the user
to conrm that the process should be shut down. The user then
has the option to terminate the process immediately, or prevent the
process (and in turn, Windows itself ) from shutting down. Also, if a
handler function takes too long to return during one of these events,
Windows will automatically give the user the option to close the
process, cancel the shutdown, or continue to wait.
When several applications are attached to the same console, each of
them receives the signals sent to that console window, and each of
their handlers has a chance to process that signal for its own process.
Additionally, you can collect multiple console applications as part of a
console process group. This is done when creating a new console process with CreateProcess. If the CREATE_NEW_PROCESS_GROUP
13 September 2001 Delphi Informant Magazine

Mike Edenfield is an applications developer for Sylvan Learning Systems in


Baltimore, MD, and an MCSD. He has five years experience with Delphi and Visual
Basic, and specializes in Microsoft SQL Server development. He can be contacted at
Michael.Edenfield@educate.com.

On Language
Begin Listing One ScreenBuffers Program
program ScreenBuffers;
uses
Windows, SysUtils;
var
hInput, hOriginal, hSecond, hActive: THandle;
arrInputRecs : array[0..9] of TInputRecord;
dwCount, dwCur : DWORD;
bQuit : Boolean = False;
coorNew : TCoord = (X:100; Y:100);
rectView: TSmallRect =
(Left:0; Top:0; Right:99; Bottom:49);
const
OUTPUT_STRING =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()[]{}';
FOREGROUND_MAGENTA = FOREGROUND_RED or FOREGROUND_BLUE;
FOREGROUND_BR_MAGENTA =
FOREGROUND_MAGENTA or FOREGROUND_INTENSITY;
procedure WriteToScreen(buf: String);
var
dwCount: DWORD;
begin
buf := buf + #13#10;
WriteConsole(hActive, PChar(@buf[1]), Length(buf),
dwCount, nil);
end;
procedure FillOutputBuffer(hBuf: THandle);
var
dwAttr, dwCur: DWORD;
begin
dwAttr := 0;
for dwCur := 0 to 100 do begin
SetConsoleTextAttribute(hSecond, dwAttr + 1);
WriteConsole(hSecond, PChar(@OUTPUT_STRING[1]),
Length(OUTPUT_STRING), dwCount, nil);
dwAttr := (dwAttr + 1) mod 15;
end;
Writeln('Secondary buffer populated with data.');
end;
procedure ClearOutputBuffer(hBuf: THandle);
var
cbsi: TConsoleScreenBufferInfo;
coorClear: TCoord;
dwWritten: DWORD;
cFill: Char;
begin
GetConsoleScreenBufferInfo(hBuf, cbsi);
coorClear.X := 0;
coorClear.Y := 0;
cFill := ' ';
FillConsoleOutputCharacter(hBuf, cFill,
cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten);
end;
procedure ScrollViewWindow(hBuf: THandle; bUp: Boolean);
var
rectNew: TSmallRect;
begin
if bUp then
rectNew.Top := -1
else
rectNew.Top := 1;
rectNew.Bottom := rectNew.Top;
rectNew.Left := 0;
rectNew.Right := 0;
SetConsoleWindowInfo(hBuf, False, rectNew);

14 September 2001 Delphi Informant Magazine

end;
procedure SwitchConsole;
begin
if hActive = hOriginal then
begin
SetConsoleActiveScreenBuffer(hSecond);
hActive := hSecond;
end
else
begin
SetConsoleActiveScreenBuffer(hOriginal);
hActive := hOriginal;
end;
end;
begin
{ First, release our existing console,
and make a new one. }
FreeConsole;
AllocConsole;
SetConsoleTitle('Screen Buffers Demo');
{ Get a handle to the auto-created input
and output buffers. }
hInput := GetStdHandle(STD_INPUT_HANDLE);
hOriginal := GetStdHandle(STD_OUTPUT_HANDLE);
hActive := hOriginal;
SetConsoleTextAttribute(hActive, FOREGROUND_BR_MAGENTA);
WriteLn('Got Standard Output Handle.');
{ Create a second screen buffer. }
hSecond := CreateConsoleScreenBuffer(GENERIC_READ or
GENERIC_WRITE, 0, nil, CONSOLE_TEXTMODE_BUFFER, nil);
{ *** Windows 95/98: Comment out from here... }
if not SetConsoleScreenBufferSize(hSecond, coorNew) then
WriteLn('error: SetConsoleScreenBufferSize() == ',
GetLastError)
else
WriteLn('Adjusted secondary screen buffer size.');
if not SetConsoleWindowInfo(hSecond, True, rectView) then
WriteLn('error: SetConsoleWindowInfo() == ',
GetLastError)
else
WriteLn('Adjusted secondary screen buffer window.');
{ *** ...to here. }
FillOutputBuffer(hSecond);
{ Process the input loop here. }
while not bQuit do begin
ReadConsoleInput(hInput, arrInputRecs[0], 10, dwCount);
for dwCur := 0 to dwCount - 1 do begin
case arrInputRecs[dwCur].EventType of
KEY_EVENT:
with arrInputRecs[dwCur].Event.KeyEvent do
if bKeyDown then
if (dwControlKeyState and
ENHANCED_KEY) > 9 then
case wVirtualKeyCode of
VK_UP
:
ScrollViewWindow(hActive, True);
VK_DOWN :
ScrollViewWindow(hActive, False);
end
else
case Ord(AsciiChar) of
13: SwitchConsole;
Ord('?'):
begin
WriteLn('Console commands: ');
WriteLn(' ?
- This Help Summary');
WriteLn(' C
- Clear secondary output buffer.');
WriteLn(' F
- Fill secondary output buffer.');
WriteLn(' Q
- Quit program.');
WriteLn(' <ent> - Toggle active screen buffer.');

On Language
WriteLn('
WriteLn('

<up> - Scroll screen buffer up.');


<dn> - Scroll screen buffer down.');
end;
Ord('C'): ClearOutputBuffer(hSecond);
Ord('F'): FillOutputBuffer(hSecond);
Ord('Q'): bQuit := True;
end; // case Ord(AsciiChar)...
end; // case arrInputRecs...
end; // for dwCur...
end; // while not bQuit...
CloseHandle(hSecond);
FreeConsole;
end.

End Listing One

Begin Listing Two Control-handlers Program


{$APPTYPE CONSOLE}
uses
Windows;
var
bHandler1, bHandler2: Boolean;
nHandler1, nHandler2: Integer;
hStdOutput, hStdInput: THandle;
arrInputRecs: array[0..9] of TInputRecord;
dwCur, dwCount: DWORD;
cCur: Char;
const
FOREGROUND_BR_CYAN = FOREGROUND_BLUE + FOREGROUND_GREEN +
FOREGROUND_INTENSITY;
FOREGROUND_BR_RED = FOREGROUND_RED +
FOREGROUND_INTENSITY;
procedure ClearOutputBuffer;
var
cbsi: TConsoleScreenBufferInfo;
coorClear: TCoord;
dwWritten: DWORD;
cFill: Char;
begin
GetConsoleScreenBufferInfo(hstdOutput, cbsi);
coorClear.X := 0;
coorClear.Y := 0;
cFill := ' ';
FillConsoleOutputCharacter(hStdOutput, cFill,
cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten);
end;
procedure PaintScreen;
var
coorHome: TCoord;
begin
coorHome.X := 0; coorHome.Y := 0;
SetConsoleCursorPosition(hStdOutput, coorHome);
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
Write('<1> Control Handler 1 Status: ');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_RED);
if bHandler1 then
WriteLn('ON ')
else
WriteLn('OFF');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
Write('<2> Control Handler 2 Status: ');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_RED);
if bHandler2 then
WriteLn('ON ')
else

15 September 2001 Delphi Informant Magazine

WriteLn('OFF');
SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
WriteLn('Console Handler 1 has fired ',
nHandler1, ' times.');
WriteLn('Console Handler 2 has fired ',
nHandler2, ' times.');
end;
function Handler1(dwSignal: DWORD): BOOL; stdcall;
begin
Inc(nHandler1);
PaintScreen;
Result := bHandler1;
end;
function Handler2(dwSignal: DWORD): BOOL; stdcall;
begin
Inc(nHandler2);
PaintScreen;
Result := bHandler2;
end;
begin
hStdInput := GetStdHandle(STD_INPUT_HANDLE);
hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
{ Register them in reverse order so they
fire Handler1 then Handler2. }
SetConsoleCtrlHandler(@Handler2, True);
SetConsoleCtrlHandler(@Handler1, True);
ClearOutputBuffer;
PaintScreen;
while True do begin
ReadConsoleInput(hStdInput, arrInputRecs[0], 10,
dwCount);
for dwCur := 0 to dwCount - 1 do
case arrInputRecs[dwCur].EventType of
KEY_EVENT:
with arrInputRecs[dwCur].Event.KeyEvent do begin
cCur := AsciiChar;
if (not bKeyDown) and (cCur = '1') then
begin
bHandler1 := not bHandler1;
PaintScreen;
end;
if (not bKeydown) and (cCur = '2') then
begin
bHandler2 := not bHandler2;
PaintScreen;
end;
end;
end;
end;
end.

End Listing Two

In Development

Custom Key Bindings / Open Tools API / Delphi 5, 6 / Kylix

By Cary Jensen, Ph.D.

Custom Key Bindings


Modify the Delphi/Kylix Code Editor to Suit Your Tastes

here is a powerful feature of the Delphi and Kylix Code editor that permits you to
add your own custom keystrokes. This little-known feature is referred to as custom
key bindings, and is part of the Open Tools API. The Open Tools API (OTA) provides
a collection of classes and interfaces you can use to write your own extensions to
the Delphi and Kylix IDE.
This article gives you an overview of this interesting feature, and provides a simple key binding class
that you can use as a starting point for creating
your own custom key bindings. This key binding
class, which I am calling the duplicate line key
binding, makes a duplicate or copy of the current

line in the Code editor. This is a feature I have


had in other code editors, and now, through key
bindings, in Delphi and Kylix.
Another interesting thing about this key binding
is that the DupLine.pas unit, which you can download (see end of article for details), can be installed
and used in Delphi 5, Delphi 6, and Kylix.

Overview of Key Bindings


A key binding is a unit that can be installed into
a design-time package. Once installed, you can
enable or disable the key binding from the Key
Mappings page of the Editor Properties dialog box,
shown in Figure 1. You can display this dialog box
by selecting Tools | Editor Options from the main
menu of Delphi or Kylix.

Figure 1: The Key Mappings page of the Editor Properties dialog box.

16 September 2001 Delphi Informant Magazine

Writing editor key bindings involves creating


class type declarations and implementing interfaces. If youre unfamiliar with these topics, you
may still be able to create key bindings by following the example shown here (as well as those
that ship with Delphi and Kylix). On the other
hand, this might be a good time to explore the
power of object-oriented programming by reading the appropriate sections of the Delphi Developers Guide or the Kylix Developers Guide, which
are part of the documentation that ships with
the respective products.

In Development
IOTAKeyboardBinding = interface(IOTANotifier)
['{F8CAF8D7-D263-11D2-ABD8-00C04FB16FB3}']
function GetBindingType: TBindingType;
function GetDisplayName: string;
function GetName: string;
procedure BindKeyboard(
const BindingServices: IOTAKeyBindingServices);
property BindingType: TBindingType read GetBindingType;
property DisplayName: string read GetDisplayName;
property Name: string read GetName;
end;

Figure 2: The declaration of the IOTAKeyboardBinding interface


in the ToolsAPI unit.
type
TDupLineBinding = class(TNotifierObject,
IOTAKeyboardBinding)
private
public
procedure DupLine(const Context: IOTAKeyContext;
KeyCode: TShortcut;
var BindingResult: TKeyBindingResult);
{ IOTAKeyboardBinding }
function GetBindingType: TBindingType;
function GetDisplayName: string;
function GetName: string;
procedure BindKeyboard(const BindingServices:
IOTAKeyBindingServices);
end;

Figure 3: Type declaration of the TDupLineBinding class.

Creating and installing an editor key binding involves a number of


explicit steps:
1) Descend a new class from TNotierObject. This class must be
declared to implement the IOTAKeyboardBinding interface. This
class is your key binding.
2) In addition to the four methods of that IOTAKeyboardBinding
interface you must implement in your key binding class, add
one additional method for each key combination you want to
add to the editor. This method is passed an object that implements the IOTAKeyContext interface. Use this object within your
implementation to read about and control the behavior of the
editor.
3) Declare and implement a stand-alone Register procedure. Within
this procedure, invoke the AddKeyboardBinding method of the
BorlandIDEServices object, passing an instance of the class you
declared in the rst step as the only argument.
4) Add the unit that includes this Register procedure to an installed
design-time package.
Each of these steps is discussed in the following sections. As mentioned earlier, these steps will dene a new key binding that adds a
single key combination. Once implemented and installed, this key
combination will permit you to duplicate the current line in the
editor by pressing CD.

Declaring the Key Binding Class


The class that denes your key binding must descend from
TNotierObject and implement the IOTAKeyboardBinding interface. Those familiar with interfaces will recall that when a class
is declared to implement an interface, it must declare and implement all the methods of that interface. Consider the declaration of
the IOTAKeyboardBinding interface (see Figure 2), which appears
in the ToolsAPI unit.
17 September 2001 Delphi Informant Magazine

As you can see, this interface declares four methods and three
properties. Your key binding class must implement the methods.
Note, however, that it does not need to implement the properties.
(This is a regular source of confusion when it comes to interfaces,
but the fact is that the properties belong to the interface and
are not required by the implementing object. Sure, you can implement the properties in the object, but you dont have to. I didnt
in this example.)
In addition to the methods of the IOTAKeyboardBinding interface,
your key binding class must include one additional method for
each custom keystroke you want to add to the editor. To be
compatible with the AddKeyBinding method used to bind these
additional methods, the methods must be TKeyBindingProc type
methods. The following is the declaration of the TKeyBindingProc
method pointer type, as it appears in the ToolsAPI unit:
TKeyBindingProc = procedure (const Context: IOTAKeyContext;
KeyCode: TShortcut; var BindingResult: TKeyBindingResult)
of object;

This declaration indicates that the additional methods you write, each of
which adds a different keystroke combination to the editor, must take
three parameters: IOTAKeyContext, TShortcut, and TKeyBindingResult.
Figure 3 shows the key binding class declared in the DupLine.pas unit.
This class, named TDupLineBinding, includes only one new key binding.

Implementing the Interface


Once youve declared your key binding class, you must implement
the four methods of the IOTAKeyboardBinding interface, as well
as each of your additional TKeyBindingProc methods. Fortunately,
implementing the IOTAKeyboardBinding interface is easy.
Implement GetBindingType by returning the type of key binding that
youre creating. There are only two types of key bindings: partial and
complete.
A complete key binding denes all the keystrokes of the editor. You can
identify your key binding as a complete key binding by returning the
value btComplete. For example, the New IDE Emacs key mapping that
ships with Kylix and Delphi 6 is dened by a complete key binding.
Fortunately, Kylix ships with the source code for this key mapping,
which is quite useful if you want to examine a complicated key binding.
A partial key binding is used to add one or more keystrokes to the
key mapping youre using. The TDupLineBinding class is a partial
key binding. Heres the implementation of GetBindingType in the
TDupLineBinding class:
function TDupLineBinding.GetBindingType: TBindingType;
begin
Result := btPartial;
end;

You can implement GetDisplayName and GetName to provide the editor


with text descriptions of your key binding. GetDisplayName should
return an informative name that Kylix will display in the Enhancement
modules list on the Key Mappings page. On the other hand, GetName is
a unique string the editor uses internally to identify your key binding. By
convention, this name should be your company name or initials followed
by the name of your key binding, because this string must be unique for
all key bindings a user might install.

In Development
procedure TDupLineBinding.DupLine(
const Context: IOTAKeyContext; KeyCode: TShortcut;
var BindingResult: TKeyBindingResult);
var
ep: IOTAEditPosition;
eb: IOTAEditBlock;
r, c: Integer;
begin
try
ep := Context.EditBuffer.EditPosition;
ep.Save;
// Save current cursor position.
r := ep.Row;
c := ep.Column;
eb := Context.EditBuffer.EditBlock;
ep.MoveBOL;
eb.Reset;
eb.BeginBlock;
eb.Extend(EP.Row+1,1);
eb.EndBlock;
eb.Copy(False);
ep.MoveBOL;
ep.Paste;
// Restore cursor position.
ep.Move(r, c);
finally
ep.Restore;
end;
BindingResult := krHandled;
end;

Figure 4: Implementing TKeyBindingProc in the TDupLineBinding


class.

The following two functions implement the GetDisplayName and


GetName methods for the TDupLineBinding class:
function TDupLineBinding.GetDisplayName: string;
begin
Result := 'Duplicate Line Binding';
end;
function TDupLineBinding.GetName: string;
begin
Result := 'jdsi.dupline';
end;

You implement the BindKeyboard method to bind your TKeyBindingProc


methods. BindKeyboard is passed an object that implements the
IOTAKeyBindingServices interface; use this reference to invoke the
AddKeyBinding method.
AddKeyBinding requires at least three parameters. The rst is an array
of TShortcut references. A TShortcut is a Word type that represents
either a single keystroke, or a keystroke plus a combination of one
or more of the following: C, A, or S, as well as left-,
right-, middle-, and double-mouse clicks. Because this parameter can
include an array, its possible to bind your TKeyBindingProc to two or
more keystrokes or key combinations.
The Menus unit in Delphi, and the QMenus unit in Kylix, contain
a function named Shortcut that you can use to easily create your
TShortcut references. This function has the following syntax:
function Shortcut(Key: Word; Shift: TShiftState):
TShortcut;

where the rst parameter is the ANSI value of the keyboard character,
and the second is a set of zero, one, or more as TShiftState. The
18 September 2001 Delphi Informant Magazine

following is the declaration of TShiftState, as it appears in the Classes


unit:
TShiftState = set of (ssShift, ssAlt, ssCtrl,
ssLeft, ssRight, ssMiddle, ssDouble);

The second parameter of BindKeyboard is a reference to your


TKeyBindingProc method that implements the behavior you want to
associate with the keystroke or key combination, and the third parameter
is a pointer to a context. In the BindKeyboard implementation in the
TDupLineBinding class, the method DupLine is passed as the second
parameter, and nil is passed in the third parameter. The following is
the implementation of the BindKeyboard method that appears in the
DupLine.pas unit:
procedure TDupLineBinding.BindKeyboard(
const BindingServices: IOTAKeyBindingServices);
begin
BindingServices.AddKeyBinding(
[Shortcut(Ord('D'), [ssCtrl])], DupLine, nil);
end;

As you can see, this BindKeyboard implementation will associate the code
implemented in the DupLine method with the CD combination.

Implementing TKeyBindingProc Methods


Implementing the methods of IOTAKeyboardBindings is pretty straightforward. Implementing your TKeyBindingProc method, however, is not.
As you can see from the TKeyBindingProc method pointer type declaration shown earlier in this article, a TKeyBindingProc is passed
three parameters. The rst, and most important, is an object that
implements the IOTAKeyContext interface. This object is your direct
link to the editor, and you can use its properties to control cursor
position, block operations, and views. The second parameter is the
TShortcut that was used to invoke your method. This is useful if
you passed more than one TShortcut in the rst parameter of the
AddKeyBinding invocation especially if you want the behavior to
be different for different keystrokes or key combinations.
The nal parameter of your TKeyBindingProc method is a
TKeyBindingResult value passed by reference. Use this parameter to signal
to the editor what it should do after your method exits. The following is
the TKeyBindingResult declaration as it appears in the ToolsAPI unit:
TKeyBindingResult = (krUnhandled, krHandled, krNextProc);

Set the BindingResult formal parameter of your TKeyBindingProc method


to krHandled if your method has successfully executed its behavior. Setting BindingResult to krHandled also has the effect of preventing any
other key bindings from processing the key, as well as preventing menu
items assigned to the key combination from processing it.
Set BindingResult to krUnhandled if you dont process the keystroke or
key combination. If you set BindingResult to krUnhandled, the editor
will permit any other key bindings assigned to the keystroke or key
combination to process it, as well as any menu items associated with
the key combination.
Set BindingResult to krNextProc if you have handled the key, but want to
permit any other key bindings associated with the keystroke or key combination to trigger as well. Similar to setting BindingResult to krHandled,
setting BindingResult to krNextProc will have the effect of preventing
menu shortcuts from receiving the keystroke or key combination.

In Development
can help you learn how to implement your key bindings. The
code shown in Figure 4 implements TKeyBindingProc from the
TDupLineBinding class.

Figure 5: The Into new package page of the Install Component


dialog box.

As you can see from this code, the IOTAKeyContext implementing


object passed in the rst parameter is your handle to access a variety
of objects that you can use to create your response. Without a doubt,
its the EditBuffer property that is most useful. This property refers to
an object that implements the IOTAEditBuffer interface. Use this object
to obtain a reference to additional interface implementing objects,
including IOTABufferOptions, IOTAEditBlock, IOTAEditPosition, and
IOTAEditView implementing objects. These objects are available using
the BufferOptions, EditBlock, EditPosition, and TopView properties of
the EditBuffer property of the Context formal parameter.
Use the IOTABufferOptions object to read information about the
status of the editor, including the various settings that can be congured on the General page of the Editor Properties dialog box. The
IOTAEditBlock object permits you to control blocks of code in the
editor. Operations that you can perform on blocks include copying,
saving to le, growing or shrinking the block, deleting, etc. You can
use the TOTAEditPosition object to manage the insertion point or
cursor, e.g. determining the position of the insertion point, moving
it, inserting single characters, pasting a copied block, etc.

Figure 6: Installing the DupLine key binding into a package


named keybin.dpk.

As mentioned earlier, the real trick in implementing your


TKeyBindingProc method is associated with the object that implements the IOTAKeyContext interface that you receive in the
Context formal parameter. Unfortunately, Borland has published
almost no documentation about how to do this. In fact, I found
only four sources of information:
1) The rst is the declarations and somewhat intermittent comments located in the ToolsAPI unit. This is one of the rst places
you should look to understand the objects youll be able to work
with. You can nd this unit in the \Source\Toolsapi directory
where you installed Delphi (/source/toolsapi for Kylix).
2) Equally valuable is the source code for the New IDE Emacs
keymapping. As mentioned earlier, this is a complete key binding. You can nd this in the \Demos\ToolsAPI\Keybindings\
Emacs directory where you installed Delphi 6 (demos/toolsapi/
keybindings/emacs for Kylix). While the units that make up
the New IDE Emacs key mapping have almost no comments,
you can get a very good idea about how to work with the
Context parameter.
3) The last two resources are useful if you have a copy of Delphi
handy. Specically, Delphi includes a fairly small partial key
binding that you can examine. That key binding can be found
in the \Demos\ToolsAPI\Editor Keybinding directory, where
either Delphi 5 or Delphi 6 is installed.
4) Finally, Allen Bauer, a member of the Tools API development
team for Delphi and Kylix R&D, has published a Wizard that
generates a key binding. This Wizard is available for download
from http://community.borland.com/. Be aware, however, that
this Wizard generates more than just a key bindings class.
In other words, it is more complicated than a key binding,
and therefore you probably shouldnt start your search for
information about implementing key bindings with the generated code.
A full discussion of the properties of IOTAKeyContent is well
beyond the scope of this article. The resources referred to here
19 September 2001 Delphi Informant Magazine

Finally, you use the TOTAEditView object to get information


about, and to some extent control the various editor windows. For example, you can use this object to determine how many
units are open, scroll individual windows, make a given window
active, and get, set, and go to bookmarks.
Turning our attention back to the DupLine method, the code
begins by assigning the IOTAEditPosition implementing object to
an IOTAEditPosition reference. While this isnt an essential step, it
simplies the code in this method, reducing the need for repeated
references to Context.EditBuffer.EditPosition. The IOTAEditPosition
variable (ep) is then used to save a reference to the current position
of the cursor. Next, an IOTAEditBlock variable (eb) is assigned the
object referenced by the Context.EditBuffer.EditBlock property. As
with the ep variable, this is done to reduce the amount of typing
(as well as to avoid using a with clause, which could be pretty
confusing since many of these objects have similar or identical
property names).
Using these two references, the cursor position is moved to the rst
column of the current line. A new block is then started, extended
one line, and copied. Next, the cursor is repositioned at the beginning of the line and the block is pasted. Returning the cursor to
its original position completes the operation, which is signaled by
setting the BindResult formal parameter to krHandled. The result
is that the current line is duplicated without the cursor appearing
to move in the editor.

The Register Procedure


For your key binding to be installed successfully into the
editor, you must register it with a design-time package
using a Register procedure. The Register procedures name is
case-sensitive and must be forward-declared in the interface
section of the unit that will be installed into the design-time
package. Furthermore, you must add an invocation of the
IOTAKeyBindingServices.AddKeyboardBinding method to the
implementation of this procedure, passing an instance of your
key binding class as the sole parameter.
Delphi Informant Magazine September 2001 19

In Development

Figure 7: The Package editor.

4) Set the Package file name to the name you want to give your
package. In Figure 6, this le is named keybin.dpk, and is
being stored in the \Lib directory where Delphi 6 is installed.
(In Linux, a good place to store this le is the hidden .Borland
directory, located in the users home directory.) Also provide
a brief description for the package in the Package description
eld.
5) Click OK. A dialog box is displayed to inform you that it will
build the package and install it.
6) Click Yes. After creating, compiling, and installing the package, you will see a dialog box conrming that the package is
installed.
7) Click OK. The installed package now appears in the Package
editor, as shown in Figure 7.
8) Close the Package editor, making sure to click Yes when it asks
if you want to save changes to the package project.
Your key binding is now available. It will now appear in the Key
Mappings page of the Editor Properties dialog box, as shown in
Figure 8. Your installed key binding is now available to all editor
key mappings. If you want to disable your key binding, uncheck
the check box that appears next to its name in the Enhancement
modules list box.
Note: If you inspect Figure 6, you will see that I stored
DupLine.pas in the Borland\components directory. This permits
me to easily share components between multiple versions of
Delphi, and also to re-install a version of Delphi and delete its
directory without losing my custom components. To use this directory, however, I had to add it to the Library path list on the Library
page of the Environment Options dialog box. Display this dialog
box by selecting Tools | Environment Options.

Conclusion

Figure 8: The newly installed key binding appears on the Key


Mappings page of the Editor Properties dialog box.

Invoke this method by dynamically binding the BorlandIDEServices


reference to the IOTAKeyboardServices interface, and pass an invocation of your key binding objects constructor as the argument. The
following is how the Register procedure implementation appears in
the DupLine unit:

As youve learned in this article, key bindings are part of the


Open Tools API that permit you to add custom keystrokes to the
Delphi/Kylix Code editor. The Open Tools API is just one of many
reasons that Delphi and Kylix are the best tools on the market.
The le referenced in this article is available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\SEP\
DI200109CJ.

procedure Register;
begin
(BorlandIDEServices as IOTAKeyboardServices).
AddKeyboardBinding(TDupLineBinding.Create);
end;

Creating and Installing a New Design-time Package


The nal step in dening a new key binding is to add the unit in
which you register your key binding to a new design-time package.
To do this, use the following steps in either Delphi or Kylix:
1) Save any changes youve made to your key binding unit.
2) Select Component | Install Component to display the Install
Component dialog box shown in Figure 5.
3) On the Into new package page, use the Browse button associated with the Unit file name eld to select the unit in which
your key bindings Register procedure appears. In this example,
the unit name is DupLine.pas.
20 September 2001 Delphi Informant Magazine

Cary Jensen is president of Jensen Data Systems, Inc., a Houston-based training and consulting company. He is the author of 18 books, including
Building Kylix Applications (Osborne/McGraw-Hill), Oracle JDeveloper
(Oracle Press), and Delphi In Depth (Osborne/McGraw-Hill). Cary
is also Contributing Editor to Delphi Informant Magazine, and an internationally respected trainer and speaker. For information about Carys
on-site training, public Delphi and Kylix seminars, and consulting
services, please visit http://www.jensendatasystems.com, or e-mail Cary at
cjensen@jensendatasystems.com.

Columns & Rows

Microsoft SQL Server 2000 / XML / Microsoft XML Parser / Delphi 5, 6

By Alex Fedorov

Using the XML Features of SQL


Server 2000
Part II: Querying Techniques

elphi has a long history of database support, from the BDE in Delphi 1 and subsequent
versions, to the ADOExpress components that implement the ADO- and OLE DB-based
data access in Delphi 5 and 6. Now, with the XML features of Microsoft SQL Server 2000,
we can talk about a third generation of data access in Delphi one thats compatible
with main IT trends.
Last month, you were introduced to the new features of Microsoft SQL Server 2000 that make it
an XML-enabled database server, e.g. the ability
to access SQL Servers using the HTTP protocol,
support for XDR (XML-Data Reduced) schemas,
the ability to specify XPath queries against these
schemas, and the ability to retrieve and write XML
data. You also learned that XML-based data extraction is implemented in SQL Server 2000 by the
FOR XML clause thats part of the SELECT statement. The ability to query Microsoft SQL Server
directly without extra data access components
and receive data represented as XML documents
opens new possibilities for Delphi developers.
To parse XML documents generated by querying
SQL Server, well naturally need a parser. And since
were using a Microsoft database, the best choice
is the Microsoft XML Parser, which provides a lot
of objects, methods, and properties to manipulate
XML documents. So well begin this months discussion by describing how Delphi interacts with the
Microsoft XML Parser, before moving on to the
Old Name

New Name

Where

type
implementation
type
var
type
type

type_
implementation_
type_
var_
type_
type_

IXMLDOMNode.nodeType
IXMLDOMDocument
IXMLDOMDocument.createNode
IXMLDOMSchemaCollection.add
IXMLElement
IXMLElement2

Figure 1: Delphi changes some method, property, and parameter


names when it imports the Microsoft XML Parser type library.

21 September 2001 Delphi Informant Magazine

main topic of how to query SQL Server 2000 for


XML data from Delphi.

Delphi and the Microsoft XML Parser


Before we can use the Microsoft XML Parser, we
need to create an interface unit that will encapsulate all objects and methods implemented in this
COM object. To do so, select Project | Import Type
Library in the Delphi IDE. Then, in the Import
Type Library dialog box, select the Microsoft
XML type library. The version you have installed
on your computer depends on the software you
have, but its recommended to have the latest version of the Microsoft XML Parser thats available
free from the Microsoft Web site. As of this writing, the most recent version is 3.0.
After selecting the type library, uncheck the
Generate Component Wrapper check box, and click
Create Unit. The SXML2_TLB.PAS le will be
created and added to your project. Later, youll
need to save it. The usual place for imported type
library interface units is the \Lib folder (under
\Delphi5 for example).
Its always wise to take a look at the generated Pascal
code, especially when youre familiar with the objects
and methods of the COM object, or youre planning
to port the existing code to Delphi. At the top of
the le in the commented Errors section, we can
nd the changes in method, property, and parameter
names performed by Delphi. In our case, we should
be aware of the changes shown in Figure 1.

Columns & Rows


var
HTTP
: IXMLHTTPRequest;
XMLDoc : IXMLDomDocument;

var
HTTP
: IXMLHTTPRequest;
XMLDoc : IXMLDomDocument;

...

...

XMLDoc := CoDOMDocument.Create;
HTTP
:= CoXMLHTTP.Create;
// Issue POST HTTP request to the URL.
with HTTP do begin
Open('POST', URL, False, '', '');
Send('');
// ResponseXML now holds resulting XML document.
XMLDoc.Load(ResponseXML);
end;

Figure 2: Calling the Open and Send methods of the


XMLHTTPRequest object.

After putting the reference to the MSXML2_TLB.PAS unit in the uses


clause of our main form unit, were ready to use the Microsoft XML
Parser in our Delphi code.

Using URL-based Queries


There are a lot of ways to invoke the Web server from Delphi. We can
use the Win32 API functions, third-party libraries or components, or
the Microsoft XML Parsers built-in functionality. Since were planning
to deal with XML documents, it makes sense to use just one COM
object to do the entire job.
The Microsoft XML Parser provides at least two ways to invoke the
Web server via an HTTP protocol. The rst is to use the generic
XMLHTTPRequest object, and its Open and Send methods (see Figure 2).
Note that the XMLHTTPRequest object gives us a lot of freedom to
specify the type of request POST, GET, or even a WebDAV- or
SOAP-specic request. And, as you can see from the code in Figure 2,
its straightforward to use.
The second way to invoke the Web server via an HTTP protocol is
to use the Load method of the XMLDOMDocument object, specifying
the URL as the parameter. This method is smart enough to recognize
the local and remote resources. It also has the built-in ability to access
resources specied by the URL. In this case, the code to load the
remote XML document is shown in Figure 3.
Now lets look at the URL itself. It should consist of two parts: the
address of the Web server (along with the name of the virtual directory), and the SQL query were planning to execute. Thats why we
create the URL in the code below by combining the URL and SQL
strings. Note that the StringReplace function is used to convert
spaces to the %20 escape sequence, because theyre recognized as
a valid part of the URL:
URL
SQL
SQL
URL

:=
:=
:=
:=

'http://terra/northwind?sql=';
'SELECT * FROM Employees';
SQL + XMLMethod;
URL + StringReplace(
SQL, ' ', '%20', [rfReplaceAll]);

URL := URL + '&root=Northwind';

Weve also used the XMLMethod variable, which species one of the
FOR XML modes:
 FOR XML RAW
 FOR XML AUTO
 FOR XML AUTO, ELEMENTS
22 September 2001 Delphi Informant Magazine

XMLDoc := CoDOMDocument.Create;
// Load remote XML document asynchronously.
XMLDoc.Async := False;
XMLDoc.Load(URL);

Figure 3: Loading the remote XML document.

// Generate URL for specified XML method.


function TForm1.SetXMLMethod(XMLMethod: string): string;
var
URL : string;
SQL : string;
begin
URL := 'http://terra/northwind?sql=';
SQL := 'SELECT FirstName, LastName, Title, Notes ' +
'FROM Employees';
SQL := SQL + ' ' + XMLMethod;
URL := URL + StringReplace(SQL, ' ', '%20',
[rfReplaceAll]);
URL := URL + '&root=Northwind';
Edit1.Text := URL;
SetXMLMethod := URL;
end;

Figure 4: The XMLMethod variable is used to specify one of the


FOR XML methods.

// Issue the FOR XML RAW request.


procedure TForm1.Button1Click(Sender: TObject);
begin
XMLDoc.Async := False;
XMLDoc.Load(SetXMLMethod('FOR XML RAW'));
Memo1.Text := XMLDoc.XML;
end;
// Issue the FOR XML AUTO request.
procedure TForm1.Button2Click(Sender: TObject);
begin
XMLDoc.Async := False;
XMLDoc.Load(SetXMLMethod('FOR XML AUTO'));
Memo1.Text := XMLDoc.XML;
end;
// Issue the FOR XML AUTO, ELEMENTS request.
procedure TForm1.Button3Click(Sender: TObject);
begin
XMLDoc.Async := False;
XMLDoc.Load(SetXMLMethod('FOR XML AUTO, ELEMENTS'));
Memo1.Text := XMLDoc.XML;
end;

Figure 5: OnClick event handlers for the different FOR XML


modes.

The code is wrapped in the SetXMLMethod function, shown in Figure 4.


Now we can put together a quick and dirty demonstration program.
Place three Button components, one Edit component, and one Memo
component on the new form. Each button will use one of the FOR
XML modes. Figure 5 shows the OnClick event handlers for each.
By clicking on one of the buttons, we issue a URL request to SQL
Server, receive it in the XMLDoc object, and show the resulting XML
document in the Memo component. Figure 6 shows the result of a
Delphi Informant Magazine September 2001 22

Columns & Rows

Figure 6: Result of a FOR XML AUTO, ELEMENTS query.


// Get column names.
if Attribs <> nil then
begin
for I := 0 to Attribs.Length-1 do begin
CI
:= ListView1.Columns.Add;
CI.Width
:= -1;
CI.Caption := Attribs.Item[I].NodeName;
CI.AutoSize := True;
end;
// Add row data.
for I := 0 to Root.ChildNodes.Length-1 do begin
Attribs
:= Root.ChildNodes[I].Attributes;
LI
:= ListView1.Items.Add;
LI.Caption := Attribs[0].Text;
for J := 1 to Attribs.Length-1 do
with LI do
SubItems.Add(Attribs[J].Text);
end;
end;

Figure 8: The results of the code shown in Figure 7.


procedure TForm1.ListView1Click(Sender: TObject);
var
Item : TListItem;
begin
Item := (Sender As TListView).Selected;
Memo1.Text := Root.ChildNodes[Item.Index].XML;
end;

Figure 9: Display the XML representation for one row of data.

Figure 7: Extracting column names and data for each row.

FOR XML AUTO, ELEMENTS query. The generated URL is shown


in the Edit component above the Memo.
As you can see from using the XML property of the XMLDOMDocument
object, we can easily access the entire XML document. Lets look at how
we can use the XML document in our Delphi applications.

Using RAW Mode


As we already learned last month, in the RAW mode we receive the
XML document, where each row of the resulting recordset is stored
with the generic <row /> element, and each column is mapped to
an attribute of this element. The attribute name equals the name
of the column.
Lets see how to process this document and display its contents in a
ListView component. First, load the XML document that results from
the following query:
SELECT * FROM Customers FOR XML RAW

Then take the root node of the XML document, and access its attributes node:
Root
:= XMLDoc.DocumentElement;
Attribs := Root.FirstChild.Attributes;

23 September 2001 Delphi Informant Magazine

Figure 10: One row of data is shown in the Memo component.

Next, extract the tables column names. To do so, iterate the Attribs
collection (of the IXMLDOMNamedNodeMap type) of the rst child
node of the XML document, and extract the names of each node in
this collection (see the rst for loop in Figure 7).
Now we can show the data. This is done with a similar technique, as
shown in the second for loop in Figure 7, but this time we iterate all
child nodes of the XML document. The result of our manipulations
is shown on another example form, containing a Button and ListView
component (see Figure 8).
To study the resulting XML representation for just one row of data, lets
add a Memo component to the form. Then enter the code shown in
Figure 9 for the OnClick event handler for the ListView component.
Now, when we run our example and click on a row, well receive its
XML representation, as shown in Figure 10.

Columns & Rows


// Show first row.
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Index := 0;
ShowOneNode(0);
end;
// Show previous row.
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
if Index > 0 then Dec(Index);
ShowOneNode(Index);
end;

Figure 11: A simple one-row browser.


procedure TForm1.Button1Click(Sender: TObject);
var
URL, SQL : string;
begin
XMLDoc := CoDOMDocument.Create;
URL := 'http://terra/northwind?sql=';
SQL := 'SELECT CompanyName, ContactTitle, ';
SQL := SQL + 'ContactName, Phone FROM Customers';
SQL := SQL + ' FOR XML RAW';
URL := URL + StringReplace(SQL, ' ', '%20',
[rfReplaceAll]);
URL := URL + '&root=Northwind';
XMLDoc.Async := False;
XMLDoc.Load(URL);
Root := XMLDoc.DocumentElement;
Nodes := Root.SelectNodes('//row');
Index := 0;
ShowOneNode(Index);
end;

Figure 12: Loading the one-row browser.

procedure TForm1.ShowOneNode(I: Integer);


var
Node : IXMLDOMNode;
begin
Node := Nodes[I];
Edit1.Text := Node.Attributes[0].Text;
Edit2.Text := Node.Attributes[1].Text;
Edit3.Text := Node.Attributes[2].Text;
Edit4.Text := Node.Attributes[3].Text;
end;

Figure 13: The ShowOneNode procedure.

This was a very simple example of how to show XML-based data in


Delphi applications. Weve assumed that all records have columns lled
with information, but if we look at some of the records, well nd that
not all of the columns are represented in the resulting XML document.
This is because when some columns have Null as their content, they
arent presented in the XML document.

// Show next row.


procedure TForm1.SpeedButton3Click(Sender: TObject);
begin
if Index < Nodes.Length-1 then Inc(Index);
ShowOneNode(Index)
end;
// Show last row.
procedure TForm1.SpeedButton4Click(Sender: TObject);
begin
Index := Nodes.Length-1;
ShowOneNode(Nodes.Length-1);
end;

Figure 14: Code for navigating rows.

After this the rst node is shown using the ShowOneNode procedure
(see Figure 13).
The code for the four SpeedButtons one for the rst row, the
previous row, the next row, and the last row is shown in Figure 14.

Conclusion
Thats it for this month. Weve seen how to extract data from Microsoft
SQL Server 2000 via its XML features. These features allow us to
extract data without using any data access components by directly
specifying our SQL query, and using the HTTP protocol to talk with
the SQL Server. We also saw several examples of how to represent the
extracted data in Delphi applications by working with the Microsoft
XML Document Object Model (DOM).
Next month, well examine data querying techniques for two more
FOR XML modes (AUTO and EXPLICIT), and demonstrate how to
use XML templates to separate our Delphi code from the XML-based
queries. See you then.
The projects referenced in this article are available on the Delphi Informant Magazine Complete Works CD located in INFORM\2001\SEP\
DI200109AF.

Getting One Row at a Time


In this example well create a simple browser that will allow us
to look at the contents of the XML document from a one row
point of view. Place a Button component, four Edit components,
and four SpeedButton components onto a form (see Figure 11).
Then, in the OnClick event handler for the Button, enter the
code shown in Figure 12.
Here, we extract the CompanyName, ContactTitle, ContactName, and
Phone columns from the Customers table, and create a list of nodes.
24 September 2001 Delphi Informant Magazine

Alex Fedorov is a Chief Technology Officer for Netface SA, based in Lausanne,
Switzerland (http://www.netface.ch). He was one of the co-authors of Professional Active Server Pages 2.0 (Wrox, 1998) and ASP Programmers Reference (Wrox, 1998), as well as Advanced Delphi Developers Guide to ADO
(Wordware, 2000).

Delphi Informant Magazine September 2001 24

Kylix Tech

Linux / Kylix / fork / execv / Environment Variables

By Brian Burton

Kylix Program Launcher


Finding and Executing Linux Programs

inux applications written in Kylix often require shared libraries to be installed


alongside the application. Launching a Linux application in a way that allows it to load
these libraries, without requiring technical finesse from the user, can be challenging.
This article describes a pure Kylix alternative to using
shell scripts to launch an application. In the process,
it demonstrates a simple use of some of the Linux
system calls in the Libc unit.

its shared libraries somewhere inside the applications


installation directory. The simplest method is to
install the executable, and all the shared libraries,
directly into the applications directory.

The Problem: Finding the Libraries

When a program requires shared libraries installed


outside of /usr/lib, users generally need to set an environment variable, LD_LIBRARY_PATH, to include
these directories. LD_LIBRARY_PATH is a colonseparated list of directory names for the Linux loader
to search when looking for shared libraries.

Applications typically install their shared libraries in


one of these locations:
 A standard system directory thats automatically
searched by the system for shared libraries, such
as /usr/lib.
 The directory containing the applications executable.
 A directory named lib under the applications
directory. With this option the applications executables are generally stored in a directory named
bin under the applications root directory.
Copying the shared libraries into the /usr/lib directory
has the advantage of simplicity. Once the library has
been installed, all users can launch the application
there without adjusting any of their environment variables. However, there are also some potential drawbacks:
 Only the root user is allowed to copy les into
/usr/lib. Often, users other than the root will
want to install and use the application without
involving a system administrator.
 Other applications that install libraries into /usr/
lib might overwrite the applications shared libraries with incompatible versions.
To allow users other than the root to install and
update your application, the best option is to install
25 September 2001 Delphi Informant Magazine

For example, suppose your application is called Foobler and is installed in the directory /usr/local/
Foobler. Further suppose that the application uses
several shared libraries that are all installed in the
Foobler directory. Any user that wants to run
Foobler will need to modify his or her shell init le
(.prole for bash or ksh, .login for csh) to dene
LD_LIBRARY_PATH with the Foobler directory. If
the users are using bash (the default shell for new
accounts on many Linux distributions) they will need
to add the following to their .prole le:
LD_LIBRARY_PATH=/usr/local/Foobler:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH

Updating the .prole le for every new application


can be tedious, and difcult to explain to new Linux
users. Many applications (including Kylix itself) work
around this problem by installing a launcher script
that adjusts environment variables before starting the
application. The primary tasks for a launcher program
are to:

Kylix Tech
const
APP_SUFFIX = '.bin';
procedure FindTarget(var target_name, target_dir: string);
var
my_name : string;
begin
my_name := ExpandFileName(ParamStr(0));
target_name := my_name + APP_SUFFIX;
target_dir := ExtractFileDir(my_name);
end;

Figure 1: Identifying the application directory.


procedure SetPath(path_var: string; target_dir: string);
var
path : string;
begin
path := GetEnvironmentVariable(path_var);
if (Length(path) > 0) then
path := target_dir + ':' + path;
else
path := target_dir;
setenv(PChar(path_var), PChar(path), 1);
end;

Figure 2: Updating the PATH and LD_LIBRARY_PATH environment variables.





identify the location of the application and its libraries;


set environment variables required to launch the application;
launch the application with the customized environment and pass
along all the arguments provided on its command line.

The following sections present an alternative launcher written in Kylix.

Finding the Application Root


The sample launcher program presented here assumes that the application to be launched is installed in the same directory as the
launcher, and has the same le name as the launcher, except for the
addition of a .bin extension. Likewise, the shared libraries used by
the application are assumed to be located in the launchers directory.
Given these assumptions, the launcher can easily identify the application directory using the ParamStr and ExpandFileName functions
from the System unit, as shown in Figure 1.
The .bin extension was chosen for this example simply to distinguish the name of the target from the name of the launcher. Linux
doesnt have a standard convention for executable le names;
any extension could be used. However, use of .exe should be
avoided, because that extension could confuse users into thinking
the program is a Windows executable rather than one for Linux.

Updating the Environment


Once the application directory has been discovered, the PATH and
LD_LIBRARY_PATH environment variables can be updated within
the launchers own environment using the GetEnvironment function
from the System unit, and the setenv procedure from the Libc unit,
as shown in Figure 2.
Adding the application directory to the PATH environment variable
isnt strictly necessary. Doing so, however, allows the application to
more easily launch other executables in its own directory.

Launching the Application


Finding the program to launch and updating the environment both
26 September 2001 Delphi Informant Magazine

procedure CopyArgStr(var dest: PChar; source: string);


var
len : Cardinal;
begin
len := Length(source) + 1;
GetMem(dest, len);
StrLCopy(dest, PChar(source), len);
end;
procedure ExecTarget(target_name: string);
type
parray = array[0..0] of PChar;
pparray = ^parray;
var
argv_size : size_t;
argv : pparray;
begin
argv_size := (ArgCount + 1) * SizeOf(PChar);
GetMem(argv, argv_size);
memcpy(argv, ArgValues, argv_size);
CopyArgStr(argv[0], target_name);
execv(PChar(target_name), PPChar(argv));
end;

Figure 3: Launching an application: copying the ArgValues


array, and calling execv.

relied mostly on routines from the System unit. However, launching the
application involves a call to a Linux system call located in the Kylix
Libc unit. The system call, execve, allows a program to recreate itself as
another program.
On UNIX-based systems such as Linux, every running process has a
parent process. Therefore, starting a new program generally involves
two system calls. First the fork system call is used to create a new child
process. The child created by fork is a nearly exact replica of the parent
process that created it. After the fork call, both the parent and child
processes have nearly identical memory images, and execution of each
process continues at the instruction immediately following the call to
fork. Usually the fork call is made in a conditional statement that allows
the parent to continue processing normally, while the child performs a
few housekeeping chores and then calls the execve system call.
The execve system allows a process to completely recreate itself as an
instance of a different program. When execve is called, the Linux kernel
identies the executable le to be executed and replaces the running process current memory image with one appropriate to the new program. In
a sense, the process is transformed from one program to another.
Theres no need to call fork in the case of the launcher program. The
launcher can simply transform itself into an instance of the application
once the environment is initialized.
The Libc unit provides a family of related procedures for invoking
the execve system call. Before calling execve, each procedure accepts
slightly different parameters and performs some special processing.
The launcher example uses the execv procedure, which accepts the
absolute path to the executable le to launch and the complete set of
command-line arguments for the program.
The Kylix System unit exposes the command-line arguments to the
application as the platform-specic global variables, ArgCount and
ArgValues. ArgCount contains the number of command-line arguments in ArgValues. ArgValues is a pointer to an array of PChars with
ArgValues+1 elements. The rst element of the array contains the path
to the executable le of the running program. The last element of the

Kylix Tech
var
target_name : string;
target_dir : string;
begin
FindTarget(target_name, target_dir);
SetPath('LD_LIBRARY_PATH', target_dir);
SetPath('PATH', target_dir);
ExecTarget(target_name);
{ We only reach this point if execv fails. }
Writeln(ErrOutput, 'error: unable to execute ' +
target_name);
Writeln(ErrOutput, Format(
'code=%d msg=%s', [errno, strerror(errno)]));
end.

Figure 4: The main code block of the launcher program.

array is a nil pointer to indicate the end of the array. The elements
in between (if any) are the additional command-line arguments used
to launch the program.
To launch the application, the sample launcher simply copies its own
ArgValues array (see Figure 3) and changes the rst element to refer
to the program being launched. Then it calls execv to actually launch
the program. Replacing the rst element in ArgValues isnt strictly
necessary, but by doing so the launched application can easily identify
its own executable le, and is completely oblivious to the fact that it
was started by the launcher, rather than the users shell.

Putting It All Together


The main code block for the launcher program simply calls the
procedures for each step in the launch process and reports an error
if the launch fails (see Figure 4). The execv procedure only returns to
the caller if the exec failed. In that situation the global variable errno
holds an integer error code describing the reason for the failure. If
this happens, the sample launcher uses the strerror function from the
Libc unit to produce a message describing the error.

Possible Improvements
The sample launcher could be extended in any number of ways to
make it more useful for a given application. For example, if the application stores its executable les in a bin directory, and its shared libraries
in a lib directory, the launcher could be modied to set the PATH
and LD_LIBRARY_PATH to different values, thus:
SetPath('LD_LIBRARY_PATH', target_dir + '../lib');
SetPath('PATH', target_dir);

The launcher could also be enhanced to set other environment variables needed by the application. For example, Kylix sets the variables
HHHOME and XPPATH in its kylixpath script.
The les referenced in this article are available on the Delphi
Informant Magazine Complete Works CD located in INFORM\2001\
SEP\DI200109BB.

Brian Burton is a software developer and consultant specializing in distributed


systems using object-oriented languages such as Kylix, Java, and C++. He provides
custom intranet and Internet solutions for clients of all sizes, including Fortune 500
companies. You can e-mail him at bburton@burton-computer.com.
27 September 2001 Delphi Informant Magazine

Greater Delphi
COM / Delphi 3-6

By Alessandro Federici

Introduction to COM
The Basic Building Blocks of Windows Development

few months ago, while looking for my first house, I spent quite a lot of time
with a real estate agent. This taught me the three most important aspects of that
business: location, location, location. With the Component Object Model (COM), the
reverse is true; location is ideally the last thing in which you are interested. Instead,
the mantra is integration, integration, integration.
Every Windows user, whether aware of it or not,
deals with COM every day. COM is used by
Microsoft Ofce when we run the spell-check utility, by many Web sites running IIS, and by the
operating system for some of its mundane tasks.
Some developers choose COM specically to build
complex, scalable, and secure enterprise systems.

Integration Yesterday
In our eld, integration can be accomplished in
many ways, and it can be applied to many things.
Imagine you are developing a word processor application. You will create or inherit a custom memo
control for editing purposes, you may include a
spell checker, and if you want to get fancy
you may also want to include a set of custom
routines that allows your users to convert the docu-

ment to HTML or RTF. The integration of all these


parts will constitute your word processor.
Now, imagine that you want to be able to update
any of these single elements without redeploying the
whole application. You may also want to make those
components available to a second application perhaps developed by someone else in another language.
These are common scenarios. Todays applications are
much bigger than those of a few years ago, so anything that can help tame this complexity is welcome.
The rst approach you may try is to use DLLs.
Dynamic-link libraries allow us to bind to and
invoke executable code at run time. In our hypothetical word processor, the DLL that includes
the conversion routines might export routines and
structures such as these:
function GetConverters: TConverterList;
procedure Convert(anID: Integer;
aDocument, aFileName: string);
TConverter = record
ID : Integer;
Name, FileExtension, Description: string;
end;

Any time you want to add a converter, youd


only have to update the DLL and the word processor would automatically have access to the
new functionality.
Figure 1: Delphis Type Library editor allows us to view and edit
COM-type libraries.
28 September 2001 Delphi Informant Magazine

This works ne and you will achieve your objective. GetConverters returns a custom TList (TCon-

Greater Delphi
To recap, here are some of the problems that COM solves:
Object-oriented language independence
Dynamic linking
Location independence





Object-oriented Language Independence


The DLL example discussed earlier has a serious problem: for functions to return an object, the client has to know its interface. An
interface is a very important concept in object-oriented programming and COM. An interface is the declaration of all the public
methods and properties of a class.
The pointer in the DLL example (to TConverterList) could be a pointer
to anything as far as the compiler is concerned. Without the interface, the
compiler wouldnt know how to nd the correct method addresses, the
parameters, and result types, etc.

Figure 2: Delphi uses type library information to generate interfaces it can use.

verterList) which may have some Delphi methods that make it handy
and easy to use.
Unfortunately this isnt an optimal solution. Worse, it doesnt work unless
youre using Delphi or Borland C++Builder. To use the result of the
function GetConverters pointer as a TConverterList, the client needs to
know what a TConverterList is. To do this, you need to share that
information. Even if you do, however, youd have a problem with nonBorland compilers. TList is a VCL class. Its not included, for instance, in
Microsoft Visual C++ or Visual Basic; developers in those environments
couldnt benet from the pointer we return.
You could have structured your DLL differently, following, for example, the approach of the Windows API function, EnumWindows,
which takes a pointer to a callback routine. Another solution would
have been to export more functions. Whichever approach you may
choose, however, youd still be conned to a world of simple data
types that is everything but object-oriented. On top of that, the DLL
has to be run on the clients computer.

Integration Today
COM is one of the technologies that helps us resolve some of these
issues. COM has a long story. Ofcially, the acronym COM was rst
used around 1993. We can trace COM roots back to Windows 3.x
where DDE and OLE were used in Microsoft Word and Excel
as a sort of rudimentary communication and interoperability glue.
Today COM is everywhere on the Windows platform. Small applications such as ICQ, CuteFTP, or Allaire HomeSite, are accessible
through COM. Application suites such as Microsoft Ofce are based
on COM. Windows-based enterprise systems leverage COM and
Microsoft Transaction Server for business-critical operations. If you
develop on Windows, you will have to face COM sooner or later. The
sooner you do, the easier it will be.
This article is about understanding COM and the reasons its important, rather than providing another how-to tutorial. Well start with
the principles behind COM, then provide a concrete example you
can download and examine for yourself (see end of article for
details). The rst part wont take long, but will give you a better
understanding of what happens in the example and why.
29 September 2001 Delphi Informant Magazine

The concept of an interface is at the heart of COM. To be language


independent, COM denes a binary standard for interface denition
and introduces the concept of type libraries. Type libraries are binary
les that contain information about a number of interfaces, i.e. how
many and what they look like.
Lets take a look. In Delphi, open the COMConverter.tlb type library
contained in the \COMConverter directory (created by the example
application). As you can see in Figure 1, this type library denes the
IConverterList interface, which contains the Count and Items properties.
Delphi knows how to interpret type libraries, and present them in a
user-friendly fashion via its Type Library editor. Microsoft Visual Basic,
Borland C++Builder, or Microsoft Visual C++ do the same. They all
support the COM binary standard and play according to its rules. Now,
from within the Type Library editor, press @. Delphi will create a le
named COMConverter_TLB.pas (see Figure 2).
Through COM, I can dene an interface that other COM-enabled
languages can understand. Delphi will use that information to generate
interfaces that it can understand and use, as we just saw. Its all there and
ready to be used, as if it were a regular Delphi object. There are some key
differences, but lets continue with the principles.

Dynamic Linking
Similar to DLLs, COM allows and actually only works through
dynamic linking. You can choose to take advantage of this in two ways:
early binding or late binding.
Before we continue, you need to register your COM library. Registration
is the process through which Windows becomes aware of a COM object
and learns how to instantiate it. To do this, you need to use a special tool
called REGSVR32.EXE (contained in Windows\System32), or Borlands
equivalent tregsvr.exe (in \Delphi5\Bin for example). Another way, if you
have the Delphi source code, is to open the COM project (in our case
COMConverter.dpr) and select Run | Register ActiveX Server.
Registering a COM server means inserting special keys into the
Windows registry. The information you will store includes the
name of the DLL or EXE le that hosts your COM object, the
identiers that uniquely identify it (see the yellow on green code
in Figure 2) and a few other items. If you dont do this, Windows
wont be able to instantiate your COM object.
Continuing our analogy to DLLs, early binding is similar to importing routines from a DLL by using the external directive. When you

Greater Delphi
do that, you embed the denition of those routines in your client,
and you expect them to match that denition exactly when you
connect to them at run time. If the name, parameters, or result type
are changed, youll have an error as soon as the application starts.
Late binding is similar to the GetProcAddress API call in which you
specify the name of the function you want to connect to using a
string, and are returned a pointer. When you do that, your client runs
ne, unless you try to use the function with the wrong parameters.
Invoking methods of a COM object through early binding is
faster than using late binding. Every time you use late binding,
youre asking Windows to look for a method called with a certain
name, return a pointer to it, and then invoke it. Using early
binding means you immediately call it, without any additional
overhead, because you already know the location of that methods
entry point. However, using late binding allows much more exibility, and makes possible things such as scripting.
As an example, this is the content of the VBTest.vbs le contained
in the example applications \WordProcessor directory:
Dim MyObj, i, s
Set MyObj = CreateObject("COMConverter.ConverterList")
s = ""
For i = 0 To (MyObj.Count-1)
s = s & MyObj.Items(i).Description & ", "
Next
MsgBox("You can save as " & s)

Double-click on it and see what happens. Our ConverterList


object will be created and the names of all supported converters
will be displayed, all without Delphi, VB, or anything else. This
is done using a late-bound call to the methods Get_Items and
Get_Count. The ActiveX Scripting engine embedded in Windows
(which is also accessible through COM) took care of parsing the
text le, and asking to nd and invoke it.
You can do the same in Delphi, but how can you make sure youre
using one instead of the other? Thats easy. The way to do late
binding in Delphi is generally by using OleVariant variables. By
using typed variables, youre using early binding. Figure 3 shows a
snippet of code from the fMainForm unit in the \WordProcessor
directory. As you can see, the only differences between the two are
the type of myobj and the way its instantiated.

implementation
uses ComObj;
{ $R *.DFM }
procedure TForm1.bLateBindingClick(Sender: TObject);
var myobj: OleVariant;
begin
myobj := CreateOLEObject('COMCOnverter.ConverterList');
ShowMessage('There are ' + IntToStr(myobj.Count) +
' converters available');
end;
procedure TForm1.bEarlyBindingClick(Sender: TObject);
var
myobj : IConverterList;
begin
myobj := CoConverterList.Create;
ShowMessage('There are ' + IntToStr(myobj.Count) +
' converters available');
end;

Figure 3: Use an OleVariant variable for late binding, and a


typed variable for early binding.

We have seen how COM lets us use objects embedded in DLLs.


Wouldnt it be nice if, on top of that, those DLLs could be located
and executed on a more powerful machine? Wouldnt it be nice not to
have to worry about TCP/IP communication and sockets? This is all
possible using Distributed COM (DCOM).
DCOM is an extension of COM that allows inter-process communication across machine boundaries. Using DCOM, the only
thing that changes for the developer is the way you instantiate
your COM object. Instead of calling CoCreate, you would now call
CoCreateRemote(<ServerName>), passing either an IP address or the
name of the machine that executes the COM object.
When you do this, Windows creates an object (proxy) on the
client machine that looks exactly like the real object. When you
call a method on the proxy, it takes care of delivering your call
and the parameters you specied to the other machine where a
listener (stub) is waiting. The stub then invokes the real method
and packages back the result. All this is done transparently for
you. Codewise, the only difference for you is to specify CoCreate
or CoCreateRemote when creating your COM object.

Conclusion

The fact that you declared myobj as an OleVariant is the key here.
That tells Delphi how you invoke the methods of a COM object.
Any time you use an OleVariant you can specify any method name.
The compiler wont complain. Try putting myobj.XYZ in the rst event
handler. It will compile successfully, but at run time Delphi will raise
an exception as soon as you hit that line of code. In the second
case, you wouldnt be able to compile it, because IConverterList doesnt
dene a method named XYZ.

COM is an excellent technology for developing exible, expandable,


and open Windows applications. It denes a standard object-oriented
way of exposing functionality and promotes integration. By embracing COM you can make your application more open, expandable,
and controllable (whenever needed) from the outside world. You will
have access to a wide set of tools and functionality embedded in your
operating system, and other applications such as Microsoft Ofce,
which will enhance the functionality you can provide.

Location Independence

If you need to develop enterprise systems, youll be able to leverage


your investment in this technology and get access to another set of
tools and servers (e.g. Microsoft Transaction Server, BizTalk, Application Center) that wont require a switch in language or approach.

Not too many years ago the terms distributed and thin client
became very popular. The two terms are often used together when
discussing systems physically split into presentation, business, and data
storage tiers (multi-tier or three-tier systems). Physically split means
that each of those tiers can be running on the same machine, or
on separate ones. In brief, multi-tier architecture was conceived to
produce cleaner designs and enhance scalability. However, its a topic
unto itself and outside the scope of this article.
30 September 2001 Delphi Informant Magazine

If you werent familiar with COM, I hope this article provided some
interesting information to get you started. If you are already using
COM, I hope it helped you understand a little better why COM
exists and when you can benet from using it.

Greater Delphi
The demonstration applications referenced in this article are available
on the Delphi Informant Magazine Complete Works CD located in
INFORM\2001\SEP\DI200109CM.

Resources
If you want to read more, I recommend the following introductory books:
 Understanding COM+ by David S. Platt, ISBN: 0-7356-0666-8,
Microsoft Press, 1999
 Inside COM (Programming Series) by Dale Rogerson, ISBN:
1-57231-349-8, Microsoft Press, 1996
 COM and DCOM: Microsofts Vision for Distributed Objects by
Roger Sessions, ISBN: 0-471-19381-X, John Wiley & Sons, 1997
You can also nd more information online at:
Microsofts COM pages at http://www.microsoft.com/com
Binh Lys Web site at http://www.techvanguards.com
Dan Misers Distribucon site at http://www.distribucon.com
Deborah Pates rudimentary home page at http://
www.djpate.freeserve.co.uk






Originally from Milan, Alessandro Federici is a recent Chicago transplant. Hes been
developing software since he can remember and has used Borland tools since
Turbo Pascal 3.02. Hes spent the last four years working extensively with the
technologies included under the Microsoft Windows DNA umbrella (COM, DCOM,
COM+, MTS, MSMQ, ASP, IISS, SQL Server, etc.), and developing distributed
systems with InterBase and Oracle as well. Hes worked as a consultant and
has been the Localization Software Engineer on a number of Microsoft projects
(Milan and Dublin). In the US, hes worked as a developer and system architect
for companies in the financial, warehousing, and supply chain management businesses. He is a system architect and team leader for the development division of
eCubix. You can contact Alex at alef@bigfoot.com, or through his Web site at
http://www.msdelphi.com.

31 September 2001 Delphi Informant Magazine

Sound+Vision
DirectX / DirectDraw / Bitmaps / Delphi 5

By Brian Phillips

DirectDraw Delphi
Strategies for Working with Large Bitmaps

icrosofts DirectDraw is a proven technology, but requires some know-how to


make it perform as advertised. As with most things, it fails when stressed beyond
the parameters it was designed to.
As far as general game programming goes, it does
quite well, but when used for many other elds
such as scientic visualization, simulation, and
other highly graphics-intensive efforts, it can and
will fail under certain circumstances. This article
discusses techniques used to overcome the limitations within the DirectDraw API, and details a
sample implementation.

Background
One of the more critical limitations to DirectDraw
involves the use of large bitmaps as image sources.
Bitmap sizes play a big role in whether DirectDraw
will function at all for certain applications. A
DirectDraw surface wont be created if it violates
the size constraints enforced by DirectX. While
displaying large bitmaps is usually fairly easy, doing

so with the speed of DirectDraw and with no


visible lag or page-ip time is another matter.
This doesnt mean that DirectDraw isnt a suitable
solution for the presentation of large bitmaps. Many
intense scientic and GIS applications require the
same level of graphics and speed as most games on
the market today. For many applications that require
fast image display, DirectDraw is one of the best
solutions with such complex and critical data.
The solution to getting a large bitmap to display
quickly and navigate smoothly is based on making
the problem smaller. Since the display of smaller
surfaces using DirectDraw surfaces is a fairly manageable task, the main goal is to reduce the problem size
to one that DirectDraw can handle. The details of
that reduction can be a little challenging to navigate,
so the techniques discussed in this article were chosen
for clarity rather than to optimize execution speed.
There are several techniques from 2D graphics handling that were combined to generate this sample
code. These are double buffering, culling, divide
and conquer, and navigation. Theres no single technique that supplies the power needed for a successful
approach. Only by combining them can we overcome the image size problem, and provide smooth
display and navigation.

Sample Code

Figure 1: This satellite image of the United States is simply too big for DirectDraw.

32 September 2001 Delphi Informant Magazine

The sample code included here used a 65MB satellite picture of the United States (see Figure 1)
as a test image. DirectDraw refused to create a surface this size. This discussion is about how to load
and display these large images without running into
DirectDraw roadblocks and do it in a way that
preserves the smooth navigation of the source image
that large images inherently need.

Sound+Vision
TQuadSurface = class(TQuadSurfaceFace)
private
NE, NW, SE, SW: TQuadSurface;
bounds: TRect;
procedure SetUpPic(var r: TRect; b: TBitmap;
lpdd: IDIRECTDRAW7);
function GetSurface(var r: TRect; lpdd: IDIRECTDRAW7;
b: TBitmap): IDirectDrawSurface7;
protected
surface: IDirectDrawSurface7;
public
procedure DrawOnSurfaceRect(var frontArea: TRect;
backArea: TRect; target: IDirectDrawSurface7);
override;
function getSurfaceRect: TRect;
function getWidth: Integer; override;
function getHeight: Integer; override;
procedure getRect(var r: TRect); override;
function Init(lpdd: IDIRECTDRAW7; File_Name: string):
Boolean; override;
function InitWithZoom(lpdd: IDIRECTDRAW7;
File_Name: string; Scale: Double): Boolean;
overload;
function InitWithZoom(lpdd: IDIRECTDRAW7; map: TBitmap;
Scale: Double): Boolean; overload;
constructor Create; virtual;
destructor Destroy; override;
end;

Figure 2: Declaration for TQuadSurface.

Basic knowledge of setting up a DirectDraw surface is required to


execute the included sample code. The concepts and techniques
used arent specic to DirectDraw, and may be applied to the
general problem of managing large 2D images. The example code
isnt in full screen mode so the examples can be applied to other
2D problems more easily.
The sample code was developed with Delphi 5 on a 500MHz Pentium
III processor using 128MB of RAM, running Microsoft Windows 98.
When running on a Windows NT Workstation or server, the code must
be altered to use the correct DirectX version supported by the platform.
Recall that only DirectX 3 is supported on Windows NT.

Double Buffering
Smooth, icker-free image display is achieved by using double buffering.
Double buffering is performing all drawing actions on a back surface,
then using a single fast copy to move the image from the back buffer
to the screen. Since the screen isnt required to update when every draw
command goes through, its signicantly smoother to the eye.
Most DirectDraw enthusiasts will recognize the ip function as double
buffering. In this example code, I avoid using ip in order to show a more
generic version that can work outside DirectX. Also, most scientic and
business applications dont use full screen mode, since word processors,
math packages, etc. are usually being used alongside graphic displays.

Culling
Since the smoothness of graphic display is a function of the amount of
work done between paints, the workload must be intelligently managed.
Work that isnt required for a specic screen shot needs to be culled.
The rule here is simple: dont do any work that isnt necessary. This can
be much more challenging than simply fullling all graphics work that
needs to be done, but leads directly to increased speed and efciency.
The sample code accomplishes this by using a quad-tree structure. Each
branch of the quad-tree understands how to cull itself, and to nd lower
33 September 2001 Delphi Informant Magazine

function TForm1.showDirectDraw: Boolean;


var
halfx, halfy: Integer;
myDC: HDC;
begin
frontrect := GetFrontRect;
if bInit = False then begin
Result := False;
Exit;
end;
{ Give the viewer of the demo app an idea how big
the screen area is. }
CAPTION := getClipperString;
{ Find the center of the viewing screen area. Scale it
for zooming purposes. Then create the area that
should be sampled. }
halfx := trunc(((
frontrect.right - frontrect.left) * scale) / 2);
halfy := trunc(((
frontrect.bottom - frontrect.top) * scale) / 2);
backrect.top := focus.Y - halfy;
backrect.bottom := focus.Y + halfy;
backrect.left := focus.X - halfx;
backrect.right := focus.X + halfx;
if lpDDSBack = nil then begin
// Just for safety, we check the back buffer.
Result := False;
Exit;
end;
// Transfer information from the qsurface tree onto
// the back buffer.
if (qSurface <> nil) then
qSurface.drawOnSurfaceRECT(
frontrect, backrect, lpDDSBack);
lpDDSBack.GetDC(myDC);
PolyLine(myDC, lastpts, 4);
lpDDSBack.ReleaseDC(myDC);
// Swap from the back buffer to the front buffer.
lpDDSPrimary.Blt(@frontrect, lpDDSBack, @frontrect,
DDBLT_WAIT, nil);
Result := True;
end;

Figure 3: The showDirectDraw method.

branches that t within the drawing zone. Some would argue that using
a quad-tree presents extra work, and that direct-array access is more
efcient. However, the quad-tree structure offers good performance and
is easy to understand in sample code, so its used here.

Divide and Conquer


Breaking a large image up into smaller pieces will solve the DirectDraw
surface problem. A large image can be recursively divided into smaller
ones until theyre of acceptable size. Doing this in a way that simplies
the culling and double-buffering operations can be challenging. Fortunately, using a quad-tree data structure will make both operations much
easier. Iterating through the bitmap, breaking it into quads, and assigning
the regions to them as data structures, will break the image down nicely.
Its important to note that breaking the bitmap into smaller manageable
sizes will x the display problem by itself. Unfortunately, the drawing
performance will be dismal. The amount of work required to show the
image carries huge performance penalties, and only by double buffering
and intelligently culling out the subsections of the tree that dont need to
be redrawn can acceptable performance be achieved.
The reverse is also true when zoom factors are considered. The
divide-and-conquer approach involves dividing up the source
image into many smaller images and displaying them. When
implementing a zoom function, large delays can occur (also known

Sound+Vision
Example Form

ROOT

NE

SE

SW

The example application will set up a DirectDraw surface, and navigate


the background image based on a focus point. [The application is available for download; see end of article for details.] All image manipulation
takes place in the TQuadSurface object (see Figure 2). The only duties
for the form are to respond to navigation, to instantiate the DirectDraw
objects, to initialize the variables, and to actually call for a rendering.

NW

NW

NE

NW

NE

NW

NE

NW

NE

SE

SW

SE

SW

SE

SW

SE

SW

Figure 4: A quad-tree structure: oval nodes have no DirectDraw


surface; square nodes have a DirectDraw surface.

as frame dragging) as a result of many sub-images being rendered


to the screen area. The solution to that problem is a level-of-detail
strategy thats outside the scope of this article.

Navigation
Once a large bitmap is loaded into memory, it still must be displayed to the screen. Since the bitmap is fairly large, a direct copy
to the screen would need excessive processor time. DirectDraw
surfaces support culling, and letting DirectDraw use its native
clipping boundaries would work, but there are easy ways to cull
out unneeded pixel transfers a bit faster.
By nding the intersections of the area that will be displayed on the
screen with the new, small bitmap regions, the proper areas of the
DirectDraw surfaces can be dened and copied onto the back buffer. The
search within the quad-tree structure will be fairly quick, and eliminate a
full search of all the possible rectangles.
To ll the back buffer with the proper subsection of the image, two
rectangles are needed. The rst denes the coordinates of the screen area
where the image will be displayed. The back buffer needs to be at least
large enough to encompass that area. Making the back buffer equal in
size to the entire screen area is an easy solution to this.
The second rectangle is the area to sample from the original
large bitmap. Dening a focus point (usually x and y) within the
bounded area of the original image is an easy way to keep track of
where the view is located. As long as the focus is conned to the
bitmap, a visible transfer will occur.
There will be occasions when the focus will reveal the edges of the
bitmap. In those situations it may be prudent to dene a perimeter
zone surrounding the bitmap where the focus wont be allowed. This
zone (usually half of the display area width on each side, and half
the display area height for top and bottom) will stop corners from
overlapping on the screen.
During the sampling operations, its important to remember that
the original bitmap that was sampled is no longer in memory.
Instead, it has been broken into smaller pieces and copied onto
individual DirectDraw surfaces. The pieces in each leaf of the
containing tree still have the bounds property that was assigned
during the split, which dened their original coordinates within
the original image. These bounds rectangles will be used to sample
the leaf surfaces and create the back-buffered image.
34 September 2001 Delphi Informant Magazine

Initial set up of the DirectDraw surfaces isnt complex. A primary


surface and a back surface are created. Both of these surfaces have
the dimensions of the window. The TQuadSurface is created and
handed a le name to process. When its nished processing, the
image values, such as focus point and scale, are computed.
When the user resizes the form, all the previous assumptions about where
to render the image are no longer valid. The FormResize method updates
the rectangle that will be rendered on the screen. Anticipating user input
is difcult. A user could resize or move the form so that portions of the
visible area overlap the boundaries of the TQuadSurface and the original
bitmap. When this happens, areas of black will creep into the display
area. To prevent this, FormResize changes the scale and focus point for
the purpose of keeping the entire image within the view area. Special
care must be taken to reset the DirectDrawClipper objects properly after a
form resize, to prevent accidental clipping within the display boundaries.
To render the bitmap onto the screen, showDirectDraw nds the area to
be sampled, and passes it along with the back buffer to the TQuadSurface
that holds the bitmap information (see Figure 3). The TQuadSurface lls
the back buffer with the needed image. The Blt method is called to get
the back buffer image onto the screen. When not in full screen mode, call
showDirectDraw from the OnPaint event handler.
Many users who prefer the faster, full-screen DirectDraw mode will
no doubt prefer to ip the surfaces, rather than blitting from one to
the other. There are special techniques applicable to full-screen mode
graphics that arent addressed directly in this article.
There are also various utility functions used in the sample code.
GetFrontRect, for example, returns a rectangle of the visible form
area in screen coordinates. Also getClipperString and getRectString
return strings for debugging and viewing purposes.

TQuadSurface and Setting up the Bitmap


The sample code implements a form implementation for a user interface.
It also supplies the back buffer for quick double-buffered blitting between
surfaces. The divide-and-conquer and culling techniques are implemented in a separate object owned by the front form. The TQuadSurface
encapsulates all of the image processing and rendering techniques into a
recursively dened object tree.
The TQuadSurface class implements the divide-and-conquer algorithm onto an arbitrary bitmap, and stores the subsections into
a tree for quick retrieval. It recursively samples each part of the
bitmap, and when the proper size is reached, stores the image onto
a DirectDraw surface at a leaf node. Recall that leaf nodes are
nodes with no children. In this data structure, the only nodes that
have reference to DirectDraw surfaces are the leaf nodes. Branch
nodes only contain the bounding information used for nding the
proper leaf nodes.
Each node of the TQuadSurface has reference to four children
nodes (see Figure 4). These are the nodes that make up the
northeast, northwest, southeast, and southwest areas of the rect-

Sound+Vision
angle. Also, each node has a bounds property that represents the
rectangle from which the node was sampled in the original bitmap
image. Leaf and branch nodes all have bounds properties.
TQuadSurface also has a reference to an IDirectDrawSurface interface.
This interface isnt created except at the leaf nodes, where it holds the
small panels that make up the entire image. Creating the TQuadSurface
initiates all of the surface references to nil. No image work is actually
performed until its Init method (shown in Figure 5) is called.
The form unit calls Init to set up the bitmap into memory. It
accepts two parameters: a DirectDraw interface capable of creating
surfaces, and the bitmap le name to load into the tree. The bounds
properties becomes equal to the area of the bitmap being loaded.
Once the root node bounds properties are set, the recursion is ready
to begin. SetUpPic is called from Init to recursively construct the
tree. The unneeded source image is freed when nished.
SetUpPic accepts a TRect area initially equal to the bounds of the
source bitmap, and a reference to the IDirectDraw passed into
Init. The recursion begins by checking if the rectangular area is
small enough to satisfy the maximum size acceptable (the sample
code uses a 256x256 pixel size, but it could improve performance
to dene a larger area). If the area is small enough to fulll the
needed maximum size, GetSurface is called to instantiate a reference to a DirectDraw surface. The recursion then stops for that
leaf, and returns control to the calling object.
If the rectangle is larger than the maximum size needed to create
a DirectDraw surface, it must be broken into four parts. The
midpoints of the rectangle are found, along with rounding information. Rounding operations are crucial in these types of applications; they can generate strange lines or offsets within the bitmap.
The easy solution used here is to let the rectangles overlap by a
single pixel.

function TQuadSurface.Init(lpdd: IDIRECTDRAW7;


File_Name: string): Boolean;
var
b: tbitmap;
begin
Result := True;
// Make sure the file is there.
if not (fileExists(file_name)) then begin
Result := False;
Exit;
end;
// Create a bitmap to load in the file.
b := tbitmap.Create;
b.LoadFromFile(File_Name);
// Initialize the root node to the bitmap area.
bounds.left := 0;
bounds.right := b.width;
bounds.top := 0;
bounds.bottom := b.height;
// SetUpPic sets up the tree structure, recursively
// breaking the bitmap into smaller pieces.
SetUpPic(bounds, b, lpdd);
// Finished with the original image; it isn't needed.
b.free;
end;

Figure 5: The TQuadSurface.Init method.

be computed. This is done through a series of ratios. These ratio


operations could be turned into a series of trigonometric function
calls, but theyre included in their raw form in the sample code.
The general problem is that not all the surface really needs to be
copied. Only the part of the image that actually intersects is necessary. The DirectDrawClipper references attached to the surfaces will
clean up any overlap, but often its easier to do it within the code.

Four new TQuadSurface objects are created from the area. Each
calls its own SetUpPic with the four new areas generated by the
bounds of our new regions. This continues until the subdivided
rectangles are within the bounds of the maximum size, where they
call GetSurface and return to their calling object.

The ratio approach to solve this is based on nding a point expressed


as a fraction along a line segment. A point along a line segment can
be expressed as a ratio between 0 and 1, multiplied by the length
of the line segment referenced from the lower bound of the line
segment. For example: Take line 1 that starts at 10 and ends at 30.
Point20 can be expressed as 0.5*(length of the line)+ the lower corner
of the line. Since the length of the line is 30-10, the point 20 can also
be expressed as 0.5*(30-10) +10=20.

GetSurface is the function that actually samples the rectangle of


interest from the source bitmap. It rst creates a new reference
to the IDirectDrawSurface from the clients IDirectDraw that was
passed into the Init function. Then it uses a few Windows API
calls such as BitBlt to move the bits from the source image onto
the destination IDirectDrawSurface. It returns the new surface
reference to the calling function.

The trick comes when projecting that point onto another line. It
can be done easily by multiplying the ratio across the new basis and
adding any offset. For example: suppose the value of 20 on line 10
to 30 needs to be transformed to a line that starts at value 100
and ends at value 200. In order that the point 20 (from 10 to 30)
be transformed so its new line is from 100 to 200, the same ratio
operation takes place that took place previously.

Remember that when the form wants to display a portion of the


image, it passes a back buffer to be lled with a subsection of the
original image. To copy a sub-area of the original bitmap onto the
back buffer, DrawOnSurfaceRect is called. This uses the same recursion methodology that the SetUpPic method used earlier. It checks
for an intersection between the current nodes bounds, and the area
that should be sampled from the original area of the sampled image.
If an intersection is found, then the child nodes are recursively
called until the leaf branches that contain an intersecting bounds
area and an instantiated IDirectDrawSurface are found.

Let Point20 equal the value of 20 on a line from 10 to 30, when


projected onto a line form 100 to 200 is equal to the ratio multiplied
by the line length, and then add the lower coordinate of the line:

Once the intersection area of the leaf node is found, the area that needs
to be copied from the leaf nodes surface onto the back buffer must

This applies directly to what DrawOnSurfaceRect does. An intersection rectangle is found from the bounds of the leaf node and the

35 September 2001 Delphi Informant Magazine

Point20 = 0.5 (length of line 10 to 30) + the lower bound of 10.


When the line is transformed to 100 to 200, Point20 maintains its
ratio of 0.5 and is now multiplied by the new line magnitude of
(200-100) and the lower bound of 100 is added to that. Therefore,
the transformed Point20 = 0.5*(200-100)+100 =150.

Sound+Vision
back area that must be lled. The frontArea variable represents the
transformation of the backArea onto the screen. Since they have the
same width-to-height ratio, a transformation along both axes will
appear to zoom in or out, but not skew. Therefore, the coordinates
found in reference to the backArea can be transformed using the
above math into coordinates useful for the screen rendering from the
back buffer. For claritys sake, the example code does this without
trigonometric functions.
The DirectDrawSurface can now copy bits from the intersected
area (in its own surface area, not the bounds area) onto the back
buffer area dened by the transformed intersection rectangle. Once
the data transfer is nished, it is up to the function that called
DrawOnSurfaceRect to determine what to do with the back buffer
that has been lled. Usually it is simply ipped or blitted onto the
primary surface for display.

Other Issues
There are a few issues to consider when using this technique. The size
of the bitmap must be of a proper ratio. For instance, if the bitmap
were two bytes wide and 10000 bytes tall, dividing it up into quads
would cause a problem there would be zero sized branches and
leaves. A good rule of thumb would be to not let the smaller size be
less than the log2 of the larger size.
Zooming is also an issue. The TQuadSurface supports zooming by
changing the size ratios of the input screen and back buffer sizes. This
method becomes inefcient as the amount of information transferred
to the back buffer increases. Noticeable lags can occur in operations
as zoom scales reach 1/4 or less. This is just because the amount of
work needed is growing as the zoom out occurs.
There are ways to get around this. The usual method is to shrink
the original bitmap to a more manageable level and rebuild the
tree. Or, if the system isnt memory-constrained, a better solution
might be to have three or more TQuadSurface objects, each one for
a different level of detail, and a broker object that understands how
to change the back rectangle needed into the proper operations in
the correct TQuadSurface.

Conclusion
By breaking up an image into smaller parts, the limitations of the
DirectDraw surface size can be overcome quickly. When reassembling
the image, special care must be taken to adequately render the proper
sections of several of the new smaller sub-images correctly. Rounding
errors can occur, and the use of ratios can result in a zoom capability.
By confronting the image management problem, DirectDraw
becomes a very usable feature for processing and displaying large
images. These techniques arent only usable for DirectDraw, but for
any two-dimensional raster image processing.
The demonstration project referenced in this article is available on the
Delphi Informant Magazine Complete Works CD located in INFORM\
2001\SEP\DI200109BP.

Brian Phillips is a Modeling and Simulation Software Engineer at the SAIC


Extended Aerospace Defense Lab. He can be reached at brian.j.phillips@saic.com.

36 September 2001 Delphi Informant Magazine

New & Used


By Ron Loewy

ExpressBars Suite 4
The New Look with Much Less Work

ay you need to write a Windows application. You know, the old-fashioned kind.
Not a Web server, not a MIDAS server in the logic layer of an n-tier application,
nothing exotic and exciting like wireless services, just good old-fashioned windows,
menus, dialog boxes, and the like.
Ah! thats easy you say. Let me show you how I
create a new form here, drop a couple of controls,
add a menu component, throw in a panel, align
it to the top so it will be a toolbar, add a status
bar, and were ready to rock and roll.
A menu-bar component set. Who needs it?
Delphi already ships with MainMenu and PopupMenu components. Theres a StatusBar component for well the status bar, ToolBar and
CoolBar for the toolbars, or the granddaddy of

Figure 1: ExpressBars SideBar and toolbars in action,


emulating the Internet Explorer-style user interface.
37 September 2001 Delphi Informant Magazine

them all, a Panel aligned to the correct corner of


the window, BitBtns and SpeedButtons, and if
you want to be fancy an action list.
So far so good. And if you want to be even fancier,
Delphi now has docking support. So whats stopping
you from writing world-class Windows applications?
Then you open one of these fancy applications
the boys in Redmond keep writing, with the
cool gradient color bars on the left side of the
menus, or the Outlook SideBar with the groups
and the items, or the customizable toolbars and
menus that Ofce offers. Suddenly you realize
your application could be a lot cooler. Nothing
stops you from adding these features; youre a star
programmer. Everything in Delphi is customizable with tons of properties and events, etc. So
before you know it, two weeks have passed and
you wrote all this cool code for the colors, the
customization, the groups, and the docking, but
you missed your deadline for the real stuff the
application needed to do. Dont you hate it when
that happens?
This is one of those times that third-party component sets can really be useful specically
ExpressBars. I got ExpressBars in a package deal
when I purchased ExpressQuantumGrid 3.0 (see
the review of ExpressQuantumGrid in the January 2001 issue of Delphi Informant Magazine).
I installed it and didnt bother reading the docu-

New & Used

Figure 2: Drop-down tree view controls in a menu bar.

mentation. I used it for the minimal functionality in a new application I was writing, yet I nd myself using it again and again.

Figure 3: Office XP-style menus.

With the release of ExpressBars Suite 4.0, I had an excuse to read


the Whats New, dive into the documentation and demonstration applications, and appreciate how much more than a menu and
toolbar suite this product really is. The icing on the cake came
when my QA engineer asked when we were going to convert an
older application to use the new menus and toolbars.

We will return to the toolbars design capabilities in a moment, but


for now lets consider the Commands tab of the property editor.
Think of a command as a menu item or a button on a traditional
toolbar. Now imagine that this command can be an edit box, a
drop-down combo box, a static item, a color selection drop-down
box that can open a color selection box, a font selection drop-down
box, a spin box, a most-recently-used list, a lookup combo, a date
editor with a drop-down calendar, a sub-menu, an image selection
drop-down box, a tree view component... You get the point.

The Components
ExpressBars installs 11 new components into the Component palette on a new ExpressBars tab. Three of these components are used
for the SideBar: an Outlook-like component with groups and items
that appear at the left side of an applications window. TdxSideBar
is the main Outlook bar-like component. Drop it on your form,
double-click to activate the component editor, and start adding
groups and items from there. For each group, you can dene the
visibility (groups might be activated based on user authorization
for example), the size of the icons for the items it contains, its
position compared to that of other groups, and its name. For each
item, you can assign the icon, a tag, a hint, its position within the
group, and custom data.
Once your groups and items are dened, its easy to use the
OnItemClick event to change views based on the item selected. The
component allows you more ne-grained control to allow you to
respond to events that happen when the active group changes, the
item selection changes, before or after a user edits an item, etc.
If you want to offer user customization of the SideBar, so they
can enable or disable groups, add items, etc., youll need to use
the TdxSideBarStore and assign the SideBars Store property to
point to it. Finally, the TdxSideBarPopupMenu can be assigned
to the SideBar component to provide standard customization
operations popup menus such as Add Group, Remove Group, Rename
Group, Remove Item, etc.
Now lets move on to the real power of this product: the menu and
toolbar components. To start working on your menu and toolbar
design, drop a TdxBarManager on your form and double-click on
it. Just as in Ofce, a toolbar and a menu are pretty much the
same. Dene toolbars in the component editors Toolbars tab. You
can designate a toolbar as the main menu of the form to have it
automatically appear at the top of your form looking like a regular
menu, or you can leave it as a regular toolbar that will appear as a
docked toolbar or a oating command window.
38 September 2001 Delphi Informant Magazine

If you cant nd the item you want to place on your toolbar among
the different classes offered, you can create a control container
command and assign an element of your design. Link this command object to the object that you place on your form, with its
visibility turned off, and youre ready to go. One of the samples
that come with ExpressBars shows how to include a Delphi-like
component palette on a toolbar.
Every command item can be customized with an associated image,
a title that can be displayed with it, and for the different
command types a set of properties and events that allow you
to customize the use of this command. Since there are 21 built-in
command types, we wont go into the details. Instead, lets return
to the toolbars.
Once you dene a toolbar, you can dene if it will be docked to
one of the four sides of the form. The bar manager will, of course,
allow you to customize to which side, if any, the toolbar can be
docked. You can also determine if users can let it oat. Then you
can start dragging and dropping command objects on it to dene
its appearance. The commands will automatically be added and
aligned, so you wont need to ght with the placement of the
elements the way you would if you were to use a simple TPanel
for a toolbar.
When a toolbar is selected in the component editor, you can assign
a plethora of options to it, such as the location where it is docked
and to which sides of the form it cant be docked. You can also
dene if the toolbar shows a size grip, if it takes the entire row
(dock width), if it can span multiple lines, what its caption is when
it oats, where it rst appears when it is oating, what kind of
border it has, and what kind of customization it provides.
Customization is a big feature offered by this product.

New & Used


When your application is executed, users can use Microsoft Ofcestyle customization commands to add, remove, rename, and rearrange commands and other options of customization for every
toolbar. This will allow a user to create favorites and display the
tools used most often on a convenient toolbar.
You may think this is a lot of functionality, but were just getting
started. You can have Ofce-style menus that hide seldom-used
commands and automatically expand after a pre-set period of time;
you can dene groups that contain other groups and commands
and allow users to enable or disable them en masse; you can use
the same commands on multiple toolbars or popup menus (thus
getting the same effect as action lists); you can dene your application with a standard look, a at look, or even an Ofce XP look
(see Figure 3). The list of functionality and customization options
of the components goes on and on, but lets stop here.

If you want to create outstanding applications, and dont want to


spend your time trying to mimic the latest state-of-the-art user
interface, ExpressBars can be a godsend.
Developer Express
6340 McLeod Dr., Suite 1
Las Vegas, NV 89120
Phone: (800) GO-DEVEX
E-Mail: info@devexpress.com
Web Site: http://www.devexpress.com
Price: ExpressBars Suite 4, US$179.99 (with source, single-developer license only) or US$129.99 (without source, single-developer
license only). Upgrades and bundle discount packages available.

Lets not forget the TdxBarDockControl component that allows you


to create a dock site on any container control (e.g. a TPanel ) on your
form. Youre no longer limited to docking only on the four sides of
the form. For example, I have an application that displays an objectselection pane on the left, and an editor pane on the right. It makes a
lot of sense to have a dock site above the editing pane which is at the
bottom of the form. That increases productivity because users dont
need to move the mouse all the way to the top of the form to issue a
command while working with the editor.

The samples that ship with the product are also a good source
of information and can teach you a lot. On top of this, the
newsgroups server provides peer support and is frequently manned
by Developer Express employees, so you will usually get excellent
technical support and answers to your questions.

Documentation, Samples, Technical Support

Conclusion

ExpressBars is a deceptive product: You can achieve great results


without writing a single line of code. Just open the component
editor, drag and drop to your hearts content, and look like a star.
You can also try to learn all the features and options the product
provides and spend the rest of your life trying arcane combinations
of properties and events just to see what happens.

Do you need ExpressBars? Of course not. Delphi comes with


enough components to create perfectly acceptable Windows applications with toolbars, menus, and the like. However, if you want to
create outstanding applications, and dont want to spend your time
trying to mimic the latest state-of-the-art user interface, ExpressBars can be a godsend. I stumbled upon this product by mistake,
but its no mistake that I keep coming back to it with every new
application. Take it for a spin.

Theres also a happy middle ground. The Help les that ship with
the product are integrated with Delphis Help system upon installation for a complete and helpful reference. However, Developer
Express took the time to write a couple of tutorial-style articles
(http://www.devexpress.com/soapbox.asp) that can get you started
in a hurry. These do a good job of explaining some of the capabilities, thus steering you to explore on your own once you understand
what features can be achieved with the different components.

39 September 2001 Delphi Informant Magazine

Ron is a software developer specializing in Business Intelligence applications.


He can be reached at rloewy@transport.com.

New & Used


By Bill Todd

SQL Tester 1.2


SQL Statement Analysis Made Easy

QL Tester 1.2 from Red Brook Software, Inc. lets you build and test SQL statements
without having to compile and run your application. This architecture lets you
use fewer query components, as well as store all your SQL statements outside your
application, so they can be changed without recompiling your program.
SQL Tester is particularly useful if youre creating
an application and you want to store your SQL
statements in external les and execute them by
using the LoadFromFile method of one of the
Delphi query components. Figure 1 shows the
main SQL Tester form.

Getting Started
SQL Tester lets you create SQL statements using
any database you can access via the BDE or ADO.

Figure 1: SQL Testers main form.

40 September 2001 Delphi Informant Magazine

A pair of radio buttons on the SQL Tester toolbar


let you choose whether you want to display BDE
or ADO databases. The Databases list box lets
you choose the database with which you want to
work. After you select the database, all the tables
in that database are listed in the Tables list box.
Select a table and the elds for that table appear
in the Fields list box.
To create a new query press I, or click
the Insert button on the navigator bar, then
type your SQL statement into the SQL Statement
memo control. Any time you need to insert a
table or eld name into your SQL statement,
simply drag it from the table list or eld list,
and drop it anywhere in the SQL Statement memo
control. Regardless of where you drop it, the
table or eld name will always be placed at the
insertion point.
If youre creating queries that involve more than
one table, SQL Tester can include a table alias
with each eld name or table name you add to
your SQL statement. To automatically add aliases
when you add eld names or table names to the
SQL statement, you must assign an alias to each
table. To do this, right-click the table name in
the Tables list box. Choose Set table alias from the

New & Used

Figure 2: The Parameters dialog box.

popup menu, then enter the alias in the dialog box and click OK.
You only need to do this one time because the table aliases are saved
in SQL Testers database.
If your query contains parameters, simply click the Parameters
button at the bottom of the main form to display the dialog box
shown in Figure 2. Use this dialog box to enter parameter values for
each of the parameters in your query. This allows you to test your
SQL statement with different parameter values.
After youve entered your SQL statement and assigned values to parameters, you can run the SQL statement by clicking the Run button at the
bottom of the main form, or by clicking the Run speed button to the
left of the SQL Statement memo control. If you run a SELECT statement,
the result set appears in the Query Results grid at the bottom of the main
form. Optionally, you can have the query results displayed in a separate
window. This lets you have up to four result sets open at one time.
You can export the query result set in either ASCII or XML format.
You can also request a live result set when testing queries, so you
can update the result set to modify your test data without having
to use another program.
If youre working with a long SQL statement, the SQL Statement
memo control may not be large enough to allow you to see the entire

SQL Tester lets you build and test SQL statements without having
to compile and run your applications. It can decrease your development time with its ability to add table names and field names
to SQL statements using drag-and-drop, and you can batch test
queries when the database schema changes. SQL Tester can also
read SQL statements from Delphi .dfm files.
Red Brook Software, Inc.
554 Watervliet Shaker Road
Latham, NY 12110
Phone: (518) 786-3199
Fax: (518) 786-3511
E-Mail: sales@redbrooksoftware.com
Web Site: http://www.redbrooksoftware.com
Price: SQL Tester 1.2, US$195.00

41 September 2001 Delphi Informant Magazine

Figure 3: The SQL Statement dialog box.

statement. To solve this problem, click the second speed button to the
left of the SQL Statement memo control and the dialog box shown in
Figure 3 will be displayed, containing the entire SQL statement. This
dialog box is resizable and provides a much larger working area.

Putting It to Use
After youve created and tested your SQL statement, youll want to
move to the Identification group (again, see Figure 1). Here you can
enter a Description for the statement. You can also enter a Group Code
to identify the SQL statements by the project to which they belong.
Use the Group Code edit box to select SQL statements for certain
batch operations. If the Database edit box doesnt contain a database
name, simply drag the database name from the Databases list box. If
you want to save the SQL statement to a le, enter the name in the
File Name edit box, and press CS (or choose File | Save).
Another nice feature is that SQL Tester lets you save your current
SQL statement as a template if you need to create many similar
queries. Click the Insert button on the navigator bar to create a query
from the template, then click the Load from Template button to display
the list of templates youve created. Double-click the template you
want to use and the SQL statement will be inserted into the SQL
Statement memo control at the insertion point. Templates dont have
to be complete working SQL statements. For example, you might
save a list of eld names you want to use in many queries.
Another timesaving feature is the Duplicate button. View any
query in the database, click the Duplicate button, and SQL Tester
creates a new query with the same SQL statement and the same
identication information.
SQL Tester lets you print queries two ways. You can print the
current query by clicking the Print speed button to the left of the
SQL Statement memo control. Otherwise, clicking the Print button
on the toolbar lets you print all the queries within a statement
number range, all queries with the same group code, or all queries
for the database you choose.
Another practical feature of SQL Tester is the ability to batch test
queries. This is handy if you make changes to your database and
want an easy way to determine which queries are affected by the
changes. To test a batch of queries, choose Tools | Batch Testing.
The Batch Testing dialog box lets you select the SQL statements
to test, and offers three ways to test them: You can choose to

New & Used


test all the queries for a specic database, all the queries with the
same group code, or all the queries within a range of statement
numbers. Click the Run button and SQL Tester executes all the
queries and lists those that generated errors. You can select any
query in the list and click the GoTo button to make changes.
Its a good idea to get in the habit of making a backup copy of
your database before batch testing queries. This is because SQL
Tester executes each SQL statement; if any of the statements make
changes to your database, the changes will take place.
Even if you dont choose to load all your SQL statements from
les, SQL Tester can still be useful, because it can read SQL
statements from Delphi .dfm les (if the .dfm les were saved
as text). This feature allows you to extract the SQL statements
from the query components in your project and batch test them
in SQL Tester. After making corrections, simply write all the SQL
statements back to the .dfm les to update your application.

Conclusion
SQL Tester can cut your development time in two ways. First,
the ability to add table names and eld names to SQL statements
using drag-and-drop can save a lot of typing when youre creating
queries. Second, you can batch test queries when the database
schema changes. Batch testing all queries in an application using
SQL Tester is a lot faster than opening every query component
in your application in the Delphi IDE to see if the query still
executes properly with a new database structure.

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. Bill 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 US and Europe. Bill
can be reached at bill@dbginc.com.

42 September 2001 Delphi Informant Magazine

TextFile

Building Delphi 6 Applications


Short of a nuclear attack or winning the lottery, nothing can
grab my attention quicker than
a book about an unreleased
version of Delphi. So when
Paul Kimmels Building Delphi
6 Applications showed up at the
ofce, I snapped at the chance
to take it home.
After making my rst cover-tocover pass, however, I was puzzled. There seemed to be a 6 in
the title that didnt belong there.
While the book did present a
complete perspective on building
Delphi applications, there were
very few specic references to
Delphi 6 and virtually nothing about writing cross-platform
applications with Kylix.
Out of curiosity, I cruised the Borland forums and discovered that
this book had generated a lot
of controversy even before it was
ofcially titled. Since Delphi 6
hadnt been released, some people
assumed that any reference to
it would violate Borlands NDA.
Flames also ew that the originally
proposed title infringed on the
turf of an existing developer series.
While I was disappointed and
mildly annoyed with the books
title, I was genuinely appalled
with the overall treatment the
author received from the online
Delphi community. Some of the
threads were so unsettling that I
couldnt help but wonder if any
other aspiring rst-time authors
might read them and decide to
pass on such a severe and intolerant audience.
This reaction also tended to
eclipse the rather substantial content of the book. No, its not

about Delphi 6 and it certainly


isnt perfect, but its a major contribution that contains plenty of
interesting and useful material.
While many of the topics in this
book have been covered by other
authors in other works, Paul
Kimmel has provided yet another
perspective that may enlighten
where the existing body of Delphi
texts havent. I also believe that
it deserves a warm welcome,
because Delphi perspectives at
my local bookstore are getting
harder and harder to nd.
So lets set the embattled title
with its bewildering D6 reference
aside and explore the content of
this book and its accompanying
CD. Well denitely encounter
value along the way along
with a few speed bumps.
The tone of the book is peer
to peer and personal, like listening to a talkative colleague
rather than a formal mentor. If
youre a beginning or intermediate programmer, this ambiance
may help you feel comfortable
with the author and presentation.
Advanced readers whove already
weathered their share of talkative
coworkers will probably prefer a
more concise presentation.
There are 19 chapters, four
appendix projects, a one-page
bibliography, and a fairly good
index. Five major topics are
developed: an IDE/OOP tutorial,
component design, COM automation, database applications,
and Internet-related programming. About half the book would
qualify as a basic Delphi tutorial,
while the other half contains
tricks, tips, and techniques for
the intermediate and advanced

43 September 2001 Delphi Informant Magazine

programmer. All skill levels will


probably appreciate the bite-sized
projects in the appendices. A
cookbook lled with self-contained and practical examples like
these would be a must-have.
The CDs descriptive readme
le was provided in an annoying proprietary le format. And
after installing a reader, I found
it contained a mere three lines
and 15 words! The CD itself
was just as sparse, providing
only a couple of publicly available packages, as well as the
books sources. The sources
themselves are interesting and
well written, but theres a sad
lack of useful comments. I
also found numerous sections of
code that had been commented
out with no explanation. While
this is sometimes done in the
heat of production, its poor
form for a tutorial.
The lions share of the 42+MB
source directory were compiled
EXEs and DLLs. Deleting them
placed the unique content on the
CD at around 2.5MB, including
pictures and Paradox tables. This
didnt make the CD seem as
Value-Packed as the back cover
of the book implied. The folder
for Chapter 17 was empty on
the CD, so I wrote the author
and asked if there was a support
site for the book. He replied
that one was in the works and
to check his Web site (http://
www.softconcepts.com) after D6
is released.
Building Delphi 6 Applications is
about building Delphi applications, but its not about Delphi
6. So if youre after a comprehensive review of the new features

in D6, or specic information


on CLX, Apache support, or
DataSnap, youll need to look
elsewhere.
These issues aside, the entire
range of Delphi development
issues are thoroughly covered, but
the presentation is a little wordy.
If it were possible to turn off
the verbose switch and recompile,
this book could probably drop
100+ pages without losing any of
its technical content.
The author is experienced and
knowledgeable and the book contains plenty of value, but the lack
of D6 content and the marginal
quality and small number of bits
on the CD make $59.99 a bit
steep. Its denitely worth a look
but denitely take that look
before taking it home.
Tom Lisjac

Building Delphi 6 Applications


by Paul Kimmel,
McGraw-Hill Professional Book Group,
http://www.osborne.com.
ISBN: 0-07-212995-6
Cover Price: US$59.99
(774 pages, CD-ROM)

TextFile

Building B2B Applications with XML


This book offers a survey of the
burgeoning technologies used
in B2B (business-to-business)
transactions over the Internet.
In 10 chapters, the author discusses introductory XML concepts; transport mechanisms
such as HTTP, SMTP, and
FTP; and security considerations. Insight into working
with B2B pioneers Commerce
One and Ariba is offered,
along with coverage of SOAP
(Simple Object Access Protocol), and BizTalk (Microsofts
implementation of SOAP). The
book closes with a checklist for
building a simple B2B system.
Java and XML code examples
are presented throughout.
Since the book doesnt have a
CD-ROM, the author repeatedly
prompts readers to visit the companion Web site for code archives
and updates on these rapidly
developing technologies. Unfortunately, the books link on the publishers site is no longer valid.
Searching the Wiley site only provided the opportunity to buy a
copy of the book. Finding the
authors personal site also failed to
yield the much vaunted supplemental materials.

Special Edition Using XHTML

For the most part, the author


provides credible explanations of
complex technologies. Beginners
wont learn enough to actually
implement solutions, but the footnotes provide a comprehensive list
of online resources. Despite the
missing companion Web site, this
book is still worthy of consideration if youre approaching B2B.
David Ringstrom

Using XHTML is an excellent


beginner- to intermediate-level
book, providing a broad overview of XHTML, and other
Web development technologies.
Whether youre an intermediate Web developer making the
switch to XHTML, or a beginner, Using XHTML will make
an excellent addition to your
reference bookshelf. I daresay,
if youre only going to buy one
book on XHTML and Web
development, make it this one.

Holzschlag wanders off into a


few hundred pages on various
Web development topics.
That complaint aside, Ms
Holzschlags Using XHTML is
a commendable treatment of
XHTML, CSS, XSL, DTDs,
and related Web and wireless
development topics, which I
heartily recommend.
Robert Leahey

Ms Holzschlags writing style


is light and chatty, making this
tome quite readable. There
are nine topical sections with
several chapters each. Each
chapter includes sections on
TroubleShooting and Designing for the Real World that
contain useful tips and rulesof-thumb.
Building B2B Applications with XML
by Michael Fitzgerald,
John Wiley & Sons, Inc.,
http://www.wiley.com/compbooks.
ISBN: 0-471-40401-2
Cover Price: US$44.99
(310 pages)

44 September 2001 Delphi Informant Magazine

My chief complaint is that the


book seems to stray from the
main topic of XHTML about
two thirds of the way through.
After a very solid introductory
treatment of the origins of
XHTML, and then a good
overview of the language, Ms

Special Edition Using XHTML


by Molly E. Holzschlag, QUE,
http://www.quepublishing.com.
ISBN: 0-7897-2431-6
Cover Price: US$39.99
(958 pages)

The Tomes of Delphi:


Algorithms and Data Structures
Julian Bucknall
Wordware Publishing, Inc.

Building Kylix Applications


Cary Jensen and Loy Anderson
Osborne/McGraw-Hill

International User Interfaces


edited by Elisa M. del Galdo
and Jakob Nielsen
John Wiley & Sons, Inc.

ISBN: 1-55622-736-1
Cover Price: US$59.95
(525 pages, CD-ROM)
http://www.wordware.com

ISBN: 0-07-212947-6
Cover Price: US$59.99
(832 pages)
http://www.osborne.com

ISBN: 0-471-14965-9
Cover Price: US$64.99
(276 pages)
http://www.wiley.com

TextFile

SQL in a Nutshell

UML Explained
There are many books about
the Unied Modeling Language. Do we really need
another? Most of the current
crop were designed for readers
with considerable technical
expertise, but theres a need
to present UML in a manner
accessible to those with whom
engineers must communicate
(stakeholders). Kendall Scotts
UML Explained is aimed especially at such stakeholders, and
lls this important gap.

fect for managers and others


not involved directly in design
or coding. It seems less
appropriate for developers,
however. For the latter audience I continue to recommend
Martin Fowlers UML Distilled
[Addison-Wesley, ISBN:
0-201-65783-X] as the appropriate introduction.
Alan C. Moore, Ph.D.

Unlike most other UML books,


this one assumes little previous
knowledge or technical jargon.
Granted, terms such as class,
component, and use case
are essential to understanding
the UML. The author is careful
to highlight and dene such
terms the rst time they are
introduced in the text. They
also appear in a glossary.

SQL in a Nutshell by Kevin Kline


and Daniel Kline is a must-have
for any developer who must work
with the SQL language. With just
over 200 pages of denitions and
examples, the book covers Microsoft SQL Server, MySQL, Oracle,
and PostgreSQL. What a developer can expect from this book
is an easy reference that will help
them with the SQL syntax of
each of these RDBMSs (Relational
Database Management Systems).
There are chapters on keywords,
functions, concepts, and new
commands.

normal form. Neither does the


book touch upon any administrative tasks like backups, data
migration, or index maintenance.
The explanation of joins also just
scratches the surface of a deep subject. These items were not in the
goals of the authors. SQL in a
Nutshell will be a great addition to
any technical reference library.
Christopher R. Shaw

UML Explained
by Kendall Scott,
Addison-Wesley,
http://www.awl.com.

The chapter on statements is


well organized and includes a
quick command reference table to
show the class, implementation,
and which RDBMS supports each
statement. Each statement explanation starts with some basic
information about the command
that is relevant to all four
RDBMSs, then provides a paragraph or two that explains each
of the variations of the statement.
The information is then followed
up by examples.

SQL in a Nutshell
by Kevin Kline with Daniel Kline, Ph.D.,
OReilly Computer Books,
http://www.oreilly.com.

ISBN: 0-201-721820-1
Cover Price: US$29.95
(151 pages)

What a reader shouldnt expect is


an in-depth explanation of relational databases, or descriptions of

ISBN: 1-56592-744-3
Cover Price: US$29.95
(214 pages)

The Elements of
User Interface Design
Theo Mandel
John Wiley & Sons, Inc.

AntiPatterns in Project Management


William J. Brown, Hays W. Skip McCormick
III, and Scott W. Thomas
John Wiley & Sons, Inc.

Exchange 2000 Server:


The Complete Reference
Scott Schnoll, Bill English, and Nick Calancia
Osborne/McGraw-Hill

ISBN: 0-471-16267-1
Cover Price: US$49.99
(440 pages)
http://www.wiley.com

ISBN: 0-471-36366-9
Cover Price: US$49.99
(460 pages)
http://www.wiley.com

ISBN: 0-07-212739-2
Cover Price: US$49.99
(793 pages)
http://www.osborne.com

This book is well organized


and does a good job of
introducing the essential UML
topics to the reader with average technical expertise. With
its discussion of the Unied
Process and its many example
UML diagrams, it seems per-

45 September 2001 Delphi Informant Magazine

Best Practices
Directions / Commentary

Look Ma, No Help Engine!

o matter how intuitive you feel your application is, there will be times when its users, or at least some of them,
will be confused. Theyll need answers to questions that arise as to how to use the program, or guidance on
how to accomplish a particular task. Its customary to provide a separate Help file for just such occasions. However,
sometimes that may seem like overkill, or you may feel its a waste of time to produce one as nobody ever
reads the Help documentation anyway. Moreover, its YAF (yet another file) that needs to be deployed with your
application, and another tool you have to learn. Or you can hire a contractor to create it.
Nevertheless, you should provide help in some form for users. You
have three choices:
 Provide no help whatsoever. Bad idea.
 Provide a full-edged Windows Help le.
 Provide a quick-and-dirty pseudo Help system.
If youre serious about offering a quality software package, the rst
option is obviously not a good one, so you must ask yourself:
Do I write a conventional Help le or not? If you do, you
can use a tool such as RoboHelp (http://www.ehelp.com), Doc-ToHelp (http://www.wextech.com), Microsoft Help Workshop, which
comes with Delphi (\Program Files\Borland\Delphi X\Help\Tools),
or HelpScribble (http://www.jgsoft.com/helpscr.html), which is written in Delphi.
If you do opt to create a conventional Help le, when is the best
time? That is, will you write the Help le before writing the application, or wait until afterward? Many software methodology experts
recommend writing the Help le rst, basing it on the detailed
design specications. An advantage to that approach is coders can
then use the Help le as the product specication document. It
should be very clear how things are supposed to look and feel,
and just exactly what it is that the application does and doesnt do.
Preferably the le is complete, and contains mocked-up screen shots
of the forms. Adhering as much as possible to what is in the Help
le aids in preventing feature creep and gold plating.
Regardless of these considerations, you may decide to either postpone
the creation of the Help le until after the application is at least
partially complete, or eschew a separate Help le altogether. Thats
where the third option comes in, providing a poor mans Help system.
This technique is especially applicable for prototypes and utilities.

Figure 1: Nothin fancy, but it gets the job done.


46 September 2001 Delphi Informant Magazine

With a modicum of extra effort, you can provide just enough help
in a manner thats not tied to the Windows OS (which is especially

Best Practices
const
crlf = #13#10;
resourcestring
SGettysburgAddress1 =
'Four score and seven years ago our fathers brought,'+crlf+
'forth upon this continent, a new nation, conceived'+crlf+
'in Liberty, and dedicated to the proposition that'+crlf+
'all men are created equal.'+crlf+crlf+
'Now we are engaged in a great civil war, testing'+crlf+
'whether that nation, or any nation, so conceived,+crlf+
'and so dedicated, can long endure. We are met here'+crlf+
'on a great battle-field of that war. We have come'+crlf+
'to dedicate a portion of it as final resting place'+crlf+
'for those who here gave their lives that the nation'+crlf+
'might live. It is altogether fitting and proper'+crlf+
'that we should do this.'+crlf+crlf+
'But in a larger sense we can not dedicate--we'+crlf+
'can not consecratewe can not hallow this ground.'+crlf+
'The brave men, living and dead, who struggled here,'+crlf+
'have consecrated it far above our poor power to add'+crlf+
'or detract. The world will little note, nor long'+crlf+
'remember, what we say here, but can never forget'+crlf+
'what they did here. It is for us, the living,'+crlf+
'rather to be dedicated here to the unfinished work'+crlf+
'which they have, thus far, so nobly carried on.';
SGettysburgAddress2 =
'It is rather for us to be here dedicated to the'+crlf+
'great task remaining before us--that from these'+crlf+
'honored dead we take increased devotion to that'+crlf+
'cause for which they here gave the last full'+crlf+
'measure of devotion--that we here highly resolve'+crlf+
'that these dead shall not have died in vain; that'+crlf+
'this nation shall have a new birth of freedom, and'+crlf+
'that this government of the people, by the people,'+crlf+
'for the people, shall not perish from the earth.';

procedure ShowHelp(ATopic: Integer);


begin
case ATopic of
Abraham:
MessageDlg(Format('%s%s', [SGettysburgAddress1,
SGettysburgAddress2]), mtInformation, [mbOK], 0);
Martin:
MessageDlg(Format('%s%s', [SIHaveADream1,
SIHaveADream2]), mtInformation, [mbOK], 0);
John:
MessageDlg(Format('%s%s%s', [SAskNot1, SAskNot2,
SAskNot3]), mtInformation, [mbOK], 0);
else
{ nothing };
end;
end;

Figure 3: The ShowHelp method.

If youve assigned HelpContext values to controls, add the following


code to each forms OnKeyDown event handler:
if Key = VK_F1 then
ShowHelp(TWinControl(ActiveControl).HelpContext);

If youve assigned HelpContext values to a form, add this code:


if Key = VK_F1 then
ShowHelp(HelpContext);

For code readability, add some constants that will correspond to the
topic you want to display, mapped to the HelpContext you provide
for the forms or controls:

Figure 2: Being resourceful.

attractive if your application may be ported to Linux via Kylix), is


easily modiable by you, and, if you use resource strings, facilitates an
easier transition to localized versions of your application. [For more
on resource strings, see Clay Shannons article Be Resourceful in the
July 2000 Delphi Informant Magazine.]
In a nutshell, this Help lite is accomplished by setting appropriate
HelpContext properties for each control or form in your application
just as is done when integrating a conventional Help le. To
do this, check for the KeyDown event to trap 1 key presses (the
natural thing for a user to do to get more information), and then
display the appropriate Help topic based on the active form or
control (see Figure 1).
You can start implementing this technique by setting the KeyPreview
property of your forms to True. If youll create one Help screen per form,
assign a value to each forms HelpContext property. If your Help will be
more granular (youll assign different Help screens to different controls
or groups of controls on each form), assign a value to each controls
HelpContext property. Then add a constants unit to your project (if you
dont already have one) to hold resourcestring declarations containing the
actual help text. For example, to create the Help screen shown in Figure
1, the pertinent declarations would look like Figure 2.
Please note when attempting to pass very long strings to the
MessageDlg function, youll nd that theres a limit to the size of
the string that can normally be displayed. After reaching this limit,
the remainder of your message will simply be truncated. There is
a workaround. As shown in Figure 2, you can break up your prose
into multiple resource strings, then concatenate them using the
Format function.
47 September 2001 Delphi Informant Magazine

const
Abraham = 0;
Martin = 1;
John
= 2;

Finally, write the ShowHelp method shown in Figure 3.


Besides the HelpContext property, you could also use the Tag property
to the same end, if youre not already using it for something else. For
that matter, even the Hint property could be used with a slight variation. Besides being the logical property to use for this purpose, using
HelpContext is especially good if you may only use this technique
temporarily (e.g. for a prototype), and plan on incorporating fulledged Help later.
For most applications, at least a little help is needed. Dont neglect it
just because you dont nd it interesting, or think its too much of a
hassle. At the very least, provide some rudimentary information for
the more challenging or unconventional parts of your program. Using
the techniques described herein, you can accomplish this quickly and
relatively painlessly.
Clay Shannon

Clay Shannon is an independent Delphi consultant, author, trainer, and mentor


operating as Clay Shannon Custom Software in Oconomowoc, WI. Hes available
for Delphi consulting work in the greater Milwaukee/Madison, WI areas, remote
development (no job too small!), and short-term assignments in other locales (ich
kann Deutsch). Clay is a certified Delphi 5 developer, and is the author of Tomes
of Delphi: Developers Guide to Troubleshooting (Wordware, 2001). You can reach
him at bclayshannon@earthlink.net.

File | New
Directions / Commentary

List Server Madness

erious issues face us every day. How can I ensure my software is of the highest quality? What new technologies
should I learn this year? Will Delphi and/or Borland survive? Should I get involved with Linux/Kylix now or later?
Should I leave my company and become an independent consultant? Should I leave consulting and join a successful
company? This month we are going to take a break from such serious issues, and see how humor can help us keep
our cool in the face of incredible stupidity in the workplace.
As many of you, I subscribe to a number of Delphi Internet discussion lists and have learned much from reading their messages.
In the January 2000 issue of Delphi Informant I wrote a column
entitled The Case for Delphi in which I shared information from
two discussion threads explaining Delphis strengths compared to
those of Visual Basic. Here I will share a couple of recent discussions from two of my favorite lists: The Delphi Advocacy Group
mailing list (tdag@yahoogroups.com) and the main Project JEDI
list (Delphi-JEDI@yahoogroups.com). Discussions on TDAG are
sometimes technical in nature, but often concern themselves with
the well-being of Delphi and the company that produces it. The
JEDI list typically discusses the various activities of that Project,
but also include some excellent discussions of translation issues from
C/C++ to Delphi. The two threads I will present here, however, are
unusual and non-technical, but I think quite entertaining.
No more Delphi? No, we arent talking about the demise of our
favorite development tool, but rather about the precarious position of
Delphi in some companies. While replacing Delphi with Visual Basic
in a particular shop isnt farfetched, this short thread that appeared on
TDAG does stretch the limits of credulity. Some well-known Delphi
people participate on this list so Ive changed the name of one well
known tool to MyCoolTool to protect the innocent.
The discussion started with this post: Some of you will laugh,
thinking this is a joke. Some of you wont believe me at all. At the
government agency where I work, in our PC Team meeting last
Friday, it was announced that we were now going to begin converting
all of our Delphi programs into WordPerfect macros. The reason?
Make things simple so anyone can understand them. All the lawyers
and 25-year secretarial types gure they can write macros. So now the
big project is to eliminate EXEs altogether, and run the entire agency
under a (nearly obsolete) word processing program. The associate
programmer who works with me told them it was a good idea.
Theres nothing like government work!
The rst reply embodies the wonderful spirit of the developers on
this list, who would rather ght than switch: Just when you thought
you heard it all. Stupidity has reached new levels. The Associate
programmer you work with should be clubbed.
48 September 2001 Delphi Informant Magazine

Another, from a Delphi tool developer (maker of MyCoolTool)


offered valuable help and advice: In a previous life I wrote
intense WordPerfect macros (e.g. I created a popup calculator
accurate to four and a half decimal points, in a macro environment that only supports whole numbers!), so I can objectively
speak to WordPerfects abilities and limitations. If these folks are
still seriously considering this, set up a conference call and Ill set
them straight.
Now I ask you: on how many lists can you expect this level of help?
This developer went on to provide specic details: In addition to the
obvious language limitations, there are these as well:
 WordPerfect macros are noticeably slow. Probably 1000-10000
times slower than Delphi programs.
 WordPerfect macros are extremely difcult to maintain. Expect
to pay 10-500 times as much time and money maintaining your
software.
 No MyCoolTool for WordPerfect.
Given the popularity of MyCoolTool among Delphi users, the next
response was to be expected No MyCoolTool for WordPerfect?
Well, what are you waiting for? This market-savvy developers reply
was ...waiting to see if the Kylix market will be bigger than the
WordPerfect macro tools market. After this the thread faded into
the cyber void.
Delphi 6 to become open source? Discussions about Borland marketing are much more common on TDAG than on the main JEDI
list, but there are occasional exceptions, as this thread will demonstrate. It appeared on the JEDI list on April 1 of this year.
Heres how it began: Borland announced today there will be
a signicant shift in the way their products will be marketed.
With the success of Kylix on the Linux platform there is no
need anymore to sell Delphi 6, said Dale L. Fuller, President and
Chief Executive Ofcer of Borland, in an interview at NBC this
afternoon. Instead there will soon be the entire product portfolio
available for free. Beginning with Delphi 6 Borland plans to give
out their major products for free one every other month in the
hope to take over the market leadership for software development.

File | New
We will repeat on the RAD market what Microsoft attained on
the browser market, said Ted Shelton, Senior Vice President, Chief
Strategy Ofcer, during a press conference which will be held early
tomorrow.
It didnt take long for the replies to start ying. The rst was an
expected one: This is great news! Have you forwarded it to the .nontech news group? However, a skeptic immediately contemplated: I
wonder if this policy will still be in effect on April 2! Surprisingly,
not everyone was pleased: A preposterous resolution I loathe! Fullers
open-sourcing obviates lucrative sales. A Borland stockholder, no
doubt. Well, you cant please everyone. Finally, someone asked the
obvious question, <grin> Do you have a URL for this story? To
which the Prime Perpetrator [PP] replied Aaaah, I knew I forgot
something :o] Then another doubting inquiry, Is this for real? I
plan to send this information to many people!!! To which the PP
responded, Sorry for the irritation. In some countries April 1 is a
day to make jokes and to pull someones leg. It is kind of tradition
to nd credible news which in reality is not true. As I said already,
this was just a joke :o]
Not everyone appreciated the discussion, so we had the request:
Please kill this thread, it is of no use and a bad joke as well. The
PPs defenders were waiting in the wings: I think it was a brilliant
joke: [IMHO it is a good idea to check something like this at the
originator: Is it that difcult to quickly check on the Borland site?
Another made the observation that is occurring right now to so many
of you: Always interesting how easy it is to fool people :-)

49 September 2001 Delphi Informant Magazine

If you doubt that, then read one of the nal contributions: You
should see their faces! My team has gone crazy here. They really
believe the stuff. Some of them are already searching borland.com
for information. Others are forwarding it to their friends around
the world! Its a pity I dont have a camera here. (Hope I dont get
red!) I will tell them the truth tomorrow. The reason for this belief
is that Delphi in India is not used widely and these guys and gals
want Delphi to come up because they know that they will be hot
cakes if it comes up. Wish that Borland would really wake up with
its marketing!
Going back to the previous defense, that individual closed with
this recommendation for that special April day: Have a look at
slashdot.org. Its full of jokes today. Well I didnt, but you can bet its
on my itinerary for April 2002. Until next month...
Alan C. Moore, Ph.D.

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

You might also like