Delphi Informant 95 2001
Delphi Informant 95 2001
Creating
Components
Assembling Delphi’s Building Blocks
By Robert Vivrette
ince its release in February, Delphi has created quite a stir in the com-
S puter industry. One of the more prominent changes I have seen is in the
arena of component design. I’m sure you’ve noticed the proliferation of
component design articles in various programming journals.
The component design phenomenon is one of the exciting features of Delphi. I don’t think you
can find a better platform to write incredibly powerful and robust components. Each component
that you write in Delphi can be just as capable and efficient as those that are pre-installed. And
once you’ve developed a new component, it is seamlessly integrated into the environment. (How
many custom control articles did you see for Visual Basic when it first came out? Not many I bet.)
This article will discuss some of the more advanced elements of component design in Delphi. The
featured example is a slide bar component, but the focus will be on how you can implement these
more advanced capabilities into your custom components.
There are several reasons for not using an edit box. First, that
approach requires the user to use the keyboard, and it would be
convenient if the user could also select a number with the mouse.
Second, the number may be irrelevant. For example, the number may
clicks and mouse movement. In addition, the user may want to use
the keyboard, so support should be implemented for the compo- The WMKillFocus procedure would also be the same. Now,
nent to respond to keyboard events. Since the slide bar can be when the component is being repainted, it’s a simple matter
selected with the keyboard, it should indicate when it has focus. of looking at the Focused property. If the component current-
We’ll also allow the slide bar to be oriented vertically or horizontally. ly has focus, this will return True. Otherwise, it will return
False.
There are a few other things I added to the component to spice
it up. The TSlideBar can display small tick marks along the slot Based on the result, the center portion of the slot is then col-
to “tell” the user there are fixed positions that the thumb will ored appropriately. To add a bit more pizzazz to the control,
jump to. A custom cursor was also added, so that when the the FocusColor property was added to define the color that will
mouse passes over the control, it presents a pointing hand — a be used when the component has focus. Inside the routine for
more appropriate mouse pointer for a slide bar control. drawing the slot is the code for managing the focus:
A Key Feature Note: When constructing case statements, try to list the items
There is nothing more frustrating than being forced to use a in ascending order. Here for example, VK_PRIOR is a constant
Windows control with only the mouse or keyboard. A well-designed with the value of 21, VK_NEXT has the value 22, and so on to
control must work with either device and should not require the VK_DOWN that has a value of 28. When a case statement is sort-
user to constantly switch between the two input devices. ed in ascending order, the compiler can optimize the code to
execute faster. If they are not in sequential ascending order, the
Again, adding this feature doesn’t require expending too many compiler must use a less efficient method to generate the
brain cells. First, we must override the KeyDown event that we needed code. All it takes is one line out of sequence to foil
are inheriting from up the object tree. In the private section of optimization, so make sure you pay attention.
the class definition, you should add a line such as:
When you add the code shown in Figure 2 and run the pro-
private gram — it doesn’t work! Rather than moving the thumb’s posi-
procedure KeyDown(var Key: Word;
Shift: TShiftState); override;
tion, the arrow keys are moving the focus between controls. To
get your control to pay attention to the arrow keys, you must
At a minimum, we want to enable the user to move the thumb trap a Windows message called WM_GETDLGCODE and tell it
using the arrow keys. I also added support for h and e to that you will handle the arrow keys. Like our WM_SETFOCUS
move the control to its minimum and maximum values respec- and WM_KILLFOCUS handlers above, we must add the follow-
tively. In addition, I decided to add support for u and ing line to the private section:
d. Since the arrow keys only move the slide bar’s position private
by one, you would be in trouble if the range of the slide bar was procedure WMGetDlgCode(var Message: TWMGetDlgCode);
1000. Therefore, u and d were enabled to move the message WM_GETDLGCODE;
slide bar’s position by 10 percent of its total length. To enable
these functions, the KeyDown procedure was used (see Figure 2). The procedure’s code is as simple as it gets:
procedure TSlideBar.WMGetDlgCode(var Message:
The Position property is (of course) the slide bar’s current posi- TWMGetDlgCode);
begin
tion. The Max and Min properties are the maximum and mini- Message.Result := DLGC_WANTARROWS;
mum values that the slide bar can reach. end;
By default, however, mouse capture has been turned on for being dragged, it determines if the MouseUp event occurred to
components that descend from the TControl object. As a the left or right of the thumb. If the click occurred on the
result, when you select the slide bar’s thumb (by holding left, the thumb is moved one position to the left. If the click
down the left mouse button), you can drag the mouse all over occurred on the right, the thumb is moved one position to
the screen and the thumb will move appropriately. As long as the right.
you keep the mouse button down, that control has “captured”
the mouse. When the mouse button is released, it immediate- MouseMove only moves the thumb if the mouse button is down,
ly releases the capture. and it is dragging the thumb. The MouseDown event, however, is
short and interesting:
You can modify whether the control will capture the mouse by
setting and/or clearing the csCaptureMouse flag from the compo- procedure TSlideBar.MouseDown(Button: TMouseButton;
Shift: TShiftState; X,Y: Integer);
nent’s ControlStyle property. If you are going to change a compo- begin
nent’s ControlStyle you should do it in the Create method. For SetFocus;
example, to ensure that a control does not capture the mouse, Dragging := PtInRect(ThumbRect,Point(X,Y));
if IsVert then
the code would look similar to this: DragVal := Y
else
ControlStyle := ControlStyle - [csCaptureMouse]; DragVal := X;
end;
protected
The MouseUp and MouseMove methods have some pretty bor- procedure Paint; override;
ing calculations in them and would not add much to this dis-
cussion. To summarize their behavior, however, MouseUp This tells Delphi that you are going to create a custom Paint
determines if the thumb was being dragged, and if so, finds method, and that you are not interested in the Paint method
the closest position to “click” the thumb to. If it was not inherited from higher up the object tree.
For the TSlideBar component, I divided the painting responsibil- sary to maintain the component’s correct appearance. Here is the
ities into a number of procedures: DrawTrench, DrawThumbBar, code that is called when the thumb moves:
RemoveThumbBar, SaveBackground, and WhereIsBar. Therefore,
procedure TSlideBar.SetPosition(A: Integer);
the Paint procedure is quite simple: begin
RemoveThumbBar;
procedure TSlideBar.Paint; FPosition := A;
begin WhereIsBar;
DrawTrench; SaveBackground;
WhereIsBar; DrawThumbBar;
SaveBackground; if Assigned(FOnChange) then
DrawThumbBar; FOnChange(Self);
end; end;
The Paint method will be called every time Windows deter- The RemoveThumbBar procedure places the saved background
mines that the component must be redrawn, and the listed image over the thumb’s current location. Then, the new position
procedures are all that are needed to completely draw the is set (FPosition is the private variable that holds the current
TSlideBar. However, don’t be fooled by the illusion that you thumb’s position). Next, WhereIsBar is called to set the region
can control when the Paint method is called. that the thumb will soon occupy (its new position). The back-
ground image is saved in that region. Finally, the thumb is
There are many times that the procedure will be called and drawn in its new position and calls any defined OnChange event
those calls will be out of your control. For example, your that the programmer might have assigned.
component may be covered by a dialog box or window from
another application. In this case, once the obstruction is By keeping the drawing to a minimum, the TSlideBar will be able
cleared, Windows marks all items that were covered as requir- to quickly update itself in response to mouse and keyboard events.
ing repainting.
Being Resourceful
It’s interesting to note however, that many of the drawing behav- Resources are your friend. Resource files are a way of organizing
iors of the TSlideBar component do not go through the Paint data and storing it in with a program or component. They are
method. In many cases, I don’t want the entire control to be generally used to hold the user-interface elements of a program
repainted, only a portion of it. For example, when the thumb is such as bitmaps, icons, cursors, strings, version information, dia-
moving, forcing the entire control to repaint is not a good deci- log boxes, etc. Developers can also define their own resource
sion. If a user is doing this fast enough, or is dragging the types if necessary (e.g. a proprietary graphics format).
thumb, the control will flicker erratically. This occurs because
each move redraws all the component’s elements. Resource files can be generated with a number of programs such
as Resource Workshop and (to a more limited extent), the Image
So, let’s apply a little common sense to the issue. If the thumb is Editor that comes with Delphi. The Image Editor is limited to
moving, then that should be the only thing that repaints. Right? three basic graphic types: bitmaps, cursors, and icons. Once a
For the most part yes, but there is one problem. When the thumb resource file has been created, it can be compiled with a program
moves, we must be able to repaint the area of the slot that it had or unit so that it is readily available. In a Delphi component, the
just covered. The slot may have focus, and it may have also cov- resource is bound with the unit (.DCU) when it is compiled. In
ered over one or more tick marks as well. At this point you may this way, a component can have access to this data even at design
be thinking that flicker didn’t look so bad after all. time (e.g. the TSlideBar component).
With the TSlideBar, a background bitmap is maintained, in To more efficiently manage system memory, Windows has a cer-
addition to the ThumbBar bitmap. This background bitmap is tain amount of flexibility when it deals with resource data. Even
the same size as the ThumbBar bitmap and will always hold the though resource data is bound with a program or component,
image behind the bitmap. Then, whenever I want to move the Windows will often choose to leave the data on the disk until it
thumb, I follow these procedures: is needed. In addition, by default resources are marked as “dis-
• Place the background image over the current location of the cardable” so that if Windows must, it can release the resource
thumb. and memory it is using. If the program needs the resource again
• Get the background image of the new location of the thumb. later, Windows will reload it. All this is completely transparent to
• Place the thumb in its new location. the programmer and user.
If I stick to this sequence in all situations when the thumb is However, there are some minor performance penalties that are
moving, the control will be doing the minimum amount of associated with allowing Windows to have this kind of flexi-
painting necessary. bility. Clearly, if a resource is being frequently swapped on
and off the hard disk, the user may be able to see the side
Now, instead of refreshing the control after every move of the effects. The program may hesitate very briefly while Windows
thumb, I can do only the procedures that are absolutely neces- retrieves the resource.
Each bitmap included in the resource file also has a mask asso-
ciated with it. A mask is used to make sections of a bitmap
transparent so you can see areas that the bitmap is sitting on.
The thumbs need masks because not all of them are rectangu-
lar. If we try to move a circular thumb around on the form, it
would have four small corners that would be painted along
with it. To solve this problem, a mask is created (see Figure 4).
Refresh; Once we have the cursor linked with the component, all that
remains to do is to obtain a handle to an HCursor, and then
end;
display it at the appropriate time. Since I did not want to force
end; everyone to use this new cursor, I created a Boolean property
called HandCursor. If HandCursor is set to True, the compo-
nent will switch to the custom cursor whenever the mouse
passes over it. If HandCursor is False, the component will use
procedure TSlideBar.DrawThumbBar; whichever cursor was defined in the inherited Cursor property.
var
TmpBmp : TBitMap;
Rect1 : TRect; To prepare for managing the cursor, we need to declare two
begin HCursor variables — one to hold a pointer to the custom cur-
try
{ Define a rectangle to mark the dimensions
sor, and the other to hold a pointer to the original cursor (so
of the thumb } it can be restored). In the component’s private section,
Rect1 := Rect(0,0,ThumbBmp.Width,ThumbBmp.Height); declare two variables:
{ Create a working bitmap }
TmpBmp := TBitmap.Create;
private
TmpBmp.Height := ThumbBmp.Height;
HandPointer : HCursor;
TmpBmp.Width := ThumbBmp.Width;
OriginalCursor : HCursor;
{ Copy the background area onto the working bitmap }
TmpBmp.Canvas.CopyMode := cmSrcCopy;
TmpBmp.Canvas.CopyRect(Rect1,BkgdBmp.Canvas,Rect1); Then we must remove the cursor from the resource file. Since
{ Copy the mask onto the working bitmap with SRCAND }
TmpBmp.Canvas.CopyMode := cmSrcAnd;
a variable was declared to hold a pointer to the new cursor, we
TmpBmp.Canvas.CopyRect(Rect1,MaskBmp.Canvas,Rect1); can load it from the resource in the Create method by using
{ Copy the thumb onto the working bitmap with this line of code:
SRCPAINT }
TmpBmp.Canvas.CopyMode := cmSrcPaint;
HandPointer := LoadCursor(HInstance,'HandPointer');
TmpBmp.Canvas.CopyRect(Rect1,ThumbBmp.Canvas,Rect1);
{ Now draw the thumb }
Canvas.CopyRect(ThumbRect,TmpBmp.Canvas,Rect1); Next, we must find a safe place to grab the original cursor. It
finally happens to work nicely in the WMGetDlgCode procedure, so the
TmpBmp.Free;
end;
following code is added there:
end;
OriginalCursor := GetClassWord(Handle,GCW_HCURSOR);
Figure 5 (Top): The SetThumbStyle method. Figure 6 (Bottom): The It’s important that the original cursor is saved at a point in
DrawThumbBar method. the component’s life where it has a cursor defined.
Otherwise you will be saving garbage. It cannot be done in
Finally, a reference to the resource file in the source code the Create method (where I originally tried it) because the
must be included to bind the resource data to the rest of the cursor is not defined at that point (i.e. before the compo-
component. To do this you use the $R compiler directive as nent is completely created).
follows:
The logical place to make the switch between the original and
{ $R SLIDEBAR.RES } new cursor would be in the MouseMove event handler. After
all, if the control was receiving MouseMove events, that indi-
cates that the mouse is over the component, right? Here’s Conclusion
code to make the switch: Although the complete Object Pascal listing for the TSlideBar com-
procedure TSlideBar.MouseMove(Shift: TShiftState;
ponent was too lengthy to present in this article, there is enough
X, Y: Integer); information here to give you a good sense of how to implement
begin many of these features in your components. [The entire component
if HandCursor then
SetClassWord(Handle, GCW_HCURSOR, HandPointer)
and source is available on diskette and for download. See below.]
else
SetClassWord(Handle, GCW_HCURSOR, OriginalCursor); Delphi provides developers with an extremely powerful and
{ Continue with the rest of MouseMove... }
end;
versatile tool in its component design capabilities. With
these, a programmer can easily develop powerful and feature-
You’re Just Stringing Me Along rich components and controls that rival any of the controls
While completing the TSlideBar component, I had a brain- that are provided in Windows itself. ∆
storm. Much of the time, a slide bar component is used to pick
different textual values from a list. A common approach to this
may be to capture the slide bar’s position when it changes, and The TSlideBar component (including its .PAS and .RES files) is
run that value through a big case statement to obtain a string available on the 1995 Delphi Informant Works CD located in
value that can then be reported in a TLabel on the form. Why INFORM\95\SEP\RV9509.
not allow the component to hold its own strings? To accom-
plish this, I simply added a TStringList object to the compo-
nent in its Create method:
Robert Vivrette is a contract programmer for a major utility company and Technical
FLabels := TStringList.Create; Editor for Delphi Informant. He has worked as a game designer and computer consul-
tant, and has experience in a number of programming languages. He can be reached
Then, I added a write access method to the string list to enable on CompuServe at 76416,1373.
the strings to be edited at design time. Since TStringList is a
complete object, it features a property editor for its strings. By
double-clicking on the Labels property in the Object Inspector,
the component will display the TStringList property editor,
allowing you to enter the strings that the component will report
depending on its current position:
By Gary Entsminger
A Dynamic Toolbar
Using Delphi’s Built-In Events
to Build a User-Configurable Toolbar
Question: Why is it necessary to drag down from the Olympian fields of Plato the fundamental
ideas of thought in natural science, and to attempt to reveal their earthly lineage?
Answer: In order to free these ideas from the taboo attached to them, and thus to achieve greater
freedom in the formation of ideas and concepts.
— Albert Einstein, Relativity, the Special and General Theory
toolbar is a panel of controls, usually located just below the menu bar
In this article, we’ll discuss this assignment problem and others that arise
when creating a dynamic toolbar. We’ll build a toolbar that acts as a gener-
ic “program launcher”.
The sample project (Speedbar.DPR) uses a menu to allow Making the Toolbar Dynamic
users to customize the toolbar. Between application ses- Now things get more interesting (call this Brainstorming
sions, the toolbar’s state is maintained in a table that is 103). We already decided to add and remove SpeedButtons at
loaded each time the application opens. Since one way to run-time and save the toolbar between application sessions.
use the toolbar is as an application launcher, we’ll also Let’s also opt for automatically loading and saving the toolbar.
allow it to optionally be a floating window (i.e. on top of
all other windows). The program uses a table to store the data for the toolbar
between sessions. How about storing the SpeedButtons them-
The User Interface: A Form and Components selves during each session?
In Delphi, building the user interface is easy. From the default
project, add the following components from the Component Each time we create a SpeedButton, Delphi allocates memo-
Palette to a form: ry for it. If a user removes a SpeedButton from the toolbar,
• Menu from the Standard page we want to recover that memory as soon as possible. To
• Panel from the Standard page recover that memory we use the SpeedButton’s built-in Free
• OpenDialog from the Dialogs page method. The only catch is that we must know which
• Table from the Data Access page SpeedButtons to free — that is, which SpeedButtons were
created at run-time. We can keep track of SpeedButtons by
We’ll use the Menu component to allow users to issue com- maintaining them in a list, using Delphi’s nifty, ready-made
mands. The Panel will contain the SpeedButtons users add at solution — the TList object.
run-time. The OpenDialog component makes it easy for
users to select files to execute, and bitmaps for the At the beginning of each session, we’ll create a list of
SpeedButtons. The table will preserve the toolbar’s state SpeedButtons. Then, each time a user adds or removes a
between application sessions. SpeedButton, we’ll update the list. If we need to remove all
SpeedButtons, we’ll iterate through the list, removing them
Edit the MenuItems Property one by one. We’ll use a list instead of an array because a list is
Double-click the Menu component to open the Menu dynamic like the toolbar. We don’t know how many
Designer. This dialog box enables you to edit the Menu SpeedButtons a user might add to a toolbar, and we don’t have
component’s MenuItems property. First, add two items to the to specify a list size at design time.
menu bar with captions &Buttons and &Form. (The amper-
sand indicates that the letter following the ampersand will be If you think maintaining a list of SpeedButtons might get expen-
the underlined shortcut key. For example, AB will select sive (in terms of memory and other resources), relax. Every
the Buttons menu item.) Object Pascal object (and therefore every component, such as a
SpeedButton) is really a pointer, a variable containing the address
Under the Buttons item add the Add button (&Add of a memory location. Therefore, a list of objects is really a list of
button) and Remove button (&Remove button). Under pointers to objects, not a list of the objects themselves.
Form add Normal (&Normal) and Always on top (&Always
on top). Figure 2 shows the form under development with When should loading and saving occur? A good time to load
the Menu Designer open. When you’re done, close the is when the Form.OnCreate event procedure executes. Saving
Menu Designer. could occur when the application terminates, or each time a
user adds a new SpeedButton to the toolbar. Let’s opt to save
the toolbar each time it is modified. This way, the table will
always be current.
NewSpeedButton: TSpeedButton;
GetButtonsFromTable
ButtonNum : Integer; The GetButtonsFromTable procedure opens the Speedo.DB
ButtonList : TList; table, moves to the first record in the table, reads the Hint
RemoveButton : Integer; { test flag } and Glyphfile fields, and then creates new SpeedButtons
based on those fields until the end of file is reached.
These variables will be visible throughout the unit:
• NewSpeedButton is a TSpeedButton component. Note that the procedure uses a try...finally block. It’s used
• ButtonNum is a SpeedButton counter. because at least three things can go wrong in this procedure:
• ButtonList maintains the list of SpeedButtons on the toolbar. the table open can fail, the table read can fail, and the
• RemoveButton is a flag that enables a SpeedButton click CreateNewSpeedButton procedure can fail. If any of these fail-
event to perform differently depending on how the flag is ures occur, we’d like to ensure that the table is closed. Note
set. (We’ll discuss this flag in detail later.) that if we try to close a table that isn’t open, it’s no problem
(no error occurs).
The complete listing of the sample application is shown in
Listing One beginning on page 21. Please refer to it as we Why use a try...finally block as opposed to a try...except
step through the code and discuss its more interesting aspects. block? In a try...finally block, the finally part of the block is
always executed. No matter what happens in
GetButtonsFromTable, we want to close the table.
response your application makes by attaching code to the (Note that this code assumes the bitmaps used are all 26
OnClick event procedure for the SpeedButton. pixels in width. You must make the appropriate adjustment
to the assignment statement for NewSpeedButton.Left if
But how do you specify behavior for an OnClick event proce- you are using bitmaps of other widths. This code also
dure that you’re adding at run-time? You can assign an assumes that you’re using the 2-image bitmaps supplied in
OnClick event procedure to any TNotifyEvent type that has the \Delphi\Images\Buttons directory as the glyphs.)
the following form:
The AddButtonsClick procedure handles the chores of display-
type ing a file browser dialog box to allow the user to associate a
TNotifyEvent = procedure(Sender: TObject) of object;
file with a button (see Figure 5). It then displays a browser
again so the user can select a glyph for the button.
Sender indicates the object that generated the event, and pro-
cedure can be any procedure. You create specific TNotifyEvent
types for any descendent of TObject (e.g. menu items,
SpeedButtons, etc.) by specifying the Sender type:
type
TSpeedButtonNotifyEvent =
procedure(Sender: TSpeedButton) of object;
var
EventName: TSpeedButtonNotifyEvent;
begin Figure 5: A file browser dialog box is displayed when Buttons | Add
EventName := GenericSpeedButtonClick; button is selected. In this manner, the user can associate a file with
NewSpeedButton := TSpeedButton.Create(Self);
the button.
NewSpeedButton.OnClick := TNotifyEvent(EventName);
end;
GenericSpeedButtonClick
This sequence is the crux of the CreateNewSpeedButton proce- At the heart of the CreateNewSpeedButton procedure is the
dure. It begins with a local var declaration of a GenericSpeedButtonClick procedure. It uses the Sender para-
TSpeedButtonNotifyEvent. It then uses a try...except block to meter to determine which object (i.e. which SpeedButton)
attempt the creation of a new SpeedButton. Why use a generated the event. In the sample application, the Sender
try...except block? The statement: is everything. If we know the Sender, we know which
SpeedButton’s corresponding Hint property to read. Also,
NewSpeedButton.Glyph.LoadFromFile(GlyphFile);
as mentioned earlier, Hint contains the file to execute.
attempts to load a bitmap from a file. It’s possible that this GenericSpeedButtonClick is a bit complex because of the
file doesn’t exist or contains an error. The try...except RemoveButton flag (variable) mentioned (and created) earli-
block tests for an EInOutError in the except part of the er. While designing this application, I encountered a prob-
block. [For more information regarding try...except blocks lem. How could I provide a way for users to remove a spe-
and exception handling, see Gary Entsminger’s article cific SpeedButton? One possibility was to show the user a
“Exceptional Handling” in the June 1995 Delphi list of available SpeedButton hints. For example, after the
Informant.] user selects a hint from the list, that hint could be matched
to its corresponding SpeedButton and deleted. This works,
The CreateNewSpeedButton procedure creates a new but required more work than I preferred.
SpeedButton, sets the properties of several SpeedButtons,
including the location (the Left property) of each SpeedButton Therefore, I devised a simple, quicker alternative. The user
based on the current button number and the Hint property, and selects a button to remove by clicking on it. However, we
adds the newly created SpeedButton to the button list. It then also want the user to be able to click on a SpeedButton
specifies an event name (GenericSpeedButtonClick), and sets the and execute a file. That’s where the RemoveButton variable
SpeedButton OnClick event to the GenericSpeedButtonClick. The comes in. If the user selects Buttons | Remove button, the
try...except block ensures that if there’s a problem with any of RemoveButton flag is turned on. Otherwise, it’s off and the
these details that the application can recover. toolbar executes the file.
{ Form open and close events } { Use the existing key to find the item to delete;
{ When the form opens, create a list to hold the or alternatively define a key here. }
SpeedButtons, reset the button count to 0, and procedure TForm1.DeleteItemFromTable(Command: string);
get previous session's buttons from a table. } begin
procedure TForm1.FormCreate(Sender: TObject); Table1.Open; { Open table }
begin Table1.FindKey([Command]); { Use existing key }
ButtonList := TList.Create; { Create SpeedButton list } Table1.Edit; { Hint field is the key }
ButtonNum := 0; { Delete this item, button.hint, from table }
GetButtonsFromTable; Table1.Delete;
end; Table1.Close; { Close table }
end;
{ When the form closes: { Generic SpeedButton click can either set up a new
* release the memory allocated to the button list. } SpeedButton toolbar or execute a file }
procedure TForm1.FormClose(Sender: TObject; procedure TForm1.GenericSpeedButtonClick(
var Action: TCloseAction); Sender:TSpeedButton);
By Thomas Miller
Leaving PB Behind
Delphi vs. PowerBuilder: A Blow-by-Blow Comparison
T through telepathy, debug the program, and automatically write the docu-
mentation and on-line help. It would compile and optimize the program
in Assembler, be operating-system-level compliant with Windows, Windows NT,
Macintosh, OS/2, and 10 flavors of UNIX. It would also support all databases
through native APIs, and integrate seamlessly with workgroup products.
Back to reality. We buy programming tools to avoid tedious programming in C/C++ and
assembler and facilitate timely delivery of software. Currently however, there are no full
fledged enterprise development tools on the market.
We may begin to see these all-encompassing tools that support multiple platforms, multiple
databases, group productivity, and rock solid compiled code in two or three years. For now,
however, PowerBuilder 4.0 (PB) from PowerSoft, Inc. is “king of the hill”. Borland’s Delphi is
the new kid in town and a more-than-able challenger. Yet, some developers are apprehensive
about using Delphi for several reasons: 1) Delphi is a new product, 2) Borland has had its
much-publicized problems, and 3) PB is well-accepted.
It’s my contention that Delphi is the best Windows development tool
on the market. It’s fast, easy-to-use, and has the world-renowned Pascal
language in its pedigree. For those who aren’t convinced of Delphi’s
capabilities, this article is for you. It’s time for you to leave PB behind.
Painters and Palettes
PB’s integrated development environment (IDE) is divided into
“Object Painters” (see Figure 1). These are the building blocks used
to create an application. On the other hand, Delphi uses forms and
an object palette metaphor (the Component Palette) for development
(see Figure 2). As any good programmer will tell you: “I can get the
system to do anything you want.” For the purposes of this discus-
sion, however, only those functions that are specifically designed into
each development system are being compared.
Let’s compare PB to Delphi using PB’s painters as the “jumping off point”.
PB’s Database Painter is one of the best tools available for cre- because it creates its own tables in each of the databases. This
ating a database from scratch. It supports indexes, keys, and makes it difficult to “point” to new databases “on-the-fly”.
extended column attributes. If you have a system that is set in The advantage with extended attributes is that a lot of data
stone, the extended attributes are wonderful. validation rules can be done at database setup time.
If you’re in a rapid application development situation (i.e. This is a perfect example of acting object-oriented versus
making little changes all the time), the modifications you being object-oriented. Delphi lacks a database table mainte-
make to existing extended attributes are not automatically nance tool. Both systems have capable browsers.
updated to objects that already use the extended attributes.
Suppose you create an extended attribute for an Employee The PB Database Profile Painter: This allows you to set your
Type drop-down list box. When you originally set the extend- database connection information. Most databases require a white
ed attribute there are three employee types. The extended paper to implement. The Borland Database Configuration Utility
attribute is then associated to three different windows. Later, is simple and straightforward. PB’s database connection is difficult
the human resource department tells you that there are four to set up, and not all the ODBC drivers work consistently. PB
Employee Types. You fix the extended attribute, recompile the does include distributable ODBC drives. With Delphi you will
program, and drop down the Employee Type drop-down list have to purchase them separately. There have been very few com-
box to find only three types. You are required go to each con- plaints about configuring the Borland Database Engine (BDE).
trol and manually refresh it before compiling.
The PB Query Painter (4.0) features an updated interactive
The extended attributes are maintained in the active database wizard to assist in creating a SQL statement (see Figure 5).
type. If you start off with Sybase, this makes it almost impos- The Query Painter is well-integrated with other data manip-
sible to use the data window for Oracle. PB stores informa- ulation tools. Delphi’s Visual Query Builder is an interactive
tion about databases in its own system catalogs. It creates cat- utility that assists in creating a SQL statement (see Figure 6).
alogs in the actual database. For example, if you are using PB has designed an intuitive and easy-to-use interface.
Sybase, it creates its system tables in the Sybase database. This Delphi’s is somewhat complex to use, even with the manuals.
is somewhat similar to setting up aliases for databases in the
BDE, but is more messy and requires more set-up work The PB Pipeline Painter allows translation from one vendor
database to another. Delphi’s BatchMove Component also
allows translation from one vendor database to another. PB
and Delphi are functionally 95 percent the same.
Figure 3 (Top Left): PB’s The PB Function Painter allows for graphically registering a
Preferences Painter. function. Delphi has no graphical “wizard” to aid in register-
Figure 4 (Bottom): ing functions. (As stated earlier, Delphi does not facilitate any
Delphi’s Environment graphically-assisted coding.)
Options dialog box.
base layout tool on the market. It does all the coding for you
in the background, prints reports, and allows easy sorting and
filtering. For a very simple, direct data access window, it’s
tops. Now let’s look at the downside.
The main reason, however, is that many PB events do not Script (code) in PB is easy, direct, and conforms to industry
interact well with each other. As an illustration, set up a sim- standards. If you are familiar with BASIC, Visual Basic, C,
ple DataWindow attached to an empty table. Next, place C++, FORTRAN, or dBASE, you can quickly settle into the
message boxes in each of the following DataWindow’s events: syntax provided by PB. PB provides over 600 functions, allows
EditChanged, ItemChanged, ItemFocusChanged, and you to add additional functions through registering external
RowFocusChanged. As you add the first row to the DLLs, and allows access to the Windows API. FUNCky
DataWindow, the events will fire off in one order. As you add Library, available from PowerSoft, is an add-on function pack
the second row to the DataWindow, the events will fire in a that provides more than 500 functions.
second (and different) order, and when you delete the two
rows to leave the DataWindow blank, the events fire in a Client/Server Data Access
third, different order. For quite a while, PB has been renowned for its database
access (the DataWindow facilitates this access). The basic
GetFocus (pre-event) and LoseFocus (post-event) are two crit- database access of the DataWindow has not changed since
ical types of events in Windows programming. Remove these Version 2 was released more than three years ago.
two event types from an event-driven system and you have a
structured system (DOS). PB has a major problem triggering PB replicates data from the server database to the workstation.
post events. The DataWindow is an object with related pre- This is the first problem. Client/server database engines were
events and post-events, and data fields are not objects and do specifically designed to return blocks of data as needed. Let’s
not directly support any events. This is another example of say you send a query to the database that has a 5,000 record
acting object-oriented instead of being object-oriented. result set. In PB, it will return all 5,000 records to the local
workstation before any operations can be performed on the
As an example, set up a simple DataWindow with a field data. RetrieveAsNeeded (a function that only returns enough
requiring verification against a second table. (An order form data to fill the screen) is simply a ruse to make the screen look
with part numbers.) In the ItemChanged, ItemFocusChanged, active. Then, in a separate database call, PB requests the rest of
and LoseFocus events, enter the following script: the data. Either way, you often have to wait as long as 15 min-
utes until you can access the data set on the screen.
if dw_1.GetColumnName() = "Part_Number" then &
MessageBox("EventName","Verify Part Number in Parts Table")
Delphi doesn’t directly support sorting or filtering. This is
a mixed blessing. Ideally, you want to sort and filter at the
Now add a button to the window. We’ll use this to change
server to save network overhead and not burden the work-
focus from the DataWindow to the Button. The message box
station. On the other hand, you cannot dynamically
represents a subroutine to verify that the entered part number
change the sorting or filtering without re-querying the
exists and to retrieve associated data from the parts table (i.e.
database. This will depend on the functionality required by
price, cost, description, etc.) Enter a part number in the field
your application.
and click the button. What happens? Nothing. Let’s see why.
Most client/server systems are designed to return a specific
PB does not run any code unless an object has focus. Using
amount of data chunks, thereby optimizing data access.
this premise, we’ll analyze why each event failed to give us the
Suppose you have a Novell Network on an Ethernet architec-
desired result:
ture running Oracle. If you set the Ethernet Packet size to
• ItemChanged Event doesn’t run because
16K, the Novell disk block size to 16K and the Oracle work-
GetColumnName doesn’t return Part_Number because
station requester data retrieve parameter to 128K, you have
the button has focus.
just optimized the transport of data from the server to the
• ItemFocusChanged doesn’t run because GetColumnName
workstation. Assume our 5,000-record query represents
doesn’t return Part_Number because the button has focus.
2000K of data. When the workstation reaches the last record
• LoseFocus doesn’t run because when the code runs, the
in the first “chunk” of data returned (128K), it then requests
button has focus and GetColumnName doesn’t return
the next chunk of data (128K). This increases overall speed,
Part_Number.
reliability, and decreases the possibility of the workstation
data set getting out of sync with the server database.
Some of you may ask: “Why aren’t you using the EditChanged
event?” This would work, but consider that for each keystroke,
New technology and other products have easily eclipsed PB’s
the system would verify the part number and display a dialog
capabilities. ODBC is now a mature API and supported by all
box with the message “Part number not on file”. A 10-charac-
popular development environments and database systems.
ter part number would return nine error messages. In short,
Many companies have developed general database access engines
PB’s inability to deal with post events is a major problem and
that are flexible and can be attached to multiple development
totally unacceptable in an event-driven environment.
environments.
TabStop := False
PB’s event handling is average at best. Not all items have events
and many events don’t run properly. Delphi’s true object-orient-
ed paradigm has no problems with pre-events and post-events,
or with multiple events interacting with each other. You are free
to program 10 or more events related to one object. Overhead
isn’t an issue with Delphi because the code is compiled into a
true .EXE that will run up to 20 times faster than PB’s p-code.
Delphi’s data access is faster, and more reliable than PB’s. It Figure 11: An example of an Object Pascal if statement. Notice the
requires substantially less coding to set up database access. first end keyword is not followed by a semicolon.
Large project support is not currently available in Delphi. Delphi is an incomplete development environment and is
You can put modal windows in DLLs which limits you to missing a database structure maintenance tool. The included
search windows, message boxes, and other inconsequential report writer is average at best and the SQL-related tools are
windows. Your more important main interface window complex and difficult to use. However, Delphi’s core pro-
(MDI Child) cannot reside in a DLL. This is best gramming elements (Windows layout, programming lan-
described by an excerpt from one of the demo programs guage, and database access) are excellent. I would pit Delphi’s
shipped with Delphi: “Note that the TDllForm1 form is capabilities in these areas against any other tool on the mar-
always used modally. Borland does not recommend using a ket. Compared to PB’s system-wide problems, Delphi only
modeless form from a DLL because the OnActivate and has five weaknesses in its core area that need to be addressed:
OnDeactivate mechanisms do not work when control is • TQuery objects should initiate faster.
transferred between a DLL-owned form and another DLL • The Query component must be more flexible and allow
or EXE owned form.” In English, this translates to: “The for procedural SQL sub-routines and database cursors.
form called from a DLL does not recognize the parent • Large projects must be supported better by allowing large
form in the EXE as its (the DLL form’s) parent form.” EXEs to be divided into libraries, or DLLs should be
Theoretically, it is possible for two modeless windows to allowed to recognize the forms in EXEs as the parent. This
have focus at the same time. This DLL problem needs to will make systems easier to manage, and increase code reuse.
be fixed, or Delphi needs to support an old-but-reliable • A more well-rounded grid component.
way to split up an EXE file — libraries. • Better OLE support.
Object Pascal takes some getting used to. For example, the Also, PB’s reporting capability is good and Delphi’s is average
Object Pascal case statement only supports ordinal variable at best. (Crystal Reports by Crystal is an excellent reporting
types. It’s disappointing that Object Pascal case statements tool and can be used with either product.) I expect some of
don’t support string variables. Also, as you program events in the other holes to be filled when the next version of Delphi is
the source code file, the events are added at the end of the released. Meanwhile you can use PB’s Database Painter until
file. When you get 2000 lines of code, you are jumping all Delphi has a similar utility.
over the file trying to find related code (the code compiles
fine). The colon/equal sign combination ( := ) is the assign- If your SQL is only average and you rely on wizards to help
ment operator, while the equal sign ( = ) is strictly a compar- with SQL statements, get a book and learn how to program
ison operator. On the positive side, once you become com- in SQL. No matter how good the wizard is, you will eventu-
fortable with the different syntax, Object Pascal is very ally have to write a complex SQL statement by yourself.
robust. In addition, there is a lot of available shareware and
third-party support to extend Object Pascal’s capabilities. There isn’t a complete enterprise development tool available,
but for core database programming prowess, Delphi is the
Conclusion new “king of the hill”. It’s fast, provides easy database access,
On paper, PB should easily outclass Delphi. It’s a well-round- a completely object-oriented programming environment, and
ed development environment with easy-to-use, intuitive tools features reliable code generation and complete extensibility. ∆
(i.e. its Painters). Once you start peeling back the pretty face,
however, PB doesn’t look as good. Slow database access, par-
tial support for some databases, a slow p-code interpreter, Thomas Miller is President of Business Software Systems, Inc., a consulting firm spe-
compiler problems, and events that don’t run properly (or at cializing in implementing accounting, distribution, manufacturing, and business man-
all), are just some of the system-wide problems. In short, PB agement systems. They are currently working on their own distribution system written
doesn’t need more functionality to capture the hearts of pro- in Delphi and supporting Oracle, Sybase, and Btrieve back-ends. You can reach
Thomas at 76652,2065.
grammers. What’s already there just needs to be fixed!
ast month’s article introduced the basics of data validation, and explored
Table-Level Validation
Validation can be produced in a number of ways when data-aware controls are involved. The first
line of defense against invalid data lies in the data tables themselves. At a minimum, the data type
of the individual fields in a table prohibit data of an incompatible type from being entered.
For example, a table will not allow letters to be entered into a field when the field type is numeric.
Likewise, fields that are of the type Date or Time require that the entered data conform to partic-
ular characteristics. By comparison, an Edit component accepts any type of alphanumeric data.
The validation provided by table field type is provided by descendants of the TField compo-
nent. These types can easily be seen when you instantiate (create) fields using the Fields
Editor. Figure 1 shows the VendNo field selected in the Fields Editor.
This field also appears in the Object Inspector, where you will notice
that it is a TFloatField component. By default, this component accepts
only numeric values.
Even when you do not explicitly instantiate your fields, Delphi repre-
sents the fields using TField components. Like those you instantiate,
the purpose of these components is to provide the first line of defense
against obviously invalid data.
However, the data type of the fields in a table provide only limited pro-
tection from unacceptable data. For example, while a field may have a
data type of Date and a name of DateOfBirth, unless further steps are
taken, any valid date can be entered. For instance, if the table is
designed to hold the names of a company’s current employees, it’s
unlikely that the date 12/31/1065 would be considered acceptable.
Field-Level Validation Now select the DBGrid and set its DataSource property to
There are two basic approaches to field-level validation in DataSource1. The contents of the Vendors table should appear
Delphi applications. One is to set the properties of the in the DBGrid. Finally, select the DBNavigator and also set
field so invalid data cannot be entered, and the second is its DataSource property to DataSource1.
to use code to check the data after it has been entered.
These approaches are rarely exclusive. You will often find It is now time to instantiate the fields of the Table compo-
yourself applying both techniques, sometimes even to the nent. Double-click the Table component to display the
same field. Fields Editor. (Alternatively, you can select the Table compo-
nent, right-click it, and select Fields editor from the pop-up ing table. When a 1 appears in this position (as it does in the
menu.) Select the Add button from the Fields Editor dialog example mask) the literal characters are saved to the table.
box. When the Add Fields dialog box is displayed, all fields
should be highlighted. Select OK to instantiate a field object The final position includes a single character. This character is
for each of the fields in the Vendor table. used in the mask to refer to blank spaces. The underscore
character ( _ ) is identified as the blank character in the mask
Validating Fields Using Properties used in this example. (Note that the third part of the mask is
The primary properties that you use to control field-level vali- required even though this particular mask does not contain
dation are EditMask and Required. You can use the EditMask blank characters.)
property to provide an input template when the field requires
data in a particular format. This usually applies only to fields Select the Table1VendorName object from the Object
that hold highly structured data such as dates, times, zip Inspector. Set the Required property to True. When the
codes, phone numbers, and other similar data. On the other Required property is set to True, the field cannot be left blank.
hand, the Required property can be used by any field that If you do leave the field blank and then attempt to post the
accepts user input. record, Delphi will generate an exception.
To demonstrate the use of both the EditMask and Required Beware that using the Required property for an instantiated
properties, use the Object Inspector to select the object named field does not alone ensure that valid data will be entered.
Table1Phone. Set the EditMask property to the following value: Let’s say a field includes an EditMask component that con-
tains literal characters (such as the dash characters in the
!000-000-0000;1;_ mask we’re using in this example). If the user changes that
field, but then erases the entered characters, Delphi does
In this example you entered the EditMask property directly. not consider the field to be blank since the literal characters
Instead, you could have opened the property editor for the appear there. Consequently, although a valid value has not
EditMask property by clicking the ellipsis that appears when been entered, and the field looks as if no value was entered,
the EditMask property is selected. The EditMask property edi- the Required property will not produce an exception.
tor is the Input Mask Editor dialog box shown in Figure 4.
You’re done. Compile the form and run it. Press I to
Using this property editor you can select from a set of pre- insert a new record. Enter the value 1 in the VendorNo field.
defined edit masks. By clicking the Masks button on this dia- Next, press b to attempt to leave the record. An exception
log box you can load a country-specific mask file (.DEM) for is generated by the Required property as shown in Figure 5.
international applications. (Note: If you run this from the integrated development envi-
ronment — the IDE — and Break On Exception is checked
Whether you create a mask, or use one from the Input Mask on the Preferences page of the Environment Options dialog
Editor, the EditMask has three parts, each separated by semi- box, press 9 to continue running after the exception.)
colons. In the first part of this edit mask, the exclamation
point, specifies that leading blanks will not be saved. The 0 Following
character specifies that a number is required in that position, the excep-
while the dash character ( - ) specifies that a dash will appear tion, enter a
in those exact positions in the field. name in the
VendorNo
The second part of this EditMask can contain only the number field. Now
0 or 1. If the number 0 appears there, literal characters within press F
the mask appear in the field, but are not saved to the underly- until you
arrive at the
Phone field.
Notice that
a mask
Figure 5: When you leave the VendorName field
appears in blank and then attempt to move to or insert another
this field. record, this exception is raised. This occurs because
Enter the 10 this field’s Required property is set to True.
characters of
a phone number, beginning with an area code. Now press b
to leave the field. Since both the Required property on
Table1VendorName field and the EditMask property in the
Table1Phone field are satisfied, the new record is posted. Close
Figure 4: The Input Mask Editor dialog box. the form to return to Delphi.
Begin by selecting Table1VendorNo from the Object Compile the program and run it. Insert a new record,
Inspector. Go to the Events page of the Object Inspector and entering a positive vendor number and a vendor name.
select OnValidate. Enter the following code into the Then press F to move to the field Address2 and enter an
OnValidate event handler: address. Next, attempt to leave the record by pressing b
or I. This will trigger the BeforePost event handler.
if Table1VendorNo.Value < 1 then
raise Exception.Create('Positive Vendor number
Your code will detect that Address1 is blank and that
required'); Address2 is not, and will display the error dialog box
shown in Figure 7. After you accept this dialog box, the
Run the program, insert a new record, and enter a value of Abort procedure generates a silent exception.
less than one (e.g. -1) in the VendorNo field. When you press
F to leave the field the exception is generated, displaying Like a raised exception, a silent exception prevents the
the message shown in Figure 6. (Again, if you’re running this record from being posted. The main difference is that a
code from the Delphi IDE, the above note regarding Break silent exception does not display a dialog box. In this exam-
On Exception applies.) ple, a custom dialog box was displayed using the MessageDlg
function. The primary advantage of raising a silent excep-
Record-level validation is applied before the entire record is tion is that you can use any dialog box, and even associate a
posted to a table. As mentioned earlier, an exception is raised custom dialog box with a help context from your applica-
automatically if the record being posted violates one or more tion’s help file. When you raise an exception, the standard
requirements of the table to which the record is being posted. Delphi exception dialog box is displayed, and you have no
You can also use custom code to evaluate the record, and raise control over its appearance and help context.
By Charles Calvert
Strings: Part II
Stripping and Manipulating Object Pascal Strings
Stripping Blanks
A classic problem is the need to strip blanks off the end of a string. Consider the code frag-
ment in Figure 1.
At first glance you might expect this code to print the number 2.03 to the screen. However, it
will not because Str2Real cannot handle the extra spaces appended after the characters 2.03.
It’s quite likely that a problem similar to this could occur in a
uses real-world program. For instance, a programmer might ask the
MathBox;
user to enter a string, and the user may accidentally append a
procedure TForm1.Button1Click(Sender: TObject); series of blanks to it (or perhaps the extra characters were
var added by other means). To ensure that your program will run
S: string;
R: Real;
correctly, you must strip those extra blank characters from the
begin end of the string.
S := '2.03 ';
R := Str2Real(S);
WriteLn(R:2:2);
The StripBlanks function (see Figure 2) can be used to remove
end; space characters from the end of a string. StripBlanks will not
change the string that you pass into it, but creates a second
string that it passes back to you as the function result. This
function StripBlanks(S: string): string;
var
means you must use this function in the following manner:
i: Integer;
begin S2 := StripBlanks(S1);
i := Length(S);
while S[i] = ' ' do begin
Delete(S,i,1);
where S1 and S2 are both strings. You can also write code that
Dec(i); looks similar to this:
end;
StripBlanks := S; S1 := StripBlanks(S1);
end;
StripBlanks has one local variable, i, which is an integer:
Figure 1 (Top): In this code example, the first line in the begin sec-
tion contains characters that need to be stripped. Figure 2 (Bottom): var
The StripBlanks function. i: Integer;
This variable is set to the length of the string passed to the Programmers often end up making reports or gathering
function: data on a daily basis. For instance, I sign onto an on-line
service nearly every day, and frequently need to store the
i := Length(S); information I glean from cyberspace in a file containing
the current date.
The Length function is one of the more fast and simple rou-
tines in the Object Pascal language. In effect, it does nothing In other words, if I sign onto CompuServe and download
more than this: the current messages from the Delphi forum, I don’t want
to store that information in a file called DELPHI.CIS. I
function Length(S: string): Integer;
begin want a file name that includes the current date, so I can
Length := Ord(S[0]); easily tell what files were downloaded on a particular day.
end; In short, I want to automatically generate file names that
look like this: DE022595.TXT, PA022695.TXT,
In short, it returns the value of the length byte that is the first DE022795.TXT, and so on, where 022795 is a date of the
character of a string. [See last month’s article for a discussion type MMDDYY.
of the length byte.] The next line in the StripBlank function
checks the value of the last character in the string under The GetTodayName function (see Figure 3) fits the bill.
investigation: This function takes two parameters: a two-letter prefix,
and a three-letter extension, and creates a file name of the
while S[i] = ' ' do begin
type we’ve just discussed. The function begins by calling
the built-in Pascal function GetDate, which returns the
More explicitly, it checks to see whether it is a blank. If it is a
current year, month, day, and day of week as Word values.
blank, the following code is executed:
If the date were Tuesday, March 25, 1994, the function
Delete(S,i,1); would return the following:
Dec(i);
Year := 1994
Month := 3
The built-in Delete function takes three parameters. The first is Day := 25
a string, the second is an offset into the string, and the third is Day-of-Week := 2 { 0 = Sunday }
the number of characters you want to delete from the first
parameter. In this case, if you passed in the string 'Sam ', Assuming that the user of this function passed in DE in the
which is the name “Sam” followed by three spaces, the last PRE parameter, and TXT in the EXT parameter, it would be
space would be lopped off so that the string would become fairly easy to use the IntToStr function to create something
'Sam ', where “Sam” is followed by two spaces. like this:
There are several problems with this result, the biggest being 1900 from the date. However, that sound of hoofbeats in
that it is 12 characters long — too long for a legal DOS file the distance is the rapid approach of the year 2000.
name. To resolve the problems, we need to change the
month to a number such as 03, to keep the day as 25, and Subtracting 1900 from 2001 would not achieve the desired
to strip the 19 from the year: result. The code therefore first converts the year into a
string, then simply lops off the first two characters with the
DE032594.TXT Delete function:
S := '0' + S; Given this array, the following command will set all the
members of this array to #0:
In the case of the GetTodayName function, the value passed in
the Len parameter is 2, because we want to translate a num- FillChar(MyArray,SizeOf(MyArray),#0);
ber such as 3 or 7 into a number such as 03 or 07.
If you want to fill the array with spaces, you could use the
The final trick in the GetTodayName function is to convert following statement:
a year such as 1994 into a two-digit number such as 94.
Clearly, this can be easily achieved by merely subtracting FillChar(MyArray,SizeOf(MyArray),#32);
This code would fill the array with the letter “A”: This code first sets S1 to a string value. It then indexes 12
bytes into that string and moves the next four bytes into a
FillChar(MyArray,SizeOf(MyArray),'A'); second string. Don’t forget to count the spaces when you
are adding the characters in a string. (And don’t forget
The key point to remember when you’re using FillChar is that the first byte is the length byte.) Finally, it sets the
that the SizeOf function can help you ensure that you are length byte of the second string to #4, which is the num-
writing the correct number of bytes to the array. The big ber of bytes that were moved into it. After executing this
mistake you can make is writing too many bytes to the code, the final statement assigns the word “Bees” to
array — this is much worse than writing too few. If you Edit1.Text. This statement accomplishes the same task using
think of the memory theater example we covered in last the Copy function:
month’s article, you can imagine ten members of the audi-
ence sitting together, all considering themselves part of S1 := 'Heebee Gee Bees';
MyArray. Right next to them are two people who make up S2 := Copy(S1, 12, 4);
WriteLn(S2);
an integer. They are busy remembering the number 25.
Now you issue the following command:
The first parameter to Copy is the string you want to get data
FillChar(MyArray, 12, #0); from, the second is an offset into that string, and the third is
the number of bytes you want to use. The function returns a
All the people who are part of MyArray will start remem- substring taken from the string in the first parameter.
bering #0, which is fine. However, the command will keep
right on going past the members of MyArray and tell the The Copy function is easier to use and safer than the Move
two folks remembering the number 25 that they should function, but it is not as powerful. If at all possible, you
both now remember #0. In other words, the Integer value should use the Copy function. However, there are times
will also be “zeroed out” as well, and a bug has been intro- when you can’t use the Copy function, particularly if you
duced into your program. You should understand that the need to move data in or out of at least one variable that is
result described here is a best-case scenario. The worst case not a string. Also, it is worth remembering that Move is
scenario is that the extra two bytes belong to another pro- very fast. If you have to perform an action repeatedly in a
gram. This means that your program will generate a loop, you should consider using Move instead of Copy.
General Protection Fault (or GPF). The moral is that you
should always use the FillChar procedure with care. As easy as it is to write data to the wrong place using the
FillChar statement, you will find that the Move statement can
Copy or Move It lead you even further astray in considerably less time. It will,
A function similar to FillChar is called Move. Its purpose is to however, rescue you from difficult situations, provided you
move a block of data from one place to another. A typical use know how to use it.
of this function might be to move one portion of a string to a
second string, or to move part of an array into a string. The Moving On
Copy function can also be used for similar purposes. The The following function puts the Move procedure to practical
advantage of the Copy function is that it is relatively safe. The use. As its name implies, the StripFirstWord function (see
disadvantages are that it is less flexible, and can be slower Figure 5) is used to remove the first word from a string. For
under some circumstances. instance, it would change the following string: 'One Two
Three', into 'Two Three'.
Move takes three parameters. The first is the variable you want
to copy data from, the second is the variable you want to move The first line in this function introduces you to the built-in
data to, and the third is the number of bytes you want to move: Pos (position) function, which locates a substring in a longer
string. In this case for instance, the Pos function is used to
procedure Move(var Source, Dest; Count: Word); find the first instance of the space character in the string
passed to the StripFirstWord function. The function returns
The following code is an example of a typical way to use the offset of the character it is looking for.
Move. If you enjoy puzzles, you might want to take a moment
to see if you can figure out what it does: More specifically, the Pos function takes two parameters. The
first is the string to search for, and the second is the string
procedure TForm1.Button1Click(Sender: TObject); you want to search. Therefore the statement Pos(#32,S)
var
S1,S2: string; looks for the space character inside a string called S.
begin
S1 := 'Heebee Gee Bees'; If you passed in this line of poetry — “The pure products of
Move(S1[12], S2[1], 4);
S2[0] := #4; America go crazy” — the Pos function would return the num-
Edit1.Text := S2; ber 4, which is the offset of the first space character in the sen-
end; tence. However, if you passed in a simpler string such as
The built-in Exit procedure simply exits the function without Conclusion
executing another line of code. This is the StripFirstWord Next month, we’ll talk more about the StripFirstWord func-
function’s sole, and rather limited, exercise in error checking. tion, and explore parsing the contents of a text file and con-
verting the data into fundamental Delphi types.
If the offset of a space character is returned by the Pos function,
the Move function transfers an “offset” number of characters This article was adapted from material for Charles Calvert’s
from the string that is passed in to a local string named S1: book, Delphi Programming Unleashed, published in 1995 by
SAMS publishing. ∆
i := Pos(#32, S);
...
Move(S[1], S1[1], i);
S1[0] := Chr(i-1); Delphi projects that demonstrate the principles discussed in this
article are available on the 1995 Delphi Informant Works CD
The second line of code sets the length byte for the newly created located in INFORM\95\SEP\CC9509.
string that contains the first word in the sentence. The next three
lines of code excise the first word from the original sentence:
Charlie Calvert works at Borland International as a manager in Developer Relations.
Size := (Length(S) - i);
Move(S[i + 1], S[1], Size);
He is the author of Delphi Programming Unleashed, Teach Yourself Windows
S[0] := Chr(Size); Programming in 21 Days, and Turbo Pascal Programming 101. He lives with his
wife, Marjorie Calvert, in Santa Cruz, CA.
By Richard D. Holmes
A Stopwatch Component
Embedding Assembly Language in Object Pascal
to Call the Windows Virtual Timer Device
Class TStopWatch is a simple stopwatch designed for timing external events. It can measure inter-
vals as short as 25 microseconds (on a 486/66). Class TProfilingStopWatch is a specialized descen-
dant of TStopWatch that is designed for profiling programs. By compensating for overhead,
TProfilingStopWatch can measure intervals as short as 2-3 microseconds. In comparison, the short-
est interval that can be measured with the DOS/Windows system clock is 55,000 microseconds.
Classes TStopWatch and TProfilingStopWatch are non-visual components that can be installed on
the Delphi Component Palette. Using them is as simple as placing a stopwatch object on the form
and then calling the methods: Start, Stop, and ElapsedTime. Two example programs are presented:
one that times external events and one that profiles code performance.
Unit VTIMERDV
The Windows Virtual Timer Device (VTD) provides access to the PC’s hardware timer within the
confines of Windows’ protected memory management system. The 8253 Programmable Interval
Timer chip ticks at a rate of 1.196 MHz, giving the VTD a resolution of 0.836 microseconds.
Calling a Windows’ virtual device driver, however, requires assembly language routines. The rou-
tines used here are based on those of Rick Grehan in his article “The Software Stopwatch” (Byte,
April 1995). Additional information can be found in “Timers and Timing in Microsoft Windows”
on the Microsoft Developer’s Network CD-ROM.
The Delphi unit VTIMERDV (see Listing Two on page 45) encapsulates the assembly language
routines that are needed to call the VTD and to process the results. The public interface to unit
VTIMERDV exports just one function, VTD_GetTime, which returns a 64-bit real number that
represents the time in seconds since Windows was started. The implementation of the unit uses the
private procedure VTD_GetEntryPoint. Noteworthy features in the implementation of
VTIMERDV include the use of embedded assembly language, the use of embedded machine code,
and the presence of an initialization section.
The procedure VTD_GetEntryPoint calls the Window software tialization section is to set the value of the conversion factor,
interrupt $2F to get the VTD’s entry point. This address is stored dSecondsPerTick.
in the variables wVTDSegment and wVTDOffset. (I’ve used a mod-
ified “Hungarian” convention for naming variables and constants. A minor shortcoming of Delphi’s asm and inline statements is
The first letter or letters of a variable’s name represent its data that you cannot reference Object Pascal constants symbolically,
type: w for Word, l for Longint, d for Double, btn for TButton, although you can reference variables by name. It was therefore
etc.) In the listing for this procedure, notice how the ability to ref- necessary to declare the conversion factor, dSecondsPerTick, as var
erence Object Pascal variables within an asm block makes it easy rather than const and to initialize its value.
to integrate assembly language routines into a Delphi program.
Class TStopWatch
The function VTD_GetTime calls the VTD to get the number of The class TStopWatch is a Delphi component that represents an
hardware timer ticks that have elapsed since Windows was start- electronic stopwatch. TStopWatch uses unit VTIMERDV in its
ed and converts the returned value from ticks to seconds. The implementation, but itself contains no traces of the low-level
implementation of this routine requires a combination of details that were the center of attention in VTIMERDV (see
embedded assembly language and in-line machine code. This is Listing Three on page 46). These details have been successfully
amazingly simple to do in Delphi, since the Object Pascal state- encapsulated within VTIMERDV.
ments within the body of a procedure or function can be freely
intermixed with asm statements and inline statements. The methods that control a TStopWatch object are the familiar
ones used to operate a stopwatch:
As shown in Listing Two, the implementation of VTD_GetTime • Start starts the watch. If the watch is already running, Start
consists of an asm block, followed by an inline block, followed does nothing. Note that starting the watch does not reset the
by another asm block. The final result is a high-performance accumulated time. This allows multiple sequences of Start
function that goes well beyond the native capabilities of Delphi, and Stop to be used to measure the cumulative time for a
yet is implemented entirely within the Delphi environment. family of related events without requiring the programmer to
create variables and write code to perform the accumulation.
The first asm block calls the VTD, which returns the lower 32 • Stop stops the watch and adds the time that has elapsed since
bits of the tick count in the EAX register and the upper 32 bits Start to the accumulator. If the watch is already stopped, Stop
in the EDX register. This poses a real challenge. How can we does nothing.
access these 32-bit registers? Delphi’s built-in assembler is a 16- • Reset stops the watch and resets the accumulated time to
bit assembler. It can manipulate the 16-bit registers AX and DX, zero. It is not necessary to call Reset before using a
but not their 32-bit counterparts EAX and EDX. The solution is TStopWatch object, since the constructor initializes all fields
to use in-line machine code. By using the $66 opcode prefix to to their reset values.
toggle the operand size, we can access the 32-bit registers even • ElapsedTime reads the watch and returns the elapsed time
though the executable itself is just a 16-bit program. Cool! in seconds. If the watch is stopped, it returns the accumu-
lated time. If the watch is running, it returns the current
By transferring the EAX and EDX registers to adjacent memory “split time”.
locations, we can also emulate a 64-bit integer variable, even though • IsRunning returns a Boolean value that indicates whether the
the largest integer type that Delphi supports is the 32-bit Longint. watch is running.
The last instruction within the inline block pushes this 64-bit inte-
ger variable onto the coprocessor’s floating point stack, where it is Example 1
implicitly converted to an 80-bit real. The second asm block multi- Class TStopWatch is a non-visual component. It should be
plies the tick count by the conversion factor dSecondsPerTick, stores installed using the standard procedures for customizing the
the calculated time as a 64-bit real in the Object Pascal return vari- Delphi component library (see Chapter 2 of the User’s Guide).
able @Result, and pops the coprocessor stack to clean up. By default it installs itself on the System page of the
Component Palette (see Figure 1). To create a TStopWatch
Important note: All of this works only on an 80386 or higher object, simply drag a stopwatch from the palette and place it
CPU with a hardware coprocessor or floating point unit. on the form. Then add the code to operate the stopwatch at
the appropriate points in your application. Like other non-
Initialization sections are another innovation in the Delphi visual components, the TStopWatch object will be visible at
programming model. The initialization section of a unit con- design time, but not at run-time.
tains code that is executed when the program is loaded, prior
to the execution of any routines in the body of the unit. In Example 1 (see Listing Four on page 48) uses two stopwatches
unit VTIMERDV, the procedure VTD_GetEntryPoint must that are controlled by buttons on the form, as shown in Figure 2.
be called before the function VTD_GetTime can be used. It shows how TStopWatch can be used to time external events. In
However, VTD_GetEntryPoint only needs to be called once. It this case the external events are just button presses, but the possi-
is therefore an ideal candidate to be placed in the unit’s ini- bilities for timing external events are limited only by your ability
tialization section. The other action needed within the ini- to make those external stimuli visible to your program.
This example also shows that an application can use two or more To reiterate, these components should be applied as follows: Use
stopwatches. Each stopwatch can be started and stopped inde- TStopWatch to measure the interval between external events, but
pendently and there are no restrictions on how many stopwatch- use TProfilingStopWatch to measure the processor time consumed
es can be running concurrently. This is because only one timer is by a block of code.
running on the 8253 Timer Chip. These components are simply
retrieving and storing numbers from this timer and then per- In addition to automatic calibration and removal of overhead, class
forming arithmetic on those values. This is not like using the TProfilingStopWatch also provides five methods that can be used to
TTimer component in Windows that consumes a software timer manually control the calibration for overhead. These calibration
from a limited pool of timers that Windows manages. methods will be of interest primarily to users attempting to mea-
sure intervals that are short compared to the observed overhead:
Class TProfilingStopWatch • CalibrateOverhead measures the overhead of making 100 calls to
Class TProfilingStopWatch is a specialized descendant of Start and Stop, and stores the average value in the class variable
TStopWatch that is designed for profiling the execution of Delphi dOverheadForStop. It also measures the overhead of making 100
programs. It automatically removes the overhead of calling the calls to ElapsedTime to read a split time and stores the average
Windows VTD from the elapsed time. Figure 3 shows why this value in the class variable dOverheadForSplit. CalibrateOverhead
overhead should be removed from a stopwatch that times blocks is called in the initialization section for unit StpWatch. It can
off code, but not from a stopwatch that times external events. also be called later to recalibrate the overhead.
• OverheadForStop returns the current value of
The overhead in calling the Windows VTD can be divided into dOverheadForStop.
two parts: preparing to read the hardware timer and returning • OverheadForSplit returns the current value of
afterwards. In Figure 3, these are shown as intervals a and b, dOverheadForSplit.
respectively. A program that triggers the stopwatch at time t1
will actually read the hardware timer at time u1 and will not
regain control of the processor until time v1.
On the other hand, for a stopwatch that profiles code, the desired
result is the interval between v1 and t2 , which represents the
amount of processor time actually used by the code. Processor
time used by the stopwatch should be excluded. The essence of
class TProfilingStopWatch is to separately measure the overhead, z
= b + a, and use it to calculate the interval, y = u2 - u1 - z, which
is of equal duration to t2 - v1. In principle, the shortest interval
that can be measured with a profiling stopwatch is one tick of the Figure 3: The difference between timing external events (x) and profil-
hardware timer (0.840 microseconds). In practice, the limiting ing code (y).
• SetOverheadForStop allows the value of dOverheadForStop to Dividing the accumulated time for the Empty Loop by the number
be set directly. of iterations gives an estimate of the accuracy in the calibration of
• SetOverheadForSplit allows the value of dOverheadForSplit to dOverheadForStop. In Figure 3, for example, the actual time was 28
be set directly. microseconds for 20 iterations. The calibration error was therefore
1.4 microseconds per Stop.
Note that these calibration methods are class methods and
that the variables dOverheadForStop and dOverheadForSplit are The Split Loop does nothing but call ElapsedTime to read the
class variables, not instance variables. At any point in time, all split time. If the calibration for the overhead of taking a split was
TProfilingStopWatch objects within a program use the same perfect, the measured time would be zero, regardless of the num-
values of dOverheadForStop and dOverheadForSplit. This ber of iterations. Dividing the accumulated time for the Split
ensures that differences in the measured overhead will not Loop by the number of iterations gives an estimate of the accu-
bias one stopwatch relative to another. The overhead correc- racy in the calibration of dOverheadForSplit. In Figure 3, the
tions will be consistent for all instances and the measured actual time was 22 microseconds for 20 iterations. The calibra-
times can be freely compared against one another. Of course, tion error was therefore 1.1 microseconds per Split.
Object Pascal doesn’t directly support Smalltalk-style “class
variables”, but with the additional encapsulation provided by In practice, the limiting factor in the resolution of
units, it’s easy to fake it. TProfilingStopWatch is the uncertainty in calibrating the overhead,
which can fluctuate by 10 percent or more.
Example 2
Example 2 (see Listing Five on page 49) uses four profiling stop- Keep in mind, however, that calibration uncertainties are sig-
watches to time the execution of three blocks of code, and to nificant only if you are trying to measure intervals that are
compare their execution time against the total time for the short compared to the measured overhead. For example, the
enclosing procedure. It illustrates the use of repeated calls to time required to execute some Windows API functions can be
Start and Stop to accumulate the total time for a family of related as much as a thousand times greater than this.
events, the use of ElapsedTime to obtain split times, and the use
of the TProfilingStopWatch calibration methods. The Recalibrate button executes the class method procedure
CalibrateOverhead and displays the new values of
Figure 4 shows the user dOverheadForStop and dOverheadForSplit. After recalibrating,
interface for this program. press the Run button again to see the effect on the measured
The Run button executes time for the Empty Loop and the Split Loop. You can also
the test procedure, explore the effect of changes in dOverheadForStop and
btnRunClick, that is being dOverheadForSplit by entering new values in the correspond-
profiled. The three blocks ing edit boxes and then pressing the Set Overhead button.
to be timed within this
procedure are the “Empty By default, each TProfilingStopWatch object automatically
Loop”, the “Split Loop”, corrects for the overhead of calling the Windows VTD.
and the “Work Loop”. However, it can do this only for the overhead incurred by its
The total time is mea- own calls to the VTD. If nested timers are used, as here, the
sured by elapsed time for the outer timer will include the overhead
ProfilingStopWatch1, the incurred by the inner timers.
Empty Loop is timed by
ProfilingStopWatch2, the The solution is to count the number of calls made by the inner
Split Loop is timed by timers and to subtract this from the elapsed time for the outer
ProfilingStopWatch3, and Figure 4: The user interface for timer, after multiplying by the values returned by
the Work Loop is timed Example 2. OverheadForStop and OverheadForSplit. This is the time report-
by ProfilingStopWatch4. ed for StpW Calls.
The number of iterations for all three loops is controlled by the The value reported for Remainder consists of all the remain-
value entered in the Iterations edit box. The Work Loop, howev- ing parts of procedure btnRunClick. This is mostly just the
er, contains a nested inner loop, so that the amount of work that control code for the Empty Loop, and its proportion of the
it performs is proportional to the Iterations parameter squared. total time is small, as expected.
The Empty Loop does nothing but start ProfilingStopWatch2 and Conclusion
then immediately stop it. Since the stopwatch is not reset Unit VTIMERDV shows the flexibility of Delphi in allowing
between iterations, the reported time is cumulative. If the calibra- the programmer to use assembly language or even machine
tion for the overhead of stopping the watch was perfect, the mea- code to perform operations that cannot be implemented
sured time would be zero, regardless of the number of iterations. directly in Object Pascal. Yet, at the same time, everything was
@RetSpot:
{ Just a place to come home to }
Richard Holmes is a senior programmer/analyst at NEC Electronics in Roseville, CA, nop
where he designs and develops client/server database applications. He can be reached end;
on CompuServe at 72037,3236.
{ The 64-bit tick count is returned in the 32-bit
registers EAX and EDX. Since Delphi's built-in
assembler does not support 32-bit registers, use
machine code to transfer the registers into two
adjacent 32-bit LongInts, which emulate a 64-bit
Begin Listing Two: The VTimerDV unit
Integer. Then push the 64-bit Integer onto the
{ Access the Windows Virtual Timer Device }
coprocessor stack. }
unit VTimerDv;
inline
(
interface
{ Toggle operand size }
$66/
function VTD_GetTime: Double;
{ mov aVTDTicks[1],eax }
$89/$06/>aVTDTicks/
implementation
{ Toggle operand size }
$66/
var
{ mov aVTDTicks[2],edx }
wVTDSegment: word;
$89/$16/>aVTDTicks+4/
wVTDOffset: word;
{ fild[64] aVTDTicks }
{ Used as a 64-bit signed Integer }
$DF/$2E/>aVTDTicks
aVTDTicks: array [1..2] of LongInt;
);
{ Actually a constant }
dSecondsPerTick: Double;
{ Convert from ticks to seconds. }
asm
procedure VTD_GetEntryPoint;
{ Multiply by the conversion factor }
begin
fmul dSecondsPerTick
{ Call Windows to get address of VTD entry point. }
{ Store in @result and pop the stack }
asm
fstp @result
{ Code to return API entry point }
end;
mov ax,$1684
end;
{ Code for Virtual Timer Device (VTD) }
mov bx,$05
initialization
{ Clear ES:DI }
VTD_GetEntryPoint;
xor di,di
mov es,di
{ The built-in assembler cannot reference Object
{ Call Windows }
Pascal constants. So must declare this as a
int $2F
variable and explicitly initialize it. }
{ Save results }
mov wVTDSegment,es
{ Timer rate is 1.196 MHz }
mov wVTDOffset,di
dSecondsPerTick := 0.836E-6;
end;
end.
end;
lblTotalTime.Caption :=
FormatFloat('0.000000',dTotalTime);
lblEmptyTime.Caption :=
FormatFloat('0.000000',dEmptyTime);
lblSplitTime.Caption :=
FormatFloat('0.000000',dSplitTime);
lblWorkTime.Caption :=
FormatFloat('0.000000',dWorkTime);
lblOverhead.Caption :=
FormatFloat('0.000000',dOverhead);
lblRemainder.Caption :=
FormatFloat('0.000000',dRemainder);
lblTotalPct.Caption := '100.00';
lblEmptyPct.Caption :=
FormatFloat('0.00',100.0 * dEmptyTime/dTotalTime);
lblSplitPct.Caption :=
FormatFloat('0.00',100.0 * dSplitTime/dTotalTime);
lblWorkPct.Caption :=
FormatFloat('0.00',100.0 * dWorkTime/dTotalTime);
lblOverheadPct.Caption :=
FormatFloat('0.00',100.0 * dOverhead/dTotalTime);
lblRemainderPct.Caption :=
FormatFloat('0.00',100.0 * dRemainder/dTotalTime);
lblStopOverhead.Caption := FormatFloat('0.0000000',
TProfilingStopWatch.OverheadForStop);
lblSplitOverhead.Caption := FormatFloat('0.0000000',
TProfilingStopWatch.OverheadForSplit);
end;
end.