On The Cover: November 1998, Volume 4, Number 11
On The Cover: November 1998, Volume 4, Number 11
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
Pythoness Software
Price: US$69 (includes source code).
Phone: (208) 359-1540
Web Site: http://www.pythoness.com
By Rod Stephens
Picture Perfect
Shrinking, Enlarging, and Rotating Images
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.
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.
(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)
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.
x’ = x * Cos(theta) + y * Sin(theta)
y’ = -x * Sin(theta) + y * Cos(theta)
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.
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
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;
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,
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);
By G. Bradley MacDonald
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.
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
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. ∆
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.
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
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
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.
By Peter Dove
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.
CameraPosition: TPoint3D;
CameraRotation: TPoint3D;
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;
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-
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
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;
NumberPoints := PolyWorld[X].NumberPoints;
PolyColor := PolyWorld[X].PolyColor;
DibColor := PolyWorld[X].DibColor;
end;
end;
end;
end;
End Listing Four
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.)
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)
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.
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
By Yorai Aminov
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.
By Warren Rachele
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.”
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.
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.
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