Delphi Informant Magazine (1995-2001)
Delphi Informant Magazine (1995-2001)
ON THE COVER
7 A New Spin on Delphi — Peter Dove and Don Peer
Misters Dove and Peer begin a series on graphics programming by
developing the TGMP component. The series will follow it from inception,
through development, to a fully-functional 3D rendering component.
FEATURES
14 Informant Spotlight — Danny Thorpe
A member of the Delphi R&D team, Mr Thorpe begins a two-part series
that explores virtual methods and polymorphism.
REVIEWS
45 InfoPower 2.0 — Product Review by Bill Todd
49 SysTools for Delphi — Product Review by Alan C. Moore, Ph.D.
After Anders
No — you didn’t miss anything; there just wasn’t a December issue of Delphi Informant. And there
was no gap in your monthly Delphi coverage; it was strictly a logistical move to help DI get wider dis-
tribution and appear sooner each month on newsstands. So relax; with any luck, we’ll get this maga-
zine thing down soon.
On to the main topic: As you’ve proba- of you worried about Delphi’s future, I As you know by now from its debut
bly heard by now (this is written on want to assure you that the product is at Comdex, Borland has given the
October 21st, 1996), Anders Hejlsberg in the hands of an incredibly compe- “Delphi treatment” to C++. This
has left for Microsoft. Hejlsberg has tent team of people for whom I harbor one’s been kept under tight wraps and
been with Borland since its inception. the deepest respect. Back in the old doesn’t have a shipping name as of
He created the now-legendary Turbo Turbo Pascal days it was possible for this writing. It masquerades under
Pascal product that revolutionized the one person to write and maintain an various names such as Ebony and
desktop computer industry with the first entire product. This is no longer the Pronto, but what’s important is what
integrated development environment. case. Delphi was built by a team, and I the product represents to the industry
have full confidence in the team’s abili- and to Borland.
News of Anders’ departure hit me like a ty to develop and deliver new versions
thunderbolt. On the several occasions of Delphi. In fact, the Delphi team at Delphi boosted academia-bound
I’ve met Anders or heard him speak, I this point is almost twice the size it Pascal into the stratosphere. Given
was invariably struck by his affability was when we shipped 1.0 in early ’95. the preponderance of C++ in the pro-
and — well — brilliance. It was com- And Delphi97 is going to be a great gramming world — as Urlocker says
forting that my all-time-favorite devel- product [with] multi-tier database “some folks think more naturally in
opment environment was in his skillful access and COM/ActiveX support.” C++” — Ebony could make Delphi
hands. What now would happen to the look like a moderate success. Things
Delphi R&D team? Would others fol- Urlocker is also encouraging: “The look bad for Borland right now, but I
low Anders to Microsoft? Would they architectural work that Anders covers is can remember a darker period. In the
just scatter to the four winds? complete for Delphi97 and we’re in winter of 1994-95, Borland was rely-
beta. Anders’ departure won’t affect the ing on the flagging sales of Paradox
From all accounts, Hejlsberg had per- ship date or features going forward. and dBASE, and had pinned all hopes
sonal reasons for moving on; he’s not so Chuck Jazdzewski will move up from on a Pascal-based product no one had
much “leaving Borland” as beginning a co-architect to chief architect. Chuck’s heard of.
new endeavor. According to Zack been here longer than I have and
Urlocker, Director of Product worked closely with Anders for eight Delphi Informant wishes you well
Management for Borland and a member years. In fact, he designed the VCL and Anders; you’ve given us an incredible
of the Delphi R&D team: “We’re cer- the UI builder that’s such a powerful tool. Gotta run though — I have this
tainly sad to see Anders leave, but it was aspect of Delphi. He’s also played an Delphi project I’m working on ...
a personal decision on his front. After important role on Latté and our
being here 13 years — since college — upcoming C++ product. So again, even
he’s decided to try something different. though we’ll miss Anders, I think the
The rest of the team is here to stay.” Delphi team is in very good shape to
ship a very impressive release. There is a Jerry Coffey, Editor-in-Chief
The most important thing to readers whole crew of folks working on
of this magazine, however, is the health Delphi97 many of whom have been Internet: [email protected]
of Delphi without Hejlsberg. involved since the very beginning of the CompuServe: 70304,3633
Fortunately, the view of the product project and have a strong vision for Fax: (916) 686-8497
being in the hands of one man is where we are taking it in this next Snail: 10519 E. Stockton Blvd.,
naive. Again, from Anders: “For those release and beyond.” Ste. 100, Elk Grove, CA 95624
This article begins a series about Delphi dent bitmaps, sprites, palettes, optimization,
graphics programming. In this series, we’ll pointers, memory management, inline assem-
develop a component, TGMP, following it bler, and reading polygon data files.
from inception, through development, to a
fully-functional 3D rendering component. If you are a C or C++ programmer, you’ll find
Our journey will take us through some basic that writing this kind of component illus-
topics such as properties, events, and proper- trates how to get Delphi to do all those things
ty editors. Eventually we’ll visit more exotic you may have taken for granted. Regardless,
and advanced topics, such as device-indepen- this discussion will bring you closer to discov-
ering what makes Delphi such a great prod-
unit Unit1; uct for games and graphics development.
interface
To summarize what we’ll create throughout
uses this series, a brief description of the TGMP
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms, Dialogs;
rendering engine is in order. TGMP will enable
you to create 3D worlds based on polygonal
type data, allowing users to move through and
TGMP = class(TComponent)
private
observe the world from all positions and
{ Private declarations } angles. (The game Doom is an example of a
protected rendering engine.) The TGMP engine will
{ Protected declarations }
public
also include shading based on light source
{ Public declarations } positions, clipping, collision detection, texture
published mapping, and Gourard shading.
{ Published declarations }
end;
Now let’s set our minds into 3D mode and
procedure Register; dive into the project.
implementation
Let the Games Begin
procedure Register;
begin
Begin by selecting File | Close All from
RegisterComponents('Graphics', [TGMP]); Delphi’s main menu. This will ensure that all
end; items currently open are closed. Next, select
Component | New. In the Component
end.
Expert, enter TGMP for the Class Name,
Figure 1: The template unit created by Delphi. TComponent for the Ancestor type, and
begin
{ We must call the inherited Create of the TComponent
from which we derived this class }
inherited Create(AOwner);
{ Set up viewport }
After you have entered these values, select OK, and Delphi HalfScreenHeight := ViewHeight div 2;
will create a template unit (see Figure 1). This is the template HalfScreenWidth := ViewWidth div 2;
unit from which TGMP will evolve.
{ Get the handle of the window }
FWindowHandle := TForm(AOwner).Handle;
Let’s start with something simple. We’ll create TGMP to be a
component capable of generating a wireframe cube and pyra- { Set the viewing distance }
ViewingDistance := 200;
mid. Figure 2 shows how the cube will appear rendered in wire-
frame. The background color is black and is therefore the color { Set the Z distance }
of the canvas on which we are drawing. (Black is the usual Z_Distance := -50;
end;
choice for background color, although you can select any color.)
The main routines that must be developed for our wireframe Figure 3: Overriding the Create constructor.
rendering component are line-drawing and screen-clearing
routines, 3D to 2D projection routines, and rotational calcu- FBackBuffer: TBitmap;
lations. The Graphic Design Interface (GDI) functions FFrontBuffer: TCanvas;
encapsulated within TCanvas make line-drawing and screen- ViewWidth, ViewHeight: Integer;
FWindowHandle: THandle;
clearing routines easy to write. (3D to 2D projection routines HalfScreenWidth, HalfScreenHeight,
and rotational calculations are covered later in this article.) ViewingDistance: Integer;
We’ll begin by adding some data members to the component.
At this point we need: By placing this code in the private section, we can restrict
1) something to draw on and something to hold the back- access to data members through well-defined methods.
ground color; (Think of the private section as the innards of a microwave.
2) variables to hold our viewing distance and variables for You wouldn’t go fiddling with those innards unless you were
the screen height and width; and a qualified microwave engineer. Access to the microwave is
3) a means to keep the handle of the window that receives through a simple pushbutton user interface, equating to the
the TGMP component. public and published methods.)
Instead of using a device context, we’ll use TBitmap as the Next we need to initialize the declared data members. To do
backbuffer, because it will be compatible with the current this, we’ll override the Create constructor as shown in Figure 3.
color depth and it encapsulates the idea of a device context. The constructor is called to create the object. The Create
This means we can set the height and the width of the method contains the code that allocates the memory and gov-
bitmap, and leave TBitmap to deal with any necessary erns the look of the component. The override keyword indi-
resource allocation. TBitmap also has a Canvas property, so cates that we want to add to the inherited Create constructor.
copying between the bitmap canvas and form canvas will be Note that Delphi allows you to cast AOwner as another object
easy. This backbuffer also allows us to draw on a hidden sur- (TForm), an example of Delphi’s polymorphism.
face, which is then copied to the screen all at once, creating a
smoother animation effect. To do this, place the following Note also that the parameter AOwner is in the Create construc-
code in the private section of the TGMP component: tor. AOwner is the owner of the component, the object respon-
also uses the keyword default with a value of minus 50. TObject3D = record
This is a bit of a misnomer — it doesn’t really establish a LineStore : array [0..100] of TLine3D;
NumberLine : Integer;
default value for the property, but rather determines if the Color : Tcolor;
value is stored in the form file. If the default value is the end;
For now, however, the object is defined and rotated about You’ll also notice we have two constants: HalfScreenHeight
its local coordinate system. Be aware that more than one and HalfScreenWidth. Because we want objects to appear at
coordinate system exists: the center of the screen, we need to add half the screen height
Local Coordinate System refers to the coordinates of the to the Y value and half the screen width to the X value.
objects’ vertices themselves.
World Coordinate System refers to the position of the Finally, we’ll create a variable of type TObject3D, fill it with
objects’ vertices in a virtual world. 3D data, assign it a color, and we’re ready to go. Listing One,
Camera Coordinate System is the final position of the beginning on page 11, shows the code for the TGMP unit,
objects’ vertices after being transformed through the with some added data members. These members are filled in
camera matrix. This transformation positions all the the Create constructor to determine the size of the window to
objects as they would be if they were seen through a render to. After this code has been added to the initial unit
camera at a given position and angle of rotation. template, save the unit as GMP.PAS.
These coordinate systems will be covered in greater detail in You are now ready to install the component into Delphi’s
future articles. component library. To do this, select Component | Install to
display the Install Components dialog box. Select the
Here are the formulas for rotation of an object through the X, GMP.PAS file, add it to the component list, and select OK to
Y, and Z axis: recompile. Delphi will return the Component Palette with a
new Graphics page containing the TGMP component.
Rotation around Z:
Our First Application
NewX := X * Cos(Angle) - y * Sin(Angle) Now that you have installed the component, we can put it to
NewY := X * Sin(Angle) + y * Cos(Angle)
work and create our wireframe application.
Rotation around X: In our first application, we’ve created two arrays that hold the
NewY := Y * Cos(Angle) - Z * Sin(Angle)
data for the cube and pyramid objects. This is to set the founda-
NewZ := Y * Sin(Angle) + Z * Cos(Angle) tion for reading polygon data files to create the 3D objects. The
arrays are declared in the type section of the application code:
Rotation around Y:
TPyramidArray = array [0..47] of Integer;
TCubeArray = array [0..71] of Integer;
NewZ := Z * Cos(Angle) - X * Sin(Angle)
NewX := X * Cos(Angle) + Z * Sin(Angle)
In the public section of the source code for the wireframe
Next we need to know how to convert a 3D line onto a 2D application, notice the two variables of type TObject3D and a
screen. This is a simple problem to solve. Simply divide X pointer to TObject3D, CurrentObject. We are using
and Y by the Z value. However, doing this leads to a CurrentObject to keep track of the object the user selected.
“zoomed” perspective, so we need to add the idea of When the user selects a new object, we assign the current
viewing distance. You get the extreme zoomed effect object pointer to the object selected.
because the algorithm assumes your nose is pressed against
the screen. You’ll notice in the Timer1.Timer procedure that we use the
pointer in the following format: CurrentObject^. By plac-
Try holding an object close to your face and you’ll see the ing the carat after the pointer CurrentObject, we are referring
zooming effect. By increasing the viewing distance, you to the object rather than its address. This enables us to have
reduce the zoom. a generic place holder. Otherwise, we would need to test
which object is selected, and have two sets of the code you
The following code converts from 3D to 2D with respect to see in the Timer1.Timer procedure (one set of code for each
viewing distance: object we create). Typically, you would have to use a case
Peter Dove is a Technical Associate with Link Associates Limited and a partner in
Graphical Magick Productions. He can be reached via the Internet at
[email protected].
Don Peer is a Technical Associate for Greenway Group Holdings Inc. (GGHI) and a
partner in Graphical Magick Productions. He can be reached via the Internet at
[email protected].
interface
uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms, Dialogs;
Figure 4: The type of graphic your TGMP rendering engine will
type
be capable of generating by the fourth article of this series.
TPoint3D = record
x,y,z : single;
end;
statement to accomplish this, but as you can see, pointers
can be an eloquent solution to long-winded programming. TLine3D = record
StartPoint, EndPoint : TPoint3D;
end;
Listing Two on page 13 shows the first program that demon-
strates the use of TGMP. The application loads a TObject3D TObject3D = record
variable and animates the object in wireframe mode. LineStore : array [0..100] of TLine3D;
NumberLine : Integer;
Color : Tcolor;
Conclusion end;
Our initial development of the TGMP rendering component
TPObject3D = ^TObject3D;
provides a solid foundation on which to add additional 3D
math and rendering procedures. To give you an indication of TGMP = class(TComponent)
where we’re headed with this series, Figure 4 shows the type private
{ Private declarations }
of graphic your TGMP rendering engine will be capable of FBackBuffer : TBitmap;
generating later in this series. FFrontBuffer : TCanvas;
FColor : TColor;
ViewWidth, ViewHeight : Integer;
We’ll also be covering the issue of code optimization in some FWindowHandle : THandle;
depth later in this series. In fact, some of those optimizations HalfScreenWidth, HalfScreenHeight,
will include code that has been presented this month. It ViewingDistance : Integer;
FDistance : Integer;
might be a good mental exercise to look over the example procedure DrawLine3D(x1,y1,z1, x2,y2,z2 : Single);
program and determine where you would optimize this code. procedure DrawLine2D(x1, y1, x2,y2 : Integer);
procedure SetDistance(Distance : Integer);
protected
Although our first application is basic, the component has been { Protected declarations }
initialized in such a manner that future articles can build upon public
it rather than re-design it from the ground up with each new { Public declarations }
constructor Create(AOwner : TComponent) ; override;
rendering process. This will become apparent in our next article destructor Destroy; override;
as we add solid matter to the wireframe objects created here. ∆ procedure ClearBackPage;
procedure RenderNow(var Object3D : TObject3D);
procedure FlipBackPage;
procedure Rotate(x,y,z, angle : Single;
References: var Object3D : TObject3D);
LaMothe, A., Black Art of 3D Game Programming procedure ChangeObjectColor(var Object3D : TObject3D;
Color : TColor);
[Waite Group Press, 1995]. published
Lampton, Christopher, Flights of Fantasy { Published declarations }
[Waite Group Press, 1993]. property BackColor : TColor read FColor write FColor;
property ZDistance : Integer read FDistance write
Lyons, Eric R., Black Art of Windows Game Programming SetDistance default -50;
[Waite Group Press, 1995]. end;
Screenx2 := HalfScreenWidth +
Round(X2*ViewingDistance/(Z2+(ZDistance)));
Screeny2 := Round(HalfScreenHeight-Y2 * procedure TGMP.FlipBackPage;
ViewingDistance/(Z2+(ZDistance))); var
DrawLine2D(Screenx1, Screeny1, Screenx2, Screeny2); ARect : TRect;
end; begin
ARect := Rect(0,0,ViewWidth,ViewHeight);
procedure TGMP.Rotate(x,y,z, angle : Single; var FFrontBuffer.CopyRect(ARect, FBackBuffer.Canvas, ARect);
Object3D : TObject3D); end;
var
P : Integer; constructor TGMP.Create(AOwner : TComponent);
NewX, NewY, NewZ : Single; begin
begin { We must call the inherited create of the Tcomponent
for P := 0 to Object3D.NumberLine - 1 do from which we derived this class }
with Object3D.LineStore[P] do begin inherited Create(AOwner);
if Z <> 0 then
begin { Create the bitmap canvas }
NewX := StartPoint.X * Cos(Angle) — FBackBuffer := TBitmap.Create;
StartPoint.y * Sin(Angle);
NewY := StartPoint.X * Sin(Angle) + { Get a pointer to the Form's canvas }
StartPoint.y * Cos(Angle); FFrontBuffer := TForm(AOwner).Canvas;
StartPoint.X := NewX;
StartPoint.y := NewY; { Get the height and width of the window }
NewX := EndPoint.X * Cos(Angle) — ViewHeight := TForm(AOwner).Height;
EndPoint.y * Sin(Angle); ViewWidth := TForm(AOwner).Width;
NewY := EndPoint.X * Sin(Angle) +
EndPoint.y * Cos(Angle); { Set the bitmaps height }
EndPoint.X := NewX; FBackBuffer.Height := ViewHeight;
EndPoint.y := NewY; FBackBuffer.Width := ViewWidth;
end;
if X <> 0 then { Set up viewport }
begin HalfScreenHeight := ViewHeight div 2;
NewY := StartPoint.Y * Cos(Angle) — HalfScreenWidth := ViewWidth div 2;
StartPoint.Z * Sin(Angle);
NewZ := StartPoint.Y * Sin(Angle) + { Get the handle of the window }
StartPoint.Z * Cos(Angle); FWindowHandle := TForm(AOwner).Handle;
StartPoint.y := Newy;
StartPoint.z := Newz; { Set the viewing distance }
NewY := EndPoint.Y * Cos(Angle) — ViewingDistance := 200;
By Danny Thorpe
Virtual Methods,
Inside Out
Virtual Methods and Polymorphism: Part I
Polymorphism is the key to leveraging your methods (i.e. methods declared with virtual,
programming investments to enable a relatively dynamic, or override), and “virtual”
small amount of code to drive a wide variety of denotes the specific term that refers only to
behaviors, without requiring carnal knowledge methods declared with the virtual directive.
of the implementation details of those behav- For example, most polymorphism concepts
iors. However, before you can extend existing and issues apply to all virtual methods, but
Delphi components, or design new, extensible there are a few noteworthy items that apply
component classes, you must have a firm only to virtual methods.
understanding of how polymorphism works
and the opportunities it provides. Review: Syntax of Virtual Methods
Here’s a review of the two kinds of virtual
True to its name, polymorphism allows methods and four language directives used to
objects to have “many forms” in Delphi, and declare them:
a component writer typically uses a mix of all Virtual methods come in two flavors: vir-
these forms to implement a new component. tual and dynamic. The only difference
In this article, we’ll closely review the imple- between them is their internal implemen-
mentation and use of one of Delphi’s poly- tations; that is, they use different tech-
morphism providers, the virtual method, and niques to achieve the same results.
some of its more peculiar sand traps and Calls to virtual methods are dispatched
exotic applications, e.g. its part in making more quickly than calls to dynamic
.EXEs smaller. (Dynamic methods, message methods.
methods, and class reference types are Seldom-overridden virtual methods
Delphi’s other polymorphism providers, but require much more storage space for their
are outside the scope of this article.) compiler-generated tables than dynamic
methods.
This article assumes you are familiar with The keywords, virtual and dynamic,
Delphi class declaration syntax and general always introduce a new method name
OOP principles. If you’re a bit rusty with into a class’ name space.
these concepts, you should first refer to the The override directive redefines the
Delphi Language Reference. Also note that in implementation of an existing virtual
this article, “virtual” denotes the general method (virtual or dynamic) that a class
term that applies to all forms of virtual inherits from an ancestor.
P.NotVirtual(1) To be able to make one call that could cover those multiple
class types, the method must be defined in a class from which
would not be allowed, because TOfficeGadget.NotVirtual all the multiple class types descend — in an ancestral class such
requires three parameters. as TBaseGadget. The ancestral class, then, is the least common
denominator for behavior across a set of related classes.
TOfficeGadget.NotVirtual obscures the TBaseGadget.NotVirtual
method name in all instances and descendants of TOfficeGadget. For polymorphism to work, all the actions common to the
The inherited method is still a part of TOfficeGadget (proven by group of classes need to at least be named in a common
the code in Figure 3); you just can’t get to it directly from ancestor. If every descendant is required to override the ances-
TOfficeGadget and descendant types. tor’s method, the ancestral method doesn’t need to do any-
thing at all; it can be declared abstract.
To get past this, you must typecast the instance variable:
If there is a behavior that is common to most of the classes in
TBaseGadget(P).NotVirtual(1) the group, the ancestor class can pick up that default behavior
and leave the descendants to override the defaults only when
If P were declared as a TOfficeGadget variable, necessary. This consolidates code higher in the class hierarchy,
P.NewMethod would also be allowed, because the compiler for greater code reuse and smaller total code size. However, pro-
can “see” NewMethod in a TOfficeGadget variable. viding default behaviors in an ancestor class can also complicate
the design issues of creating flexible, extensible classes, since
Descendant >= ancestor. An instance of a descendant type what is done by ancestors usually cannot be entirely undone.
could be greater than its ancestor type in both services and data.
However, the descendant-type instance can never be less than Polymorphism lets ancestors reach into descendants.
what its ancestors define. This makes it possible for you to use a Another aspect of polymorphism doesn’t appear to involve
variable of an ancestral type (e.g. TBaseGadget) to refer to an instance pointer types at all — at least not explicitly.
instance of a descendant type without loss of information.
Consider the code fragment in Figure 4. The
Inheritance is a one-way street. With a variable of a particular TBaseGadget.NotVirtual method contains an unqualified call to
class type, you can access any public symbol (field, property, or ThisIsVirtual. When P refers to an instance of TKitchenGadget,
method) defined in any of that class’ ancestors. You can assign
an instance of a descendant class into that variable, but cannot procedure TBaseGadget.NotVirtual;
access any new fields or methods defined by the descendant begin
class. The fields of the descendant class are certainly in the ThisIsVirtual(17);
end;
instance data that the variable refers to, yet the compiler has no
way of knowing that run-time situation at compile time. var
P: TBaseGadget;
Figure 5: The structure of the Virtual Method Table, and its relationship to the object instance.
P.NotVirtual will call TBaseGadget.NotVirtual. Nothing new, so method call. You’ve specified that you want only the
far. However, when that code calls ThisIsVirtual, it will execute TBaseGadget.ThisIsVirtual method called, so the compiler
TKitchenGadget.ThisIsVirtual. Surprise! Even within the depths does exactly what you tell it to do. Dispatching this as a
of TBaseGadget, a non-virtual method, a virtual method call is virtual method call may call some other version of that
directed to the appropriate code. method, which would violate your explicit instructions.
Except in special circumstances, you don’t want this in
How can this be? The resolution of virtual method calls your code because it defeats the whole purpose of making
depends on the object instance associated with the call. A ThisIsVirtual virtual.
pointer to the object instance is secretly passed into all
method calls, surfacing inside methods as the Self identifier. The Virtual Method Table
Inside TBaseGadget.NotVirtual, a call to ThisIsVirtual is actu- A Virtual Method Table (VMT) is an array of pointers to all
ally a call to Self.ThisIsVirtual. Self, in this context, operates the virtual methods defined in a class and all the virtual
like a variable of type TBaseGadget that refers to an instance methods the class inherits from its ancestors. A VMT is creat-
of type TKitchenGadget. Thus, when the instance type is ed by the compiler for every class type, because all classes
TKitchenGadget, the virtual method call resolves, at run time, descend from TObject and TObject has a virtual destructor
to TKitchenGadget.ThisIsVirtual. named Destroy. In Delphi, VMTs are stored in the program’s
code space. Only one VMT exists per class type; multiple
How is this useful? An ancestral method — virtual or not — instances of the same class type refer to the same VMT. At
can call a sequence of virtual methods. The descendants can run time, the VMT is a read-only lookup table.
determine the specific behavior of one or more of those vir-
tual methods. The ancestor determines the sequence in Structure of the VMT. As shown in Figure 5, the first four
which the methods are called, plus miscellaneous setup and bytes of data in an object instance are a pointer to that class
cleanup code. The ancestor, however, does not completely type’s VMT. The VMT pointer points to the first entry in the
determine the final behavior of the descendants. The descen- VMT’s list of four-byte pointers to the entry points of the
dants inherit the sequence logic from the ancestor, and can class’ virtual methods. Since methods can never be deleted in
override one or more of the steps in that sequence. But, the descendant classes, the location of a virtual method in the
descendants don’t have to reproduce the entire sequence VMT is the same throughout all descendant classes. Thus,
logic. This is one of the ways OOP promotes code reuse. the compiler can view a virtual method simply as a unique
entry in the class’ VMT. As we’ll see shortly, this is exactly
Fully-qualified method calls are reduced to static calls. As a how virtual method calls are dispatched. Thinking of virtual
footnote, consider what happens if TBaseGadget.NotVirtual methods as indexes into an array of code pointers will also
contains a qualified call to TBaseGadget.ThisIsVirtual: help us visualize how method name conflicts are resolved by
the compiler.
procedure TBaseGadget.NotVirtual;
begin
TBaseGadget.ThisIsVirtual(17);
The VMT does not contain information indicating how
end; many virtual methods are stored in it or where the VMT
ends. The VMT is constructed by the compiler and accessed
Although ThisIsVirtual is a virtual method, a fully-quali- by compiler-generated code, so it doesn’t need to make notes
fied method call will compile down to a regular static to itself about size or number of entries. (This does, however,
Now let’s examine the mechanics behind the magic of virtual This article is adapted from material for Danny Thorpe’s book,
method calls. Delphi Component Design [Addison-Wesley, 1996].
The standard technique for saving this type The structure of an INI file is simple. Each
of information is to use INI files in contains one or more sections. The name of
Windows 3.1x, and the Registry in each section appears within brackets. Each
Windows 95 and Windows NT. This section contains zero, one, or multiple keys.
month’s installment offers an overview of The name of each key appears on a separate
how to save this information, focusing on line, and is separated from its value by the
the creation of a “most recently used” “equal” sign ( = ). In Figure 1, for example,
(MRU) file list as an example. the INI file contains a section named
Library. Within this section is a key
Using INI Files named SearchPath. The value of this key
In Windows 3.1x, the standard technique for is C:\DELPHI\LIB.
storing user-specific information is to employ
an INI (initialization) file. While typically Delphi provides a unit named IniFiles that
stored in the Windows directory, it can also defines the TIniFile class. This class encapsu-
be stored in the same directory as the EXE lates all the basic calls you need to work with
file (as long as the EXE isn’t stored in a INI files. You create an instance of the
shared directory). Delphi 1, for example, TIniFile class (i.e. an object of type IniFile)
stores information concerning its installation, by calling its Create constructor. This method
as well as user preferences and settings, in a has the following syntax:
file named DELPHI.INI. A portion of this
file, located in the \WINDOWS directory, is constructor Create(const FileName: string);
shown in Figure 1.
You pass a single string parameter when you
call Create. This parameter identifies the name
[Library] of an INI file that will be either opened or
SearchPath=C:\DELPHI\LIB
ComponentLibrary=C:\DELPHI\BIN\COMPLIB.DCL created (e.g. 'TestINI.INI'). If the specified
SaveLibrarySource=0 INI file does not exist, calling Create creates it.
MapFile=0 Otherwise, Create opens the INI file. If the
LinkBuffer=0
DebugInfo=0 file name you supply includes a DOS path,
the INI file will be created or opened in the
[Gallery] directory you specify. If you omit the path,
BaseDir=C:\DELPHI\GALLERY
GalleryProjects=1 the Windows directory is used by default.
GalleryForms=1
Once you’ve created an INI file, you’re ready
Figure 1: A portion of the DELPHI.INI file. to read and write keys to it, using the
procedure LoadMRU;
procedure UpdateMRU(const FileName: string);
procedure WriteMRU;
procedure DisplayMRU;
const
MaxMRUS = 4;
When calling WriteString, you pass three string parameters. 7) Implement the new methods described in step 4. An
The first is the name of the section you’re writing to, the sec- example of these methods implemented for a TForm1
ond is the name of the key, and the third is the value you’re class is shown in Listing Three on page 23.
assigning to the key.
Using these methods is straightforward. Call LoadMRU and
The corresponding read methods are ReadString, ReadBool, DisplayMRU from the form’s OnCreate event handler. Each time
and ReadInteger. The following is the syntax of ReadString: a new file is opened, call UpdateMRU. Finally, call WriteMRU
from the form’s OnDestroy event handler. You should also explic-
function ReadString( const Section, Ident, itly free both the IniFile object and the StringList object from
Default: string): string; within the OnDestroy event handler. Figure 3 shows examples of
how these event handlers might look.
When you call ReadString, you pass the name of the section
and the key that you want to read. In addition, you pass a Using the Registry
third argument whose value ReadString retains if the specified While Windows 3.1x makes extensive use of INI files for stor-
section and key do not exist. ing client-specific information, Windows 95 and Windows NT
encourage the use of the Registry for this purpose. The Registry
These methods are employed in the project named INI.DPR, is a centralized information database used as a repository for all
shown in Figure 2. This project demonstrates how to imple- client information by Windows 95 and Windows NT. You can
ment a generic MRU list for a Delphi menu. Using this
example code, however, requires some preparation. procedure TForm1.FormCreate(Sender: TObject);
Specifically, you must perform the following steps: begin
1) Add the IniFiles unit to the uses clause for the form. This MRUS := TStringList.Create;
LoadMRU;
clause can appear in either the interface or the implemen- DisplayMRU;
tation section of the unit. end;
var
{$IFDEF Win32}
Figure 4: REGEDIT.EXE allows you to view the Registry Editor. Ini: TRegIniFile;
{$ELSE}
Ini: TIniFile;
view the Registry Editor using REGEDIT.EXE (see Figure 4), {$ENDIF}
an application located in the \WINDOWS folder.
Furthermore, you must also use conditional compiler direc-
The Registry consists of keys, subkeys, values, and data. This tives when calling the Create constructor for the declared
information is depicted hierarchically, meaning that a key variable, as well as for the uses clause. For example, the fol-
may contain subkeys, and those subkeys may also contain lowing code calls the TIniFile constructor under Delphi 1,
subkeys. Typically, the lowest subkey on a branch will have and the TRegIniFile constructor under Delphi 2:
one or more values displayed in the right panel of the
Registry Editor dialog box. Within this right panel, the name {$IFDEF Win32}
of the value appears in the left column, and the data associat- Ini := TRegIniFile.Create('mru.ini');
{$ELSE}
ed with that value appears in the right column. Ini := TIniFile.Create('mru.ini');
{$ENDIF}
The Registry offers many advantages over INI files:
The Registry does not have an inherent size limit; INI An example of a project that uses these techniques is shown
files are limited to 64KB. in Figure 5. This project is named INIREG.DPR. Figure 6
The Registry is structured hierarchically, giving you more shows how the Registry appears at run time after compiling
flexibility in how information is stored, and making spe- the INIREG application under Delphi 2.
cific values easier to locate.
The Registry can be administered remotely by a system
administrator (or your Delphi code, for that matter).
A single Registry can store information about multiple
users.
Delphi 2 has two Object Pascal classes for working with the
Registry. These are TRegIniFile and TRegistry. Both are
defined in the Registry unit.
The Create method of TRegIniFile either opens or creates a Using the TRegistry Class
subkey in the HKEY_CURRENT_USER root key, using the The TRegIniFile class is extremely useful when creating applica-
file name as the key name. For example, the code in Listing tions that must be used in both 16- and 32-bit environments, as
Three creates an INI file named MRU.INI. If you change the well as for quickly porting applications to a 32-bit environment.
TIniFile references to TRegIniFile, and replace the IniFiles It’s limited, however, in that it permits you to add subkeys only
unit with the TRegistry unit, this code will create a key directly under the HKEY_CURRENT_USER root key.
named MRU.INI under the HKEY_CURRENT_USER root
key. Furthermore, instead of creating sections using the write For more complete control, you should use the TRegistry class,
methods, subkeys to MRU.INI will be created. In addition, which permits you to add subkeys and values to any key within
keys written to a section become values within the subkey. the Registry. While the TRegistry class contains a large number
MI := TMenuItem(FindComponent(Concat(
'mru',inttoStr(i+1))));
MI.Visible := True;
MI.Caption := MRUS.Strings[i];
Inc(i);
end;
MRUDiv.Visible := MRU1.Visible;
end;
procedure TForm1.WriteMRU;
var
The files referenced in this article are available on the Delphi Ini: TIniFile;
i: Integer;
Informant Works CD located in INFORM\97\JAN\DI9701CJ.
begin
if MRUSChanged then
begin
Ini := TIniFile.Create('mru.ini');
Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database try
i := 0;
development company. He is author of more than a dozen books, including Delphi
while (i < (MRUS.Count)) and (i < MaxMRUS) do begin
In Depth [Osborne/McGraw-Hill, 1996]. He is also Contributing Editor to Delphi Ini.WriteString('MRUS',Concat('mru',
Informant. You can reach Jensen Data Systems at (713) 359-3311, or via inttoStr(i+1)),MRUS.Strings[i]);
CompuServe at 76307,1533. Inc(i);
end;
finally
Ini.Free;
end;
Begin Listing Three — Example MRU Implementation
end;
procedure TForm1.LoadMRU;
end;
var
Ini: TIniFile;
End Listing Three
i: Integer;
Value: string;
begin
Begin Listing Four — Demonstrating the TRegistry Class
var
Ini := TIniFile.Create('mru.ini');
{ Hold the MRUs during the application }
try
MRUS: TStringList;
{ Load MRUs into Tstrings }
{ Flag for writing MRUs from OnDestroy for form }
for i := 1 to MaxMRUS do
MRUSChanged: Boolean;
begin
Reg: TRegistry;
Value := Ini.ReadString(
procedure TForm1.LoadMRU;
'MRUS',Concat('MRU',IntToStr(i)),'');
var
MRUS.Add(value);
i: Integer;
end;
Value: string;
{ Remove blank MRUs }
begin
while MRUS.IndexOf('') <> -1 do
Reg := TRegistry.Create;
MRUS.Delete(MRUS.IndexOf(''));
try
finally
Reg.OpenKey('\Software\YourCompany\ThisApp\MRUS',True);
Ini.Free;
{ Load MRUs into Tstrings }
end;
for i := 1 to MaxMRUS do begin
end;
if Reg.ValueExists(Concat('MRU',IntToStr(i))) then
begin
procedure TForm1.DisplayMRU;
Value := Reg.ReadString(
var
Concat('MRU',IntToStr(i)));
MI: TMenuItem;
MRUS.Add(value);
i: Integer;
end
begin
else
i := 0;
Break;
while (i < (MRUS.Count)) and (i < MaxMRUS) do
end;
begin
finally
Reg.Free;
end;
end;
procedure TForm1.DisplayMRU;
var
MI: TMenuItem;
i: Integer;
begin
i := 0;
while (i < (MRUS.Count)) and (i < MaxMRUS) do begin
MI := TMenuItem(FindComponent(Concat('mru',
inttoStr(i+1))));
MI.Visible := True;
MI.Caption := MRUS.Strings[i];
Inc(i);
end;
MRUDiv.Visible := MRU1.Visible;
end;
procedure TForm1.WriteMRU;
var
i: Integer;
begin
if MRUSChanged then
begin
Reg := TRegistry.Create;
try
Reg.OpenKey('\Software\YourCompany\ThisApp\MRUS',
True);
i := 0;
while (i < (MRUS.Count)) and (i < MaxMRUS) do begin
Reg.WriteString(Concat('mru',
inttoStr(i+1)),MRUS.Strings[i]);
Inc(i);
end;
finally
Reg.Free;
end;
end;
end;
By Ray Lischner
Stream of Consciousness
The VCL’s Internal Component Messages
The component messages are generated by set the Color property of a control, it broad-
the VCL in response to Windows messages casts the Cm_ParentColorChanged message
and user actions. The default action of a com- to its children so they can respond to the
ponent when it receives many of the VCL change, if needed. Delphi’s pre-defined com-
component messages is to broadcast that mes- ponents, for example, check the ParentColor
sage to all its child components. These mes- property, and if it is True, change the com-
sages originate with a Windows control that ponent’s Color to match the Parent control’s
receives a standard Windows message. new Color.
For example, a form receives a The messages described here are only some of
Wm_WinIniChange message, and broadcasts the internal component messages that Delphi
a Cm_WinIniChange message to its children, defines. Note that not all Windows messages
which in turn broadcast that message to their have corresponding component messages.
children, until it’s been sent to every child on
the form (see Figure 1). If you want your component to intercept,
say, a Wm_Compacting message sent to the
This enables you to write components that form, you cannot use a component message,
can respond to standard Windows messages but must set up an application message
without interfering with the form, which is hook, which is a completely different topic.
important when you’re writing a reusable
component. The component can silently do Many of the component messages don’t have
its work, responding to messages the form specific argument types, so you can use
automatically sends; the application pro- TMessage. Others do have specific types, such
grammer never needs to be concerned with as TCmDialogChar for the Cm_DialogChar
these details. message. If a message type is defined, the
descriptions here show the message type.
Other Cm_ messages are generated by VCL
components to broadcast to their child com- Some messages require that a result be stored
ponents, informing the children of a change in the message argument’s Result field. The
in the parent’s state. For example, when you value of the result depends on the message.
Windows sends
Wm_WinIniChange Application
to Application’s dispatches
message queue Wm_WinIniChange
to main form Form sends
Cm_WinIniChange
to Self
Application waits
for next message
Cm_AppKeyDown Message the main form, even if the child has its own menu. If you
The Cm_AppKeyDown message is sent from a control to the went to the trouble of adding a menu bar to a child form, you
Application object in response to a Cn_KeyDown or probably want the child form to handle its own menu short-
Cn_SysKeyDown event. The control first checks whether the cut messages. To do this, you must intercept the
key event is a shortcut for a pop-up menu. If not, the con- Cm_AppKeyDown and the Cm_AppSysCommand messages.
trol sends the Cm_AppKeyDown event to the application, The former handles the accelerator shortcuts and the latter
and the application handles the message by checking handles the caption shortcuts. Because the Cm_AppKeyDown
whether the key-down event is a shortcut key for the main message is sent only to the Application object, you cannot
form. If so, the main form handles the event, and the mes- write a handler for it.
sage Result is set to 1. If Result is zero, the main form didn’t
handle the event, which leaves the control that originated You can, however, write a message hook to intercept the mes-
the message to handle the Cn_KeyDown event. In other sage. The message hook can pretend the message has been
words, this message is how Delphi forwards a menu shortcut handled, thereby preventing the main form’s menu from ever
key from a child form to the main form. seeing the shortcut key. That lets each form handle its own
key stroke events. Figure 2 shows one way to hook the
For this message, a shortcut key is the value of a menu item’s Cm_AppKeyDown and Cm_AppSysCommand messages to pre-
ShortCut property. When the menu item Caption contains an vent the main form from receiving them.
ampersand (&), the letter after the ampersand is also a short-
cut key, but it’s treated differently. In this article, the latter The Cm_AppKeyDown message, like other key stroke messages,
shortcuts are called caption shortcuts and the former are called uses the TWmKeyDown message type. The CharCode field is
accelerator shortcuts. the virtual key code, and the KeyData field holds the modifier
keys, repeat count, and other key data. Figure 3 shows a
In Delphi 2, you don’t need to handle or send the description of the constituent parts of the KeyData field.
Cm_AppKeyDown message. If a child form has a menu bar, it
handles its own menu shortcuts, and if it doesn’t, it forwards Cm_AppSysCommand Message
the shortcut keys to the main form. In Delphi 1, however, you The Cm_AppSysCommand message is sent in response to a
must handle this message if you want a child form to have its Wm_SysCommand message, but only when the sending form
own menu bar, separate from the main form’s menu bar. is not the main form, the form is not minimized, the com-
In Delphi 1, a child form forwards its menu shortcut keys to mand is Sc_KeyMenu, and the key is not a space or a hyphen
procedure TMainForm.FormCreate(Sender: TObject); (wParam) and the sender button (lParam), but no message
begin type is defined. For your convenience, Figure 4 shows the
Application.HookMainWindow(AppKeyDownHook)
end; TCmButtonPressed type, which you can use when handling
this message.
Figure 2: How to intercept and discard the Cm_AppKeyDown
and Cm_AppSysCommand events. If you derive a new class from TSpeedButton, you can handle
this message if you want to implement different behavior
when a speed button is pressed. You can also handle this event
type for non–speed button components, perhaps to reflect in a dif-
TKeyDataFlag = ( kdExtended, kdUnused1, kdUnused2, ferent manner which speed button is currently depressed.
kdInternal1, kdInternal2, kdAltDown,
kdWasDown, kdReleased);
TKeyDataFlags = set of TKeyDataFlag; The result is ignored for this message.
TKeyData = record
RepeatCount: Word;
ScanCode:
Flags:
Byte;
TKeyDataFlags;
Cm_ColorChanged Message
end; When a control’s Color property changes, it sends the
Cm_ColorChanged message to itself. The default message
handler invalidates the control, so Windows will repaint it.
Figure 3: TKeyData record for interpreting key event data.
A control with children broadcasts the
Cm_ParentColorChanged message to its children so they can
(-). In other words, this message is sent when the user presses respond to the color change, if needed.
a caption shortcut key.
TWinControl also copies the color to its Brush.Color prop-
In Delphi 1, you must intercept this message when you want erty, and TForm copies the Color to its Canvas.Brush.Color
a child form to have its own menu bar, separate from the property. If you derive a class from a different base class
menu of the main form. Figure 2 shows how to do this. (such as TGraphicControl ) that publishes the Color proper-
Otherwise, there is no reason to handle or send this message. ty, you might want to handle the Cm_ColorChanged mes-
sage. Remember to call the inherited message handler to
In Delphi 1, a form sends the Cm_AppSysCommand to the ensure the control is refreshed and all child controls are
application, which forwards it to the main menu. The mes- properly notified.
sage type is TWmSysCommand. In Delphi 2, a control first
sends the Cm_AppSysCommand message to its parent form; Because this message takes no arguments and returns no
the parent form forwards it to the application only if the result, you should use TWmNoParams for the message type.
form is an MDI child, has no menu, or has a main menu
whose AutoMerge property is set to True. This lets a child Cm_ControlListChange Message
with its own menu handle the shortcut key. The lParam A window control sends the Cm_ControlListChange mes-
argument is a pointer to the original message’s TMessage sage to itself when its control list changes; that is, when a
record. The parent form passes this TMessage record to the control is added or removed. A TDBCtrlPanel component
Application object, which forwards it to the main form. updates its database links when it receives this message.
In most cases, your component doesn’t need to handle this mes- When using the Application object in a DLL, be sure to
sage. If you’re writing a component that must respond to mouse assign the main application’s handle to the
events at design time, you can supply a message handler. If you Application.Handle property. This allows the Application
decide that the mouse cursor is not in a design area, call the object in the DLL to forward the Cm_DialogHandle mes-
inherited message handler. The inherited handler checks for sage to the Application object in the main application.
child controls that might need to handle this message. Figure 5 shows an example of an Initialize procedure in a
DLL, which takes a window handle as an argument,
The message type is TCmDesignHitTest. The cursor position assigning it to Application.Handle.
is given by the XPos and YPos fields, or by the Pos field, which
is of type TPoint. The Keys field gives the state of the V To use the DLL’s Initialize procedure, you must pass
and C modifiers, and tells you which mouse button is Application.Handle as its argument. This is shown in Figure 6.
pressed, in the same manner as TWmMouseMove (as a bit
mask). Call the KeysToShiftState function to convert this bit Because the Cm_DialogHandle message is used internally
mask into a TShiftState set, which is easier to use. by the TApplication class, there is no reason for you to send
or handle it. Instead, use the Application.DialogHandle
Cm_DialogHandle Message property. This message is defined only in Delphi 2.
In Delphi 2, an Application object in a DLL sends the
Cm_DialogHandle message to its counterpart in the applica- Cm_Drag Message
tion, which responds by setting or returning its The Cm_Drag message communicates drag events between
DialogHandle property. When wParam equals 1, the controls. The DragMessage field specifies what kind of drag
Application object returns the current value of event is taking place (Enter, Leave, Move, Drop, Cancel, or
DialogHandle; otherwise, it sets DialogHandle to the value Find Target), and the DragRec field points to a TDragRec
of the lParam field. This lets Delphi keep a single record, which contains a reference to the Source and Target
DialogHandle for an application and all DLLs. objects and the cursor position.
Delphi controls do not handle This message takes no arguments and returns no result, so
this message except to forward it you should use TWmNoParams as the message type.
to the parent control. The
lParam argument is the message Cm_ShowHintChanged Message
sender, or nil if the sender is the Figure 12: Moving the The Cm_ShowHintChanged message is similar to
Application object. This message mouse on a form. Cm_ColorChanged, but corresponds to the ShowHint property.
Application sends
Cm_MouseEnter
to OK Button OK Button forwards
Cm_MouseEnter
to Panel Panel forwards
Cm_MouseEnter
User moves mouse to Form
over Cancel Button
(Wm_MouseMove
not shown)
Application sends
Cm_MouseLeave
to OK Button OK Button forwards
Cm_MouseLeave
to Panel Panel forwards
Cm_MouseLeave
to Form
Application sends
Cm_MouseEnter
to Cancel Button Cancel Button forwards
Cm_MouseEnter
to Panel Panel forwards
Cm_MouseEnter
to Form
Cm_WinIniChange Message
When a form receives a Wm_WinIniChange message, it Ray Lischner is a software author and consultant for Tempest Software
sends Cm_WinIniChange to itself. The default behavior of (http://www.tempest-sw.com). His current focus is object technology, especially in
Delphi, Java, and Smalltalk. You can reach him at [email protected].
a control when it receives the Cm_WinIniChange message
By Mark Ostroff
What’s in a Name?
Naming Conventions for Delphi Projects
In fact, Delphi might make it too easy. effective naming system. The larger the
Soon you can have a bewildering number number of objects in your project, the
of components in your application with more important the naming of those
useful names such as Edit1, Query2, and objects becomes. Otherwise, you’ll have
DataSource5. Delphi’s default object names more and more difficulty finding the cor-
are only good as place holders until you rect object to select in the Object
develop a workable naming convention. Inspector’s pull-down lists. A workable
naming convention is also important when
Why Design a Naming Convention? you start working on a project with a team
Without some forethought, the list of com- of developers, because you might not be
ponent names you choose could become as the only developer creating object names.
confusing as the names Delphi assigns.
Many of us begin naming our Delphi Figure 1 shows the result of creating a new
objects based on their functionality — project using the SDIApp template from
names such as ExitButton, DeptField, and Delphi’s Object Repository. Notice that no
DateSpin. apparent organization exists for this list of
objects. Because the names are ambiguous
This convention seems to make sense for a and inconsistent, it’s hard to tell which object
while. However, as you use your objects, it some of the names refer to.
soon becomes apparent that you need a bet-
ter way of organizing their names. Do FileMenu and MainMenu refer to sepa-
rate objects of type TMainMenu? Do
Organization becomes important. Delphi’s ExitItem and FileMenu both refer to
Object Inspector lists your components in TMenuItem objects? These aren’t idle ques-
alphabetical order. This feature is only use- tions. As your projects grow, you will
ful in large projects when you have an remember what type of component you
want to work with, but you might not
remember its name.
Internet Solution Pack components under the prefix web. If Data object naming. Look at the DataModule displayed in
you rarely use DDE, you might want to use the prefix dde Figure 5. Assigning a DataSet to the dtsContacts DataSource
for all four DDE component types. object is simplified by this naming convention. It’s easy to see
that this DataModule contains a number of Query, Stored
Some Examples Procedure, and Table objects.
The effects on the ease of development are best seen in an
example. In Figure 2, we saw that poor object naming can Menu object naming. Note that this DataModule also con-
make method selection difficult. tains an application standard PopUpMenu named
You only need two designations for file names, one for Forms
and another for DataModules. Simply use a two-letter prefix
when you save the file (see Figure 7).
Conclusion
Try this naming convention in your next project. After an
adjustment period, I think you’ll find your Delphi develop-
Figure 5 (Top): An example of a workable naming convention. ment will proceed even more rapidly than before. As your
Figure 6 (Bottom): Easing method selection.
project grows, you’ll also realize more efficiencies.
pumStdPopUp. Because PopUpMenus are more amenable to This article builds on the work of two groups. The initial
reuse, it makes sense to include standard ones in a effort was started by Borland. Many of the concepts and
DataModule. naming conventions covered here are used by Borland for
developing its in-house applications.
It also makes sense to give them their own type prefix.
PopUpMenus are used differently than MainMenus, and Additional work was performed by the Delphi Study Group,
their type prefix should reflect this, rather than lumping sponsored by Informant Communications Group on its
them together with other menus. CompuServe forum. Additional thanks go to Tom Arnold
and the other members of the group for consolidating much
Menus also have unique naming convention requirements. of the discussion about type prefixes. ∆
Typically you’ll want to perform one set of actions on a
menu object itself. You’ll want to execute another kind of
Mark Ostroff has over 18 years experience in the computer industry. He began by
logic in response to events that occur for MenuItems with- programming minicomputer medical research data acquisition systems, device
in that menu object. interfaces, and process control database systems in a variety of 3GL computer lan-
guages. He then moved to PCs using dBASE and Clipper to create systems for the
Thus, the prefix list in Figure 4 suggests mnu and pum for US Navy, as well as for IBM’s COS Division. He also volunteered to help create the
MainMenu and PopUpMenu objects, respectively, and mni original Paradox-based “InTouch System” for the Friends of the Vietnam Veterans’
Memorial. Mark has worked for Borland for the past six years as a Systems
for use with MenuItems themselves. The clarity this adds Engineer, specializing in database applications.
to selecting event handlers is shown in Figure 6.
By David Faulkner
TBarCode
A Custom Bar Code Component
This article presents TBarCode, a compo- potentially expensive, and a lot of trouble
nent that displays bar codes which can be compared to the ease with which TBarCode
read into a computer using an optical scan- was assembled.
ner as the input device. TBarCode imple-
ments the Code 39 specification, but you The Specification
should be able to extend it to display any Code 39 maps each of a set of pre-defined
bar code format. Figure 1 shows TBarCode printable characters to a set of nine bars. A
in action. bar can be wide or narrow, black or white,
meaning both the black bars and the spaces
Before creating this component, I experi- between are meaningful.
mented with bar code fonts that display
coded symbols for each character, i.e. set- There’s no specific width assigned to a bar;
ting TLabel ’s Font property to a bar code instead, only the ratio of the bars is speci-
font effectively creates a bar code. While fied. The wide bars must be 2.2 to 3.0
this approach worked, it meant I had to times as wide as the narrow bars. The larg-
worry about licensing the font and er ratio is usually favored, as it results in
installing it on users’ machines. This was fewer read errors.
A legal Code 39 bar code requires a start and stop char- constructor TBarCode.Create(AOwner: TComponent);
begin
acter — the * character is encoded as NWNNWNWNN. Thus inherited Create(AOwner);
the shortest Code 39 bar code is three characters, ControlStyle := ControlStyle - [csOpaque];
although the bar code-reading hardware usually strips off Alignment := taCenter;
AutoSize := False;
the * characters. This requirement also makes it easy to Height := 50;
visually check if a bar code is Code 39. If the first nine Width := 175;
bars match the last nine bars and the bars are two dis- end;
The interesting part here is that the cValidCode39Characters Figure 3: A Code 39 bar code created by TBarCode.
constant is not only used to validate the input; it’s also a
map into BarCodeTable. For example, the character “A” is Other Goodies
the 11th character in the cValidCode39Characters string and In addition to TBarCode, you should check out the down-
corresponds to the 11th element in the BarCodeTable array. load file. It contains TDBBarCode, a data-aware version of
TBarCode; TQRBarcode, a QuickReport compatible ver-
BarCodeTable is a two-dimensional array whose first ele- sion; and TQRDBBarCode, a QuickReport data-aware ver-
ment is 1,1 for conveyance (as compared to 0,0). Each row sion. Both 16- and 32-bit resource files are included, so it
of BarCodeTable contains a series of nine Ws and Ns which can be compiled in Delphi 1 or 2.
specify the pattern of wide and narrow bars for the corre-
sponding character. Conclusion
I’ve included the sample program, shown in Figure 3. It
PaintABar lets you enter a value at run time and see the resulting bar
PaintABar’s job is to paint a single bar at the passed coordi- code. The sample program also prints the bar code for you
nates. The code that paints the bar is: so you can test it against your optical scanner.
TWorldMap
Seeing the World in a New Way
APPLICATION PROFILE
The TWorldMap component is totally configurable by the
developer. Its functionality includes built-in zoom, built-in
adjustable latitude/longitude grid, a custom point object, cus-
tom line object, clickable polygons, custom printing, and cre-
ating bitmaps. Available in both 16- and 32-bit versions,
TWorldMap is a royalty-free, easy-to-use solution for many
day-to-day geo-presentaion needs.
The FDT team planned on using a bitmap image for the map.
However, while waiting for the pictures to use in the application, Figure 4: A phone sales distribution system written by Gary
Holbrook.
the team saw an advertisment for TWorldMap. The team thought
it would provide a better interface, if the component could be
the Environmental Protection Agency (EPA) uses TWorldMap
obtained in time; only three weeks remained until the flight soft-
ware needed to be delivered, and it isn’t normally possible to pur- for its Clean Air Act health benefit model (see Figure 3); the
chase software that quickly for a government project. They called US Marine Corps uses it for its scheduling system; a commer-
Don to determine if NASA had a license, or if there was some cial marketing company is using the component to color states
way to use the code. It turned out that another group at the by sales revenue (see Figure 4); and the US Army is integrat-
Johnson Space Center, working on a Shuttle emergency landing ing TWorldMap into the Automated BattleBook system to
site program, had the software, and Don let the team tag on. show the world’s reserve equipment depots.
It took less than a week to write the program using the Looking Ahead
TWorldMap component. The program includes a scale bar, Custom systems are created by Don for customers who need
variably spaced latitude/longitude grid bars, and site informa- specific capabilities, and all added capabilities are made avail-
tion that displays when the cursor approaches a marked site,
able to registered users.
which is why it took as long as it did. TWorldMap made the
application much better.
Don currently has a few major clients that are waiting for the
The FDT team has received nothing but favorable comments map to support different projections, live scrolling, infinite
from NASA staffers, including astronauts, who have seen the zoom-in, and, of course, a native link to database tables; these
application. Additional features are planned for the next version. are his current areas of focus. ∆
— David B. Chiquelin
Lead Programmer, Field Deployable Trainer project
United Space Alliance
Canada uses the component to show truck routes; three com- Don Bauer is Director of Software Development, East Coast for Marotz, Inc. (a
Delphi Development Company), and is currently converting the Navy’s Fleet
mercial astrology systems use TWorldMap (see Figure 2); in Modernization Program Management Information System to Delphi 2. He can be
Newport Beach, CA, a statistics company uses TWorldMap to reached at [email protected].
show salary and benefit distribution within North America;
InfoPower 2.0
Making Life Easier for Database Developers
Figure 4: If no picture fits the bill, InfoPower 2.0 lets you “roll
your own.”
the user enters an invalid value. Using these events, you can
determine if the value entered by the user is indeed invalid. If
the value is invalid, you can display an error message that
identifies the invalid field when the user tries to post the
record. Because the OnInvalidValue event identifies the
offending field, you can also move focus to that field and
change its color.
Filter Dialog
Figure 2 (Top): The Select Fields dialog box provides an easy
TwwFilterDialog gives end users an easy way to filter or query
way to enter pictures. Figure 3 (Bottom): Simply select the pic- a dataset on multiple fields. To use it, just drop the
ture that best fits your needs. FilterDialog component on your form, set its DataSource
property, and provide a button or menu choice to call its
Another good example of the superiority of InfoPower’s Execute method. Calling Execute displays the dialog box
pictures over Delphi’s edit masks is the use of the picture: shown in Figure 5.
#[#]/#[#]/##[##] You can specify one of two ways to select records. If the dataset
being searched is a TwwTable or TwwQBE, then a BDE filter is
for a simple date. Not only does this picture allow you to applied. If the dataset is a TwwQuery, you can either filter the
enter either a one- or two-digit month or day, it also lets query result set, or let the FilterDialog modify the WHERE
you enter either a two- or four-digit year. clause of the query and re-execute it to select the records. You
can set the caption of the dialog box, as well as choose the
If you’ve ever tried to use a Delphi edit mask for a date, you fields for which the user can enter selection criteria.
have probably discovered that it puts the two literal characters
(the slashes) into the field as soon as it gets focus; you can’t The View Summary button allows users to see the field or
eliminate them if you then decide to leave the field blank. fields for which they have entered selection criteria. The
InfoPower doesn’t insert the literals until, when typing your By Value and By Range tabs let users enter a single value
value, you reach the point at which they appear. If you high- to search for, or a range of values. Entering a single value
light the field and delete the value, the literal characters dis- lets users choose to search for an exact match, a record
appear as well, allowing you to easily leave a field blank. that starts with the search value, or a record that contains
the search value anywhere in the field. Users can also
As with all other InfoPower features, pictures work identi- select a case-sensitive search.
cally in both Delphi 1 and 2. Also, they apply whether
you assign a value to a field in code, or the data is entered Wait — There’s More
by the user through a form. InfoPower 2.0 includes three other new components:
1) The TwwIntl component is a non-visual control that
By setting the AllowInvalidExit property to True, you can allows easy internationalizing of applications that use
allow a user to exit a field that contains an invalid value. InfoPower. It provides a central location where you can
You can also control whether the pictures are used for change the captions, hints, and button styles used by all
interactive editing by setting the UsePictureMask property. of InfoPower’s built-in, end-user dialog boxes.
2) The TwwDBSpinEdit lets users easily increment or decre-
InfoPower controls also include OnCheckValue and ment the value in a numeric or date field using the
OnInvalidValue events to let you control what happens when mouse or the up- and down-arrow keys. You can set the
AN INFOPOWER PRIMER
For those who haven’t had the pleasure, here’s a short rundown of
existing features in InfoPower version 1.0. All are available to users of
Delphi 1 or 2.
ost third-party Delphi products fall into one of two categories: com-
M ponent libraries or utilities. The first is self-explanatory; the latter
helps programmers develop, debug, and profile applications, and create
custom components.
SysTools for Delphi is different; it contains no ibility with Win/Sys or take full advantage of
components or utilities. It is however, a large Delphi’s new features. TurboPower opted for
library of low-level routines that can help with the latter, writing SysTools almost from
the inner workings of applications. Produced scratch in Delphi (with a significant amount
by TurboPower Software Company, SysTools of assembly code). SysTools works with either
is based on an earlier TurboPower product, Delphi 1 or 2. While many of its methods
Win/Sys Library for Borland Pascal and C++. and properties have the same names and para-
While most tools in the earlier library are meters as in Win/Sys Library, some do not.
included, some differences exist (see Figure 1).
SysTools works with either 16- or 32-bit
A Comparison of Win/Sys and SysTools operating systems. In 16-bit programs,
In transforming the Win/Sys Library into most SysTools routines are similar to those
SysTools for Delphi, TurboPower had to in Win/Sys; however, this isn’t the case with
decide whether to provide full reverse compat- 32-bit programs. Some Win/Sys units aren’t
available, including WsTimer (timing and
Purpose of Unit Win/Sys Library SysTools delay procedures), WsDPMI (DPMI
access), and WsHeap (Heap analysis).
String Manipulation WSString STString
TurboPower also has a new utility, Memory
STStr? (S/L/Z)
Sleuth (available separately), that includes
Date/Time Manipulation WSDate STDate
Low Level Operating WSInLine STUtils tools to analyze and report various prob-
System Routines WSDos lems such as memory leaks and resource
Bit Set Manipulation WSBitSet STBits allocation errors in 32-bit programs.
String Dictionary WSPchDct STDict
Double Ended Queue WSQueue STDQue The classes comprising the SysTools library
Large Array and WSLarray STLarr can be divided into three groups: string and
Large Matrix (2 dim) STMatrix numerical data manipulation, container class-
Collection Classes WSColl STColl es, and a low-level interface to the Windows
Linked List WSList STList operating system. With the exception of the
Virtual Array (not implemented) STVarr container classes (which are discussed in one
Tree Class WSTree STTree chapter), each class is explained in its own
Sorting Engine WSSort STSort chapter of the manual which defines all the
Timer WSTimer (not implemented) key properties and methods using clear
BCD Arithmetic Methods (not implemented) STBCD examples. Each chapter also includes a sam-
Registry/INI File Access (not implemented) STRegIni ple application demonstrating its features and
Figure 1: Differences between the tools in Win/Sys Library and capabilities. Now we’ll look at the features,
SysTools for Delphi. uses, and limitations of the main class.
procedure CreateConfigEntry;
begin
CFG.Free;
CFG := nil;
if not DirectoryExists(ConfigTest.InstallPath) then
MkDir(ConfigTest.InstallPath);
{$IfDef Win32}
CFG := TSTRegIni.Create(RICUser, False);
CFG.CurSubKey := 'Software';
CFG.CreateKey('MyApp');
{$Else}
if FileExists('C:\Windows\MyApp.Ini') then
DeleteFile('C:\Windows\MyApp.Ini');
CFG := TSTRegIni.Create('C:\Windows\MyApp.Ini',True);
{$EndIf}
CFG.CreateKey('User Info');
CFG.CurSubKey := 'User Info';
CFG.WriteString('User', ConfigTest.UserName);
Figure 6: The example program, RIEdit, allows you to view and CFG.WriteDateTime('Config_Changed',
edit .INI files or the Windows Registry. ConfigTest.CreateTime);
CFG.CreateKey('Program Options');
CFG.CurSubKey := 'Program Options';
tionality of Delphi components (e.g. Number of Drives by CFG.WriteString('Prog_Directory',
ConfigTest.InstallPath);
the TDriveCombo component). Win/Sys’ ExistFile function
CFG.WriteBoolean('Program Option Enabled',
now has a Delphi equivalent (FileExists in the SysUtils ConfigTest.ProgOptEnabled);
unit); and various methods such as ExtractFileExt and CFG.Free;
end;
ExtractFileName functions have been replaced by Delphi’s
filename parsing functions. Figure 7: InstallPath, UserName, CreateTime, and
ProgOptEnabled hold the application’s configuration data.
Registry and .INI Files
The Windows Registry (.REG) and .INI files are used in
Assembly Code in Delphi
Windows 3.x, Windows 95, and Windows NT. While .INI
One of the bonuses in SysTools is a large collection of
files were the preferred way to store information in Windows
routines demonstrating the different ways to use assembly
3.x, the Registry has assumed this role in Windows 95.
code. Two units have the bulk of ASM code. The STBase
unit contains a number of strict assembler functions. The
Since .INI files are ASCII text files, they are easy to edit.
other unit, STUtils, includes assembly code within
.REG files, which are binary, pose more problems.
Delphi methods and functions. There is even inline code
However, SysTools’ TRegIni class provides all the tools nec-
for some of the 16-bit primitives (inline is no longer sup-
essary to write to or read from .REG and .INI files. You
ported in Delphi 2, but was used in Delphi 1). Let’s look
can use practically the same code to work with both types.
at two examples.
The example application for this unit, RIEdit, mimics the
Windows 95 RegEdit utility. It transparently opens and
At the start of the STUtils unit is a series of 16-bit proce-
shows either the Registry or a particular .INI file (see
dures and functions written in inline code for Delphi 1.
Figure 6). Data types you can write to or read from a .REG
Most are low level. How low? Here is the 16-bit version of
or .INI file include strings, Boolean, datetime types, inte-
MakeWord, a function to construct a word out of two bytes:
gers, floating point, and so on.
function MakeWord(H, L : Byte) : Word;
A configuration example. To demonstrate the power of this { Construct a word from two bytes }
{$IFNDEF OS32}
class, I’ve written a small demonstration program that sets a inline(
few configuration options after testing for a serial number. It $58/ { pop ax ;low byte into AL }
consists of a project file and four unit files for each of the $5B/ { pop bx ;high byte into BL }
$88/$DC); { mov ah,bl ;high byte into AH }
four forms: CGFTest, SerialDg (a Serial Number testing dia- {$ENDIF}
log box), EntDlg (Entry Dialog Box), and ShowCFG (which
allows both viewing and editing of existing .REG or .INI Now let’s take a look at the 32-bit version for Delphi 2:
files). Admittedly, the program is much simpler than
SysTools’ example program. However, it demonstrates the function MakeWord(H, L : Byte) : Word;
begin
power of these tools. Result := (Word(H) shl 8) or L;
end;
Following is the method that writes to a .REG or .INI file.
CFG is the variable that refers to the file being written to. Among other things, this comparison demonstrates the opti-
InstallPath, UserName, CreateTime, and ProgOptEnabled hold mization built into Delphi 2. It’s no longer necessary to write
the application’s configuration data. Note the compiler direc- as much assembly code to optimize. Few of the 32-bit exam-
tives that enable the appropriate changes for Delphi 1 or 2 ples contain assembly code. The Delphi 2 version of the
(see Figure 7). WriteVolumeLabel function is lean compared to Delphi 1:
he term business object has a certain ethereal quality to it. Although touted for years by object-
T oriented (OO) proponents, questioning its utility isn’t necessarily wrong. Most developers have
found it hard to sink their teeth into business objects and use them in real-life applications. Last year,
I talked about emerging trends in object-oriented programming. This month, we’ll take a closer look
at one of those trends — business objects — and determine to what extent you can take advantage
of them in Delphi.
What are business objects? Perhaps the should be able to gain a return on the ented language, so the ability to create
simplest way to understand what busi- investment in future applications. business object classes is not an issue.
ness objects are is to contrast them with However, objects instantiated at run
what I call system objects. System objects Business objects simplify the process of time are stored only in memory; all
are reusable components that can be making rule changes. After developing a information related to an object is lost
used across a variety of business appli- user interface component (such as an when you close an application. Thus,
cations. The TTable component, along edit box), how often will you need to in order for business objects to be use-
with all other components in the make changes to it? While some ful, you must be able to store them
Delphi Visual Component Library, is a changes are expected, it’s unlikely that persistently between sessions.
good example of a system object. In many properties or methods of this Unfortunately, out of the box, Delphi
contrast, business objects encapsulate visual component will need to be modi- has little to offer when it comes to
business logic specific to a business fied or added. Contrast this edit box object storage. Because Delphi is pri-
entity (such as a customer, student, or with a typical business object. As the marily targeted as a client/server devel-
invoice) or a process (such as a purchase companies these business objects imi- opment tool, its database access is
or ATM withdrawal). System objects tate are constantly changing, there will focused exclusively on being a good
are horizontal, whereas business objects be an ongoing need to make changes to client for back-end relational database
are — by their nature — vertical crea- a business object. Because of encapsula- systems. Also, Delphi data classes don’t
tures, specific to a particular line of tion, a properly designed business support persistent storage of objects,
business, e.g. a business object devel- object can be modified rather quickly, so you really have no choice but to do
oped for a bank probably won’t have without causing a ripple effect across an it on your own. In an upcoming
tangible benefits for an HMO. application — as long as its external File | New, we’ll look at the four
interface remains constant. options you have for creating a custom
Why use business objects? When devel- business object storage solution.
oping applications, the greatest amount of Business objects separate business rules
energy is usually spent on the business- from the rest of the application. In Are you using business objects in
specific parts of the application. Given client/server systems, a fundamental Delphi? If so, contact me at
this investment, it’s clear that business issue has always been determining [email protected] and let me
rules are the heart of most applications. where the business rules of the applica- know how you’re using them. I’ll com-
Thus, encapsulating these rules into busi- tion should be stored: within the pile this information and share it with
ness objects has several advantages: client’s user interface, or as stored pro- all of you. ∆
cedures on the server. From an OO
Business objects provide the ability to standpoint, the solution is to partition — Richard Wagner
build a reusable component architecture. the application, adding a business logic
Reuse is touted as one of the funda- layer between the presentation and
mental motivations behind OO database levels. Such an application Richard Wagner is the Chief Technology
methodologies. As with any Delphi architecture becomes more extensible Officer of Acadia Software in the Boston,
component you create, a well-built and easier to maintain. MA area. He welcomes your comments
business object can be reused in multi- at [email protected] or
ple applications. Therefore, after a com- Can you use business objects in on the File | New home page at
pany has developed business objects, it Delphi? Delphi features an object-ori- http://www.acadians.com/filenew.htm.