0% found this document useful (0 votes)
50 views

Delphi Informant 95 2001

The document discusses several Delphi components and tools. It includes articles on creating a slidebar component, a dynamic toolbar component, a stopwatch component, and data validation techniques. It also provides news on a new Delphi accounting package and book releases. Sections include features, departments, and developer tools.

Uploaded by

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

Delphi Informant 95 2001

The document discusses several Delphi components and tools. It includes articles on creating a slidebar component, a dynamic toolbar component, a stopwatch component, and data validation techniques. It also provides news on a new Delphi accounting package and book releases. Sections include features, departments, and developer tools.

Uploaded by

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

September 1995 - Volume 1, Number 5

Creating
Components
Assembling Delphi’s Building Blocks

Cover Art By: Victor Kongkadee

ON THE COVER FEATURES


8 The TSlideBar Component — Robert Vivrette 23 Informant Spotlight — Thomas Miller
Mr Vivrette gets our component issue off to a rollicking start This month’s “Spotlight” features a blow-by-blow com-
with his fully-implemented SlideBar. Not only will you come parison of Delphi and PowerBuilder from a recent convert.
away with a useful and attractive Windows control, you’ll A long-time PB developer, Mr Miller puts both products
learn how it was built. Many techniques are presented along through their paces before concluding “It’s time to leave
the way including: handling focus, handling keyboard and PowerBuilder behind.”
mouse input, painting a control’s canvas, storing images in a
resource file, and more. 31 DBNavigator — Cary Jensen, Ph.D.
In this month’s DBNavigator, Mr Jensen concludes a two-
16 A Dynamic Toolbar — Gary Entsminger part series on data validation. This time the focus is on
Here’s a floating Toolbar that your application’s users can validating input for data-aware TField components. Topics
modify on-the-fly — adding and deleting buttons as the discussed include: creating input masks, the Required
need arises. Mr Entsminger tackles several topics while dis- property, and the OnValidate and BeforePost events.
cussing the component, including: unit variables, the
FormCreate event, exception handling, TNotifyEvent, the 36 OP Basics — Charles Calvert
Sender parameter, hints, glyphs, the API ShellExecute It’s the second of a three-part series on Object Pascal
function, and more. Download it and put it to use! strings. This month Mr Calvert shares some string-han-
dling techniques (including how to strip trailing blanks),
41 A Stopwatch Component — Richard Holmes and introduces the Object Pascal Length, GetDate, Delete,
More awesome components! And this one is not for the faint FillChar, Copy, and Move functions.
of heart. Mr Holmes uses embedded assembly language and
in-line code to access the Windows Virtual Timer Device to
create an extraordinarily precise software stopwatch. Even if DEPARTMENTS
you’re not up to writing assembler, the Stopwatch compo-
nents are useful as they come — one is even designed 2 Delphi Tools
specifically for profiling programs. 5 Newsline

SEPTEMBER 1995 Delphi INFORMANT ▲ 1


Delphi Delphi Accounting Package Released
ColumbuSoft of Columbus, out changes to the code. Price: US$500 for the first module,
T O O L S
OH has released Accounting for The source code includes US$250 for any additional modules.
Delphi, a series of general utility functions that can be
New Products
accounting modules written used in other applications, Contact: ColumbuSoft, 1525 Norma
and Solutions
entirely in Object Pascal including a reusable report Road, Columbus, OH 43229
(Delphi’s native language) and printer with integrated on-
sold with the source code. screen preview. ReportSmith Phone: (800) 692-2150 or (614) 885-
Purchasers receive a license that or other report generators are 7789
allows them to resell their com- not required.
piled applications without addi- A free demonstration version is Fax: (614) 885-2077
tional royalties or fees. available. It has several source
Using the same fundamental code files, and a developer’s help E-mail: 76702.556@compuserve.com
approach as previous file with technical documenta-
ColumbuSoft products, tion and file structures.
Accounting for Delphi is a
New Delphi Books
batch-oriented, double-entry
system. The system provides
Developing Windows the means for Delphi develop-
Applications Using Delphi
Paul Penrod ers to provide general account-
John Wiley & Sons ing functionality that inte-
ISBN: 0-471-11017-5
Developing introduces object-
grates with their custom or
oriented programming techniques, vertical market applications.
and then develops a Windows Accounting for Delphi
application step-by-step. It covers
topics such as Object Pascal, GUI, includes general ledger,
debugging and testing applications, accounts payable, accounts
event handling, exception handling,
error handling, using the run-time receivable, order entry, inven-
libraries, and compiling executables. tory/purchasing, fixed assets,
Price: US$29.95 (353 pages)
Phone: (212) 850-6630
payroll, and job costing mod-
ules. Each can be used alone
or with other modules with-
Delphi: A Developer’s Guide
Vince Kellen & Bill Todd
M&T Books
ISBN: 1-55851-455-4
New HeadConv 1.0: C DLL Header Converter Expert for Delphi
Delphi guides readers through HeadConv 1.0, a C DLL third-party C DLLs. FORUM: Library 14), and the
every aspect of Delphi, including:
Delphi’s database controls, VCL
Header Converter Expert for HeadConv has full support Delphi Forum (GO DELPHI:
components, the Borland Delphi, is now available. for functions and procedures, Library 22). The file name is
Database Engine, InterBase, SQL Targeted at the serious argument and return types (128 HDCNV1.ZIP. All registered
Server, and object-oriented
programming and techniques. Delphi developer, the custom type conversions), and users will receive source code
Price: US$44.95 HeadConv Expert assists the generates implicit Delphi for the expert and stand-alone
(820 pages, CD-ROM)
Phone: (800) 488-5233
conversion of C DLL header import units. The expert is EXEs (source code for the
files to Delphi import units, integrated in the Delphi IDE, parser is not provided), a
giving Delphi users access to simplifying the conversion detailed WinHelp file, and the
process by opening automati- capability of generating explic-
cally within the IDE. There is it import units. Registered
limited (non-complex) support users may also submit feature
for typedefs, structs, unions, requests, and will receive regu-
enums, and conditional compi- lar updates by CompuServe
lations. HeadConv cannot per- mail when available.
form 100 percent of the con-
version, but offers major assis- Price: US$25, registrations can be made
tance in converting C DLL in the SWREG Forum (id #6533).
header files.
The shareware version of Contact: Bob Swart, P.O. Box 799,
HeadConv 1.0 is available on 5702 NP, Helmond, the Netherlands
CompuServe from the
Informant Forum (GO ICG- E-mail: drbob@pi.net or 100434,2072

SEPTEMBER 1995 Delphi INFORMANT ▲ 2


Delphi ProtoView Releases Delphi Tools
ProtoView Development endar for the year specified. Applications created using the
T O O L S
Corporation of Cranbury, NJ PICS features a time control ProtoView Interface
has released ProtoView that allows graphical 24-hour Component Set may be dis-
New Products
Interface Component Set time selection and formatting tributed royalty-free.
and Solutions
(PICS) version 1.5 for Delphi, with various styles, a numeric
a DLL/VBX controls toolkit. edit control that allows picture Price: PICS 1.5 for Delphi is US$149;
PICS is a collection of masking and scientific nota- source code, US$495.
Windows user-interface tion formats for numeric
objects for creating Delphi input, and display styles Contact: ProtoView, 2540 Route 130,
forms. The PICS Interface including LED readout, Cranbury, NJ 08512
Component Set includes a counter, and normal.
hierarchical list box with mul- Additional PICS for Delphi Phone: (800) 231-8588, or
tiple sort levels, volume dial, controls include a multi-direc- (609) 655-5000
font selection, date, multi- tional arrow control, a font
directional button, numeric selection control, and a versa- Fax: (609) 655-5353
edit, percent bar, time, and tile Push Button control.
New Delphi Books
icon/bitmap button controls.
Mastering Delphi
Marco Cantu PICS includes design-time
Sybex visual setting through the
ISBN: 0-7821-1739-2
This book introduces programmers Delphi Object Inspector, node
to all of Delphi’s features and searching functions, movable
techniques, including the secrets of
the integrated development envi-
sub-trees, expandable hierar-
ronment, programming language, chical levels, and bitmap
the custom components, and graphics for individual nodes.
Windows programming in general.
Topics covered include OLE, DDE, In addition, PICS has a
DLL, and graphics. The companion data/calendar control that
disk contains the source code to
the examples from the book and allows graphical calendar selec-
an assortment of utilities. tion and formatting of various
Price: US$39.99
(1,500 pages, CD)
data values. Users can press a
Phone: (510) 523-8233 date button and display a cal-

New AccuSoft Image Format Library 5.0


AccuSoft Corporation of JPEG, Photo CD, G3, G4, of the VBX 16 but is two to
Westborough, MA has PCX, GIF, DIB, WPG, BMP, three times faster. Extra fea-
announced the release of TGA, PICT, EPS, and WMF. tures include anti-aliased dis-
AccuSoft Image Format This library also features play, advanced scanner con-
Library 5.0, a raster imaging advanced color reduction, 30 trol, and sub-degree rotation.
DLL/VBX toolkit. It features percent faster JPEG, automatic The VBX 32 Pro Gold has all
36 formats including TIFF, thumbnails, new file informa- the features of the standard 32-
tion function, file I/O replace- bit version but is two to four
ment, status bar through call times faster. It also supports
backs for functions, new scan- ImageAccel, large image, and
ning features, and group three black and white, Group IV
and four raw data can be out- and JPEG images. No special
put directly. drivers are required.
The AccuSoft VBX custom
control provides developers Prices: Starts at US$495.
with a complete imaging
toolkit, according to the com- Contact: AccuSoft Corporation, Two
pany. There are no function Westborough Business Park, Westborough,
calls since everything is imple- MA 01581
mented as properties and
events. This version also has Phone: (508) 898-2770
over 130 properties.
The VBX 32 has all features Fax: (508) 898-9662

SEPTEMBER 1995 Delphi INFORMANT ▲ 3


Delphi SilverWare Windows Communications Tool Kit Ships
T O O L S SilverWare Inc., of Dallas, dialer, modem defaults, file Price: US$299 (no royalties, includes
TX has announced the release transfer options, and more, free technical support and a 30-day
New Products of The SilverWare Windows with a single function call. money back guarantee).
and Solutions Communications Tool Kit ver- It also features: low/high
sion 5.01, a multi-language level support, beyond Contact: SilverWare Inc., 3010 LBJ
communication library for COM1/COM2, multi COM Freeway, #740, Dallas, TX 75234
Microsoft Windows. The I/O boards, hardware/software
library is a Windows dynamic flow control, file transfers, Phone: (214) 247-0131
link library (DLL) accessible Smartmodem support, auto
from any Windows language or dialer, high-level remote input, Fax: (214) 406-9999
program that can make FAR immediate, transmit, asyn-
PASCAL function calls and chronous timer functions, BBS: (214) 247-2177
pass 16- and 32-bit parameters comprehensive return code
by reference and value. It offers system, documentation, and WWW Internet Home Page URL:
complete support for the fol- examples for every language. http://rampages.onramp.net/~silver
Delphi Training lowing languages: Object
Pascal, ObjectPAL, C/C++,
The 4GL Consulting Group
Ltd., of San Mateo, CA is offer- CA-Visual Objects, Clarion for
ing beginning and advanced Windows, Clip4Win, Visual
courses in Delphi. Taught by a
developer, these classes cover dBASE, FiveWin, Power
Object Pascal, object-oriented Builder, Turbo Pascal for
programming, client/server
database applications and Windows, and Visual Basic.
Delphi components. The SilverWare Windows
4GL will be offering classes
throughout this month. For Communications Tool Kit has
more information and built-in, high-level dialog box
registration materials, contact
Ian Hart at (415) 348-4848 functions to decrease develop-
or fax (415) 349-4683. ment time. These dialog box
The course instructor may be
contacted via CompuServe at functions enable developers to
72143,467. prompt users for COM port
selection, UART settings, auto

Crystal Announces 32-bit Windows Versions


Crystal of Vancouver, BC has will run on all Windows plat- addition, this version offers
announced a new 32-bit ver- forms including Windows 3.1. the speed and stability of a
sion of Crystal Reports for Crystal Reports’ new features 32-bit operating system, a
Windows 95 and Windows include enhanced graphing tabbed interface, and long-
NT. It will be available in both with customizable graph types, file-name support.
16-bit and 32-bit versions, and import and export capabilities
for Lotus Notes, support for Price: Crystal Reports Standard 4.5,
Access 2.0 OLE picture fields, US$195; Crystal Reports Professional 4.5,
and support for the Microsoft US$395; Upgrades to Crystal Reports
Access Engine 2.5. It can Professional 4.5 from previous versions of
export to Excel 5.0, save report Crystal Reports Standard or Professional
options with a report (simpli- plus OEM versions of Crystal Reports,
fying report distribution), drill US$199.
down on graphs, and supports
the new Borland Database Contact: Crystal, a Seagate Software
Engine (IDAPI) and Paradox Company, 1095 West Pender Street, 4th
5.0 for Windows. Floor, Vancouver, BC, Canada V6E 2M6
According to the company,
developers can deliver report- Phone: (604) 681-3435, or (800)
ing solutions for their 32-bit 877-2340
environments using the 32-bit
Report Engine and OCX. In Fax: (604) 681-2934

SEPTEMBER 1995 Delphi INFORMANT ▲ 4


News Delphi 32: Supports OCXes, OLE Automation, and More
Scotts Valley, CA — Borland
International is scheduled to
L I N E unveil product details at their
San Diego conference regard-
September 1995 ing their new 32-bit versions
of Delphi and Delphi
Client/Server. Delphi 32’s
enhancements include an opti-
mized 32-bit native code com-
piler, a Windows 95 user-
interface in the Delphi IDE, a
high performance 32-bit
Borland Database Engine and
SQL links, additional example
programs and demonstrations,
Virtual User Group Meets and improved documentation
The Informant CompuServe (including a printed language
Forum will host a Delphi Virtual
User Group meeting
reference manual). Developers currently using tabs, progress bars, already
Wednesday, Sept. 13, 1995 at Delphi 32 will support OLE Delphi can create applica- said, etc. These components
6 p.m. (PST). During this hour controls (OCXes), OLE tions that look similar to a can then be added to the
long session, we’ll discuss the
Borland Conference and the
automation, as well as provide Windows 95 application by Component Palette.
latest trends in the Delphi full support for Windows NT, using components such as In addition, Delphi 32 sup-
Community. To access the and Windows 95. notebook tabs, outline con- ports long file names, new
Informant CompuServe Forum
type “GO ICGFORUM” at any
With OLE automation, devel- trols, spin controls, and tool dialog boxes, styles, and
CompuServe “GO” prompt. opers will be able to create or help. Then, when 32-bit ver- immediate access to
control scriptable applications sions of Delphi are available, Windows 95 API including
with a variety of other applica- developers can recompile facilities such as multi-
tions including Paradox for their existing applications for threading, plug and play,
Windows, Visual dBASE, true 32-bit performance on MAPI, and more. In addi-
Microsoft Office, WordPerfect Windows 95 or Windows tion, Delphi 32 will make it
Office, and others. NT without rewriting code. easy to create applications
The current versions of Delphi Developers using low-level that meet Windows 95 logo
and Delphi Client/Server are code that is dependent on requirements.
compatible with Windows 3.1, Windows 16-bit segmented Currently, the 32-bit ver-
Windows for Workgroups 3.11, architecture, or Windows 3.1 sions of Delphi and Delphi
Windows 95 beta releases, features not supported by Client/Server are in beta test-
Windows NT 3.5 and 3.51, Windows 95, will need to ing, and are expected to be
and OS/2. alter their code as necessary. available about 90 days after
With Delphi 32, developers the commercial release of
will not be able to create 16- Windows 95.
bit applications. However, Borland is planning to offer
applications created in special upgrade pricing for
Delphi 32 that do not use registered Delphi users.
32-bit specific features can Customers who purchased
be recompiled with the 16- Delphi Client/Server with
bit version of Delphi and maintenance will receive the
then run on Windows 3.1. new 32-bit version free of
Delphi 32 is expected to charge.
meet all the Windows 95 Borland expects the 16-
logo requirements. It will and 32-bit operating systems
include additional compo- to co-exist for the next 18 to
nents to support new 24 months and will continue
Windows 95 specific features to sell and support the 16-bit
such as rich text editing, version of Delphi after
Windows 95 style notebook Delphi 32 is released.

SEPTEMBER 1995 Delphi INFORMANT ▲ 5


News Borland Makes Profit in First Quarter
Scotts Valley, CA — Borland
International Inc. has
30, 1995 quarter was US$2.8
million or US$.10 per share,
transactions, Borland would
have reported an operating loss
L I N E announced revenues for its first compared with net income of of US$35 million, on revenues
quarter ending June 30, 1995 US$61.4 million or US$1.88 of US$44.6 million, in the
September 1995 of US$53.8 million. Revenues per share in the first quarter a quarter ending June 30, 1994.
from the first fiscal quarter of year ago. Included in the prior Total operating expenses for
the prior year were US$69.1 year’s results is a US$99.9 mil- the quarter were US$43 mil-
million, however it included lion non-operating gain on the lion, a 37 percent decrease from
US$24.5 million from the sale sale of Borland’s Quattro Pro US$68.5 million for the same
of Paradox licenses to Novell. spreadsheet product line to quarter of the previous year,
Excluding the non-recurring Novell, Inc., the Paradox license exclusive of the write-off of pur-
revenue from the sale of the revenue of US$24.5 million, chased technology of US$16.2
Paradox licenses, first quarter and a one-time charge for pur- million. The lower expenses in
fiscal 1996 revenues increased chased technology of US$16.2 the quarter ended June 30,
20 percent from the same quar- million related to the company’s 1995 reflect the restructuring
ter of the prior year. acquisition of ReportSmith. efforts started this year.
The net income for the June Excluding these non-recurring According to Gary Wetsel,
president of Borland, the quar-
ICG to Publish Oracle Informant ter’s results reflect Borland’s
efforts to reduce costs and the
Elk Grove, CA — Informant Magazine-only subscriptions continued success of Delphi.
Communications Group, Inc. to Oracle Informant are avail-
(ICG) has announced it will able to US subscribers for Borland Developers
be publishing its third prod-
uct-specific technical maga-
US$49.95 a year (12 issues).
An optional subscription to
Conference, London
zine, Oracle® Informant. the Oracle Informant 1996 Announced
Oracle Informant will contain Companion Disk is also avail- London, England — Borland
in-depth technical articles on able. The Oracle Informant International, Desktop
client/server development with Companion Disk contains all Associates Limited, and
Oracle® Workgroup/2000 the source code and support Dunstan Thomas Limited
tools. The premiere issue of files for each article appearing announced the third Borland
Oracle Informant is scheduled in Oracle Informant. One-year Developers Conference,
for Winter 1995. magazine and Companion London 1996 will be held in
Oracle Informant will place Disk subscriptions are avail- London, England on April 28
heavy emphasis on database able for US$119.95 a year (12 through 30 1996 at the Royal
application development using issues and disks). Oracle Lancaster Hotel, Lancaster
Oracle Workgroup/2000 Informant will also be available Gate, London.
products. Featuring regularly- at major newsstand outlets. The event will feature over 40
appearing articles covering Authors and developers sessions covering all Borland
technical issues regarding interested in contributing arti- mainstream products: Delphi,
Oracle7 Workgroup Server, cles can obtain a writer’s style Paradox, and dBASE, C++,
Personal Oracle7, Oracle guide and editorial calendar by client/server solutions,
Power Objects, Oracle Objects contacting ICG Associate InterBase, and many other top-
for OLE, and Oracle Mobile Editor Carol Boosembark at ics including case studies. The
Agents, Oracle developers will (916) 686-6610, ext. 16 or via sessions will cover program-
have an independent, compre- e-mail at 75702.1274@com- ming, solutions, tools and tech-
hensive source of in-depth puserve.com. niques, and methodologies.
technical information. Vendors interested in adver- Pricing for the Borland
Oracle Informant will also tising can obtain an Oracle Developers Conference,
feature news from the Oracle Informant Media Kit by con- London 1996 is £495
community, third-party prod- tacting ICG Advertising (US$742). Those attending
uct information, product and Director Lynn Beaudoin at only one day pay £275
book reviews, and Oracle user (916) 686-6610, ext. 17 or via (US$412). For a complete
group information each e-mail at 74764.1205@com- brochure call the conference
month. puserve.com. office at +44 0181 788 0057.

SEPTEMBER 1995 Delphi INFORMANT ▲ 6


Software World and Client/Server Developers to Meet in San Jose
Andover, MA — DCI’s
Software World and
Deploying Complex Applica-
tions, Professional Rapid
Ed Yourdon, consultant and
methodologist; Phillip White,
News
Client/Server Developers Application Development, and President, CEO and Chairman L I N E
Conference and Exposition is Practical Business Process Re- of the Board, Informix
heading to the San Jose Engineering Strategies. For Software; George Schussel, September 1995
Convention Center in San technical growth, the show will Chairman and CEO, DCI;
Jose, CA on Oct. 10-12, feature tracks on Distributed Rodney Knowles III, Director
1995. The event will feature Objects, Next Generation of of Special Projects, Atlanta
over 250 exhibitors, several Component-based Develop- Committee for the Olympic
management and technical ment, Windows 95 and OS/2, Games; and Steve Mills,
tracks, keynote presentations, Visual Programming — Tips General Manager of Software
and special events. and Techniques, Cross Platform Solutions Division, IBM.
The management tracks Solutions, and Leveraging For more information call:
include Managing Complex Lotus Notes. (508) 470-3880, fax: (508)
Software Projects, The Future Attendees will hear from sev- 470-0526, or e-mail:
of Database Management, eral keynote speakers including DCIconf1@aol.com.

Borland Ships Visual dBASE 5.5 and Compiler


Scotts Valley, CA — Borland Visual dBASE Compiler are estimated street price for Software Development ’95 East
International Inc. has released US$349.95 each. Special Visual dBASE Client/Server is Over 200 venders are expected
to attend this year’s Software
its Visual dBASE 5.5 database upgrade prices are available for US$695. For more informa- Development East in Washington,
and Visual dBASE Compiler current dBASE users and tion or to place orders, call D.C., Oct. 2-6. The event
for the Microsoft Windows competitive products. The Borland at (800) 233-2444. features more than 150 lectures,
workshops, and tutorials, covering
3.1 and Windows 95 operat- topics such as C++, Windows
ing systems. The company also
announced a new product:
10th Annual PC Expo in Chicago 95/NT development, object-ori-
ented programming, database
programming and design, and
Visual dBASE Client/Server. Fort Lee, NJ — The Blenheim Wollongong Group, Inc. more. For more information call
Visual dBASE 5.5 is the only Group has announced that the PC Expo will also have an Miller Freeman Inc. at (800) 441-
second-generation, object-ori- Tenth Annual PC Expo will “Internet Theater” enabling 8826, fax (415) 905-2222,
e-mail sd95east@mfi.com, or
ented Xbase database. It fea- be held October 3 through 5 attendees to learn about the visit their home page at:
tures new productivity tools at the McCormick Place East resources available on the http://www.mfi.com/sdconfs/
for users and developers, per- in Chicago, IL. The event will Internet, as well as gain
formance enhancements, and host over 200 exhibitors, fea- hands-on experience. In addi-
client/server capabilities. The turing applications supporting tion, there will be several
separate Visual dBASE Windows 95, OS/2, and areas for specific products and
Compiler allows developers to Macintosh operating systems. services. These include an
create and deploy stand-alone Blenheim expects to attract Internet Pavilion, Networking
.EXE applications royalty-free over 30,000 attendees, includ- Pavilion, Multimedia
to users. ing corporate volume buyers Pavilion, and Technology
Using the new client/server from the business and govern- Recruitment Center.
version of Visual dBASE, ment sector. Volume resellers There will be a variety of
developers can create front- in the audience will include seminars, half-day work-
ends to existing Oracle, software developers, dealers, shops, and tutorials consist-
Sybase, Microsoft SQL Server, VARs, and consultants. ing of in-depth user case
Borland InterBase, and Keynote speakers for PC studies available throughout
Informix database servers. The Expo feature: Pallab Chatterjee, the event, such as network-
product includes Visual president of Personal Product- ing, emerging technologies,
dBASE, the Visual dBASE ivity Products business, Texas technology management,
Compiler, native Borland SQL Instruments, Inc.; James P. Windows, client/server,
Links, a single-user Local McNeil, executive vice presi- Internet, and Groupware.
InterBase Server and Borland’s dent, corporate development, For more information call The
new Data Pump Expert. Cheyenne Software; and Blenheim Group at (800) 829-
The estimated street prices Robert E. Lawton, vice presi- 3976 or visit their home page
for Visual dBASE 5.5 and the dent of marketing, The at: http://WWW.shownet.com.

SEPTEMBER 1995 Delphi INFORMANT ▲ 7


On the Cover
Delphi / Object Pascal

By Robert Vivrette

The TSlideBar Component


Component Design Techniques for the Initiated

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.

The TSlideBar component (see Figure 1) is functionally similar to a stan-


dard scroll bar, but it is more attractive and consumes less screen real
estate. While developing this component, we’ll cover these topics:
• Handling the focus
• Receiving input from the keyboard
• Mouse capture and mouse events
• Painting on a control’s canvas
• Storing images in a resource file
• Transparent areas and masking
• Working with a TStringList

The TSlideBar Component


Before going into the details, let’s look at what we want from this com-
ponent. At its most elementary level, a slide bar component is really
just a way of enabling the user to select a number. So, why not just use
an edit box and have the user enter a number?

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

SEPTEMBER 1995 Delphi INFORMANT ▲ 8


On The Cover

be an index for a list The TSlideBar component should be no different.


of strings. Suppose Fortunately, managing which object has focus in an applica-
you are using the tion is primarily the operating system’s job. All that needs to
control to indicate a be done from the component’s perspective is to recognize
movie rating. Typical when the component has gained or lost focus, and determine
responses may be G, how to indicate this to the user.
PG, PG-13, and R.
The slide bar could Detecting a focus change is just a matter of including handlers
be used to display that will trap the appropriate Windows messages. In this case,
these four choices we are looking for the WM_SETFOCUS and WM_KILLFOCUS mes-
without requiring sages. In the private section of your component’s class declara-
the user to enter Figure 1: A demonstration of the SlideBar tion, you would include these lines of code:
component. Note that some have different
anything. In addi-
thumbs, width trenches, and orientations private
tion, the user does (vertical vs. horizontal). In addition, some are procedure WMSetFocus(var Message: TWMSetFocus);
not have to be con- raised while others are lowered, some have message WM_SETFOCUS;
cerned that the pro- tick marks, and one of the controls currently procedure WMKillFocus(var Message: TWMKillFocus);
gram has assigned 0 has the focus (as indicated by the red high- message WM_KILLFOCUS;
light). Finally, the control features an optional
to G, 1 to PG, 2 to
“pointing hand” custom cursor. When the user tabs through the controls on the form, or
PG-13, and 3 to R.
clicks on a particular control, Windows first sends a WM_KILL-
So we want the basic ability to obtain a number from the FOCUS message to the control that had focus. Then, a
component (i.e. its “position”). But let’s take it a bit further. WM_SETFOCUS message is sent to the control that is gaining
The thumb that we are sliding back and forth may not fit the focus. From the component’s side, we simply write handlers
design scheme of the application as a whole. So, we’ll allow for these messages. In the case of TSlideBar, I just want the
the developer to have different thumbs. We can also allow the component to repaint itself when it gains or loses focus, as fol-
slot that the thumb moves along to be raised or lowered, and lows:
of variable width.
procedure TSlideBar.WMSetFocus(var Message: TWMSetFocus);
begin
Naturally, we want to have the mouse move the thumb. Therefore Refresh;
the component will have to be “mouse aware”, responding to mouse end;

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:

{ Now color a filled rectangle in the center


Finally, I decided it would be handy if the control could hold a if the control has focus }
list of strings for its various positions. That way, the control itself if Focused then
could report which string has been selected, rather than getting Brush.Color := FocusColor
else
the control’s position and running through a big case statement Brush.Color := clSilver;
to determine the string.
Pen.Style := psClear;
{ Draw the focus highlight }
Focus! Focus! Rectangle(X1+1, Y1+1, X2+1, Y2+1);
In a Windows application, at any time, the user must be able to
clearly see which control on a form currently has focus. Most There are different ways of implementing focus features. For
controls indicate this by altering their appearance. A button for example, in many cases the WMSetFocus procedure could
example, will have its text surrounded by a dotted line. An edit draw the focus highlight. However, I chose not to do this
box will show a blinking insertion point, or highlight some of with TSlideBar, because the thumb sits over the focus high-
the text inside. Scroll bars (when they are allowed to receive light and I would have to redraw it as well. (Once I start
focus) often have a blinking thumb. doing that, then I might as well redraw the whole thing.)

SEPTEMBER 1995 Delphi INFORMANT ▲ 9


On The Cover

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;

With the TSlideBar component, we are only interested in trap-


procedure TSlideBar.KeyDown(var Key: Word; ping the arrow keys. However, there may be instances in other
Shift: TShiftState);
var components where you will want to catch other keys.
b : Integer;
begin The WMGetDlgCode handler can also use any combination of
b := MaxInt(1,(Max-Min) div 10);
case Key of other constants including, but not limited to:
VK_PRIOR : if (Position-b) > Min then • DLGC_WANTALLKEYS
Position := Position - b • DLGC_WANTCHARS
else
Position := Min; • DLGC_WANTTAB
VK_NEXT : if (Position+b) < Max then • DLGC_WANTMESSAGE
Position := Position + b
else
Position := Max; You can read more about this topic by searching on WM_GETDL-
VK_END : if IsVert then GCODE in the Windows API on-line help file (WINAPI.HLP).
Position := Min
else
Position := Max; Build a Better Mouse Trap
VK_HOME : if IsVert then Normally, a Windows control or form only receives mouse mes-
Position := Max sages when the mouse cursor is over its client area (i.e. the area
else
Position := Min; bounding the dimensions of the control or the inside area of a
VK_LEFT : if Position > Min then form). Sometimes however, it becomes necessary for a control to
Position := Position - 1; receive mouse messages even when the mouse is outside this area.
VK_UP : if Position < Max then
Position := Position + 1;
This is known as “capturing” the mouse.
VK_RIGHT : if Position < Max then
Position := Position + 1; Fortunately with the Delphi component designer, mouse capture
VK_DOWN : if Position > Min then
Position := Position - 1;
is (for the most part) handled for you. For example, with the
end; TSlideBar component, we don’t want the user to have to keep
end; the mouse within the slide bar’s boundaries. If mouse capture
was not on, the mouse messages would immediately stop after
the mouse leaves the control.
Figure 2: The KeyDown procedure.

SEPTEMBER 1995 Delphi INFORMANT ▲ 10


On The Cover

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;

Also keep in mind that when a component has captured the


mouse and you move outside its client area, the MouseMove First, the TSlideBar component tells Windows that it now has
messages being generated may be reporting negative X and/or Y the focus. This makes sense since the user has just clicked the
coordinates (since they are relative to the component’s coordinate mouse button on the control. Next, a Boolean value called
system). Make sure your program knows how to handle this. Dragging is set to True or False provided the MouseDown event
occurred within the rectangle that bounds the thumb’s current
Since the TControl object has the csMouseCapture flag on by position. If so, the MouseMove events will enable the thumb to
default, we don’t need to include any code in our component move with the mouse.
to manage the mouse capture. It will just work. However, this
is an important concept to understand for developing well- The third line simply saves the X or Y position of the mouse
behaved components. click depending on whether the component is oriented in a hori-
zontal or vertical position. If the slide bar is a vertical one, we are
Besides providing for the mouse capture, the TSlideBar must also interested in where the mouse is on the Y scale so the thumb can
respond to various mouse events (to enable the mouse to move be moved appropriately. If the slide bar is horizontal, we would
the thumb). Here are the basic activities we want to trap: then be interested in the X coordinate.
• If the user clicks on either side of the thumb, the thumb
should shift a single position in the appropriate direction. Painting the Town
• If the user selects the thumb, he or she should be able to slide One of the basic features of a custom component is its appear-
it back and forth. The thumb should slide smoothly while ance. Sometimes you may inherit a component’s appearance
being dragged, and not hop between positions. (This would from an ancestor, but often you must provide your own “look”
ruin the visual effect that you are holding onto the thumb.) to custom components. The control’s appearance is not an aspect
When the left mouse button is released, the thumb should of component design that should be left for last.
“click” to the closest position (indicated by the tick marks).
Fortunately for us, the object we are likely to descend from
To respond to these activities, we must detect the MouseDown, (TCustomControl) has most of what we need for drawing our
MouseUp, and MouseMove events. The TSlideBar component own component. The key element TCustomControl provides is
overrides these procedures as follows: the Canvas property. With a canvas, we can easily draw until we
are content with the component’s appearance.
protected
procedure MouseUp(Button: TMouseButton; First, you must override the inherited Paint method. This Paint
Shift: TShiftState; X,Y: Integer); override;
procedure MouseDown(Button: TMouseButton; method only provides a dashed rectangle at design time, and
Shift: TShiftState; X,Y: Integer); override; often that is not something we want. In the protected section of
procedure MouseMove(Shift: TShiftState; your component class declaration you would add a line:
X,Y: Integer); override;

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 11


On The Cover

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 12


On The Cover

If necessary, the programmer can override these default behaviors


by marking a resource as “fixed”, rather than “discardable” or
“movable”. This prevents Windows from playing with it. In addi-
tion, you can mark a resource as “pre-load” or “load-on-call” to
control when Windows will load the resource. (Pre-load loads
the resource when the program starts. Load-on-call loads the
resource only the first time the resource is referenced.) However,
unless it is required for performance reasons, it’s better program-
ming practice to let Windows manage the resources on its own
by keeping the default options.

The resources used by TSlideBar consist of a collection of


bitmaps for the different thumbs, and a cursor. (We’ll discuss
the cursor later.) Each bitmap is given an identifying label as
shown in Figure 3.

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).

Figure 4 shows how Circle1Mask is used to determine the por-


tions of Circle1 that we want. (Think of it as a kind of “cookie
cutter”.) If you stacked the two bitmaps, the only portions of
Circle1 that would be painted on the control are those areas
that would show through the black portions of Circle1Mask.
The white portions of the mask would be replaced with what-
ever background pixels happened to be under the thumb.

Loading the bitmaps into the TSlideBar component is fairly straight-


forward. First, we must set up some TBitmap objects to hold the
three we will be dealing with: ThumbBar, ThumbBar Mask, and a
storage bitmap for the Background pixels. In the Create method of Figure 3 (Top): The Delphi Image Editor. Each bitmap is assigned an
identifying label. Figure 4 (Bottom): Creating the mask for the circular
the component, we would set these up with this code: thumb in the Image Editor.

constructor TSlideBar.Create(AOwner: TComponent);


begin
the same dimensions as the currently selected thumb. Then, the
inherited Create(AOwner); background area (that would normally reside under the thumb)
ThumbBmp := TBitmap.Create; is copied onto this temporary bitmap. Next, the mask bitmap is
MaskBmp := TBitmap.Create;
BkgdBmp := TBitmap.Create;
painted onto the temporary bitmap using the SrcAnd copy
{ The rest of the Create method goes here ... } mode. This causes Windows to perform some math on the
end; combination of the pixels in such a way that the black areas of
the mask are erased from the destination image.
Whenever a new thumb style is chosen, the SetThumbStyle method
is called. (It is the “write” access method for the FThumbStyle pri- Now, we paint the bitmap of the thumb over the resulting tem-
vate variable.) The code in Figure 5 shows a portion of what this porary image. This time however, we use the SrcPaint copy mode
method would resemble. Then, when it is time to paint the thumb that causes the image we want to be merged with the existing
on the screen, the real work begins (see Figure 6). Although this background. Then we copy the resulting image to the canvas
code looks a little convoluted, it’s effective. Windows performs with the CopyRect command.
these actions when it deals with minimized icons of running appli-
cations. Each icon has a transparent area around it so that the Notice also that this whole procedure is within a try...finally
desktop color (or image) will show through. structure. This ensures that the temporary bitmap is released
even if an error occurs during the drawing process. This prevents
The first step is to create a temporary bitmap to hold an inter- resource leaks and is a feature you should always keep in mind if
mediate step in the process. The temporary bitmap is made to your component uses memory or other resources.

SEPTEMBER 1995 Delphi INFORMANT ▲ 13


On The Cover

Let’s Give ‘em a Hand!


procedure TSlideBar.SetThumbStyle(A: TThumbStyle); One of the nice features of the TSlideBar component is that it
begin
if ThumbStyle <> A then
has an embedded cursor that can be used at run-time. It’s a
begin little more visually correct to have a hand or finger (versus an
FThumbStyle := A; arrow) move a sliding thumb. You do have the ability to
case ThumbStyle of
tsCircle1 : ThumbBmp.Handle :=
change any of the predefined cursors by altering the compo-
adBitmap(HInstance,'Circle1'); nent’s Cursor property, but you’re limited to the cursors that
tsSquare1 : ThumbBmp.Handle := are provided with Delphi. What if you want your own?
LoadBitmap(HInstance,'Square1');
{ Same for the rest of the bitmaps }
end; In this case, we do want our own because none of the stock cur-
case ThumbStyle of sors would make any more sense than the arrow does. Therefore,
tsCircle1 : MaskBmp.Handle :=
LoadBitmap(HInstance,'Circle1Mask');
a custom cursor was included with TSlideBar. The cursor is cre-
tsSquare1 : MaskBmp.Handle := ated using Image Editor or Resource Workshop, and then saved
LoadBitmap(HInstance,'Square1Mask'); into an .RES file. This .RES file is then bound with the compo-
{ Same for the rest of the masks }
end;
nent when it is compiled.

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-

SEPTEMBER 1995 Delphi INFORMANT ▲ 14


On The Cover

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:

procedure TSlideBar.SetLabels(A: TStringList);


begin
FLabels.Assign(A);
end;

Finally, I added a public procedure called CurrentLabel that


obtains the current string value from the TSlideBar component:

function TSlideBar.CurrentLabel: string;


begin
if ((Position-Min+1) <= Labels.Count) and
(Position >= Min) then
CurrentLabel := Labels[Position-Min]
else
CurrentLabel := '<Un-Defined>';
end;

In your program, you can add a procedure to the TSlideBar com-


ponent’s OnChange event that fetches the current label and then
feeds it into a TLabel on the form. From working with a huge
case statement, we have boiled it down to a single line of code:

procedure TForm1.SlideBar1Change(Sender: TObject);


begin
Label1.Caption := SlideBar1.CurrentLabel;
end;

SEPTEMBER 1995 Delphi INFORMANT ▲ 15


On the Cover
Delphi / Object Pascal

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

A at the top of a form. Typically, a toolbar behaves like a menu. For


example, a user clicks on a button or a menu item, and the application
responds to the Click event. In Delphi, most code executes in response to
events. Thus, most components contain an OnClick event procedure that you
can modify to suit specific applications. The OnClick event procedure, you’ll
soon discover, can be the star of an application.

How to Create a Toolbar


It’s easy to add a toolbar to any Delphi application:
• Add a Panel component to a form.
• Set the Panel’s Align property to alTop to force the toolbar to align itself to the top of the form
even when the form is resized.
• Add controls (usually SpeedButtons) to the panel.
• Assign control properties. For example, a SpeedButton component
needs a glyph, a bitmap image indicating what the SpeedButton does
when clicked.
• Modify the OnClick event procedure for each control to indicate
what the application should do when the control is clicked.

Figure 1 shows a SpeedButton toolbar at


design time.
Figure 1: The sample application
But what if you want to let users create,
at design time.
destroy, or change the properties of the
controls on a toolbar at run-time? You can, but it’s trickier because it
means an application must create new objects (say, SpeedButtons) at
run-time and then assign their OnClick event procedures to actions that
are unknown at design time.

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”.

SEPTEMBER 1995 Delphi INFORMANT ▲ 16


On The Cover

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.

There are a few more details we need to worry about, including:


how to execute a file, how to create an event for each new
SpeedButton, and how to protect the application from I/O
exceptions. Before we discuss these details, however, let’s finish
designing more of the application.

Create the Toolbar Table


First, use the Database Desktop to create a table called
Speedo.DB. There are at least two important pieces of
information to save between sessions to reload a toolbar —
the SpeedButton’s Hint and Glyph properties. Both of
these properties are strings. The Glyph property specifies a
file containing a bitmap to appear on the SpeedButton.
Figure 2: Building the menu with the Menu Deigner. The Hint property is the text (or balloon help) that

SEPTEMBER 1995 Delphi INFORMANT ▲ 17


On The Cover

appears when the user moves the mouse pointer over a


SpeedButton. In addition, in the toolbar application, Hint
serves a second purpose — it contains the full path of a
file or application to execute.
Figure 4: Use the Object
Next, add two string fields to Speedo.DB: Hint and Inspector to set the Table compo-
Glyphfile. These correspond to the Hint and Glyph proper- nent’s TableName property to
ties. Make the Hint field a key field. We’ll use this key Speedo.DB.
later to remove SpeedButtons from the toolbar. Figure 3
shows the structure of this table during a Database
Desktop session. After you create the table, set the table
component’s TableName property to Speedo.DB
(see Figure 4).

Unit Variables The FormCreate Event Procedure


When you create a new form or use the form in the default The FormCreate event procedure is one of the first events
project, Delphi creates a form variable in the var section of that occurs when you run an application. Thus, it’s a good
the form’s unit: event for initializing variables and performing other initial-
ization tasks. In the sample application, when the form
var
opens we’ll create a list to hold the toolbar’s SpeedButtons,
Form1: Tform1;
reset ButtonNum to 0, and get the previous session’s buttons
from the table and load them onto the toolbar.
Add the following variables to that var section:

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.

Creating a New SpeedButton


The CreateNewSpeedButton procedure is the heart of the
GetButtonsFromTable procedure. CreateNewSpeedButton relies
on a very interesting generic Object Pascal type,
TNotifyEvent.

TNotifyEvent is a type of event that notifies a component


when a specific event occurs. OnClick for example, is of type
TNotifyEvent, and it notifies a control that a click event
occurred on the control.

When a user presses a key or clicks the mouse, an event


occurs. Windows sends this event to the window or object
Figure 3: A Database Desktop session. This is the structure of the that was the focus of the key press or mouse click. For exam-
Speedo table (Speedo.DB). ple, if the click occurs on a SpeedButton, you determine the

SEPTEMBER 1995 Delphi INFORMANT ▲ 18


On The Cover

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;

After you’ve defined a TSpeedButtonNotifyEvent type, you can


declare a variable of TSpeedButtonNotifyEvent type, assign a
procedure to the variable, create a new SpeedButton, and
assign its OnClick event procedure to a TNotifyEvent type (in
this case, a TSpeedButtonNotifyEvent type):

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 19


On The Cover

If the flag is on, the Staying On Top


toolbar asks the user Finally, let’s wrap up the application by adding Normal and
if the button is to be AlwaysOnTop menu click event procedures. These allow the
removed. It does this application to either “float” above all other windows on the
by displaying an desktop or to behave normally.
Information box with
Yes and No buttons In Visual Basic, you must call the Windows API to create a
(see Figure 6). If the floating form. However, in Delphi a floating form is a snap.
user selects Yes, the Simply change the form’s FormStyle property to either
button is removed fsStayOnTop or fsNormal. Then, to keep the user informed of
from the table and the form’s status, place a check next to the active menu item.
list (freeing memo-
ry). The toolbar The FormClose Event Procedure
application then The FormClose event procedure occurs when you close a form
immediately returns (or a single-form application). Thus, it’s a good event for
to run mode by turn- cleanup tasks. In this application, when the form closes we’ll
ing the flag off (see release the memory allocated to the button list.
Figure 7). Thus, the Figure 6 (Top): Selecting Buttons |
OnClick event is Remove button displays this information Conclusion
assigned at run-time, dialog box. Figure 7 (Bottom): This mes-
sage is displayed when you click the No
There’s another way you can go with this of course. Instead
and has two func- button on the Information dialog box of using a Paradox table to hold values between sessions, you
tions depending on shown in Figure 6. could use an .INI file. Both approaches are fine, although
the setting of the the Paradox table does require a lot of overhead (the Borland
RemoveButton flag. Database Engine, or BDE) if you want to distribute the tool-
bar as part of an application and don’t need the BDE for
Let’s quickly look at the remaining custom methods. anything else. [For a detailed description of how to imple-
DeleteItemFromTable uses the existing key of a table to find ment an .INI file using Delphi, see Douglas Horn’s article
an item to delete. FreeButtons traverses the button list, “Initialization Rites” in the August 1995 Delphi Informant.]
releasing the memory allocated to each SpeedButton and
removing the SpeedButton from the button list. And that does it. Have fun with this one, and if you cus-
tomize the application, tell me about it. ∆
ShellExecute
The last statement in GenericSpeedButtonClick calls the This article is adapted from material for Gary Entsminger’s
ExecuteFile method. It converts the Pascal strings for Command, forthcoming book The Way of Delphi (Prentice-Hall).
Params, and WorkDir to the null-terminated strings required by
the Windows API function ShellExecute. It then invokes The demonstration program referenced in this article is available
ShellExecute to open the file. on the 1995 Delphi Informant Works CD located in
INFORM\95\SEP\GE9509.
If ShellExecute is asked to open a file, it runs the file if it’s an
executable. Otherwise, it opens the file. Optionally, you can
ask ShellExecute to print a file. Gary Entsminger is the author of The Tao of Objects, an Introduction to Object-orient-
ed Programming, 2nd ed. (M&T 1995) and Secrets of the Visual Basic Masters, 2nd
ed. (Sams, 1994). He is currently working on The Way of Delphi, an advanced Delphi
book for Prentice Hall, and is Technical Editor for Delphi Informant. He can be reached
on CompuServe @71141,3006.

SEPTEMBER 1995 Delphi INFORMANT ▲ 20


On The Cover

Begin Listing One: Toolbar.PAS


unit Toolbar; begin
{ Free the SpeedButton list }
interface ButtonList.Free;
end;
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, procedure TForm1.GetButtonsFromTable;
Graphics, Controls, Forms, Dialogs, StdCtrls, var
Buttons, Menus, ShellAPI, ExtCtrls, DB, DBTables; NewApp: string;
Glyphfile: TFileName;
type begin
{ Define a SpeedButton Notify Event type } try
TSpeedButtonNotifyEvent = Table1.Open;
procedure(Sender: TSpeedButton) of object; Table1.First;
TForm1 = class(TForm) while not Table1.EOF do
MainMenu1: TMainMenu; begin
Style1: TMenuItem; ButtonNum := ButtonNum + 1;
Normal1: TMenuItem; NewApp := Table1.FieldByName('Hint').AsString;
AlwaysOnTop1: TMenuItem; GlyphFile :=
Buttons1: TMenuItem; Table1.FieldByName('GlyphFile').AsString;
OpenDialog1: TOpenDialog; CreateNewSpeedButton (ButtonNum, NewApp, GlyphFile);
Panel1: TPanel; Table1.Next;
Table1: TTable; end;
AddButtons: TMenuItem; finally
Removebutton1: TMenuItem; Table1.Close;
{ events } end;
procedure Normal1Click(Sender: TObject); end;
procedure AlwaysOnTop1Click(Sender: TObject);
procedure AddButtonsClick(Sender: TObject); { Generic execute file routine }
procedure Removebutton1Click(Sender: TObject); procedure TForm1.ExecuteFile(Command, Params, WorkDir:
procedure FormCreate(Sender: TObject); string);
procedure FormClose(Sender: TObject; begin
var Action: TCloseAction); { Convert Pascal string to Null-termintated strings }
private Command := Command + #0;
{ Private declarations } Params := Params + #0;
procedure CreateNewSpeedButton(ButtonNum:Integer; WorkDir := WorkDir + #0;
NewApp: string; GlyphFile: TFileName); { Run/open application/file }
procedure SaveSpeedButton(ButtonNum: Integer; if ShellExecute(Application.MainForm.Handle, 'Open',
NewApp: string; GlyphFile: TFileName); @Command[1], @Params[1], @WorkDir[1],
procedure GetButtonsFromTable; SW_SHOWNORMAL) < 32 then
procedure DeleteItemFromTable(Command: string); MessageDlg('Could not execute ' + Command, mtError,
procedure FreeButtons; [mbOK],0);
procedure ExecuteFile(Command,Params,WorkDir: string); end;
{ Private generic event handler }
procedure GenericSpeedButtonClick(Sender: TSpeedButton); procedure TForm1.FreeButtons;
public var
{ Public declarations } I : Integer;
end; begin
{ Go through the button list until the end is reached;
var * release the memory allocated to each SpeedButton
Form1: TForm1; * remove the button from the list. }
NewSpeedButton: TSpeedButton; for I := 0 to ButtonList.Count do
ButtonNum : Integer; begin
RemoveButton : Integer; { Test flag } NewSpeedButton := ButtonList.Items[0];
ButtonList : TList; NewSpeedButton.Free;
ButtonList.Remove(NewSpeedButton);
implementation end;
ButtonNum := 0;
{$R *.DFM} end;

{ 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);

SEPTEMBER 1995 Delphi INFORMANT ▲ 21


On The Cover

var OpenDialog1.FileName := '';


Command, Params, WorkDir: string; if OpenDialog1.Execute then
begin begin
{ Get the file name and path from hint } ButtonNum := ButtonNum + 1;
Command := Sender.Hint; CreateNewSpeedButton(ButtonNum, NewApp,
{ Edit table - remove button } OpenDialog1.FileName);
if RemoveButton = 1 then SaveSpeedButton(ButtonNum, NewApp,
begin OpenDialog1.FileName);
DeleteItemFromTable(Command); { Remove button } { Set Remove Button flag to false }
FreeButtons; { Remove buttons, release memory } RemoveButton := 0;
GetButtonsFromTable; { Reload new table of buttons } ShowMessage('In Run Mode');
RemoveButton := 0; { Reset button count } end;
ShowMessage('Returning to Run Mode'); end
end else
else ShowMessage('File does not exist.');
{ Execute file } end;
begin end;
Params := '';
WorkDir := ''; { Remove button click event simply sets a remove flag,
ExecuteFile(Command, Params, WorkDir); which the generic click event checks before deciding
end; how to carry out the click event }
end; procedure TForm1.Removebutton1Click(Sender: TObject);
begin
procedure TForm1.CreateNewSpeedButton(ButtonNum: Integer; if MessageDlg('Remove next selected button?',
NewApp: string; GlyphFile: TFileName); mtInformation, [mbYes, mbNo], 0) = mrYes then
var RemoveButton := 1
EventName: TSpeedButtonNotifyEvent; else
begin begin
try RemoveButton := 0;
NewSpeedButton := TSpeedButton.Create(Self); ShowMessage('Returning to Run Mode');
NewSpeedButton.Glyph.LoadFromFile(GlyphFile); end;
NewSpeedButton.Parent := Panel1; end;
NewSpeedButton.Left := (ButtonNum - 1) * 26;
NewSpeedButton.Top := 0; { Add each new SpeedButton to end of SpeedButton table }
NewSpeedButton.Hint := NewApp; procedure TForm1.SaveSpeedButton(ButtonNum: Integer;
NewSpeedButton.ShowHint := True; NewApp: string; GlyphFile: TFileName);
NewSpeedButton.NumGlyphs := 2; { Assume 2-image bitmap } begin
ButtonList.Add(NewSpeedButton); { Add instance to list } with Table1 do { Add to Speedo table }
{ Assign new event, created at runtime, to the try
SpeedButton OnClick event, GenericSpeedButtonClick } Open;
EventName := GenericSpeedButtonClick; Last; { Move to the end of the table. }
NewSpeedButton.OnClick := TNotifyEvent(EventName); Insert;
except FieldByName('Hint').AsString := NewApp;
on E: EInOutError do FieldByName('GlyphFile').AsString := GlyphFile;
begin Post;
MessageDlg('Unable to create SpeedButton. ' + finally;
E.Message, mtInformation, [mbOK],0); Close;
{ Free button resources } end;
NewSpeedButton.Free; end;
ButtonNum := ButtonNum - 1;
end; { Form style click event procedures: an AlwaysOnTop click
end; makes the form 'float' above all other forms and a
end; Normal click allows the form to behave normally }
procedure TForm1.Normal1Click(Sender: TObject);
{ Use the Open Dialog to get a file name and a begin
corresponding bitmap for the new SpeedButton to be AlwaysOnTop1.Checked := False;
associated with the file. } Form1.FormStyle := fsNormal;
procedure TForm1.AddButtonsClick(Sender: TObject); Normal1.Checked := True;
var end;
NewApp: string;
begin procedure TForm1.AlwaysOnTop1Click(Sender: TObject);
OpenDialog1.Title := begin
'Select a file to associate with a button.'; Normal1.Checked := False;
OpenDialog1.Filter := 'All files|*.*'; Form1.FormStyle := fsStayOnTop;
OpenDialog1.FileName := ''; AlwaysOnTop1.Checked := True;
if OpenDialog1.Execute then end;
begin
NewApp:= OpenDialog1.FileName; end.
if FileExists(NewApp) then
begin End Listing One
OpenDialog1.InitialDir := '';
OpenDialog1.Title :=
'Select a glyph for the SpeedButton.';
OpenDialog1.Filter := 'Bitmap files|*.bmp';

SEPTEMBER 1995 Delphi INFORMANT ▲ 22


Informant Spotlight
Delphi / Power Builder

By Thomas Miller

Leaving PB Behind
Delphi vs. PowerBuilder: A Blow-by-Blow Comparison

he perfect enterprise development tool would allow us to program

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”.

The PB Application Painter creates an application shell, opening script,


library paths, and allows you to set the application icon. Delphi creates

SEPTEMBER 1995 Delphi INFORMANT ▲ 23


Informant Spotlight

This is a new painter for PB (version 4) and was sorely needed


for compiling large projects. Compiling code has five separate
steps. And, compiling 15MB of source code can take three hours
to complete. If you change one object in the program, PB highly
recommends you recompile the entire program. In addition, PB
recommends compiling only on your local hard drive, and turn-
ing off Windows SMARTDrive. When you compile in PB, you
are really pre-compiling for the p-code interpreter. New releases
of the interpreter fix many bugs, but inevitably introduce new
ones. In addition, sub-routines that worked fine for months are
suddenly non-functional.

In contrast, Delphi is fast, easy, and specifically supports incre-


mental re-compiles. A 15MB source program will compile in
about 10 to 15 minutes. The compiler options allow you to set
extra debugging features for in-house testing and then later turn
them off for distribution of your program. This allows for
tighter, faster deliverable programs that are true EXEs and DLLs.

The PB Window Painter assembles various other objects or


controls (including data window controls) into a functioning
window. The Delphi form is an object where you can assem-
ble other objects to create a functioning window. The func-
tionality of these two objects is similar.

The PB Menu Painter enables you to design and program


menus (main menus). Delphi has a Menu Designer for creat-
ing MainMenu and PopupMenu components. PB enables you
to make menu items active, inactive, visible, or hidden at run-
time. Delphi gives you complete control of the menu at run-
time to include adding, deleting, changing and merging par-
ent, and child menus.

The PB Structure Painter enables you to graphically declare a


Figure 1 (Top): The PowerBuilder 4.0 opening screen. The floating
structure. Delphi refers to the structure data type as a record.
palette contains icons that open the major painters used by
PowerBuilder. Figure 2 (Bottom): Delphi’s opening screen. Delphi There is no graphical wizard to aid in declaring this type of vari-
uses a single-document interface (SDI) that is compliant with Windows able. Delphi does not facilitate any graphically-assisted coding.
95. The system window, located at the top of the screen, contains the This would require an extra step to convert from the graphical
menu, Speedbar, and a Component Palette. The Object Inspector is on representation to Object Pascal before the code is compiled.
the left. It allows you to set an object’s properties and events. The form
This is a trade off between ease-of-use and speed and reliability.
is on the right. It allows you to design a window of an application.

The PB Preference Painter enables you to set many of the


a project shell, opening code, allows you to set the application preferences used by PB (see Figure 3). Delphi Environment
icon, and default help file for context-sensitive help. Options allow you to set many preferences for the Delphi
IDE (see Figure 4).
Delphi doesn’t support libraries in the same sense as PB. In
PB, the libraries are used to segregate the code into smaller PB presents more options under its Preferences Painter.
manageable pieces of deliverable code. In Delphi, they are Unfortunately, most are codes and difficult to set. Delphi
extensions to the development environment and are referred gives you an adequate number of environment settings that
to as the Visual Component Library (VCL). Not all objects in are easy to access, and you are always welcome to dig into
the VCL are visual. To break up large projects, Delphi sup- the source code to change any or all defaults.
ports DLLs with restrictions which are discussed later in this
article. PB doesn’t directly support context-sensitive help. The PB Database Painter allows you to create and manipu-
late tables and columns, and set extended attributes for
The PB Project Painter allows for configuring a macro for com- columns in a table. The Database Painter also allows you to
piling an application. Delphi enables you to configure compiler browse and manipulate data. The Delphi Database Desktop
options, linker options, and directories for additional resources. supports data browsing and manipulation.

SEPTEMBER 1995 Delphi INFORMANT ▲ 24


Informant Spotlight

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.

The PB Library Painter enables hierarchical graphical display


of objects grouped in libraries. Delphi graphically displays its
programming units.

One of Delphi’s weakest points is its inability to support dis-


tributable libraries. This is only a problem for very large pro-
jects (50 or more windows). In Pascal distributable overlays
are referred to as “overlays”, and are not currently supported
in Delphi or Object Pascal. Unfortunately, PB’s libraries don’t
serve one of their primary functions, which is compiled code
segregation. As mentioned earlier, when you make a change
to one library, PB recommends you re-compile and redistrib-
ute the entire system. PB’s libraries are not truly object-ori-
ented, and are not designed to be shared between multiple
programs. This defeats another major benefit of libraries.

The PB User Object Painter graphically enables you to


program extensions to the programming environment, and
access the Windows API and non-visual objects. PB’s user
objects are a very crude equivalent of Delphi’s components,
but because PB is not object-oriented, you can’t create real

SEPTEMBER 1995 Delphi INFORMANT ▲ 25


Informant Spotlight

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 DataWindow will not support many non-data-related


objects. For example, you can’t place a button or an edit box
on the DataWindow. This can make it difficult to design an
effective graphical user interface (GUI) for your end-user.

The DataWindow uses a data replication algorithm to access


data. This is a major problem in several areas. First, it takes
a long time to retrieve all the data to the workstation and
problems with synchronization can arise. For example, if the
replication gets out of sync with the server database, you
have to retrieve the data from the server again. Large
amounts of data replicated locally consume huge amounts of
resources and can slow the workstation to a crawl. And,
when two DataWindows are sharing the same data, you
have to manage the synchronization manually. Furthermore,
the SQL code is written for you, making it difficult to
change it programmatically.

When distributing an off-the-shelf program, it’s difficult to


programmatically set the database owner. PB writes extended
attributes about the database into the DataWindow. When
you change a column attribute (i.e. size or not null) in the
database, you have to manually fix each DataWindow access-
Figure 5 (Top): PB’s Query Builder. Figure 6 (Bottom): Delphi’s
Visual Query Builder.
ing the table to recognize the table change. This is because
the DataWindow is not truly object-oriented.
components. Nevertheless, user objects help with standard-
izing and re-use. Non-visual user objects are PB’s equivalent Also, the DataWindow is not always sensitive to programmat-
of classes. Delphi supports these programming capabilities ic changes to its buffers (e.g. window “A” assigns a value to
non-graphically. In addition, Delphi extends the program- column “1” in window “B”). In advanced programming, the
ming environment through the VCL. There are also non- DataWindow has three data buffers that must be managed.
visual components. Suddenly, the DataWindow is not the “do-all automatic data-
base interface” tool it claims.
PB has tried to make advanced programming graphical. It just
hasn’t worked. The user-objects are difficult and complex to In PB, the DataWindow represents most of the user inter-
implement. Delphi has taken a more practical, direct approach face. It becomes important for the DataWindow to be flexi-
and requires you to write straight code and place it appropriately. ble in handling design issues in the development environ-
Writing in assembler, accessing DLLs, and writing to the ment and at run-time. One word for the PB faithful:
Windows API is complex enough without adding a graphical dwModify. Need I say more?
encapsulation layer.
For the rest of you, dwModify is a C++/Assembler type syn-
This covers most of the major sub-systems in PB and Delphi tax to modify attributes of the DataWindow programmati-
that are easily compared on a direct basis. Now we’ll compare cally. It is not for the faint of heart! PB has converted much
data access, events, and scripting. This is more of an “apples of the old DataWindow command structure to a BASIC-
and oranges” type comparison. type command structure in version 4. However, there is still
enough functionality available through the old, arcane, syn-
PB Data Access, Events, and Coding tax of dwModify to be annoying.
If you have ever attended a PB seminar you have definitely heard
that “The DataWindow is our crown jewel.” Unfortunately, Windows is an event-driven operating system. Therefore,
these days it’s a chipped and cracked crown jewel. it’s important that any Windows development tool properly
trigger events and related code. PB is sorely lacking in this
The DataWindow is easy to work with and can create a data area. PB recommends using events sparingly. This is partly
access window quickly. It is arguably the most flexible data- due to the large amount of overhead associated with events.

SEPTEMBER 1995 Delphi INFORMANT ▲ 26


Informant Spotlight

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 27


Informant Spotlight

Delphi Data Access, Events, and Coding DataSource Table PrintDialog


Component Component Component
RadioGroup
Scroll Box
Delphi is a new breed of tool featuring a general data-
base access engine that works under the traditional
client/server paradigm. In addition, Delphi sports an
IDE that is truly object-oriented.

When you first start Delphi, the most noticeable dif-


ference is the lack of individual painters. Delphi is a BitBtn
Component
“top-down” development tool. First you open a form
(window), and then place objects on the form: panels,
buttons, edit boxes, and/or other non-visual objects
(see Figure 7). The objects you add to a form can
then contain their own associated objects. Thus the
parent/child/grandchild etc. relationship is created
(see Figure 8).

If this sounds like more work — it is! To lay out a sim-


ple window minus the code may take 15 minutes to do Panel OLEContainer
Components Component
in PB, and as much as 45 minutes in Delphi. However,
don’t get discouraged — there are numerous gains in
other areas of Delphi that more than compensate for the differ-
ence in time and capability.

Delphi’s Component Palette is the centerpiece of its develop-


ment environment. Some 75 objects, visual and non-visual,
are available from this palette. The palette is broken into the
following general categories: Non-Data Aware Components,
Data Access Components, Data Aware Components,
Windows Non-Visual Dialog Components, and System
Components.

Individual components worth special mention are:


• Scrolling Panel: This allows for controlling the scroll bars
when the panel shrinks vertically or horizontally smaller
than a user-defined threshold. (Not available in PB.) Figure 7 (Top): A Delphi form with the following components:
• TabPages: This is two components working together DataSource, Table, PrintDialog, ScrollBox, RadioGroup, two Panels,
allowing multi-page panels with tabs at the bottom of the BitBtn, and an OLEContainer. Figure 8 (Bottom): The object hierarchy
for the form shown in Figure 7.
page, similar to a spreadsheet. (Not available in PB.)
• TabbedNotebook: This component has multiple pages with
large folder-style tabs at the top of the page (see Figure 9). • System Components: These include Timer, PaintBox,
(Not available in PB.) FileListBox, DirectoryListBox, DriveComboBox,
• DataSource: This is an object that, when linked with FilterComboBox, MediaPlayer, OLEContainer,
supporting objects, creates the data connection to the DDEClientConversation, DDEClientItem,
database. It automatically manages commits, rollbacks, DDEServerConversation, and DDEServerItem. (The
adds, saves, deletes, undos and refreshes the data set. (PB same functionality in PB requires extra coding, or is
requires extra coding.) not available.)
• DBNavigator: A graphical object that controls scrolling,
add, edit, delete, undo, and refresh functions for data By using just one of these objects to replace the coding
aware controls. (PB requires extra coding.) necessary in PB, you have recovered your lost 30 minutes.
• DBLookupList / DBLookupCombo: These two controls The only object that is missing is a multi-line grid compo-
allow you to point to another table for population and, on nent. This is one of the most popular and powerful inter-
selection, return the selected item to the current table and face items in PB and the only large hole in the Delphi
column. (PB requires extra coding.) GUI design objects repertoire. Delphi’s grid component is
• Dialog Objects: Include Windows File, Save, Font, Print, a traditional spreadsheet type interface that only supports
Printer Setup, Color Palette, Find, and Find and Replace an edit box interface. However, all other design GUI ele-
dialog boxes. (PB requires extra coding.) ments found in PB are directly supported, surpassed, or
easily reproduced with minor programming in Delphi. Of

SEPTEMBER 1995 Delphi INFORMANT ▲ 28


Informant Spotlight

Delphi directly supports parent/child table relationships in


Figure 9: the database objects, auto-synchronizes two views of the same
Delphi’s table (the equivalent of PB share data), and even supports
TabbedNotebook stored procedures.
component, an
amalgamation of
Conversely, PB only supports stored procedures with cer-
the TabSet and
Notebook tain databases. Delphi has two data control components:
components. Table and Query. Table gives you direct access to the entire
table, is fast, and easy-to-implement. The Query compo-
nent allows you to access the table by limiting the result
set with a SQL statement.
Figure 10: This Delphi
form shows a DBNavigator
There is, however, room for improving Delphi’s Query
control and an extended component. In PB, SQL statements are executed extremely
DBNavigator control at fast. Delphi’s Query component seems to “hiccup” once
run-time. before it sends the SQL statement to the database server.
Depending on the size of the result set, Delphi’s Query
component is still — hands-down — faster than PB. The
course, if you feel there is something lacking, you may Query component returns result sets to an array — not
modify and extend any of the existing components, or variables — and doesn’t support cursors at the database
acquire a third-party component. There are several third server. These are all minor inconveniences at best. The
party grid components that give you some of the function- Query component is effective for simple, basic SQL state-
ality of PB grids (e.g. those from Woll2Woll and ments. And the StoredProcedure component handles very
Orpheus). More important, Delphi and all its components complex SQL sub-routines with ease. You will have to be
are written in Object Pascal making Delphi completely creative with some SQL sub-routines to avoid putting
extensible. For example, Figure 10 demonstrates Delphi’s them into stored procedures.
extensibility with a standard DBNavigator component and
its extended counterpart. Try that with PB! Delphi is based on Object Pascal. Object Pascal rivals the
power and flexibility of C/C++, but is strongly typed (restric-
In Delphi, you control objects through properties and events. tive variable declaration) to keep you out of trouble. The syn-
Delphi provides access to properties and events via its Object tax is different from other mainstream programming tools and
Inspector. In Delphi, all properties are in one easy-to-read takes some time to get comfortable with. Blocks of code start
dialog box, while PB spreads them across several menu items. with begin and finish with end (see Figure 11). This includes
sub-blocks of code found within if statements. Each statement
In general, Delphi supports 20 to 25 percent more proper- ends with a semicolon. This saves you from using the amper-
ties per object than PB. Setting most properties in Delphi is sand character to continue the line as you must do with PB.
usually accomplished through a drop-down list box and can
programmatically be set using the same keywords as found In Delphi, the only place this isn’t true is in the if statements.
in the Object Inspector. For instance, the TabStop property Placing a semicolon in the wrong place is similar to placing
can be either True or False in the Object Inspector. To pro- an endif (in PB) in the wrong place. In Delphi, this type of
grammatically change TabStop to False, the statement is: error is much more difficult to find.

TabStop := False

No dwModify and no guessing. If the attribute is supported in


the Object Inspector, it can be controlled programmatically.

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 29


Informant Spotlight

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!

SEPTEMBER 1995 Delphi INFORMANT ▲ 30


DBNavigator
Delphi / Object Pascal

By Cary Jensen, Ph.D.

Data Validation: Part II


Delphi’s Data-Aware Components

ast month’s article introduced the basics of data validation, and explored

L how to apply validation to edit controls not associated with database


tables. This month’s “DBNavigator” continues this discussion by consider-
ing the issues that apply to data-aware components, that is, those that permit
you to edit tables.

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.

Most table formats provide for further restrictions to be placed on


entered data. Paradox tables, for example, have a feature called validity

SEPTEMBER 1995 Delphi INFORMANT ▲ 31


DBNavigator

checks. Using validity


checks you can specify Figure 2: An
that dates earlier than exception
January 1, 1910 (for raised by
example) cannot be Delphi when
entered into the an attempt is
made to post
DateOfBirth field. a record con-
taining invalid
SQL tables provide you data to a
with even greater con- table.
trol over acceptable val-
ues for fields in the
Figure 1: When a field is instantiated, form of triggers. A trig-
a TField descendant is created to rep- ger is a mechanism on
resent the field. These TField compo- the database server that One characteristic that both these techniques share is their need
nents provide default validation based executes a custom code for you to instantiate (create) the field object. As discussed in a
on field type.
routine in response to previous article [Cary Jensen’s “DBNavigator” column in the
certain events. For example, you can create a trigger that will June 1995 Delphi Informant], this is done using the Fields Editor.
execute a validation routine when any SQL client (such as
Delphi) attempts to post the contents of a changed record (i.e. The following example demonstrates both types of field-
save the record to the table). Furthermore, if the code executed level validation. It makes use of the \DBDEMOS files
by the trigger determines that the contents of the record being installed with Delphi. If you want to follow along with this
posted are invalid, the record is rejected and no changes are demonstration but do not have the files on your computer,
made to the database. you should reinstall Delphi using a Custom installation to
install only the sample files.
The advantages of table-based validation are many. First, the
validation is stored with the table to which it applies. Begin by creating a new project. On the form, place the
Consequently, every application that has access to the data is following components: a DataSource, Table, DBGrid, and
affected. Second, table-based validation often means that it is DBNavigator. The form you create may resemble the one
unnecessary to add additional validation to controls on forms. in Figure 3.
This is particularly advantageous when the same tables appear
on many different forms in an application.
Figure 3:
Table-based validation impacts Delphi applications through A new
exceptions. When Delphi attempts to post a new or modified form for
record, the record will only be posted if the record passes all validation
demon-
table-based validation. If the record cannot be posted, an
stration.
exception is raised. Figure 2 displays an exception created
when a record violates a minimum value validity check for a
Paradox table.

The exception shown in Figure 2 deserves additional com-


ment. Paradox for Windows developers have come to expect
that field-level validation will occur when a field is departed Select the DataSource component and set its DataSet prop-
(except for Required validity checks that are applied when the erty to Table1. Select the Table component and set its
record is posted). In Delphi applications these validity checks DatabaseName property to DBDEMOS (the alias defined by
are not tested until the record is being posted, even when Delphi when you install the sample files). Set the TableName
Paradox tables are being used. property to VENDORS.DB, and the Active property to True.

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-

SEPTEMBER 1995 Delphi INFORMANT ▲ 32


DBNavigator

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 33


DBNavigator

Validating Fields from Event Handlers


Validating fields using event handlers is more direct when you
Figure 6:
are using data-aware components instead of an Edit component.
An excep-
(An Edit component is a text box that is not associated with a tion raised
field in a table.) An Edit component provides only a few events by an
that are appropriate for attaching validation code, with the OnValidate
OnExit event being the best (although less than ideal) solution. event han-
dler.

When it comes to field validation, data-aware field objects are


easier to use. This is because instantiated fields possess an
event handler that best represents the data validation needs of
developers. This event handler is OnValidate.
an exception (silent or otherwise) if your code determines
The OnValidate event triggers whenever Delphi attempts to that the record is invalid.
accept a value entered by the user. You can attach code to this
event, and explicitly test the value that the user has entered Like the preceding demonstration of field-level validation,
into the field. If your code determines that the value is record-level validation involves using event handlers. For our
invalid, you can prevent the value from being accepted by example, we’ll use the BeforePost event handler, which
raising an exception. belongs to all descendants of the TDataSet class (this
includes Table, Query, and StoredProc components). The
In most cases, you will raise an exception to prevent a value code you add to the BeforePost event handler is used to evalu-
from being accepted in one of two ways. The first is to use ate the fields (or other related controls) on the form. If your
the raise keyword to create a new exception, and the second code determines that the record is not valid, generating an
is to create a silent exception using the Abort procedure. The exception prevents the record from being posted.
raise keyword is used in the following demonstration.
The following example demonstrates this technique. In this
When you use raise within an OnValidate event handler, you case, however, the use of a silent exception is demonstrated.
can create an exception that will display an error message and Begin by selecting the Table component. Then, from the
prevent the field from being accepted. While there are a num- Events page, open the BeforePost event handler and add the
ber of ways of doing this (including techniques requiring that following code:
you define an exception object, as demonstrated in last if Table1Address1.Value = '' and
month’s “DBNavigator”), the easiest way is to use raise with Table1Address2.Value <> '' then
the Create method of the exception class. This can be accom- begin
MessageDlg('Address1 cannot be blank',
plished with the following line of code: mtError,[mbOK],0);
Abort;
raise Exception.Create('Your error message goes here') end;

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 34


DBNavigator

OnCloseQuery event handler from the Object Inspector.


Enter the following code:
Figure 7:
Code in the if MessageDlg('Close this form',mtConfirmation,
BeforePost [mbYes,mbCancel],0) <> mrYes then
event han- CanClose := False;
dler displays
an error Compile the form and run it. Next, double-click the Control
message. menu to close the form. This will trigger the form’s
OnCloseQuery event handler, and the dialog box shown in
Figure 8 will be displayed. If you select Yes, the form will
close. Otherwise, the code in the event handler assigns the
value False to CanClose, and the form remains open.
Form-Level Validation
Form-level validation is performed on the entire form, not
just a single field or record. Most commonly, this type of
validation is employed when the user has finished entering Figure 8:
data into the form. Like field-level and record-level valida- This Confirm
tion, form-level validation is performed using event han- dialog box
dlers. Specifically, this takes place in the form’s asks the user
to confirm a
OnCloseQuery event handler. request to
close a form.
Before continuing, it’s important to note that closing a
form does not destroy it. It only makes the form invisible
to the user. (You destroy a form using either the Release or
Free methods.)
Conclusion
The objects on a closed form can still be accessed by other Data validation is an important part of your Delphi applica-
forms in your application. Typically, the code you place in the tions. Whenever possible, you should use the validation
OnCloseQuery event handler ensures that this data is valid, offered by your data tables, and the properties of your form’s
and therefore usable by forms that may read the data from components to ensure that entered data is valid.
the closed form.
When this does not provide the level of validation that you
Unlike the OnValidate and BeforePost event handlers, you do require, you can use the valuable event handlers that Delphi
not raise an exception if your validation determines that the provides to add this essential capability to your applications. ∆
form should not be closed. Instead, you assign the value False
to the Boolean formal parameter CanClose, which is passed by The demonstration project referenced in this article is available
reference to the OnCloseQuery event handler. The default value on the 1995 Delphi Informant Works CD located in
for this parameter is True. INFORM\95\SEP\CJ9509.

The following demonstrates a simple use of the


OnCloseQuery event handler. In this case, the only valida- Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database
tion that occurs is when the user confirms his or her inten- development company. He is a developer, trainer, author of numerous books on data-
base software, and Contributing Editor of Delphi Informant. You can reach Jensen Data
tion to close the form. Begin by selecting the form’s Systems at (713) 359-3311, or through CompuServe at 76307,1533.

SEPTEMBER 1995 Delphi INFORMANT ▲ 35


OP Basics
Delphi / Object Pascal

By Charles Calvert

Strings: Part II
Stripping and Manipulating Object Pascal Strings

ast month we introduced Object Pascal strings, and began to discuss

L the basics of string manipulation — searching for a substring within a


string, and parsing lengthy strings. As promised, in this month’s install-
ment, we’ll talk about stripping blanks from the end of a string, and functions
for manipulating 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;

SEPTEMBER 1995 Delphi INFORMANT ▲ 36


OP Basics

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:

The function then decrements the value of i and returns to DE3251994.TXT


the top of the loop to see if the next character is a space:

while S[i] = ' ' do begin


{----------------------------------------------------
Name: GetTodayName function
If the next character is a space, it is also deleted from the end Declaration: GetTodayName(Pre, Ext: string): string;
of the string. The entire process is repeated until the last char- Unit: StrBox
Code: S
acter in the string is no longer a space. At that point, the
Date: 03/01/94
function ends and a string is returned that is guaranteed not Description: Return a filename of type PRE0101.EXT,
to have any spaces appended to the end of it. where PRE and EXT are user supplied strings,
and 0101 is today's date. PRE must not be
longer than 2 letters.
Becoming familiar with functions such as StripBlanks is -----------------------------------------------------}
essential for all serious programmers. It isn’t really that this function GetTodayName(Pre, Ext: string): string;
var
one particular function is so important (although I do use
y, m, d, dow : Word;
this function fairly often). What is important is that begin
StripBlanks is the kind of function that solves a common GetDate(y,m,d,dow);
Year := Int2StrPad0(y, 4);
programming problem, and that it does so by manipulating a
Delete(Year, 1, 2);
chunk of data — byte by byte. GetTodayName := Pre + Int2StrPad0(m, 2) +
Int2StrPad0(d, 2) +
Date-Based File Names end;
Year + '.' + Ext;

Now let’s turn our attention to another Delphi function that


manipulates strings. This function is not likely to be used daily
by most programmers, but when you do need it, it’s very handy. Figure 3: The GetTodayName function.

SEPTEMBER 1995 Delphi INFORMANT ▲ 37


OP Basics

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:

To achieve this end, GetTodayName needs a special function Year := Int2StrPad0(y,4);


Delete(Year,1,2);
that will not only convert a number to a string, but also pad
it with an appropriate quantity of zeros. The Int2StrPad func-
In this case, a 4 is passed to Int2StrPad0, because the year was
tion is what we need (see Figure 4).
originally a four-digit number.
{----------------------------------------------------
Name: Int2StrPad0 function
As mentioned earlier, the Delete function is built into the
Declaration: Int2StrPad0(N: LongInt; Len: Integer): string; Delphi language. It deletes characters from a string, starting
Unit: MathBox at the offset specified in the second parameter. The number
Code: N
Date: 03/01/94
of characters to be deleted is specified in the third parame-
Description: Converts a number into a string and pads ter. (You can search on “Delete” in Delphi’s on-line help for
the string with zeros if it is less than more details.)
Len characters long.
-----------------------------------------------------}
function Int2StrPad0(N: LongInt; Len: Integer): string; If you step back now and view the GetTodayName function as
var a whole, you can see that it allows you to pass in the first two
S : string;
begin
letters and the extension for a file name. In return, it supplies
Str(N:0,S); you with a string containing the prefix and extension, plus
while Length(S) < Len do the current date. It’s great to have functions like this available
S := '0' + S;
Int2StrPad0 := S;
to you when you need them.
end;
Using the Move and FillChar Functions
Figure 4: The Int2StrPad function. The next two built-in Delphi methods we’ll examine are fast
and powerful. Speed of this sort is a luxury, but it comes replete
with some dangers that you must be sure to sidestep. In particu-
This very useful function first uses the built-in Pascal routine
lar, neither the FillChar nor the Move function has much in the
called Str to convert a LongInt into a string. If the string that
way of built-in error checking. FillChar is usually used to initial-
results is longer than Len bytes in length, the function sim-
ize an array, record, or string. It will, however, fill a structure not
ply exits and returns the string. However, if the string is less
only with zeros but with whatever character you specify.
than Len bytes, the function appends zeros in front of it
until it is Len characters long. Here is the transformation
FillChar takes three parameters. The first is the variable you
caused by each successive iteration of the while loop if N
want to copy bytes into, the second is the number of bytes
equals 2 and Len equals 4:
you want to fill, and the third is the character you want
2 { First iteration } placed in those bytes:
02 { Second iteration }
002 { Third iteration } procedure FillChar(var X; Count: Word; value);
0002 { Fourth iteration }

Consider the following array:


The function checks to see if the string is four characters
long. If it isn’t, the function adds a zero to the beginning var
of the string: MyArray: array[0..10] of Char;

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);

SEPTEMBER 1995 Delphi INFORMANT ▲ 38


OP Basics

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

SEPTEMBER 1995 Delphi INFORMANT ▲ 39


OP Basics

The first step is to determine the number of characters in


{---------------------------------------------------- the sentence after the first word is removed. This is found
Name: StripFirstWord function
Declaration: StripFirstWord(S : string) : string;
by subtracting the number returned by Pos from the total
Unit: StrBox length of the sentence. StripFirstWord then moves the
Code: S remaining portion of the string from a position “offset”
Date: 03/02/94
Description: Strip the first word from a sentence,
characters deep in the following string:
return the shortened sentence. Return original
string if there is no first word. She was a child and I was a child, In a kingdom by the sea
-----------------------------------------------------}
function StripFirstWord(S : string) : string;
var
to the very first spot in the string:
i, Size: Integer;
S1: string; was a child and I was a child, In a kingdom by the sea sea
begin
i := Pos(#32, S);
if i = 0 then begin
The extra characters — represented in this case by the second
StripFirstWord := S; occurrence of “sea” — are then lopped off by setting the
Exit; length byte to the appropriate number of characters:
end;
was a child and I was a child, In a kingdom by the sea
Size := (Length(S) - i);
Move(S[i + 1], S[1], Size);
S[0] := Chr(Size); The function then returns the first word of the sentence, and
StripFirstWord := S;
end;
also the shortened sentence.

The StripFirstWord function is not perfect. For instance, some


Figure 5: The StripFirstWord function.
readers may have noticed that the function would not per-
form as advertised if the first characters of the string passed to
“Williams”, Pos would return 0 because there is no space char- it were spaces. Overall, however, it does the job required of it.
acter in the string. If the function does not find a space charac-
ter in the string, it returns an empty string: Of course, you could write a function that strips spaces
from the beginning of a string. Then you could first pass a
if i = 0 then begin
StripFirstWord := '';
string to the new function you created, and then pass it on
Exit; to the StripFirstWord function. (We’ll do this in the next
end; article of this series.)

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 40


On the Cover
Delphi / Object Pascal

By Richard D. Holmes

A Stopwatch Component
Embedding Assembly Language in Object Pascal
to Call the Windows Virtual Timer Device

he hallmark of Delphi is flexibility combined with performance. One

T example of this flexibility is Delphi’s ability to combine a very high-level


language, Object Pascal, with embedded assembly language and even
in-line machine code. Using these features, I have built two high-speed soft-
ware stopwatches.

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 41


On the Cover

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.

SEPTEMBER 1995 Delphi INFORMANT ▲ 42


On the Cover

factor is the uncertainty in cal-


ibrating the overhead, which
appears to be on the order of
2-3 microseconds.

By comparison, the DOS system clock, which ticks only 18


Figure 1 (Top): The times per second, has a resolution of 55 milliseconds (55,000
TStopWatch and microseconds). Although the Windows API function
TProfilingStopWatch compo- GetTickCount returns the number of milliseconds since
nents installed on the Windows was started, the value is only updated every 55 mil-
Component Palette. Figure
liseconds. Therefore, its actual resolution is identical to that
2 (Bottom): The user inter-
face for Example 1. of the DOS clock. The resolution of TStopWatch is therefore
over 2,000 times better than the system clock used by DOS
and Windows, and the resolution of TProfilingStopWatch is
nearly 20,000 times better.

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.

For a stopwatch that is used to measure the interval between two


external events, t1 and t2, no correction is needed for overhead.
The measured interval x = u2 - u1 is equal to t2 - t1, since the
lag time a is identical for both events. The shortest interval, x,
that can be measured between two external events is therefore b
+ a. On a 486/66 PC, this is about 22 microseconds.

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).

SEPTEMBER 1995 Delphi INFORMANT ▲ 43


On the Cover

• 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

SEPTEMBER 1995 Delphi INFORMANT ▲ 44


On the Cover

done within the Delphi environment and within the structural


function VTD_GetTime: Double;
framework of Object Pascal with its excellent support for
begin;
strong type checking and safe, structured programming. { Call VTD to get current "micro-ticks". }
asm
Class TStopWatch builds upon the foundation provided by { Load the entry point }
VTIMERDV to create an easy-to-use Delphi component that mov dx,wVTDSegment
provides a high-speed timer with the ability to resolve exter- mov ax,wVTDOffset
{ Push our call-back address }
nal events that are separated by intervals as short as 25 push cs
microseconds. Class TProfilingStopWatch is a specialized mov bx, offset @RetSpot
descendant of TStopWatch that can profile code blocks as push bx
short as 2-3 microseconds. ∆ { Push the entry point }
push dx
push ax
The stopwatch components and demonstration forms referenced in
{ Load ID for function VTD_Get_Real_Time }
this article are available on the 1995 Delphi Informant Works mov ax,$100
CD located in INFORM\95\SEP\RH9509. { Call the VTD }
retf

@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;

End Listing Two

SEPTEMBER 1995 Delphi INFORMANT ▲ 45


On the Cover

Begin Listing Three: The StpWatch Unit procedure TStopWatch.Start;


{ Exports classes TStopWatch and TProfilingStopWatch. } begin
unit StpWatch; if TimerIsRunning then
{ do nothing }
interface else
begin
uses Classes, VTimerDv; dStartTime := VTD_GetTime;
TimerIsRunning := True;
type end;
TStopWatch = class(TComponent) end;
{ A simple stopwatch, appropriate for timing external
events. } procedure TStopWatch.Stop;
private begin
TimerIsRunning: Boolean; { Stop the timer and update the accumulator. }
dStartTime: Double; if TimerIsRunning then
dCurrTime: Double; begin
dAccumTime: Double; dCurrTime := VTD_GetTime;
public dAccumTime := dAccumTime + (dCurrTime - dStartTime);
{ Uses default constructor. All data fields are TimerIsRunning := False;
initialized to zero. } end
function IsRunning: Boolean; virtual; else
procedure Start; virtual; ; { do nothing }
procedure Stop; virtual; end;
procedure Reset; virtual;
function ElapsedTime: Double; virtual; procedure TStopWatch.Reset;
end; begin
{ Stop the timer and reset the accumulator. }
TProfilingStopWatch = class(TStopWatch) TimerIsRunning := False;
{ A specialized stopwatch for profiling Delphi dAccumTime := 0.0;
programs. Automatically corrects for the overhead end;
of calling the Windows VTD. Calibration is done
via class methods and class variables, thereby function TStopWatch.ElapsedTime: Double;
ensuring that overhead corrections are identical
for all instances. } begin
private
lNumSplits: LongInt; { Return the elapsed time in seconds. }
public if TimerIsRunning then
{ Uses default constructor. All data fields are begin
initialized to zero. } { If the timer is running, return the current
procedure Stop; override; "split" time. }
procedure Reset; override; dCurrTime := VTD_GetTime;
function ElapsedTime: Double; override; result := dAccumTime + (dCurrTime - dStartTime);
class procedure CalibrateOverhead; end
class function OverheadForStop: Double; else
class function OverheadForSplit: Double; result := dAccumTime;
class procedure SetOverheadForStop end;
(dNewOverheadForStop: Double);
class procedure SetOverheadForSplit
(dNewOverheadForSplit:Double); procedure TProfilingStopWatch.Stop;
end; begin
{ Stop the timer and update the accumulator. }
procedure Register; if TimerIsRunning then
begin
implementation dCurrTime := VTD_GetTime;
dAccumTime := dAccumTime + (dCurrTime - dStartTime)
var - dOverheadForStop -
{ Class variables shared by all instances of lNumSplits*dOverheadForSplit;
TProfilingStopWatch. } TimerIsRunning := False;
dOverheadForStop: Double; end
dOverheadForSplit: Double; else
; { do nothing }
function TStopWatch.IsRunning: Boolean; end;
begin
result := TimerIsRunning; procedure TProfilingStopWatch.Reset;
end; begin
{ Stop the timer and reset the accumulator. }

SEPTEMBER 1995 Delphi INFORMANT ▲ 46


On the Cover

dAccumTime := 0.0; { Measure the overhead of starting and stopping the


lNumSplits := 0; stopwatch. }
TimerIsRunning := False; const
end; NumCalls = 100;
var
function TProfilingStopWatch.ElapsedTime: Double; CalibrationWatch: TProfilingStopWatch;
begin dSplitTime: Double;
{ Return the elapsed time in seconds. } i: Integer;
if TimerIsRunning then begin
begin CalibrationWatch := TProfilingStopWatch.Create(nil);
{ If the timer is running, return the current dOverheadForStop := 0.0;
"split" time. } dOverheadForSplit := 0.0;
dCurrTime := VTD_GetTime;
inc(lNumSplits); { Measure the overhead of stopping the watch. }
result := dAccumTime + (dCurrTime - dStartTime) CalibrationWatch.Reset;
- lNumSplits*dOverheadForSplit; for i := 1 to NumCalls do
end begin
else CalibrationWatch.Start;
result := dAccumTime; CalibrationWatch.Stop;
end; end;
dOverheadForStop :=
class function TProfilingStopWatch.OverheadForStop: Double; CalibrationWatch.ElapsedTime / NumCalls;
begin
result := dOverheadForStop; { Measure the overhead of reading a split time. }
end; CalibrationWatch.Reset;
CalibrationWatch.Start;
class function TProfilingStopWatch.OverheadForSplit: for i := 1 to NumCalls do
Double; dSplitTime := CalibrationWatch.ElapsedTime;
begin CalibrationWatch.Stop;
result := dOverheadForSplit; dOverheadForSplit := (CalibrationWatch.ElapsedTime -
end; dOverheadForStop) / NumCalls;
CalibrationWatch.Free;
class procedure TProfilingStopWatch.SetOverheadForStop end;
(dNewOverheadForStop: Double);
begin procedure Register;
dOverheadForStop := dNewOverheadForStop; begin
end; RegisterComponents('System',[TStopWatch]);
RegisterComponents('System',[TProfilingStopWatch]);
class procedure TProfilingStopWatch.SetOverheadForSplit end;
(dNewOverheadForSplit: Double);
begin initialization
dOverheadForSplit := dNewOverheadForSplit; TProfilingStopWatch.CalibrateOverhead;
end; end.

class procedure TProfilingStopWatch.CalibrateOverhead;


End Listing Three

SEPTEMBER 1995 Delphi INFORMANT ▲ 47


On the Cover

Begin Listing Four: The Main Unit DisplayResults;


{ Example 1 -- How to use class TStopWatch. } end;
unit Main;
procedure TMainForm.btnStart2Click(Sender: TObject);
interface begin
StopWatch2.Start;
uses DisplayResults;
SysUtils, WinTypes, WinProcs, Messages, Classes, end;
Graphics, Controls, Forms, Dialogs, StdCtrls, StpWatch;
procedure TMainForm.btnStop1Click(Sender: TObject);
type begin
TMainForm = class(TForm) StopWatch1.Stop;
StopWatch1: TStopWatch; dTime1 := StopWatch1.ElapsedTime;
StopWatch2: TStopWatch; DisplayResults;
btnStart1: TButton; end;
btnStop1: TButton;
btnReset1: TButton; procedure TMainForm.btnStop2Click(Sender: TObject);
btnSplit1: TButton; begin
btnStart2: TButton; StopWatch2.Stop;
btnStop2: TButton; dTime2 := StopWatch2.ElapsedTime;
btnReset2: TButton; DisplayResults;
btnSplit2: TButton; end;
lblTime1: TLabel;
lblTime2: TLabel; procedure TMainForm.btnReset1Click(Sender: TObject);
cbRunning1: TCheckBox; begin
cbRunning2: TCheckBox; StopWatch1.Reset;
dTime1 := StopWatch1.ElapsedTime;
Label1: TLabel; DisplayResults;
Label2: TLabel; end;
procedure FormActivate(Sender: TObject);
procedure btnStart1Click(Sender: TObject); procedure TMainForm.btnReset2Click(Sender: TObject);
procedure btnStop1Click(Sender: TObject); begin
procedure btnReset1Click(Sender: TObject); StopWatch2.Reset;
procedure btnSplit1Click(Sender: TObject); dTime2 := StopWatch2.ElapsedTime;
procedure btnStart2Click(Sender: TObject); DisplayResults;
procedure btnStop2Click(Sender: TObject); end;
procedure btnReset2Click(Sender: TObject);
procedure btnSplit2Click(Sender: TObject); procedure TMainForm.btnSplit1Click(Sender: TObject);
procedure DisplayResults; begin
private dTime1 := StopWatch1.ElapsedTime;
{ Private declarations } DisplayResults;
public end;
{ Public declarations }
end; procedure TMainForm.btnSplit2Click(Sender: TObject);
begin
var dTime2 := StopWatch2.ElapsedTime;
MainForm: TMainForm; DisplayResults;
end;
implementation
procedure TMainForm.DisplayResults;
{ $R *.DFM } begin
lblTime1.Caption := FormatFloat('0.000000',dTime1);
var lblTime2.Caption := FormatFloat('0.000000',dTime2);
dTime1: Double; if StopWatch1.IsRunning then
dTime2: Double; cbRunning1.State := cbChecked
else
procedure TMainForm.FormActivate(Sender: TObject); cbRunning1.State := cbUnchecked;
begin if StopWatch2.IsRunning then
dTime1 := 0.0; cbRunning2.State := cbChecked
dTime2 := 0.0; else
DisplayResults; cbRunning2.State := cbUnchecked;
end; end;

procedure TMainForm.btnStart1Click(Sender: TObject); end.


begin
StopWatch1.Start; End Listing Four

SEPTEMBER 1995 Delphi INFORMANT ▲ 48


On the Cover

Begin Listing Five: TProfilingStopWatch procedure TfmMain.FormActivate(Sender: TObject);


{ Example 2 -- How to use class TProfilingStopWatch. } begin
unit Main; ebIterations.Text := '10';
ebStopOverhead.Text :=
interface FormatFloat('0.0000000',
uses TProfilingStopWatch.OverheadForStop);
SysUtils, WinTypes, WinProcs, Messages, Classes, ebSplitOverhead.Text :=
Graphics, Controls, Forms, Dialogs, StdCtrls, StpWatch, FormatFloat('0.0000000',
VTimerDv; TProfilingStopWatch.OverheadForSplit);
btnRun.SetFocus;
type btnRunClick(self);
TfmMain = class(TForm) end;
{ Non-Visual components }
ProfilingStopWatch1: TProfilingStopWatch; procedure TfmMain.btnRunClick(Sender: TObject);
ProfilingStopWatch2: TProfilingStopWatch; var
ProfilingStopWatch3: TProfilingStopWatch; I, j, lIterations, lNumInnerStops,
ProfilingStopWatch4: TProfilingStopWatch; lNumInnerSplits: LongInt;
{ Visual components } X, dTotalTime, EmptyTime, WorkTime, Overhead,
btnRun: TButton; dRemainder, dSplitTime: Double;
btnSetOverhead: TButton; begin
btnRecalibrate: TButton; lIterations := StrToInt(ebIterations.Text);
ebIterations: TEdit;
ebStopOverhead: TEdit; ProfilingStopWatch1.Reset;
ebSplitOverhead: TEdit; ProfilingStopWatch2.Reset;
lblTotalTime: TLabel; ProfilingStopWatch3.Reset;
lblTotalPct: TLabel; ProfilingStopWatch4.Reset;
lblEmptyTime: TLabel; lNumInnerStops := 0;
lblEmptyPct: TLabel; lNumInnerSplits := 0;
lblWorkTime: TLabel;
lblWorkPct: TLabel; { Start the overall timer. }
lblOverhead: TLabel; ProfilingStopWatch1.Start;
lblOverheadPct: TLabel;
lblRemainder: TLabel; { Run ProfilingStopWatch2 in an empty loop. }
lblRemainderPct: TLabel; for i := 1 to lIterations do
lblStopOverhead: TLabel; begin
lblSplitOverhead: TLabel; ProfilingStopWatch2.Start;
lblSplitTime: TLabel; ProfilingStopWatch2.Stop;
lblSplitPct: TLabel; lNumInnerStops := lNumInnerStops + 2;
Label1: TLabel; end;
Label2: TLabel;
Label3: TLabel; { Run ProfilingStopWatch3 in a loop with splits. }
Label4: TLabel; ProfilingStopWatch3.Start;
Label5: TLabel; for i := 1 to lIterations do
Label6: TLabel; { Get split time }
Label7: TLabel; dSplitTime := ProfilingStopWatch3.ElapsedTime;
Label8: TLabel; ProfilingStopWatch3.Stop;
Label9: TLabel; inc(lNumInnerStops);
Label10: TLabel; lNumInnerSplits := lNumInnerSplits + lIterations;
Label11: TLabel; { Recalculate x by repeated multiplication. }
procedure FormActivate(Sender: TObject); ProfilingStopWatch4.Start;
procedure btnRunClick(Sender: TObject); inc(lNumInnerStops);
procedure btnSetOverheadClick(Sender: TObject); X := 1.0;
procedure btnRecalibrateClick(Sender: TObject); for i := 1 to lIterations do
private for j := 1 to i do
{ Private declarations } begin
public X := sqrt(X);
{ Public declarations } X := ln(X);
end; X := exp(X);
X := X * X;
var end;
fmMain: TfmMain;
ProfilingStopWatch4.Stop;
implementation inc(lNumInnerStops);

{ $R *.DFM } { Stop the overall timer. }


ProfilingStopWatch1.Stop;

SEPTEMBER 1995 Delphi INFORMANT ▲ 49


On the Cover

{ Format and display the results. }


dTotalTime := ProfilingStopWatch1.ElapsedTime;
dEmptyTime := ProfilingStopWatch2.ElapsedTime;
dSplitTime := ProfilingStopWatch3.ElapsedTime;
dWorkTime := ProfilingStopWatch4.ElapsedTime;
dOverhead :=
lNumInnerStops*TProfilingStopWatch.OverheadForStop +
lNumInnerSplits*TProfilingStopWatch.OverheadForSplit;
dRemainder := dTotalTime - dEmptyTime - dSplitTime -
dWorkTime - dOverhead;

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;

procedure TfmMain.btnSetOverheadClick(Sender: TObject);


begin
TProfilingStopWatch.SetOverheadForStop
(StrToFloat(ebStopOverhead.Text));
TProfilingStopWatch.SetOverheadForSplit
(StrToFloat(ebSplitOverhead.Text));
lblStopOverhead.Caption := FormatFloat('0.0000000',
TProfilingStopWatch.OverheadForStop);
lblSplitOverhead.Caption := FormatFloat('0.0000000',
TProfilingStopWatch.OverheadForSplit);
end;

procedure TfmMain.btnRecalibrateClick(Sender: TObject);


begin
TProfilingStopWatch.CalibrateOverhead;
lblStopOverhead.Caption := FormatFloat('0.0000000',
TProfilingStopWatch.OverheadForStop);
lblSplitOverhead.Caption := FormatFloat('0.0000000',
TProfilingStopWatch.OverheadForSplit);
end;

end.

End Listing Five

SEPTEMBER 1995 Delphi INFORMANT ▲ 50

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy