0% found this document useful (0 votes)
35 views46 pages

On The Cover: November 1998, Volume 4, Number 11

1) Woll2Woll Software announced the release of InfoPower 4, an upgrade to its visual component suite for Delphi. New features include enhanced RichEdit and grid controls. 2) Sandage and Associates released CodeBase Components II for Delphi, which provides TDataSet encapsulations of CodeBase's table and query system, allowing it to function as a replacement for the BDE. 3) D C AL CODA released version 2.0 of YourTraySpell Words Suite, a configurable spell checker, thesaurus, dictionary and text editor utility that resides in the Windows system tray and can analyze text from any application. It is customizable and supports multiple languages.

Uploaded by

reader-647470
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views46 pages

On The Cover: November 1998, Volume 4, Number 11

1) Woll2Woll Software announced the release of InfoPower 4, an upgrade to its visual component suite for Delphi. New features include enhanced RichEdit and grid controls. 2) Sandage and Associates released CodeBase Components II for Delphi, which provides TDataSet encapsulations of CodeBase's table and query system, allowing it to function as a replacement for the BDE. 3) D C AL CODA released version 2.0 of YourTraySpell Words Suite, a configurable spell checker, thesaurus, dictionary and text editor utility that resides in the Windows system tray and can analyze text from any application. It is customizable and supports multiple languages.

Uploaded by

reader-647470
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 46

November 1998, Volume 4, Number 11

Cover Art By: Darryl Dennis

ON THE COVER
6 Picture Perfect — Rod Stephens 31 Dynamic Delphi
Mr Stephens demonstrates algorithms for mapping output pixels back Thread-Safe DLLs — Gregory Deatz
to input positions and using a weighted average to shrink, enlarge, or Mr Deatz explains how you can write a thread-safe DLL, even if you
rotate an image. He even provides the complete source, so you can put don’t know how the calling application uses threads. Also discussed
these powerful techniques to use in your own applications. are the DllEntryPoint function, thread-local storage, and more.

FEATURES
12 Informant Spotlight 36 OP Tech
Tray Icons — Kevin Bluck Is Delphi Running the Code? — Yorai Aminov
You know those icons on the right side of the Windows 95/98/NT Shareware developers (among others) often need to know if code is
taskbar? They’re called tray icons. Mr Bluck explains the tray icon API, running under Delphi control. It’s a simple question, but determin-
and provides us with a component to easily put tray icons to use. ing the answer is not. Mr Aminov shows how it’s done.
18 In Development
The Object Repository — G. Bradley MacDonald
Delphi’s Object Repository is a great way to share forms and/or objects
among your projects — and with other developers, if you know how.
Mr MacDonald shares some OR tips and techniques. REVIEWS
40 Wanda the Wizard Wizard
21 DBNavigator Product Review by Warren Rachele
Delphi Database Development: Part III —
Cary Jensen, Ph.D.
Dr Jensen continues his database series. This month the focus is on the
Database object, which is responsible for nothing less than the data-
base connection itself.
DEPARTMENTS
2 Delphi Tools
26 Sound+Vision 5 Newsline
The Camera Never Lies — Peter Dove 45 From the Trenches by Dan Miser
Better late than never! Mr Dove concludes the graphics programming 46 File | New by Alan C. Moore, Ph.D.
series he began with Don Peer in January, 1997 with a look at camera
coordinate systems, animated textures, and foreground pictures.
1 November 1998 Delphi Informant
Delphi Woll2Woll Ships InfoPower 4
Woll2Woll Software
T O O L S announced that InfoPower
4, an upgrade to its visual
New Products component suite for Delphi
and Solutions 3 and 4 and C++Builder, is
shipping.
InfoPower 4’s enhancements
include enhanced RichEdit
control, which is now based RecordViewPanel com-
on Microsoft’s RichEdit ponent that provides a
Version 2 and supports convenient way to
embedding of bitmaps and embed a panel onto any
OLE objects, display and form containing an edit
automatic opening of Internet control for each field in
URL links, multi-level undo year; an enhanced grid to the table; and an extended
and redo, and database filter- support images in both the validation language to sup-
ing on RichEdit fields; new titles and the data cells, a port the IncrementalSearch
date and time controls, footer section to display col- edit control.
including a data-bound umn summary information,
Sandage and Associates
Ships CodeBase DateTime picker and a and animated column drag- Woll2Woll Software
Components II for Delphi MonthCalendar, support for ging; a new extendable Price: US$199; InfoPower
Sandage and Associates Year-2000 compliance, for- DBNavigator component that Professional, US$299 (includes source
announced CodeBase matting masks, and many dis- supports user-definable code and C++Builder compatibili-
Components II for Delphi.
Version 1 provided VCL support play customizations for the images and actions, integra- ty); upgrades for owners of InfoPower
for Sequiter’s CodeBase calendar; enhanced usability, tion with InfoPower’s dialog 3 are US$99, and US$129 for the
Database Engine. Version 2 such as auto-filling of the cur- boxes, flexible control over professional version.
offers TDataSet encapsulations
of the table and query system, rent date and auto-advancing the layout, and support for Phone: (800) WOL2WOL
making it a true “plug-n-play” upon a valid month, day, or multiple rows of icons; a Web Site: http://www.woll2woll.com
replacement for the BDE. With
CodeBase Components II for
Delphi, developers can connect
D C AL CODA Releases YourTraySpell Words Suite 2.0
Delphi’s native data-aware con- D C AL CODA released meaning and context of choose which characters to
trols (plus many third-party con- version 2.0 of YourTraySpell words. ignore, set hotkeys, add
trols, including InfoPower) to the
CodeBase engine. Words Suite (YTS), a config- YTS resides in the words, use the ClipViewer to
CodeBase Components II for urable spell check, thesaurus, Windows 95/98/NT System see words in context, and
Delphi comes with myriad data- dictionary, text editor utility, Tray, and can analyze any more.
aware controls that let you cre-
ate, index, browse, edit, and and optimized word reposi- word, paragraph, or docu-
update Clipper, FoxPro, and tory. ment from any application, D C AL CODA
dBASE files. Included are propri- YTS spell checks all ver- text editor, HTML editor, Price: US$29.95 for single-user
etary data-aware controls for
Delphi 1, 2, and 3, plus two sions of Delphi, including or database. YTS is cus- license; site licenses are available.
TDataSet descendant controls the Object Inspector, IDE tomizable — set it to run Phone: (530) 272-8133
for Delphi 3, allowing the use of Editor, Captions and Hints, upon Windows startup, Web Site: http://www.dcalcoda.com
Delphi’s native data-aware con-
trols (along with many third- and more. A Delphi-specific
party controls). dictionary is available.
CodeBase Components II for Dictionaries in several lan-
Delphi is available for US$210.
For more information, call guages, including American
(626) 351-1299 or visit English, British English,
http://www.softsand.com. Danish, Dutch, French,
German, Italian, Norwegian,
Polish, Spanish, and Swedish,
put an end to spelling dis-
crepancies, inconsistencies,
and errors. Additionally, the
30,000-word Roget’s
Thesaurus and 160,000-word
Definitions Dictionary pro-
vide access to synonyms and
antonyms, as well as the

2 November 1998 Delphi Informant


Delphi ASTA Releases ASTA 1.0
The ASTA Technology
T O O L S Group announced the release
of ASTA 1.0, a smart thin
New Products architecture comprised of
and Solutions native VCL components
designed for n-tier, thin-client
computing in Delphi.
ASTA components behave
like native Delphi controls.
ASTA’s TAstaClientDataset, a
hybrid TQuery-TTable com-
ponent, delivers design-time don’t ASTA can be combined
data and supports third-party require with other technologies,
database including ActiveX.
tools, such as InfoPower. It
drivers, a
also supports master/detail
BDE, ASTA Technology Group
relationships and cached ODBC, DLLs, DCOM, or Price: From US$250 per developer
update functionality. It can client configuration — and US$250 for deployed server
Engineering Objects be utilized for transitioning only the executable. (US$399 for package); special license
Announces Matrix Math existing programs into ASTA’s thin-client applica- packages are available for corporate
Toolkit 4 Internet-ready, three-tier tions are suited for use and vertical market developers.
Engineering Objects Int’l applications. over any TCP/IP network, E-Mail: info@astatech.com
announced Matrix Math Toolkit
Version 4 for Delphi 4 (MMTv4), ASTA-based applications including the Internet. Web Site: http://www.astatech.com
which provides the classes need-
ed to make array, vector, and South Pacific Announces TCompress 4.0 and TCompLHA 4.0 for Delphi 4
matrix programming an easy
plug-in. South Pacific Information compress/expand rich text SFX and MAKEEXE, exam-
MMTv4 exposes the terms of Services Ltd. announced the database fields (Delphi 3 ple projects for making self-
vectors and matrices through the release of Delphi 4 versions of only); the COMPDEMO extracting/self-installing
use of the Delphi default proper-
ty. Matrix code can now be TCompress and TCompLHA application, a full-source, archives; and SEGDEMO
completely dynamic, yet still use compression component drag-and-drop demonstration and ADDRESS, full-source
the standard matrix notation. suites. of multi-file and database applications showing how to
MMTv4 uses the Delphi 4
zero-based dynamic array as the TCompress 4.0 provides field compression; and source create segmented archives,
basis for vectors and matrices, native components for Delphi examples. and how to add easy
so array shape can be defined and C++Builder, supporting TCompLHA 4.0 helps create backup/restore functions to
at run time, rather than design
time. The dynamic array imple- easy creation of multi-file and manage archives compati- any database application.
mentation almost eliminates compressed archives, and ble with the LHArc and LHA
pointer notation, so the code is database, file, and in-memory utilities. One-step methods, South Pacific Information
cleaner and easier to read.
All the classes are persistent compression using streams. such as Scan, Compress, Services Ltd.
(that is, instances can write Included in the TCompress Expand, Delete, and Verify Price: TCompLHA 4.0 registration and
themselves to, and read them- 4.0 set are the TCompress enhance archive management. license, US$59; TCompress 4.0 registra-
selves from, streams). Vectors
and matrices are designed to component, for general pur- Included in the tion and license, US$59.
work together. Along with the pose multi-file archive, TCompLHA 4.0 set are the Web Site: http://www.spis.co.nz
source code, the toolkit ships resource, and stream compres- TCompLHA component; the
with demonstration programs.
MMTv4 is available for sion (includes RLE and LZH TSegLHA compo-
US$79.95 (single license), compression as standard); the nent, a segmented
US$63.96 (up to 10 licenses), or COMPONLY unit, a version archive and backup
US$51.97 (11 or more). Visit
http://www.inconresearch.com/eoi of TCompress for making manager compo-
for more information. applications that don’t require nent; the
the Borland Database Engine; TCompLHAStream
the TCDBImage component, component, a full-
which uses TCompress to com- source TStream
press/expand database image derivative for com-
fields; the TCDBMemo com- pression to and from
ponent, which uses TCompress any stream;
to compress/expand database LHADEMO, a full-
memo fields; TCDBRichText source, drag-and-
component, which uses drop archive manag-
TCompress to er demonstration;

3 November 1998 Delphi Informant


Delphi DBI Technologies Announces Release of Solutions::PIM Professional
DBI Technologies Inc.
T O O L S released Solutions::PIM
Professional, an upgrade to
New Products the company’s visual calen-
and Solutions daring tool, Solutions::PIM.
The collection of 15
ActiveX controls allows
developers to add personal
information management,
calendaring, and scheduling
capabilities to Windows
applications.
The Solutions::PIM
Professional package still
includes the yearly, monthly,
week-view, and day-view
calendars. Also included are Another addition to the control, which fills the gap
Soletta Announces Standard the date and time input package is the multi-column between insubstantial list
Delphi Library controls, the alarm control, day-view visual calendar controls and complex grids
Soletta announced the open and a .WAV file player. control. Developers can set that are excessive for most
beta version of the Standard
Delphi Library (SDL), a data Enhancements to existing up ctMDay to view a single applications. With ctList,
structure, object persistence, and controls include new print person’s schedule over sever- developers can display
algorithm library designed for methods that allow graphi- al days, or several people’s Microsoft Outlook-style “to-
the Delphi environment. SDL is
based on the mature design of cal printing of the displayed schedules in a single day, do” lists or e-mail message
the Standard Template Library information in the visual showing one to 10 actual displays (with sub-text),
(STL), the container-library stan- calendars. New stylings have columns (and up to 32,000 with sorting, column sizing,
dard for C++.
SDL is designed for intermedi- been added to the visual cal- additional virtual columns). and more.
ate to advanced Delphi pro- endars, such as NT-display- Drag-and-drop is supported
grammers who need sophisticat- styles and week-of-the-year to and from the control, as DBI Technologies Inc.
ed data structures or wish to
take advantage of SDL’s large
numbering. New security well as between columns Price: US$349 (32-bit ActiveX con-
library of generic algorithms. methods make it possible to within the control. trols, online tutorial, and online Help).
SDL is also appropriate for pro- lock appointments based on The new package also Phone: (204) 985-5770
grammers experienced with the
C++ STL, or ObjectSpace’s JGL
user ID and other logic. includes the Enhanced List Web Site: http://www.dbi-tech.com
(Java Generic Library).
SDL offers a number of fea-
tures not found in any other Pythoness Releases PSetting 2
Delphi class library, including Pythoness Software has the management of an appli- ing hidden properties and
natural storage of atomic data
types, allowing SDL containers to released PSetting 2, a compo- cation’s state, including win- complex types (including
be used to hold any Delphi data nent set for Delphi 2, 3, and dow positions, dockable tool- TCollection and TComponent
type (such as Integers, Strings, 4 that simplifies the process of bar locations, font colors and descendants); better registry
Extended values) with no special
syntax; generic algorithms; inte- maintaining application set- sizes, etc. management and control,
grated persistence; a complete tings between runs. PSetting 2 features several including saving state infor-
set of data structures; and atom- PSetting 2 aims to automate enhancements, including stor- mation in
ic, associative data structures.
SDL is available for US$75 HKEY_LOCAL_MACHINE
(SDL binary), US$250 (SDL under Windows NT;
source). For more information, improved MRU list control,
visit http://www.soletta.com.
including automatic detection
of files that no longer exist;
automatic application restart
after a Windows shutdown;
and more events for control-
ling save and restore options.

Pythoness Software
Price: US$69 (includes source code).
Phone: (208) 359-1540
Web Site: http://www.pythoness.com

4 November 1998 Delphi Informant


News Inprise Announces Strategic Alliance with Sun Microsystems
Denver, CO — Inprise
Corp. announced it has
request broker (ORB).
Earlier this year, Sun
February 1998. Sun and
Inprise are now providing
L I N E entered into a strategic announced a migration ser- consulting, as well as docu-
alliance with Sun vices agreement to transi- mentation, that map key
November 1998 Microsystems, Inc. to team tion users of the Solaris NEO features to VisiBroker
Inprise’s development tech- NEO ORB to Visigenic’s features to streamline the
nologies with Sun’s Solaris VisiBroker for Java and transition between ORBs.
operating environment. VisiBroker for C++ ORB Since the beginning of the
Corporations will be able to technology. Visigenic year, Inprise has marketed
take advantage of Inprise’s Software was subsequently VisiBroker to established
tools for building and run- acquired by Inprise in Sun customers.
ning enterprise applications
on the Solaris. The alliance
Inprise Details Enterprise Application Server
is one of several ongoing Strategy
initiatives between Sun and Denver, CO — Inprise integrated suite of enter-
Inprise. Corp. unveiled the key prise middleware and devel-
Through a series of mar- components of its Inprise opment tools that provide a
keting programs, the two Application Server. solution for simplifying the
companies will continue to Scheduled for delivery in development, deployment,
migrate Sun customers to late 1998, the Inprise and management of distrib-
Inprise’s VisiBroker object Application Server is an uted applications.
Key components of the
American Automobile Association Builds Inprise Application Server
Reservation System with VisiBroker for Java include visual development
Denver, CO — Inprise are part of an overall AAA tool integration, centralized
Corp. announced the association. Because each club management of distributed
American Automobile uses different hardware and applications, a standards-
Association (AAA) has adopt- software to meet its business based infrastructure, reli-
ed Inprise’s VisiBroker for needs, AAA was looking to able transactions in a dis-
Java object request broker build one application that tributed environment, and
technology as a key building would enable the integration enterprise-level security.
block for its new travel reser- of each individual club’s dis- For more information on
vation system, which will go parate legacy systems; provide the Inprise Application
live by the end of 1998. information access to a large Server, a detailed executive
With more than 40 million number of users throughout white paper is available on
members, AAA is the largest the federation; and save time the Inprise Web site at
motoring and travel organiza- and money on application http://www.inprise.com/
tion in the world. development. appserver/appserver.html.
AAA is using VisiBroker to
develop computing systems
Inprise Transfers Visual dBASE Development
that provide a range of travel and Marketing Responsibilities to InterBase
reservation products and ser- Denver, CO — Inprise on the Borland visual
vices, such as air, hotel, car Corp. announced that its development tools and 32-
rental, cruise bookings, and independent subsidiary, bit database technology.
auto-travel routing requests InterBase Software Corp., Based in Scotts Valley,
and assistance. The automo- will assume responsibility CA, InterBase delivers
bile organization selected for the future development InterBase, a high-perfor-
VisiBroker as its distributed and marketing of its Visual mance SQL database
object infrastructure because dBASE family of Windows designed to be embedded
of its integrated development database products. into value-added reseller
tools and its ability to tie in Visual dBASE 7 applications.
disparate data sources, such Professional and Visual For more information on
as Apollo — a large, global dBASE 7 Client/Server InterBase Software Corp.
reservation system for the Suite are the Windows and the Visual dBASE family
travel industry. 95/NT versions of the of Windows database prod-
AAA is a federation made up Xbase database develop- ucts visit their Web site
of 94 independent clubs that ment environment, based at http://www.interbase.com.
5 November 1998 Delphi Informant
On the Cover
Delphi 1, 2, 3, 4 / Graphics

By Rod Stephens

Picture Perfect
Shrinking, Enlarging, and Rotating Images

D elphi provides a couple of easy ways to resize an image. If you set a


TImage control’s Stretch property to True and resize it, the control stretches
or shrinks its image to fit. You can also use the TCanvas object’s StretchDraw
procedure to resize a graphic and copy it onto a canvas.

The following code stretches the image held nal, every other pixel is removed from the
in the imgInput control to fit into the image. Unfortunately, the pixels Delphi
imgOutput control: decides to remove may not be the best choic-
es. The removed pixels may contain informa-
imgOutput.Canvas.StretchDraw tion that is necessary to convey the shape of
(imgOutput.ClientRect,
imgInput.Picture.Graphic); the original image.

These methods are fast and easy, but they At best, the result may be rough and
often give unsightly results. To enlarge an jagged, as shown by the smaller text in
image, these techniques simply duplicate Figure 2. At worst, whole pieces of the
each pixel enough times to fill the new image may disappear. For example, if the
image. If the new image is five times as big as original picture contains a vertical line one
the old one, each pixel is converted into a lit- pixel wide, shrinking the image may
tle five-by-five box. This produces a blocky remove every pixel in the line. Similarly in
picture like the one shown in Figure 1. To Figure 2, thin parts of the smaller text have
shrink an image, these techniques remove a disappeared. The text “Rod Stephens” at
fraction of the pixels from the original image. the bottom contains several gaps. Removing
If the new image is half the size of the origi- important color information can also cause
the strange plaid-like patterns shown in the
red text and in the image of the hourglass.

This article explains how you can enlarge


and shrink images smoothly in Delphi.
Figure 3 shows an image similar to the one
shown in Figure 1, but this picture has
been enlarged smoothly. The image shows
none of the blockiness in Figure 1, but it is
much fuzzier. The original image is only a
fifth as wide and tall as the picture in
Figure 3, so it contains only 1/25th as
many pixels. There isn’t enough informa-
tion in the original image to fill all the
Figure 1: Enlarging an image using StretchDraw pixel values in the enlarged image smoothly
produces a blocky result. without some blurring.

6 November 1998 Delphi Informant


On the Cover
The picture in Figure 4 is
much smoother than the
version shown in Figure 2.
There is some blurring in
this image, but the effect
only smoothes edges that
might otherwise be jagged.
This image shows no gaps
in the small text and does
not contain the annoying Figure 5: Mapping an output pixel back to the input
plaid-like effects in the red pixels that determine its value.
text or hourglass.

Roadmap to Reduction
To shrink an image, TCanvas.StretchDraw removes some of the pixels from the origi-
nal image. The problem with this method is that information contained in the
removed pixels is completely lost. If they happen to contain important data, such as
the pixels that lie along a thin vertical line, the result can be disappointing.
Figure 2: Shrinking an image with
StretchDraw can produce a rough, jagged
result.
A better method is to combine nearby pixels by averaging them to produce the
pixels in the reduced output image. One way to do this is to consider each pixel
in the output image. For each output pixel, the program calculates the pixels in
the input image that map to that output pixel. It then averages the red, green,
and blue components of those input pixels to produce the output pixel’s color
value. This process is shown graphically in Figure 5.

Listing One (on page 10) shows the ShrinkPicture procedure — a Delphi rou-
tine that reduces an image. The function reduces the area from_x1 <= x <=
from_x2, from_y1 <= y <= from_y2 in the input canvas from_canvas, into the
area to_x1 <= x <= to_x2, to_y1 <= y <= to_y2 in the output canvas to_canvas.

The key to the code is the function that maps an output pixel back to the input
pixels that determine its value. If the image is being scaled by factors of xscale
and yscale in the X and Y directions, respectively, then the output pixel (to_x,
Figure 3: Enlarging an image smoothly to_y) is mapped to the rectangle x1 <= x <= x2, y1 <= y <= y2 where:
produces a slightly fuzzy image with no
blockiness. y1 = Trunc((to_y - to_y1) / yscale + from_y1)
y2 = Trunc((to_y + 1 - to_y1) / yscale + from_y1) - 1
x1 = Trunc((to_x - to_x1) / xscale + from_x1)
x2 = Trunc((to_x + 1 - to_x1) / xscale + from_x1) - 1

After the procedure finds the coordinates of the input rectangle, it calculates the
average of the red, green, and blue color components of those pixels. It assigns
the resulting color components to the output pixel (to_x, to_y).

The example program Sizer demonstrates this method for reducing images (this
program is available for download; see end of article for details). Select File |
Open to load an image. Enter a scaling factor of less than 1 in the Scale edit box.
If you click the Quick Scale button, the program uses StretchDraw to shrink the
image by the factor you specified. If you click the Smooth Scale button, the pro-
gram uses the ShrinkPicture procedure to shrink the image smoothly. Figure 6
shows the Sizer program in action.

Enlightening Enlargement
To enlarge an image, StretchDraw duplicates each pixel in the original image.
If the enlarged image is five times as wide and five times as tall as the origi-
Figure 4: Shrinking an image smoothly
nal, StretchDraw turns each pixel into a five-by-five block of pixels in the
removes jagged edges.
enlarged image. This gives a blocky result like the one shown in Figure 1.

7 November 1998 Delphi Informant


On the Cover
While shrinking and enlarging an
image seem to be very different tasks,
a technique similar to the one used by
the procedure ShrinkPicture described
in the previous section allows a pro-
gram to enlarge an image smoothly.

For each output pixel, the program cal-


culates the point within the input
image that maps to the output pixel.
Figure 7 shows this mapping graphical-
ly. Most of the time, that point doesn’t
correspond to an integral pixel loca-
tion. In Figure 7, the output pixel is
mapped to a point in the lower-right
corner of the upper-left input pixel.
Figure 6: The example program Sizer smoothly shrinking a picture.
If the image is being scaled by factors
of xscale and yscale in the X and Y
directions, then the output pixel (to_x, to_y) is mapped to
the point (sfrom_x, sfrom_y) where:

sfrom_y = (to_y - to_y1) / yscale + from_y1


sfrom_x = (to_x - to_x1) / xscale + from_x1

The program then examines the four pixels at integral loca-


tions near the computed input point. In Figure 7, those
pixels are the four in the upper-left corner. Those pixels
have coordinates:

(ifrom_x, ifrom_y) Figure 7: Mapping an output pixel back to a point within the
input image.
(ifrom_x + 1, ifrom_y)
(ifrom_x, ifrom_y + 1)
(ifrom_x + 1, ifrom_y + 1)

where the values ifrom_x and ifrom_y are:

ifrom_y = Trunc(sfrom_y)
ifrom_x = Trunc(sfrom_x)

The program examines the red, green, and blue color compo-
nent values of these four pixels. It uses weighted averages to
calculate the components of the output pixel. The average is
taken so the input pixels closest to the input point contribute
the most to the result.
Figure 8: The example program Sizer smoothly enlarging a picture.
Listing Two (on page 10) shows the EnlargePicture procedure
— a Delphi routine that uses this mapping method to
enlarge images. If you examine the code closely, you can veri- | Open to load an image. Enter a scaling factor greater
fy the special case that occurs when the input point corre- than 1 in the Scale edit box. If you click the Quick Scale
sponds exactly to one of the four input pixels. In that case, button, the program uses StretchDraw to enlarge the image
the weighting factors for the other three points are zero, so by the factor you specified. If you click the Smooth Scale
the output pixel’s entire value is due solely to the one input button, the program uses the procedure EnlargePicture to
pixel. That makes some sense. If an input pixel maps exactly enlarge the image smoothly. Figure 8 shows the Sizer pro-
to an output pixel, they should have the same color. gram enlarging a picture smoothly using EnlargePicture.

In addition to shrinking images, the Sizer program uses Spin Cycle


the EnlargePicture procedure to enlarge images. Select File The equations for shrinking or enlarging an image are fairly

8 November 1998 Delphi Informant


On the Cover
straightforward. In fact, they are simple enough that it’s possible // Calculate the height and width of the rotated picture.
to map blocks of pixels from the input image onto the pixels in procedure TSizerForm.GetRotatedSize(theta: Single;
old_width, old_height: Integer;
the output image. The procedures described here do the oppo-
var new_width, new_height: Integer);
site; they map output pixels back to points in the input image. begin
new_width := Round(Abs(old_width * Cos(theta)) +
Abs(old_height * Sin(theta)));
Either method will work for enlargement or reduction, but
new_height := Round(Abs(old_width * Sin(theta)) +
the reverse method described here also makes many other Abs(old_height * Cos(theta)));
kinds of smooth transformations manageable. For example, end;
you can use reverse mapping to rotate images smoothly. Figure 9: Code that calculates the height and width needed for
an output image.
When you rotate a point (x, y) through the angle theta
around the origin (0, 0), the resulting point has coordinates
(x’, y’ ) where:

x’ = x * Cos(theta) + y * Sin(theta)
y’ = -x * Sin(theta) + y * Cos(theta)

These equations give the mapping from an input position to


an output position.

The inverse of a rotation through the angle theta is a rotation


through the angle -theta. In other words, to map an output pixel
back to an input position, you apply the previous equations to
rotate the point through the angle -theta. If the output pixel is at Figure 10: The example program Sizer smoothly rotating a
position (x’, y’ ), then the input position is (x, y) where: picture 30 degrees.

x = x’ * Cos(-theta) + y’ * Sin(-theta) The Sizer program uses the RotatePicture procedure to rotate
y = -x’ * Sin(-theta) + y’ * Cos(-theta) images. Select File | Open to load an image. Enter an angle in
degrees in the Angle edit box. If you click the Rotate button,
Because Sin(-theta) = -Sin(theta) and the program uses RotatePicture to smoothly rotate the image.
Cos(-theta) = Cos(theta), these equations simplify to: Figure 10 shows the Sizer program after it has rotated a pic-
ture 30 degrees.
x = x’ * Cos(theta) - y’ * Sin(theta)
y = x’ * Sin(theta) + y’ * Cos(theta) Get Warped
The technique of mapping output pixels back to input posi-
Using these equations, a program can rotate an image. For tions and using a weighted average is a powerful one. It lets
each pixel in the output image, the program uses the pre- you shrink, enlarge, or rotate an image fairly easily. It also lets
vious equations to find the point within the input image you apply more complicated transformations to an image.
that maps to the output pixel. It then uses a weighted For example, you can stretch, twist, or otherwise warp an
average of the four nearest pixels’ color components to image to produce strange results. Just reverse the transforma-
find the output pixel’s color exactly as the procedure tion so you can map output pixels back to input positions
EnlargePicture does. and take a weighted average.

Listing Three (on page 11) shows the RotatePicture procedure Try some shape distorting transformations and see what you
— a routine that uses this technique to rotate an image come up with. If you create a particularly unusual image,
smoothly. This code varies from the previous discussion drop me a note. If it’s interesting enough, I may post it on
slightly, so it can rotate images around their centers, rather my Web site for all to enjoy. ∆
than around the origin. This is why the coordinates (to_cx,
to_cy) are subtracted from the output pixel’s position before The files referenced in this article are available on the Delphi
the calculation, and the coordinates (from_cx, from_cy) are Informant Works CD located in INFORM\98\NOV\DI9811RS.
added to the results.

One last rotation detail remains. When a rectangular picture Rod is the author of several books, including Ready-to-Run Delphi 3.0
is rotated, the corners stick out, so the result is taller and Algorithms [1998] and Visual Basic Graphics Programming [1997], both from
wider than the original image. The procedure GetRotatedSize, John Wiley & Sons. He also writes algorithm columns in Visual Basic Developer
shown in Figure 9, calculates the height and width the out- and Microsoft Office & Visual Basic for Applications Developer. Rod can be
put image needs. The program can use this procedure to reached via e-mail at RodStephens@vb-helper.com, or see what else he’s done
decide how big it should make the output control. at his Web site at http://www.vb-helper.com.

9 November 1998 Delphi Informant


On the Cover

Begin Listing One — ShrinkPicture Begin Listing Two — EnlargePicture


// Shrink the picture in from_canvas and place it in // Enlarge the picture in from_canvas and place it
// to_canvas. // in to_canvas.
procedure TSizerForm.ShrinkPicture( procedure TSizerForm.EnlargePicture(
from_canvas, to_canvas: TCanvas; from_canvas, to_canvas: TCanvas;
from_x1, from_y1, from_x2, from_y2: Integer; from_x1, from_y1, from_x2, from_y2: Integer;
to_x1, to_y1, to_x2, to_y2: Integer); to_x1, to_y1, to_x2, to_y2: Integer);
var
var
xscale, yscale : Single;
xscale, yscale : Single;
to_y, to_x : Integer;
sfrom_y, sfrom_x : Single;
x1, x2, y1, y2 : Integer;
ifrom_y, ifrom_x : Integer;
ix, iy : Integer;
to_y, to_x : Integer;
new_red, new_green : Integer;
weight_x, weight_y : array[0..1] of Single;
new_blue : Integer;
total_red, total_green : Single; weight : Single;
total_blue : Single; new_red, new_green : Integer;
ratio : Single; new_blue : Integer;
begin total_red, total_green : Single;
// Compute the scaling parameters. This is useful if total_blue : Single;
// the image is not being scaled proportionally. ix, iy : Integer;
xscale := (to_x2 - to_x1 + 1) / (from_x2 - from_x1); begin
yscale := (to_y2 - to_y1 + 1) / (from_y2 - from_y1); // Compute the scaling parameters. This is useful if
// the image is not being scaled proportionally.
// Perform the reduction. xscale := (to_x2 - to_x1 + 1) / (from_x2 - from_x1);
for to_y := to_y1 to to_y2 do begin yscale := (to_y2 - to_y1 + 1) / (from_y2 - from_y1);
y1 := Trunc((to_y - to_y1) / yscale + from_y1);
y2 := Trunc((to_y + 1 - to_y1) / yscale + from_y1) - // Perform the enlargement.
1;
for to_y := to_y1 to to_y2 do begin
for to_x := to_x1 to to_x2 do begin
sfrom_y := (to_y - to_y1) / yscale + from_y1;
x1 := Trunc((to_x - to_x1) / xscale + from_x1);
ifrom_y := Trunc(sfrom_y);
x2 := Trunc((to_x + 1 - to_x1) / xscale + from_x1) -
weight_y[1] := sfrom_y - ifrom_y;
1;
weight_y[0] := 1 - weight_y[1];
// Average the values in from_canvas within for to_x := to_x1 to to_x2 do begin
// the box (x1, y1) - (x2, y2). sfrom_x := (to_x - to_x1) / xscale + from_x1;
total_red := 0; ifrom_x := Trunc(sfrom_x);
total_green := 0; weight_x[1] := sfrom_x - ifrom_x;
total_blue := 0; weight_x[0] := 1 - weight_x[1];
for iy := y1 to y2 do begin // Average the color components of the four
for ix := x1 to x2 do begin // nearest pixels in from_canvas.
SeparateColor(from_canvas.Pixels[ix, iy], total_red := 0.0;
new_red, new_green, new_blue); total_green := 0.0;
total_red := total_red + new_red; total_blue := 0.0;
total_green := total_green + new_green; for ix := 0 to 1 do begin
total_blue := total_blue + new_blue; for iy := 0 to 1 do begin
end; SeparateColor(from_canvas.Pixels[
end;
ifrom_x + ix, ifrom_y + iy],
ratio := 1 / (x2 - x1 + 1) / (y2 - y1 + 1);
new_red, new_green, new_blue);
to_canvas.Pixels[to_x, to_y] := RGB(
weight := weight_x[ix] * weight_y[iy];
Round(total_red * ratio),
total_red := total_red + new_red *
Round(total_green * ratio),
weight;
Round(total_blue * ratio));
total_green := total_green + new_green *
end; // End for to_x := to_x1 to to_x2 - 1 loop.
end; // End for to_y := to_y1 to to_y2 - 1 loop. weight;
end; total_blue := total_blue + new_blue *
weight;
// Separate a color into red, green, and blue components. end;
procedure TSizerForm.SeparateColor(color: TColor; end;
var red, green, blue: Integer);
begin // Set the output pixel's value.
red := color mod 256; to_canvas.Pixels[to_x, to_y] := RGB(
green := (color div 256) mod 256; Round(total_red),
blue := color div 65536; Round(total_green),
end; Round(total_blue));
end; // End for to_x := to_x1 to to_x2 loop.
// Combine red, green, and blue color components. end; // End for to_y := to_y1 to to_y2 loop.
function TSizerForm.RGB(red, green, blue: Integer):
end;
TColor;
begin End Listing Two
Result := red + 256 * (green + 256 * blue);
end;
End Listing One

10 November 1998 Delphi Informant


On the Cover

Begin Listing Three — RotatePicture


// Rotate the picture in from_canvas around its center
// through the angle theta in radians placing the result
// in the center of to_canvas.
procedure TSizerForm.RotatePicture(
from_canvas, to_canvas: TCanvas; theta: Single;
from_x1, from_y1, from_x2, from_y2: Integer;
to_x1, to_y1, to_x2, to_y2: Integer);
var
sin_theta, cos_theta : Single;
from_cx, from_cy : Single;
to_cx, to_cy : Single;
sfrom_y, sfrom_x : Single;
ifrom_y, ifrom_x : Integer;
to_y, to_x : Integer;
weight_x, weight_y : array[0..1] of Single;
weight : Single;
new_red, new_green : Integer;
new_blue : Integer;
total_red, total_green : Single;
total_blue : Single;
ix, iy : Integer;
begin
// Calculate the sine and cosine of theta for later.
sin_theta := Sin(theta);
cos_theta := Cos(theta);

// Find the centers of the canvases.


from_cx := (from_x2 - from_x1) / 2;
from_cy := (from_y2 - from_y1) / 2;
to_cx := (to_x2 - to_x1) / 2;
to_cy := (to_y2 - to_y1) / 2;

// Perform the rotation.


for to_y := to_y1 to to_y2 do begin
for to_x := to_x1 to to_x2 do begin
// Find the location (from_x, from_y) that
// rotates to position (to_x, to_y).
sfrom_x := from_cx + (to_x - to_cx) * cos_theta -
(to_y - to_cy) * sin_theta;
ifrom_x := Trunc(sfrom_x);

sfrom_y := from_cy + (to_x - to_cx) * sin_theta +


(to_y - to_cy) * cos_theta;
ifrom_y := Trunc(sfrom_y);

// Only process this pixel if all four adjacent input


// pixels are inside the allowed input area.
if (ifrom_x >= from_x1) and (ifrom_x < from_x2) and
(ifrom_y >= from_y1) and (ifrom_y < from_y2) then
begin
// Calculate the weights.
weight_y[1] := sfrom_y - ifrom_y;
weight_y[0] := 1 - weight_y[1];
weight_x[1] := sfrom_x - ifrom_x;
weight_x[0] := 1 - weight_x[1];

// Average the color components of the four


// nearest pixels in from_canvas.
total_red := 0.0;
total_green := 0.0;
total_blue := 0.0;
for ix := 0 to 1 do begin
for iy := 0 to 1 do begin
SeparateColor(
from_canvas.Pixels[ifrom_x + ix,
ifrom_y + iy], new_red, new_green, new_blue);
weight := weight_x[ix] * weight_y[iy];
total_red := total_red + new_red * weight;
total_green:= total_green + new_green * weight;
total_blue := total_blue + new_blue * weight;
end;
end;

// Set the output pixel's value.


to_canvas.Pixels[to_x, to_y] := RGB(
Round(total_red), Round(total_green),
Round(total_blue));
end; // End if adjacent pixels in bounds.
end; // End for to_x := to_x1 to to_x2 loop.
end; // End for to_y := to_y1 to to_y2 loop.
end;
End Listing Three
11 November 1998 Delphi Informant
Informant Spotlight
Delphi 2, 3, 4 / Windows 95, 98, NT

By Kevin Bluck

Tray Icons
Implementing Them the Delphi Way

N o doubt you’ve seen them: those little pictures down on the right side of
the Windows 95/98/NT Taskbar. They’re called Tray Icons, and they pro-
vide a very handy place to stash a program that you want to run quietly in the
background, offering just enough visual feedback to keep track of it without
cluttering up the desktop. Like many aspects of the Windows 95/NT Shell, how-
ever, there is precious little information available on how to implement them.
This article aims to give you all the information you need to fully understand
the tray icon API, and presents a component for implementing them the Delphi
way. (The packaged component and a demonstration application are available
for download; see end of article for details.)

The Windows API for tray icons is remark- Inprise as TNotifyIconData. Let’s look at this
ably small. In fact, it consists of only a single record’s structure, element by element:
function: Shell_NotifyIcon. This function,
TNotifyIconData = record
and its accompanying data record, cbSize: DWORD;
TNotifyIconData, are the only tools you’ll use Wnd: HWND;
to manipulate your icons. As is so often the uID: UINT;
uFlags: UINT;
case with the Windows API, however, this uCallbackMessage: UINT;
apparent simplicity is deceptive. As usual, the hIcon: HICON;
devil is in the details. szTip: array [0..63] of
AnsiChar;
end;
The Basics
The Shell_NotifyIcon function and its asso- cbSize must be set to the size in bytes of the
ciated data types are defined by Inprise (nee entire record. Because this record is a fixed
Borland) in the ShellAPI unit. This func- size, this is easily accomplished by using the
tion has a simple interface, with only two SizeOf function.
arguments:
function Shell_NotifyIcon(dwMessage: DWORD; Wnd must be set to the handle of the win-
lpData: PNotifyIconData): BOOL; stdcall; dow that will receive the tray icon’s notifica-
tion messages. These notifications are primar-
The first argument, dwMessage, is straightfor- ily of mouse events, such as clicks. We’ll go
ward. There are three things you can do to a into more detail about these later. For now,
tray icon: add, modify, or delete. Accordingly, just remember that a window handle must be
there are three constants defined for this pur- associated with every tray icon.
pose: NIM_ADD, NIM_MODIFY, and
NIM_DELETE. Simply pass the constant uID is provided so your application and
appropriate for the desired operation. Windows can identify a particular tray icon.
This value is included in the notification mes-
The second argument lpData, is more compli- sages sent to the window identified in the Wnd
cated. This is a pointer to a record defined by member. Windows identifies each icon in the
12 November 1998 Delphi Informant
Informant Spotlight
tray by the combination of window handle and this ID. If each version of Shell_NotifyIcon is, predictably,
icon is governed by a different window, you can set uID to any Shell_Notif yIconW, which takes a data record of type
value you wish and ignore it (for all practical purposes). If you TNotif yIconDataW. The only difference between the two
are using one window to control multiple icons, however, you record structures is the szTip member, which is an:
should assign and keep track of meaningful values in this mem-
ber, as neither you nor Windows would have any other way of array[0..63] of WideChar
telling which icon is which.
in the wide version. All other elements are identical.
uFlags is used to alert the Shell_NotifyIcon function which of
the three optional data members have valid values. There are Shell_NotifyIcon returns a Boolean value (like most Win32
three constants defined for this purpose: NIF_MESSAGE, API functions), which reports if the function succeeded. I’ve
NIF_ICON, and NIF_TIP. uFlags may be set to any or all of found the errors returned by the GetLastError API function are
these using the or operator. As an example, including remarkably uninformative when Shell_Notif yIcon fails. The
NIF_MESSAGE in uFlags signals Shell_Notif yIcon that the only message I could coax it to reveal was, “A Windows API
uCallbackMessage member has a valid value. If this flag is not function failed.” Gee, thanks for that probing insight,
included, Shell_NotifyIcon will ignore any information in Windows! Our only consolation is that the tray icon API is
uCallbackMessage. Similarly, NIF_ICON corresponds to simple enough that the problem is usually not difficult to find.
hIcon, and NIF_TIP governs szTip.
Mousing Around
uCallbackMessage is where you specify the value of the notifica- We’re still missing one important aspect of tray icons: reacting to
tion message sent to the owning window whose handle is in mouse input. The only way a user can interact with a tray icon is
Wnd. This value can be any that doesn’t correspond to any other to use the mouse. Accordingly, we need a way to detect these
message that might be handled by the window. The best way to mouse events. A tray icon is not a window in the normal sense.
ensure this is to add some constant value to WM_USER. In It’s governed by the system tray notification area, which is a
fact, just using WM_USER by itself is sufficient. The message special-purpose system window, and over which we have no easy
value merely needs to be unique for the window, not the entire means of control. Because the tray icon is not a window, it
system, or even throughout your application. Remember, this requires a window handle to be supplied to the Wnd data mem-
member is ignored if the NIF_MESSAGE constant is not ber in the TNotif yIconData record. This gives a place for the sys-
included in the uFlags member. tem tray window to send notification messages when it deter-
mines that mouse activity is occurring over your icon. The value
hIcon accepts the handle of the icon image that you wish to specified by the uCallbackMessage member is the actual identifier
appear in the tray. The easiest way to get at this value in Delphi of the notification message for that icon, so the window that
is to load your icon into a TIcon object, and use the Handle handles the message can recognize it.
property of that object. As before, you must set the NIF_ICON
flag to alert Shell_Notif yIcon that it should interpret the value of It’s important to understand that the messages sent to the
this member. It’s quite possible to modify the tray icon’s appear- message handler window are not actual Windows mouse
ance after adding it to the tray by submitting a different icon messages. They don’t include any of the extra information
handle via the hIcon member in conjunction with a normally packaged with such messages, such as the mouse
NIM_MODIFY operation. If you’ve noticed “animated” tray position or the keyboard state. They are simply notification
icons before, they work by doing exactly this sort of icon swap- messages, which tell you only that a mouse event occurred.
ping, probably managed by a timer.
Let’s examine the difference between a normal mouse message
szTip is the last member. This is the text of the tooltip, which and a tray icon notification message. All Windows messages
appears above most tray icons when you rest the mouse cursor have three primary components: the message identifier, a two-
over the icon for a second or two. It is, like almost every other byte piece of data commonly known as wParam, and a four-
string in Windows, a null-terminated string. However, it’s impor- byte piece of data known as lParam. We’ll compare the
tant to note that this member is actually defined as a static array Windows message that’s sent when the left mouse button is
of 64 characters, not a character pointer. This means that after pushed down over a normal window, and the message sent
you allow for the null terminator, a maximum of 63 characters when the same action occurs over a tray icon. For the normal
will fit in the tooltip. This should not be a practical concern for window, it receives a message with an identifier equal to the
most developers, but there’s always somebody who wants to constant integer value WM_LBUTTONDOWN, a wParam
stretch a metaphor a little too far. Like the other two optional value encoding the state of the keyboard, and an lParam
data members, the icon’s tip will not be updated unless the encoding the position of the mouse relative to the window’s
NIF_TIP flag is set in the uFlags member. client area. The window handling the same event for a tray
icon, however, receives a message whose identifier is whatever
Unicode Applications value was specified by the latest valid uCallbackMessage value,
If you wish to develop Unicode applications, there are a wParam with a value equal to the uID value for the icon,
wide-character versions of these API elements. The wide and an lParam containing the constant integer value

13 November 1998 Delphi Informant


Informant Spotlight
WM_LBUTTONDOWN. As you can see, all the tray icon in the tray. Thus, we should provide some means for turn-
message handling window knows is that the button was ing design-time visibility on and off.
pressed. It has no idea exactly where it was pressed, nor
whether any keys such as S or C were down at the One last thing: Tray-icon applications often start with the
same time. This pretty much eliminates our ability to respond only visible evidence of their execution being the icon.
to input any more sophisticated than a simple mouse click. Furthermore, a tray-icon application often hides itself com-
pletely (except for the tray icon), including hiding its
To further illustrate how mouse input is handled by the taskbar button. It would be convenient for the component
message handler window, here’s a code snippet from the user if the component provided some simple means of hiding
window procedure of a tray icon’s message handler window: the entire application from the desktop and taskbar.

All these considerations lead to the following list of published


// If the message is a tray notification message...
if Msg.Msg = WM_TRAYNOTIFY then begin properties:
// Check the lParam piece of the message structure to see
// what happened in the tray. property Hint: string;
case (Msg.lParam) of property Icon: TIcon;
WM_RBUTTONDOWN: ...; property PopupMenu: TPopupMenu;
WM_MBUTTONUP: ...; property ShowAtDesignTime: Boolean;
WM_LBUTTONDBLCLK: ...; property ShowApplication: Boolean;
WM_MOUSEMOVE: ...; property Visible: Boolean;

These four types of notification messages are basically all the What About Events?
message handler will receive from the tray icon. There are a The only things that happen to tray icons without the appli-
few other obscure ones, such as palette change notifications, cation’s prior knowledge are mouse events. We can’t use the
which are probably of no use to any but the most unusual standard TControl mouse event types, however, because the
development efforts. The messages for mouse button down, notification-style message just doesn’t provide the same infor-
up, and double-click come in groups of three — one each for mation as the full mouse messages that TControl objects
the left, middle, and right buttons. Mouse move messages also receive. As a result, we’re pretty much limited to mouse but-
arrive, but as there is no good way of determining the mouse’s ton and click events. Although they’re not likely to be of
exact position at the time of the message, the rectangle occu- much use without position information, we’ll throw in move
pied by the tray icon, or any method of setting the tray icon events just to be complete.
to capture all mouse input, it’s difficult to determine when the
mouse moves off the tray icon. Yes, it would be possible to Here’s the list:
call GetCursorPos in response to these events, but that won’t
necessarily produce the mouse position at the time the mes- property OnClick: TkbTrayClickEvent;
sage was generated. On a slow system with a rapidly moving property OnDoubleClick: TkbTrayClickEvent;
property OnMouseDown: TkbTrayMouseButtonEvent;
mouse, the mouse position retrieved from GetCursorPos could property OnMouseMove: TNotifyEvent;
be quite far from the point where the message was generated property OnMouseUp: TkbTrayMouseButtonEvent;
after it finally works its way through the message queue. At
this time, I have not solved this problem, nor found any Next, we consider the subject of run-time and read-only prop-
information suggesting an answer. Maybe you can. erties. Although there are a few “internals” that might surface,
such as the notification message value, it’s hard to imagine
Creating a Component what possible use they could be outside the context of the tray
Enough of this messy API stuff. Now that you know what icon. There’s really no need to expose such pointless detail.
to do at the Windows level, let’s get started making a com-
ponent that will eliminate the need for you to remember it. Run-time Methods
The last area of the public interface to consider, the run-time
First, let’s define the component’s public interface. There methods, does lend a few candidates for consideration. Almost
are a few obvious things we need to provide for the imple- any component that has a visible interface should provide a
mentation of a tray icon. An icon seems foremost. Also, Refresh method. Also, the component user may want to invoke
tray icons typically have a tooltip hint and a popup menu the popup menu directly. These are the methods:
associated with them. Of course, like all visual interface ele-
ments, we’ll want a means to show and hide the tray icon. procedure Refresh;
procedure ShowPopupMenu;

A couple of less obvious traits come to mind after a bit of


reflection on how this component will likely be used. We’ll There are, of course, many details to implementing a compo-
probably want to be able to check it out at design time, nent beyond the core functions this component encapsulates.
without running the application to see our handiwork. On There are many other excellent references that cover the
the other hand, when the work is ready to test run from minutiae of implementing custom components in Delphi, so
Delphi, we probably won’t want to see two identical icons this article will not cover them. Instead, it will concentrate

14 November 1998 Delphi Informant


Informant Spotlight
only on those pieces of the component’s implementation that ...
directly relate to the specific problem of tray icons. // If the new value is different from the existing value...
if (NewValue <> Self.FVisible) then begin
// If the new value is True, and we are either Designing
Shell_NotifyIcon // and ShowAtDesignTime is True or else we are not
The implementation of tray icons revolves around the call to // designing at all, add the icon to the tray.
if ((NewValue) and
Shell_NotifyIcon. Everything we do in this component will
(((csDesigning in Self.ComponentState) and
be in preparation for the moment of that function call. Let’s (Self.ShowAtDesignTime)) or
outline how the public properties and methods will relate to (not (csDesigning in Self.ComponentState)))) then
that single goal. Self.NotifyTrayIcon(NIM_ADD);
// Otherwise, delete the icon from the tray.
else
The property most directly linked to Shell_NotifyIcon is the Self.NotifyTrayIcon(NIM_DELETE);
Visible property. Setting this property to True will cause ...
end;
Shell_NotifyIcon to be invoked with an operation of ...
NIM_ADD, causing the icon to appear in the tray. As you
might guess, setting it to False will call Shell_NotifyIcon with Figure 1: Code from the private property writer method, SetVisible.
an operation of NIM_DELETE, and the icon disappears.
One minor complication to this rather simple scenario is
...
the action of ShowAtDesignTime. If Visible is set to True at
// If the new value is different from the existing value...
design time, it must check to see if ShowAtDesignTime is if (NewValue <> Self.FShowAtDesignTime) then begin
also True before adding the icon. The property handles the // If we are now designing, and the component is set to
logic, but the mechanics of calling Shell_NotifyIcon are dele- // Visible...
if ((csDesigning in Self.ComponentState) and
gated to another private method, Notif yTrayIcon, which will (Self.Visible)) then
be discussed later. Meanwhile, the code snippet in Figure 1 // If the new value is True, add the icon to the Tray.
(from the private property writer method, SetVisible) illus- if (NewValue) then
Self.NotifyTrayIcon(NIM_ADD);
trates the logic. // If the new value is False, delete the icon from
// the Tray.
ShowAtDesignTime has a similar problem in reverse. If Visible else
Self.NotifyTrayIcon(NIM_DELETE);
is False, then it doesn’t matter what ShowAtDesignTime is
...
being set to. If Visible is True, however, and we are designing end;
at the moment, then ShowAtDesignTime is responsible for ...
seeing that the icon is added and deleted. Again, the call to
Shell_NotifyIcon is delegated to NotifyTrayIcon, in the interest Figure 2: Code from the property writer method,
of centralizing that code. Figure 2 shows how the property SetShowAtDesignTime.
writer method, SetShowAtDesignTime, does its thing.
own invisible window, whose sole purpose is to handle those
The Hint and Icon properties have a similar problem, i.e. how notification messages, and over whose destiny we have
to update the visible properties of the tray icon while it’s sit- absolute control. This step eliminates problems with con-
ting in the tray. This is exactly the sort of job for which the flicting messages and hooks.
NIM_MODIFY operation is intended (live updates to an
existing icon). As you might guess, the properties don’t modi- Inprise thoughtfully provided a couple of functions to facilitate
fy the icon themselves; they update their internal storage and this scheme. They are AllocateHWnd and DeallocateHWnd,
call the Refresh method, which calls the now-famous private found in the Forms unit. AllocateHWnd’s purpose in life is to
NotifyTrayIcon method. generate a handle to an invisible window, using the window
message-handling procedure you provide. Exactly what we
You might think the PopupMenu property is in the same need! Now, in the constructor, we can get and store a handle
live-update boat as Hint and Icon, but it’s not. The popup to a window that has nothing better to do than manage our
menu is generated only on request, so it’s sufficient simply icon’s messages:
to update its internal storage and leave it at that. The new
// Allocate invisible window to handle tray notification
menu will be available the next time the ShowPopupMenu // messages.
function is called. Self.FWindowHandle := AllocateHWnd(Self.WindowProcedure);

Managing the Messages As you can see, this call is trivial. What’s important is the
Next, we consider all the events. A window must be avail- window procedure we passed. This is where the notifica-
able to process all the notification messages generated by the tion messages from the icon are received, and where we
user’s interaction with the icon. How best to provide this have the opportunity to dispatch them. AllocateHWnd
window? We could use the application’s main form, but that takes a single TWndMethod argument, a class-member pro-
would require hooking that form’s window procedure, a cedure that itself takes a single TMessage argument. It’s up
messy undertaking at best. It seems simplest to generate our to us to provide that procedure. To give you an idea,

15 November 1998 Delphi Informant


Informant Spotlight

procedure TkbTrayIcon.WindowProcedure(var Msg: TMessage); al members will contain valid data. Simply updating all
begin three every time is much easier than trying to determine
// If the message is a tray notification message...
which has changed, and carries no noticeable performance
if Msg.Msg = WM_TRAYNOTIFY then begin
// Check the lParam piece of the message structure to penalty. The message identifier in uCallbackMessage never
// see what happened in the tray. changes; it’s always the WM_TRAYNOTIFY constant
case (Msg.lParam) of
defined in our unit.
WM_LBUTTONDBLCLK: Self.DoubleClick(mbLeft);
...
end; Assigning the Icon
end
Assigning the icon requires a little fancy footwork. There’s really
// If the message is not a tray notification message,
// then send it to the default window procedure for no point inserting a tray icon without an icon, yet the compo-
// handling. nent user might not have assigned an icon to the component,
else begin
and may never intend to. If this is the case, we’ll simply use the
Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg,
Msg.wParam, Msg.lParam); Application object’s icon. The Application object always has an
end; icon, even if one wasn’t assigned by the developer — even at
end;
design time — so it seems the best place to fetch a default. You
Figure 3: An abbreviated version of our component’s procedure.
should know that at design time, the Application object refers to
Delphi, so the default icon will be Delphi’s, not the icon you
Figure 3 shows an abbreviated version of our component’s may have assigned in the Project Options. Any icon you assigned
procedure. to the Application object will appear at run time. Of course, if
you have assigned an icon to the tray icon component’s Icon
As we discussed earlier, we first check the message identifier to property, that icon will appear at both design and run time:
verify that this incoming message is a WM_TRAYNOTIFY // If this component has an icon assigned, use that for the
message. We ignore all others and send them to default han- // icon shown in the tray.
if (Self.FIcon.Handle <> 0) then
dling, because none of the miscellaneous messages typically
IconData.hIcon := Self.FIcon.Handle;
broadcast to every window in the system interest us. else
WM_TRAYNOTIFY is a constant we define; it’s not provided // Otherwise, use the Application's icon.
IconData.hIcon := Application.Icon.Handle;
by Windows. Setting it equal to WM_USER is the easiest
thing to do, and perfectly safe. Once we’re satisfied this is
indeed a notification message, we decode the lParam value to The hint string also requires a bit of manipulation. Because
determine which event the message is relating to us, and switch it’s possible to assign a string longer than the maximum
to an event-dispatch method based on that information. tooltip size of 63 characters, we must ensure the property
Because the message-handler window in our component is only value reflects the text of the tooltip if such an overlong
handling a single tray icon, we can safely ignore the uID value string is assigned:
encoded into the wParam data member.
// If the hint string is defined, then load it into the
// icon data structure. Note that the structure can only
// hold 63 characters, so trim it if necessary.
The NotifyTrayIcon Method StrPLCopy(IconData.szTip,Self.FHint,High(IconData.szTip));
Now, with all these supporting tasks worked out, we can final- Self.FHint := StrPas(IconData.szTip);
ly get to the heart of the matter: the long-awaited call to
Shell_NotifyIcon. It might seem a bit anticlimactic, but this Finally, the moment of truth. We call Shell_NotifyIcon:
function is found in only one place throughout the entire
component. This place, of course, is the NotifyTrayIcon private // Instruct shell to perform operation on tray icon based
method. Let’s work our way through it. // on the Operation value and the IconData structure.
Shell_NotifyIcon(Operation, @IconData);

First, set up the TNotifyIconData structure:


The result is a component that makes tray icons almost trivial to
// Set up the tray icon data structure. implement. You can forget all that Windows API unpleasantness.
IconData.cbSize := SizeOf(IconData);
IconData.Wnd := Self.FWindowHandle;
IconData.uID := 0; How to Use Tray Icons
IconData.uFlags := NIF_MESSAGE or NIF_ICON or NIF_TIP; Now that you have this spiffy component to add tray icons
IconData.uCallbackMessage := WM_TRAYNOTIFY; with the greatest of ease, a few words on how to use them
to maximum beneficial effect are in order. Like all tools,
cbSize takes the actual size in bytes of its own structure. they can be used for good or evil. It’s important to remem-
Wnd takes the handle we created for our private message- ber that the Taskbar tray is a shared resource. Every appli-
handling window in the constructor. uID takes 0, because cation installed by the user has the opportunity to com-
we have no need to use it (thanks to our private window, pete for space in that area. Don’t go nuts adding icons to
which handles absolutely nothing other than this icon). We the tray, or the useful impact of this excellent metaphor
add all three flags to uFlags, signifying that all three option- will be diluted. The user may learn to hate the little guys.

16 November 1998 Delphi Informant


Informant Spotlight
Because the tray is a system resource, it’s appropriate to Windows, provides a good example of such a popup.
use it only to convey information of a global nature, or to This window should appear close to the tray icon, so
provide an outlet for applications that ought to be con- the user doesn’t have to hunt for it. If you have no
stantly monitored without interfering with the user’s other additional information worth displaying, or the con-
work. An example of the global sort of icon is the Volume trols you wish to provide are too complex for such a
Control provided by Microsoft. This icon allows the user fleeting popup window, then do nothing on a left-click.
to control the volume of every sound generated by every Right-click: Display a popup menu displaying possible
application. That’s probably worth a 16-by-16 pixel square operations for the icon. These operations should always
of valuable screen real estate. An example of the monitor- include an Exit command for users who wish to kill an
ing kind of icon would be an e-mail indicator. I have one application altogether. There’s nothing more irritating
that came with Netscape Communicator that waves a little than an application you can’t get rid of. The default
flag whenever I have mail to download. That one is also command on this menu usually should be to show the
worth 256 pixels. main form of the icon’s application, providing complete
access to all functions of the application.
If your icon is of the status-monitor kind, give some seri- Double-click: Execute the popup menu’s default comand.
ous thought to how you can present as much information This should be to show the application’s main form.
as possible to the user via its appearance alone. Requiring
users to physically interact with it to get any useful infor- Conclusion
mation defeats the purpose of a status icon; they should The system tray is a compact and powerful interface
have a good idea of the application’s status just by looking metaphor, providing visual feedback and almost unlimited
at the icon. Animation is particularly effective at directing facility for user interaction into an extraordinarily compact
user attention to important events. You don’t need a package. With the information presented in this article, you
graphics workstation to do this; perfectly good animation can exploit this compelling feature in your own software. See
effects can be achieved with two or three icons swapped what you can build — and have fun! ∆
around with a timer.
The component and demonstration applications are available
Generally, a tray icon should support some standard behaviors. on the Delphi Informant Works CD located in
These include: INFORM\98\NOV\DI9811KB.
Mouse hover: Provide a tooltip that, at a minimum, iden-
tifies the purpose of the icon. Ideally, the tooltip text
should expand on the status information provided by the
Kevin Bluck is an independent contractor specializing in Delphi develop-
icon. For example, an e-mail indicator might specify how ment. He lives in Sacramento, CA with his lovely wife Natasha. He spends
many messages are waiting. his spare time chasing weather balloons and rockets as a member of JP
Left-click: Provide some sort of popup window to dis- Aerospace (http://www.jpaerospace.com), a group striving to be the first
play further information, or offer basic controls to the amateurs to send a rocket into space. Thanks to Jay Hallett for his valuable
user. Popup windows are preferable to standard win- assistance in developing the icon component. Kevin can be reached via e-
dows, because they can be dismissed by clicking else- mail at kbluck@ix.netcom.com.
where. The Volume Control icon, included with

17 November 1998 Delphi Informant


In Development
Delphi 3 / Team Development / Object Repository

By G. Bradley MacDonald

The Object Repository


An Easy Tool for Sharing and Standardizing Forms

D elphi’s Object Repository (OR) is a great method for sharing forms and/or
objects among your projects, as well as your developers and their projects.
The OR is only available to the machine on which Delphi is installed. So, each
developer has his or her own copy of the OR that is inaccessible to other devel-
opers. To fully utilize the OR, you may want to consider sharing it among all the
developers in your company.

One of the benefits of a shared OR is stan- when you create a new form or the main
dardization. For example, you might have a form of new projects. The nice thing about
standard header that should be used on all this is that if you need to change the header
forms created for your company. Rather than or add some other object later, you simply
have each developer put the header on the change the form in the shared OR. The
forms, you can create a form with the header changes flow through all projects for all
saved to the shared OR, making it automati- developers that inherited from this form the
cally available to all developers to inherit or next time they are opened.
copy. You can even make it the default form
You can even take this idea to an extreme;
Forms= you might have an entire maintenance pro-
Dialogs= gram, with all the logic on one form in the
Projects=
Data Modules=
OR. When you want a new program to
TimeAcct= maintain a particular table, simply inherit
from the form in the OR, make the connec-
[C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCANCL1]
Type=FormTemplate
tions to the correct DataSet, drop the fields
Name=Standard Dialog from the DBExplorer onto the form, and
Page=Dialogs you’re done. If you use the inherit option
Icon=C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCANCL1.ICO
Description=OK, Cancel along bottom of dialog.
when creating the new form, this would allow
Author=Borland you to update all maintenance programs sim-
DefaultMainForm=0 ply by updating the one copy of the form in
DefaultNewForm=0
Ancestor=
the shared OR.

[C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCNHLP1] What Makes Up the OR?


Type=FormTemplate
Name=Dialog with Help
The OR consists of two main parts: the con-
Page=Dialogs figuration file and the objects. The configu-
Icon=C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCNHLP1.ICO ration file is really nothing more than a plain
Description=OK, Cancel, Help along bottom of dialog. This is
an Inherited Form.
text file named Delphi32.dro. This file is the
Author=Borland heart and soul of the OR, and contains the
DefaultMainForm=0 references to most of the objects shown in
DefaultNewForm=0
Ancestor=C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCANCL1
the OR dialog box. When you add or
remove an object from the OR, you are really
Figure 1: A sample Delphi32.dro file. adding or removing lines from this file (see

18 November 1998 Delphi Informant


In Development

Figure 3: The Environment Options dialog box.

Before:
[C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCANCL1]
Type=FormTemplate
Name=Standard Dialog
Page=Dialogs
Icon=C:\PROGRAM FILES\BORLAND\DELPHI 3\OBJREPOS\OKCANCL1.ICO
Description=OK, Cancel along bottom of dialog.
Author=Borland
DefaultMainForm=0
DefaultNewForm=0
Figure 2: Sample directory structure. Ancestor=

Figure 1). This file, by default, is held in the directory path After:
\Borland\Delphi 3\Bin. All the objects that ship with Delphi [\\NTSvr1\ShareName\Delphi\ShrdOBJREPOS\OKCANCL1]
Type=FormTemplate
are held in the directory \Borland\Delphi 3\Objrepos and its Name=Standard Dialog
subdirectories (see Figure 2). Page=Dialogs
Icon=\\NTSvr1\ShareName\Delphi\ShrdOBJREPOS\OKCANCL1.ICO
Description=OK, Cancel along bottom of dialog.
Any objects you create and add to the repository don’t have Author=Borland
to be kept in this directory structure. However, if they are to DefaultMainForm=0
be shared among your developers, they need to be stored on DefaultNewForm=0
Ancestor=
a shared drive on the LAN.
Figure 4: Sample change of an entry in the OR.
How to Share the OR
At the bottom of the Preferences tab of the Environment same share. This is an important point. If you use the
Options dialog box the Directory edit box in the Shared drive:directory format and a developer connects to the
Repository section allows you to specify the directory you same server and directory using a different drive letter,
wish to use to locate the shared OR (see Figure 3). When they won’t be able to access the objects in the shared OR
you enter a directory name in this edit box, Delphi will because of the hard-coded reference to the drive letter in
create a Delphi32.dro file there for you if it doesn’t exist. the Delphi32.dro file.
The directory you enter must be a shared directory on the
LAN, perhaps a shared drive on an NT server. Each Delphi Issues of Sharing
developer must point to the same shared directory. This is a The one drawback with simply changing the shared reposi-
case when you should consider using a UNC name (i.e. tory location in the Environment Options dialog box is
\NTServer\ShareName\Delphi\SharedObjRepos) rather that you lose access to some of the objects that install with
than the standard drive:directory naming style (i.e. Delphi. An easy solution to this is to copy the entire
X:\Delphi\SharedObjRepos). ObjRePos directory structure from the default Delphi
install to the new shared directory before changing the
The reason for the UNC convention is that it references a shared repository location in the Environment Options
server and does not depend on what drive letter you have dialog box. Then, modify each entry in the Delphi32.dro
assigned. If you use the drive:directory method, each file to point to the new location (see Figure 4). This allows
developer must have the same drive letter assigned to this you to share not only your own objects, but those installed

19 November 1998 Delphi Informant


In Development
with Delphi as well. I prefer this method because it provides
the most flexibility.

The main concern with sharing the OR is that if a change is


made, it’s made to all developers who are using the shared
OR. All projects for all developers that have inherited from
an object in the OR, which is changed, will be affected.
Remember that objects that have been copied from objects
in the OR are not affected by changing the original object in
the OR. The change could be something as simple as a
developer changing the default new form or default main
form for the shared OR. If one developer changes which
form will be the default for all new forms, it affects all users
of that shared repository. This change is then forced on all
other developers.

While this may be a good way to make global changes, it


can be very confusing — and dangerous. For example,
developer A changes the default new form before going
home on Monday, and developer B comes in Tuesday morn-
ing and tries to add a new form to his or her project.
However, instead of getting the traditional blank form,
developer B gets whatever form developer A selected as the
default. If developer B doesn’t know what happened, they
may lose valuable time trying to determine what is wrong
with Delphi. (In my early tinkering with sharing the OR,
this exact problem occurred, resulting in a call to Inprise’s
support line.)

As the preceding example shows, developers that share a


LAN-based OR have to be a little more careful about the
changes they make. There are times when, as a developer,
you want to try out an idea and don’t want to expose other
developers to any problems that it may cause. In this case,
you could simply blank out the Directory edit box in the
Shared Repository section on the Preferences tab of the
Environment Options dialog box. This would then point
you to the original OR on your machine, and any changes
that you make there will only affect you. At this point, you
can perform your testing, and when you’re finished, you can
point back to the shared OR and apply the changes there. I
find this useful, as it gives me a ‘sandbox’ in which to try
things out before I change the shared OR.

Conclusion
Sharing the Object Repository is easy, and can be a great tool
for standardization. It does require that a little more care be
taken on the part of the developers when updating and using
it. However, the small amount of risk is worth the immense
benefits that can be realized by sharing forms and other
objects among all your developers and across your company. ∆

G. Bradley MacDonald is the Technology Planner at the Liquor Distribution Branch


of British Columbia, where he is responsible for supporting Delphi and AS/400
developers. He can be reached at Bradley_MacDonald@LDB.GOV.BC.CA
or Bradley_MacDonald@BC.Sympatico.CA.

20 November 1998 Delphi Informant


DBNavigator
Aliases / Database Components / BDE

By Cary Jensen, Ph.D.

Delphi Database Development


Part III: The Database Component

I n last month’s “DBNavigator,” we considered the role of the BDEDataSet com-


ponents. This month’s installment continues our extended series on database
topics with an introduction to the Database component.

The Database component is responsible for date, i.e. it doesn’t need to be explicitly read
providing BDEDataSet components with from a remote server.)
information about the nature and location of
your data. (TTable, TQuery, and TStoredProc Global vs. Local Aliases
objects are all BDEDataSet components.) You control which Database a particular
Specifically, a Database component stores the BDEDataSet uses by assigning an alias to the
location of the data (whether it’s local or BDEDataSet’s DatabaseName property. An
remote) and what driver to use to access this alias is either the DatabaseName property of
data. It’s also responsible for holding configu- a Database component, or the name of a
ration information pertaining to the data configured database within the BDE config-
access. For example, a Database component uration file. (You modify your BDE configu-
can define parameters that control how data ration using the BDE Administrator.) An
is accessed and updated. alias that references a database configuration
from the BDE is referred to as a “global
There is another, equally important role the alias,” and one that references the
Database component plays. It represents your DatabaseName property of a Database com-
connection to a remote server in a client/serv- ponent is referred to as a “local alias.”
er application. Using TDatabase methods, you
can connect to, or disconnect from, a server; (Note: Technically speaking, there is a third
start, commit, and rollback transactions; and value that can be assigned to a BDEDataSet’s
store schema information about the files of DatabaseName property. If your data is
your database. (Schema information includes stored in local tables, you can assign the path
data about your database, including the tables, of your data files to the DatabaseName prop-
fields, indexes, and so forth.) erty. While this value is not a true alias, it’s
more similar to a global alias than to a local
A Database component plays a similar role alias, in that such a value uses the parameters
with respect to local data — those file server- defined on the Configuration page of the
based applications that use Paradox or BDE Administrator based on the data type
dBASE tables. However, these tables are con- of the file you’re accessing.)
trolled directly by the BDE (Borland
Database Engine). Consequently, no true Global aliases. A global alias gets its name
login connection is required, all transactions from the fact that it’s available to any appli-
are controlled directly by the BDE, and local cation that can use the BDE. For example,
databases don’t store schema information any developer using Delphi, C++Builder, or
that needs to be read by the database. Data Gateway for Java can use a global alias
(Because the BDE controls all access to local for the access to data. DBDEMOS and
tables, schema information is always up-to- IBLOCAL are examples of global aliases.

21 November 1998 Delphi Informant


DBNavigator
They’re configured during Delphi’s installation, and refer to configuration, while still maintaining final control over
sample data files used by many Delphi sample projects. particular parameters. A very simple case of this involves
using the parameters stored in a global alias, and then
An application that uses a global alias is configurable outside adding or changing one or more parameters, such as a
of the program logic in your application. Specifically, it’s possi- password, in the Database. (Note that a password can’t be
ble to write an application that’s completely unaware of the stored in a BDE configuration file.)
details of the data location, driver type, or connection parame- The second reason for combining global and local aliases
ters. Such applications are easily scalable, i.e. you can change is to provide a component for controlling transactions and
the data location, type, and parameters without re-compiling server connections. Such a local Database may use all the
your application. For example, an application that makes use configuration information from a global alias, while pro-
of a global alias can be designed and tested on a stand-alone viding a convenient component for starting, committing,
machine, yet be deployed in a client/server environment with- and rolling back transactions.
out being re-compiled. It’s only necessary to update the data-
base configuration for the alias using the BDE Administrator. Configuring global aliases using the BDE Administrator is
beyond the scope of this article. For information on configur-
Local aliases. In contrast, local aliases are available only to the ing global aliases, please refer to the BDE Administrator’s
application in which the corresponding Database component online Help. Configuring local Database components, on the
appears. Local aliases are created by adding a Database com- other hand, is the topic of the remainder of this article.
ponent to one of your forms or data modules, and then set-
ting the Database’s properties to identify the driver and data Databases vs. Aliases
access parameters. It’s important to make a clear distinction between a Database
component and an alias. A Database component is an
Normally, you add a Database component that defines a local instance of the TDatabase class. Every BDEDataSet requires a
alias at design time. However, it’s perfectly valid, although Database component to define and control its access to data.
typically more work, to add one programmatically at run An alias, by comparison, defines either an existing Database
time. This can be done by calling the TDatabase class’ con- component, or a set of parameters that will be applied to an
structor, and then configuring the properties of the Database automatically created Database component.
object the constructor returns.
When you attempt to activate a BDEDataSet, it first determines
Once a Database has been created, the DatabaseName proper- whether it’s connected to a Database (via the DatabaseName
ty of the Database component is visible to all objects within property). If the DatabaseName property has been assigned a
the application. Specifically, the DatabaseName property is local alias (the DatabaseName property of an existing Database
stored in the global name space, making this alias visible even component), the BDEDataSet first checks whether this
to code that appears in units that don’t use the unit in which Database is open. If the Database is open, the BDEDataSet
the Database component is referenced (either as a variable or attempts to open itself. If the Database isn’t open, the
a member field of type declaration). BDEDataSet must first open the Database before opening itself.

While a global alias has the advantage of being configurable When the first BDEDataSet making use of a given global alias
outside of your application, local aliases also have an important attempts to open, it begins by creating an instance of the
advantage. Using a local alias, your code can control all aspects TDatabase class. The parameters used for this Database are
of the connection. For example, it can be written to define the drawn from the BDE configuration based on the global alias
database driver and access control parameters based on infor- name. As each additional BDEDataSet that uses the same glob-
mation determined at run time. To determine the location of al alias attempts to open, each will note the Database created by
the data, for instance, your application could read an .INI file the first BDEDataSet to open, and will attach to that Database.
on a shared network drive, or it could test the location of the
application’s executable (using the Application.ExeName Configuring a Local Alias
method, or the ParamStr(0) function call). As mentioned earlier, a local alias is one associated with a
Database component in your application. Although it doesn’t
These two alias types — global and local — aren’t mutually matter how this component is created, the typical technique is
exclusive. Indeed, it’s not uncommon for the data access to add a Database to a form or data module, and then use the
information used by a BDEDataSet to come from both alias Database Editor dialog box to configure the Database. This
types at the same time. Specifically, a Database (whose technique provides for the automatic creation of the Database
DatabaseName property constitutes a local alias) can be con- component (as part of the creation of the form or data mod-
figured to read its initial configuration information from a ule on which it appears), as well as simplifying the process of
global alias. This is often done for one of two reasons: configuring a BDEDataSet to use the Database.
A local Database can selectively override parameters stored This process is demonstrated in the following steps:
in a global alias. This permits your application to leverage 1) Create a new project.
the configuration flexibility provided through the BDE 2) Add a Data Module by selecting File | New from Delphi’s

22 November 1998 Delphi Informant


DBNavigator
main menu, and then double-clicking the Data Module
Wizard on the New page of the Object Repository.
3) Add a Database component to the Data Module.
4) Display the Database Editor (see Figure 1) by double-
clicking on the Database, or by right-clicking the
Database and selecting Database Editor.
5) At Name enter temp. This value is the local alias name.
6) Move to the Driver name drop-down list (we’re not going
to use the Alias name drop-down list in this example).
Using the Driver name drop-down list, select STANDARD
(the driver is used with Paradox and dBASE tables).
7) Click the Defaults button. This loads the Parameter overrides
list box with default parameters based on the BDE configu-
ration file. Because a very simple local alias is being created,
Figure 1: The Database Editor.
there are only three parameters: PATH, DEFAULT DRIVER, and
ENABLE BCD (Binary Coded Decimal).
8) Leave DEFAULT DRIVER and ENABLE BCD with their default
values. For PATH, enter the fully qualified directory path
where Delphi stored its sample Paradox and dBASE tables.
In Delphi 4 this path is C:\Program Files\Common Files\
Borland Shared\Data. In all previous versions, it’s the
Demos\Data directory under the directory in which Delphi
is installed. For example, in Delphi 3 this path is
C:\Program Files\Borland\Delphi3\Demos\Data. When
you’re done, your Database Editor should look like that
shown in Figure 2. Click OK to accept the dialog box.
9) Ensure the data module is created before the main form.
To do this, select Project | Options to display the Project
Options dialog box. Drag DataModule2 to the top position
in the Auto-create forms list. Alternatively, you can simply Figure 2: The Database Editor defining a local alias.
edit the project (.DPR) file, moving the call to CreateForm
for the data module to the line before the call to the form’s
CreateForm statement. (Note: Only data modules can
appear before the main form in the Auto-create forms list.
By definition, the main form is the first auto-created form.)
10) Continuing with the building of your main form, return
to Form1, and add a DBNavigator and DBGrid compo-
nent to it. Set the DBNavigator’s Align property to alTop,
and the DBGrid’s Align property to alClient.
11) Add one DataSource and one Table to the form. Set the
DataSource’s DataSet property to Table1. Set the
DataSource property of both the DBNavigator and the
DBGrid to DataSource1.
12)We’re now ready to configure the Table to use the local
alias. Set the Table’s DatabaseName property to temp
(the DatabaseName property of the Database you
Figure 3: A sample database application that uses a local alias.
entered in step 5). Next, set the Table’s TableName
property to customer.db. Finally, set the Table’s Active
property to True. Table can open, it locates the Database referenced using the
local alias, and attempts to open it. Once the Database is
Run the application. Your form should look as shown in opened, the Table can attempt to open itself. Once the Table
Figure 3. is open, the DataSource informs the DBNavigator and the
DBGrid to read the data and paint themselves appropriately.
When you run this application, the data module is created
first, making the Database available. Then, the form is creat- Experienced Delphi database developers who read the previ-
ed, which causes the creation of the objects that have been ous description will no doubt be wondering why I didn’t place
placed on it. Once these objects are created, their properties the DataSource and the Table on the Data Module along with
are loaded, including the Table’s Active property. Before the the Database. The answer is that I wanted to demonstrate that

23 November 1998 Delphi Informant


DBNavigator
the local alias was accessible from Table1’s Name drop-down
list, even though the unit in which Form1 is defined doesn’t
use the unit in which the data module is defined. If we had
placed the Table and/or the DataSource on the Data Module,
Form1’s unit would be required to use DataModule2’s unit.

Controlling Database Parameters at Run Time


In the preceding example, the path to the data was hard coded.
As mentioned earlier, it’s possible to define the parameters of a
Database at run time. To demonstrate this, modify the example
project you’ve created by following these steps:
1) At Form1, set Table1’s Active property to False.
2) At the Data Module, double-click the Database to display
its Database Editor. Click the Clear button to erase the
Figure 4: The Database Editor with the password stored.
parameters from the Parameter overrides list box.
3) At Form1, select Form1 from the Object Inspector. From
the Events page, double-click the OnCreate field to gener- 7) In this case, we’ll be adding the password to the local
ate an OnCreate event handler for the form. alias. To do this, enter the following line into the
4) Edit the OnCreate event handler to look like the following: Parameter overrides list box:

procedure TForm1.FormCreate(Sender: TObject); PASSWORD=masterkey


begin
DataModule2.Database1.Params.Clear;
DataModule2.Database1.Params.Add( 8) Because the password is now stored with the Database
'PATH=D:\Program Files\Common Files' + component, it’s no longer necessary to prompt the user
'\Borland Shared\Data'); for the password. To disable the display of the Database
DataModule2.Database1.Params.Add(
'DEFAULT DRIVER=PARADOX'); Login dialog box, remove the check from the Login
DataModule2.Database1.Params.Add('ENABLE BCD=FALSE'); prompt check box of the Database Editor. The Database
Table1.Open; Editor should look as shown in Figure 4.
end;
9) The remaining steps of this example are identical to
steps 9 through 12 in the example given under the sec-
5) Finally, with Form1 selected, select File | Use Unit to dis- tion “Configuring a Local Alias.” Consequently, those
play the Use Unit dialog box. Select Unit2 from this list. steps aren’t repeated here. However, there is one differ-
Note that although the local alias, temp, is available to the ence. Instead of adding a Table, use a Query compo-
Table without using Unit2, the Database reference nent. To configure the Query, set its DatabaseName
(Database1) isn’t available to Unit1 unless it’s using Unit2. property to csdemo, and its SQL property to the follow-
6) Press 9 to run the project. Again, your application ing SQL statement:
should look like the one shown in Figure 3.
SELECT * FROM CUSTOMER
Although the actual parameters added to the Database’s
Params property were hard coded in this example, it would 10) Set the Query’s Active property to True. The query will
have been perfectly acceptable to base these parameters on execute, and the returned records will be displayed in the
information determined at run time. DBGrid.

Creating a Local Alias Based on a Global Alias Press 9 to run the project. You’ll notice the main form is
Earlier in this article you learned that local aliases can be displayed without prompting for the password. In this case,
based on global aliases. The following steps demonstrate how the local alias uses all parameters of IBLOCAL, which identify
this is done: where the data is located and what driver to use. The
1) Create a new project. Database component, however, adds the password, which is
2) Add a Data Module to the project. used to establish a connection to the server when the Query
3) Add a Database component to the Data Module. component attempts to execute.
4) Display the Database Editor for the Database component.
5) Enter csdemo as Name. This value is the local alias name. This example demonstrates how to override global alias para-
6) Move to the Alias name field and select IBLOCAL. When meters with a local alias. However, it’s rarely wise to permit
you use the Alias name field, you’re specifying that the unchallenged access to a database server. Consequently, this
parameters of your local alias will be based on the technique is normally only used during development, where
named global alias. If there are any parameters you you would like to avoid having to enter the password each
want to add or override, you specify these using the time you test your application. Before deploying such an
Parameter overrides list box. application, you should return to the Database Editor dialog

24 November 1998 Delphi Informant


DBNavigator
box and remove the password parameter from the Parameter
overrides list box, and enable the Login prompt check box.

Conclusion
Database components are used to customize access to a data-
base, as well as to control database connections and transac-
tions. This article demonstrated how to configure a
BDEDataSet component to use a Database component, pro-
viding you with control over access to the underlying data.

In next month’s “DBNavigator” we will take an in-depth look


at Data Modules, including when you should use them, and
when they should be avoided. ∆

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


database development company. He is co-author of 17 books, including
Oracle JDeveloper [Oracle Press, 1998], JBuilder Essentials
[Osborne/McGraw-Hill, 1998], and Delphi in Depth [Osborne/McGraw-Hill,
1996]. He is a Contributing Editor of Delphi Informant, and is an interna-
tionally respected trainer of Delphi and Java. For information about Jensen
Data Systems’ consulting or training services, visit http://idt.net/~jdsi or
e-mail Cary at cjensen@compuserve.com.

25 November 1998 Delphi Informant


Sound+Vision
Graphics Programming / Gaming

By Peter Dove

The Camera Never Lies


Delphi Graphics Programming: Part VI

I t’s here! This is the long-awaited finale of the “Delphi Graphics Programming”
series that started in January, 1997. In this series, we watched the development
of the TGMP 3D-rendering engine, from its inception through several phases of
growth (see Figure 1). We’re almost done.

In this final installment, we’ll cover how to Unfortunately, you’re only able to stand at
get a camera coordinate system working, the center of your 3D universe and look
adding animated textures, and finally, how ahead. You can not turn around and see
to include foreground pictures, i.e. those what’s behind you, you can not look up or
that allow you to view your 3D objects down, and you can not decide to have a
through a window frame, a cockpit, etc. walk around. Rather limiting, one would
We haven’t been able to include everything think.
we would have liked, but we hope that
what follows will be enough to send you on To remedy this situation, we’ll be adding a
your way to developing your own version. camera coordinate system; we are adding
the concept of having a camera that can be
Smile Please moved anywhere in the system by posi-
So far, TGMP allows you to add an object tioning it at coordinates X, Y, and Z, or
and move it around in 3D space. rotated about its X, Y, and Z axes.

Let’s start by adding two variables to the


public section of the TGMP component:

CameraPosition: TPoint3D;
CameraRotation: TPoint3D;

Also, we need to add a section to the


TObject3D record to store the camera coor-
dinates for all the polygons:

PolyCamera: array[0..MAXPOLYS] of TPolygon;

Adding the camera coordinate functionality


works similarly to the way we added the
world coordinate system. We created a new
method named WorldToCamera, which takes
the objects after the LocalToWorld procedure
Figure 1: The TGMP application in action. has processed it.

26 November 1998 Delphi Informant


Sound+Vision
The explanation of how it works is simple. Imagine you’re property EnableForegroundBitmap : Boolean
standing at the center of the 3D universe. You have an read FEnableForeground write SetEnableForeground;
property ForegroundBitmap : TPicture
object directly in front of you. Now, imagine that the read GetForegroundBitmap write SetForegroundBitmap;
camera (you) moves to the right. If an outside observer
saw a snapshot of the scene before and after you moved, The two private members for the properties in the following list-
they would not be able to tell whether it was the camera ing should be placed into the private section of TGMP. Also list-
(you) that had moved or the object itself. You get the same ed is a support private member named FForegroundDIB, which
picture if you move the camera to the right by five units allows a quicker way of putting the bitmap onto the screen:
or move the object to the left by five units. The trick of
making it look as though we have a camera is based on FForegroundDIB : TDib16Bit; // Stores foreground bitmap.
FForegroundBitmap : TPicture; // Holds foreground bitmap.
this principle. FEnableForeground : Boolean; // Is foreground enabled?

The WorldToCamera procedure achieves the trick of cam- The three private methods that support the new properties
era movement in stages (see Listing Four on page 30). are listed in Figure 2, and are straightforward. The last
First, the procedure takes the variable CameraPosition and method, SetEnableForeground, changes the private member
subtracts its values from that of all the polygons. FEnableForeground, then calls the Paint method to ensure
Remember that if you move the camera five units to the the display keeps up to date with the current state.
right (i.e. CameraPosition.X is 5), the object looks as
though it has been moved five units to the left. The following statements must be added to the Create con-
structor to initialize the new foreground properties:
Second, the procedure takes the CameraRotation variable
and applies the inverse rotations to all the polygons. If, { ****** Added to support foreground properties ****** }
while you look at this page, you rotate your head to the
// Create the foreground Bitmap.
right, it looks as if the page has rotated to the left. FForegroundBitmap := TPicture.Create;
// Create the foreground DIB.
Last, we must remember to apply the same tricks to the FForegroundDib := TDib16Bit.Create(ViewHeight, ViewWidth);

light source as well; it also has a position and a direction. { ****** End of support for foreground properties ****** }

We also need to change two methods for this to work. The A new method, CopyForeground, is also created, and is called
first one is RemoveBackfacesAndShade. In this method, we every time the frame is rendered. CopyForeground works in a
need to change all references to PolyWorld to PolyCamera. simple manner (see Figure 3). By the time CopyForeground
The other method we need to change is RenderNow. This
method must have the statement: procedure TGMP.SetForegroundBitmap(Value: TPicture);
begin
WorldToCamera(Object3D); FForegroundBitmap.Assign(Value);
end;

inserted into every section of the case statement following function TGMP.GetForegroundBitmap: TPicture;
the statement: begin
result := FForegroundBitmap;
end;
LocalToWorld(Object3D);
procedure TGMP.SetEnableForeground(Value: Boolean);
begin
Also, every reference to Object3D.PolyWorld[x] must be FEnableForeground := Value;
replaced with Object3D.PolyCamera[x] for the correct poly- Paint;
gons to be rendered. end;

Figure 2: These three private methods support


Through the Eye of a Needle ForegroundBitmap and EnableForegroundBitmap.
Foreground pictures are nearly as useful as background pic-
procedure TGMP.CopyForeground;
tures. By using a foreground picture, you can make your
var
display show the controls of a cockpit in an airplane, or X, Y : Integer;
make it seem as though you’re looking through a keyhole. PixelColor : Word;
begin
for X := 0 to FForegroundDib.Width - 1 do
To begin, we need to create two new design-time proper- for Y := 0 to FForegroundDib.Height - 1 do begin
ties. One is named ForegroundBitmap, which is of type PixelColor := FForeGroundDiB.GetPixel(X,Y);
if PixelColor <> 0 then
TPicture. This property allows you to load a bitmap. The
FDib.SetPixel(X, Y, PixelColor);
second property, EnableForegroundBitmap, tells the render- end;
ing engine whether to include the foreground in its final end;
image. Both properties need to go into the published sec- Figure 3: The CopyForeground method is called every time the
tion of TGMP: frame is rendered.

27 November 1998 Delphi Informant


Sound+Vision

procedure TGMP.Paint;
var
tmpBitmap : TBitmap;
begin
inherited;
Figure 4: A { New ClearBackPage using the DIB class's methods. Notice
sample image that it now passed a 16-bit color value rather than
a TColor. }
with a fore-
FDib.ClearBackPage(CalculateRGBWord(FColor));
ground.
{ Create a bitmap. }
tmpBitmap := TBitmap.Create;
{ Assign the Foreground's Handle to it. }
tmpBitmap.Handle := FForegroundDIB.GetHandle;
{ Stretch it to the full window. }
tmpBitmap.Canvas.StretchDraw(
ClientRect, ForegroundBitmap.Bitmap);
{ Release the handle and free the temporary bitmap. }
tmpBitmap.ReleaseHandle;
tmpBitmap.Free;
{ Copy background buffer to main screen. }
FlipBackPage;
end;
is called, the bitmap you selected as the foreground is stored
in FForegroundDIB. The scene has been rendered into Figure 5: The customized Paint method.
FDIB. TGMP chooses black as its invisible color. It now
scans each pixel in the FForegroundDIB and transfers it to For this to work, we need only one private variable,
FDIB. If the method comes across the color 0 (Black) in FAnimationList. Its declaration follows, and should be
FForegroundDIB, it doesn’t transfer it to the rendered image. placed in the private section of TGMP:

Figure 4 shows a sample with a foreground image. As you can FAnimationList: TList; // List of all animation frames.
see, the image has a picture of a frame with a kind of gun scope
in the center. If you were to choose this as a foreground image, We’re also going to use three methods to support animated
everything but the black would be transferred onto the window. textures. Their declarations should be placed in the public
section of TGMP (see Figure 6). Following each method is
There are two more methods that need to be changed to a section explaining its functionality.
complete the implementation of the foreground bitmap.
The first is the WMSize method. This has two lines that AddAnimationFrame allows the programmer to add a single
need to be added to update the height and size of the animation frame to FAnimationList. Remember that with this
FForegroundDIB: rendering engine, your bitmap size for textures is 128- by-
128 pixels. The AddAnimationFrame declares a pointer to
FForegroundDIB.Free;
FForegroundDIB := TDib16Bit.Create(ViewHeight, ViewWidth); TBitmapStorage. The first line of the method dynamically cre-
ates new memory exactly the size of TBitmapStorage, and puts
The other method is Paint (see Figure 5). To transfer the the pointer to it in pBitmapStorage.
newly chosen background bitmap into FForegroundDIB
from FForegroundBitmap, we to need to create a temporary We now add that pointer to FAnimationList. (Remember
bitmap in the shape of the local variable tmpBitmap. First, that TList simply holds a list of pointers.) After adding the
we create the temporary bitmap, then we assign the handle pointer to FAnimationList, the next loop copies all the pixels
of the FForegroundDIB to it. This allows us to use the from the parameter of AddAnimationFrame, Bitmap, into
methods belonging to TBitmap to write onto the BitmapStorage. Notice how we access the TBitmapStorage
FForegroundDIB. We then use the StretchDraw method of array from the pointer:
TBitmap to draw the ForegroundBitmap into
FForegroundDIB. After we have finished with TBitmap, we pBitmapStorage^[x,y]

release its handle. Releasing the handle this way does not
destroy it. We then free it, and the work is done. The ^ symbol dereferences the pointer, i.e. it tells the com-
piler to treat pBitmapStorage as type TBitmapStorage instead
Animated Textures of as a pointer to a TBitmapStorage structure. You may have
Wouldn’t it be great to have a ball of fire with animated noticed that we’re copying the bitmap in upside down (note
flames? That’s what got me thinking we should create animat- [127-y]). This is because DIBs are stored upside down.
ed textures. There are many uses; one that springs to mind is
spooling video onto 3D objects. We’ll implement an animat- Our next method is DeleteAnimationFrame, which accepts
ed texturing algorithm that is pretty simple, but we’re sure one parameter named Frame (see Figure 7). This tells the
you’ll have fun with it. The great thing about this feature is method which frame to delete from FAnimationList. Again,
that we get involved with dynamic memory allocation. we declare a local variable named pBitmapStorage as a point-

28 November 1998 Delphi Informant


Sound+Vision

procedure TGMP.AddAnimationFrame(Bitmap : TBitmap); { Section 1 - to be used on the OnShow event of the form. }
var { Load animation frames. }
X, Y : Integer; Bitmap := TBitmap.Create;
pBitmapStorage : ^TBitmapStorage; for X := 1 to 33 do begin
begin Bitmap.LoadFromFile(ExtractFilePath(
{ Create a dynamic TBitmapStorage. } Application.ExeName) + 'Frame' + IntToStr(X) + '.bmp');
New(pBitmapStorage); GMP1.AddAnimationFrame(Bitmap);
{ Add it to the animation list. } end;
FAnimationList.Add(pBitmapStorage); Bitmap.Free;
{ Fill in the data. }
for X := 0 to 127 do { Section 2 - to be used in the Timer event. }
for Y := 0 to 127 do var
pBitmapStorage^[x,127-y] := AnimFrame: Integer = 0;
CalculateRGBWord(Bitmap.Canvas.Pixels[X,Y]); begin
end; if Start1.Checked then begin
GMP1.SetCurrentBitmapWithAnimationFrame(AnimFrame);
Figure 6: Calling three methods that support animated textures. Inc(AnimFrame);
if AnimFrame > 32 then
AnimFrame := 0;
procedure TGMP.DeleteAnimationFrame(Frame: Integer); end;
var
pBitmapStorage : ^TBitmapStorage;
Figure 8: A simple example of calling animated frames.
begin
{ Check to see the it is a valid frame. }
if (Frame < 0) or (Frame > FAnimationList.Count) then
Exit; parameter is the source, the place from which we want to
{ Get a pointer to the TBitmapStorage. } copy. This parameter is also of type Pointer. I’ve used
pBitmapStorage := FAnimationList[Frame]; FAnimationList[Frame], a pointer to the TBitmapStructure
{ Dispose of the memory. }
Dispose(pBitmapStorage); of the nth (Frame) frame. The last parameter is the
{ Decrease the list. } amount (in bytes) of the memory to copy, which in our
FAnimationList.Delete(Frame); case is SizeOf(TBitmapStorage). The SizeOf function
end;
returns the size of an object/record/structure in bytes.
Figure 7: The DeleteAnimationFrame method accepts the Frame
parameter.
To show you how to use animated textures, we’ve included
a snippet from the demonstration application (see Figure
er to TBitmapStorage. The method checks that the Frame 8). The first section shows how to set up the animation,
parameter is valid, then assigns from FAnimationList the nth and the second section shows how to animate the textures.
(Frame) pointer. It then calls the Dispose procedure, which
releases memory to the exact size of TBitmapStorage. The Conclusion
nth (Frame) entry in the FAnimationList is also deleted. That’s it! You now have a basic 3D-rendering engine. Of
course, it can be made to go faster and have much more func-
SetCurrentBitmapWithAnimationFrame, the last method in tionality, but you’re on the right path to take it further your-
this trio, is easy to figure out: self. This series took us through the evolution of the TGMP
rendering component using basic techniques, such as proper-
procedure TGMP.SetCurrentBitmapWithAnimationFrame( ties, events, and property editors, as well as more advanced
Frame: Integer);
begin techniques, including device-independent bitmaps, sprites, and
{ Make sure the frame is a valid one. } memory management. We covered a lot in this series, so I’m
if (Frame < 0) or (Frame > FAnimationList.Count) then sure you will find many of the techniques useful for your own
Exit;
{ Copy the memory of the animation frame projects. You should also be convinced that Delphi is worthy
into FCurrentBitmap. } of respect as a games and graphics programming language. ∆
CopyMemory(@FCurrentBitmap, FAnimationList[Frame],
SizeOf(TBitmapStorage));
end; [Note: Parts 1 through 5 of this series were published in the
following issues of Delphi Informant (all in 1997): January,
CurrentBitmap is the texture mapped onto the polygons February, March, April, and July.]
as they are rendered. So, to create animated textures,
we only need to change the CurrentBitmap every frame. The entire sample application referenced in this article is avail-
Obviously, we need a quick way of doing this. The able on the Delphi Informant Works CD located in
SetCurrentBitmapWithAnimationFrame method achieves INFORM\98\NOV\DI9811PD.
this by first making sure the Frame parameter is valid.
Peter Dove is Managing Director of Kortex Systems Limited, based in the
Then, it uses the procedure CopyMemory, which accepts United Kingdom. Kortex Systems Limited has a range of software devel-
three parameters: The first is the destination, and is of opment services that can be seen at http://www.kortex.co.uk. Peter can
type Pointer. I have this as @FCurrentBitmap, which be e-mailed at peterd@kortex.co.uk.
means “memory address of FCurrentBitmap.” The second

29 November 1998 Delphi Informant


Sound+Vision

Begin Listing Four — WorldToCamera procedure


procedure TGMP.WorldToCamera(var AnObject: TObject3D);
var
X : Integer;
begin
{ Loops though all polygons, transferring them to
PolyCamera. }
with AnObject do begin
for X := 0 to NumberPolys -1 do begin
{ Additional optimizations. }
with PolyCamera[x] do begin
{ Subtract camera coordinates from
world coordinates. }
Point[0].X :=
PolyWorld[X].Point[0].X - CameraPosition.X;
Point[0].Y :=
PolyWorld[X].Point[0].Y - CameraPosition.Y;
Point[0].Z :=
PolyWorld[X].Point[0].Z - CameraPosition.Z;

Point[1].X :=
PolyWorld[X].Point[1].X - CameraPosition.X;
Point[1].Y :=
PolyWorld[X].Point[1].Y - CameraPosition.Y;
Point[1].Z :=
PolyWorld[X].Point[1].Z - CameraPosition.Z;

Point[2].X :=
PolyWorld[X].Point[2].X - CameraPosition.X;
Point[2].Y :=
PolyWorld[X].Point[2].Y - CameraPosition.Y;
Point[2].Z :=
PolyWorld[X].Point[2].Z - CameraPosition.Z;

Point[3].X :=
PolyWorld[X].Point[3].X - CameraPosition.X;
Point[3].Y :=
PolyWorld[X].Point[3].Y - CameraPosition.Y;
Point[3].Z :=
PolyWorld[X].Point[3].Z - CameraPosition.Z;

{ Rotate the points using the camera rotation. }


RotatePoint(1, 0, 0, -CameraRotation.X, Point[0]);
RotatePoint(0, 1, 0, -CameraRotation.Y, Point[0]);
RotatePoint(0, 0, 1, -CameraRotation.Z, Point[0]);

RotatePoint(1, 0, 0, -CameraRotation.X, Point[1]);


RotatePoint(0, 1, 0, -CameraRotation.Y, Point[1]);
RotatePoint(0, 0, 1, -CameraRotation.Z, Point[1]);

RotatePoint(1, 0, 0, -CameraRotation.X, Point[2]);


RotatePoint(0, 1, 0, -CameraRotation.Y, Point[2]);
RotatePoint(0, 0, 1, -CameraRotation.Z, Point[2]);

RotatePoint(1, 0, 0, -CameraRotation.X, Point[3]);


RotatePoint(0, 1, 0, -CameraRotation.Y, Point[3]);
RotatePoint(0, 0, 1, -CameraRotation.Z, Point[3]);

{ Rotate the light source. }


TransLightSource := LightSource;
RotatePoint(1, 0, 0, -DegToRad(CameraRotation.X),
TransLightSource);
RotatePoint(0, 1, 0, -DegToRad(CameraRotation.Y),
TransLightSource);
RotatePoint(0, 0, 1, -DegToRad(CameraRotation.Z),
TransLightSource);

NumberPoints := PolyWorld[X].NumberPoints;
PolyColor := PolyWorld[X].PolyColor;
DibColor := PolyWorld[X].DibColor;
end;
end;
end;
end;
End Listing Four

30 November 1998 Delphi Informant


Dynamic Delphi
Threading / DLLs

By Gregory Deatz

Thread-Safe DLLs
Creating Win32 DLLs for Multi-threaded Applications

M ulti-threaded applications are now the norm, not the exception. Indeed, the
safest assumption to make about a Win32 program is that it uses multi-
threading. Therefore, as software houses publish mechanisms for extending an
application’s functionality, the developer is forced to write thread-safe libraries.

When most of us think about the implemen- GetNextWord will return the second word,
tation of a particular procedure or function, then the third word, and so on — until
we generally think in terms of local and glob- GetNextWord is forced to return an empty
al variables. However, a procedure or function string because there are no more words in st.
in a DLL can be called by multiple threads Clearly, these functions can be implemented
simultaneously, and care must be taken to in a thread-safe manner, and they can also be
ensure that variables previously understood to implemented without taking the possible
be “global” are not actually “thread-local.” existence of threads into consideration.

Take, for example, functions that must retain Note: There are many ways to achieve similar
“state” between calls. A developer might GetNextWord functionality; this is just one
choose to implement a set of functions like: implementation. The key is to recognize that
there are types of questions where it’s desirable
function GetFirstWord(st: string): string; that the function library, and not the calling
function GetNextWord(st: string): string; application, retain state between function calls.
For readers generally unfamiliar with manage-
When a developer needs to parse a string, he ment of thread-local variables, and the need
or she first calls GetFirstWord, which returns for them, the Exe1 program (see Figure 1) and
the first word of st. The developer is unaware its accompanying DLL (Dll1) are intended to
that GetFirstWord remembers where it demonstrate the simple case of a single-thread-
stopped scanning st, so subsequent calls to ed process and a non-thread-safe DLL. They
provide the simple base for all subsequent
thread-safe examples, and are left for the reader
to study. (The complete source for Exe1, Dll1,
and all other demonstration programs dis-
cussed in this article are available for down-
load; see end of article for details.)

Delphi’s Memory Manager


Before we go too far, we must discuss the
Delphi memory manager. Way back when,
when Delphi was Borland Pascal, and it was
built for DOS, the memory manager did not
have to concern itself with threads, so the
Figure 1: An example of how to declare a valid DllProc and memory manager Delphi used in version 1
how to allow it to be called. was not thread-safe.

31 November 1998 Delphi Informant


Dynamic Delphi

unit DllCode;
be considered a problem, except that the initialization section
... of a unit in a DLL is exactly equivalent to the call:
implementation
...
DllEntryPoint(DLL_PROCESS_ATTACH)
procedure DllEntry(Reason: Integer);
...
initialization Likewise, the finalization section of a unit is exactly equiva-
...
DllProc := @DllEntry;
lent to the call:
...
end. DllEntryPoint(DLL_PROCESS_DETACH)

Figure 2: Declaring and setting up DllProc so it can be called.


Because process initialization and finalization take place in
the obvious places in a Delphi unit, all examples of DllEntry
For some strange reason, Borland resolved the problem by will only respond to DLL_THREAD_ATTACH and
introducing a global variable. The variable, IsMultiThread, DLL_THREAD_DETACH.
when set to True, tells the memory manager to wrap itself in
a Windows-critical section, thus ensuring the memory man- Two Important Issues
ager is thread-safe. Issue one. Suppose an application starts some threads; it then
loads the DLL. The DLL is never explicitly told that those
When using Delphi’s TThread class, the developer is assured threads are executing, i.e. DllEntry will never be called with
that this variable is set. However, if the developer uses “raw” the DLL_THREAD_ATTACH argument for those threads.
Windows threads, or develops a DLL for a multi-threaded However, if those threads exit gracefully, the DLL will be
application, this variable must be set manually. informed they are closing.

This is very important! When developing a DLL for a multi- This means the DLL is potentially responsible for cleaning
threaded application, you must set the IsMultiThread variable. up uninitialized data.

Managing Thread-local Variables Issue two. Suppose an application loads a DLL; it then starts
The key to proper initialization and finalization of thread- some threads. Before gracefully closing its threads, the appli-
local variables is a procedure named DllEntryPoint, which is cation detaches from the DLL. The DLL will be told the
an inherent part of all Win32 DLLs, and it is called when- process is detaching (with DLL_PROCESS_DETACH), but
ever a process or thread attaches to a DLL. Delphi gives the it won’t be explicitly told that each thread is also detaching.
user access to this procedure through a procedure pointer
named DllProc, which is defined in the System unit. This means that any clean-up procedures associated with
the freeing of resources in the DLL won’t be called. (It’s left
DllEntryPoint takes a single Integer argument. The possible as a bit of a brain-teaser to figure this one out.)
values of this argument are DLL_PROCESS_ATTACH,
DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, So what do these issues mean? In brief: Take care!
and DLL_THREAD_DETACH (more about these in just a Unfortunately, there is nothing intuitive about these reali-
bit). Figure 2 shows how to declare a valid DllProc and how ties, although they’re relatively easy to understand using an
to get it called. example. The example program Exe2a allows you to load
Dll2 and watch it call process-level and thread-level initial-
Note that there is no declaration for DllEntry in the inter- ization and finalization. (To make sure you have a clear
face section. This is because the existence of the procedure understanding of the previous comments, you should make
need only be known to the unit itself. When a process first it a point to play around with variations of creating and
attaches to Dll2, the initialization section of DllCode exe- destroying threads and loading and unloading the DLL.)
cutes, where DllProc is set to the address of DllEntry.
But what’s to manage here? If you’re using thread-local
Now that a process has attached to the DLL, DllEntry will scalars (native Delphi types like Integer) or Delphi strings,
be called with DLL_THREAD_ATTACH whenever a you don’t have much to worry about. Exe2b demonstrates
thread is created in the calling application. Whenever a how Dll2 uses a threadvar declaration of an Integer to
thread in the calling application exits gracefully, DllEntry maintain a thread-local index into the string passed to
will be called with DLL_THREAD_DETACH. When a GetFirstWord and GetNextWord. (Again, Exe2a and Exe2b
process is unloading the library, DllEntry will be called with are available for download; see end of article for details.)
DLL_PROCESS_DETACH.
Can Delphi Burp?
Note that because we were not able to specify our DllEntryPoint In conjunction with DllEntry and the initialization and
procedure until the initialization of the DLL, DllEntry will finalization sections of the DllCode unit, threadvar makes
never be called with DLL_PROCESS_ATTACH. This might it easy to manage thread-local variables. Right? Not quite.

32 November 1998 Delphi Informant


Dynamic Delphi
procedure DllEntry(Reason: Integer); unless you put a value in a thread-local variable, you’re guar-
begin anteed it’s “zeroed out.” And, indeed, direct use of Win32’s
case Reason of
DLL_THREAD_ATTACH:
thread-local storage API (discussed later) shows this to be
begin true. However, Delphi has a quirk where its method of
tlObject := TTestObject.Create; thread-local storage has a small gas problem.
DllShowMessage(tlObject.ObjectName);
end;
DLL_THREAD_DETACH: The demonstration programs Dll3 and Exe3 demonstrate
begin how we can get Delphi to burp. First, look carefully at the
if (tlObject = nil) then
DllShowMessage('Object is nil.')
source code for DllEntry in Figure 3. The code is simple, and
else it makes it clear that when a thread attaches to the DLL, it
DllShowMessage(tlObject.ObjectName); will create a thread-local instantiation of TTestObject in
tlObject.Free;
end;
tlObject. Likewise, when a thread detaches from a DLL, it
end; will free whatever resources it allocated for tlObject.
end;

Figure 3: The DllEntry procedure. Now, to get Dll3 to burp:


1) Launch Exe3.
2) Start three threads (see Figure 4), noting that because
the DLL is not yet loaded, no entry points in the DLL
are being called. (Three is an arbitrary number, and is
sufficient for demonstrating the problem.)
3) Load the DLL.
4) Start three more threads, noting the messages the DLL
displays.
5) Go back to the first thread you loaded; close the thread,
noting the fact that tlObject is nil (see Figure 5). Do this
for the next two threads.
6) Now try to close the next thread (see Figure 6). And the
next. And the next (see Figure 7).

What happened to the thread-local objects we created? They


Figure 4: Creating threads in the Dll3 demonstration program. appear to have been lost in the shuffle. Obviously, something is
wrong, as the code is not behaving as expected, and the prob-
lem isn’t in the code we’ve written. The code is absurdly simple!

Forgetting about whose bug it is, the next most obvious


question is: How do we work around this issue, so that we
can maintain thread-local objects?

The Win32 API


We can work around this problem by directly using the
Win32 API. TLSAlloc, TLSFree, TLSGetValue, and
TLSSetValue are the system calls used to manage thread-
local storage:
TLSAlloc is used to allocate an index into the thread-
local “store.” A single, global index refers to a thread-
local pointer — a 32-bit value that is local to the thread.
Figure 5: Closing one of the first three threads, i.e. one of those TLSFree is used to free an index that was previously allo-
created before the DLL was loaded. cated using TLSAlloc.
TLSGetValue is used to query the value of the thread-
Let’s review for a moment. We have navigated process-level local pointer referred to by the index allocated using
and thread-level initialization and finalization of DLLs. It’s TLSAlloc.
quite possible that we’ve exhausted the topic. You should TLSSetValue is used to set the value of the thread-local
have all the tools you require to go off and write completely pointer referred to by the index allocated using
thread-safe DLLs, and manage the instantiation and clean- TLSAlloc.
up of thread-local objects. Or should you?
A code snippet from Dll4 (shown in Figure 8) demonstrates
Here’s the way thread-local storage is supposed to work: All how each of these functions are used. This is what happens,
thread-local storage is zero-initialized. This means that step by step:

33 November 1998 Delphi Informant


Dynamic Delphi

unit DllCode;
...
procedure CreateObject;
procedure FreeObject;

var
tlsObjectIndex: DWord;
...
implementation
...
procedure CreateObject;
Figure 6: After closing the fourth thread, i.e. one created after begin
the DLL was loaded. There appears to be a problem. // First, create the new object and store it in the
// thread-local slot referred to by tlsObjectIndex.
TLSSetValue(tlsObjectIndex,
Pointer(TTestObject.Create));
// Get object just created and display its ObjectName.
DllShowMessage(TTestObject(
TLSGetValue(tlsObjectIndex)).ObjectName);
end;

procedure FreeObject;
begin
// Get thread-local value and report it's nil, or give
// the ObjectName property that was stored there.
if (TLSGetValue(tlsObjectIndex) = nil) then
DllShowMessage('Object is nil.')
else
DllShowMessage(
TTestObject(TLSGetValue(
Figure 7: After closing the final thread ... oops! tlsObjectIndex)).ObjectName);
// Free the object.
TTestObject(TLSGetValue(tlsObjectIndex)).Free;
// Set its value in the thread-local store to nil.
1) The initialization section allocates an index into the TLSSetValue(tlsObjectIndex, nil);
thread-local store and saves this index in tlsObjectIndex. end;
...
2) It then sets DllProc to the address of DllEntry. procedure DllEntry(Reason: Integer);
3) Now it calls CreateObject, which sets the thread-local begin
case Reason of
pointer referred to by tlsObjectIndex to the pointer to
DLL_THREAD_ATTACH: CreateObject;
the newly created TTestObject. CreateObject uses DLL_THREAD_DETACH: FreeObject;
TLSGetValue to then retrieve the newly created thread- end;
end;
local object, so that its ObjectName property can be dis- ...
played back to the user. initialization
4) Whenever a thread is created, CreateObject is called. tlsObjectIndex := TLSAlloc;
DllProc := @DllEntry;
5) Whenever a thread detaches from the DLL, FreeObject is CreateObject;
called, which first displays a message informing the user
finalization
either that the object was never initialized, or it displays the
FreeObject;
ObjectName property of the thread-local object. FreeObject TLSFree(tlsObjectIndex);
then frees the object referred to by the thread-local pointer,
end.
and it then sets the thread-local pointer back to nil.
6) When the library is finally unloaded, it ensures the
Figure 8: An example of how the TLSAlloc, TLSFree,
process-level thread frees up any thread-local storage by
TLSGetValue, and TLSSetValue functions are used.
calling FreeObject.
7) Finally, tlsObjectIndex is freed using TLSFree.
Here are some closing thoughts on maintaining a thread-
It should be clear that the semantics of the Win32 system safe environment within DLLs (even in applications):
calls and Delphi’s threadvar are identical. It’s just a bit more DllEntryPoint is a nifty way for a DLL to stay aware of
cumbersome to use the Win32 calls. active threads in the calling application.
You can run Exe4 and follow the burping exercise in the It’s generally safe to use the threadvar construct to main-
previous section. You’ll notice our fourth example runs as tain thread-local scalars, including Delphi’s native string
expected, and Dll4 does not misbehave during clean-up. type.
It is generally not safe to use the threadvar construct to
Conclusion maintain thread-local objects. Instead, you should use
We’ve just studied thread-local variables and the DLL entry the Win32 API directly. Although it’s a bit more cum-
procedure, and we’ve seen that maintaining a truly thread- bersome, it’s still quite simple to use.
safe environment is generally easy, except that management If you are writing an application that must maintain
of objects can be a little tricky. thread-local scalars and objects, it’s probably better to

34 November 1998 Delphi Informant


Dynamic Delphi
use the Win32 API functions exclusively — if anything,
this promotes consistency.
Before making the decision to maintain thread-local
variables, one not-so-obvious question to ask yourself
is: Do you really need them? This article was written
with the assumption that thread-local variables are
sometimes desirable and possibly even necessary.
However, many functions, including GetFirstWord and
GetNextWord, can be implemented without global and
thread-local variables.

When developing DLLs for third-party applications, the


developer generally has no idea of the internal workings of
the calling application: the application could be multi-
threaded, or it could be single-threaded. This article has
explained how to write a thread-safe DLL, even if the devel-
oper doesn’t know how the calling application uses threads.
Along the way, we’ve discussed the Windows DllEntryPoint
function, thread-local storage, and appropriate clean-up of
“thread-locally” instantiated structures and objects. ∆

All demonstration programs referenced in this article are avail-


able on the Delphi Informant Works CD located in
INFORM\98\NOV\DI9811GD.

Gregory Deatz is a senior programmer/analyst at Hoagland, Longo, Moran, Dunst


& Doukas, a law firm in New Brunswick, NJ. He has been working with Delphi
and InterBase for approximately two years and has been developing under the
Windows API for approximately five years. His current focus is in legal billing and
case management applications. He is the author of FreeUDFLib, a free UDF library
for InterBase written entirely in Delphi and hosted at http://www.interbase.com.
He can be reached via e-mail at gdeatz@hlmdd.com, by phone at
(732) 545-4579, or by fax at (732) 545-4579.

35 November 1998 Delphi Informant


OP Tech
Win32 Development / Shareware

By Yorai Aminov

Is Delphi Running the Code?


Determining If a Program Was Started in the Delphi IDE

H ow can I tell if code is running under Delphi? It’s an important question to


shareware authors who want to make their programs available only within
the Delphi IDE. At least until they’re paid for! A common answer is to look for
one of Delphi’s standard windows, but that’s not a satisfactory method. The
fact that Delphi is running doesn’t necessarily mean code is being executed
under its control.

This article presents a method for detecting if IsDebuggerRunning function, which returns
code is running under the control of Delphi, True if a process is running in the context of
i.e. if it was run from the Delphi IDE. a debugger. Windows 95, however, provides
Unfortunately, a great deal of the data need- no documented way of determining this
ed to determine this can only be obtained by information. Similarly, the ToolHelp32 func-
undocumented functions. An additional tions provide a simple method for determin-
obstacle is that both the documented and ing the parent of a process, but are imple-
undocumented functions necessary for this mented only in Windows 95. According to
task are incompatible across Windows plat- the Microsoft Win32 SDK, these functions
forms. The method used in this article han- are also implemented in Windows NT 5.0.
dles Windows 95 and Windows NT. Under earlier versions of NT, however, this
information must be obtained through
This article assumes the reader is familiar undocumented methods.
with Win32 processes; a full explanation of
processes is beyond the scope of this article. Because of these limitations, four separate
If you are not familiar with them, I suggest algorithms must be provided: two for deter-
Advanced Windows by Jeffrey Richter mining the parent process (one for Windows
[Microsoft Press, 1995]. Another source is NT and one for Windows 95), and two to
the Win32 SDK. An online version of the check for the presence of a debugger.
SDK documentation is available at
http://www.microsoft.com/msdn. Determining the Parent Process
Under Windows 95 (and NT 5.0), the
The Necessary Information ToolHelp32 functions can be used to provide
To determine if it’s running under the con- information regarding processes. The
trol of Delphi, the code needs to know two ToolHelp32 functions, constants, and data
things: It needs to determine if it was started types are defined in the TlHelp32 import
by Delphi, and if it’s being debugged. If the unit, supplied with Delphi 3. Unfortunately,
answer to both of these questions is yes, the this unit implicitly links the functions to the
code is running under Delphi’s control. executable, preventing any code using the
unit from running on NT. To make the code
Although the questions are simple, obtaining portable, explicit linking using the
the answers to them can be complicated. For LoadLibrary and GetProcAddress functions
example, Windows NT supplies the must be employed.

36 November 1998 Delphi Informant


OP Tech
type processes, threads, modules, and heaps is constantly
PProcessEntry32 = ^TProcessEntry32; changing, a snapshot ensures the data won’t change while
TProcessEntry32 = record
dwSize: DWORD; being examined.
cntUsage: DWORD;
th32ProcessID: DWORD; Such a snapshot is required to determine the parent of the
th32DefaultHeapID: DWORD;
th32ModuleID: DWORD;
current process. The snapshot is retrieved by calling the
cntThreads: DWORD; CreateToolHelp32Snapshot function, specifying the
th32ParentProcessID: DWORD; TH32CS_SNAPPROCESS flag. The function returns a han-
pcPriClassBase: Longint;
dwFlags: DWORD;
dle to a snapshot, which must be released later by calling
szExeFile: array[0..MAX_PATH - 1] of Char; CloseHandle.
end;

Once the snapshot is obtained, the Process32First and


Figure 1: The TProcessEntry32 record.
Process32Next functions are used to “walk” the snapshot.
These functions return True for success, and False for fail-
ure. If the function fails, the GetLastError function can be
function GetParentProcessIDForWindows: Integer; used to retrieve error information. When GetLastError
var
Kernel32: THandle;
returns ERROR_NO_MORE_FILES, the end of the
CreateToolhelp32Snapshot: TCreateToolhelp32Snapshot; process list has been reached. The Process32First and the
Process32First: TProcess32First; Process32Next functions take a TProcessEntry32 record (see
Process32Next: TProcess32Next;
Snapshot: THandle;
Figure 1) as a parameter. The functions fill this record
Entry: TProcessEntry32; with information. The only fields of interest to us are
WalkResult: Boolean; th32ProcessID, which we compare with our own process
ID: Integer;
begin
ID (obtained by calling the GetCurrentProcessId function),
Result := 0; and th32ParentProcessID, the process ID of the parent
Kernel32 := LoadLibrary('KERNEL32.DLL'); process.
if Kernel32 <> 0 then begin
CreateToolhelp32Snapshot :=
GetProcAddress(Kernel32,'CreateToolhelp32Snapshot'); Figure 2 lists the GetParentProcessIDForWindows function,
Process32First := GetProcAddress(Kernel32, which returns the parent process ID under Windows 95 and
Process32First');
Process32Next := GetProcAddress(Kernel32,
NT 5.0 using ToolHelp32 functions. The function uses
'Process32Next'); LoadLibrary and GetProcAddress to access the ToolHelp32
if Assigned(CreateToolhelp32Snapshot) and functions from KERNEL32.DLL, and walks the process list
Assigned(Process32First) and
Assigned(Process32Next) then begin
to retrieve the parent process ID.
ID := GetCurrentProcessId;
Snapshot := Under versions of NT earlier than 5.0, the ToolHelp32
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if Snapshot <> -1 then begin
functions are not implemented, nor does any documented
Entry.dwSize := SizeOf(TProcessEntry32); way of obtaining the parent process ID exist. The solution
WalkResult := Process32First(Snapshot, Entry); is to use the undocumented NtQueryInformationProcess
while (GetLastError <> ERROR_NO_MORE_FILES) and
(Result = 0) do begin
function. The function is defined in NTDDK.H in the
if WalkResult then begin Microsoft Windows NT Device Driver Kit, along with the
if Entry.th32ProcessID = ID then data structures it uses. This function can return many types
Result := Entry.th32ParentProcessID;
end;
of information regarding a process, but for this article, only
Entry.dwSize := SizeOf(TProcessEntry32); the PROCESS_BASIC_INFORMATION class is neces-
WalkResult := Process32Next(Snapshot, Entry); sary. The basic process information includes the ID of the
end;
CloseHandle(Snapshot);
parent process. Figure 3 lists the TProcessBasicInformation
end; type, translated from the C header file.
end;
FreeLibrary(Kernel32);
end; The NtQueryInformationProcess function is implemented
end; in NTDLL.DLL, and its address can be retrieved by call-
ing GetProcAddress. The function takes as a parameter a
handle to a process (which we obtain by calling
Figure 2: The GetParentProcessIDForWindows function accesses
the ToolHelp32 functions from KERNEL32.DLL, and walks the
GetCurrentProcess), the type of information requested, a
process list to retrieve the parent process ID. pointer to a buffer to receive information, the size of the
buffer, and a pointer to a variable that receives the actual
size of the output data. GetParentProcessIDForNT simply
The ToolHelp32 functions work by creating a snapshot, loads NTDLL.DLL, retrieves the address of
then walking through it. A snapshot is a description of the NtQueryInformationProcess, and calls it to retrieve the par-
requested information at a single moment. Because the ent process ID, stored in TProcessBasicInformation’s
information handled by the ToolHelp32 functions — InheritedFromUniqueProcessID field.

37 November 1998 Delphi Informant


OP Tech
type
PProcessDatabase = ^TProcessDatabase;
type
TProcessDatabase = packed record
TProcessBasicInformation = packed record
DontCare1: array[0..7] of Integer;
ExitStatus: Integer;
Flags: Integer;
PebBaseAddress: Pointer;
DontCare2: array[0..11] of Integer;
AffinityMask: Integer;
DebugeeCB: Integer;
BasePriority: Integer;
DontCare3: array[0..22] of Integer;
UniqueProcessID: Integer;
DontCare4: Word;
InheritedFromUniqueProcessID: Integer;
end;
end;

Figure 5: The TProcessDatabase record.

Figure 3: TProcessBasicInformation. function IsDebuggerPresentForWindows: Boolean;


var
PDB: PProcessDatabase;
function GetParentProcessID: Integer;
TID: Integer;
var
OSVersionInfo: TOSVersionInfo; Obsfucator: Integer;
begin begin
OSVersionInfo.dwOSVersionInfoSize := Result := False;
SizeOf(TOSVersionInfo); Obsfucator := 0;
GetVersionEx(OSVersionInfo); TID := GetCurrentThreadID;
if (OSVersionInfo.dwPlatformId=VER_PLATFORM_WIN32_NT)and asm
(OSVersionInfo.dwMajorVersion < 5) then MOV EAX, FS:[18h]
Result := GetParentProcessIDForNT
SUB EAX, 10h
else
Result := GetParentProcessIDForWindows; XOR EAX, [TID]
end; MOV [Obsfucator], EAX
end;
if Obsfucator <> 0 then begin
Figure 4: GetParentProcessID checks the operating system ver- PDB := Pointer(GetCurrentProcessID xor Obsfucator);
sion and platform, and calls the appropriate function. Result := (PDB^.Flags and fDebugSingle) <> 0;
end;
end;
The GetParentProcessID function (see Figure 4) simply checks
Figure 6: IsDebuggerPresentForWindows tests for the presence
the operating system version and platform, and calls the appro- of a debugger.
priate function to do the work. If the code is running on
Windows 95 or NT 5.0 and higher, it uses the ToolHelp32 the purposes of this article, however, only two fields of the
version; otherwise, it uses NtQueryInformationProcess. PDB are of interest. Therefore, the TProcessDatabase (see
Once the parent process ID has been obtained, it can be Figure 5) record used by the code ignores all other fields. For
compared with Delphi’s process ID (explained later in this more information about the PDB, I strongly recommend
article). Even if the IDs match, this is insufficient proof that reading Pietrek’s book.
Delphi controls the code. It’s quite possible the code is part
of a program started by Delphi, for example, through the The Flags field contains a bit mask describing various prop-
Tools menu. It’s also necessary to make sure the process is erties of the process. The only bit that interests us is the
being debugged. fDebugSingle flag ($00000001), which is set when a process
is being debugged. The reason the DebugeeCB field is also
Determining the Presence of a Debugger defined in the record is that it appears to be a pointer to the
Again, Windows 95 and NT differ. NT provides the debugee’s context block when a process is being debugged.
IsDebuggerRunning function, which returns True if the cur- This means that it will be non-zero if the process is being
rent process is being debugged by a Ring 3 debugger, such as debugged, which provides an alternative method of deter-
Delphi’s integrated debugger. Neither NT nor Windows 95 mining this information.
provide a way, documented or not, of determining whether a
kernel-mode debugger, such as SoftICE, is running. There The tricky part is obtaining a pointer to the PDB. The solu-
are undocumented ways of determining whether a specific tion is to use the “Obsfucator” value (the term comes from
kernel-mode debugger is running, but the question itself is Microsoft binaries, where it was originally misspelled). The
irrelevant to this article. value returned from the GetCurrentProcessId function is actu-
ally a pointer to the PDB, xor’ed with the Obsfucator value.
Under Windows 95, there is no documented way of deter- The same algorithm is implemented in GetCurrentThreadId.
mining if a process is being debugged. Again, this informa- To complicate things, this value is calculated upon system
tion can only be obtained using undocumented techniques. startup. This means that to get to the PDB, the value of the
Obsfucator must be obtained. Fortunately, the rest is simple,
When a new process is created under Windows 95, the ker- because simply xor’ing the result of GetCurrentProcessId with
nel creates a Process Database (PDB) for it. A process data- the Obsfucator gives the pointer back to the PDB.
base is an internal object that contains information about a
process. In Windows 95 System Programming Secrets [IDG To calculate the Obsfucator value, the code uses the result of
Books, 1995], Matt Pietrek describes the PDB in detail. For GetCurrentThreadId. GetCurrentThreadId returns, much like

38 November 1998 Delphi Informant


OP Tech
procedure EnumWindowsProc(Window: THandle; Checking for Delphi’s Control
LParam: Integer); stdcall; Once the code has determined the presence of a debugger,
var
ClassName: string;
it should compare the parent process ID with Delphi’s
begin process ID. However, it’s possible for multiple instances of
SetLength(ClassName, 255); Delphi to be running, so all instances should be checked.
GetClassName(Window, PChar(ClassName), 255);
SetLength(ClassName, StrLen(PChar(ClassName)));
The easiest way to find an instance of Delphi is to look for
if ClassName = 'TAppBuilder' then its main window. Delphi’s main form’s window class is
TList(LParam).Add(Pointer(Window)); TAppBuilder (at least through version 3). The
end;
RunningUnderDelphi function and its helper routine,
function RunningUnderDelphi: Boolean; EnumWindowsProc (see Figure 7), enumerate through all
var top-level windows by calling EnumWindows, check for the
List: TList;
i: Integer;
TAppBuilder class name, and save the handles of matching
ID, ParentID: Integer; windows in a TList by storing integers instead of pointers.
begin Each window handle in the list is then checked by obtain-
Result := False;
ParentID := GetParentProcessID;
ing the process ID for the window through the
if (ParentID <> 0) and (IsDebuggerPresent) then begin GetWindowThreadProcessID function and comparing that
List := TList.Create; ID with the parent process ID. If a match is found, the
EnumWindows(@EnumWindowsProc, Integer(List));
for i := 0 to List.Count - 1 do begin
function returns True and the search is aborted.
GetWindowThreadProcessID(Integer(List[i]), @ID);
if ID = ParentID then begin Conclusion
Result := True;
Break;
This article presents a method of determining if code runs
end; under the control of Delphi. As with all code that uses undoc-
end; umented methods, there’s no guarantee it will work on future
List.Free;
end;
versions of Windows. On the other hand, the techniques out-
end; lined in this article can be used for many purposes. Although
Figure 7: The RunningUnderDelphi procedure and
the Win32 API can be quite powerful, there are many cases
EnumWindowsProc function. where a program needs to know more than Windows is ready
to tell. The use of undocumented functions and data blocks
GetCurrentProcessId, a pointer to the thread database (an object may be the only way to accomplish a necessary task. ∆
similar to the process database), xor’ed with Obsfucator.
Therefore, if we have a pointer to the current process thread The demonstration application referenced in this article is avail-
database, we can xor it with the thread ID to get the Obsfucator. able on the Delphi Informant Works CD located in
The thread database starts 16 ($10) bytes before the thread INFORM\98\NOV\DI9811YA.
information block (TIB), which is used a lot by Win32. Both
Windows NT and Windows 95 dedicate the FS register to
pointing at the TIB for the current thread. Based on this infor-
mation, the calculation of the address of the thread database is
Yorai has been programming for over 10 years — as a professional developer for
simply the linear address of the TIB, minus $10. The linear three years. He uses Delphi as his main programming tool, and writes mostly com-
address of the TIB is stored at offset $18 in the TIB, so the ponents, visual controls, Win32 API code, and medium- to large-scale client/server
address of the thread database is FS:[$18] - $10. database applications. His previous programming experience includes DOS develop-
ment in Turbo Pascal, C, and assembly, OS/2 1.x development in C, Windows devel-
The IsDebuggerPresentForWindows function (see Figure 6) opment in C, and Turbo Pascal for Windows. He has published several articles in
Delphi Developer (Pinnacle Publishing), dealing with implementation of advanced
uses the PDB’s Flags field to test for the presence of a debug- Delphi and Win32 techniques, and is a member of TeamB. Yorai can be reached via
ger. It uses assembly code to access the FS register and calcu- e-mail at yaminov@trendline.co.il.
late the Obsfucator, and uses the result of this calculation to
locate the PDB.

39 November 1998 Delphi Informant


New & Used

By Warren Rachele

Wanda the Wizard Wizard


A RAD Tool for Creating Wizards

W izards are a common feature of the modern Windows landscape. They


lead the user through a series of linked — sometimes complex — steps,
collecting data and allowing decisions to be made in an orderly, controlled
fashion. A wide range of tasks lend themselves to a wizard interface: help sys-
tems, product registration, configuration, and training tools are just a few of
the obvious choices. Yet creating an effective wizard takes an enormous invest-
ment of time and energy to develop. Wanda the Wizard Wizard is a software
tool that seeks to make the creation of these interface items and their inclusion
into your Delphi program efficient and easy.

Wanda the Wizard Wizard is a RAD tool Run button. This process engages the run-
designed solely to create wizards. Currently time engine, WANDA.DLL. The VCL com-
in version 1.5.2.2, the tool is produced by ponent used to include Wanda technology in
Ingeneering Inc. of Ann Arbor, MI. The a Delphi program is an interface to this DLL,
tightly focused environment and intelli- which becomes a part of the distribution file
gence provides an additional benefit for the set for the program.
developer: The process of building the wiz-
ards can be off-loaded to staff more attuned The installation of Wanda the Wizard
to interface issues, such as an interface Wizard runs automatically, providing the
designer or trainer. programmer with the option of where to
install the product and the menu group in
Using the Tools which it appears. A complete installation,
Upon starting the Wizard Designer, the including the interface objects for all sup-
developer is immersed in a RAD environ- ported products and the example files, con-
ment that is immediately familiar. A property sumes just under 4MB of disk space.
sheet takes up the left side of the IDE, with a Installation is a manual process, which adds a
component toolbar stretching across the top. new tab to the VCL toolbar and a single
The rest of the IDE window is devoted to component.
the wizard page (form) being developed. The
process of composing the wizard pages fol- So, why bother with a third-party tool to
lows proven methodologies. A component is build an object that could be created within
selected from the toolbar and dragged onto the Delphi environment? The answer is:
the page. Once positioned, the developer sets rapid, focused development. Wanda makes it
the properties for each component to achieve easy to transfer the flow of ideas into action
the desired effect. by incorporating wizard-specific properties
and methods into its components. Intelligent
Saving the wizard project results in a propri- ideas are everywhere in this tool, starting
etary .WZX file. Executing the wizard with the base window, referred to in wizard
through the IDE is a matter of clicking the nomenclature as a “page.”

40 November 1998 Delphi Informant


New & Used
Wanda has borrowed an idea from the page layout profes-
sion, allowing the designer to establish a master page. This
is a non-visible page that can become the basis for all
remaining pages created in the wizard. The designer can
establish the size, color, logo, button arrangement, and any
other common attribute one time on the master page. Each
new page that is created, identifying this object in the
MasterPage property, inherits all the properties and attribut-
es of the original design. Delphi programmers will immedi-
ately see the benefits of this object-oriented design. Any
changes that are required of the fixed components or
attributes need only be made once. The changes made to
the master page immediately cascade through the child
pages. Not only does this reduce the possibility of error, it
saves the programmer considerable time and effort, espe-
Figure 1: A wizard in development.
cially when dealing with active items, such as buttons and
their associated linkages.
The data contained in the list box must be provided as an
Pages other than the master page are also assigned a page external text source. In this case, the list was composed in a
type. The choice made (Normal, Start, End) determines the text editor and saved as a .TXT file. To bring text and bitmap
logical flow through the dialog boxes and whether certain resources into a Wanda wizard, they must be registered with
button types are enabled. For example, a page type of End the Resource Manager in the IDE. Once the items have been
requires that the users of the wizard have worked their way to registered, they become available to the workspace. In the
the last page of dialog before a Done button will become case of the list box, the Resource property has been set to a
enabled. Consider the amount of work required in a general- test list registered as buylist. Though the data or bitmap will
purpose development tool to build this kind of functionality appear right away in the wizard, modification of the underly-
into a series of forms. ing data is not automatically reflected within the wizard. The
object must be removed from the resource manager and the
The button components and their associated action codes modified version added back into the resource list. To com-
offer a limited, but focused, selection of methods. All but- plete the update, components calling the resource need their
ton actions, with the exception of Done and Cancel, are ori- properties reset.
ented toward the user’s navigation of the wizard pages.
Buttons can be provided to go to the first, last, previous, or Wanda provides a run-time emulator integrated into the
next pages, as well as specific pages within the stack. IDE that allows the designer/builder to test and debug the
Initially, the programmer might feel limited by the restric- wizard. Figure 4 shows the Surfside wizard in “execution
tions imposed by the selection of components, or the fact mode.” The IDE minimizes and is replaced on screen with
that no coding is allowed within the wizard. Once the the wizard form and the Run-Time Emulator (RTEM).
mind-set of the wizard system is acknowledged, however, The RTEM window is segmented into three boxes: Page
the programmer realizes that these are the only items and Stack, Results, and Query. The Page Stack dynamically
actions needed to implement the ideal wizard, and can focus demonstrates the logical flow from page to page, moving a
on the application. pointer that follows the user’s movement. The Query edit
box allows the designer to determine the contents or state
An example of a simple wizard in development is shown in of any component on the wizard. Each component is
Figure 1. The master page for this wizard shows the common assigned a numeric identifier that must be prefaced with
components that will appear on every child page. The button the character “c” to be used in the query. When queried,
actions have been set as methods of this page and do not the contents of the selected component are shown in the
need to be modified on the pages derived from it. The Results list box.
designer is now free to concentrate on content and logical
flow, rather than the mechanics of getting from page to page Integration with Delphi
and handling user actions that don’t go according to plan. Using the Wanda-created wizard in a Delphi program is as
Figures 2 and 3 show the two visible pages that make up the simple as dropping the Wanda component on a form and set-
registration wizard for Surfside Digital Design. Each of the ting the appropriate properties. When the user needs to run
data elements captured on either page is available for query the wizard, the Active property is set to True, and the Wanda
when the wizard has completed. Figure 3 shows the last page run-time engine activates and runs the wizard. The Surfside
of the wizard containing a list box from which the user will demonstration program shown in Figure 5 is designed to run
identify the source of the program. This component, as well the registration wizard and query the results for use in the
as the bitmap in the logo component, demonstrates an idio- Delphi program. The Register button’s Click method is used
syncrasy of the Wanda environment. to activate the wizard.

41 November 1998 Delphi Informant


New & Used

Figure 2: One of two visible pages that make up the registration Figure 4: The Surfside wizard in execution mode.
wizard for Surfside Digital Design.

Figure 3: The other visible page for Surfside’s registration wizard. Figure 5: The Surfside demonstration program in the Delphi IDE.

Listing Five (on page 44) demonstrates the behind-the-scenes before the QueryString method. This directs the query
work necessary to fully exploit the power of the Wanda wiz- method to the particular component by passing the integer
ard. Although the component offers the ability to run the value of the component ID to the function, setting up an
wizard through property settings, any truly useful work is internal pointer. The QueryString method is called with a
going to be accomplished through external function calls to PChar as the parameter, giving the function a place to put
the run-time engine, WANDA.DLL. the null-terminated string it will return from the wizard.

The first point to examine is the method called to run Immediately below this set of instructions is a second query
the wizard. Setting the Active property to True will activate to the Organization field of the wizard. In this case, the
the wizard, but the calling program won’t be able to query component ID is set through the use of the Wanda compo-
the wizard’s exit status. By calling the RunWizard method, nent’s Query function, something the documentation
the program is able to determine whether the user success- appears to warn against. Both approaches worked consis-
fully completed the wizard or canceled out of it. The tently. Figure 6 shows a view of the executing Surfside
RunWizard method also handles memory management demonstration after a return from the wizard. All the fields
in the event that a FreeWizard call was not successful in from the wizard form have been queried, and the data
removing the memory captured by the DLL, or not retrieved into the Delphi program.
used at all.
A disappointment was the handling of the list box query. The
The remaining code is devoted to querying the components query returns a numeric value representative of the list position
on the wizard form to obtain their contents. Contrary to of the item clicked by the user. Unable to query the contents of
the documentation supplied with the product, it appears the resource directly to capture the text of the item, the pro-
that the programmer has a choice of the methods used to grammer would be required to include the text resource in the
set up the query of a component. The statements following Delphi program to accomplish this feat, and having the text list
the { Query Name. } comment show a call to SetComponent in two work areas leads to the possibility of update anomalies.

42 November 1998 Delphi Informant


New & Used
mysteriously moved from their
original position. Thinking this
was a case of a lazy mouse hand,
the components were moved
back into position where they Wanda the Wizard Wizard has some
rough edges, particularly when it
remained. Then, while reviewing comes to its documentation.
the properties for a component, However, intelligent ideas are abun-
a button was seen to move as the dant and it provides rapid, focused
development of objects that could be
list of properties was clicked created within Delphi. Given the
from field to field. Moving up prevalence of wizards in Windows
the list in ascending order, the applications today, Wanda proves to
be a promising and practical addition
button moved up a notch as to a Delphi developer’s toolbox.
each property was clicked.
Ingeneering Inc.
1645 Morehead Drive
Also frustrating was the default Ann Arbor, MI 48103
Figure 6: The Surfside demonstration at run time after returning
activity of the Open dialog box.
from the Surfside wizard. There’s no way to set the default Phone: (734) 662-4646
Fax: (734) 662-4682
directory to the project directory, E-Mail: info@ingeninc.com
The Product forcing the user to start in the Web Site: http://www.ingeninc.com
Wanda the Wizard Wizard ships on a single diskette, or it can My Documents folder. Finally, Price: US$199.95 (direct from
Ingeneering Inc.)
be downloaded in demonstration form from Ingeneering Inc.’s there is no online Help file; the
Web site at http://www.ingeninc.com. It ships with the docu- Help | Topics menu option directs
mentation in an Adobe .PDF (Portable Document Format) file, the programmer back to the .PDF or .PRN file.
with a printed version of the manual available from the compa-
ny at an additional cost. Conclusion
Wanda the Wizard Wizard is a worthwhile tool for the pro-
The documentation is a low point of the product. The organi- grammer looking to add wizard functionality to a program.
zation of the document leads the user to a lot of dead ends As long as its limits are understood and programmer expecta-
without providing answers. An example is the presentation of tions appropriately tempered, the development of wizards to
the tutorials early in the manual. Following the description step be included in a Delphi program will not be a disappointing
by step, wizards are created that deliver many unexpected experience. In fact, working with the tool leads to new ideas
results, such as buttons never being activated. No explanation is for additional functionality and usefulness. The roughness of
provided for situations such as this, leading to a good deal of the product and the documentation should not dissuade
frustration for first-time users. The answer to this particular developers and interface designers from exploring the possi-
problem is buried as a casual reference to page types much fur- bility of adding this software to their toolbox. ∆
ther on in the documentation. The programmer would do well
to read the documentation thoroughly before attempting to use
the product. A secondary issue with the documentation is that
Warren Rachele is Chief Architect of The Hunter Group, an Evergreen, CO soft-
it is shipped incomplete, with numerous references for specific ware development company specializing in database-management software. The
product support labeled as “coming.” company has served its customers since 1987. Warren also teaches program-
ming, hardware architecture, and database management at the college level. He
The IDE shows a few rough edges as well. For example, com- can be reached by e-mail at wrachele@earthlink.net.
ponents such as buttons and edit fields were found to have

43 November 1998 Delphi Informant


New & Used

Begin Listing Five — Surfside Demo if not lastrun then begin


{ User clicked on Cancel or pressed Escape. }
unit surfdemo;
lbRegistered.Caption := 'User Canceled: UNREGIS-
TERED!';
interface
lbRegistered.Visible := True;
end
uses
else begin
Windows, Messages, SysUtils, Classes, Graphics, Controls,
{ User completed the wizard and clicked Done. }
Forms, Dialogs, StdCtrls, Buttons, WandaRun;
lbMessage1.Caption := 'Thank you for registering.';
lbRegistered.Caption := 'Registered';
type
lbRegistered.Visible := True;
TForm1 = class(TForm)
WandaRun1: TWandaRun;
{ Query Name. }
BitBtn1: TBitBtn;
if not SetComponent(19) then
BitBtn2: TBitBtn;
raise Exception.Create('Query Error')
lbMessage1: TLabel;
else begin
lbRegistered: TLabel;
QueryString(s);
lbName: TLabel;
lbName.Caption := StrPas(s);
lbOrg: TLabel;
lbName.Visible := True;
lbAddress: TLabel;
end;
lbCity: TLabel;
lbState: TLabel;
{ Query Organization. }
lbZip: TLabel;
WandaRun1.Query := 'c20';
lbPurchased: TLabel;
QueryString(s);
procedure BitBtn2Click(Sender: TObject);
lbOrg.Caption := StrPas(s);
private
lbOrg.Visible := True;
{ Private declarations. }
{ Query Address. }
public
WandaRun1.Query := 'c21';
{ Public declarations. }
QueryString(s);
end;
lbAddress.Caption := StrPas(s);
lbAddress.Visible := True;
var
{ Query City. }
Form1: TForm1;
WandaRun1.Query := 'c22';
QueryString(s);
implementation
lbCity.Caption := StrPas(s);
lbCity.Visible := True;
{$R *.DFM}
{ -- Wanda run-time DLL imports -- } { Query State. }
procedure QueryString(s: PChar); WandaRun1.Query := 'c23';
stdcall; external 'Wanda.DLL'; QueryString(s);
function SetComponent(id: Integer): LongBool; lbState.Caption := StrPas(s);
stdcall; external 'Wanda.DLL'; lbState.Visible := True;
function RunWizard(fn: PChar): LongBool; { Query Zip. }
stdcall; external 'Wanda.DLL’; WandaRun1.Query := 'c24';
function FreeWizard: LongBool; QuerySTring(s);
stdcall; external 'Wanda.DLL'; lbZip.Caption := StrPas(s);
function QueryLong(id: Integer): Integer; lbZip.Visible := True;
stdcall; external 'Wanda.DLL'; { Query the Listbox. }
WandaRun1.Query := 'c28';
procedure TForm1.BitBtn2Click(Sender: TObject); lbPurchased.Caption := FloatToStr(QueryLong(28));
var lbPurchased.Visible := True;
s : array[0..75] of Char;
lastrun : Boolean; FreeWizard;
begin end;
lbMessage1.Caption := end;
'Starting the Registration Wizard...';
end.
{ Activate the Wanda run time. }
lastrun := RunWizard(PChar(WandaRun1.WizardName));
{ Query the exit return value. }
End Listing Five

44 November 1998 Delphi Informant


From the Trenches
Directions / Commentary

The Joy of Demos

L ive technology previews — or “demos” as they’re commonly known — are typically nothing more than a
façade, cobbled together to chug along on the inside, while giving the appearance of grandeur on the
outside. Demos are loathed by developers because of the extra work required to simply meet an “artificial”
milestone. To make matters worse, a demo is never accounted for in the schedule, forcing the developer to
make up the time somehow.

Of course, demos have their merits. Although the demonstration was rela- Like every seasoned developer, your ini-
Management can get a product in tively simple, it illustrated Inprise’s tial reaction is: “Fine. How does this
front of a user and start to solicit feed- commitment to create tools for devel- help me today?” With Inprise giving
back early in the development process. opers that must provide solutions for you a sneak peek at tomorrow’s vision,
This lowers development costs and multiple platforms. Granted, this fea- you have a unique opportunity to pre-
keeps the product’s features closer to ture won’t be on your desktop tomor- pare for that future. You’ll reap the
user expectations. It can also provide row; there’s plenty of work left, but the rewards as you find each piece of soft-
positive reinforcement that a course is demonstration was impressive. All the ware you write becoming available to a
worth pursuing. more so considering the venue. How larger segment of the computer popula-
many times have you demonstrated a tion.
Which takes us to the recent pre-release compiler to 3,000 people,
Borland/Inprise conference, held the including the international media? It Who knows where the Delphi compil-
third week of August, in Denver must have been a serious temptation for er will go next? More and more people
Colorado. In front of nearly 3,000 peo- Dilbert’s Dark Angel of Demos. are asking for things like Delphi/Linux
ple, Chuck Jazdzewski, Delphi’s princi- and Delphi/Windows CE. While this
pal architect, demonstrated a pre-release The demonstration drove home some technology preview is a far cry from
Delphi compiler that produced Java additional points as well: implementing these products, it shows
byte code. The result was a thundering Inprise is at the forefront of interop- that Inprise could provide these tools if
round of well-deserved applause. erability, and is second-to-none in the market demanded.
technical ability.
The demonstration created a TDatabase You can leverage your Delphi skills Advances like MIDAS client for Java
component, and attached it to a JDBC and experience to throw your pail further strengthen Inprise’s commit-
datasource through a new TDatabase and shovel into the Java sandbox. ment to get “Any Data. Any Time.
property. After flushing the sample out to The Delphi team has shown that a Anywhere.” It looks like it’s not just a
loop through and display records, a significant shift in the marketplace slogan at Inprise — it’s a way of life. ∆
replacement command-line compiler, doesn’t mean the end of Delphi. In
dcc32j, was invoked to produce Java byte fact, as Delphi embraces these tech- — Dan Miser
code. The intended use of this technology nologies and innovations, it gives
is to allow non-visual applications created developers a way to play in that Dan Miser is a Design Architect for
in Delphi to run on any platform that has arena quickly and easily. I’ll go out Stratagem, a consulting company in
a Java Virtual Machine (JVM). It’s an on a limb and state that Java won’t Milwaukee. He has been a Borland
interesting idea: Write your application be the last innovation in our indus- Certified Client/Server Developer since
server for a multi-tier solution in Delphi, try. It’s nice to know that Inprise 1996, and is a frequent contributor to
and you can run the application server on has the ability to react when the Delphi Informant. You can contact him
another platform, e.g. a Sun box. industry changes. at http://www.execpc.com/~dmiser.

45 November 1998 Delphi Informant


File | New
Directions / Commentary

The Conference: Borland/Inprise 1998

A s usual, this year’s Borland/Inprise Conference was outstanding. I had the opportunity to meet and
exchange ideas with many readers, get the latest news on Delphi add-on tools, get a preview of
Delphi’s future, and attend some great sessions.
With this company’s new name and its From Borland to Inprise. Looking ers are an independent militia, fighting
goal to find a niche with leading corpo- ahead, what can we expect from under the same banner. While few at
rations, an important question remains: Inprise and Delphi? While I can’t this conference would be likely to praise
“What implications does Inprise’s new promise a definitive answer, I can Microsoft as a tool maker, a number of
vision hold for the average developer?” share some of my perceptions. These developers and writers made this obser-
At the same time, with Inprise’s procla- are based in part on Inprise’s skillful vation: “Microsoft certainly knows how
mations on the importance of its third- presentation of its vision, juxtaposed to keep its third-party tool makers in the
party tool makers, these developers are with concerns of developers. loop.” Does Inprise do enough of this?
wondering if they can expect an even Many months before Borland decided to Borland/Inprise: You have produced the
closer relationship in the future. I’ll change its name to Inprise, its new CEO, fastest and most efficient compilers. You
explore these and other issues, but first Del Yocum, charted a very new direction. have developed and made available an
I’ll share some of my personal highlights The goal was twofold: find a new, unique open tools environment that allows for
from the conference. niche in the industry, and develop new infinite expansion. You have presented a
Personal highlights. At the center of relationships with leading corporations. workable plan to integrate all these tools.
each conference are the excellent tutori- The acquisition of Visigenic and the name But do you realize you don’t have to pro-
als, presentations, showcases, and birds- change were just the latest steps in the duce every conceivable extension, be it a
of-a-feather sessions. This year, I attended mission outlined by Chairman Yocum at component or an IDE enhancement?
— for the first time — one of the pre- the 1997 Borland Conference in Consider conducting frank and detailed
conference tutorials, Cary Jensen’s won- Nashville. Furthermore, Inprise’s interest discussions with your partners — your
derful overview of JBuilder, where he in developing and exploiting connections real partners, the third-party tool makers.
gave a tour of its IDE and explained between its tools on the one hand, and its Let them know what you plan to do and
some of the important differences active courting of industry clients, is also find out where they can make a contri-
between JBuilder and Delphi. I also not new. What seems new in 1998 is a bution. And never forget the entry-level
attended Bob Swart’s perennial discussion cohesive game plan to actually make it so. developer, without whom Delphi would
of optimization in which he presented The vision was presented in an awe- wither on the vine. King Yocum, with
some nice profiling tips and techniques, some theatrical keynote — à la Camelot the hope that you will see some wisdom
along with new aspects of Delphi 4. — where the wise King Yocum, con- in your humble scribe’s suggestions, and
Mark Miller’s session on Class Design cerned for the welfare of the subjects of that your benevolence will continue to
was spectacular — insightful, fast the Land of Bor, leads his brave knights shine on all of your subjects, I leave you
paced, humorous, and impressive. Mark on a quest for the Orb of Knowledge. with this: “Ask not what your developers
is one of the few people with the audac- The Orb’s discovery leads to the estab- can do for you; ask what you can do for
ity to perform coding improvisations in lishment of a new kingdom: Inprise! your developers.” ∆
front of a large audience. Other sessions filled in the details.
I attended Ray Konopka’s impressive As impressive as all of this was, a nag- — Alan C. Moore, Ph.D.
showcase on Raize Software’s new debug- ging concern continues to lurk in the
ging tool, CodeSite. The dozen or so background. Most of us would agree Alan Moore is a Professor of Music at
developers who showed up at 7 A.M. that Borland has produced the best Kentucky State University, specializing in
(yes, A.M.) for the Project Jedi Birds-of-a- Windows development tools on this music composition and music theory. He
Feather session, hosted by Robert Love, planet. But, as it grows, will this new has been developing education-related
proved that this project is alive and well. incarnation of Borland called Inprise applications with the Borland languages
I met one of my heroes, Tom Swan, and remember the importance of the aver- for more than 10 years. He has published
other writers, including John Ayers. As age developers, the soldiers in the a number of articles in various technical
usual, I spent a lot of time in the exhibi- trenches? Without them, the battle will journals. Using Delphi, he specializes in
tion area, learning the latest developments surely be lost. Inprise must continue its writing custom components and imple-
from TurboPower, Eagle Software, Raize commitment to make available entry- menting multimedia capabilities in appli-
Software, Nevrona Designs, Skyline Tools, level versions of these tools so the ranks cations, particularly sound and music.
and some new tool makers I am sure we’ll of this army will continue to grow. You can reach Alan on the Internet at
be hearing from in the future. At the same time, third-party tool mak- acmdoc@aol.com.
46 November 1998 Delphi Informant

You might also like