You are on page 1of 49

September 2001, Volume 7, Number 9

Cover Art By: Arthur Dugoni

ON THE COVER
5 OP Tech 32 Sound+Vision
Two-tier Database Development — Bill Todd DirectDraw Delphi — Brian Phillips
Bill Todd shows you how to build and deploy a two-tier client/server Microsoft’s DirectDraw is a proven technology, but requires some
application using dbExpress, one of the major new features in Delphi know-how to make it perform as advertised. Brian Phillips shares
6, and the sample InterBase database that ships with Delphi. several techniques and details a sample implementation.

FEATURES
REVIEWS
10 On Language 37 ExpressBars Suite 4
Console Applications: Part II — Mike Edenfield Product Review by Ron Loewy
In Part II of his console programming series, Mike Edenfield shares
several advanced features that console applications offer program- 40 SQL Tester 1.2
mers, including screen buffers, view windows, and console modes. Product Review by Bill Todd
43 Building Delphi 6 Applications
16 In Development Book Review by Tom Lisjac
Custom Key Bindings — Cary Jensen, Ph.D.
Cary Jensen introduces custom key bindings, a little-known feature of 44 Building B2B Applications with XML
the Delphi Open Tools API that permits you to add custom keystroke Book Review by David Ringstrom
combinations to the Code editor. And it works for Kylix too! 44 Special Edition Using XHTML
Book Review by Robert Leahey
21 Columns & Rows
45 UML Explained
Using the XML Features of SQL Server 2000: Part II
Book Review by Alan C. Moore, Ph.D.
— Alex Fedorov
This month, Alex Fedorov describes how Delphi interacts with the 45 SQL in a Nutshell
Microsoft XML Parser, before moving on to the main topic of how to Book Review by Christopher R. Shaw
query SQL Server 2000 for XML data from Delphi.
DEPARTMENTS
25 Kylix Tech 2 Delphi Tools
Kylix Program Launcher — Brian Burton 46 Best Practices by Clay Shannon
Brian Burton demonstrates a pure Kylix alternative to using shell 48 File | New by Alan C. Moore, Ph.D.
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 rea-
sons it’s important — from its underlying principles to a concrete
example you can download and examine for yourself.
1 September 2001 Delphi Informant Magazine
ModelMaker Tools Releases ModelMaker 6.0 for Delphi
Delphi ModelMaker Tools announced
T O O L S the release of ModelMaker 6.0, a
two-way productivity and UML-
New Products style CASE modeling tool.
and Solutions 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 spec-
ifiers, which give near-100 per-
cent compatibility with Delphi
source code. A new code
importer eliminates virtually all
import restrictions. Code gener-
ation allows more customization
such as assigning or maintaining values. Enhancement of the ing capabilities.
a custom code order and highly ModelMaker Tools API enables Check the ModelMaker Web site
simplified generation and import creating plug-ins for reporting, for a free fully functional demon-
of “in-source” documentation. inserting code fragments, etc. stration of ModelMaker 6.0.
The Code Template palette sim- Existing interfaces have been
plifies creating and applying enriched and new ones, such ModelMaker Tools
snippets of related code. The as access to the Unit Code Price: Single-user license, US$269;
Creational wizard automatically Explorer, have been added. 10-user license, US$995; site license,
inserts and maintains code to Improvements in the GUI and US$1,995.
initialize and destroy class fields other functions improve over- Contact: info@modelmakertools.com
and assign property default view, navigation, and restructur- Web Site: http://www.modelmakertools.com

NAG Software Solutions Announces Support for Delphi 6


NAG Software Solutions developers to access a number of ing access to open windows and
is shipping NAG Components critical settings and system-wide processes, and modifying the
for Delphi 2.1, a component parameters on the end-user’s oper- default look of hints.
suite with complete support for ating system, such as locale, Developers can obtain a free,
Delphi 6. regional settings, desktop, screen fully functional trial copy of
NAG Components for Delphi saver, memory, processor type, and NAG Components for Delphi
2.1 is a cross-platform suite of system paths. NAG Components 2.1 from NAG Software Solu-
more than 70 native general-pur- for Delphi 2.1 also includes com- tions’ Web site or from the
pose components for Delphi. The ponents that encapsulate specific Delphi 6 Companion CD.
product includes visual controls peripherals such as the screen, key- Customers using a licensed ver-
aimed at enhancing the look and board, and mouse for retrieving sion of NAG Components for
feel of desktop and data-driven low-level details directly from the Delphi 2.x have received a free
applications, as well as a variety corresponding device drivers. update, while customers using
of system-level components that NAG Components for Delphi older versions need to upgrade
enable developers to interact with 2.1 supports Windows operating to version 2.1 to get support for
the end user’s system. systems and has enhanced prop- Delphi 6.
The visual controls implement erty editors and dedicated com-
dynamic borders with over 10 ponents for creating custom NAG Software Solutions
predefined styles, as well as screen savers, putting applica- Price: Standard, US$89.95; Professional,
custom gradient or bitmap-based tions in the system tray, making US$104.95.
background fills. The non-visual, applications run automatically at Contact: sales@nagsoftware.com
system-level components enable startup, tuning up forms, gain- Web Site: http://www.nagsoftware.com

2 September 2001 Delphi Informant Magazine


Eldean AB Announces ESS-Model
Delphi Eldean AB announced ESS- in full-scale projects. ESS- A free trial version is available
T O O L S Model, an object model reverse- Model is a reverse engine for at the Eldean Software Web
engine tool for professional Delphi/Kylix and Java files. site. It features automatic gen-
New Products developers. ESS-Model source code is visu- eration of UML-standard class
and Solutions A lot of development time alized in automatically gener- diagrams from your source, fast
is wasted finding out the object ated UML-standard static Class installation, fast execution, a
design or how classes are related Diagrams. Delphi/Kylix (.dpr, .pas) and
Java (.class, .java) parser, visibil-
ity filtering, drag-and-drop user
interface, easy integration into
Delphi/Kylix, full control from
the command-line, and diagram
auto-layout.
The registered version of ESS-
Model offers all the features of
the free version, as well as Java-
doc-style HTML documenta-
tion, XMI export features, and
no limit on input files.
ESS-Model is available in a
Windows version; a Linux ver-
sion is planned.

Eldean AB
Price: Trial version, free; complete ver-
sion, US$49.
Contact: essmodel@eldean.se
Web Site: http://www.essmodel.com

Greatis Software Releases Greatis Form Skin 1.0


Greatis Software released Grea- skin size, moving window by Software Web site. It contains
tis Form Skin 1.0, transparent client area for non-caption forms, a compiled EXE demonstra-
window components for Delphi simple defining form transpar- tion, printable documentation
and C++Builder. ency rules, and simple defining of in PDF format, and trial
The Form Skin pack contains any form areas as system areas, versions of all Form Skin
components that allow devel- such as caption, caption buttons, components.
opers to create windows with resize borders, etc.
transparent areas and with arbi- TBitmapFormSkin offers Skin Greatis Software
trary shapes. and TransparentColor proper- Price: Single-user license, US$29.95;
Form Skin contains three ties so you can define form 10-user license, US$149.95; site license,
components. TCustomFormSkin shapes and Skin images at US$299.95.
is a base parent component design time. Contact: b-team@greatis.com
for all skin components; it A demonstration kit is Web Site: http://www.greatis.com/
defines all methods, properties, available from the Greatis formskin.htm
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 compo-
nents. Just derive your compo-
nent from TCustomFormSkin and
override any virtual methods.
TCustomFormSkin and
TSimpleFormSkin feature simple
controlling of system area (cap-
tion and borders), transparency,
simple controlling of controls
transparency, form auto-size by

3 September 2001 Delphi Informant Magazine


Julian Ziersch Software Introduces WPTools Version 3.1
Delphi Julian Ziersch Software intro- dialog boxes and GUI controls, for links and fields. You can
T O O L S duced WPTools 3.1, a word table and graphic handling, display pop-up hints for links
processing component devel- and header and footer func- and fields, and forms and/or
New Products oped in Object Pascal for tionality. The integrated “RTF- contracts are supported via spe-
and Solutions Delphi and C++Builder. engine” supports a variety of cial input fields. WPTools 3.1
WPTools 3.1 offers WYSI- font and paragraph attributes, has the ability to read HTML
WYG layout view, ready-to-use including hyperlinks, decimal with the tags displayed as
tabs, and jus- objects.
tified text. It Use WPTools 3.1 with wPDF
reads and to export documents created
writes RTF, in WPTools as PDF files for
HTML, and automatically generated out-
ANSI files, lines and links.
and can be A demonstration version of
attached to WPTools 3.1 is available at the
any database. Web site.
WPTools
3.1 offers Julian Ziersch Software
features such Price: Contact Julian Ziersch Software for pric-
as paragraph ing information.
styles and Contact: info@wptools.de
hover effects Web Site: http://www.wptools.com

TurboPower Ships SysTools for Kylix


TurboPower Software Co. is formats; and routines for accessing includes printed documentation,
shipping SysTools for Kylix, a ver- Linux operating system services. examples, and online help. No
sion of its productivity library for SysTools for Kylix provides royalties are charged when using
Linux programmers using Bor- programming constructions the SysTools for Kylix in compiled
land Kylix (Delphi for Linux). compiler does not. For example, Linux applications.
SysTools for Kylix has over 800 SysTools for Kylix has a set of
optimized, time-tested routines for reusable container classes, includ- TurboPower Software Co.
handling common programming ing trees, queues, and stacks, that Price: SysTools for Kylix, US$249; upgrade
operations such as string manipu- can be added to projects for from SysTools 3 for Windows, US$149;
lation, date/time math, high-preci- sophisticated data management. upgrade from other TurboPower products,
sion calculations, and sorting. The SysTools for Kylix also includes US$199. SysTools Platform Bundle (Windows
library also offers specialized rou- improved streams for creating and Linux editions), US$349. Shipping and
tines for financial and statistical and accessing data in disk files. handling charges not included.
calculations; generating, viewing, SysTools for Kylix ships with Contact: (800) 333-4160
and printing a variety of barcode complete source code. It also 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 Recog-
nition 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 auto-
matically detect where the bar- type. It will decode a barcode A free trial version of Barcode
code is on the page and read value in a fraction of a second. Recognition Suite 2.0 is available
it from that location. Recogni- The user need only define the at the SkyLine Tools Imaging
tion of multiple barcodes on a graphical image of the barcode Web site.
page is also possible. as a graphical file (BMP, TIFF,
The proprietary algorithm is JPEG, or any other supported SkyLine Tools Imaging
based on the fuzzy logic approach format) or as a DIB handle, or Price: US$1,999.
in image recognition, solving the HBitmap, or HDC. The barcode Contact: sales@imagelib.com
problem of identifying barcode module will do the rest. Web Site: http://www.imagelib.com

4 September 2001 Delphi Informant Magazine


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

By Bill Todd

Two-tier Database Development


dbExpress Makes It Easy in Delphi 6

d 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 Todd’s article “dbExpress” in the March 2001 Delphi Informant Magazine.]

If you have built applications using the MIDAS form. The main form contains a page control
components in a prior version of Delphi, you’ll with three tabs labeled Employee Sales, Custom-
find yourself on familiar ground when building ers, and Schema. The Employee Sales tab shows
your first dbExpress application. If you haven’t the Employees table and the Sales table from the
used MIDAS, your first dbExpress application sample InterBase database in a master-detail rela-
will involve a lot of new concepts. The best way tionship. The combo box at the top of the form
to get through the basics is to walk through build- lets you select a department. The application then
ing the sample application that accompanies this displays the employees in the selected department
article (see end of article for download details). and their sales records. The Customers tab lets you
view and edit the data in the customer table. The
Creating the Application Schema tab lets you view information about the
The first step in building a typical two-tiered database schema.
client/server application using dbExpress is to
create a new project and add a data module. Figure To build this application, start by dropping a
1 shows the finished data module for the sample SQLConnection component on the data module.
application. Figure 2 shows the application’s main Then type EmpConn as the name for the compo-
nent. 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 configurable
by loading their values from an INI file or from
Figure 1: The example project’s data module. the Windows registry. The application described

5 September 2001 Delphi Informant Magazine


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.

property to the SQL statement you want to use. In this case the
Figure 2: The example project’s main form. SQL statement is:

SELECT * FROM EMPLOYEE


WHERE DEPT_NO = :DEPT_NO

Although you don’t 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 component’s Params property. SELECT * FROM SALES


WHERE SALES_REP = :EMP_NO

in this article loads the database path from the EMP.INI file by Set the SalesDs DataSource property to the EmpLinkSrc Data-
calling the LoadIniSettings method, shown in Figure 4, from the data Source component. This will cause the SalesDs SQLDataSet to get
module’s OnCreate event handler. the value of its :EMP_NO parameter from the current record in
EmpDs, and also cause the Sales records to be embedded in the
Go to the DataSnap page of the Component palette, and add a Employee dataset as a nested dataset field. Next, add a DataSetProvider
LocalConnection component to the data module. This is a new from the Data Access page of the Component palette, and set its
component added in Delphi 6 for building two-tier applications DataSet property to the Employee SQLDataSet, EmpDs.
where the DataSetProvider and the ClientDataSet are in the same
application. While putting a ClientDataSet and its provider in the The final two components you need to supply data to the
same application is nothing new, in prior versions of Delphi there Employee Sales tab on the main form are a ClientDataSet and a
were limitations caused by lack of a connection component. These DataSource. Set the DataSource’s DataSet property to connect it
include a lack of access to the connection component’s methods, to the ClientDataSet. Set the RemoteConnection property of the
events, and properties, particularly the AppServer property. The ClientDataSet to the LocalConnection component, then set its
LocalConnection component is optional, but including it gives ProviderName property to the DataSetProvider for the Employee
you access to its properties, methods, and events, and allows DataSet, EmpProv. Name the ClientDataSet EmpCds.
you to write an application that you can convert to multi-tier
architecture with fewer changes later. The DataSetProvider and ClientDataSet components are required,
because all the dbExpress dataset components provide a unidirec-
Next, return to the dbExpress tab on the Component palette tional read-only cursor, i.e. you cannot move backward through the
and add a SQLDataSet component to the data module. While records and you cannot make changes using dbExpress components.
dbExpress includes three other dataset components, SQLTable, To be able to browse both backward and forward through records
SQLQuery, and SQLStoredProc, these components are provided and insert, delete, and update records, you need the ClientDataSet
as analogs to the BDE dataset components to make conversion of component to buffer and edit the records supplied by the dbExpress
BDE applications to dbExpress easy. The SQLDataSet component dataset component, and the DataSetProvider to generate the SQL
can do everything the other three dataset components can do, statements to insert, delete, and update records in the database.
so there’s no reason to use anything else in a new application.
Begin by setting the SQLDataSet’s SQLConnection property to the The components added so far provide everything necessary to
EmpConn connection component. Next, set the CommandText display information from the Employees table in the top grid

6 September 2001 Delphi Informant Magazine


OP Tech

procedure TMainForm.LoadDeptCombo; procedure TMainForm.DeptComboChange(Sender: TObject);


begin begin
with EmpDm.DeptDs do begin with EmpDm.EmpCds do begin
Open; Close;
while not EOF do begin Params.ParamByName('DEPT_NO').AsString :=
DeptCombo.Items.Add(FieldByName('DEPT_NO').AsString + Copy(DeptCombo.Text, 1, 3);
' - ' + FieldByName('DEPARTMENT').AsString); Open;
Next; end;
end; end;
Close;
end;
Figure 6: The department combo box’s OnChange event handler.
end;

Figure 5: The LoadDeptCombo method.


If you’ve never worked with MIDAS, this may seem like a
complex process. Remember, however, that these components
on the main form. To display the records from the Sales table, are also designed to work in multi-tier applications where the
add another ClientDataSet and DataSource to the data module, ClientDataSet is in a client program running on one computer,
naming them SalesCds and SalesSrc respectively. Select the and the DataSetProvider and SQLDataSet are in an application
Employee ClientDataSet, double-click it to open the Fields editor, server program running on a different computer.
then right-click and choose Add All Fields. Note the last field in the
Fields editor is named SalesDs, the same name as the SQLDataSet If all you need to do is select records from a single table and edit
component for the Sales table. This is the nested dataset field that them, you can use a single SQLClientDataSet component from the
contains the Sales records linked to each record in the Employees dbExpress page of the Component palette, instead of a SQLDataSet,
table. Now select the SalesDs ClientDataSet, click its DataSetField DataSetProvider, and ClientDataSet. The Customers tab on the main
property, and click the arrow to open the drop-down list. The form uses this technique. The SQLClientDataSet is a new compo-
only entry in the list will be EmpCdsSalesDs, assuming that you nent in Delphi 6 that includes a ClientDataSet, DataSetProvider,
used the component names shown in Figure 1. The last step is to and ClientDataSet in a single component. Just set its DBConnection
set the DataSet property of the DataSource component to connect property to your SQLConnection component, add a SQL SELECT
it to the Sales ClientDataSet. statement to its CommandText property, and you’re ready to go. In the
sample application, the SELECT statement is:
This application uses typical client/server architecture by forcing
the user to provide some selection criteria that’s used to fetch a SELECT * FROM CUSTOMER
small set of records. In this case, the selection criteria is provided WHERE STATE_PROVINCE = :STATE_PROVINCE
ORDER BY CUSTOMER
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 Now add a DataSource component and connect it to the
contains a SQLDataSet component named DeptDs that’s used to SQLClientDataSet. Then connect the DBGrid on the Customers
load the combo box. page to the DataSource, and you’re finished. The SQLClientDataSet
has MasterSource and MasterFields properties that can be used to
Figure 5 shows the LoadDeptCombo method that’s called from create a master-detail relationship. However, this is very inefficient.
the main form’s OnCreate event handler. This code opens the For master-detail relationships, it is much better to use separate
department SQLDataSet and uses a while loop to iterate through SQLDataSet, DataSetProvider, and ClientDataSet components.
the records and load the combo box. Note that no Provider
or ClientDataSet component is required in this case, because The Customer table works just like the Employee Sales tab.
the dataset is only being read and traversed from beginning to Choosing a state from the combo box causes its OnChange event
end. Because this SQLDataSet isn’t being used with a Provider handler to close the SQLClientDataSet, assign the state to its
and ClientDataSet, its NoMetaData property is set to True. This :STATE_PROVINCE parameter, and open the SQLClientDataSet.
improves performance, because metadata information required for
updates isn’t retrieved from the server. How It Works
If you’ve worked with MIDAS before, you can skip this section. If
When the application starts, the main form’s OnCreate event you haven’t, here’s a brief explanation of how the provide/resolve
handler calls a custom method that opens both the employee architecture implemented by the dbExpress components works.
and sales ClientDataSets. Since no values have been assigned to
the parameters in the SQL statements, no records are returned, When you open a ClientDataSet, it sends a request to its provider
and both grids are empty. When the user chooses a department requesting data. The provider, in turn, opens the dataset it’s con-
from the combo box, the combo box’s OnChange event handler, nected to and retrieves all the rows supplied by that data set. The
shown in Figure 6, executes. This code closes the Employee provider stores the data in a variant in a proprietary format and
ClientDataSet, extracts the department number from the first sends the data packet to the ClientDataSet. The ClientDataSet
three characters of the combo box’s Text property, and assigns receives the data packet from the provider, extracts the data, and
it to the ClientDataSet’s DEPT_NO parameter. When the loads it into memory.
ClientDataSet is opened, it sends this new parameter value
through the provider to the SQLDataSet for the Employee table, When you edit the ClientDataSet data, the changes are stored
opens the SQLDataSet, retrieves the records returned by the in memory and a property named Delta. To update the database
query, and closes the SQLDataSet. you must call the ClientDataSet’s ApplyUpdates method. In the

7 September 2001 Delphi Informant Magazine


OP Tech
Getting the Database Schema
Figure 7 shows the Schema tab of the application’s 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 you’re viewing schema information for tables or stored
procedures. For tables or system tables, you can view information
on columns or indices. If you’re 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 8 illustrates the use of the data module’s UpdateSchema


method, which is called from the event handler of the Schema
Type combo box. The SchemaType parameter that’s passed to this
Figure 7: The Schema tab of the sample application’s main form. method is actually the ItemIndex property of the combo box.
The code starts by closing the SchemaCds ClientDataSet that’s
sample application, this is done in the ClientDataSet’s AfterPost connected through a provider to the SchemaDs SQLDataSet.
event handler. Calling ApplyUpdates sends the Delta property back The SchemaDs SQLDataSet’s SQLConnection property is set to
to the provider. The provider starts a transaction, looks at each the EmpConn SQLConnection component, but its CommandText
of the changes recorded in the Delta data packet, creates and property is null.
executes SQL statements to update the database, then commits
the transaction. The case statement in Figure 8 determines whether the user
chose Stored Procedures, System Tables, or Tables in the Schema Type
This may seem like a complex architecture, but it has tremendous combo box by comparing the SchemaType parameter to three con-
advantages, such as: stants, StoredProcs, SysTables, and Tables, declared in the interface
1) The provider and its data set can be in one application, while section of the data module’s unit. In each case, the code calls the
the ClientDataSet is in a different application running on SQLDataSet’s SetSchemaInfo method, passing the constant that
a different computer. This makes building multi-tier applica- matches the user’s choice as the first parameter.
tions easy.
2) Transactions are very short-lived, thus reducing concurrency The second parameter of SetSchemaInfo is SchemaObject, and is used
problems on the database server. only when getting information about the columns or indices of a
3) You can sort the ClientDataSet’s data in any order by setting table, or the parameters of a stored procedure. The third parameter,
its IndexFieldNames property. SchemaPattern, lets you filter the result set using the same syntax as
4) If you’re holding a large amount of data in memory in a the SQL LIKE operator. For example, you could pass 'CUST%' to
ClientDataSet, you can create in-memory indices for light- get all the tables that start with CUST. Calling SetSchemaInfo tells
ning-fast searching and sorting, without the overhead of the SQLDataSet to fetch the requested schema information when
maintaining these indices on the database server. it’s opened, and ignore the value of its CommandText property. If
5) You can view a subset of the records in the ClientDataSet by you’ve retrieved schema information and want the SQLDataSet to
setting its Filter property using SQL WHERE syntax. execute its CommandText the next time it’s open, call SetSchemaInfo
6) You can save the ClientDataSet’s data to your local hard and pass stNone as the first parameter.
drive; disconnect from the network; insert, delete, and update
records; then reconnect to the network and apply those After calling SetSchemaInfo, the UpdateSchema method calls the
updates to the database. This makes creating applications for main form’s LoadSchemaDetail method, and passes either sdtTable
field sales and service personnel a snap. or sdtProcedure to indicate whether the user is viewing schema
7) You can sort the ClientDataSet on calculated fields. information for tables or stored procedures. The sdtTable and
8) You can define maintained aggregates (i.e. fields that compute sdtProcedure constants are members of an enumeration declared in
the sum, average, minimum, maximum, or count of all the the interface section of the main form’s unit.
records or groups of records) in the ClientDataSet.
9) You can use the ProviderFlags property of the field object Figure 9 shows the LoadSchemaDetail method. This method clears
of the dataset connected to the DataSetProvider to control the Schema Details combo box and adds columns and indices as
which fields are used to locate records to be updated, and choices if the user is viewing schema information for tables, or
which fields will actually be updated. This allows you to procedure parameters if the user is viewing schema information
easily update the fields from one table in the result set of for stored procedures. The parameters passed to Items.Add are
a join query while ignoring the fields from the other table constants declared earlier in the unit.
or tables.
10) You can save the ClientDataSet’s data in XML format, and Figure 10 shows the Schema Detail combo box’s OnChange event
load XML data into a client data set. handler. This event handler simply calls the data module’s
UpdateSchemaDetail method, passing a parameter that identifies
These are just a few of the advantages of the dbExpress architecture. whether the user chose columns, indices, or procedure parameters.
If you’ve never worked with the ClientDataSet and DataSetProvider
components, it will be well worth your while to spend as much time The UpdateSchemaDetail method calls the SchemaDetDs SQLDataSet’s
as you can to learn about all of their capabilities. SetSchemaInfo method and passes the constant stColumns, stIndexes,

8 September 2001 Delphi Informant Magazine


OP Tech

procedure TEmpDm.UpdateSchema(SchemaType: Integer); procedure TMainForm.LoadSchemaDetail(


begin DetailType: TSchemaDetailType);
with SchemaDs do begin begin
SchemaCds.Close; with SchemaDetCombo do begin
case SchemaType of Items.Clear;
StoredProcs: case DetailType of
begin sdtTable:
SetSchemaInfo(stProcedures, '', ''); begin
MainForm.LoadSchemaDetail(sdtProcedure); Items.Add(Columns);
end; Items.Add(Indices);
SysTables: end;
begin sdtProcedure: Items.Add(ProcParams);
SetSchemaInfo(stSysTables, '', ''); end;
MainForm.LoadSchemaDetail(sdtTable); end;
end; end;
Tables:
begin
SetSchemaInfo(stTables, '', '');
Figure 9: The LoadSchemaDetail method.
MainForm.LoadSchemaDetail(sdtTable);
end;
procedure TMainForm.SchemaDetComboChange(Sender: TObject);
end; // case
begin
SchemaCds.Open;
case SchemaCombo.ItemIndex of
end; // with
StoredProcs:
end;
EmpDm.UpdateSchemaDetail(ProcedureParams);
SysTables:
Figure 8: This procedure is called by the OnChange event han- EmpDm.UpdateSchemaDetail(SchemaDetCombo.ItemIndex);
dler for the Schema Type combo box. Tables:
EmpDm.UpdateSchemaDetail(SchemaDetCombo.ItemIndex);
end;
end;
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 field from the SchemaDs Figure 10: The OnChange event handler for the Schema
Details combo box.
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 INI file, a text file, or somewhere else. This also lets you provide
in the upper grid. The UpdateSchemaDetail method is also called from your own user interface for changing these properties.
the AfterScroll event handler of the SchemaDs SQLDataSet, so the
information displayed in the details grid will update as you scroll from dbExpress also makes writing an application that supports multiple
record to record in the upper grid. As you can see, SetSchemaInfo database servers easy. All you have to do is supply the driver DLL
makes it very easy to get any schema information you may need in for the database server your client is using and set the DriverName,
your application. LibraryName, and VendorLib properties of the SQLConnection
component to the correct values for that database server. dbExpress
Deploying Your Application is small, fast, and easy to deploy, making it an excellent tool for
There are two ways you can deploy an application that uses dbEx- writing database applications. ∆
press. The first is to deploy MIDAS.DLL and the driver DLL for
your database with your application. To find the name of the driver The sample project referenced in this article is available on the Delphi
DLL for your database, just look at the LibraryName property of Informant Magazine Complete Works CD located in INFORM\2001\
your SQLConnection component. This property is filled in auto- SEP\DI200109BT.
matically 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
application’s EXE file, or to any folder on the path, and you’re done.

If you don’t 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. 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
Conclusion programming books, author of more than 80 articles, a Contributing Editor
dbExpress makes it easy to create and deploy applications that to Delphi Informant Magazine, and a member of Team Borland, providing
use SQL database servers. Since all of the database connection technical support on the Borland Internet newsgroups. Bill is also a nationally
information is contained in the properties of the SQLConnection known trainer and has taught Delphi programming classes across the country
component, you have complete control over whether this informa- and overseas, and is a frequent speaker at Borland Developer Conferences in
tion is embedded in your application and stored in the registry, an the US and Europe. Bill can be reached at bill@dbginc.com.

9 September 2001 Delphi Informant Magazine


On Language
Console Applications / Input/Output / Delphi 2-6

By Mike Edenfield

Console Applications
Part II: Advanced I/O

L 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 applica- Windows console-mode API allows you to take
tions can do, however. Far from being restricted advantage of all the features of the Windows pro-
to old, pre-Windows programming techniques, the tected-mode environment, as well as advanced fea-
tures of the console window that go far beyond
what was possible in MS-DOS.
function ReadConsoleOutput(hConsoleOutput: THandle;
lpBuffer: Pointer; dwBufferSize, dwBufferCoord: TCoord; Low-level Output
var lpReadRegion: TSmallRect): BOOL; stdcall;
The console window’s output consists of a collec-
function WriteConsoleOutput(hConsoleOutput: THandle;
lpBuffer: Pointer; dwBufferSize, dwBufferCoord: TCoord;
tion of character cells. Each cell has a character
var lpWriteRegion: TSmallRect): BOOL; stdcall; and an attribute associated with it. Internally, Win-
function FillConsoleOutputCharacter( dows treats the output as a two-dimensional array
hConsoleOutput: THandle; cCharacter: Char; of CHAR_INFO records. Several low-level output
nLength: DWORD; dwWriteCoord: TCoord;
var lpNumberOfCharsWritten: DWORD): BOOL; stdcall;
functions exist that allow you to act on a range of
function FillConsoleOutputAttribute( these cells at once (see Figure 1).
hConsoleOutput: THandle; wAttribute: Word;
nLength: DWORD; dwWriteCoord: TCoord; The first two functions act on a rectangular region
var lpNumberOfAttrsWritten: DWORD): BOOL; stdcall;
of the screen. The destination buffer pointer is filled
function ReadConsoleOutputCharacter(
hConsoleOutput: THandle; lpCharacter: PAnsiChar;
with an array of CHAR_INFO structures contain-
nLength: DWORD; dwReadCoord: TCoord; ing the cells at the requested region. The remaining
var lpNumberOfCharsRead: DWORD): BOOL; stdcall; functions act on a continuous series of these char-
function ReadConsoleOutputAttribute( acters, returning either an array of characters or
hConsoleOutput: THandle; lpAttribute: Pointer;
nLength: DWORD; dwReadCoord: TCoord;
attributes. Wrapping is automatic at the end of the
var lpNumberOfAttrsRead: DWORD): BOOL; stdcall; line. If the bottom of the screen is reached, the
function WriteConsoleOutputCharacter( functions cease processing and return the number
hConsoleOutput: THandle; lpCharacter: PChar; of characters written in the last var parameter.
nLength: DWORD; dwWriteCoord: TCoord;
var lpNumberOfCharsWritten: DWORD): BOOL; stdcall;
function WriteConsoleOutputAttribute(
Use of these functions is fairly straightforward. The
hConsoleOutput: THandle; lpAttribute: Pointer; sample programs in Listings One and Two (begin-
nLength: DWORD; dwWriteCoord: TCoord; ning on page 14) demonstrate a common use of
var lpNumberOfAttrsWritten: DWORD): BOOL; stdcall; the FillConsoleOutputCharacter function: clearing
the screen. This is done by simply filling the entire
Figure 1: Low-level console output functions. screen, starting at 0,0, and proceeding for (height *

10 September 2001 Delphi Informant Magazine


On Language
width) characters with a space character. This technique is preferred If your application already has a console, you must call FreeConsole
over others because it’s fast and compatible with both Windows before trying to allocate a new one. Calling FreeConsole with no
NT/2000 and Windows 95/98. console present has no negative effects, so it’s good practice to always
call it first. AllocConsole creates a new console window, attaches the
Consoles, Screen Buffers, and View Windows standard handles to it, and displays it onscreen. Note that calling
Until now, we’ve been working under a few assumptions. All of our FreeConsole from a child process will return control of the console
programs act as if: to the parent process, allowing it to continue to process input. (This
 there were already a single console present, and we had to use it; is the effect you get when you run a windowed application from a
 there were only one screen of output available; and command-line prompt: the windowed application releases the console
 the output screen was only as big as the console window. immediately, and your prompt returns.)

These three assumptions are the defaults for new console applications, Contained within a console are its input and output buffers. Each
but by no means are they the only options. Processes are free to console has a single input buffer that handles all input events. However,
detach themselves from their parent’s console and create their own, there’s no such limit to the number of output buffers, which are referred
as well as create multiple output buffers and various-sized windows to as “screen buffers.” There are two-dimensional grids of characters
into those output buffers. Taking advantage of these features requires representing what’s to be displayed on the console. A single screen buffer,
an understanding of the three different Windows objects involved in the same size as the console window, is created automatically. The API
displaying text to a console. calls to manipulate screen buffers are shown in Figure 3.

The console itself is an abstract object created by Windows, either You can create as many screen buffers as you want, but only one of
in response to a console application being run, or at the request of them can be active at any given time. The various console output
the application. It contains several I/O buffers, and a visible window functions all take a handle to a screen buffer, which doesn’t have to
representing the program interface. The API calls that deal directly be the active screen buffer. The standard output handle retrieved from
with the console are shown in Figure 2. GetStdHandle, however, will always refer to whichever console buffer
is active at the time it’s called.
Unfortunately, the GetConsoleWindow function only works in
Windows 2000. It allows you to get a legitimate window handle It’s interesting to note how changing the active screen buffer
to the console window, and use it with API calls such as affects calls to the library functions, Write and Writeln. These
ShowWindow. In earlier operating systems, you could get this functions act on a global textfile-type variable called Output by
information via EnumWindows or similar functions. To assist you default. This variable is attached to the standard output when
in finding the console’s window handle, you can retrieve (or your application first executes, and never changes. Keep this in
change) the title bar for the console window with the appropriate mind if you change the active screen buffer, as your output from
functions. If you don’t change it, the console window takes on the Write or Writeln may no longer be visible until you switch active
name of the last process to attach to it (such as the EXE name buffers. Note in Listing One the function WriteToScreen, which
of a DOS command). 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 shouldn’t


function GetConsoleWindow: THandle; stdcall;
be confused with the console window itself, although it’s very closely
function AllocConsole: BOOL; stdcall;
function FreeConsole: BOOL; stdcall; related. The view window of a screen buffer is the portion of the
function SetConsoleTitle(lpConsoleTitle: PChar): screen buffer visible onscreen. To work with the view windows, use
BOOL; stdcall; these functions:
function GetConsoleTitle(lpConsoleTitle: PChar;
nSize: DWORD): DWORD; stdcall;
function GetLargestConsoleWindowSize(
hConsoleOutput: THandle): TCoord; stdcall;
Figure 2: These API calls deal directly with the console. function SetConsoleWindowInfo(hConsoleOutput: THandle;
bAbsolute: BOOL; const lpConsoleWindow: TSmallRect):
BOOL; stdcall;
function CreateConsoleScreenBuffer(
dwDesiredAccess, dwShareMode: DWORD; This SetConsoleWindowInfo function changes the size and position
lpSecurityAttributes: PSecurityAttributes; of the view window. This can be used to easily scroll a console
dwFlags: DWORD; lpScreenBufferData: Pointer):
THandle; stdcall;
window by simply moving the view window down the screen
function GetConsoleScreenBufferInfo( buffer as needed. The maximum window size is determined by
hConsoleOutput: THandle; the size of the screen buffer attached to it, and the size of the
var lpConsoleScreenBufferInfo: TConsoleScreenBufferInfo): font the user has chosen for the console itself. Windows provides
BOOL; stdcall;
the GetLargestConsoleWindowSize function that takes into account
function SetConsoleActiveScreenBuffer(
hConsoleOutput: THandle): BOOL; stdcall; all these factors, and determines the maximum size of a console
function SetConsoleScreenBufferSize( window given its current characteristics. According to Microsoft’s
hConsoleOutput: THandle; dwSize: TCoord): BOOL; stdcall; documentation, attempting to set a window size larger than this,
function ScrollConsoleScreenBuffer(hConsoleOutput: THandle; or which extends beyond its screen buffer, results in an error.
const lpScrollRectangle: TSmallRect;
lpClipRectangle: PSmallRect; dwDestinationOrigin: TCoord;
However, the actual behavior of the SetConsoleWindowInfo function
var lpFill: TCharInfo): BOOL; stdcall; is a bit different. There are also discrepancies between Windows
95/98 and Windows NT/2000 console-mode behavior. Listing One
Figure 3: API calls for manipulating screen buffers. demonstrates two examples of these differences.

11 September 2001 Delphi Informant Magazine


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 differ-
ent sets of console-mode options affecting the high- and low-level
I/O functions. They’re 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.

For high-level input and output, the following console modes are
available:
 ENABLE_LINE_INPUT — Input is processed by your applica-
tion one line at a time.
Figure 4: The example ScreenBuffers program at run time.  ENABLE_ECHO_INPUT — Characters are echoed back to the
screen as entered.
 ENABLE_PROCESSED_INPUT — Windows handles editing
Attempting to scroll beyond the bottom of the screen buffer in and control characters internally.
NT/2000 actually causes the view window to shrink. This confusing  ENABLE_PROCESSED_OUTPUT — Windows handles con-
behavior occurs when you attempt to set the view window position trol sequences automatically.
via relative coordinates, and the bottom coordinate is beyond the  ENABLE_WRAP_AT_EOL_OUTPUT — Lines wrap auto-
bottom of the screen buffer. In this case, the top coordinate is matically at the edge of the screen buffer.
adjusted as requested, but the bottom coordinate is left the same,
effectively changing the number of lines in the view window. To By default, all five of these options are turned on. This mode of operation
avoid this problem, use the function GetConsoleScreenBufferInfo, and is sometimes called “cooked” mode, because Windows performs most
either calculate absolute coordinates for the new view window instead of the editing and control-code handling for you. Your ReadFile calls
of relative ones, or verify that your relative coordinates won’t cause will block until a carriage return is entered, and Windows will correctly
this shrinking behavior before setting them. 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
This odd behavior doesn’t occur in Windows 95/98. In fact, Win- mode is disabled automatically if you disable LINE_INPUT. Also,
dows 95/98 programs cannot change the size of their output buf- attempts to turn off WRAP_AT_EOL_OUTPUT mode under Win-
fer’s view window under any circumstances. Attempting to change dows 95/98 are silently ignored.
the size of a view window will render the attached output buffer
useless: no output will be displayed as long as the view window Low-level console output, using functions such as WriteConsoleOutputCharacter
size differs from the size of the actual console window. To most or FillConsoleOutputAttribute, isn’t affected by any input or output
clearly see this behavior, run Listing One under Windows 95/98, modes. This low-level output is always in a very raw, direct form.
then comment out indicated sections and run it again. As long as However, the ReadConsoleInput API and related low-level input func-
the second screen buffer is a different size than the first, it will tions are affected by these three input modes:
appear empty. In contrast, running the application under Windows  ENABLE_MOUSE_INPUT determines if MOUSE_EVENT
NT/2000 will result in the dimensions of the console window input events are reported to your application.
changing as the active screen buffer changes, and the text in all  ENABLE_WINDOW_INPUT determines if
buffers will always be visible. WINDOW_BUFFER_SIZE_EVENT input events are reported
to your application.
Notice there’s no {$APPTYPE CONSOLE} directive in Listing  ENABLE_PROCESSED_INPUT automatically handles CC.
One because it’s not needed; the console is created by a call to Alloc-
Console. (The run-time result of Listing One is shown in Figure 4.) Processed input mode here simply means that Windows automati-
This is done primarily for demonstration purposes. In general, if you cally calls the control handler when CC is pressed. By default,
know you’ll always have a console while your program is running, processed and mouse input are on, but window input is off. Note
you’ll get better performance by letting Windows create it automati- that no matter what console mode you are in, you’ll always receive
cally. The API call will be used more effectively in later installments KEY_EVENT, FOCUS_EVENT, and MENU_EVENT messages.
of this series, when we create console windows for normal GUI
applications. In practice, you’ll rarely need to change these modes. While turning
off the default input modes for high-level input gives you much
Console Modes finer control over console input, you can achieve the same effect
Much of the behavior of the console window depends on what mode simply by using the low-level input functions. And, as we are about
Windows has put the console in. For most situations, the default to see, it’s a simple task to override the default CC handler to
settings for the console mode will be sufficient. However, changing do what you want.
these modes allows you a very intimate level of control over the
behavior of the console. Changing and querying the console modes is Console Control Handlers
accomplished via two API calls: One of the key elements often mentioned when discussing console
input modes is the console’s control handler. This handler is a func-
function GetConsoleMode(hConsoleHandle: THandle;
tion that Windows calls when certain system-level actions take place
var lpMode: DWORD): BOOL; stdcall; that could affect the console. Windows installs an empty handler that
function SetConsoleMode(hConsoleHandle: THandle; simply calls ExitProcess when the console is created. To install your
dwMode: DWORD): BOOL; stdcall; own, use this API call:

12 September 2001 Delphi Informant Magazine


On Language
flag isn’t specified, any child processes of a console process belong
to the same group. Console applications can then send signals to a
specified process group, using the following API call:

function GenerateConsoleCtrlEvent(dwCtrlEvent: DWORD;


dwProcessGroupId: DWORD): BOOL; stdcall;

This function will send the specified 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.
The sample program in Listing Two only deals with the more basic
behavior of signal handlers. (It’s shown at run time in Figure 5.) We
function SetConsoleCtrlHandler( simply install a pair of control handlers and have them demonstrate
HandlerRoutine: TFNHandlerRoutine; Add: BOOL): the effect of the return values on Windows behavior. As you can see by
BOOL; stdcall;
the behavior of this sample, if none of the other handlers return True
to indicate they’ve handled a given signal, the internal default handler
The HandlerRoutine is a pointer to a function that accepts a single is still executed last. Its behavior is to simply kill the process whenever
DWORD parameter, and returns a BOOL value, called with the stdcall any of the control signals are received. If this isn’t the behavior you
calling convention. The function adds or removes (depending on the want, you must make sure to install a control handler that returns True
value of the Add parameter) your function from the list of registered as soon as possible after your program begins executing.
handlers. In Windows NT/2000, you can also add and remove NULL
as a HandlerRoutine. This special handler is designed specifically to Conclusion
ignore CC; otherwise it passes control to the default handler. In this installment of the console programming series, we went over
the advanced features console applications provide to programmers.
Each handler is called, in the reverse order they were registered, We’ve now covered nearly every aspect of console programming and
whenever a system event occurs. The list of events your handler may the console APIs, and observed how to take very low-level control of
receive in its single DWORD parameter are: the console and its associated buffers.
 CTRL_C_EVENT — User pressed CC.
 CTRL_BREAK_EVENT — User pressed Cak. However, console programming is most effective when used with
 CTRL_CLOSE_EVENT — User attempted to close the console the rest of the Windows API. In the final article in this series,
window. next month, we’ll examine how to use other common Windows
 CTRL_LOGOFF_EVENT — User attempted to log off the programming techniques in conjunction with console applications.
system. In particular, we’ll examine how to use a console within a program
 CTRL_SHUTDOWN_EVENT — User attempted to shut thread, create a message queue that a GUI-less application can use,
down windows. use consoles and graphical windows in the same application, and
replace the standard input and output handles with handles of our
Each handler has the opportunity to handle the system events on its own, including other file handles and anonymous pipes, to perform
own, or pass them along the chain. If the control handler believes it I/O redirection from within a Windows application. ∆
has successfully handled the event, it should return True. This will
abort the processing of the event. Otherwise, it should return False to The sample programs referenced in this article are available on the
pass control to the next handler in the chain. 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 confirm 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 Mike Edenfield is an applications developer for Sylvan Learning Systems in
their handlers has a chance to process that signal for its own process. Baltimore, MD, and an MCSD. He has five years experience with Delphi and Visual
Additionally, you can collect multiple console applications as part of a Basic, and specializes in Microsoft SQL Server development. He can be contacted at
console process group. This is done when creating a new console pro- Michael.Edenfield@educate.com.
cess with CreateProcess. If the CREATE_NEW_PROCESS_GROUP

13 September 2001 Delphi Informant Magazine


On Language

Begin Listing One — ScreenBuffers Program end;

procedure SwitchConsole;
program ScreenBuffers; begin
if hActive = hOriginal then
uses begin
Windows, SysUtils; SetConsoleActiveScreenBuffer(hSecond);
hActive := hSecond;
var end
hInput, hOriginal, hSecond, hActive: THandle; else
arrInputRecs : array[0..9] of TInputRecord; begin
dwCount, dwCur : DWORD; SetConsoleActiveScreenBuffer(hOriginal);
hActive := hOriginal;
bQuit : Boolean = False; end;
coorNew : TCoord = (X:100; Y:100); end;
rectView: TSmallRect =
(Left:0; Top:0; Right:99; Bottom:49); begin
{ First, release our existing console,
const and make a new one. }
OUTPUT_STRING = FreeConsole;
'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()[]{}'; AllocConsole;
FOREGROUND_MAGENTA = FOREGROUND_RED or FOREGROUND_BLUE; SetConsoleTitle('Screen Buffers Demo');
FOREGROUND_BR_MAGENTA = { Get a handle to the auto-created input
FOREGROUND_MAGENTA or FOREGROUND_INTENSITY; and output buffers. }
hInput := GetStdHandle(STD_INPUT_HANDLE);
procedure WriteToScreen(buf: String); hOriginal := GetStdHandle(STD_OUTPUT_HANDLE);
var hActive := hOriginal;
dwCount: DWORD;
begin SetConsoleTextAttribute(hActive, FOREGROUND_BR_MAGENTA);
buf := buf + #13#10; WriteLn('Got Standard Output Handle.');
WriteConsole(hActive, PChar(@buf[1]), Length(buf), { Create a second screen buffer. }
dwCount, nil); hSecond := CreateConsoleScreenBuffer(GENERIC_READ or
end; GENERIC_WRITE, 0, nil, CONSOLE_TEXTMODE_BUFFER, nil);

procedure FillOutputBuffer(hBuf: THandle); { *** Windows 95/98: Comment out from here... }
var if not SetConsoleScreenBufferSize(hSecond, coorNew) then
dwAttr, dwCur: DWORD; WriteLn('error: SetConsoleScreenBufferSize() == ',
begin GetLastError)
dwAttr := 0; else
for dwCur := 0 to 100 do begin WriteLn('Adjusted secondary screen buffer size.');
SetConsoleTextAttribute(hSecond, dwAttr + 1);
WriteConsole(hSecond, PChar(@OUTPUT_STRING[1]), if not SetConsoleWindowInfo(hSecond, True, rectView) then
Length(OUTPUT_STRING), dwCount, nil); WriteLn('error: SetConsoleWindowInfo() == ',
dwAttr := (dwAttr + 1) mod 15; GetLastError)
end; else
Writeln('Secondary buffer populated with data.'); WriteLn('Adjusted secondary screen buffer window.');
end; { *** ...to here. }

procedure ClearOutputBuffer(hBuf: THandle); FillOutputBuffer(hSecond);


var { Process the input loop here. }
cbsi: TConsoleScreenBufferInfo; while not bQuit do begin
coorClear: TCoord; ReadConsoleInput(hInput, arrInputRecs[0], 10, dwCount);
dwWritten: DWORD; for dwCur := 0 to dwCount - 1 do begin
cFill: Char; case arrInputRecs[dwCur].EventType of
begin KEY_EVENT:
GetConsoleScreenBufferInfo(hBuf, cbsi); with arrInputRecs[dwCur].Event.KeyEvent do
if bKeyDown then
coorClear.X := 0; if (dwControlKeyState and
coorClear.Y := 0; ENHANCED_KEY) > 9 then
cFill := ' '; case wVirtualKeyCode of
FillConsoleOutputCharacter(hBuf, cFill, VK_UP :
cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten); ScrollViewWindow(hActive, True);
end; VK_DOWN :
ScrollViewWindow(hActive, False);
procedure ScrollViewWindow(hBuf: THandle; bUp: Boolean); end
var else
rectNew: TSmallRect; case Ord(AsciiChar) of
begin 13: SwitchConsole;
if bUp then Ord('?'):
rectNew.Top := -1 begin
else WriteLn('Console commands: ');
rectNew.Top := 1; WriteLn(' ? - This Help Summary');
rectNew.Bottom := rectNew.Top; WriteLn(' C - Clear secondary output buffer.');
rectNew.Left := 0; WriteLn(' F - Fill secondary output buffer.');
rectNew.Right := 0; WriteLn(' Q - Quit program.');
WriteLn(' <ent> - Toggle active screen buffer.');
SetConsoleWindowInfo(hBuf, False, rectNew);

14 September 2001 Delphi Informant Magazine


On Language

WriteLn('<up> - Scroll screen buffer up.'); WriteLn('OFF');


WriteLn('<dn> - Scroll screen buffer down.'); SetConsoleTextAttribute(hStdOutput, FOREGROUND_BR_CYAN);
end; WriteLn('Console Handler 1 has fired ',
Ord('C'): ClearOutputBuffer(hSecond); nHandler1, ' times.');
Ord('F'): FillOutputBuffer(hSecond); WriteLn('Console Handler 2 has fired ',
Ord('Q'): bQuit := True; nHandler2, ' times.');
end; // case Ord(AsciiChar)... end;
end; // case arrInputRecs...
end; // for dwCur... function Handler1(dwSignal: DWORD): BOOL; stdcall;
end; // while not bQuit... begin
CloseHandle(hSecond); Inc(nHandler1);
FreeConsole; PaintScreen;
end. Result := bHandler1;
end;

End Listing One function Handler2(dwSignal: DWORD): BOOL; stdcall;


begin
Inc(nHandler2);
PaintScreen;
Result := bHandler2;
Begin Listing Two — Control-handlers Program end;

{$APPTYPE CONSOLE} begin


uses hStdInput := GetStdHandle(STD_INPUT_HANDLE);
Windows; hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
{ Register them in reverse order so they
var fire Handler1 then Handler2. }
bHandler1, bHandler2: Boolean; SetConsoleCtrlHandler(@Handler2, True);
nHandler1, nHandler2: Integer; SetConsoleCtrlHandler(@Handler1, True);
hStdOutput, hStdInput: THandle;
arrInputRecs: array[0..9] of TInputRecord; ClearOutputBuffer;
dwCur, dwCount: DWORD; PaintScreen;
cCur: Char;
while True do begin
const ReadConsoleInput(hStdInput, arrInputRecs[0], 10,
FOREGROUND_BR_CYAN = FOREGROUND_BLUE + FOREGROUND_GREEN + dwCount);
FOREGROUND_INTENSITY; for dwCur := 0 to dwCount - 1 do
FOREGROUND_BR_RED = FOREGROUND_RED + case arrInputRecs[dwCur].EventType of
FOREGROUND_INTENSITY; KEY_EVENT:
with arrInputRecs[dwCur].Event.KeyEvent do begin
procedure ClearOutputBuffer; cCur := AsciiChar;
var if (not bKeyDown) and (cCur = '1') then
cbsi: TConsoleScreenBufferInfo; begin
coorClear: TCoord; bHandler1 := not bHandler1;
dwWritten: DWORD; PaintScreen;
cFill: Char; end;
begin if (not bKeydown) and (cCur = '2') then
GetConsoleScreenBufferInfo(hstdOutput, cbsi); begin
coorClear.X := 0; bHandler2 := not bHandler2;
coorClear.Y := 0; PaintScreen;
cFill := ' '; end;
FillConsoleOutputCharacter(hStdOutput, cFill, end;
cbsi.dwSize.X * cbsi.dwSize.Y, coorClear, dwWritten); end;
end; end;
end.
procedure PaintScreen;
var
coorHome: TCoord; End Listing Two
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


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

T 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 interest- line in the Code editor. This is a feature I have
ing feature, and provides a simple key binding class had in other code editors, and now, through key
that you can use as a starting point for creating bindings, in Delphi and Kylix.
your own custom key bindings. This key binding
class, which I am calling the duplicate line key Another interesting thing about this key binding
binding, makes a duplicate or copy of the current is that the DupLine.pas unit, which you can down-
load (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.

Writing editor key bindings involves creating


class type declarations and implementing inter-
faces. If you’re unfamiliar with these topics, you
may still be able to create key bindings by fol-
lowing 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 read-
ing the appropriate sections of the Delphi Devel-
oper’s Guide or the Kylix Developer’s Guide, which
are part of the documentation that ships with
Figure 1: The Key Mappings page of the Editor Properties dialog box. the respective products.

16 September 2001 Delphi Informant Magazine


In Development

IOTAKeyboardBinding = interface(IOTANotifier) As you can see, this interface declares four methods and three
['{F8CAF8D7-D263-11D2-ABD8-00C04FB16FB3}'] properties. Your key binding class must implement the methods.
function GetBindingType: TBindingType; Note, however, that it does not need to implement the properties.
function GetDisplayName: string;
(This is a regular source of confusion when it comes to interfaces,
function GetName: string;
procedure BindKeyboard( but the fact is that the properties belong to the interface and
const BindingServices: IOTAKeyBindingServices); are not required by the implementing object. Sure, you can imple-
ment the properties in the object, but you don’t have to. I didn’t
property BindingType: TBindingType read GetBindingType; in this example.)
property DisplayName: string read GetDisplayName;
property Name: string read GetName;
end; In addition to the methods of the IOTAKeyboardBinding interface,
your key binding class must include one additional method for
Figure 2: The declaration of the IOTAKeyboardBinding interface each custom keystroke you want to add to the editor. To be
in the ToolsAPI unit. compatible with the AddKeyBinding method used to bind these
additional methods, the methods must be TKeyBindingProc type
type methods. The following is the declaration of the TKeyBindingProc
TDupLineBinding = class(TNotifierObject, method pointer type, as it appears in the ToolsAPI unit:
IOTAKeyboardBinding)
private
public TKeyBindingProc = procedure (const Context: IOTAKeyContext;
procedure DupLine(const Context: IOTAKeyContext; KeyCode: TShortcut; var BindingResult: TKeyBindingResult)
KeyCode: TShortcut; of object;
var BindingResult: TKeyBindingResult);
{ IOTAKeyboardBinding }
function GetBindingType: TBindingType;
This declaration indicates that the additional methods you write, each of
function GetDisplayName: string; which adds a different keystroke combination to the editor, must take
function GetName: string; three parameters: IOTAKeyContext, TShortcut, and TKeyBindingResult.
procedure BindKeyboard(const BindingServices:
IOTAKeyBindingServices);
Figure 3 shows the key binding class declared in the DupLine.pas unit.
end;
This class, named TDupLineBinding, includes only one new key binding.
Figure 3: Type declaration of the TDupLineBinding class.
Implementing the Interface
Once you’ve declared your key binding class, you must implement
Creating and installing an editor key binding involves a number of the four methods of the IOTAKeyboardBinding interface, as well
explicit steps: as each of your additional TKeyBindingProc methods. Fortunately,
1) Descend a new class from TNotifierObject. This class must be implementing the IOTAKeyboardBinding interface is easy.
declared to implement the IOTAKeyboardBinding interface. This
class is your key binding. Implement GetBindingType by returning the type of key binding that
2) In addition to the four methods of that IOTAKeyboardBinding you’re creating. There are only two types of key bindings: partial and
interface you must implement in your key binding class, add complete.
one additional method for each key combination you want to
add to the editor. This method is passed an object that imple- A complete key binding defines all the keystrokes of the editor. You can
ments the IOTAKeyContext interface. Use this object within your identify your key binding as a complete key binding by returning the
implementation to read about and control the behavior of the value btComplete. For example, the New IDE Emacs key mapping that
editor. ships with Kylix and Delphi 6 is defined by a complete key binding.
3) Declare and implement a stand-alone Register procedure. Within Fortunately, Kylix ships with the source code for this key mapping,
this procedure, invoke the AddKeyboardBinding method of the which is quite useful if you want to examine a complicated key binding.
BorlandIDEServices object, passing an instance of the class you
declared in the first step as the only argument. A partial key binding is used to add one or more keystrokes to the
4) Add the unit that includes this Register procedure to an installed key mapping you’re using. The TDupLineBinding class is a partial
design-time package. key binding. Here’s the implementation of GetBindingType in the
TDupLineBinding class:
Each of these steps is discussed in the following sections. As men-
tioned earlier, these steps will define a new key binding that adds a function TDupLineBinding.GetBindingType: TBindingType;
single key combination. Once implemented and installed, this key begin
Result := btPartial;
combination will permit you to duplicate the current line in the
end;
editor by pressing CD.

Declaring the Key Binding Class You can implement GetDisplayName and GetName to provide the editor
The class that defines your key binding must descend from with text descriptions of your key binding. GetDisplayName should
TNotifierObject and implement the IOTAKeyboardBinding inter- return an informative name that Kylix will display in the Enhancement
face. Those familiar with interfaces will recall that when a class modules list on the Key Mappings page. On the other hand, GetName is
is declared to implement an interface, it must declare and imple- a unique string the editor uses internally to identify your key binding. By
ment all the methods of that interface. Consider the declaration of convention, this name should be your company name or initials followed
the IOTAKeyboardBinding interface (see Figure 2), which appears by the name of your key binding, because this string must be unique for
in the ToolsAPI unit. all key bindings a user might install.

17 September 2001 Delphi Informant Magazine


In Development

procedure TDupLineBinding.DupLine( following is the declaration of TShiftState, as it appears in the Classes


const Context: IOTAKeyContext; KeyCode: TShortcut; unit:
var BindingResult: TKeyBindingResult);
var TShiftState = set of (ssShift, ssAlt, ssCtrl,
ep: IOTAEditPosition; ssLeft, ssRight, ssMiddle, ssDouble);
eb: IOTAEditBlock;
r, c: Integer;
begin The second parameter of BindKeyboard is a reference to your
try TKeyBindingProc method that implements the behavior you want to
ep := Context.EditBuffer.EditPosition;
associate with the keystroke or key combination, and the third parameter
ep.Save;
// Save current cursor position. is a pointer to a context. In the BindKeyboard implementation in the
r := ep.Row; TDupLineBinding class, the method DupLine is passed as the second
c := ep.Column; parameter, and nil is passed in the third parameter. The following is
eb := Context.EditBuffer.EditBlock; the implementation of the BindKeyboard method that appears in the
ep.MoveBOL;
eb.Reset;
DupLine.pas unit:
eb.BeginBlock;
eb.Extend(EP.Row+1,1); procedure TDupLineBinding.BindKeyboard(
eb.EndBlock; const BindingServices: IOTAKeyBindingServices);
eb.Copy(False); begin
ep.MoveBOL; BindingServices.AddKeyBinding(
ep.Paste; [Shortcut(Ord('D'), [ssCtrl])], DupLine, nil);
// Restore cursor position. end;
ep.Move(r, c);
finally As you can see, this BindKeyboard implementation will associate the code
ep.Restore;
end;
implemented in the DupLine method with the CD combination.
BindingResult := krHandled;
end; Implementing TKeyBindingProc Methods
Implementing the methods of IOTAKeyboardBindings is pretty straight-
Figure 4: Implementing TKeyBindingProc in the TDupLineBinding forward. Implementing your TKeyBindingProc method, however, is not.
class.
As you can see from the TKeyBindingProc method pointer type dec-
The following two functions implement the GetDisplayName and laration shown earlier in this article, a TKeyBindingProc is passed
GetName methods for the TDupLineBinding class: three parameters. The first, and most important, is an object that
implements the IOTAKeyContext interface. This object is your direct
function TDupLineBinding.GetDisplayName: string; link to the editor, and you can use its properties to control cursor
begin position, block operations, and views. The second parameter is the
Result := 'Duplicate Line Binding';
TShortcut that was used to invoke your method. This is useful if
end;
you passed more than one TShortcut in the first parameter of the
function TDupLineBinding.GetName: string; AddKeyBinding invocation — especially if you want the behavior to
begin be different for different keystrokes or key combinations.
Result := 'jdsi.dupline';
end;
The final parameter of your TKeyBindingProc method is a
TKeyBindingResult value passed by reference. Use this parameter to signal
You implement the BindKeyboard method to bind your TKeyBindingProc to the editor what it should do after your method exits. The following is
methods. BindKeyboard is passed an object that implements the the TKeyBindingResult declaration as it appears in the ToolsAPI unit:
IOTAKeyBindingServices interface; use this reference to invoke the
AddKeyBinding method. TKeyBindingResult = (krUnhandled, krHandled, krNextProc);

AddKeyBinding requires at least three parameters. The first is an array Set the BindingResult formal parameter of your TKeyBindingProc method
of TShortcut references. A TShortcut is a Word type that represents to krHandled if your method has successfully executed its behavior. Set-
either a single keystroke, or a keystroke plus a combination of one ting BindingResult to krHandled also has the effect of preventing any
or more of the following: C, A, or S, as well as left-, other key bindings from processing the key, as well as preventing menu
right-, middle-, and double-mouse clicks. Because this parameter can items assigned to the key combination from processing it.
include an array, it’s possible to bind your TKeyBindingProc to two or
more keystrokes or key combinations. Set BindingResult to krUnhandled if you don’t process the keystroke or
key combination. If you set BindingResult to krUnhandled, the editor
The Menus unit in Delphi, and the QMenus unit in Kylix, contain will permit any other key bindings assigned to the keystroke or key
a function named Shortcut that you can use to easily create your combination to process it, as well as any menu items associated with
TShortcut references. This function has the following syntax: the key combination.

function Shortcut(Key: Word; Shift: TShiftState): Set BindingResult to krNextProc if you have handled the key, but want to
TShortcut; permit any other key bindings associated with the keystroke or key com-
bination to trigger as well. Similar to setting BindingResult to krHandled,
where the first parameter is the ANSI value of the keyboard character, setting BindingResult to krNextProc will have the effect of preventing
and the second is a set of zero, one, or more as TShiftState. The menu shortcuts from receiving the keystroke or key combination.

18 September 2001 Delphi Informant Magazine


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

As you can see from this code, the IOTAKeyContext implementing


object passed in the first parameter is your handle to access a variety
of objects that you can use to create your response. Without a doubt,
it’s 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
Figure 5: The Into new package page of the Install Component IOTAEditView implementing objects. These objects are available using
dialog box. 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 config-
ured 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 file, 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.
Finally, you use the TOTAEditView object to get information
about, and — to some extent — control the various editor win-
As mentioned earlier, the real trick in implementing your dows. For example, you can use this object to determine how many
TKeyBindingProc method is associated with the object that imple- units are open, scroll individual windows, make a given window
ments the IOTAKeyContext interface that you receive in the active, and get, set, and go to bookmarks.
Context formal parameter. Unfortunately, Borland has published
almost no documentation about how to do this. In fact, I found Turning our attention back to the DupLine method, the code
only four sources of information: begins by assigning the IOTAEditPosition implementing object to
1) The first is the declarations and somewhat intermittent com- an IOTAEditPosition reference. While this isn’t an essential step, it
ments located in the ToolsAPI unit. This is one of the first places simplifies the code in this method, reducing the need for repeated
you should look to understand the objects you’ll be able to work references to Context.EditBuffer.EditPosition. The IOTAEditPosition
with. You can find this unit in the \Source\Toolsapi directory variable (ep) is then used to save a reference to the current position
where you installed Delphi (/source/toolsapi for Kylix). of the cursor. Next, an IOTAEditBlock variable (eb) is assigned the
2) Equally valuable is the source code for the New IDE Emacs object referenced by the Context.EditBuffer.EditBlock property. As
keymapping. As mentioned earlier, this is a complete key bind- with the ep variable, this is done to reduce the amount of typing
ing. You can find this in the \Demos\ToolsAPI\Keybindings\ (as well as to avoid using a with clause, which could be pretty
Emacs directory where you installed Delphi 6 (demos/toolsapi/ confusing since many of these objects have similar or identical
keybindings/emacs for Kylix). While the units that make up property names).
the New IDE Emacs key mapping have almost no comments,
you can get a very good idea about how to work with the Using these two references, the cursor position is moved to the first
Context parameter. column of the current line. A new block is then started, extended
3) The last two resources are useful if you have a copy of Delphi one line, and copied. Next, the cursor is repositioned at the begin-
handy. Specifically, Delphi includes a fairly small partial key ning of the line and the block is pasted. Returning the cursor to
binding that you can examine. That key binding can be found its original position completes the operation, which is signaled by
in the \Demos\ToolsAPI\Editor Keybinding directory, where setting the BindResult formal parameter to krHandled. The result
either Delphi 5 or Delphi 6 is installed. is that the current line is duplicated without the cursor appearing
4) Finally, Allen Bauer, a member of the Tools API development to move in the editor.
team for Delphi and Kylix R&D, has published a Wizard that
generates a key binding. This Wizard is available for download The Register Procedure
from http://community.borland.com/. Be aware, however, that For your key binding to be installed successfully into the
this Wizard generates more than just a key bindings class. editor, you must register it with a design-time package
In other words, it is more complicated than a key binding, using a Register procedure. The Register procedure’s name is
and therefore you probably shouldn’t start your search for case-sensitive and must be forward-declared in the interface
information about implementing key bindings with the gener- section of the unit that will be installed into the design-time
ated code. package. Furthermore, you must add an invocation of the
IOTAKeyBindingServices.AddKeyboardBinding method to the
A full discussion of the properties of IOTAKeyContent is well implementation of this procedure, passing an instance of your
beyond the scope of this article. The resources referred to here key binding class as the sole parameter.

19 September 2001 Delphi Informant Magazine Delphi Informant Magazine September 2001 19
In Development
4) Set the Package file name to the name you want to give your
package. In Figure 6, this file 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 file is the hidden .Borland
directory, located in the user’s home directory.) Also provide
a brief description for the package in the Package description
field.
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 pack-
age, you will see a dialog box confirming 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
Figure 7: The Package editor. 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 direc-
tory, 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
As you’ve 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. ∆
Figure 8: The newly installed key binding appears on the Key
Mappings page of the Editor Properties dialog box. The file referenced in this article is available on the Delphi Informant
Magazine Complete Works CD located in INFORM\2001\SEP\
Invoke this method by dynamically binding the BorlandIDEServices DI200109CJ.
reference to the IOTAKeyboardServices interface, and pass an invoca-
tion of your key binding object’s constructor as the argument. The
following is how the Register procedure implementation appears in
the DupLine unit:

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

Creating and Installing a New Design-time Package


The final step in defining a new key binding is to add the unit in
which you register your key binding to a new design-time package. Cary Jensen is president of Jensen Data Systems, Inc., a Houston-based train-
To do this, use the following steps in either Delphi or Kylix: ing and consulting company. He is the author of 18 books, including
1) Save any changes you’ve made to your key binding unit. Building Kylix Applications (Osborne/McGraw-Hill), Oracle JDeveloper
2) Select Component | Install Component to display the Install (Oracle Press), and Delphi In Depth (Osborne/McGraw-Hill). Cary
Component dialog box shown in Figure 5. is also Contributing Editor to Delphi Informant Magazine, and an inter-
3) On the Into new package page, use the Browse button associ- nationally respected trainer and speaker. For information about Cary’s
ated with the Unit file name field to select the unit in which on-site training, public Delphi and Kylix seminars, and consulting
your key binding’s Register procedure appears. In this example, services, please visit http://www.jensendatasystems.com, or e-mail Cary at
the unit name is DupLine.pas. cjensen@jensendatasystems.com.

20 September 2001 Delphi Informant Magazine


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

D 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 that’s compatible
with main IT trends.

Last month, you were introduced to the new fea- main topic of how to query SQL Server 2000 for
tures of Microsoft SQL Server 2000 that make it XML data from Delphi.
an XML-enabled database server, e.g. the ability
to access SQL Servers using the HTTP protocol, Delphi and the Microsoft XML Parser
support for XDR (XML-Data Reduced) schemas, Before we can use the Microsoft XML Parser, we
the ability to specify XPath queries against these need to create an interface unit that will encapsu-
schemas, and the ability to retrieve and write XML late all objects and methods implemented in this
data. You also learned that XML-based data extrac- COM object. To do so, select Project | Import Type
tion is implemented in SQL Server 2000 by the Library in the Delphi IDE. Then, in the Import
FOR XML clause that’s part of the SELECT state- Type Library dialog box, select the Microsoft
ment. The ability to query Microsoft SQL Server XML type library. The version you have installed
directly — without extra data access components on your computer depends on the software you
— and receive data represented as XML documents have, but it’s recommended to have the latest ver-
opens new possibilities for Delphi developers. sion of the Microsoft XML Parser that’s available
free from the Microsoft Web site. As of this writ-
To parse XML documents generated by querying ing, the most recent version is 3.0.
SQL Server, we’ll naturally need a parser. And since
we’re using a Microsoft database, the best choice After selecting the type library, uncheck the
is the Microsoft XML Parser, which provides a lot Generate Component Wrapper check box, and click
of objects, methods, and properties to manipulate Create Unit. The SXML2_TLB.PAS file will be
XML documents. So we’ll begin this month’s discus- created and added to your project. Later, you’ll
sion by describing how Delphi interacts with the need to save it. The usual place for imported type
Microsoft XML Parser, before moving on to the library interface units is the \Lib folder (under
\Delphi5 for example).
Old Name New Name Where
type type_ IXMLDOMNode.nodeType It’s always wise to take a look at the generated Pascal
implementation implementation_ IXMLDOMDocument code, especially when you’re familiar with the objects
type type_ IXMLDOMDocument.createNode and methods of the COM object, or you’re planning
var var_ IXMLDOMSchemaCollection.add to port the existing code to Delphi. At the top of
type type_ IXMLElement the file in the commented “Errors” section, we can
type type_ IXMLElement2 find the changes in method, property, and parameter
Figure 1: Delphi changes some method, property, and parameter names performed by Delphi. In our case, we should
names when it imports the Microsoft XML Parser type library. be aware of the changes shown in Figure 1.

21 September 2001 Delphi Informant Magazine


Columns & Rows

var var
HTTP : IXMLHTTPRequest; HTTP : IXMLHTTPRequest;
XMLDoc : IXMLDomDocument; XMLDoc : IXMLDomDocument;

... ...

XMLDoc := CoDOMDocument.Create; XMLDoc := CoDOMDocument.Create;


HTTP := CoXMLHTTP.Create; // Load remote XML document asynchronously.
// Issue POST HTTP request to the URL. XMLDoc.Async := False;
with HTTP do begin XMLDoc.Load(URL);
Open('POST', URL, False, '', '');
Send('');
// ResponseXML now holds resulting XML document. Figure 3: Loading the remote XML document.
XMLDoc.Load(ResponseXML);
end;
// Generate URL for specified XML method.
function TForm1.SetXMLMethod(XMLMethod: string): string;
Figure 2: Calling the Open and Send methods of the
var
XMLHTTPRequest object. URL : string;
SQL : string;
begin
After putting the reference to the MSXML2_TLB.PAS unit in the uses
URL := 'http://terra/northwind?sql=';
clause of our main form unit, we’re ready to use the Microsoft XML SQL := 'SELECT FirstName, LastName, Title, Notes ' +
Parser in our Delphi code. 'FROM Employees';
SQL := SQL + ' ' + XMLMethod;
Using URL-based Queries URL := URL + StringReplace(SQL, ' ', '%20',
[rfReplaceAll]);
There are a lot of ways to invoke the Web server from Delphi. We can URL := URL + '&root=Northwind';
use the Win32 API functions, third-party libraries or components, or Edit1.Text := URL;
the Microsoft XML Parser’s built-in functionality. Since we’re planning SetXMLMethod := URL;
to deal with XML documents, it makes sense to use just one COM end;

object to do the entire job.


Figure 4: The XMLMethod variable is used to specify one of the
The Microsoft XML Parser provides at least two ways to invoke the FOR XML methods.
Web server via an HTTP protocol. The first is to use the generic
XMLHTTPRequest object, and its Open and Send methods (see Figure 2). // Issue the FOR XML RAW request.
procedure TForm1.Button1Click(Sender: TObject);
Note that the XMLHTTPRequest object gives us a lot of freedom to begin
XMLDoc.Async := False;
specify the type of request — POST, GET, or even a WebDAV- or XMLDoc.Load(SetXMLMethod('FOR XML RAW'));
SOAP-specific request. And, as you can see from the code in Figure 2, Memo1.Text := XMLDoc.XML;
it’s straightforward to use. end;

// Issue the FOR XML AUTO request.


The second way to invoke the Web server via an HTTP protocol is
procedure TForm1.Button2Click(Sender: TObject);
to use the Load method of the XMLDOMDocument object, specifying begin
the URL as the parameter. This method is smart enough to recognize XMLDoc.Async := False;
the local and remote resources. It also has the built-in ability to access XMLDoc.Load(SetXMLMethod('FOR XML AUTO'));
resources specified by the URL. In this case, the code to load the Memo1.Text := XMLDoc.XML;
end;
remote XML document is shown in Figure 3.
// Issue the FOR XML AUTO, ELEMENTS request.
Now let’s look at the URL itself. It should consist of two parts: the procedure TForm1.Button3Click(Sender: TObject);
address of the Web server (along with the name of the virtual direc- begin
XMLDoc.Async := False;
tory), and the SQL query we’re planning to execute. That’s why we
XMLDoc.Load(SetXMLMethod('FOR XML AUTO, ELEMENTS'));
create the URL in the code below by combining the URL and SQL Memo1.Text := XMLDoc.XML;
strings. Note that the StringReplace function is used to convert end;
spaces to the %20 escape sequence, because they’re recognized as
a valid part of the URL: Figure 5: OnClick event handlers for the different FOR XML
modes.
URL := 'http://terra/northwind?sql=';
SQL := 'SELECT * FROM Employees'; The code is wrapped in the SetXMLMethod function, shown in Figure 4.
SQL := SQL + XMLMethod;
URL := URL + StringReplace(
SQL, ' ', '%20', [rfReplaceAll]); Now we can put together a quick and dirty demonstration program.
URL := URL + '&root=Northwind'; Place three Button components, one Edit component, and one Memo
component on the new form. Each button will use one of the FOR
We’ve also used the XMLMethod variable, which specifies one of the XML modes. Figure 5 shows the OnClick event handlers for each.
FOR XML modes:
 FOR XML RAW By clicking on one of the buttons, we issue a URL request to SQL
 FOR XML AUTO Server, receive it in the XMLDoc object, and show the resulting XML
 FOR XML AUTO, ELEMENTS document in the Memo component. Figure 6 shows the result of a

22 September 2001 Delphi Informant Magazine Delphi Informant Magazine September 2001 22
Columns & Rows

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


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

// Get column names.


procedure TForm1.ListView1Click(Sender: TObject);
if Attribs <> nil then
var
begin
Item : TListItem;
for I := 0 to Attribs.Length-1 do begin
begin
CI := ListView1.Columns.Add;
Item := (Sender As TListView).Selected;
CI.Width := -1;
Memo1.Text := Root.ChildNodes[Item.Index].XML;
CI.Caption := Attribs.Item[I].NodeName;
end;
CI.AutoSize := True;
end;
Figure 9: Display the XML representation for one row of data.
// 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 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. Let’s look at how
we can use the XML document in our Delphi applications.
Figure 10: One row of data is shown in the Memo component.
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 Next, extract the table’s column names. To do so, iterate the Attribs
with the generic <row /> element, and each column is mapped to collection (of the IXMLDOMNamedNodeMap type) of the first child
an attribute of this element. The attribute name equals the name node of the XML document, and extract the names of each node in
of the column. this collection (see the first for loop in Figure 7).

Let’s see how to process this document and display its contents in a Now we can show the data. This is done with a similar technique, as
ListView component. First, load the XML document that results from shown in the second for loop in Figure 7, but this time we iterate all
the following query: child nodes of the XML document. The result of our manipulations
is shown on another example form, containing a Button and ListView
SELECT * FROM Customers FOR XML RAW component (see Figure 8).

Then take the root node of the XML document, and access its attri- To study the resulting XML representation for just one row of data, let’s
butes node: add a Memo component to the form. Then enter the code shown in
Figure 9 for the OnClick event handler for the ListView component.
Root := XMLDoc.DocumentElement; Now, when we run our example and click on a row, we’ll receive its
Attribs := Root.FirstChild.Attributes; XML representation, as shown in Figure 10.

23 September 2001 Delphi Informant Magazine


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;

// Show next row.


procedure TForm1.SpeedButton3Click(Sender: TObject);
Figure 11: A simple one-row browser.
begin
if Index < Nodes.Length-1 then Inc(Index);
procedure TForm1.Button1Click(Sender: TObject); ShowOneNode(Index)
var end;
URL, SQL : string;
begin // Show last row.
XMLDoc := CoDOMDocument.Create; procedure TForm1.SpeedButton4Click(Sender: TObject);
URL := 'http://terra/northwind?sql='; begin
SQL := 'SELECT CompanyName, ContactTitle, '; Index := Nodes.Length-1;
SQL := SQL + 'ContactName, Phone FROM Customers'; ShowOneNode(Nodes.Length-1);
SQL := SQL + ' FOR XML RAW'; end;
URL := URL + StringReplace(SQL, ' ', '%20',
[rfReplaceAll]); Figure 14: Code for navigating rows.
URL := URL + '&root=Northwind';
XMLDoc.Async := False;
XMLDoc.Load(URL); After this the first node is shown using the ShowOneNode procedure
Root := XMLDoc.DocumentElement;
Nodes := Root.SelectNodes('//row');
(see Figure 13).
Index := 0;
ShowOneNode(Index); The code for the four SpeedButtons — one for the first row, the
end; previous row, the next row, and the last row — is shown in Figure 14.

Figure 12: Loading the one-row browser. Conclusion


That’s it for this month. We’ve seen how to extract data from Microsoft
SQL Server 2000 via its XML features. These features allow us to
procedure TForm1.ShowOneNode(I: Integer);
var extract data without using any data access components by directly
Node : IXMLDOMNode; specifying our SQL query, and using the HTTP protocol to talk with
begin the SQL Server. We also saw several examples of how to represent the
Node := Nodes[I]; extracted data in Delphi applications by working with the Microsoft
Edit1.Text := Node.Attributes[0].Text;
Edit2.Text := Node.Attributes[1].Text;
XML Document Object Model (DOM).
Edit3.Text := Node.Attributes[2].Text;
Edit4.Text := Node.Attributes[3].Text; Next month, we’ll examine data querying techniques for two more
end; FOR XML modes (AUTO and EXPLICIT), and demonstrate how to
use XML templates to separate our Delphi code from the XML-based
Figure 13: The ShowOneNode procedure.
queries. See you then. ∆

This was a very simple example of how to show XML-based data in The projects referenced in this article are available on the Delphi Infor-
Delphi applications. We’ve assumed that all records have columns filled mant Magazine Complete Works CD located in INFORM\2001\SEP\
with information, but if we look at some of the records, we’ll find that DI200109AF.
not all of the columns are represented in the resulting XML document.
This is because when some columns have Null as their content, they
aren’t presented in the XML document.

Getting One Row at a Time


In this example we’ll 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). Alex Fedorov is a Chief Technology Officer for Netface SA, based in Lausanne,
Then, in the OnClick event handler for the Button, enter the Switzerland (http://www.netface.ch). He was one of the co-authors of Profes-
code shown in Figure 12. sional Active Server Pages 2.0 (Wrox, 1998) and ASP Programmer’s Refer-
ence (Wrox, 1998), as well as Advanced Delphi Developer’s Guide to ADO
Here, we extract the CompanyName, ContactTitle, ContactName, and (Wordware, 2000).
Phone columns from the Customers table, and create a list of nodes.

24 September 2001 Delphi Informant Magazine 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

L 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 its shared libraries somewhere inside the application’s
shell scripts to launch an application. In the process, installation directory. The simplest method is to
it demonstrates a simple use of some of the Linux install the executable, and all the shared libraries,
system calls in the Libc unit. directly into the application’s directory.

The Problem: Finding the Libraries When a program requires shared libraries installed
Applications typically install their shared libraries in outside of /usr/lib, users generally need to set an envi-
one of these locations: ronment variable, LD_LIBRARY_PATH, to include
 A standard system directory that’s automatically these directories. LD_LIBRARY_PATH is a colon-
searched by the system for shared libraries, such separated list of directory names for the Linux loader
as /usr/lib. to search when looking for shared libraries.
 The directory containing the application’s execut-
able. For example, suppose your application is called Foo-
 A directory named “lib” under the application’s bler and is installed in the directory /usr/local/
directory. With this option the application’s exe- Foobler. Further suppose that the application uses
cutables are generally stored in a directory named several shared libraries that are all installed in the
“bin” under the application’s root directory. Foobler directory. Any user that wants to run
Foobler will need to modify his or her shell init file
Copying the shared libraries into the /usr/lib directory (.profile for bash or ksh, .login for csh) to define
has the advantage of simplicity. Once the library has LD_LIBRARY_PATH with the Foobler directory. If
been installed, all users can launch the application the users are using bash (the default shell for new
there without adjusting any of their environment vari- accounts on many Linux distributions) they will need
ables. However, there are also some potential draw- to add the following to their .profile file:
backs:
 Only the root user is allowed to copy files into LD_LIBRARY_PATH=/usr/local/Foobler:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
/usr/lib. Often, users other than the root will
want to install and use the application without
involving a system administrator. Updating the .profile file for every new application
 Other applications that install libraries into /usr/ can be tedious, and difficult to explain to new Linux
lib might overwrite the application’s shared librar- users. Many applications (including Kylix itself) work
ies with incompatible versions. around this problem by installing a “launcher” script
that adjusts environment variables before starting the
To allow users other than the root to install and application. The primary tasks for a launcher program
update your application, the best option is to install are to:

25 September 2001 Delphi Informant Magazine


Kylix Tech

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


APP_SUFFIX = '.bin'; var
len : Cardinal;
procedure FindTarget(var target_name, target_dir: string); begin
var len := Length(source) + 1;
my_name : string; GetMem(dest, len);
begin StrLCopy(dest, PChar(source), len);
my_name := ExpandFileName(ParamStr(0)); end;
target_name := my_name + APP_SUFFIX;
target_dir := ExtractFileDir(my_name); procedure ExecTarget(target_name: string);
end; type
parray = array[0..0] of PChar;
Figure 1: Identifying the application directory. pparray = ^parray;
var
argv_size : size_t;
procedure SetPath(path_var: string; target_dir: string); argv : pparray;
var begin
path : string; argv_size := (ArgCount + 1) * SizeOf(PChar);
begin GetMem(argv, argv_size);
path := GetEnvironmentVariable(path_var); memcpy(argv, ArgValues, argv_size);
if (Length(path) > 0) then CopyArgStr(argv[0], target_name);
path := target_dir + ':' + path; execv(PChar(target_name), PPChar(argv));
else end;
path := target_dir;
setenv(PChar(path_var), PChar(path), 1);
Figure 3: Launching an application: copying the ArgValues
end;
array, and calling execv.
Figure 2: Updating the PATH and LD_LIBRARY_PATH environ-
ment variables.
relied mostly on routines from the System unit. However, launching the
application involves a call to a Linux system call located in the Kylix
 identify the location of the application and its libraries; Libc unit. The system call, execve, allows a program to recreate itself as
 set environment variables required to launch the application; another program.
 launch the application with the customized environment and pass
along all the arguments provided on its command line. On UNIX-based systems such as Linux, every running process has a
parent process. Therefore, starting a new program generally involves
The following sections present an alternative launcher written in Kylix. 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
Finding the Application Root process that created it. After the fork call, both the parent and child
The sample launcher program presented here assumes that the appli- processes have nearly identical memory images, and execution of each
cation to be launched is installed in the same directory as the process continues at the instruction immediately following the call to
launcher, and has the same file name as the launcher, except for the fork. Usually the fork call is made in a conditional statement that allows
addition of a .bin extension. Likewise, the shared libraries used by the parent to continue processing normally, while the child performs a
the application are assumed to be located in the launcher’s directory. few housekeeping chores and then calls the execve system call.
Given these assumptions, the launcher can easily identify the applica-
tion directory using the ParamStr and ExpandFileName functions The execve system allows a process to completely recreate itself as an
from the System unit, as shown in Figure 1. instance of a different program. When execve is called, the Linux kernel
identifies the executable file to be executed and replaces the running pro-
The .bin extension was chosen for this example simply to distin- cess’ current memory image with one appropriate to the new program. In
guish the name of the target from the name of the launcher. Linux a sense, the process is transformed from one program to another.
doesn’t have a standard convention for executable file names;
any extension could be used. However, use of .exe should be There’s no need to call fork in the case of the launcher program. The
avoided, because that extension could confuse users into thinking launcher can simply transform itself into an instance of the application
the program is a Windows executable rather than one for Linux. once the environment is initialized.

Updating the Environment The Libc unit provides a family of related procedures for invoking
Once the application directory has been discovered, the PATH and the execve system call. Before calling execve, each procedure accepts
LD_LIBRARY_PATH environment variables can be updated within slightly different parameters and performs some special processing.
the launcher’s own environment using the GetEnvironment function The launcher example uses the execv procedure, which accepts the
from the System unit, and the setenv procedure from the Libc unit, absolute path to the executable file to launch and the complete set of
as shown in Figure 2. command-line arguments for the program.

Adding the application directory to the PATH environment variable The Kylix System unit exposes the command-line arguments to the
isn’t strictly necessary. Doing so, however, allows the application to application as the platform-specific global variables, ArgCount and
more easily launch other executables in its own directory. ArgValues. ArgCount contains the number of command-line argu-
ments in ArgValues. ArgValues is a pointer to an array of PChars with
Launching the Application ArgValues+1 elements. The first element of the array contains the path
Finding the program to launch and updating the environment both to the executable file of the running program. The last element of the

26 September 2001 Delphi Informant Magazine


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 first element to refer
to the program being launched. Then it calls execv to actually launch
the program. Replacing the first element in ArgValues isn’t strictly
necessary, but by doing so the launched application can easily identify
its own executable file, and is completely oblivious to the fact that it
was started by the launcher, rather than the user’s 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 applica-
tion stores its executable files in a bin directory, and its shared libraries
in a lib directory, the launcher could be modified 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 vari-


ables needed by the application. For example, Kylix sets the variables
HHHOME and XPPATH in its kylixpath script. ∆

The files 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

A 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, ment to HTML or RTF. The integration of all these
deals with COM every day. COM is used by parts will constitute your word processor.
Microsoft Office when we run the spell-check util-
ity, by many Web sites running IIS, and by the Now, imagine that you want to be able to update
operating system for some of its mundane tasks. any of these single elements without redeploying the
Some developers choose COM specifically to build whole application. You may also want to make those
complex, scalable, and secure enterprise systems. components available to a second application — per-
haps developed by someone else in another language.
Integration Yesterday These are common scenarios. Today’s applications are
In our field, integration can be accomplished in much bigger than those of a few years ago, so any-
many ways, and it can be applied to many things. thing that can help tame this complexity is welcome.
Imagine you are developing a word processor appli-
cation. You will create or inherit a custom memo The first approach you may try is to use DLLs.
control for editing purposes, you may include a Dynamic-link libraries allow us to bind to and
spell checker, and — if you want to get fancy invoke executable code at run time. In our hypo-
— you may also want to include a set of custom thetical word processor, the DLL that includes
routines that allows your users to convert the docu- 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, you’d


only have to update the DLL and the word pro-
cessor would automatically have access to the
new functionality.

Figure 1: Delphi’s Type Library editor allows us to view and edit This works fine and you will achieve your objec-
COM-type libraries. tive. GetConverters returns a custom TList (TCon-

28 September 2001 Delphi Informant Magazine


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 func-
tions to return an object, the client has to know its interface. An
interface is a very important concept in object-oriented program-
ming 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 wouldn’t know how to find the correct method addresses, the
parameters, and result types, etc.

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


independent, COM defines a binary standard for interface definition
and introduces the concept of type libraries. Type libraries are binary
Figure 2: Delphi uses type library information to generate inter- files that contain information about a number of interfaces, i.e. how
faces it can use. many and what they look like.

verterList) which may have some Delphi methods that make it handy Let’s take a look. In Delphi, open the COMConverter.tlb type library
and easy to use. contained in the \COMConverter directory (created by the example
application). As you can see in Figure 1, this type library defines the
Unfortunately this isn’t an optimal solution. Worse, it doesn’t work unless IConverterList interface, which contains the Count and Items properties.
you’re using Delphi or Borland C++Builder. To use the result of the
function GetConverters pointer as a TConverterList, the client needs to Delphi knows how to interpret type libraries, and present them in a
know what a TConverterList is. To do this, you need to share that user-friendly fashion via its Type Library editor. Microsoft Visual Basic,
information. Even if you do, however, you’d have a problem with non- Borland C++Builder, or Microsoft Visual C++ do the same. They all
Borland compilers. TList is a VCL class. It’s not included, for instance, in support the COM binary standard and play according to its rules. Now,
Microsoft Visual C++ or Visual Basic; developers in those environments from within the Type Library editor, press @. Delphi will create a file
couldn’t benefit from the pointer we return. named COMConverter_TLB.pas (see Figure 2).

You could have structured your DLL differently, following, for exam- Through COM, I can define an interface that other COM-enabled
ple, the approach of the Windows API function, EnumWindows, languages can understand. Delphi will use that information to generate
which takes a pointer to a callback routine. Another solution would interfaces that it can understand and use, as we just saw. It’s all there and
have been to export more functions. Whichever approach you may ready to be used, as if it were a regular Delphi object. There are some key
choose, however, you’d still be confined to a world of simple data differences, but let’s continue with the principles.
types that is everything but object-oriented. On top of that, the DLL
has to be run on the client’s computer. Dynamic Linking
Similar to DLLs, COM allows — and actually only works through —
Integration Today dynamic linking. You can choose to take advantage of this in two ways:
COM is one of the technologies that helps us resolve some of these early binding or late binding.
issues. COM has a long story. Officially, the acronym COM was first
used around 1993. We can trace COM roots back to Windows 3.x Before we continue, you need to register your COM library. Registration
where DDE and OLE were used in Microsoft Word and Excel is the process through which Windows becomes aware of a COM object
as a sort of rudimentary communication and interoperability glue. and learns how to instantiate it. To do this, you need to use a special tool
Today COM is everywhere on the Windows platform. Small applica- called REGSVR32.EXE (contained in Windows\System32), or Borland’s
tions such as ICQ, CuteFTP, or Allaire HomeSite, are accessible equivalent tregsvr.exe (in \Delphi5\Bin for example). Another way, if you
through COM. Application suites such as Microsoft Office are based have the Delphi source code, is to open the COM project (in our case
on COM. Windows-based enterprise systems leverage COM and COMConverter.dpr) and select Run | Register ActiveX Server.
Microsoft Transaction Server for business-critical operations. If you
develop on Windows, you will have to face COM sooner or later. The Registering a COM server means inserting special keys into the
sooner you do, the easier it will be. Windows registry. The information you will store includes the
name of the DLL or EXE file that hosts your COM object, the
This article is about understanding COM and the reasons it’s impor- identifiers that uniquely identify it (see the yellow on green code
tant, rather than providing another how-to tutorial. We’ll start with in Figure 2) and a few other items. If you don’t do this, Windows
the principles behind COM, then provide a concrete example you won’t be able to instantiate your COM object.
can download and examine for yourself (see end of article for
details). The first part won’t take long, but will give you a better Continuing our analogy to DLLs, early binding is similar to import-
understanding of what happens in the example and why. ing routines from a DLL by using the external directive. When you

29 September 2001 Delphi Informant Magazine


Greater Delphi
do that, you embed the definition of those routines in your client, implementation
and you expect them to match that definition exactly when you
connect to them at run time. If the name, parameters, or result type uses ComObj;
are changed, you’ll have an error as soon as the application starts.
{ $R *.DFM }

Late binding is similar to the GetProcAddress API call in which you procedure TForm1.bLateBindingClick(Sender: TObject);
specify the name of the function you want to connect to using a var myobj: OleVariant;
string, and are returned a pointer. When you do that, your client runs begin
myobj := CreateOLEObject('COMCOnverter.ConverterList');
fine, unless you try to use the function with the wrong parameters.
ShowMessage('There are ' + IntToStr(myobj.Count) +
' converters available');
Invoking methods of a COM object through early binding is end;
faster than using late binding. Every time you use late binding,
you’re asking Windows to look for a method called with a certain procedure TForm1.bEarlyBindingClick(Sender: TObject);
var
name, return a pointer to it, and then invoke it. Using early myobj : IConverterList;
binding means you immediately call it, without any additional begin
overhead, because you already know the location of that method’s myobj := CoConverterList.Create;
entry point. However, using late binding allows much more flex- ShowMessage('There are ' + IntToStr(myobj.Count) +
' converters available');
ibility, and makes possible things such as scripting.
end;

As an example, this is the content of the VBTest.vbs file contained Figure 3: Use an OleVariant variable for late binding, and a
in the example application’s \WordProcessor directory: typed variable for early binding.

Dim MyObj, i, s We have seen how COM lets us use objects embedded in DLLs.
Set MyObj = CreateObject("COMConverter.ConverterList") Wouldn’t it be nice if, on top of that, those DLLs could be located
s = ""
and executed on a more powerful machine? Wouldn’t it be nice not to
For i = 0 To (MyObj.Count-1)
s = s & MyObj.Items(i).Description & ", " have to worry about TCP/IP communication and sockets? This is all
Next possible using Distributed COM (DCOM).
MsgBox("You can save as " & s)
DCOM is an extension of COM that allows inter-process com-
Double-click on it and see what happens. Our ConverterList munication across machine boundaries. Using DCOM, the only
object will be created and the names of all supported converters thing that changes for the developer is the way you instantiate
will be displayed, all without Delphi, VB, or anything else. This your COM object. Instead of calling CoCreate, you would now call
is done using a late-bound call to the methods Get_Items and CoCreateRemote(<ServerName>), passing either an IP address or the
Get_Count. The ActiveX Scripting engine embedded in Windows name of the machine that executes the COM object.
(which is also accessible through COM) took care of parsing the
text file, and asking to find and invoke it. When you do this, Windows creates an object (proxy) on the
client machine that looks exactly like the real object. When you
You can do the same in Delphi, but how can you make sure you’re call a method on the proxy, it takes care of delivering your call
using one instead of the other? That’s easy. The way to do late and the parameters you specified to the other machine where a
binding in Delphi is generally by using OleVariant variables. By listener (stub) is waiting. The stub then invokes the real method
using typed variables, you’re using early binding. Figure 3 shows a and packages back the result. All this is done transparently for
snippet of code from the fMainForm unit in the \WordProcessor you. Codewise, the only difference for you is to specify CoCreate
directory. As you can see, the only differences between the two are or CoCreateRemote when creating your COM object.
the type of myobj and the way it’s instantiated.
Conclusion
The fact that you declared myobj as an OleVariant is the key here. COM is an excellent technology for developing flexible, expandable,
That tells Delphi how you invoke the methods of a COM object. and open Windows applications. It defines a standard object-oriented
Any time you use an OleVariant you can specify any method name. way of exposing functionality and promotes integration. By embrac-
The compiler won’t complain. Try putting myobj.XYZ in the first event ing COM you can make your application more open, expandable,
handler. It will compile successfully, but at run time Delphi will raise and controllable (whenever needed) from the outside world. You will
an exception as soon as you hit that line of code. In the second have access to a wide set of tools and functionality embedded in your
case, you wouldn’t be able to compile it, because IConverterList doesn’t operating system, and other applications such as Microsoft Office,
define a method named XYZ. which will enhance the functionality you can provide.

Location Independence If you need to develop enterprise systems, you’ll be able to leverage
Not too many years ago the terms “distributed” and “thin client” your investment in this technology and get access to another set of
became very popular. The two terms are often used together when tools and servers (e.g. Microsoft Transaction Server, BizTalk, Applica-
discussing systems physically split into presentation, business, and data tion Center) that won’t require a switch in language or approach.
storage tiers (multi-tier or three-tier systems). Physically split means
that each of those tiers can be running on the same machine, or If you weren’t familiar with COM, I hope this article provided some
on separate ones. In brief, multi-tier architecture was conceived to interesting information to get you started. If you are already using
produce cleaner designs and enhance scalability. However, it’s a topic COM, I hope it helped you understand a little better why COM
unto itself and outside the scope of this article. exists and when you can benefit from using it. ∆

30 September 2001 Delphi Informant Magazine


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: Microsoft’s Vision for Distributed Objects by
Roger Sessions, ISBN: 0-471-19381-X, John Wiley & Sons, 1997

You can also find more information online at:


 Microsoft’s COM pages at http://www.microsoft.com/com
 Binh Ly’s Web site at http://www.techvanguards.com
 Dan Miser’s Distribucon site at http://www.distribucon.com
 Deborah Pate’s “rudimentary” home page at http://
www.djpate.freeserve.co.uk

Originally from Milan, Alessandro Federici is a recent Chicago transplant. He’s been
developing software since he can remember and has used Borland tools since
Turbo Pascal 3.02. He’s 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. He’s worked as a consultant and
has been the Localization Software Engineer on a number of Microsoft projects
(Milan and Dublin). In the US, he’s worked as a developer and system architect
for companies in the financial, warehousing, and supply chain management busi-
nesses. 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

M icrosoft’s 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 so with the speed of DirectDraw — and with no
quite well, but when used for many other fields visible lag or page-flip time — is another matter.
such as scientific visualization, simulation, and
other highly graphics-intensive efforts, it can and This doesn’t mean that DirectDraw isn’t a suitable
will fail under certain circumstances. This article solution for the presentation of large bitmaps. Many
discusses techniques used to overcome the limita- intense scientific and GIS applications require the
tions within the DirectDraw API, and details a same level of graphics and speed as most games on
sample implementation. the market today. For many applications that require
fast image display, DirectDraw is one of the best
Background solutions with such complex and critical data.
One of the more critical limitations to DirectDraw
involves the use of large bitmaps as image sources. The solution to getting a large bitmap to display
Bitmap sizes play a big role in whether DirectDraw quickly and navigate smoothly is based on making
will function at all for certain applications. A the problem smaller. Since the display of smaller
DirectDraw surface won’t be created if it violates surfaces using DirectDraw surfaces is a fairly manage-
the size constraints enforced by DirectX. While able task, the main goal is to reduce the problem size
displaying large bitmaps is usually fairly easy, doing 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 han-


dling that were combined to generate this sample
code. These are double buffering, culling, divide
and conquer, and navigation. There’s no single tech-
nique that supplies the power needed for a successful
approach. Only by combining them can we over-
come the image size problem, and provide smooth
display and navigation.

Sample Code
The sample code included here used a 65MB sat-
ellite picture of the United States (see Figure 1)
as a test image. DirectDraw refused to create a sur-
face 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
Figure 1: This satellite image of the United States is simply too big for DirectDraw. that large images inherently need.

32 September 2001 Delphi Informant Magazine


Sound+Vision

TQuadSurface = class(TQuadSurfaceFace) function TForm1.showDirectDraw: Boolean;


private var
NE, NW, SE, SW: TQuadSurface; halfx, halfy: Integer;
bounds: TRect; myDC: HDC;
procedure SetUpPic(var r: TRect; b: TBitmap; begin
lpdd: IDIRECTDRAW7); frontrect := GetFrontRect;
function GetSurface(var r: TRect; lpdd: IDIRECTDRAW7; if bInit = False then begin
b: TBitmap): IDirectDrawSurface7; Result := False;
protected Exit;
surface: IDirectDrawSurface7; end;
public { Give the viewer of the demo app an idea how big
procedure DrawOnSurfaceRect(var frontArea: TRect; the screen area is. }
backArea: TRect; target: IDirectDrawSurface7); CAPTION := getClipperString;
override; { Find the center of the viewing screen area. Scale it
function getSurfaceRect: TRect; for zooming purposes. Then create the area that
function getWidth: Integer; override; should be sampled. }
function getHeight: Integer; override; halfx := trunc(((
procedure getRect(var r: TRect); override; frontrect.right - frontrect.left) * scale) / 2);
function Init(lpdd: IDIRECTDRAW7; File_Name: string): halfy := trunc(((
Boolean; override; frontrect.bottom - frontrect.top) * scale) / 2);
function InitWithZoom(lpdd: IDIRECTDRAW7; backrect.top := focus.Y - halfy;
File_Name: string; Scale: Double): Boolean; backrect.bottom := focus.Y + halfy;
overload; backrect.left := focus.X - halfx;
function InitWithZoom(lpdd: IDIRECTDRAW7; map: TBitmap; backrect.right := focus.X + halfx;
Scale: Double): Boolean; overload; if lpDDSBack = nil then begin
constructor Create; virtual; // Just for safety, we check the back buffer.
destructor Destroy; override; Result := False;
end; Exit;
end;
// Transfer information from the qsurface tree onto
Figure 2: Declaration for TQuadSurface.
// the back buffer.
if (qSurface <> nil) then
qSurface.drawOnSurfaceRECT(
Basic knowledge of setting up a DirectDraw surface is required to frontrect, backrect, lpDDSBack);
execute the included sample code. The concepts and techniques lpDDSBack.GetDC(myDC);
used aren’t specific to DirectDraw, and may be applied to the PolyLine(myDC, lastpts, 4);
general problem of managing large 2D images. The example code lpDDSBack.ReleaseDC(myDC);
// Swap from the back buffer to the front buffer.
isn’t in “full screen mode” so the examples can be applied to other lpDDSPrimary.Blt(@frontrect, lpDDSBack, @frontrect,
2D problems more easily. DDBLT_WAIT, nil);
Result := True;
The sample code was developed with Delphi 5 on a 500MHz Pentium end;

III processor using 128MB of RAM, running Microsoft Windows 98.


When running on a Windows NT Workstation or server, the code must Figure 3: The showDirectDraw method.
be altered to use the correct DirectX version supported by the platform.
Recall that only DirectX 3 is supported on Windows NT. branches that fit within the drawing zone. Some would argue that using
a quad-tree presents extra work, and that direct-array access is more
Double Buffering efficient. However, the quad-tree structure offers good performance and
Smooth, flicker-free image display is achieved by using double buffering. is easy to understand in sample code, so it’s used here.
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 Divide and Conquer
to the screen. Since the screen isn’t required to update when every draw Breaking a large image up into smaller pieces will solve the DirectDraw
command goes through, it’s significantly smoother to the eye. surface problem. A large image can be recursively divided into smaller
ones until they’re of acceptable size. Doing this in a way that simplifies
Most DirectDraw enthusiasts will recognize the flip function as double the culling and double-buffering operations can be challenging. Fortu-
buffering. In this example code, I avoid using flip in order to show a more nately, using a quad-tree data structure will make both operations much
generic version that can work outside DirectX. Also, most scientific and easier. Iterating through the bitmap, breaking it into quads, and assigning
business applications don’t use full screen mode, since word processors, the regions to them as data structures, will break the image down nicely.
math packages, etc. are usually being used alongside graphic displays.
It’s important to note that breaking the bitmap into smaller manageable
Culling sizes will fix the display problem by itself. Unfortunately, the drawing
Since the smoothness of graphic display is a function of the amount of performance will be dismal. The amount of work required to show the
work done between paints, the workload must be intelligently managed. image carries huge performance penalties, and only by double buffering
Work that isn’t required for a specific screen shot needs to be “culled.” and intelligently culling out the subsections of the tree that don’t need to
The rule here is simple: don’t do any work that isn’t necessary. This can be redrawn can acceptable performance be achieved.
be much more challenging than simply fulfilling all graphics work that
needs to be done, but leads directly to increased speed and efficiency. The reverse is also true when zoom factors are considered. The
divide-and-conquer approach involves dividing up the source
The sample code accomplishes this by using a quad-tree structure. Each image into many smaller images and displaying them. When
branch of the quad-tree understands how to cull itself, and to find lower implementing a zoom function, large delays can occur (also known

33 September 2001 Delphi Informant Magazine


Sound+Vision

ROOT
Example Form
The example application will set up a DirectDraw surface, and navigate
the background image based on a focus point. [The application is avail-
able 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
NE SE SW NW
objects, to initialize the variables, and to actually call for a rendering.

Initial set up of the DirectDraw surfaces isn’t complex. A primary


surface and a back surface are created. Both of these surfaces have
NW NE NW NE NW NE NW NE the dimensions of the window. The TQuadSurface is created and
handed a file name to process. When it’s finished processing, the
image values, such as focus point and scale, are computed.
SE SW SE SW SE SW SE SW
When the user resizes the form, all the previous assumptions about where
Figure 4: A quad-tree structure: oval nodes have no DirectDraw
to render the image are no longer valid. The FormResize method updates
surface; square nodes have a DirectDraw surface.
the rectangle that will be rendered on the screen. Anticipating user input
is difficult. A user could resize or move the form so that portions of the
as frame dragging) as a result of many sub-images being rendered visible area overlap the boundaries of the TQuadSurface and the original
to the screen area. The solution to that problem is a level-of-detail bitmap. When this happens, areas of black will creep into the display
strategy that’s outside the scope of this article. area. To prevent this, FormResize changes the scale and focus point for
the purpose of keeping the entire image within the view area. Special
Navigation care must be taken to reset the DirectDrawClipper objects properly after a
Once a large bitmap is loaded into memory, it still must be dis- form resize, to prevent accidental clipping within the display boundaries.
played to the screen. Since the bitmap is fairly large, a direct copy
to the screen would need excessive processor time. DirectDraw To render the bitmap onto the screen, showDirectDraw finds the area to
surfaces support culling, and letting DirectDraw use its native be sampled, and passes it along with the back buffer to the TQuadSurface
clipping boundaries would work, but there are easy ways to cull that holds the bitmap information (see Figure 3). The TQuadSurface fills
out unneeded pixel transfers a bit faster. 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
By finding the intersections of the area that will be displayed on the showDirectDraw from the OnPaint event handler.
screen with the new, small bitmap regions, the proper areas of the
DirectDraw surfaces can be defined and copied onto the back buffer. The Many users who prefer the faster, full-screen DirectDraw mode will
search within the quad-tree structure will be fairly quick, and eliminate a no doubt prefer to flip the surfaces, rather than blitting from one to
full search of all the possible rectangles. the other. There are special techniques applicable to full-screen mode
graphics that aren’t addressed directly in this article.
To fill the back buffer with the proper subsection of the image, two
rectangles are needed. The first defines the coordinates of the screen area There are also various utility functions used in the sample code.
where the image will be displayed. The back buffer needs to be at least GetFrontRect, for example, returns a rectangle of the visible form
large enough to encompass that area. Making the back buffer equal in area in screen coordinates. Also getClipperString and getRectString
size to the entire screen area is an easy solution to this. return strings for debugging and viewing purposes.

The second rectangle is the area to sample from the original TQuadSurface and Setting up the Bitmap
large bitmap. Defining a focus point (usually x and y) within the The sample code implements a form implementation for a user interface.
bounded area of the original image is an easy way to keep track of It also supplies the back buffer for quick double-buffered blitting between
where the view is located. As long as the focus is confined to the surfaces. The divide-and-conquer and culling techniques are imple-
bitmap, a visible transfer will occur. mented in a separate object owned by the front form. The TQuadSurface
encapsulates all of the image processing and rendering techniques into a
There will be occasions when the focus will reveal the edges of the recursively defined object tree.
bitmap. In those situations it may be prudent to define a perimeter
zone surrounding the bitmap where the focus won’t be allowed. This The TQuadSurface class implements the divide-and-conquer algo-
zone (usually half of the display area width on each side, and half rithm onto an arbitrary bitmap, and stores the subsections into
the display area height for top and bottom) will stop corners from a tree for quick retrieval. It recursively samples each part of the
overlapping on the screen. bitmap, and when the proper size is reached, stores the image onto
a DirectDraw surface at a leaf node. Recall that leaf nodes are
During the sampling operations, it’s important to remember that nodes with no children. In this data structure, the only nodes that
the original bitmap that was sampled is no longer in memory. have reference to DirectDraw surfaces are the leaf nodes. Branch
Instead, it has been broken into smaller pieces and copied onto nodes only contain the bounding information used for finding the
individual DirectDraw surfaces. The pieces in each leaf of the proper leaf nodes.
containing tree still have the bounds property that was assigned
during the split, which defined their original coordinates within Each node of the TQuadSurface has reference to four children
the original image. These bounds rectangles will be used to sample nodes (see Figure 4). These are the nodes that make up the
the leaf surfaces and create the back-buffered image. northeast, northwest, southeast, and southwest areas of the rect-

34 September 2001 Delphi Informant Magazine


Sound+Vision
angle. Also, each node has a bounds property that represents the function TQuadSurface.Init(lpdd: IDIRECTDRAW7;
rectangle from which the node was sampled in the original bitmap File_Name: string): Boolean;
image. Leaf and branch nodes all have bounds properties. var
b: tbitmap;
begin
TQuadSurface also has a reference to an IDirectDrawSurface interface. Result := True;
This interface isn’t created except at the leaf nodes, where it holds the // Make sure the file is there.
small panels that make up the entire image. Creating the TQuadSurface if not (fileExists(file_name)) then begin
initiates all of the surface references to nil. No image work is actually Result := False;
Exit;
performed until its Init method (shown in Figure 5) is called.
end;
// Create a bitmap to load in the file.
The form unit calls Init to set up the bitmap into memory. It b := tbitmap.Create;
accepts two parameters: a DirectDraw interface capable of creating b.LoadFromFile(File_Name);
surfaces, and the bitmap file name to load into the tree. The bounds // Initialize the root node to the bitmap area.
bounds.left := 0;
properties becomes equal to the area of the bitmap being loaded. bounds.right := b.width;
Once the root node bounds properties are set, the recursion is ready bounds.top := 0;
to begin. SetUpPic is called from Init to recursively construct the bounds.bottom := b.height;
tree. The unneeded source image is freed when finished. // SetUpPic sets up the tree structure, recursively
// breaking the bitmap into smaller pieces.
SetUpPic(bounds, b, lpdd);
SetUpPic accepts a TRect area initially equal to the bounds of the // Finished with the original image; it isn't needed.
source bitmap, and a reference to the IDirectDraw passed into b.free;
Init. The recursion begins by checking if the rectangular area is end;
small enough to satisfy the maximum size acceptable (the sample
code uses a 256x256 pixel size, but it could improve performance Figure 5: The TQuadSurface.Init method.
to define a larger area). If the area is small enough to fulfill the
needed maximum size, GetSurface is called to instantiate a refer- be computed. This is done through a series of ratios. These ratio
ence to a DirectDraw surface. The recursion then stops for that operations could be turned into a series of trigonometric function
leaf, and returns control to the calling object. calls, but they’re included in their raw form in the sample code.

If the rectangle is larger than the maximum size needed to create The general problem is that not all the surface really needs to be
a DirectDraw surface, it must be broken into four parts. The copied. Only the part of the image that actually intersects is neces-
midpoints of the rectangle are found, along with rounding infor- sary. The DirectDrawClipper references attached to the surfaces will
mation. Rounding operations are crucial in these types of applica- clean up any overlap, but often it’s easier to do it within the code.
tions; they can generate strange lines or offsets within the bitmap.
The easy solution used here is to let the rectangles overlap by a The ratio approach to solve this is based on finding a point expressed
single pixel. 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
Four new TQuadSurface objects are created from the area. Each of the line segment referenced from the lower bound of the line
calls its own SetUpPic with the four new areas generated by the segment. For example: Take line 1 that starts at 10 and ends at 30.
bounds of our new regions. This continues until the subdivided Point20 can be expressed as 0.5*(length of the line)+ the lower corner
rectangles are within the bounds of the maximum size, where they of the line. Since the length of the line is 30-10, the point 20 can also
call GetSurface and return to their calling object. be expressed as 0.5*(30-10) +10=20.

GetSurface is the function that actually samples the rectangle of The trick comes when projecting that point onto another line. It
interest from the source bitmap. It first creates a new reference can be done easily by multiplying the ratio across the new basis and
to the IDirectDrawSurface from the clients IDirectDraw that was adding any offset. For example: suppose the value of 20 on line 10
passed into the Init function. Then it uses a few Windows API to 30 needs to be transformed to a line that starts at value 100
calls such as BitBlt to move the bits from the source image onto and ends at value 200. In order that the point 20 (from 10 to 30)
the destination IDirectDrawSurface. It returns the new surface be transformed so its new line is from 100 to 200, the same ratio
reference to the calling function. operation takes place that took place previously.

Remember that when the form wants to display a portion of the Let Point20 equal the value of 20 on a line from 10 to 30, when
image, it passes a back buffer to be filled with a subsection of the projected onto a line form 100 to 200 is equal to the ratio multiplied
original image. To copy a sub-area of the original bitmap onto the by the line length, and then add the lower coordinate of the line:
back buffer, DrawOnSurfaceRect is called. This uses the same recur-
sion methodology that the SetUpPic method used earlier. It checks Point20 = 0.5 (length of line 10 to 30) + the lower bound of 10.
for an intersection between the current node’s bounds, and the area
that should be sampled from the original area of the sampled image. When the line is transformed to 100 to 200, Point20 maintains its
If an intersection is found, then the child nodes are recursively ratio of 0.5 and is now multiplied by the new line magnitude of
called until the leaf branches that contain an intersecting bounds (200-100) and the lower bound of 100 is added to that. Therefore,
area and an instantiated IDirectDrawSurface are found. the transformed Point20 = 0.5*(200-100)+100 =150.

Once the intersection area of the leaf node is found, the area that needs This applies directly to what DrawOnSurfaceRect does. An intersec-
to be copied from the leaf node’s surface onto the back buffer must tion rectangle is found from the bounds of the leaf node and the

35 September 2001 Delphi Informant Magazine


Sound+Vision
back area that must be filled. 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 clarity’s 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 defined by the transformed intersection rectangle. Once
the data transfer is finished, it is up to the function that called
DrawOnSurfaceRect to determine what to do with the back buffer
that has been filled. Usually it is simply flipped 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 inefficient 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 isn’t 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 aren’t 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

S 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! that’s easy you say. Let me show you how I them all, a Panel aligned to the correct corner of
create a new form here, drop a couple of controls, the window, BitBtns and SpeedButtons, and — if
add a menu component, throw in a panel, align you want to be fancy — an action list.
it to the top so it will be a toolbar, add a status
bar, and we’re ready to rock and roll. So far so good. And if you want to be even fancier,
Delphi now has docking support. So what’s stopping
A menu-bar component set. Who needs it? you from writing world-class Windows applications?
Delphi already ships with MainMenu and Pop-
upMenu components. There’s a StatusBar com- Then you open one of these fancy applications
ponent for — well — the status bar, ToolBar and the boys in Redmond keep writing, with the
CoolBar for the toolbars, or the granddaddy of 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 Office offers. Suddenly you realize
your application could be a lot cooler. Nothing
stops you from adding these features; you’re a star
programmer. Everything in Delphi is customiz-
able 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. Don’t you hate it when
that happens?

This is one of those times that third-party com-


ponent sets can really be useful — specifically
ExpressBars. I got ExpressBars in a package deal
when I purchased ExpressQuantumGrid 3.0 (see
the review of ExpressQuantumGrid in the Janu-
Figure 1: ExpressBars SideBar and toolbars in action, ary 2001 issue of Delphi Informant Magazine).
emulating the Internet Explorer-style user interface. I installed it and didn’t bother reading the docu-

37 September 2001 Delphi Informant Magazine


New & Used

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

mentation. I used it for the minimal functionality in a new appli- Figure 3: Office XP-style menus.
cation I was writing, yet I find myself using it again and again.

With the release of ExpressBars Suite 4.0, I had an excuse to read We will return to the toolbar’s design capabilities in a moment, but
the “What’s New,” dive into the documentation and demonstra- for now let’s consider the Commands tab of the property editor.
tion applications, and appreciate how much more than a menu and Think of a command as a menu item or a button on a traditional
toolbar suite this product really is. The icing on the cake came toolbar. Now imagine that this command can be an edit box, a
when my QA engineer asked when we were going to convert an drop-down combo box, a static item, a color selection drop-down
older application to use the new menus and toolbars. 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
The Components editor with a drop-down calendar, a sub-menu, an image selection
ExpressBars installs 11 new components into the Component pal- drop-down box, a tree view component... You get the point.
ette on a new ExpressBars tab. Three of these components are used
for the SideBar: an Outlook-like component with groups and items If you can’t find the item you want to place on your toolbar among
that appear at the left side of an application’s window. TdxSideBar the different classes offered, you can create a control container
is the main Outlook bar-like component. Drop it on your form, command and assign an element of your design. Link this com-
double-click to activate the component editor, and start adding mand object to the object that you place on your form, with its
groups and items from there. For each group, you can define the visibility turned off, and you’re ready to go. One of the samples
visibility (groups might be activated based on user authorization that come with ExpressBars shows how to include a Delphi-like
for example), the size of the icons for the items it contains, its component palette on a toolbar.
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 Every command item can be customized with an associated image,
group, and custom data. a title that can be displayed with it, and — for the different
command types — a set of properties and events that allow you
Once your groups and items are defined, it’s easy to use the to customize the use of this command. Since there are 21 built-in
OnItemClick event to change views based on the item selected. The command types, we won’t go into the details. Instead, let’s return
component allows you more fine-grained control to allow you to to the toolbars.
respond to events that happen when the active group changes, the
item selection changes, before or after a user edits an item, etc. Once you define a toolbar, you can define if it will be docked to
If you want to offer user customization of the SideBar, so they one of the four sides of the form. The bar manager will, of course,
can enable or disable groups, add items, etc., you’ll need to use allow you to customize to which side, if any, the toolbar can be
the TdxSideBarStore and assign the SideBar’s Store property to docked. You can also determine if users can let it float. Then you
point to it. Finally, the TdxSideBarPopupMenu can be assigned can start dragging and dropping command objects on it to define
to the SideBar component to provide “standard” customization its appearance. The commands will automatically be added and
operations popup menus such as Add Group, Remove Group, Rename aligned, so you won’t need to fight with the placement of the
Group, Remove Item, etc. elements the way you would if you were to use a simple TPanel
for a toolbar.
Now let’s move on to the real power of this product: the menu and
toolbar components. To start working on your menu and toolbar When a toolbar is selected in the component editor, you can assign
design, drop a TdxBarManager on your form and double-click on a plethora of options to it, such as the location where it is docked
it. Just as in Office, a toolbar and a menu are pretty much the and to which sides of the form it can’t be docked. You can also
same. Define toolbars in the component editor’s Toolbars tab. You define if the toolbar shows a size grip, if it takes the entire row
can designate a toolbar as the main menu of the form to have it (dock width), if it can span multiple lines, what its caption is when
automatically appear at the top of your form looking like a regular it floats, where it first appears when it is floating, what kind of
menu, or you can leave it as a regular toolbar that will appear as a border it has, and what kind of customization it provides.
docked toolbar or a floating command window. Customization is a big feature offered by this product.

38 September 2001 Delphi Informant Magazine


New & Used
When your application is executed, users can use Microsoft Office-
style customization commands to add, remove, rename, and rear-
range commands and other options of customization for every
toolbar. This will allow a user to create favorites and display the If you want to create outstanding applications, and don’t want to
tools used most often on a convenient toolbar. spend your time trying to mimic the latest state-of-the-art user
interface, ExpressBars can be a godsend.
You may think this is a lot of functionality, but we’re just getting
started. You can have Office-style menus that hide seldom-used
commands and automatically expand after a pre-set period of time; Developer Express
you can define groups that contain other groups and commands 6340 McLeod Dr., Suite 1
and allow users to enable or disable them en masse; you can use Las Vegas, NV 89120
the same commands on multiple toolbars or popup menus (thus
getting the same effect as action lists); you can define your applica- Phone: (800) GO-DEVEX
tion with a standard look, a flat look, or even an Office XP look E-Mail: info@devexpress.com
(see Figure 3). The list of functionality and customization options Web Site: http://www.devexpress.com
of the components goes on and on, but let’s stop here. Price: ExpressBars Suite 4, US$179.99 (with source, single-devel-
oper license only) or US$129.99 (without source, single-developer
Let’s not forget the TdxBarDockControl component that allows you license only). Upgrades and bundle discount packages available.
to create a dock site on any container control (e.g. a TPanel ) on your
form. You’re no longer limited to docking only on the four sides of
the form. For example, I have an application that displays an object-
selection pane on the left, and an editor pane on the right. It makes a The samples that ship with the product are also a good source
lot of sense to have a dock site above the editing pane which is at the of information and can teach you a lot. On top of this, the
bottom of the form. That increases productivity because users don’t newsgroups server provides peer support and is frequently manned
need to move the mouse all the way to the top of the form to issue a by Developer Express employees, so you will usually get excellent
command while working with the editor. technical support and answers to your questions.

Documentation, Samples, Technical Support Conclusion


ExpressBars is a deceptive product: You can achieve great results Do you need ExpressBars? Of course not. Delphi comes with
without writing a single line of code. Just open the component enough components to create perfectly acceptable Windows appli-
editor, drag and drop to your heart’s content, and look like a star. cations with toolbars, menus, and the like. However, if you want to
You can also try to learn all the features and options the product create outstanding applications, and don’t want to spend your time
provides and spend the rest of your life trying arcane combinations trying to mimic the latest state-of-the-art user interface, Express-
of properties and events just to see what happens. Bars can be a godsend. I stumbled upon this product by mistake,
but it’s no mistake that I keep coming back to it with every new
There’s also a happy middle ground. The Help files that ship with application. Take it for a spin. ∆
the product are integrated with Delphi’s Help system upon instal-
lation 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 capabili- Ron is a software developer specializing in Business Intelligence applications.
ties, thus steering you to explore on your own once you understand He can be reached at rloewy@transport.com.
what features can be achieved with the different components.

39 September 2001 Delphi Informant Magazine


New & Used
By Bill Todd

SQL Tester 1.2


SQL Statement Analysis Made Easy

S 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 you’re creating A pair of radio buttons on the SQL Tester toolbar
an application and you want to store your SQL let you choose whether you want to display BDE
statements in external files and execute them by or ADO databases. The Databases list box lets
using the LoadFromFile method of one of the you choose the database with which you want to
Delphi query components. Figure 1 shows the work. After you select the database, all the tables
main SQL Tester form. in that database are listed in the Tables list box.
Select a table and the fields for that table appear
Getting Started in the Fields list box.
SQL Tester lets you create SQL statements using
any database you can access via the BDE or ADO. 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 field name into your SQL statement,
simply drag it from the table list or field list,
and drop it anywhere in the SQL Statement memo
control. Regardless of where you drop it, the
table or field name will always be placed at the
insertion point.

If you’re creating queries that involve more than


one table, SQL Tester can include a table alias
with each field name or table name you add to
your SQL statement. To automatically add aliases
when you add field 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
Figure 1: SQL Tester’s main form. the Tables list box. Choose Set table alias from the

40 September 2001 Delphi Informant Magazine


New & Used

Figure 2: The Parameters dialog box.


Figure 3: The SQL Statement 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 Tester’s database. 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
If your query contains parameters, simply click the Parameters Figure 3 will be displayed, containing the entire SQL statement. This
button at the bottom of the main form to display the dialog box dialog box is resizable and provides a much larger working area.
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 Putting It to Use
SQL statement with different parameter values. After you’ve created and tested your SQL statement, you’ll want to
move to the Identification group (again, see Figure 1). Here you can
After you’ve entered your SQL statement and assigned values to param- enter a Description for the statement. You can also enter a Group Code
eters, you can run the SQL statement by clicking the Run button at the to identify the SQL statements by the project to which they belong.
bottom of the main form, or by clicking the Run speed button to the Use the Group Code edit box to select SQL statements for certain
left of the SQL Statement memo control. If you run a SELECT statement, batch operations. If the Database edit box doesn’t contain a database
the result set appears in the Query Results grid at the bottom of the main name, simply drag the database name from the Databases list box. If
form. Optionally, you can have the query results displayed in a separate you want to save the SQL statement to a file, enter the name in the
window. This lets you have up to four result sets open at one time. File Name edit box, and press CS (or choose File | Save).

You can export the query result set in either ASCII or XML format. Another nice feature is that SQL Tester lets you save your current
You can also request a live result set when testing queries, so you SQL statement as a template if you need to create many similar
can update the result set to modify your test data without having queries. Click the Insert button on the navigator bar to create a query
to use another program. from the template, then click the Load from Template button to display
the list of templates you’ve created. Double-click the template you
If you’re working with a long SQL statement, the SQL Statement want to use and the SQL statement will be inserted into the SQL
memo control may not be large enough to allow you to see the entire Statement memo control at the insertion point. Templates don’t have
to be complete working SQL statements. For example, you might
save a list of field 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
SQL Tester lets you build and test SQL statements without having
creates a new query with the same SQL statement and the same
to compile and run your applications. It can decrease your develop-
identification information.
ment time with its ability to add table names and field names
to SQL statements using drag-and-drop, and you can batch test
SQL Tester lets you print queries two ways. You can print the
queries when the database schema changes. SQL Tester can also
current query by clicking the Print speed button to the left of the
read SQL statements from Delphi .dfm files.
SQL Statement memo control. Otherwise, clicking the Print button
on the toolbar lets you print all the queries within a statement
Red Brook Software, Inc. number range, all queries with the same group code, or all queries
554 Watervliet Shaker Road for the database you choose.
Latham, NY 12110
Another practical feature of SQL Tester is the ability to batch test
Phone: (518) 786-3199 queries. This is handy if you make changes to your database and
Fax: (518) 786-3511 want an easy way to determine which queries are affected by the
E-Mail: sales@redbrooksoftware.com changes. To test a batch of queries, choose Tools | Batch Testing.
Web Site: http://www.redbrooksoftware.com
Price: SQL Tester 1.2, US$195.00 The Batch Testing dialog box lets you select the SQL statements
to test, and offers three ways to test them: You can choose to

41 September 2001 Delphi Informant Magazine


New & Used
test all the queries for a specific 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.
It’s 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 don’t choose to load all your SQL statements from
files, SQL Tester can still be useful, because it can read SQL
statements from Delphi .dfm files (if the .dfm files 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 files to update your application.

Conclusion
SQL Tester can cut your development time in two ways. First,
the ability to add table names and field names to SQL statements
using drag-and-drop can save a lot of typing when you’re 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 win- about Delphi 6 and it certainly programmer. All skill levels will in D6, or specific information
ning the lottery, nothing can isn’t perfect, but it’s a major con- probably appreciate the bite-sized on CLX, Apache support, or
grab my attention quicker than tribution that contains plenty of projects in the appendices. A DataSnap, you’ll need to look
a book about an unreleased interesting and useful material. cookbook filled with self-con- elsewhere.
version of Delphi. So when While many of the topics in this tained and practical examples like
Paul Kimmel’s Building Delphi book have been covered by other these would be a must-have. These issues aside, the entire
6 Applications showed up at the authors in other works, Paul range of Delphi development
office, I snapped at the chance Kimmel has provided yet another The CD’s descriptive readme issues are thoroughly covered, but
to take it home. perspective that may enlighten file was provided in an annoy- the presentation is a little wordy.
where the existing body of Delphi ing proprietary file format. And If it were possible to turn off
After making my first cover-to- texts haven’t. I also believe that after installing a reader, I found the verbose switch and recompile,
cover pass, however, I was puz- it deserves a warm welcome, it contained a mere three lines this book could probably drop
zled. There seemed to be a “6” in because Delphi perspectives at and 15 words! The CD itself 100+ pages without losing any of
the title that didn’t belong there. my local bookstore are getting was just as sparse, providing its technical content.
While the book did present a harder and harder to find. only a couple of publicly avail-
complete perspective on building able packages, as well as the The author is experienced and
Delphi applications, there were So let’s set the embattled title book’s sources. The sources knowledgeable and the book con-
very few specific references to with its bewildering D6 reference themselves are interesting and tains plenty of value, but the lack
Delphi 6 — and virtually noth- aside and explore the content of well written, but there’s a sad of D6 content and the marginal
ing about writing cross-platform this book and its accompanying lack of useful comments. I quality and small number of bits
applications with Kylix. CD. We’ll definitely encounter also found numerous sections of on the CD make $59.99 a bit
value along the way — along code that had been commented steep. It’s definitely worth a look
Out of curiosity, I cruised the Bor- with a few speed bumps. out with no explanation. While — but definitely take that look
land forums and discovered that this is sometimes done in the before taking it home.
this book had generated a lot The tone of the book is peer heat of production, it’s poor
of controversy even before it was to peer and personal, like listen- form for a tutorial. — Tom Lisjac
officially titled. Since Delphi 6 ing to a talkative colleague
hadn’t been released, some people rather than a formal mentor. If The lion’s share of the 42+MB
assumed that any reference to you’re a beginning or interme- source directory were compiled
it would violate Borland’s NDA. diate programmer, this ambiance EXEs and DLLs. Deleting them
Flames also flew that the originally may help you feel comfortable placed the unique content on the
proposed title infringed on the with the author and presentation. CD at around 2.5MB, including
turf of an existing developer series. Advanced readers who’ve already pictures and Paradox tables. This
weathered their share of talkative didn’t make the CD seem as
While I was disappointed and coworkers will probably prefer a “Value-Packed” as the back cover
mildly annoyed with the book’s more concise presentation. of the book implied. The folder
title, I was genuinely appalled for Chapter 17 was empty on
with the overall treatment the There are 19 chapters, four the CD, so I wrote the author
author received from the online appendix projects, a one-page and asked if there was a support
Delphi community. Some of the bibliography, and a fairly good site for the book. He replied
threads were so unsettling that I index. Five major topics are that one was in the works and
couldn’t help but wonder if any developed: an IDE/OOP tutorial, to check his Web site (http://
other aspiring first-time authors component design, COM auto- www.softconcepts.com) after D6 Building Delphi 6 Applications
might read them and decide to mation, database applications, is released. by Paul Kimmel,
pass on such a severe and intoler- and Internet-related program- McGraw-Hill Professional Book Group,
ant audience. ming. About half the book would Building Delphi 6 Applications is http://www.osborne.com.
qualify as a basic Delphi tutorial, about building Delphi applica-
This reaction also tended to while the other half contains tions, but it’s not about Delphi ISBN: 0-07-212995-6
eclipse the rather substantial con- tricks, tips, and techniques for 6. So if you’re after a comprehen- Cover Price: US$59.99
tent of the book. No, it’s not the intermediate and advanced sive review of the new features (774 pages, CD-ROM)

43 September 2001 Delphi Informant Magazine


TextFile

Building B2B Applications with XML Special Edition Using XHTML


This book offers a survey of the For the most part, the author Using XHTML is an excellent Holzschlag wanders off into a
burgeoning technologies used provides credible explanations of beginner- to intermediate-level few hundred pages on various
in B2B (business-to-business) complex technologies. Beginners book, providing a broad over- Web development topics.
transactions over the Internet. won’t learn enough to actually view of XHTML, and other
In 10 chapters, the author dis- implement solutions, but the foot- Web development technologies. That complaint aside, Ms
cusses introductory XML con- notes provide a comprehensive list Holzschlag’s Using XHTML is
cepts; transport mechanisms of online resources. Despite the Whether you’re an intermedi- a commendable treatment of
such as HTTP, SMTP, and missing companion Web site, this ate Web developer making the XHTML, CSS, XSL, DTDs,
FTP; and security consider- book is still worthy of consider- switch to XHTML, or a begin- and related Web and wireless
ations. Insight into working ation if you’re approaching B2B. ner, Using XHTML will make development topics, which I
with B2B pioneers Commerce an excellent addition to your heartily recommend.
One and Ariba is offered, — David Ringstrom reference bookshelf. I daresay,
along with coverage of SOAP if you’re only going to buy one — Robert Leahey
(Simple Object Access Proto- book on XHTML and Web
col), and BizTalk (Microsoft’s development, make it this one.
implementation of SOAP). The
book closes with a checklist for Ms Holzschlag’s writing style
building a simple B2B system. is light and chatty, making this
Java and XML code examples tome quite readable. There
are presented throughout. are nine topical sections with
several chapters each. Each
Since the book doesn’t have a chapter includes sections on
CD-ROM, the author repeatedly TroubleShooting and Design-
prompts readers to visit the com- ing for the Real World that
panion Web site for code archives contain useful tips and rules-
and updates on these rapidly of-thumb.
developing technologies. Unfortu-
nately, the book’s link on the pub- Building B2B Applications with XML My chief complaint is that the
lisher’s site is no longer valid. by Michael Fitzgerald, book seems to stray from the Special Edition Using XHTML
Searching the Wiley site only pro- John Wiley & Sons, Inc., main topic of XHTML about by Molly E. Holzschlag, QUE,
vided the opportunity to buy a http://www.wiley.com/compbooks. two thirds of the way through. http://www.quepublishing.com.
copy of the book. Finding the After a very solid introductory
author’s personal site also failed to ISBN: 0-471-40401-2 treatment of the origins of ISBN: 0-7897-2431-6
yield the much vaunted supple- Cover Price: US$44.99 XHTML, and then a good Cover Price: US$39.99
mental materials. (310 pages) overview of the language, Ms (958 pages)

The Tomes of Delphi: International User Interfaces


Algorithms and Data Structures Building Kylix Applications edited by Elisa M. del Galdo
Julian Bucknall Cary Jensen and Loy Anderson and Jakob Nielsen
Wordware Publishing, Inc. Osborne/McGraw-Hill John Wiley & Sons, Inc.

ISBN: 1-55622-736-1 ISBN: 0-07-212947-6 ISBN: 0-471-14965-9


Cover Price: US$59.95 Cover Price: US$59.99 Cover Price: US$64.99
(525 pages, CD-ROM) (832 pages) (276 pages)
http://www.wordware.com http://www.osborne.com http://www.wiley.com

44 September 2001 Delphi Informant Magazine


TextFile

UML Explained SQL in a Nutshell


There are many books about fect for managers and others SQL in a Nutshell by Kevin Kline normal form. Neither does the
the Unified Modeling Lan- not involved directly in design and Daniel Kline is a must-have book touch upon any adminis-
guage. Do we really need or coding. It seems less for any developer who must work trative tasks like backups, data
another? Most of the current appropriate for developers, with the SQL language. With just migration, or index maintenance.
crop were designed for readers however. For the latter audi- over 200 pages of definitions and The explanation of joins also just
with considerable technical ence I continue to recommend examples, the book covers Micro- scratches the surface of a deep sub-
expertise, but there’s a need Martin Fowler’s UML Distilled soft SQL Server, MySQL, Oracle, ject. These items were not in the
to present UML in a manner [Addison-Wesley, ISBN: and PostgreSQL. What a devel- goals of the authors. SQL in a
accessible to those with whom 0-201-65783-X] as the appro- oper can expect from this book Nutshell will be a great addition to
engineers must communicate priate introduction. is an easy reference that will help any technical reference library.
(stakeholders). Kendall Scott’s them with the SQL syntax of
UML Explained is aimed espe- — Alan C. Moore, Ph.D. each of these RDBMSs (Relational — Christopher R. Shaw
cially at such stakeholders, and Database Management Systems).
fills this important gap. There are chapters on keywords,
functions, concepts, and new
Unlike most other UML books, commands.
this one assumes little previous
knowledge or technical jargon. The chapter on statements is
Granted, terms such as “class,” well organized and includes a
“component,” and “use case” quick command reference table to
are essential to understanding show the class, implementation,
the UML. The author is careful and which RDBMS supports each
to highlight and define such statement. Each statement expla-
terms the first time they are nation starts with some basic
introduced in the text. They information about the command
also appear in a glossary. that is relevant to all four
RDBMSs, then provides a para-
This book is well organized UML Explained graph or two that explains each SQL in a Nutshell
and does a good job of by Kendall Scott, of the variations of the statement. by Kevin Kline with Daniel Kline, Ph.D.,
introducing the essential UML Addison-Wesley, The information is then followed O’Reilly Computer Books,
topics to the reader with aver- http://www.awl.com. up by examples. http://www.oreilly.com.
age technical expertise. With
its discussion of the Unified ISBN: 0-201-721820-1 What a reader shouldn’t expect is ISBN: 1-56592-744-3
Process and its many example Cover Price: US$29.95 an in-depth explanation of rela- Cover Price: US$29.95
UML diagrams, it seems per- (151 pages) tional databases, or descriptions of (214 pages)

The Elements of AntiPatterns in Project Management Exchange 2000 Server:


User Interface Design William J. Brown, Hays W. “Skip” McCormick The Complete Reference
Theo Mandel III, and Scott W. Thomas Scott Schnoll, Bill English, and Nick Calancia
John Wiley & Sons, Inc. John Wiley & Sons, Inc. Osborne/McGraw-Hill

ISBN: 0-471-16267-1 ISBN: 0-471-36366-9 ISBN: 0-07-212739-2


Cover Price: US$49.99 Cover Price: US$49.99 Cover Price: US$49.99
(440 pages) (460 pages) (793 pages)
http://www.wiley.com http://www.wiley.com http://www.osborne.com

45 September 2001 Delphi Informant Magazine


Best Practices
Directions / Commentary

Look Ma, No Help Engine!

N 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. They’ll need answers to questions that arise as to how to use the program, or guidance on
how to accomplish a particular task. It’s customary to provide a separate Help file for just such occasions. However,
sometimes that may seem like overkill, or you may feel it’s a waste of time to produce one as “nobody ever
reads the Help documentation anyway.” Moreover, it’s 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-fledged Windows Help file.
 Provide a quick-and-dirty pseudo Help system.
If you’re serious about offering a quality software package, the first
option is obviously not a good one, so you must ask yourself:
Do I write a conventional Help file or not? If you do, you
can use a tool such as RoboHelp (http://www.ehelp.com), Doc-To-
Help (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 writ-
ten in Delphi.

If you do opt to create a conventional Help file, when is the best


time? That is, will you write the Help file before writing the applica-
tion, or wait until afterward? Many software methodology experts
recommend writing the Help file first, basing it on the detailed
design specifications. An advantage to that approach is coders can
then use the Help file as the product specification 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 doesn’t do.
Preferably the file is complete, and contains mocked-up screen shots
of the forms. Adhering as much as possible to what is in the Help
file aids in preventing feature creep and gold plating.

Regardless of these considerations, you may decide to either postpone


the creation of the Help file until after the application is at least
partially complete, or eschew a separate Help file altogether. That’s
where the third option comes in, providing a poor man’s Help system.
This technique is especially applicable for prototypes and utilities.

With a modicum of extra effort, you can provide “just enough” help
Figure 1: Nothin’ fancy, but it gets the job done. in a manner that’s not tied to the Windows OS (which is especially

46 September 2001 Delphi Informant Magazine


Best Practices

const procedure ShowHelp(ATopic: Integer);


crlf = #13#10; begin
resourcestring case ATopic of
SGettysburgAddress1 = Abraham:
'Four score and seven years ago our fathers brought,'+crlf+ MessageDlg(Format('%s%s', [SGettysburgAddress1,
'forth upon this continent, a new nation, conceived'+crlf+ SGettysburgAddress2]), mtInformation, [mbOK], 0);
'in Liberty, and dedicated to the proposition that'+crlf+ Martin:
'all men are created equal.'+crlf+crlf+ MessageDlg(Format('%s%s', [SIHaveADream1,
'Now we are engaged in a great civil war, testing'+crlf+ SIHaveADream2]), mtInformation, [mbOK], 0);
'whether that nation, or any nation, so conceived,+crlf+ John:
'and so dedicated, can long endure. We are met here'+crlf+ MessageDlg(Format('%s%s%s', [SAskNot1, SAskNot2,
'on a great battle-field of that war. We have come'+crlf+ SAskNot3]), mtInformation, [mbOK], 0);
'to dedicate a portion of it as final resting place'+crlf+ else
'for those who here gave their lives that the nation'+crlf+ { nothing };
'might live. It is altogether fitting and proper'+crlf+ end;
'that we should do this.'+crlf+crlf+ end;
'But in a larger sense we can not dedicate--we'+crlf+
'can not consecrate—we can not hallow this ground.'+crlf+ Figure 3: The ShowHelp method.
'The brave men, living and dead, who struggled here,'+crlf+
'have consecrated it far above our poor power to add'+crlf+ If you’ve assigned HelpContext values to controls, add the following
'or detract. The world will little note, nor long'+crlf+
code to each form’s OnKeyDown event handler:
'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+ if Key = VK_F1 then
'which they have, thus far, so nobly carried on.'; ShowHelp(TWinControl(ActiveControl).HelpContext);
SGettysburgAddress2 =
'It is rather for us to be here dedicated to the'+crlf+ If you’ve assigned HelpContext values to a form, add this code:
'great task remaining before us--that from these'+crlf+
'honored dead we take increased devotion to that'+crlf+ if Key = VK_F1 then
'cause for which they here gave the last full'+crlf+ ShowHelp(HelpContext);
'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 code readability, add some constants that will correspond to the
'for the people, shall not perish from the earth.'; topic you want to display, mapped to the HelpContext you provide
for the forms or controls:
Figure 2: Being resourceful.
const
Abraham = 0;
attractive if your application may be ported to Linux via Kylix), is
Martin = 1;
easily modifiable by you, and, if you use resource strings, facilitates an John = 2;
easier transition to localized versions of your application. [For more
on resource strings, see Clay Shannon’s article “Be Resourceful” in the Finally, write the ShowHelp method shown in Figure 3.
July 2000 Delphi Informant Magazine.]
Besides the HelpContext property, you could also use the Tag property
In a nutshell, this “Help lite” is accomplished by setting appropriate to the same end, if you’re not already using it for something else. For
HelpContext properties for each control or form in your application that matter, even the Hint property could be used with a slight varia-
— just as is done when integrating a conventional Help file. To tion. Besides being the logical property to use for this purpose, using
do this, check for the KeyDown event to trap 1 key presses (the HelpContext is especially good if you may only use this technique
natural thing for a user to do to get more information), and then temporarily (e.g. for a prototype), and plan on incorporating full-
display the appropriate Help topic based on the active form or fledged Help later.
control (see Figure 1).
For most applications, at least a little help is needed. Don’t neglect it
You can start implementing this technique by setting the KeyPreview just because you don’t find it interesting, or think it’s too much of a
property of your forms to True. If you’ll create one Help screen per form, hassle. At the very least, provide some rudimentary information for
assign a value to each form’s HelpContext property. If your Help will be the more challenging or unconventional parts of your program. Using
more granular (you’ll assign different Help screens to different controls the techniques described herein, you can accomplish this quickly and
or groups of controls on each form), assign a value to each control’s relatively painlessly. ∆
HelpContext property. Then add a constants unit to your project (if you
don’t already have one) to hold resourcestring declarations containing the — Clay Shannon
actual help text. For example, to create the Help screen shown in Figure
1, the pertinent declarations would look like Figure 2.
Clay Shannon is an independent Delphi consultant, author, trainer, and mentor
Please note when attempting to pass very long strings to the operating as Clay Shannon Custom Software in Oconomowoc, WI. He’s available
MessageDlg function, you’ll find that there’s a limit to the size of for Delphi consulting work in the greater Milwaukee/Madison, WI areas, remote
the string that can normally be displayed. After reaching this limit, development (no job too small!), and short-term assignments in other locales (ich
the remainder of your message will simply be truncated. There is kann Deutsch). Clay is a certified Delphi 5 developer, and is the author of Tomes
a workaround. As shown in Figure 2, you can break up your prose of Delphi: Developer’s Guide to Troubleshooting (Wordware, 2001). You can reach
into multiple resource strings, then concatenate them using the him at bclayshannon@earthlink.net.
Format function.

47 September 2001 Delphi Informant Magazine


File | New
Directions / Commentary

List Server Madness

S 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 discus- Another, from a Delphi tool developer (maker of MyCoolTool)
sion lists and have learned much from reading their messages. offered valuable help and advice: “In a previous life I wrote
In the January 2000 issue of Delphi Informant I wrote a column intense WordPerfect macros (e.g. I created a popup calculator
entitled “The Case for Delphi” in which I shared information from accurate to four and a half decimal points, in a macro environ-
two discussion threads explaining Delphi’s strengths compared to ment that only supports whole numbers!), so I can objectively
those of Visual Basic. Here I will share a couple of recent discus- speak to WordPerfect’s abilities and limitations. If these folks are
sions from two of my favorite lists: The Delphi Advocacy Group still seriously considering this, set up a conference call and I’ll set
mailing list (tdag@yahoogroups.com) and the main Project JEDI them straight.”
list (Delphi-JEDI@yahoogroups.com). Discussions on TDAG are
sometimes technical in nature, but often concern themselves with Now I ask you: on how many lists can you expect this level of help?
the well-being of Delphi and the company that produces it. The This developer went on to provide specific details: “In addition to the
JEDI list typically discusses the various activities of that Project, obvious language limitations, there are these as well:
but also include some excellent discussions of translation issues from  WordPerfect macros are noticeably slow. Probably 1000-10000
C/C++ to Delphi. The two threads I will present here, however, are times slower than Delphi programs.
unusual and non-technical, but — I think — quite entertaining.  WordPerfect macros are extremely difficult to maintain. Expect
to pay 10-500 times as much time and money maintaining your
No more Delphi? No, we aren’t talking about the demise of our software.
favorite development tool, but rather about the precarious position of  No MyCoolTool for WordPerfect.”
Delphi in some companies. While replacing Delphi with Visual Basic
in a particular shop isn’t farfetched, this short thread that appeared on Given the popularity of MyCoolTool among Delphi users, the next
TDAG does stretch the limits of credulity. Some well-known Delphi response was to be expected “No MyCoolTool for WordPerfect?
people participate on this list so I’ve changed the name of one well Well, what are you waiting for?” This market-savvy developer’s reply
known tool to MyCoolTool to protect the innocent. was “...waiting to see if the Kylix market will be bigger than the
WordPerfect macro tools market.” After this the thread faded into
The discussion started with this post: “Some of you will laugh, the cyber void.
thinking this is a joke. Some of you won’t believe me at all. At the
government agency where I work, in our ‘PC Team’ meeting last Delphi 6 to become open source? Discussions about Borland mar-
Friday, it was announced that we were now going to begin converting keting are much more common on TDAG than on the main JEDI
all of our Delphi programs into WordPerfect macros. The reason? list, but there are occasional exceptions, as this thread will demon-
Make things simple so anyone can understand them. All the lawyers strate. It appeared on the JEDI list on April 1 of this year.
and 25-year secretarial types figure they can write macros. So now the
big project is to eliminate EXEs altogether, and run the entire agency Here’s how it began: “Borland announced today there will be
under a (nearly obsolete) word processing program. The associate a significant shift in the way their products will be marketed.
‘programmer’ who works with me told them it was ‘a good idea.’ With the success of Kylix on the Linux platform there is no
There’s nothing like government work!” need anymore to sell Delphi 6, said Dale L. Fuller, President and
Chief Executive Officer of Borland, in an interview at NBC this
The first reply embodies the wonderful spirit of the developers on afternoon. Instead there will soon be the entire product portfolio
this list, who would rather fight than switch: “Just when you thought available for free. Beginning with Delphi 6 Borland plans to give
you heard it all. Stupidity has reached new levels. The ‘Associate’ out their major products for free one every other month in the
programmer you work with should be clubbed.” hope to take over the market leadership for software development.

48 September 2001 Delphi Informant Magazine


File | New
‘We will repeat on the RAD market what Microsoft attained on If you doubt that, then read one of the final contributions: “You
the browser market’, said Ted Shelton, Senior Vice President, Chief should see their faces! My team has gone crazy here. They really
Strategy Officer, during a press conference which will be held early believe the stuff. Some of them are already searching borland.com
tomorrow.” for information. Others are forwarding it to their friends around
the world! It’s a pity I don’t have a camera here. (Hope I don’t get
It didn’t take long for the replies to start flying. The first was an fired!) I will tell them the truth tomorrow. The reason for this belief
expected one: “This is great news! Have you forwarded it to the .non- is that Delphi in India is not used widely and these guys and gals
tech news group?” However, a skeptic immediately contemplated: “I want Delphi to come up because they know that they will be hot
wonder if this policy will still be in effect on April 2!” Surprisingly, cakes if it comes up. Wish that Borland would really wake up with
not everyone was pleased: “A preposterous resolution I loathe! Fuller’s its marketing!”
open-sourcing obviates lucrative sales.” A Borland stockholder, no
doubt. Well, you can’t please everyone. Finally, someone asked the Going back to the previous defense, that individual closed with
obvious question, “<grin> Do you have a URL for this story?” To this recommendation for that special April day: “Have a look at
which the Prime Perpetrator [PP] replied “Aaaah, I knew I forgot slashdot.org. It’s full of jokes today.” Well I didn’t, but you can bet it’s
something :o]” Then another doubting inquiry, “Is this for real? I on my itinerary for April 2002. Until next month... ∆
plan to send this information to many people!!!” To which the PP
responded, “Sorry for the irritation. In some countries April 1 is a — Alan C. Moore, Ph.D.
day to make jokes and to pull someone’s leg. It is kind of tradition
to find credible news which in reality is not true. As I said already,
this was just a joke :o]” 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
Not everyone appreciated the discussion, so we had the request: developing education-related applications with the Borland languages for more than
“Please kill this thread, it is of no use and a bad joke as well.” The 15 years. He is the author of The Tomes of Delphi: Win32 Multimedia API [Wordware
PP’s defenders were waiting in the wings: “I think it was a brilliant Publishing, 2000] and co-author (with John Penman) of an upcoming book in the
joke: [IMHO it is a good idea to check something like this at the Tomes series on Communications APIs. He has also published a number of articles in
originator: Is it that difficult to quickly check on the Borland site?” various technical journals. Using Delphi, he specializes in writing custom components
Another made the observation that is occurring right now to so many and implementing multimedia capabilities in applications, particularly sound and
of you: “Always interesting how easy it is to fool people :-)” music. You can reach Alan on the Internet at acmdoc@aol.com.

49 September 2001 Delphi Informant Magazine

You might also like