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

Delphi Informant 95 2001

The document is the December 1995 issue of Delphi Informant magazine. It includes articles on building a stream-based persistent object library, using the Delphi Visual Component Library capabilities to store and load components to and from streams. Other articles discuss using the MRU list, creating an electronic poker game, using Windows API regions to create invisible mouse-sensitive areas, and database navigation techniques. The issue also includes a product review of ReportPrinter and letters to the editor responding to a previous article on why programmers love Delphi.

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)
118 views

Delphi Informant 95 2001

The document is the December 1995 issue of Delphi Informant magazine. It includes articles on building a stream-based persistent object library, using the Delphi Visual Component Library capabilities to store and load components to and from streams. Other articles discuss using the MRU list, creating an electronic poker game, using Windows API regions to create invisible mouse-sensitive areas, and database navigation techniques. The issue also includes a product review of ReportPrinter and letters to the editor responding to a previous article on why programmers love Delphi.

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/ 52

December 1995 - Volume 1, Number 8

Objects in the Stream


Building a Stream-Based Persistent Object Library

ON THE COVER
7 Objects in the Stream — Alan Ciemian Cover Art By: Doug Smith
The Delphi VCL includes powerful capabilities for storing
and loading components to and from streams, but these
capabilities are difficult to implement and are nearly 44 At Your Fingertips — David Rippy
undocumented — until now. Mr Ciemian presents a set Mr Rippy, our favorite Object Pascal trickster, returns with
of classes that provide the framework for a stream-based techniques for: changing the color of selected rows in a
persistent object library. DBGrid, executing other programs from a Delphi applica-
tion, an efficient way to play .WAV files, and a nifty
Database Desktop quick tip.
FEATURES
46 Visual Programming — Blake Versiga
18 Informant Spotlight — Douglas Horn Mr Versiga gets creative with the Delphi Outline com-
The MRU list is a standard fixture of shrink-wrapped ponent and uses it to track the lineage of — yes —
Windows applications, but is not in Delphi’s ready-made Great Pyraneese dogs. It’s an introduction to the many
bag of tricks. This is no longer a problem however, uses of the Outline component including its owner-
because Mr Horn shows us how to present users with a list draw capabilities.
of their most recently used files.
REVIEWS
23 Useless Stuff — David Faulkner
Need a break? How about an electronic poker game? This 50 ReportPrinter — Product review by Bill Todd
month, Mr Faulkner introduces a CardDeck component and Consisting of four Delphi components, Nevrona Designs’
then uses it to create a little bit of Las Vegas. And despite ReportPrinter 1.1 is a compact alternative to Delphi’s
this column’s name, you’re bound to glean one or two ReportSmith, but is it up to the task of heavy-duty report-
useful techniques. ing? Mr Todd reviews the new tool, as well as the Delphi
component included as part of Crystal Reports 4.5.
33 Sights & Sounds — Sedge Simons, Ph.D.
Need hotspots? That is, do you need to create irregularly-
shaped mouse-sensitive areas on a form? Need them to DEPARTMENTS
be invisible? No problem. Dr Simons shows us how to use 2 Editorial
Windows API regions to create unorthodox interfaces. 3 Delphi Tools
5 Newsline
38 DBNavigator — Cary Jensen, Ph.D.
When it comes to databases, locating specific records
quickly — with or without an index — is what it’s all
about. And Delphi offers a wealth of methods for the task.
Dr Jensen shares seven demonstration projects, and you’ll
want to hit CompuServe to get them all.

DECEMBER 1995 Delphi INFORMANT ▲ 1


Symposium
“If you disagree violently with some of my choices I shall be pleased.
We arrive at values only through dialectic.”

— Anthony Burgess, 99 Novels

here was no violent reaction per se to Vince Kellen’s article “Why Do Programmers Love Delphi?”
T in our October 1995 issue, but reaction there was. Let’s start with this note from Mr Sarasalo:

Vince Kellen, number of developers who are mak- Send flames to: Dominique Léger, in any of the trade magazines, and I
I think you are right on the money ing it their #1 coding tool. Down Québec could not find anyone at Borland who
on how Delphi feels "thinner". with VB! Long live Delphi! could help me. Is Local InterBase
Thinner than VB, ObjectPAL, Access Tim Bacon (York, England) PS: I love the Delphi Informant. It’s included in the BDE distribution rights
or Actor, but no thinner than C/SDK. pages are filled with useful informa- that comes with the client (desktop)
Comparing it to ObjectPAL, how- And finally, from what may tion and helpful hints. Can’t leave version of Delphi? ...To use InterBase
ever, I think there is another now be a sovereign nation, the office without it! is clearly more pain and hassle than to
aspect: encapsulation. Object Pascal this message from Mr Léger: use Paradox tables. Yet the long-term
enables, and if you do more than Love is a strong word, and benefits of InterBase are clear. ... Are
just add features to forms, encour- Dear Editor, deeply appreciated in this there other independent software
ages good interface design between I am writing this letter in response context. Perhaps there is developers and shareware authors out
different parts of a software project. to Vince Kellen’s article “Why Do more passion at work here there who find this Local InterBase
So, in my experience, while Programmers Love Delphi?” in the than you admit? Many matter troubling? Please comment.
ObjectPAL is great for small jobs October 1995 issue of Delphi thanks in any case, and here Many thanks, Paul
and in-house productivity enhance- Informant. ... I generally agree with is Vince’ reply: (P. K. Winter, Toronto, ON)
ments, when one is working on a his arguments but I feel he missed
larger project that needs to be the most important feature of Dominique brings up a great point Yes there are. This is a com-
maintained over time, Delphi's Delphi: Delphi Is Easy. about how easy it is to understand the mon question, and now,
Object Pascal is far superior. Nothing can beat the ease of use Object Pascal language, but I have to thanks to InterBase Product
Then code maintenance with when one can type disagree with Dominique when he Marketing Manager Keith
Delphi's editor is much easier than MyWindow.Width := 100 or says that programmers aren’t looking Bigelow, I have an answer.
with ObjectPAL's editor. MyWindow.Caption := ‘My Title’. for a challenge of learning a new tool It takes the form of a news
Wilhelm Sarasalo, Pacific Software We switched from C to Delphi like Delphi. I am using the word chal- item and appears on page
because it’s easy to program, fast to lenge in a positive sense, not a nega- 10. And thank you Paul for
This message from Mr Bacon compile, and generates self-con- tive one. I have found that many (not the kind words about my
ends with some sloganeering: tained executables. Personally, I all) programmers get tired of working two favorite magazines.
prefer C syntax, but since we are in in the same environment unless there
Vince, a very competitive market, efficient is some challenge. Whether it’s learn- Thanks for reading.
I saw your article ( "101 reasons tools can’t be ignored. In fact, give ing the complexities of writing serious
why programmers prefer Delphi", or me a Delphi tool with C syntax and custom components or delving into
some such title ) and felt compelled I’m switching back ... Windows events, Delphi provides a
to respond. You dismissed the idea I don’t think programmers are smooth ramp to give programmers of
that the incredible depth and breadth looking for the challenge of learning all skill levels a challenge.
of the Object Pascal language was the a new tool like Delphi. I don’t want Vince Kellen
reason why Delphi was such a cool to fight with my knife when I’m eat- Jerry Coffey,
product. I have to disagree! ing a steak. I just want the meat cut, And in closing, I have anoth- Editor-in-Chief
All the coders at this site love nothing more. Mr Kellen is mistaken er topic I’d like to address
Delphi because of Object Pascal: it here. I believe the challenge lies in this month. CompuServe: 70304,3633
hides and simplifies the complicated the greater possibilities offered to Internet: 70304.3633@com-
stuff by default, but when you need the programmer when he is devel- Dear Jerry: puserve.com
the power routines you can switch oping an application because he can Just as I continue to enjoy the Fax: 916-686-8497
them in easily. The IDE is really easy do much more in less time. Delphi is Paradox Informant magazine, I Snail: 10519 E. Stockton
to work with, and what better exam- a sharp knife. find the Delphi Informant a gold Blvd., Ste. 142,
ple code could you get than the mine of news, ideas, and tech- Elk Grove, CA
source of the product itself? As is your logic Dominique. niques. Keep it coming! 95624
Borland has really come up with a But does his postscript With respect to Delphi, I have a con-
great product, and it shows in the betray him? cern which I have not seen addressed

DECEMBER 1995 Delphi INFORMANT ▲ 2


Delphi TurboPower Releases Async Professional for Delphi
TurboPower Software of B+, ASCII, and more. It has Includes source code; example programs;
T O O L S Colorado Springs, CO is ship- an event-driven dialing engine, documentation; free support via phone,
ping Async Professional for flexible modem database with fax, and e-mail; free updates; and a 60-
New Products Delphi, a serial communica- over 100 pre-configured day money-back guarantee.
and Solutions tions library with VCL com- modems, and logging and trac-
ponents for Delphi. ing tools for debugging. APD Contact: TurboPower Software, PO Box
Async Professional for Delphi can use COMM.DRV, other 49009, Colorado Springs, CO 80949-9009
(APD) is the first communica- COMM.DRV-compatible
tions toolkit to include ready-to- replacements, or FOSSIL. It Phone: (800) 333-4160 or
use terminal windows, protocol includes an ANSI terminal (719) 260-9136
status, modem selection, and emulator with scrollback and
dialing dialog boxes as compo- capture. Fax: (719) 260-7151
nents in Delphi’s native VCL.
All the library functions, from Price: US$179. No royalties required. E-Mail: CIS: 76004,2611
serial port access to file transfers,
Delphi Training Tour
are implemented as VCL com-
ponents. APD also has a unique
Grumpfish Inc. has posted its architecture that offers automat-
training schedule through June
1996. It offers three courses: ic background functions, such
Introduction to Delphi (one day), as file transfers, dialing terminal
Delphi Fundamentals (two days),
and Delphi Advanced (three days). updating, etc. The port compo-
The classes are US$295 to nent generates events under pro-
US$995, with a discount offered
for early registration. grammer-defined conditions.
For more information call This approach maximizes com-
1-800-GO-DELPHI or e-mail
CompuServe: 102121,2741 munications throughput and
or Internet: responsiveness of the application
training@ZacCatalog.com.
to user input.
APD also offers a full selec-
tion of protocols including
Z/Y/XModem, CompuServe

ReportWorks 1.03 for Delphi Ships


HSoftWare of Kitchener, ReportWorks offers left, Inspector, the preview page
Ontario, has released version right, or center column justifi- can be set to have a black bor-
1.03 of ReportWorks for cation, pre-defined line sizes der outline.
Delphi. The ReportWorks and widths, standard graphic Tracking of report page
component allows developers shapes, and imported images lines shows the developer the
to create reports with few lines such as bitmaps and metafiles. report’s printing status, and
of code and little overhead. It also supports memo fields, line height can be set by text
and text strings longer than height or lines-per-inch.
255 characters can be output
with automatic word-wrap Price: US$34.95 (no royalties, source
within the report borders. code included). Shareware is available on
With ReportWorks, page mea- the Informant and Delphi CompuServe
surements can be made in forums, filename RPTWORKS.ZIP.
inches, millimeters, or points, Registration can be made in the SWREG
and the fonts, pens, and forum (ID# 6751).
brushes are customizable.
ReportWorks can direct Contact: HSoftWare, 385 Fairway Road
reports to the built-in preview South, Suite 4A-154, Kitchener, Ontario,
screen, the printer, or to a file. Canada N2C 2N9
The screen redraw enables the
user to quickly skim or skip Fax: (519) 894-0739
report pages, and zoom to any
percentage. From the Object E-Mail: CIS: 76741,2077

DECEMBER 1995 Delphi INFORMANT ▲ 3


Delphi Objective Software Technology Introduces ABC for Delphi
Objective Software
T O O L S
Technology has released ABC
for Delphi, which extends
New Products
Delphi’s VCL with over 25
and Solutions
visual control components for
building database applications,
graphical systems, or general
programming.
ABC for Delphi provides
central error handling with
messages, log files, and error
locations that tie exceptions to
original source code. A data-
base version also offers table-
driven messages and error logs.
New Delphi Books Developers can monitor
reports and log program execu- added to forms, and there are UK (+44 181 994 4842),
Delphi Developer’s Guide
Xavier Pacheco & Steve Teixeira tion using the Stopwatch com- special components to add K&R Software in Germany
Sams Publishing ponent. The Navigator compo- these effects to the background (+49 2272 901585) or GUI
ISBN: 0-672-30704-9
Price: US$49.99 nent scrolls records up to four of MDI main forms. Several Computing in Australia (+61
(907 pages, CD-ROM) times faster than the standard picture buttons provide 3 9804 3999).
Phone: (800) 428-5331
Delphi navigator, and adds extra advanced graphics, 3D labels,
buttons for jumps, bookmarks, and repeating click events on a Price: Runtime Version 1.0a, US$95; Pro
and append processing. form. A transparent button Version 1.0a with source code, US$184.
The DBTableGrid and can also detect mouse move-
DBQueryGrid components ment over other controls. Contact: Objective Software Technology,
build a complete database form Demonstration versions are PO Box E138, Queen Victoria Terrace, ACT
and require only three property available in the Informant and 2600, Australia
settings for a live connection. Delphi CompuServe forums,
This eliminates the need to link filenames: ABC1DEMO.EXE, Phone: +61 6 273 2100
additional Table, Query, and and ABC1COMP.EXE.
DataSource components. For orders call ZAC catalogs Fax: +61 6 273 2190
Shaded backgrounds and in the USA (1-800-GO-DEL-
tiled images can easily be PHI), QBS Software in the E-Mail: CIS: 100035,2441

VCaLive Offers New 3D Features


Odyssey Technologies, Inc. of tive appearance with bitmap- ping, automatic font-sizing of
Cincinnati, OH has released textured 3D labels and shapes, text boxes, and grid printing.
VCaLive, a VCL component as well as animation effects.
pack for Delphi. These compo- This pack includes JazLabel Price: US$89.
nents give applications a distinc- and JazShape which extend the
functionality of Delphi’s Label Contact: Odyssey Technologies, Inc.,
and Shape components. PO Box 62733, Cincinnati, OH 45262-0733
Developers can add impact with
3D extrusion, shadowing, and Phone: (800) 293-7893
bitmap texturing. In addition,
the JazCredits and JazBanners Fax: (513) 777-8026
components provide animation
by scrolling information either Web Site: http: //ww2.eos.net/odyssey/
horizontally or vertically.
VCaLive also has a Layout E-Mail: Internet: odyssey@eos.net
unit to extend Delphi’s Printers
unit. It features margin settings,
column printing, word-wrap-

DECEMBER 1995 Delphi INFORMANT ▲ 4


InterBase Distribution, Pricing, and Licensing Explained
News Scotts Valley, CA — Every
copy of Delphi and Delphi
the SQL their application
requires as well as the server
L I N E Client/Server ships with a copy objects to make their database
of the Local InterBase Server applications perform: triggers,
December 1995 that allows Delphi developers stored procedures, and views.
to design and prototype Local InterBase applications
client/server applications can scale up to any of the 13
remotely on one machine. This versions of InterBase currently
means that development can available, including Windows
occur on a laptop while on the NT, Novell NetWare, and 10
train, airplane, or at the cus- versions of UNIX.
tomer site, and the ultimate For example, if an applica-
database to be used can be tion will be deployed against
computer to the NT database
changed when the application InterBase for NT, the developer
server’s data volume, without
is ready to be deployed. can use Delphi and the Local
worrying about rewriting server
Because InterBase is an InterBase Server to create a
objects such as triggers and
ANSI SQL 92-compliant serv- database application with SQL
stored procedures. Once copied
er, all the dynamic SQL and queries, views, and stored pro-
to the NT server, the database
transactions that Delphi devel- cedures. The resulting applica-
Borland Reports a Profit and its objects automatically
Borland International Inc. has opers write in their application tion, running on the Windows
announced a net income of adopt the security of NT, the
US$2.6 million (US$.08 per
against Local InterBase are or Windows 95 development
multi-user capability of NT,
share) on revenues of US$51.3 immediately applicable to any computer, can then be scaled
million for its second fiscal quar- and the performance of a dedi-
ter ending September 30, 1995. multi-user SQL server such as up to a multi-user application
These results reflect the second
cated NT database server. The
InterBase NT, Oracle 7, Sybase by copying the database to an
consecutive quarter of profitabili- Delphi application, while until
ty since Borland restructured in 10, and Microsoft SQL Server. installed InterBase Workgroup
January 1995, and focused its now used an alias to point to
strategy on software developers.
InterBase workgroup and Server for NT. Using the
the database on the develop-
The net income for the six enterprise developers receive Windows File Manager, the
months ending September 30, ment laptop, requires one sim-
1995 was US$5.4 million the greatest advantage by using developer can drag and drop
(US$.18 per share) on revenues
ple change to redirect the alias
Local InterBase. They can write the database from the laptop
of US$105.1 million. so that it now points to the
According to Borland’s President,
InterBase NT Server’s copy of
Gary Wetsel, this progress was
particularly pleasing because UK Borland Developers Conference 1996 the database.
Borland overcame the seasonal
slowness in Europe and the uncer- London — Borland Inter- All Borland Developers To clarify, the standard
tainty surrounding Windows 95.
He also said the results reflect national UK Limited, Desktop Conference delegates will Delphi package includes Local
growth in their client/server busi- Associates Limited, and receive a free copy of Borland InterBase, but no distribution
ness, including Delphi
Client/Server and InterBase. Dunstan Thomas Limited have software, documentation, and rights. Delphi Client/Server
Net income for the same quar- planned the UK Borland a diskette or CD containing includes Local InterBase, and
ter in the prior fiscal year was
US$4 million (US$.01 per share) Developers Conference, 1996. all the conference sessions and Local InterBase distribution
on revenues of US$81.3 million.
The event will be held in code, and a free tote bag con- rights. When distributed,
London, England, April 28- taining sample issues of Delphi Local InterBase is a single-user
30, 1996 at the Royal Informant, Paradox Informant, database.
Lancaster Hotel, Lancaster and Oracle Informant. To take a prototype into pro-
Gate, London. Pricing for the UK Borland duction, the developer would
In the last few months, the Developers Conference 1996 buy either the NT or NLM
UK Borland Developers for both days is £495.00 + version (US$650), the number
Conference has added 10 vat. Those attending only one of user licenses (US$195 each,
new conference sessions. day pay £250.00 + vat. US$1670/10 pack), and then
With a total of 50 sessions, Discounts are available for move the database to the serv-
these meetings will discuss three or more attending from er’s data volume. (Use File
all Borland products, includ- the same company. Manager for moving from
ing: Delphi, Paradox, C++, For more information phone Windows 3.1 to Windows NT,
client/server solutions, Desktop Associates Limited at use Server Manager’s Back-up
InterBase, and others. +44 (0)171 381 9995, fax +44 and Restore for going from
They will also address pro- (0)171 381 9777, or e-mail CIS: Windows 3.1 to NetWare.)
gramming, solutions, tools and 100016,552 (Internet: Like Delphi, InterBase has a
techniques, and methodologies. 100016.552@compuserve.com). money-back guarantee.

DECEMBER 1995 Delphi INFORMANT ▲ 5


Informant Communications Group Launches New Web Site
News Elk Grove, CA — Informant
Communications Group, Inc.
ICG apparel, and general
information.
the editor. ICG will also have a
variety of links to third-party
L I N E (ICG) has launched a new The magazine section consists vendors that provide add-in
Web site on the Internet. of several pages covering every products for Delphi, Paradox,
December 1995 Located at http://www.infor- aspect of Delphi Informant, and Oracle.
mant.com, this site offers Paradox Informant, and Oracle Users can also browse the
detailed information about all Informant. One of its more third-party catalogs produced by
Informant publications and unique features is the Table of ICG including Delphi Power
services. The new Informant Contents outlining the current Tools, Paradox Power Tools, and
Web site replaces the issue’s articles. Users will be able Borland C++ Power Tools. For
Informant Bulletin Board to sample content from each those interested in advertising in
(ICGBB) which will be discon- magazine, and have access to a an ICG magazine or catalog,
tinued December 31, 1995. listing of bookstores that carry advertising media kits can be
From the initial Web site, ICG publications. A magazine requested via this Web page.
users can choose from several article index will also be con- In early 1995, ICG opened
sections, including magazines, tained on the Web page. the Informant forum on
catalogs, CDs, files to down- At the Web site, customers CompuServe (GO ICGFO-
load, advertising, ICG news, can place subscription orders RUM). The files located in
Database &
for any ICG magazine, check this forum will also be avail-
Client/Server World
DCI’s Database & Client/Server
SD 95 East: Update the status of a subscription, able on the Internet at
World is scheduled for December request a free sample issue, and ftp.informant.com. Details on
5-8, 1995 at the Navy Pier in
Washington, DC — At place orders for back issues. the ICG ftp site were not
Chicago. The event will feature Software Development 95 East, Users can also order the Delphi available at press time.
six conferences covering a variety
of client/server and database Borland wasted no time getting Informant Works and Paradox Companies and user groups
topics. Over 300 venders are Delphi32 into the spotlight.
scheduled to display their Informant Works CD-ROMs. interested in linking their
products and services. The October event hosted over Those interested in contribut- Web site to the ICG Web site
For more information,
contact DCI at (508) 470-3870
150 venders and 8,000 profes- ing articles to any ICG publica- should contact Carol
or e-mail sionals, and the Borland booth tion will have access to writer Boosembark via CompuServe
74404.156@compuserve.com.
was full during the entire 3-day style guides and editorial calen- at 72702,1274 or call (916)
show. Groups of 40 or more dars, and can submit letters to 686-6610 ext. 16.
Borland Promotes developers gathered for
Gross, Rosenberg, demonstrations of Delphi32,
and Bartelmie
Borland International Inc. has Paradox 7, and C++ 5.0.
TurboPower’s Orpheus Bundles with BSS
promoted Paul Gross to senior
vice president of Research and
On the eve of the show, Application Framework
Development, Jonathan B. Borland held a product dem- Fairfax Station, VA — gram, and one DLL that main-
Rosenberg to vice president of
Borland C++ and Companion onstration and reception. C++ Business Software Systems has tains all the search windows.
Products Development, and Product Manager Bill Dunlap announced an agreement to The units include a registration
Marcia Bartelmie to vice presi-
dent of Human Resources. began the presentation with an bundle TurboPower’s Orpheus splash screen, a custom login
overview of the new version of with their new BSS Applica- screen, a Single Document
C++. This was followed by the tion Framework. Interface (SDI) menu panel
Dover Elevator Systems The BSS Application with custom DBNavigator, and
Delphi case study, and Delphi Framework was developed to maintenance, transaction, post-
Product Manager Zack maintain consistent program ing, system setup, and search
Urlocker concluded the design and increase program- screens.
evening with a discus- ming efficiency. It can advance a The application framework
sion of the new fea- development project two to extensively uses Orpheus to
tures in Delphi32. three months into its develop- offer features not found in
SD 96 West will wel- ment cycle, according to the many standard Delphi VCL
come over 300 venders company. In the bundle, the controls.
to the Moscone Center BSS Application Framework The BSS Application Frame-
on March 25-29, 1996. consists of three projects, 10 work and Orpheus bundle is
For information call units, a custom DBNavigator, available for US$495 per user.
Miller Freeman at and a project guide. For more information,
(415) 905-2702, or The three projects include contact Business
visit their web site at one for development, one mas- Software Systems, Inc.
http://www.mfi.com/sdconfs. ter project that is the final pro- at (703) 503-5600.

DECEMBER 1995 Delphi INFORMANT ▲ 6


On the Cover
Delphi / Object Pascal

By Alan Ciemian

Objects in the Stream


A Framework for a Stream-Based Persistent Object Library

elphi strongly encourages and supports object-oriented programming.

D However, Delphi lacks a simple mechanism for object persistence.


Although the Visual Component Library (VCL) includes powerful capa-
bilities for storing and loading components to and from streams, these capabili-
ties are difficult to implement and are nearly undocumented. Furthermore, for
managing simpler non-component persistent objects, Delphi’s component
streaming facilities seem like overkill.

This article presents a set of classes that provide the framework for a stream-based persistent
object library. In some ways, the classes resemble the streaming facilities of Borland’s OWL
(Object Windows Library), but they take full advantage of Delphi’s updated class model and
class registration facilities, and integrate well with the VCL.

The design decisions of these classes were based on:


1) Full support of heterogeneous objects and lists of objects.
2) Simple class interfaces that “feel” like Delphi.
3) Reasonable performance and storage efficiency without compromising items 1 and 2.

Overview
Before getting into the details, I will present a quick survey of the class-
es, their relationships to each other, and their relationships to the VCL
hierarchy (see Figure 1).

TacStreamable is an abstract class that defines the required interface for


classes supporting stream-based persistence. It’s a subclass of TPersistent,
Delphi’s base class for persistent classes. The TPersistent class is the high-
est class in the VCL hierarchy accepted by Delphi’s run-time class regis-
tration facilities. TPersistent also declares the Assign method (and its pro-
tected implementation AssignTo), the VCL’s interface for object assign-
ment. The TacStreamable class requires both features.

TacObjStringList is a base class for lists of persistent objects. It’s reason-


able to suspect that TacObjStringList is a subclass of TStrings or TList.
However, because it’s extremely useful to treat an entire list as a single
persistent object, I descended TacObjStringList from TacStreamable. If
Delphi supported multiple inheritance, TacObjStringList would be a sub-

DECEMBER 1995 Delphi INFORMANT ▲ 7


On the Cover

type
TacStreamable = class(TPersistent)
protected
{ Centralized field initialization }
Figure 1: The procedure InitFields; virtual;
custom classes { Stream interface }
presented in this constructor CreateFromStream(Stream: TacObjStream);
article and their procedure SaveToStream (Stream: TacObjStream);
position in the virtual; abstract;
procedure ReadFromStream(Stream: TacObjStream);
VCL hierarchy.
virtual; abstract;
{ Property methods }
function GetAsString: string; virtual;
public
{ Constructors }
constructor Create;
constructor CreateClone(const Other: TacStreamable);
{ Properties }
class of TStrings and TacStreamable. Since it does not, property AsString: string
read GetAsString;
TacObjStringList acquires its list characteristics through anoth- end;
er form of code reuse — namely containment.
{ TacStreamable implementation }
TacObjStream is an abstract class that defines the required constructor TacStreamable.Create;
interface for streams that read and save TacStreamable objects. begin
It provides the functionality for saving and reading objects to inherited Create;
InitFields;
any TStream subclass. It’s abstract because it does not include end;
the logic for managing the actual TStream.
constructor TacStreamable.CreateClone
( const Other : TacStreamable );
TacFileObjStream is a TacObjStream subclass that implements
a file-based stream for persistent objects. In another example begin
of containment, the file stream functionality is provided by a Create;
Assign(Other);
privately managed instance of TFileStream. end;

TacStreamable: Building Persistent Objects constructor TacStreamable.CreateFromStream


( Stream : TacObjStream );
Objects that need persistence must be instances of a subclass begin
of TacStreamable. This class defines the object’s responsibilities Create;
in the streaming process. Figure 2 shows the source listing for ReadFromStream(Stream);
end;
the TacStreamable class.
procedure TacStreamable.InitFields;
The protected virtual methods, SaveToStream and begin
end;
ReadFromStream, are the most critical. Subclasses must override
these two methods to provide the necessary logic for saving and function TacStreamable.GetAsString;
reading the state of their objects. The TacObjStream class relies begin
Result := '';
on these methods for reading and saving all persistent objects. end;

The protected virtual method, InitFields, provides for class- Figure 2: The source listing for TacStreamable.
specific private initialization. All three constructors declared
for the TacStreamable class call this method either directly or TacStreamable subclasses must override either the
indirectly. Since TacStreamable does not contain any data TPersistent.AssignTo or the TPersistent.Assign method to
fields, the default implementation of InitFields is empty. define the copy semantic of the class.

The Create constructor creates an instance with a default CreateFromStream is a protected constructor called by
state by calling the inherited constructor and the virtual TacObjStream to create an instance from a stream. After call-
InitFields method. ing the Create constructor to initialize the instance to a
default state, it calls the ReadFromStream method to update
The CreateClone constructor creates an instance from the state from the stream.
another assignment-compatible instance. It relies on the
Create constructor to initialize the object to a default state, The public property, AsString, returns the string representa-
and the Assign method, inherited from TPersistent, to copy tion of a persistent object. The protected virtual method,
the other object’s state. CreateClone is analogous to a C++ GetAsString, provides the implementation. TacObjStringList
copy constructor. Since it depends on TPersistent.Assign, instances use the string representation as a means of refer-

DECEMBER 1995 Delphi INFORMANT ▲ 8


On the Cover

encing and displaying their contained objects. By default, time, each instance of the original class is associated with an
GetAsString returns an empty string. object of a second class, to which it delegates, or forwards, all
related method calls.
TacObjStringList: Managing Persistent Objects
If your application only uses a few or a fixed number of persis- Specifically, the contained list reference determines the list behav-
tent objects, saving and reading them one by one is probably ior for TacObjStringList instances. Depending on the actual class
sufficient. For applications that create a large or indeterminate of the contained list object, individual TacObjStringList instances
number of persistent objects, the ability to manage objects in can behave as if inherited from TStringList, TListBoxStrings,
heterogeneous lists is critical. The TacObjStringList class is a base TComboBoxStrings, or any other subclass of TStrings.
class for persistent object containers. As previously discussed, the
TacObjStringList class is a subclass of TacStreamable and sup- Although the TacObjStringList class uses string lists internal-
ports streaming the entire list as a single persistent object. ly, it does not directly expose these to its clients. Items are
inserted and removed as TacStreamable objects only. The
The TacObjStringList class acquires its list characteristics value of each object’s AsString property determines its associ-
through containment. Containment is an alternative ated string. This string identifies objects in the list and visu-
means of code reuse that allows a class to use the services ally represents objects in lists associated with visible controls.
of another class by maintaining a reference to an instance
of that class. The TacObjStringList class supports object ownership and also
provides insertion and deletion notifications for its subclasses.
Containment has advantages and disadvantages as com-
pared to inheritance. To expose the methods of the con- One last note before examining the implementation: the
tained object, the class must declare its own corresponding design of TacObjStringList facilitates its use in conjunction
methods that eventually call the methods of the contained with list controls. Consider the following source:
object. The resulting advantages include:
• control over method visibility, var
ObjList : TacObjStringList;
• the ability to redefine the interface to the contained ...
object’s services, and { ListBox refers to a TListBox component placed on a form }
• the ability to modify the behavior of any of the contained ObjList := TacObjStringList.Create(ListBox.Items,False);

object’s methods.
This code creates a TacObjStringList object, ObjList, that refer-
The resulting disadvantages include: ences the string list associated with a specific list box, ListBox.
• the added function call required, and Used in this manner, ObjList also acts as a list manager for
• the inability to access the contained object’s protected ListBox. You can perform most list manipulations, including
interface. insertions, deletions, and persistent storage, directly on the
ObjList and have the results automatically reflected in the visible
Delphi includes two distinct types of list classes: the familiar list. This reduces coupling the application’s logic to the interface,
string lists (subclasses of TStrings) used by numerous VCL limiting the effects of interface changes (such as changing from
components and the more generic TList class. Each item in a a list box to a combo box) to a few lines of code.
string list consists of a string and an associated object. Each
item in a TList is a generic pointer. The implementation of TacObjStringList can logically be
divided into two parts: list management and persistence.
The TacObjStringList class uses a contained list reference of Figure 3 shows the source listing for the class interface. The
type TStrings. Using the TStrings class instead of the TList following discussion covers the major aspects of the class.
class results in a more versatile class that works well in com- Refer to the complete source for the remaining details.
bination with Delphi’s components (especially list-based
controls such as TListBox and TComboBox). TStrings is an The TacObjStringList Class
abstract class that defines the interface for all of Delphi’s Five private fields relate to list management. The FList field is a
string lists, including TStringList, TListBoxStrings, and reference to the contained list object. FOwnList is a Boolean flag
TComboBoxStrings. Since the list reference type is TStrings that specifies ownership of the list referenced by FList.
instead of a particular subclass, the TacObjStringList class FOwnObjects, another Boolean flag, specifies ownership of the
can work directly with any type of string list. objects contained in the list. In both cases, ownership means the
TacObjStringList object has responsibility for freeing the owned
Using the TStrings reference in this manner, TacObjStringList object(s). The FOnInsert and FOnDelete fields are method refer-
simulates a limited form of delegation. Delegation, in an ences for the insertion and deletion notification events.
object-oriented sense, refers to a form of per-object inheri-
tance that allows different instances of a single class to behave Public properties provide access to the TStrings object (read-
as if they were each inherited from a different class. At run- only), the count of objects in the list (read-only) and the list

DECEMBER 1995 Delphi INFORMANT ▲ 9


On the Cover

type
TacObjListIndex = Integer; { Indexing into lists } public
TacObjListCount = LongInt; { For saving list count { Construction/Destruction }
to stream. } constructor Create(const Strings : TStrings;
const OwnObjects : Boolean);
type { for TacObjStringList notifications } destructor Destroy; override;
TacObjListNotifyEvent = { List object access }
procedure (Idx: TacObjListIndex) of object; function AtIndex(const Idx: TacObjListIndex):
TacStreamable;
type function AtName(const Name: string): TacStreamable;
TacObjStringList = class(TacStreamable) { Standard list methods }
private procedure BeginUpdate;
{ Reference to contained list } procedure EndUpdate;
FList : TStrings; function Add(const Obj: TacStreamable):
{ Flag for list ownership } TacObjListIndex;
FOwnList : Boolean; procedure Insert(const Idx: TacObjListIndex;
{ Flag for list item ownership } const Obj: TacStreamable);
FOwnObjects : Boolean; procedure Move(const FromIdx: TacObjListIndex;
{ Delete notification } const ToIdx: TacObjListIndex);
FOnDelete : TacObjListNotifyEvent; { Deletes delete the objects if they are owned }
{ Insert notification } procedure DeleteIdx(const Idx: TacObjListIndex);
FOnInsert : TacObjListNotifyEvent; procedure DeleteObj(const Obj: TacStreamable);
procedure ResetList(const Strings : TStrings; procedure DeleteName(const Name: string);
const OwnObjs : Boolean); procedure DeleteAll;
procedure CloneContents(const OtherList: { Removes NEVER delete the objects }
TacObjStringList); function RemoveIdx(const Idx: TacObjListIndex):
procedure FreeList; TacStreamable;
procedure FreeObjects; function RemoveObj(const Obj: TacStreamable):
{ Property access methods } TacStreamable;
function GetCount: TacObjListIndex; function RemoveName(const Name: string):
protected TacStreamable;
{ TPersistent overrides } { ObjStringList specific methods }
procedure AssignTo(Dest: TPersistent); override; procedure UpdateObjectName(const Idx: TacObjListIndex);
{ TacStreamable overrides } { Public properties }
procedure InitFields; override; property Strings: TStrings
procedure ReadFromStream(Stream: TacObjStream); read FList;
override; property Count: TacObjListIndex
procedure SaveToStream (Stream: TacObjStream); read GetCount;
override; property OwnObjects: Boolean
{ Protected properties } read FOwnObjects
property OnObjDelete: TacObjListNotifyEvent write FOwnObjects;
read FOnDelete property OwnList: Boolean
write FOnDelete; read FOwnList
property OnObjInsert: TacObjListNotifyEvent write FOwnList;
read FOnInsert end;
write FOnInsert;

Figure 3: The source listing for the TacObjStringList interface.

and object ownership settings. Protected properties allow The Destroy destructor override frees owned objects and lists
derived classes to install handlers for the insertion and dele- before calling the inherited destructor.
tion notifications.
The public methods, AtIndex and AtName, provide access to
The private methods ResetList, CloneContents, FreeObjects, the objects in the list. AtIndex returns the object at a specific
and FreeList manage the list reference. ResetList updates the index, and AtName returns the object with a specific string
list reference and the list and object ownership flags. representation.
CloneContents rebuilds the list to duplicate the contents of
another resulting in a list containing copies of the objects The UpdateObjectName method updates an object’s string
in the source list, not references to the same objects. representation from its AsString property. This method
FreeObjects and FreeList are helper methods for freeing all should be called for any object in the list that is modified in
the objects in the list and the list itself. a way that could potentially affect its string representation.

The constructor, Create, takes two parameters — a Most of the remaining public methods are concerned with
TStrings object reference and an object ownership flag. The list manipulation. Most of these map directly or indirectly
TStrings reference identifies an existing list object to use as to methods of the TStrings class. In some cases, the
the list delegate. If the reference is nil, a new TStringList TacObjStringList methods augment the TStrings behavior
object is created to serve as the list delegate. The object with additional error checking and support for notifications
ownership flag determines whether or not the list owns the and object ownership. For details, refer to the complete
contained objects. source code listing.

DECEMBER 1995 Delphi INFORMANT ▲ 10


On the Cover

Implementing TacObjStringList
The implementation of TacObjStringList is also a good exam-
ple of building a persistent class. There are five requirements
for a fully functional persistent class:
1) It must be a descendant of TacStreamable.
2) It must override the AssignTo method to support assign-
ment and copy construction.
3) It must override the InitFields method to perform class
specific initialization.
4) It must override the SaveToStream and ReadFromStream
methods.
5) It must be registered if objects will be created directly from it.
Figure 5: TacObjStringList assignments.
In addition, the class should override the GetAsString method
to support string-based list access or display in list controls. course, subclasses can easily override this behavior as needed.
For valid assignments, the destination object is cast to a
The following discussion approaches these requirements in TacObjStringList and the CloneContents method is called to
the context of the TacObjStringList class. As previously stated, duplicate the source list.
TacObjStringList is a subclass of TacStreamable.
The InitFields implementation calls the inherited version
Figure 4 shows the source listing of the AssignTo method. and then initializes the private list reference to nil and the
First, you must verify that the instance is not being assigned ownership flags to False.
to itself. This is more than an efficiency issue. Allowing
AssignTo to proceed with itself as the destination object would The SaveToStream method saves the count of the objects in
result in memory leaks and nasty general protection faults. the list to the stream and then iterates through the list, saving
The next concern is with the class of the destination object. each object with a call to the stream’s SaveObject method. The
At first glance, it might appear that the type checking per- ReadFromStream method reverses this process. It reads the
formed by the as operator when casting the destination from count of the objects in the list from the stream and then cre-
TPersistent to TacObjStringList would be sufficient to guaran- ates each object with a call to the stream’s ReadObject method.
tee assignment compatibility. However, the as operator only
requires that the destination object’s class be TacObjStringList TacObjStringList does not override the GetAsString method.
or one of its subclasses. Considering the source object’s class It’s unlikely that string representations of TacObjStringList
may also be TacObjStringList or one of its subclasses, this objects will be required.
would allow all the assignments (see Figure 5).
That’s it. TacObjStringList is now a fully functional persistent
Unfortunately, in cases (A) and (B), the assignment may not be class. I have intentionally not yet registered the class. Registration
proper. In both cases, the destination class may include fields is required only for classes used to create actual objects. Because
and behaviors unknown to the source class. Therefore, the any given application may or may not create direct instances of
implementation of TacObjStringList performs an explicit check TacObjStringList, the application is responsible for registration.
to restrict assignments to the type indicated by case (C). Of
TacObjStream: Building a Home for Persistent Objects
procedure TacObjStringList.AssignTo(Dest : TPersistent); TacObjStream is the base class for persistent object streams.
var Before examining its particulars, it’s important to understand
DestStringList : TacObjStringList;
begin
how TacObjStream organizes data in the stream. Conceptually,
if ( Dest = self ) then Exit; the stream consists of four areas:
1) the Stream Header
if ( (Dest is TacObjStringList) and
(Self is Dest.ClassType) ) then
2) the User Header
begin { Assigning to same or superclass } 3) the Object Data
DestStringList := ( Dest as TacObjStringList ); 4) the Class Table
DestStringList.ResetList(DestStringList.FList,True);
DestStringList.CloneContents(self);
end Figure 6 illustrates this format. The stream and user head-
else ers are fixed length areas. More specifically, the size of the
begin { TPersistent will process error }
inherited AssignTo(Dest);
header for any particular stream must remain constant for
end; the life of the stream, although it may vary from stream to
end; stream. The stream header begins every stream created
from the TacObjStream class. It includes a signature and
Figure 4: The TacObjStringList.AssignTo procedure. version number for identifying the stream, and an offset to

DECEMBER 1995 Delphi INFORMANT ▲ 11


On the Cover

the class table. TacObjStream The private methods, SaveStreamHeader and ReadStreamHeader,
subclasses can define and man- manage the stream header. After processing the standard stream
age the optional user header header, these methods call the SaveHeader and ReadHeader
area as needed. methods, respectively. Subclasses override these protected virtual
methods to define and manage a user header. By default, their
The object data area contains implementation is empty.
the persistent object data. A 2-
byte class ID, written by The class table is managed by the following private meth-
TacObjStream, prefixes each ods: PrepareClassTable, SaveClassTable, ReadClassTable, and
object’s data. The class ID iden- AddClassRef. The organization of the class table is depen-
tifies the object’s class type indi- dent on the stream’s access mode. The PrepareClassTable
rectly through the class table. method is responsible for properly configuring the class
The class table stores the class table prior to its use.
type information for each class
represented in the stream. The For input mode streams, the class table is organized as an
class table consists of a counter, unsorted string list of class names ordered by class ID. The pri-
specifying the number of class vate ReadClassTable method reads each entry from the stream
entries, followed by the class and inserts the class name into the class table at a position
Figure 6: The TacObjStream
entries in the form of <class determined by the class ID. It also calls Delphi’s FindClass pro-
file format. name, class id> pairs. cedure to determine the class type reference for the class name
and stores that in the entry’s corresponding Objects property.
The initial implementation of TacObjStream did not include a
class table. Instead of writing a class ID before each object, it For output mode streams, the class table is organized as a
wrote the class name. Delphi uses a similar procedure when sav- sorted string list of class names, each associated with an inte-
ing components. However, since each class name can occupy up ger class ID. The table builds incrementally as objects are
to 64 bytes (they are 63 character strings), this scheme can be saved to the stream. Each saved object results in a call to the
space inefficient, especially for streams containing large numbers stream’s AddClassRef method. A call to the object’s ClassName
of small objects. Performance may also suffer, as it requires a method returns the class name. If the class name is already in
class type lookup for each object. the table, AddClassRef returns the previously assigned class
ID. If the class name is not in the table, AddClassRef creates a
The class table implementation avoids these problems and new class table entry, assigns it the next class ID, and returns
is more efficient in almost all cases. The class table stores the class ID. The private method, SaveClassTable, appends the
each referenced class name (just once) along with its asso- class table to the stream before the stream is closed.
ciated class ID. As a result, the per-object overhead
decreases from a possible 64 bytes to a constant 2 bytes. It’s important to realize that ClassName always returns the
Also, reading from the stream requires a class type lookup class name of the object’s actual class, not of the reference
only once per class instead of once per object. Although variable’s class. Specifically, even though AddClassRef sees all
the class table does add complexity, I think the benefits objects passed to it as TacStreamable references, if an object of
more than justify its use. class TMyStreamableObject (a subclass of TacStreamable) is
passed, the call to ClassName returns “TMyStreamableObject,”
Figure 7 shows the source listing for the TacObjStream class not “TacStreamable.”
interface. TacObjStream includes three private fields. The
FMode field stores the stream’s current access mode. It’s of For append mode streams, the class table is initialized by
type TacObjStreamMode. TacObjStream provides three access ReadClassTable, but must be organized as if in output mode.
modes: input, output, and append. Input mode is for reading This ensures the class table is properly updated if new classes
some or all the objects in a stream, output mode is for saving of objects are appended to the stream.
an entire stream, and append mode is for adding objects to
the end of an existing stream. As previously stated, TacObjStream does not implement the
physical stream management. This is left to its subclasses.
The remaining two fields relate to the stream’s on disk for- Instead, it performs all stream operations through a reference
mat. The FHeader field contains the stream header. The to Delphi’s abstract stream class TStream. This is similar to
FClassTable field is a reference to the TStringList object that using a TStrings reference in TacObjStringList; it makes it pos-
maintains the in-memory class table. sible to transparently support multiple types of streams.

The Create constructor merely initializes the private fields. TacObjStream declares three abstract protected methods that
The Destroy destructor override ensures the stream is closed define its required internal stream interface. Subclasses must
and the class table string list is freed. override these methods to produce a fully working stream

DECEMBER 1995 Delphi INFORMANT ▲ 12


On the Cover

type
TacObjStream = class(TObject)
{ Only 63 chars of identifiers are significant }
private
TacStreamableClassName = string[63];
FMode : TacObjStreamMode; { Access mode }
{ Identifies class of streamed objects }
FHeader : TacObjStreamHeader;{ Stream header }
TacStreamableClassId = Integer;
FClassTable : TStringList; { In-memory class
{ Index into class list }
lookup table }
TacStreamableClassIdx = Integer;
{ Stream header management }
procedure SaveStreamHeader;
type
procedure ReadStreamHeader;
TacObjStreamMode =
{ Class table management }
(
procedure PrepareClassTable(const Mode:
osmClosed, { Stream not open }
TacObjStreamMode);
osmInput, { For reading only }
procedure SaveClassTable;
osmOutput, { For writing only, starts with
procedure ReadClassTable;
empty stream }
function AddClassRef(const Obj: TacStreamable):
osmAppend { For writing only, starts with
TacStreamableClassId;
current contents }
protected
);
{ Abstract internal stream interface }
TacObjStreamModes = set of TacObjStreamMode;
function GetStream: TStream; virtual; abstract;
procedure OpenStream(const Mode: TacObjStreamMode);
{ Standard stream header. Starts every TacObjStream. }
virtual; abstract;
type
procedure CloseStream; virtual; abstract;
TacObjStreamHeader = record
{ Error handling }
Signature : array[0..7] of Char;
procedure ValidateStreamMode(const Modes:
Version : LongInt;
TacObjStreamModes);
ClassTableOffset : LongInt;
procedure ObjStreamError(Exc: Exception); virtual;
end;
{ Placeholders for user added headers }
procedure SaveHeader; virtual;
const
procedure ReadHeader; virtual;
DefaultObjStreamHeader : TacObjStreamHeader =
public
(
{ Construction/Destruction }
Signature : 'ACSTREAM';
constructor Create;
Version : $00000000;
destructor Destroy; override;
ClassTableOffset : $00000000
{ Opening and closing stream }
);
procedure OpenForInput;
procedure OpenForOutput;
type { TacObjStream exception classes }
procedure OpenForAppend;
EacObjStream = class(Exception)
procedure Close;
{ Base class for TacObjStream Exceptions }
{ Save and Read methods for streaming objects }
end;
procedure SaveObject(const Obj: TacStreamable);
function ReadObject(const Obj: TacStreamable):
EacObjStreamInvalid = class(EacObjStream)
TacStreamable;
{ Unexpected stream format, header unrecognized }
{ Methods used by objects to read/write their data }
end;
procedure SaveBuffer(const Buffer; Count: Longint);
procedure ReadBuffer(var Buffer; Count: Longint);
EacObjStreamWrongMode = class(EacObjStream)
procedure SaveCStr(const CStr: PChar);
{ Stream is in the wrong mode for requested operation }
function ReadCStr: PChar;
end;
end;
type

Figure 7: The TacObjStream interface source listing.

class for TacStreamable objects. The GetStream method only ReadFromStream methods of TacStreamable subclasses.
needs to return a reference to the actual stream object. This Their implementation is trivial. The calls are simply for-
reference provides access to the actual stream for data inser- warded to the TStream object returned by GetStream.
tion and extraction. The other two required methods, SaveCStr and ReadCStr are helper functions that simplify
OpenStream and CloseStream, impose a standard protocol for the handling of C-style, null-terminated strings.
opening and closing the stream that is not necessarily linked
to the stream’s creation and destruction. This minimizes the The most interesting methods are SaveObject and ReadObject.
limitations resulting from TacObjStream’s inability to sup- SaveObject writes a persistent object to the stream, and
port simultaneous input and output on an open stream. ReadObject recreates a persistent object from the stream. Because
of the previously discussed infrastructure, the implementation of
The public interface of TacObjStream includes methods for SaveObject and ReadObject is relatively simple (see Figure 8).
opening and closing the stream. The OpenForInput,
OpenForOutput, and OpenForAppend open the stream and SaveObject saves the class ID, returned by a call to AddClassRef,
prepare it for reading, saving, and appending, respectively. and then saves the object with a call to its SaveToStream method.
The Close method closes any open stream.
ReadObject reads the class ID and determines the associated
The SaveBuffer and ReadBuffer methods are provided class type by using the class ID as an index into the class table.
mainly for implementing the SaveToStream and Since the class type reference is stored as a TObject reference, it

DECEMBER 1995 Delphi INFORMANT ▲ 13


On the Cover

to perform the actual initialization, the call occurs in the context


procedure TacObjStream.SaveObject
(const Obj : TacStreamable);
of the desired class.
var
ClassId : TacStreamableClassId; At this point, ReadObject could simply return a reference to the
begin
ValidateStreamMode([osmOutput, osmAppend]);
newly created object. However, the current implementation
includes an added degree of flexibility. The ReadObject method
if ( Assigned(Obj) ) then takes a single parameter of type TacStreamable. If nil is passed,
begin
{ Get the class ID }
ReadObject returns the created object, as expected. If a valid
ClassId := AddClassRef(Obj); object reference is passed, ReadObject calls the object’s Assign
{ Save the class ID } method to update its state from the created object, frees the
GetStream.WriteBuffer(ClassId, Sizeof(ClassId));
{ Save the object }
created object, and returns the passed object reference. This
Obj.SaveToStream(self); added capability circumvents a weakness of ReadObject.
end; ReadObject always uses the CreateFromStream constructor,
end;
which in turn, relies on the default constructor, Create. Even if
function TacObjStream.ReadObject the constructor was virtual, all TacStreamable based classes
( const Obj : TacStreamable ): TacStreamable; would still be limited to a single constructor with a predefined
var
ClassId : TacStreamableClassId;
parameter list. As implemented, if the class of the next object
ObjType : TacStreamableClass; in the stream is known, the object can be initialized by any
NewObj : TacStreamable; declared constructor before calling ReadObject.
begin
ValidateStreamMode([osmInput]);
TacFileObjStream: Saving Objects to Disk
Result := nil; In most applications involving persistent objects, the objects are
{ Read class ID and get the corresponding class type
stored in disk files. TacFileObjStream is a concrete subclass of
reference } TacObjStream for disk file-based streams. Since most of the
GetStream.ReadBuffer(ClassId, sizeof(ClassId)); functionality is provided by TacObjStream, its implementation
ObjType :=
TacStreamableClass(FClassTable.Objects[ClassId]);
is rather short. Figure 9 shows the complete listing for the class.

{ Create a new object of the proper class from the The class declares two private fields, FFilename and
stream data }
NewObj := ObjType.CreateFromStream(self);
FFileStream. FFilename stores the name of the disk file so the
file can be opened and closed as needed. FFileStream is a ref-
if ( Assigned(Obj) ) then erence to the actual TFileStream object created to access the
begin { Assign created object to passed object and
return object }
file associated with the filename.
try
obj.Assign(NewObj); As required, TacFileObjStream overrides the protected meth-
Result := Obj;
finally
ods GetStream, OpenStream, and CloseStream. GetStream
NewObj.Free; returns the FFileStream stream reference. OpenStream creates a
end; TFileStream object and saves the reference in the FFileStream
end
else
field. The desired TacObjStream access mode, passed in as a
begin { Just return created object } parameter, determines the file access mode of the created
Result := NewObj; stream. CloseStream frees the TFileStream object referenced by
end;
end;
FFileStream and resets the field to nil. The corresponding disk
file is automatically closed by the TFileStream instance before
Figure 8: The TacObjStream SaveObject and ReadObject functions. it’s destroyed.

must be cast back to a TacStreamableClass reference before it’s This class has a single constructor, Create, that takes a filename
assigned to the local class reference variable, ObjType. as a parameter. It calls the inherited constructor and then saves
TacStreamableClass is a class reference type that can reference the filename in the FFilename field. The inherited destructor,
any class descended from TacStreamable. This cast should Destroy, is overridden. It calls the inherited destructor and then
always be valid since all objects saved to the stream must be frees the TFileStream instance, if it still exists.
instances of TacStreamable subclasses.
Streams and Persistent Objects in Action
A call to the CreateFromStream constructor with ObjType as the The remainder of this article presents a complete, albeit
class reference creates a new object from the stream. This call somewhat trivial, application that exercises most of the
causes Delphi to create an instance of the actual TacStreamable library. The name of the application is Resource Logger. It
subclass referenced by the ObjType variable. Thus, even though exists solely for periodically logging the system’s resource
the CreateFromStream constructor is a static method of status. It optionally tracks free Windows resources, avail-
TacStreamable, when it calls the virtual ReadFromStream method able memory, and available disk space.

DECEMBER 1995 Delphi INFORMANT ▲ 14


On the Cover

type
TacFileObjStream = class(TacObjStream)
private
FFilename : TFilename;
FFileStream : TFileStream;
protected
{ Required internal stream interface }
function GetStream: TStream; override;
procedure OpenStream(const Mode: TacObjStreamMode);
override;
procedure CloseStream; override;
public
{ Construction/Destruction }
constructor Create(const Filename: TFilename);
destructor Destroy; override;
{ Properties }
property Filename: TFilename
read FFilename;
end;

implementation

{ ************** TacFileObjStream ******************* }

constructor TacFileObjStream.Create
( const Filename : TFilename );
begin
inherited Create;
FFilename := Filename; Figure 10 (Top): The Resource Logger form at run-time. Figure 11
end; (Bottom): Resource Logger class hierarchy.

destructor TacFileObjStream.Destroy;
begin
Figure 11 shows the relevant class hierarchy for the applica-
inherited Destroy; tion. The TLogItem subclass of TacStreamable is the base class
{ Postponed stream Free so TacObjStream can close for all types of log entries. A distinct subclass of TLogItem is
it up, if needed }
FFileStream.Free;
declared for each of the three types of resources. TOptions is
end; another subclass of TacStreamable used for saving the logging
options. The TLog class is a simple subclass of TacObjStringList
function TacFileObjStream.GetStream: TStream;
begin
that adds a default array property for accessing the log items
Result := FFileStream; with array-like syntax. While active, the application manages
end; the log with an instance of TLog, referencing the list associated
procedure TacFileObjStream.OpenStream
with the form’s list box. The TLogStream subclass of
( const Mode : TacObjStreamMode ); TacFileObjStream manages the application’s data file.
var
StreamFileMode : Word;
begin
All the application-specific persistent classes are declared and
case Mode of implemented in the EXAMPLE unit. The implementations
osmInput : StreamFileMode := are all similar. Figure 12 shows the TMemoryLogItem class
fmOpenRead or fmShareDenyWrite;
osmOutput : StreamFileMode := fmCreate;
source listing as an example.
osmAppend : StreamFileMode :=
fmOpenReadWrite or fmShareDenyWrite; The TLogStream class is an example of subclassing
end;
FFileStream := TFileStream.Create(Filename,
TacFileObjStream to add a user-defined stream header (see
StreamFileMode); Figure 13). The SaveHeader method override saves the appli-
end; cation specific signature, and the ReadHeader method over-
procedure TacFileObjStream.CloseStream;
ride reads the signature and verifies it. ReadHeader raises an
begin exception if the signature is invalid.
FFileStream.Free;
FFileStream := nil;
end;
The EXAMPLE unit registers the TResourceLogItem,
TMemoryLogItem, TDiskLogItem, TOptions, and TLog classes
Figure 9: The TacFileObjStream class. in its initialization section. Only objects of these five classes
are instantiated and streamed.
The application consists of a single form (see Figure 10). A
list box displays the logged entries and a group of check boxes The main form unit contains all the application’s major func-
provides control over logging for each type of resource. The tionality. Up to this point, the discussion has focused on
three command buttons start and stop logging, clear the log, implementing the persistent and stream classes. The remainder
and summarize the logged entries. examines the form unit as an illustration of using the classes.

DECEMBER 1995 Delphi INFORMANT ▲ 15


On the Cover

end;
type
end;
{ TMemoryLogItem handles memory usage log entries. }
TMemoryLogItem = class(TLogItem)
procedure TMemoryLogItem.InitFields;
private
begin
FFreeSpace : LongInt; { Amount of free memory (KB) }
{ Allow TLogItem to initialize }
FLargestBlock : LongInt; { Size of largest available
inherited InitFields;
block (KB) }
protected
FFreeSpace := GetFreeSpace(0) div 1024;
{ TPersistent overrides }
FLargestBlock := GlobalCompact(0) div 1024;
procedure AssignTo(Dest: TPersistent); override;
end;
{ TacStreamable overrides }
procedure InitFields; override;
procedure TMemoryLogItem.SaveToStream(Stream:
function GetAsString: string; override;
TacObjStream);
procedure SaveToStream(Stream: TacObjStream);
begin
override;
{ Allow TLogItem chance to save }
procedure ReadFromStream(Stream: TacObjStream);
inherited SaveToStream(Stream);
override;
public
Stream.SaveBuffer(FFreeSpace,SizeOf(FFreeSpace));
{ Properties }
Stream.SaveBuffer(FLargestBlock,SizeOf(FLargestBlock));
property FreeSpace: LongInt
end;
read FFreeSpace;
property LargestBlock: LongInt
procedure TMemoryLogItem.ReadFromStream(Stream:
read FLargestBlock;
TacObjStream);
end;
begin
{ Allow TLogItem chance to read }
implementation
inherited ReadFromStream(Stream);
procedure TMemoryLogItem.AssignTo(Dest: TPersistent);
Stream.ReadBuffer(FFreeSpace,SizeOf(FFreeSpace));
begin
Stream.ReadBuffer(FLargestBlock,SizeOf(FLargestBlock));
if ( (Dest is TMemoryLogItem) and
end;
(Self is Dest.ClassType) ) then
begin
function TMemoryLogItem.GetAsString: string;
inherited AssignTo(TLogItem(Dest));
begin
with Dest as TMemoryLogItem do
{ Get Timestamp string from TLogItem }
begin
Result := inherited GetAsString;
FFreeSpace := self.FFreeSpace;
FLargestBlock := self.FLargestBlock;
{ Append memory descriptions }
end;
AppendStr(Result,' Mem ');
end
AppendStr(Result,Format(' Free[%dKB]',[FreeSpace]));
else
AppendStr (Result,Format (' Largest[%dKB]',[LargestBlock]));
begin
end;
inherited AssignTo(Dest);

Figure 12: The TMemoryLogItem class.

The form class declares three private fields. The FOptions The application saves its option settings and the active log
field is a reference to the TOptions object used to save the entries into a single stream. The SaveLog and ReadLog meth-
state of the logging options. The FLog field is a reference ods are responsible for the stream management. Figure 14
to the TLog object used to contain and manage the active shows their source listing.
log entries. The FLogStream field is a reference to the
TLogStream object used to manage the application data ReadLog opens FLogStream for input, calls the ReadObject
file. All three objects are created in the FormCreate method method twice (once for the options object and once for the
and freed in the FormDestroy method. list object), and then closes FLogStream. Since both objects
were previously created, their references are passed to
The Start Logging button toggles logging on and off. ReadObject. As discussed earlier, this alternative use updates
When logging is enabled, a Timer component triggers a the existing objects instead of creating new ones. For the
periodic event, and log entries for the selected resources options object, this is mainly for convenience, as it avoids
are created and added to the log with a call to FLog.Add. having to free the current options object. Regarding the log,
however, it’s critical. FLog uses the form’s list box as its stor-
The Clear Log button clears all entries from the log with a age list. Freeing the FLog object and letting ReadObject create
call to FLog.DeleteAll. Since the list owns the log entry a new one would break the association.
objects, it automatically frees them.
SaveLog is nearly identical to ReadLog. The only differences
The Summarize button reports the maximum usage for each are that it opens FLogStream for output and calls SaveObject
resource. This button’s handler, BtnSummarizeClick, demon- in place of ReadObject. In both cases, the list object referenced
strates iteration of a TacObjStringList instance and assignment of by FLog handles reading and saving the individual log entries,
TacStreamable objects. regardless of their specific class.

DECEMBER 1995 Delphi INFORMANT ▲ 16


On the Cover

type
Conclusion
{ TLogStream represents the log data file } The TacStreamable, TacObjStringList, TacObjStream, and
TSignature = string[6]; TacFileObjStream classes provide the minimum functionality for a
TLogStream = class(TacFileObjStream) useful persistent object library. For many applications, this func-
private
FSignature : TSignature; tionality may be sufficient. For others, it should provide a reason-
protected able starting point for more sophisticated implementations.
procedure ReadHeader; override;
procedure SaveHeader; override;
end; There are many enhancements to consider. TacObjStream
could be extended to support simultaneous reading and writ-
implementation ing, or random object access. The accompanying source code
const includes a TacObjStream subclass for memory-based streams.
LOG_SIGNATURE : TSignature = 'LOGDAT'; Support for other types of streams might also be useful.
Iterators for TacObjStringList will be my next enhancement. I
procedure TLogStream.ReadHeader;
begin would also like to have other types of object containers, such
ReadBuffer(FSignature, SizeOf(FSignature)); as stacks and queues.
{ Throw exception if invalid signature }
if ( FSignature <> LOG_SIGNATURE ) then
begin Whether you use the library as-is or as a starting point for
raise Exception.Create('Unrecognized data file'); your own, there are no more excuses for avoiding persistent
end; objects and streams in your Delphi applications. ∆
end;

procedure TLogStream.SaveHeader; References


begin • Booch, Grady, Object-Oriented Analysis and Design, The
FSignature := LOG_SIGNATURE;
SaveBuffer(FSignature, SizeOf(FSignature)); Benjamin/Cummings Publishing Company, Inc., 1994.
end; • Coplien, James O., Advanced C++, Addison-Wesley
Publishing Company, 1992.

{ ReadLog reads the options and log from the data file } The demonstration application referenced in this article is avail-
procedure TMainForm.ReadLog;
begin able on the 1995 Delphi Informant Works CD located in
if ( FileExists(FLogStream.Filename) ) then INFORM\95\DEC\DI9512AC.
begin
with FLogStream do
begin
OpenForInput;
ReadObject(FOptions);
Alan Ciemian is a contract programmer and software developer specializing in Windows
ReadObject(FLog);
Close;
development using C++, Delphi, and Paradox for Windows. He can be reached on
end; CompuServe at 70134,611 or on the Internet at 70134.611@compuserve.com.
end;
end;

{ SaveLog saves the options and the log to the data file }
procedure TMainForm.SaveLog;
begin
with FLogStream do
begin
OpenForOutput;
SaveObject(FOptions);
SaveObject(FLog);
Close;
end;
end;

Figure 13 (Top): The TLogStream class. Figure 14 (Bottom):


TMainForm’s SaveLog and ReadLog procedures.

DECEMBER 1995 Delphi INFORMANT ▲ 17


Informant Spotlight
Delphi / Object Pascal

By Douglas Horn

Delphi MRU
Adding a Shortcut to Your Most Recently Used Files

t’s a familiar feature in Windows programming — a section of the File menu

I that lists the last files accessed by an application. Clicking on any of these file-
names opens the corresponding file immediately, allowing users to quickly pick
up where they left off. These MRU (or most recently used) lists are popular time
savers, and are available in most commercial Windows applications (see Figure 1).
Unfortunately, MRU lists are not standard in Delphi — you must create them.

In a previous issue we covered integrating .INI files into Delphi appli-


cations. [For more information regarding .INI files, see Douglas Horn’s
article “Initialization Rites” in the August 1995 Delphi Informant.]
MRU lists are a natural extension of this topic because they use .INI
files to store the list of most recently used files between application ses-
sions. This article illustrates how to include MRU lists in new or exist-
ing applications with minimal programming.

Starting Off
MRU lists can be added just as easily to new or existing applica-
tions. To illustrate this, we’ll add an MRU list to one of the sample
applications that ships with Delphi.

The text editor sample application, described in Chapter 10 of the


Delphi User’s Guide, is a good introduction to Multiple Document
Interface (MDI) applications. It’s not necessary to perform the tutorial
to understand this article, but any reader with questions about program-
ming MDI applications should use it. The completed text editor appli-
cation, TEXTEDIT, is in the \DELPHI\DEMOS\DOC\TEXTEDIT
directory (assuming the default directories were used at Delphi installa- Figure 1: The Microsoft Excel
tion time). File menu showing typical
placement and appearance of
There are five basic steps required to add an MRU list to an MRU list items.
application:
1) Add the MRU list objects.
2) Manage the .INI file.
3) Update the MRU list.
4) Display the MRU list.
5) Open files from the MRU list.

DECEMBER 1995 Delphi INFORMANT ▲ 18


Informant Spotlight

Adding MRU List Objects The MRU list is a TStringList component that maintains a list of
To create an MRU list, you must add two types of objects to an MRU files while the application is running. To add this list, first
application: the menu items to access the list, and the list itself. declare the MRUList variable under FrameForm’s public part of
The menu items are any number of blank items positioned prop- the interface section.
erly on the main menu. This sample application uses four,
public
because it’s the standard number and is easy to work with. MRUList: TStringList;
However, a list can hold up to nine items. In some Windows
applications, such as Microsoft Word 6.0 for Windows, the user This allows access by the application’s other form, EditForm.
can configure the number of MRU items. Using more than nine
is possible, but not recommended, because it makes the menus Now that MRUList is declared, it must be put into action. Since it
long and the program runs out of single-digit numbers to use as will be active for the life of the application session, it should be
menu shortcut keys. created in the OnCreate event when the application starts.
MRUList is a global variable. It will be accessible throughout the
The proper position of the MRU list on a menu is under the application and automatically freed when the application is closed:
File menu group, above Exit, in its own subsection. Mnemonic
shortcut keys for MRU list items are always listed in numerical procedure TFrameForm.FormCreate(Sender: TObject);
order (beginning with 1), corresponding to the number of MRU begin
MRUList := TStringList.Create;
items (again, see Figure 1). end;

To add the MRU menu items to TEXTEDIT, first open the menu Managing the .INI File
editor for the application’s main (MDIParent) form, FrameForm. The container for the MRU list (the StringList object, MRUList)
Starting just below the lowest separator (at Exit), insert five new only contains its contents while the application is running.
menu items. From top to bottom, name these items MRU1, Between program sessions, it stores the list to an .INI file.
MRU2, MRU3, MRU4, and MRUSeparator. Leave the captions
for items MRU 1 through 4 blank. These will be filled by code at The best time to load values into MRUList is when the list is creat-
run-time. The caption for MRUSeparator should be set to a ed. Since this application uses four items, a loop should be added to
hyphen ( - ) to create a menu separator line. Set the Tag property the FormCreate procedure to read four values from the MRU section
of the items as shown in Figure 2. of the .INI file, TEXTEDIT.INI (in the C:\WINDOWS directory).
They should then be added to the string list. If the .INI file does
When the menu items on the MDI parent form (FrameForm) are not exist, it’s created automatically, and blank values are added to
complete (see Figure 3), repeat the process on the application’s the list. Figure 4 shows the updated FormCreate procedure.
MDI child form (EditForm) using the same names and proper-
ties. This does not create a conflict, because although the items The FormCreate procedure loads the contents of the .INI file into
are merged at run-time, they exist on different menus. the MRUList string list at startup. To have anything to load, howev-
er, the application must save the contents of the list upon closing.
Name Caption Tag Figure 2 (Left): Setting the Tag To accomplish this, the FormDestroy procedure is made similar to
MRU1 blank 0 properties for individual lines in FormCreate, except FormDestroy (see Figure 5) copies the contents
our example. of the string list back to the .INI file for storage (see Figure 6) until
MRU2 blank 1
Figure 3 (Bottom): New MRU
MRU3 blank 2 the program is run again.
list menu items added to the
MRU4 blank 3 FrameForm main menu via the
Menu Editor. Finally, for Delphi to access the .INI file (and for the application
MRUSeparator - 0
to run), IniFiles must be added to the uses statement in the
interface section of the FrameForm code:

uses WinTypes, WinProcs, Classes, Graphics, Controls,


Printers, Menus, MDIEdit, Dialogs, Forms, IniFiles;

Updating the List


The MRU list now exists and can be opened and saved with the
application. It must also be updated periodically as new files are
used. Traditionally, MRU lists contain the names of all files
opened or saved, with the most recently used file at the top. To
keep the list current, a procedure must be created to update it,
and then referenced every time a file is opened or saved.

The procedure, MRUUpdate (see Figure 7), limits the list to


four items and prevents duplicate entries. First, it loops through

DECEMBER 1995 Delphi INFORMANT ▲ 19


Informant Spotlight

procedure TFrameForm.MRUUpdate(Sender: TObject;


procedure TFrameForm.FormCreate(Sender: Tobject);
const AddFileName: String);
var
var
AppIni: TIniFile; { Declare IniFile variable }
Index: Integer; { Declare index variable }
Index: Integer; { Declare loop Index variable }
begin
begin
Index := 0;
MRUList := TStringList.Create; { Create link to IniFile }
{ Compare AddFileName to MRUList items }
AppIni := TIniFile.Create('TEXTEDIT.INI');
while Index < (MRUList.count - 1) do
for Index := 0 to 3 do;
if AddFileName = MRUList[Index] then
{ Add items from INI File to MRU list }
{ If already there, delete each occurrence }
MRUList.Add(AppIni.ReadString('MRU',IntToStr(Index),'');
MRUList.delete(Index)
AppIni.Free; { Free IniFile link from memory }
else
end;
Index := Index + 1;
while MRUList.count > 3 do
MRUList.delete(MRUList.Count - 1);
procedure TFrameForm.FormDestroy(Sender: Tobject);
while MRUList.count < 3 do
var
MRUList.add('');
AppIni: TIniFile;
{ Add fourth item to the top }
Index: Integer;
MRUList.Insert(0,AddFileName);
begin
end;
AppIni := TIniFile.Create('TEXTEDIT.INI');
for Index := 0 to 3 do;
{ Write contents of MRU list to .INI file } Figure 7: The MRUUpdate procedure.
AppIni.WriteString('MRU',IntToStr(Index),MRUList[Index]);
AppIni.Free; Adding the same statement at the same place in the
end;
TEditForm.Save1Click procedure ensures the MRU list is also
Figure 4 (Top): The FormCreate procedure reads MRU information
updated when a file is saved. In TEXTEDIT, the SaveAs proce-
from the .INI file. Figure 5 (Bottom): The FormDestroy procedure. dure calls the Save procedure. In applications structured differ-
ently, the SaveAs event handler must also reference the
the items in the MRUUpdate procedure.
TStringList
object, MRUList, Displaying the List
and compares The program now saves, loads, and updates the values in the
each to the name MRU list, but it still does not display them. Displaying the
of the file being MRU list menu items in an MDI application requires two iden-
opened or saved, tical procedures. In a Single Document Interface (SDI) applica-
AddFileName. If tion, only a single event handler is required. With multiple doc-
an entry matches, Figure 6: TEXTEDIT.INI showing the newly cre- uments, however, both the parent and child forms have their
it is deleted from ated .INI file and its saved contents. own menus. When these menus are merged, the child form is
the MRU list opened and the parent menu becomes invisible. But when a
array. When the procedure has looped through all the items in child form is not open, accessing the form’s menu produces an
the TStringList object, it does some housekeeping to ensure the error event. Therefore, the procedure for displaying the MRU
MRU list contains exactly three objects, then adds the new, menu items must be precise.
fourth object (AddFileName) to the top of the list.
The MRUDisplay procedure is quite simple. The caption of each
MRUUpdate is called when a file is opened or saved. menu item is set to the filename of the corresponding MRU list
According to de facto standards for MRU lists, new files are item. An ampersand, a number, and a space (e.g. &1 ) precede the
not added to the list until they are assigned a filename and filename, telling Delphi to underline the number so it can call
saved. Likewise, closed files are not added to the list unless the file. If a particular MRU menu item is blank, it will not dis-
they are saved. play. If MRU1 contains a filename, the separator line is visible.

To perform this, the MRUUpdate must be called from the applica- The most efficient way to set the Visible property to True if the
tion’s Open and Save procedures. In the sample TEXTEDIT appli- menu item is not blank, is to use the compound statement:
cation, these procedures are TEditForm.Open and
MRU1.Visible := (MRUList[0] <> '');
TEditForm.Save1Click. The Open procedure receives a filename
from the OpenDialog procedure called in TFrameForm.OpenChild.
By adding the line: This sets the Visible property to True if the List item contains a
filename (i.e. if MRUList[x] is not blank). Otherwise it sets the
FrameForm.MRUUpdate(Self,Filename); property to False, hiding the item.

before the procedure’s end keyword, we can tell the procedure to The code for TFrameForm.MRUDisplay is shown in Figure 8.
update the MRU list by using the open file’s name as the new The TEditForm.MRUDisplay procedure is identical, except it’s in
addition to the list. the EditForm code, and references the MDI child menu items.

DECEMBER 1995 Delphi INFORMANT ▲ 20


Informant Spotlight

procedure TFrameForm.MRUDisplay(Sender: TObject); procedure TFrameForm.OpenChild(Sender: TObject);


begin var
MRU1.Caption := '&1 ' + MRUList[0]; EditForm: TEditForm;
MRU1.Visible := (MRUList[0] <> ''); begin
MRUSeparator.Visible := MRU1.Visible; if OpenFileDialog.Execute then
MRU2.Caption := '&2 ' + MRUList[1]; begin
MRU2.Visible := (MRUList[1] <> ''); EditForm := TEditForm.Create(Self);
MRU3.Caption := '&3 ' + MRUList[2]; EditForm.Open(OpenFileDialog.Filename);
MRU3.Visible := (MRUList[2] <> ''); EditForm.Show;
MRU4.Caption := '&4 ' + MRUList[3]; EditForm.MRUDisplay(Sender); { Update MRU menu items }
MRU4.Visible := (MRUList[3] <> ''); end;
end; end;

Figure 8: The MRUDisplay procedure.


procedure TEditForm.Save1Click(Sender: TObject);
{ ... listing partially omitted }
The procedure would be called: begin
if (Filename = '') or IsReadOnly(Filename) then
procedure TEditForm.MRUDisplay(Sender: TObject) SaveAs1Click(Sender)
else
begin
Normally, two identical procedures would not be necessary; one CreateBackup(Filename);
could call the other to reduce code size. However, this case is an Memo1.Lines.SaveToFile(Filename);
Memo1.Modified := False;
exception. Because the MDI parent form cannot “see” the MDI FrameForm.MRUUpdate(Sender,Filename);
child forms (except when creating them) it cannot update menu MRUDisplay(Sender); { Display MRU List (EditForm) }
items on these forms. Hence, the duplicate procedures. end;
end;

Although the routines are straightforward, they must be called at Figure 9 (Top): The OpenChild procedure.
the correct time to display the current MRU list and avoid possi- Figure 10 (Bottom): The EditForm.Save1Click procedure.
ble errors. The EditForm version is the busiest, as the EditForm’s
menu is displayed whenever files are open. The FrameForm ver- must also be added to the end of the EditForm.Save1Click
sion of the menu is only called when the application is first acti- procedure as shown in Figure 10. This allows the program to
vated, and when an MDI child form is closed. This updates the display the current MRU information when a file has been
parent form’s menu in case the child form being closed is the last saved and the MRU list is updated.
one and the parent form’s menu will be displayed again. To
enable these functions, add: Finally, to update the MRU menu items in MDI child windows,
MRUDisplay(Sender);
the application must be instructed to call EditForm.MRUDisplay
whenever the focus switches between forms. Otherwise, each child
to the last line of the FrameForm.FormCreate procedure, and add: window displays the MRU list items as they were when the form
was last updated, and not necessarily the current information. To
FrameForm.MRUDisplay(Sender); enable this display update, set the EditForm’s OnActivate event to
MRUDisplay, using the Events Page of the Object Inspector.
to the last line of the EditForm.FormClose procedure.
Opening Files from the MRU List
The EditForm.MRUDisplay procedure is called by the The last task in adding an MRU list to an application is actually
FrameForm.OpenChild procedure by adding the statement: opening the files with the click of a button. The TEXTEDIT
application can already open a child window using the
EditForm.MRUDisplay(Sender); FrameForm.OpenChild procedure. This procedure obtains the
name of the file to open from an Open File common dialog
at the last line of the procedure. This procedure defines and box. Since the MRU list already contains the name of the
creates the child form. Since EditForm is declared as a local desired file, the dialog box is unnecessary.
variable in this FrameForm procedure, the procedure can call
EditForm.MRUDisplay, even though it is invisible to the rest of The FrameForm.MRUOpenChild procedure (see Figure 11) must
the program. The complete OpenChild procedure is shown in only reference the MRU list and obtain the filename associated
Figure 9. (Note that all lines but: with the selected MRU menu item. To do this, the procedure uses
the Sender’s Tag property (set when the MRU menu items were
EditForm.MRUDisplay(Sender);
created in step one), and assumes the Sender is a TMenuItem.
already exist in the TEXTEDIT sample application.)
Once this event handler is written, it should be set as the OnClick
event handler for all four MRU menu items (i.e. MRU1, MRU2,
The same statement:
MRU3, and MRU4) on the FrameForm main menu. The corre-
EditForm.MRUDisplay(Sender);
sponding event handler for the EditForm MRU menu items should

DECEMBER 1995 Delphi INFORMANT ▲ 21


Informant Spotlight

procedure TFrameForm.MRUOpenChild(Sender: TObject);


var
EditForm: TEditForm;
Index: Integer;
begin
{ Set Index using Sender.Tag }
Index := TMenuItem(Sender).Tag;
if MRUList[Index] <> '' then
begin
EditForm := TEditForm.Create(Self);
EditForm.open(MRUList[Index]);
EditForm.show;
EditForm.MRUDisplay(Sender);
end;
end;

Figure 11 (Left): The FrameForm.MRUOpenChild procedure. Figure 12 (Right): The TEXTEDIT application with its new MRU list
displayed.

be called MRUClick. The procedure EditForm.MRUClick references as files are opened and saved. Add procedures to display the con-
FrameForm.MRUOpenChild: tents of the string list to the menu items on both parent and child
forms. Finally, include a simple event handler to open files from
procedure TEditForm.MRUClick(Sender: TObject); the string list when the corresponding menu item is clicked.
begin
FrameForm.MRUOpenChild(Sender);
end; So there you have it! Five steps to a more professional, user-
friendly application. ∆
Ready to Run
The MRU list in TEXTEDIT.DPR is now ready to run (see
Figure 12). It will keep track of the files and allow those used last The demonstration project referenced in this article is available on
to be instantly opened. the 1995 Delphi Informant Works CD located in
INFORM\95\DEC\DI9512DH.
It should now be easy to add an MRU list to any new or existing
Delphi application by following these steps. First, add the menu
objects and string list to the application. Next, add simple proce- Douglas Horn is a free-lance writer and computer consultant in Seattle, WA. He spe-
dures for moving data back and forth between the string list and cializes in multilingual applications, particularly those using Japanese and other Asian
an .INI file. Then, write an event handler to update the string list languages. He can be reached via e-mail at internet:horn@halcyon.com.

DECEMBER 1995 Delphi INFORMANT ▲ 22


Useless Stuff
Delphi / Object Pascal

By David Faulkner

Upping the Ante


Building a Poker Game with the TCardDeck Component

h sure, Delphi Informant is full of useful, business-like, get-the-job-done-


O better-and-faster components. Boring stuff. Now for some fun.

This article presents a playing card component, TCardDeck, and a video poker game (see Figure 1)
easily made from that component. We’ll start by learning how to use the component, and then deal
with some technical details of its implementation. (And if you download this component, you could
try cutting in on the lucrative Solitaire market for Windows 95.)

Installing the CardDeck Component


To use the CardDeck component, download the source code and place
it in a directory (e.g. \DELPHI\CARDDECK). The necessary files are
shown in Figure 2.
File Function
With the source code down- CARDDECK.PAS The source code.
loaded, you can install the CARDDECK.RES A resource file with the
component. To do this, start cardface bitmaps.

Delphi, open the Options CARDDECK.DCR A Delphi resource file with


the CardDeck’s Component
menu, and select Install Palette icon.
Components to display the
Figure 2: The Delphi files necessary to get you
Install Components dialog box. onto the playing table.
Click the Add button and sup-
ply the path and name of CARDDECK.PAS. If you don’t have a component
named TCardDeck, Delphi adds CardDeck to the Installed Units list. Click
the OK button. Delphi compiles the new component and places it on the
DI (Delphi Informant) tab of the Component Palette (see Figure 3).

Using the TCardDeck


Start a new project and drop a CardDeck component on it. The visual repre-
sentation of TCardDeck is an Ace of Spades, 71 pixels wide and 96 pixels tall
(see Figure 4).

TCardDeck is a descendant of TGraphicControl (as is TLabel and


TShape) so you are probably familiar with its properties. Two new
properties introduced by this component are Stretch and Value. Stretch
Figure 1: This video poker game was built using the is a Boolean type, and when it’s set to True the card face bitmap is
TCardDeck component. The first screen shows the player held
onto a pair of nines and then pressed the Deal button. When
stretched to TCardDeck’s height and width. When False, the card face
a pair of aces is added to the held cards, Delphi flashes bitmap is always 71 x 96 pixels, regardless of the component’s height
smiles. and width properties.

DECEMBER 1995 Delphi INFORMANT ▲ 23


Useless Stuff

TCardDeck Informant
When writing a component, you must first decide which existing
component to descend from. In TCardDeck’s case, descending
from TGraphicControl seems logical. It already has some essential
properties, such as Position and Size, and has a canvas that makes
drawing easy. Additionally, TGraphicControl has events such as
Paint and drag-and-drop support that are necessary in the
TCardDeck component.

TCardDeck was created with the Component Expert (see Figure 6).
To open this dialog box, select File | New Component. When you
press the OK button, Delphi builds a unit with the necessary class
declaration and register procedure. Now, complete the code to
make the component functional.

Constants
CARDDECK.PAS begins by declaring many constants. Each card
name, color, and card are assigned numeric values. A number of
subrange types are created to simplify programming with
TCardDeck. For example, the constants:
Figure 3 (Top): The TCardDeck component on the DI page of the
cdSpades = [1..13];
Component Palette. Figure 4 (Bottom): The component on a form.
cdHearts = [14..26];
cdClubs = [27..39];
The Value property is an integer that represents the card face dis- cdDiamonds = [40..52];
cdBlackCards = [1..13,27..39];
played. 1 is the Ace of Spades, 2 is the 2 of Spades, 14 is the Ace cdRedCards = [14..26,40..52];
of Hearts, 27 is the Ace of Clubs, 40 is the Ace of Diamonds,
and 52 is the King of Diamonds. You can work with numbers or make it easy to determine a card’s color or suit.
a set of constants that adds readability to programs using
TCardDeck components. Figure 4, for example, shows the con- A set of TCardEntries is also configured as a constant. It’s
stant cdAceOfSpades in the Object Inspector. declared as:
The last 10 “cards” are Card 53 (cdJoker), the Joker, and cards 54 type
TCardEntry = record
to 62 (cdCardBack1..cdCardBack9) with bitmaps representing the Value: cdCardDeck;
back of each card. Name: PChar;
end;
const
TCardDeck Utility Functions Cards: array[1..62] of TCardEntry =
The ability to display card faces on a form makes it easy to ((Value: cdAceofSpades;
write card games, but TCardDeck goes further with some utility Name: 'cdAceofSpades'),
Value: cd2ofSpades;
functions. When a TCardDeck component is placed on a form, Name: 'cd2ofSpades'),
Delphi automatically includes the CARDDECK.PAS unit in
your form’s unit. By using TCardDeck, you automatically have Using the constant Cards array, it’s simple to map the string
access to the functions shown in Figure 5. version of a card’s value to its integer value. This is necessary
to set up TCardDeck’s Value property which accepts a string or
There is no built-in help for these functions or the extra proper- an integer.
ties defined by TCardDeck. Therefore, you may want to keep the
source code open on an editor tab. To do this, select File | Open TCardDeck.Paint
File from Delphi’s menu. Then, select CARDDECK.PAS from TCardDeck’s most important feature is its ability to display
the appropriate directory. You’ll find a component’s source code bitmaps that represent card faces. To do this, TCardDeck over-
is as good as any help system. rides TGraphicComponent’s Paint procedure:
We often have several tabs open in the editor to Delphi’s type
units (buy the VCL source if you don't have it). Note that TCardDeck = class(TGraphicControl)
although the File Open dialog box starts with a *.PAS filter, protected
the editor can be used to view any text file (such as those { Protected declarations }
written for an import routine, or HTML source code that procedure Paint; override;
may be interfaced).

DECEMBER 1995 Delphi INFORMANT ▲ 24


Useless Stuff

Function Description
function IsCardRed(Value: cdCardDeck) : Boolean; Accepts a card value. Returns True if card is red, otherwise False.
function IsCardBlack(Value: cdCardDeck) : Boolean; Accepts a card value. Returns True if card is black, otherwise False.
function IsFaceCard(Value: cdCardDeck) : Boolean; Accepts a card value. Returns True if card is a face card,
othewise False.
function AreCardsSameColor(Value1, Accepts two card values. Returns True if cards are the same color,
Value2: cdCardDeck) : otherwise False.
Boolean;
function AreCardsSameSuit(Value1, Accepts two card values. Returns True if cards are the same suit,
Value2: cdCardDeck) : otherwise False.
Boolean;
function AreCardsSameValue(Value1, Accepts two card values. Returns True if the cards have the same
Value2: cdCardDeck) : face value and the same color, otherwise False. For example, if Figure 5: Each function used
Boolean; the 2 of spades and 2 of diamonds are passed, True is returned. by TCardDeck.
function CardColor(Value: cdCardDeck) : integer; Returns an integer representing the color of the card passed to it.
CARDDECK.PAS defines the following constants for card colors:
cdBlack = 1;
cdRed = 2;
function CardSuit(Value: cdCardDeck) : integer; Returns an integer representing the suit of the card passed to it.
CARDDECK.PAS defines the following constants for card suits:
cdSpade = 1;
cdHeart = 2;
cdClub = 3;
cdDiamond = 4;
function CardValue(Value: cdCardDeck) : integer; Returns an integer representing the face value of the card passed
to it. The number 1 represents an Ace, 2 represents a 2, and 13
represents a King.

Simply stated,
whenever procedure TCardDeck.Paint;
var
Windows requests BitMap:TBitMap;
TCardDeck to FacePChar: array [0..25] of char;
paint itself, the begin
strpcopy(FacePChar,CardToString(Value));
TCardDeck.Paint BitMap := TBitMap.create;
procedure is called. try
See Figure 7 for an BitMap.handle := LoadBitMap(HInstance,FacePChar);
if not BitMap.empty then
abbreviated ver- Figure 6: The Component Expert dialog box. begin
sion. The code not if Stretch then
shown paints the corners of the cards the same color as the canvas.stretchdraw(clientrect,BitMap)
else
background. The entire code listing for the project begins on Canvas.draw(0,0,BitMap);
page 27. end;
finally
BitMap.free;
Some important information should be noted within this proce- end;
dure. Once the variable BitMap is created, the code uses a end;
try..finally block. This protects the component from leaking
memory if a GPF occurs. The BitMap.Create command (or any Figure 7: An abbreviated version of the TCardDeck.Paint procedure.
Create command) allocates memory. As a component designer,
you must free this memory, even if the component causes an the dollar symbol ( $ ) on a blank line in the code editor and
exception during processing. In this case, if something goes press 1. You can now surf the help system for information.
wrong while painting the component, the finally clause calls With CARDDECK.RES included in the .EXE, this code is used
BitMap.Free to release the allocated memory. to extract a bitmap from the resource file:

The bitmaps for card faces are stored in CARDDECK.RES, a stan- BitMap.handle := LoadBitMap(HInstance,FacePChar);

dard Windows resource file. With the Borland Resource Editor or


Delphi’s Image Editor, you can view and modify bitmaps outside The LoadBitMap procedure is actually a Windows API call.
Delphi. To make resources in an .RES file available at run-time, a Since the WINPROCS.PAS unit wraps many Windows API
compiler directive is necessary. It instructs Delphi to include the calls, including this one, calling LoadBitMap is relatively simple.
.RES file in the .EXE file. For example, TCardDeck uses the: The HInstance variable is a global variable available in Delphi
programs. In the call above, HInstance tells Windows which
{$R CARDDECK.RES} “instance” of a program is running, and to find the bitmap in
the correct .EXE. The FacePChar is a PChar version of a
compiler directive. Although it resembles one, it’s not a com- CardDeck.Value. The bitmaps in the resource files are identified
ment. To learn more about the many compiler directives, type by these values, i.e. cdAceOfSpades, cd2OfSpades, etc.

DECEMBER 1995 Delphi INFORMANT ▲ 25


Useless Stuff

With the bitmap loaded, this code paints the screen: the SetValue procedure:

if Stretch then procedure TCardValueProperty.SetValue(const Value: string);


canvas.stretchdraw(clientrect,BitMap) var
else NewValue: cdCardDeck;
canvas.draw(0,0,BitMap); begin
if IdentToCard(Value, NewValue) then
SetOrdValue(NewValue)
The cdCardDeck Custom Property Editor else inherited SetValue(Value);
The next fun part of the CardDeck component is the custom end;
property editor for TCardDeck’s Value property. When Value
is displayed in the Object Inspector, a card name or number This code checks the new value. If the check fails, i.e. IdentToCard
can be entered, or a card can be selected from a list. Although returns False, the code calls the TIntegerProperty.SetValue procedure
it’s beneficial to the component user, it’s work for the compo- to trigger an error.
nent writer.
GetValues: Since TCardValueProperty allows a card name to be
First, TCardValueProperty is declared by descending from selected from a drop-down list, TCardValueProperty must supply
TIntegerProperty: that list to the Object Inspector. This is accomplished with the
GetValues procedure:
type TCardValueProperty = class(TIntegerProperty)
public procedure TCardValueProperty.GetValues(Proc: TGetStrProc);
function GetAttributes: TPropertyAttributes; override; var
function GetValue: string; override; I: Integer;
procedure GetValues(Proc: TGetStrProc); override; begin
procedure SetValue(const Value: string); override; for I := Low(Cards) to High(Cards) do
end; Proc(Cards[I].Name);
end;
GetAttributes: While TCardValueProperty is similar to
TIntegerProperty, it’s different because we want a drop-down list This is an odd procedure. It’s passed a pointer to another proce-
of values. To tell the Object Inspector that TCardValueProperty dure as a parameter, and this passed procedure calls for each card
has a list of values, the code cited above overrides the name in the drop-down list. When GetValues is finished, each
GetAttributes function with a single line of code: item in the drop-down list is registered with the Object Inspector.

function TCardValueProperty.GetAttributes: Registering the TCardValueProperty


TPropertyAttributes;
begin With TCardValueProperty declared and its code written, Delphi
Result := [paValueList,paMultiSelect]; must be told that the new property editor exists. This is done in
end; TCardDeck’s Register procedure:

The paValueList constant tells the Object Inspector that a drop- procedure Register;
down list is available. The paMultiSelect constant instructs the begin
RegisterComponents('DI',[TCardDeck]);
Object Inspector to continue displaying this property if more RegisterPropertyEditor(TypeInfo(cdCardDeck),TCardDeck,
than one TCardDeck component is selected. This allows you to 'Value',TCardValueProperty);
select multiple instances of a TCardDeck object and set their val- end;

ues simultaneously in the Object Inspector.


RegisterPropertyEditor informs Delphi of the new property editor
GetValue: The Object Inspector can only display properties as type. The first parameter is information about the property’s Type.
strings. Since the TCardDeck Value property is an integer, the The second parameter indicates that the TCardValueProperty can
GetValue procedure is overridden to convert the integer into a string: only be applied to TCardDeck components, and the third tells
Delphi the TCardValueProperty can only be applied to properties
function TCardValueProperty.GetValue: string; named Value. This parameter can remain blank to use
begin TCardValueProperty with properties other than Value. The final
Result := CardToString(GetOrdValue);
end; parameter is the class of property editor.

The CardToString function is part of the code in the Conclusion


TCardDeck unit and returns a user-readable string such as Several years ago I received a little hand-held US$20 poker
cdAceOfSpades. game as a Christmas gift. It seemed to me a reasonable experi-
ment to recreate this game on a US$3000 90MHz Pentium.
SetValue: TCardValueProperty must limit the property’s This exercise is made easy with the help of Delphi and the
expected values to integers between 1 and 62, as well as iden- TCardDeck component. Download the program and have some
tifiers from cdAceOfSpades to cdCardBack9. When the user fun. We’ll explore the game’s inner workings in a future article.
inputs a value into this property, the Object Inspector calls If you’ve never written a component before, give it a try. To see

DECEMBER 1995 Delphi INFORMANT ▲ 26


Useless Stuff

your custom component on the Component Palette, and set its


cdAceofClubs = 27;
custom properties in the Object Inspector — it’s a real feeling of cd2ofClubs = 28;
accomplishment. ∆ cd3ofClubs = 29;
cd4ofClubs = 30;
cd5ofClubs = 31;
The CardDeck component and sample poker game referenced in this
cd6ofClubs = 32;
article are available on the 1995 Delphi Informant Works CD cd7ofClubs = 33;
located in INFORM\95\DEC\DI9512DF. cd8ofClubs = 34;
cd9ofClubs = 35;
cd10ofClubs = 36;
cdJackofClubs = 37;
cdQueenofClubs = 38;
David Faulkner is a developer with Silver Software in Kula, HI. He is also Contributing cdKingofClubs = 39;
Editor to Paradox Informant, and co-author of Using Delphi: Special Edition (Que, 1995).
Mr Faulkner can be reached at (808) 878-2714, or on CompuServe at 76116,3513. cdAceofDiamonds = 40;
cd2ofDiamonds = 41;
cd3ofDiamonds = 42;
cd4ofDiamonds = 43;
Begin Listing One — Cardsp.DPR cd5ofDiamonds = 44;
program Cardsp;
cd6ofDiamonds = 45;
uses cd7ofDiamonds = 46;
Forms, cd8ofDiamonds = 47;
Cardu in 'CARDU.PAS' { Form1 }; cd9ofDiamonds = 48;
cd10ofDiamonds = 49;
{$R *.RES} cdJackofDiamonds = 50;
cdQueenofDiamonds = 51;
begin cdKingofDiamonds = 52;
Application.CreateForm(TForm1, Form1);
Application.Run; CdJoker = 53;
end. cdCardBack1 =54;
cdCardBack2 = 55;
End Listing One cdCardBack3 = 56;
cdCardBack4 = 57;
Begin Listing Two — CardDeck.PAS cdCardBack5 = 58;
{$R CARDDECK.RES} cdCardBack6 = 59;
cdCardBack7 = 60;
unit CardDeck; cdCardBack8 = 61;
cdCardBack9 = 62;
interface
cdFaceCards = [1,11..14,24..27,37..40,50..52];
uses SysUtils, WinProcs, Classes, Graphics,
Controls, DsgnIntf; cdSpades = [1..13];
cdHearts = [14..26];
const cdClubs = [27..39];
cdAceofSpades = 1; cdDiamonds = [40..52];
cd2ofSpades = 2;
cd3ofSpades = 3; cdBlackCards = [1..13,27..39];
cd4ofSpades = 4; cdRedCards = [14..26,40..52];
cd5ofSpades = 5;
cd6ofSpades = 6; cdSpade = 1;
cd7ofSpades = 7; cdHeart = 2;
cd8ofSpades = 8; cdClub = 3;
cd9ofSpades = 9; cdDiamond = 4;
cd10ofSpades = 10; cdBlack = 1;
cdJackofSpades = 11; cdRed = 2;
cdQueenofSpades = 12;
cdKingofSpades = 13; type
cdCardDeck = 1..62; { 52 cards + 1 joker + 9 backs }
cdAceofHearts = 14;
cd2ofHearts = 15; { Create custom property editor that accepts both
cd3ofHearts = 16; card names or card numbers more easily }
cd4ofHearts = 17; type
cd5ofHearts = 18; TCardEntry = record
cd6ofHearts = 19; Value: cdCardDeck;
cd7ofHearts = 20; Name: string[18];
cd8ofHearts = 21; end;
cd9ofHearts = 22;
cd10ofHearts = 23; { This array maps card numbers to card names }
cdJackofHearts = 24; const
cdQueenofHearts = 25; Cards: array[1..62] of TCardEntry = (
cdKingofHearts = 26; (Value: cdAceofSpades; Name: 'cdAceofSpades'),

DECEMBER 1995 Delphi INFORMANT ▲ 27


Useless Stuff

(Value: cd2ofSpades; Name: 'cd2ofSpades'), public


(Value: cd3ofSpades; Name: 'cd3ofSpades'), function GetAttributes: TPropertyAttributes; override;
(Value: cd4ofSpades; Name: 'cd4ofSpades'), function GetValue: string; override;
(Value: cd5ofSpades; Name: 'cd5ofSpades'), procedure GetValues(Proc: TGetStrProc); override;
(Value: cd6ofSpades; Name: 'cd6ofSpades'), procedure SetValue(const Value: string); override;
(Value: cd7ofSpades; Name: 'cd7ofSpades'), end;
(Value: cd8ofSpades; Name: 'cd8ofSpades'),
(Value: cd9ofSpades; Name: 'cd9ofSpades'), { Now create TCardDeck }
(Value: cd10ofSpades; Name: 'cd10ofSpades'), type
(Value: cdJackofSpades; Name: 'cdJackofSpades'), TCardDeck = class(TGraphicControl)
(Value: cdQueenofSpades; Name: 'cdQueenofSpades'),
(Value: cdKingofSpades; Name: 'cdKingofSpades'), private
FValue: cdCardDeck; { value of card being diplayed }
(Value: cdAceofHearts; Name: 'cdAceofHearts'), FStretch: Boolean; { stretch bitmap to fit client? }
(Value: cd2ofHearts; Name: 'cd2ofHearts'), procedure SetValue(Value: cdCardDeck);
(Value: cd3ofHearts; Name: 'cd3ofHearts'), procedure SetStretch(Value: Boolean);
(Value: cd4ofHearts; Name: 'cd4ofHearts'),
(Value: cd5ofHearts; Name: 'cd5ofHearts'), protected
procedure Paint; override;
(Value: cd6ofHearts; Name: 'cd6ofHearts'),
(Value: cd7ofHearts; Name: 'cd7ofHearts'), public
(Value: cd8ofHearts; Name: 'cd8ofHearts'), constructor Create(AOwner: TComponent); override;
(Value: cd9ofHearts; Name: 'cd9ofHearts'),
(Value: cd10ofHearts; Name: 'cd10ofHearts'), published
(Value: cdJackofHearts; Name: 'cdJackofHearts'), property Value: cdCardDeck read FValue
(Value: cdQueenofHearts; Name: 'cdQueenofHearts'), write SetValue default cdAceofSpades;
(Value: cdKingofHearts; Name: 'cdKingofHearts'), property Stretch: Boolean read FStretch
write SetStretch default False;
(Value: cdAceofClubs; Name: 'cdAceofClubs'), { surface drag and drop events so programmers can
(Value: cd2ofClubs ; Name: 'cd2ofClubs'), make cards that drag }
(Value: cd3ofClubs ; Name: 'cd3ofClubs'), property DragCursor;
(Value: cd4ofClubs ; Name: 'cd4ofClubs'), property DragMode;
(Value: cd5ofClubs ; Name: 'cd5ofClubs'), property OnDragDrop;
(Value: cd6ofClubs ; Name: 'cd6ofClubs'), property OnDragOver;
(Value: cd7ofClubs ; Name: 'cd7ofClubs'), property OnEndDrag;
(Value: cd8ofClubs ; Name: 'cd8ofClubs'), property OnMouseDown;
(Value: cd9ofClubs ; Name: 'cd9ofClubs'), property OnMouseMove;
(Value: cd10ofClubs; Name: 'cd10ofClubs'), property OnMouseUp;
(Value: cdJackofClubs; Name: 'cdJackofClubs'), property OnClick;
(Value: cdQueenofClubs; Name: 'cdQueenofClubs'), property OnDblClick;
(Value: cdKingofClubs; Name: 'cdKingofClubs'),
end;
(Value: cdAceofDiamonds; Name: 'cdAceofDiamonds'),
(Value: cd2ofDiamonds; Name: 'cd2ofDiamonds'), function CardToIdent(Card: cdCardDeck;
(Value: cd3ofDiamonds; Name: 'cd3ofDiamonds'), var Ident: string): Boolean;
(Value: cd4ofDiamonds; Name: 'cd4ofDiamonds'), function IdentToCard(const Ident: string;
(Value: cd5ofDiamonds; Name: 'cd5ofDiamonds'), var Card: cdCardDeck): Boolean;
(Value: cd6ofDiamonds; Name: 'cd6ofDiamonds'), function CardToString(Card: cdCardDeck): string;
(Value: cd7ofDiamonds; Name: 'cd7ofDiamonds'), procedure Register;
(Value: cd8ofDiamonds; Name: 'cd8ofDiamonds'),
(Value: cd9ofDiamonds; Name: 'cd9ofDiamonds'), { Utility routines for the programmer }
(Value: cd10ofDiamonds; Name: 'cd10ofDiamonds'), function IsCardRed(Value:cdCardDeck): Boolean;
(Value: cdJackofDiamonds; Name: 'cdJackofDiamonds'), function IsCardBlack(Value:cdCardDeck): Boolean;
(Value: cdQueenofDiamonds; Name: 'cdQueenofDiamonds'), function IsFaceCard(Value:cdCardDeck): Boolean;
(Value: cdKingofDiamonds; Name: 'cdKingofDiamonds'),
function AreCardsSameColor(Value1,
(Value: CdJoker; Name: 'CdJoker'), Value2: cdCardDeck) : Boolean;
(Value: cdCardBack1; Name: 'cdCardBack1'), function AreCardsSameSuit(Value1,
(Value: cdCardBack2; Name: 'cdCardBack2'), Value2: cdCardDeck) : Boolean;
(Value: cdCardBack3; Name: 'cdCardBack3'), function AreCardsSameValue(Value1,
(Value: cdCardBack4; Name: 'cdCardBack4'), Value2: cdCardDeck) : Boolean;
(Value: cdCardBack5; Name: 'cdCardBack5'),
(Value: cdCardBack6; Name: 'cdCardBack6'), function CardColor(Value: cdCardDeck): integer;
(Value: cdCardBack7; Name: 'cdCardBack7'), function CardSuit(Value: cdCardDeck): integer;
(Value: cdCardBack8; Name: 'cdCardBack8'), function CardValue(Value: cdCardDeck): integer;
(Value: cdCardBack9; Name: 'cdCardBack9') );
implementation
type
{ Create a custom property editor for cdCardDeck -- { Put cdCardDeck on palette, register custom property editor }
allows user to type in name of card, type in number procedure Register;
of card, or select card from list } begin
TCardValueProperty = class(TIntegerProperty) RegisterComponents('DI', [TCardDeck]);

DECEMBER 1995 Delphi INFORMANT ▲ 28


Useless Stuff

RegisterPropertyEditor(TypeInfo(cdCardDeck),TCardDeck, procedure TCardDeck.SetStretch(Value: Boolean);


'Value',TCardValueProperty); begin
end; if Value<>FStretch then
begin
{ paint the card face bitmap on the screen } if not Value then begin
procedure TCardDeck.Paint; Height := 96;
var Width := 71;
BitMap:TBitMap; end;
BackGroundColor:TColor; FStretch := Value;
FacePChar: array [0..25] of char; Invalidate;
begin end;
{ convert value to Pchar so can call Windows API } end;

strpcopy(FacePChar,CardToString(Value)); { ** TCardValueProperty custom property editor routines ** }


BitMap := TBitMap.create; function TCardValueProperty.GetAttributes:
try TPropertyAttributes;
BitMap.handle := LoadBitMap(HInstance,FacePChar); begin
if not BitMap.empty then begin Result := [paValueList, paMultiSelect];
if Stretch then end;
canvas.stretchdraw(clientrect,BitMap)
function TCardValueProperty.GetValue: string;
else begin
{ draw clear rectangle to see color of background } begin
canvas.brush.style:=bsclear; Result := CardToString(GetOrdValue);
canvas.pen.style:=psclear; end;
canvas.rectangle(0,0,0,0);
BackGroundColor:=canvas.pixels[0,0]; procedure TCardValueProperty.GetValues(Proc: TGetStrProc);
{ now draw the bitmap } var
Canvas.draw(0,0,BitMap); I: Integer;
{ set corners of bitmap to background color } begin
canvas.pixels[0,0]:=BackGroundColor; for I := Low(Cards) to High(Cards) do Proc(Cards[I].Name);
canvas.pixels[1,0]:=BackGroundColor; end;
canvas.pixels[0,1]:=BackGroundColor;
procedure TCardValueProperty.SetValue(const Value: string);
canvas.pixels[bitmap.width-1,0] := BackGroundColor; var
canvas.pixels[bitmap.width-2,0] := BackGroundColor; NewValue: cdCardDeck;
canvas.pixels[bitmap.width-1,1] := BackGroundColor; begin
if IdentToCard(Value, NewValue) then
canvas.pixels[bitmap.width-1, SetOrdValue(NewValue)
bitmap.height-1] := BackGroundColor; else inherited SetValue(Value);
canvas.pixels[bitmap.width-2, end;
bitmap.height-1] := BackGroundColor; { end of TCardValueProperty custom property editor routines }
canvas.pixels[bitmap.width-1,
bitmap.height-2] := BackGroundColor; function CardToString(Card: cdCardDeck): string;
begin
canvas.pixels[0,bitmap.height-1]:= BackGroundColor; if not CardToIdent(Card, Result) then
canvas.pixels[1,bitmap.height-1]:= BackGroundColor; FmtStr(Result, '$%.8x', [Card]);
canvas.pixels[0,bitmap.height-2]:= BackGroundColor; end;
end;
end; function CardToIdent(Card: cdCardDeck;
finally var Ident: string): Boolean;
BitMap.free; var
end; I: Integer;
end; begin
Result := False;
constructor TCardDeck.Create(AOwner: TComponent); for I := Low(Cards) to High(Cards) do
begin
inherited Create(AOwner); if Cards[I].Value = Card then
Height := 96; begin
Width := 71; Result := True;
Value := cdAceofSpades; Ident := Cards[I].Name;
end; Exit;
end;
procedure TCardDeck.SetValue(Value: cdCardDeck); end;
begin
if Value<>FValue then function IdentToCard(const Ident: string;
begin var Card: cdCardDeck): Boolean;
FValue := Value; var
repaint; I: Integer;
end; begin
end; Result := False;
for I := Low(Cards) to High(Cards) do

DECEMBER 1995 Delphi INFORMANT ▲ 29


Useless Stuff

if CompareText(Cards[I].Name, Ident) = 0 then


begin Begin Listing Three — Cardu.PAS
Result := True; unit Cardu;
Card := Cards[I].Value;
Exit; interface
end;
end; uses WinTypes, WinProcs, Classes, Graphics, Forms,
Controls, StdCtrls, SysUtils, Dialogs, mmSystem, ExtCtrls,
{ ** utility routines for the programmer ** } Carddeck, Buttons;
function IsCardRed(value: cdCardDeck): Boolean;
begin type
Result := value in cdRedCards; TForm1 = class(TForm)
end; DealOrDraw: TButton;
Hold1: TLabel;
function IsCardBlack(value: cdCardDeck): Boolean; Hold2: TLabel;
begin Hold3: TLabel;
Result := value in cdBlackCards; Hold4: TLabel;
end; Hold5: TLabel;
HoldButton1: TButton;
HoldButton2: TButton;
HoldButton3: TButton;
function AreCardsSameColor(Value1, HoldButton4: TButton;
Value2: cdCardDeck): Boolean; HoldButton5: TButton;
begin Timer1: TTimer;
Result := CardColor(Value1) = CardColor(Value2); HoldOrDraw: TLabel;
end; Shape1: TShape;
Pot: TLabel;
function AreCardsSameSuit(Value1, CardDeck1: TCardDeck;
Value2: cdCardDeck) : Boolean; CardDeck2: TCardDeck;
begin CardDeck3: TCardDeck;
Result := CardSuit(Value1) = CardSuit(Value2); CardDeck4: TCardDeck;
end; CardDeck5: TCardDeck;
Label1: TLabel;
function AreCardsSameValue(Value1, Label2: TLabel;
Value2: cdCardDeck) : Boolean; Label3: TLabel;
begin Label4: TLabel;
Result := CardValue(Value1) = CardValue(Value2); Label5: TLabel;
end; Label6: TLabel;
Label7: TLabel;
function IsFaceCard(Value: cdCardDeck):Boolean; Label8: TLabel;
begin Label9: TLabel;
Result := value in cdFaceCards; Label10: TLabel;
end; Label11: TLabel;
Label12: TLabel;
function CardColor(Value: cdCardDeck):integer; Label13: TLabel;
begin Label14: TLabel;
result:=0; Label15: TLabel;
if value in cdRedCards then Result := cdRed; Label16: TLabel;
if value in cdBlackCards then Result := cdBlack; SoundButton: TBitBtn;
end;
procedure DealOrDrawClick(Sender: TObject);
function CardSuit(Value: cdCardDeck):integer; procedure FormCreate(Sender: TObject);
begin procedure HoldButton1Click(Sender: TObject);
result:=0; procedure SoundButtonClick(Sender: TObject);
if value in cdSpades then Result := cdSpade; procedure Timer1Timer(Sender: TObject);
if value in cdHearts then Result := cdHeart;
if value in cdClubs then Result := cdClub; end;
if value in cdDiamonds then Result := cdDiamond;
end; type
TCardHand = array[1..5] of byte;
function CardValue(Value: cdCardDeck): integer;
var var
Suit: integer; Form1: TForm1;
begin aUsedCards: array[1..52] of Boolean;{ card dealt yet? }
result := 0; aUsedTag: array[1..5] of Boolean; { card scored yet? }
Suit := CardSuit(Value); bFirstDeal: Boolean; { opening deal? }
if Suit<>0 then result := Value-(Suit-1)*13 aHand: TCardHand; { cards in hand }
end; iWinnings: Longint; { loot from last win }
bBlinker: Boolean; { blink winning cards }
end. bNextSong: Boolean; { song to play on win }
bSound: Boolean; { is sound turned on? }
End Listing Two bJustWon: Boolean; { did user just win? }

DECEMBER 1995 Delphi INFORMANT ▲ 30


Useless Stuff

implementation TCardDeck(FindComponent('CardDeck'+
inttostr(iCardCount))).value:=x;
{$R *.DFM} if bSound then sndPlaySound('CARD.WAV',2);
aUsedCards[x]:=True;
procedure TForm1.DealOrDrawClick(Sender: TObject); aHand[iCardCount]:=x;
var Tlabel(FindComponent('Hold'
x,y,z: byte; { general purpose } inttostr(iCardCount))).visible := False;
iCurrentCount: byte; { # of matching cards in hand } end;
iCardCount: byte; { used to step through 52 cards } DealOrDraw.Caption:='&Deal';
bTwoOfAKind, { two of a kind }
bTwoPair, { two pair } { Now score the whole thing }
bThreeOfAKind, { three of a kind } bTwoOfAKind := False;
bFourOfAKind, { four of a kind } bTwoPair := False;
bJacksOrBetter, { a pair of jacks or better } bThreeOfAKind := False;
bFlush, { a flush } bFourOfAKind := False;
bStraight, { a straight } bJacksOrBetter := False;
bStraightFlush, { a straight flush } bFlush := True;
bFullHouse, { a full house } bStraight := True;
bRoyalFlush: Boolean; { is this a really lucky player } bFullHouse := False;
aFaceCopy: TCardHand; { a copy of the current hand } for x:=1 to 5 do
aUsedTag[x] := False;
begin
if bSound then { compare face value of each card to other
sndPlaySound('Deal.wav',3); cards counting matches }
for x:=1 to 5 do begin
Timer1.enabled := False; if aUsedtag[x] then continue;
if iWinnings<>0 then begin iCurrentCount:=0;
Pot.caption:=IntToStr(StrToInt(Pot.caption)+iWinnings); for y:=x+1 to 5 do begin
iWinnings:=0; if (CardValue(aHand[x])=CardValue(aHand[y])) and
end; (not aUsedTag[y]) then begin
aUsedTag[y] := True;
if bFirstDeal and (StrToInt(Pot.caption)=0) then begin aUsedTag[x] := True;
MessageDlg('Busted, You have no money left to bet!', inc(iCurrentCount);
mtWarning,[mbOK], 0); if isFaceCard(aHand[x]) then
exit; bJacksOrBetter := True;
end; end;
end; { end of inner for loop }
if bFirstDeal then begin
for x:=1 to 52 do { initialize deck } case
iCurrentCount of
aUsedCards[x] := False; 3:bFourOfAKind := True;
DealOrDraw.Caption := '&Draw'; 2:bThreeOfAKind := True;
HoldOrDraw.Caption := 'Hold or Draw'; 1:if bTwoOfAKind then
Pot.caption := IntToStr(StrToInt(Pot.caption)-5); bTwoPair := True
{ Pick out 5 new cards } else
for iCardCount:=1 to 5 do begin bTwoOfAKind := True;
TButton(FindComponent('HoldButton' + end; { End of CurrentCount case }
inttostr(iCardCount))).enabled:=True; end; { End of outer loop }
TLabel(FindComponent('Hold' +
inttostr(iCardCount))).visible:=False; { it's a flush if CardSuit of all five cards is the same }
repeat { Pick a random card } for x:=2 to 5 do
x := random(51)+1; bFlush := (CardSuit(aHand[1])=
until not aUsedCards[x]; CardSuit(aHand[x])) and bFlush;
TCardDeck(FindComponent('CardDeck' +
inttostr(iCardCount))).value := x; { now detect a straight, start by creating a sorted
if bSound then sndPlaySound('CARD.WAV',2); copy of hand using copy so can 'blink' winning cards }
aUsedCards[x] := True; for x:=1 to 5 do
aHand[iCardCount] := x; afacecopy[x] := CardValue(ahand[x]);
end;
end { just a little bubble sort }
else for x:= 1 to 4 do begin
begin for y:=x to 5 do begin
HoldOrDraw.caption := ''; if afacecopy[y] > afacecopy[x] then begin
for iCardCount := 1 to 5 do z := afacecopy[x];
begin { deal new cards for non held cards } afacecopy[x] := afacecopy[y];
TButton(FindComponent('HoldButton'+ afacecopy[y] := z;
inttostr(iCardCount))).enabled := False; end;
if Tlabel(FindComponent('Hold'+ end;
inttostr(iCardCount))).visible then end;
continue;
repeat { if it’s a straight then each consecutive card will
x := random(51)+1; have face value of one more than previous card }
until not aUsedCards[x]; for x:=2 to 5 do

DECEMBER 1995 Delphi INFORMANT ▲ 31


Useless Stuff

if afacecopy[x]<>afacecopy[x-1]-1 then if bSound then begin


bStraight:=False;
if TLabel(FindComponent('Hold'+s)).visible then
bFullHouse := bThreeOfAKind and bTwoOfAKind; sndPlaySound('HOLD.WAV',3)
bStraightFlush := bStraight and bFlush; else
bRoyalFlush := sndPlaySound('UNHOLD.WAV',3);
bFlush and bStraight and (CardValue(ahand[1])=14); end;

if bRoyalFlush then iWinnings:=3000 else ActiveControl:=DealOrDraw;


if bStraightFlush then iWinnings:=250 else
if bFourOfAKind then iWinnings:=125 else end;
if bFullHouse then iWinnings:=40 else
if bFlush then iWinnings:=25 else procedure TForm1.SoundButtonClick(Sender: TObject);
if bStraight then iWinnings:=20 else begin
if bThreeOfAKind then iWinnings:=15 else bSound := Not bSound;
if bTwoPair then iWinnings:=10 else ActiveControl := DealOrDraw;
if bTwoOfAKind and bJacksOrBetter then end;
iWinnings:=5 else iWinnings:=0;
procedure TForm1.Timer1Timer(Sender: TObject);
{ if winnings are >20 then all five cards are var
involved, otherwise aUsedTag is already set } x: byte;
begin
if bBlinker then begin
if iWinnings >= 20 then for x:=1 to 5 do
for x:=1 to 5 do if aUsedTag[x] then
aUsedTag[x] := True; TCardDeck(FindComponent('CardDeck'+
inttostr(x))).value:=cdCardBack5;
{ if user won then set up timer to give the user end
the loot and play sounds } else begin
if Boolean(iWinnings) then begin for x:=1 to 5 do
if bSound then if aUsedTag[x] then
sndPlaySound('Win.wav',2); TCardDeck(FindComponent('CardDeck'+
bJustWon := True; inttostr(x))).value:=aHand[x];
Timer1.Interval := 200; end;
Timer1Timer(Tobject(TForm1)); bBlinker := not bBlinker;
Timer1.enabled := True;
if iWinnings>0 then begin
end; if bSound then
end; { End of if second hand } sndPlaySound('Coin.wav',3);
Pot.caption := IntToStr(StrToInt(Pot.caption)+1);
bFirstDeal := not bFirstDeal; iWinnings := iWinnings-1;
end; end
else
procedure TForm1.FormCreate(Sender: TObject); if bJustWon then begin
Timer1.Interval := 300;
begin bJustWon := False;
bSound := True; if bSound then begin
bNextSong := False; bNextSong:= not bNextSong;
bBlinker := False; if bNextSong then
Pot.caption := '100'; sndPlaySound('Money.wav',3)
Randomize; else
bFirstDeal := True; sndPlaySound('Happy.wav',3);
end; end;
end;
procedure TForm1.HoldButton1Click(Sender: TObject); end;
var
s: string[1]; end.
i: byte;
begin
if bFirstDeal then exit; End Listing Three
{ get the last character of component that called this
routine, the last character is either 1,2,3,4, or 5 }
s := (sender as Tcomponent).name[length(
(sender as Tcomponent).name)];

{ Each card has a label with the word 'Hold' as its


caption above the card, toggle the visibility of that
label when user clicks Hold button or card }
TLabel(FindComponent('Hold'+s)).visible :=
not TLabel(FindComponent('Hold'+s)).visible;

DECEMBER 1995 Delphi INFORMANT ▲ 32


Sights & Sounds
Delphi / Object Pascal

By Sedge Simons, Ph.D.

Hotspots
Creating Mouse-Sensitive Areas on Your Forms

reating a full-featured graphical user interface for applications

C involving maps, drawings, or charts introduces a challenge for the


Delphi developer. Such applications usually require hotspots, i.e.
on-screen areas or objects that are sensitive to mouse events. This article
introduces regions, a Windows API data structure that enables you to cre-
ate hotspots of any size or shape.

Interacting with an Image


Most of us have used drawing, charting, or mapping software that allows interaction with
an image. For instance, a drawing tool may be used to create a circle that can then be select-
ed and its properties modified. In a geographical information system (GIS), we can click on
a map’s feature to view information about it stored in a database.

These examples illustrate hotspots — areas of an image that are sensitive to mouse events.
These hotspots usually coincide with what Delphi developers consider objects: shapes on a
drawing or features on a map.

The simplest way to create hotspots in a Delphi application is to place components on


the image. [For tips on hotspots, see David Rippy’s “At Your Fingertips” in the August
1995 Delphi Informant.] These components may be visible
objects or transparent ones that overlap part of another
image. This technique has two significant drawbacks:
• Although components can appear to be any shape, they
always cover a rectangular area, and are sensitive to mouse
events anywhere in that rectangle.
• Components that are part of the visible image can
require special programming when printing.

Regions
Regions are a flexible means of creating hotspots. They are
independent of any on-screen visual image and are stored as
data structures. The relationship between regions and an image
is decided by the programmer. The Windows API includes a
full suite of functions for creating and manipulating regions of
practically any size or shape. Under Windows 3.1, the only
Figure 1: Region functions as defined in Delphi’s Windows API restriction is that each region is limited to a size of 32,767 by
on-line help. 32,767 units, or 64KB of memory, whichever is smaller.

DECEMBER 1995 Delphi INFORMANT ▲ 33


Sights & Sounds

A region can be viewed as a set of points. Normally, these points procedure TForm1.ButtonEllipseClick(Sender: TObject);
will coincide with pixels in an image. A function in the var
Windows API tests a point to determine if it’s within a specified x1,x2,y1,y2: Integer;
begin
region. This function can also map mouse events from an on- FreeRegions;
screen image to a set of previously defined regions. Figure 1 ImageHandle := Image1.Canvas.Handle;
shows a list of Windows API region functions. Nregions := 5;
for i := 1 to Nregions do
begin
Using Regions x1 := i*40;
The typical steps for using regions are to: x2 := x1 + 35;
y1 := i*30;
• allocate memory y2 := x1 + 25;
• create and store regions New(RegionRecord);
• respond to events RegionRecord^.rgn := CreateEllipticRgn(x1,y1,x2,y2);
RegionRecord^.id := ' Hello from elliptic region ' +
• free memory IntToStr(i);
RegionList.Add(RegionRecord);
To illustrate these steps, the example application defines end;
ShowOrHide(Sender);
regions that correspond to various shapes shown in a Label1.Caption := 'Elliptic regions created.'
TImage object. It then responds to MouseDown events in end;
the image (see Figure 2).
Figure 3: The custom ButtonEllipseClick procedure.

space in the 64KB of memory used for the data and stack
segments (a Windows 3.1 limitation). In this example, the
Figure 2:
Our sample records are stored in a TList.
application.
The shapes The Object Pascal code shown in Figure 3 creates five
shown in the regions when the user clicks the button labeled Elliptic
TImage object
Regions. A record is stored for each region. Similar code,
correspond to
regions defined shown in Listing Five on page 35, creates regions of differ-
by the code in ent shapes in response to clicks on the other buttons.
Listing Five.
It’s important to remember that the regions are completely
independent of what is shown on screen. In this example, the
ShowOrHide procedure is called after the regions are created.
Allocate memory. When a region is created, the Windows If the Show radio button is checked, ShowOrHide will draw
API allocates memory for the data structure, then returns a shapes on the TImage representing the regions. Otherwise the
handle. It’s your responsibility to track these handles in the image remains blank. Regardless of what the image displays,
Delphi application, and to free corresponding objects when the regions exist and are ready to use.
they become unnecessary.
Respond to events. From a practical standpoint, something on
Create and store regions. Typically, information about the screen will almost always coincide with a region you create. This
region is stored with the region handle. For example, if each is done by executing independent drawing commands when cre-
region represents a record in a table, storing that record’s key ating a region, or creating it first and then drawing a frame
with the region handle allows information to be displayed around it in a specified device context. The latter method repre-
from the table in response to a mouse click. One way to do sents the most versatile code and is implemented in the
this is to define a record object that contains the region han- ShowOrHide procedure (see Figure 4).
dle and any other useful fields.
Interacting with regions means responding to events. In
In this example, the record is defined as: Figure 4, we want to retrieve data from the region record in
response to mouse clicks in the TImage control. Therefore,
arec = record
rgn: HRgn;
the code is attached to the TImage MouseDown event that will
id: string; attempt to map the coordinates of the mouse event to one of
end; the regions we have defined.

In this record rgn is the region handle, and id is a simple The Windows API function, PtInRegion, maps events to
string we create and store as a short description of the region. regions. It returns True if the specified point is inside the
specified region. In response to a MouseDown event on
The records of each region can be stored in an array or in a Image1, we simply iterate through the list of regions, testing
TList. An array may be more convenient, but it requires each to see if the event occurred inside it (see Figure 5).

DECEMBER 1995 Delphi INFORMANT ▲ 34


Sights & Sounds

procedure TForm1.ShowOrHide(Sender: TObject); procedure TForm1.FormClose(Sender: TObject;


begin var Action: TCloseAction);
Bitmap := Tbitmap.Create; begin
Bitmap.Width := Image1.Width; FreeRegions;
BitMap.Height := Image1.Height; DeleteObject(RedBrush);
Image1.Picture.Graphic := Bitmap; RegionList.Free;
if RadioButtonShow.Checked then end;
begin
ImageHandle := Image1.Canvas.Handle; procedure TForm1.FreeRegions;
for i := 0 to RegionList.Count - 1 do var
begin i: Integer;
RegionRecord := RegionList.Items[i]; begin
FrameRgn(ImageHandle,RegionRecord^.Rgn, for i := 0 to RegionList.Count - 1 do
RedBrush,1,1); begin
end; RegionRecord := RegionList.Items[i];
Image1.Invalidate; DeleteObject(RegionRecord^.Rgn);
Label1.Caption := ''; end;
end; RegionList.Clear;
Bitmap.Free; end;
end;

Figure 6: The FormClose and custom FreeRegions procedures are


procedure TForm1.Image1MouseDown (Sender: TObject; responsible for releasing resources.
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var region functions are problem solvers in other situations, too.
i: Integer;
begin
Just try writing a function like PtInRegion yourself! ∆
Label1.Caption := '';
for i := 0 to RegionList.Count - 1 do The demonstration project referenced in this article is available
begin
RegionRecord := RegionList.Items[i];
on the 1995 Delphi Informant Works CD located in
if PtInRegion(RegionRecord^.Rgn, x, y) then INFORM\95\DEC\DI9512SS.
Label1.Caption := RegionRecord^.id;
end;
end; Dr. Simons is a senior systems analyst at Jensen Data Systems, Inc., a Texas-based
provider of database training, consulting, and application development. He writes
Figure 4 (Top): The custom ShowOrHide procedure. Figure 5 applications and does consulting in Delphi and Paradox. You can reach him through
(Bottom): The custom Image1MouseDown procedure. CompuServe at 70771,75 or by calling Jensen Data Systems, Inc. at (713) 359-3311.
Free memory. As mentioned earlier, it’s the programmer’s
responsibility to free the memory occupied by region objects.
In this example, we must also free the TList object that stores Begin Listing Four — Region.DPR
the records for each region. Each region is freed with a call to program Region;

DeleteObject. Then we can clear the TList. On the FormClose uses


event (see Figure 6), the TList is freed in the usual Delphi Forms,
manner. And don’t free the TList first! You need those handles Region1 in 'REGION1.PAS' {Form1};
to free the regions.
{$R *.RES}

Using the Application begin


To create regions when you first run the application, select Application.CreateForm(TForm1, Form1);
Application.Run;
the Show radio button and click one of the three region but-
end.
tons. Outlines corresponding to the regions are then drawn End Listing Four
on the image. Remember, the regions are not part of the
image. As you click on different parts of the image, the pro- Begin Listing Five — Region1.PAS
unit Region1;
gram searches the regions to determine if the MouseDown
event occurred inside a region. If so, it will display the string interface
that was saved with the region record.
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics,
Select the Hide radio button and continue clicking on the Controls, Forms, Dialogs, ExtCtrls, StdCtrls;
image. The regions remain in place, and the mouse response
is the same. However, nothing has been drawn on the image. type
Prec = ^arec;
arec = record
Conclusion rgn: HRgn;
Regions let you create just about any type of hotspot you can id: string;
imagine. Although hotspots are probably the most common end;
application, regions and the powerful set of Windows API

DECEMBER 1995 Delphi INFORMANT ▲ 35


Sights & Sounds

{ The next three procedures create regions in resonse to


TForm1 = class(TForm)
the buttons on the form. }
ScrollBox1: TScrollBox;
procedure TForm1.ButtonEllipseClick(Sender: TObject);
Image1: TImage;
var
ButtonEllipse: TButton;
x1,x2,y1,y2: Integer;
Bevel1: TBevel;
begin
Label1: TLabel;
FreeRegions;
ButtonPolygon: TButton;
ImageHandle := Image1.Canvas.Handle;
ButtonRectangle: TButton;
Nregions := 5;
Label2: TLabel;
for i := 1 to Nregions do
Bevel2: TBevel;
begin
RadioButtonShow: TRadioButton;
x1 := i*40;
RadioButtonHide: TRadioButton;
x2 := x1 + 35;
procedure Image1MouseDown(Sender: TObject; Button:
y1 := i*30;
TMouseButton; Shift: TShiftState; X, Y: Integer);
y2 := x1 + 25;
procedure FormCreate(Sender: TObject);
New(RegionRecord);
procedure FormClose(Sender: TObject;
RegionRecord^.rgn := CreateEllipticRgn(x1,y1, x2,y2);
var Action: TCloseAction);
RegionRecord^.id := ' Hello from elliptic region ' +
procedure FreeRegions;
IntToStr(i);
procedure ButtonPolygonClick(Sender: TObject);
RegionList.Add(RegionRecord);
procedure ButtonEllipseClick(Sender: TObject);
end;
procedure ButtonRectangleClick(Sender: TObject);
ShowOrHide(Sender);
procedure ShowOrHide(Sender: TObject);
Label1.Caption := 'Elliptic regions created.'
private
end;
{ Private declarations }
public
procedure TForm1.ButtonPolygonClick(Sender: TObject);
{ Public declarations }
var
end;
PointArray: array[1..10] of TPoint;
npoints: integer;
var
begin
Form1: TForm1;
FreeRegions;
RedBrush: HBrush;
ImageHandle := Image1.Canvas.Handle;
ImageHandle: HDC;
Nregions := 3;
Bitmap: Tbitmap;
PointArray[1] := Point(5, 5);
PointArray[2] := Point(100, 5);
Nregions: Integer;
PointArray[3] := Point(110, 70);
RegionList: Tlist;
New(RegionRecord);
RegionRecord: Prec;
RegionRecord^.rgn := CreatePolygonRgn(PointArray, 3,
i: Integer;
WINDING);
RegionRecord^.id := ' Hello from polygon region 1';
implementation
RegionList.Add(RegionRecord);
PointArray[1] := Point(15, 80);
{$R *.DFM}
PointArray[2] := Point(120, 100);
PointArray[3] := Point(20, 120);
procedure TForm1.FormCreate(Sender: TObject);
PointArray[4] := Point(15, 160);
begin
New(RegionRecord);
RedBrush := CreateSolidBrush(clRed);
RegionRecord^.rgn := CreatePolygonRgn(PointArray, 4,
Nregions := 0;
WINDING);
RegionList := Tlist.Create;
RegionRecord^.id := ' Hello from polygon region 2';
end;
RegionList.Add(RegionRecord);
PointArray[1] := Point(200, 5);
procedure TForm1.FormClose(Sender: TObject;
PointArray[2] := Point(300, 10);
var Action: TCloseAction);
PointArray[3] := Point(280, 100);
begin
PointArray[4] := Point(250, 160);
FreeRegions;
PointArray[5] := Point(200, 90);
DeleteObject(RedBrush);
New(RegionRecord);
RegionList.Free;
RegionRecord^.rgn := CreatePolygonRgn(PointArray, 5,
end;
WINDING);
RegionRecord^.id := ' Hello from polygon region 3';
procedure TForm1.FreeRegions;
RegionList.Add(RegionRecord);
var
ShowOrHide(Sender);
i: Integer;
Label1.Caption := 'Polygon regions created.'
begin
end;
for i := 0 to RegionList.Count - 1 do
begin
procedure TForm1.ButtonRectangleClick(Sender: TObject);
RegionRecord := RegionList.Items[i];
var
DeleteObject(RegionRecord^.Rgn);
x1,x2,y1,y2,y0,i0: Integer;
end;

RegionList.Clear;
begin
end;
FreeRegions;

DECEMBER 1995 Delphi INFORMANT ▲ 36


Sights & Sounds

ImageHandle := Image1.Canvas.Handle;
for i0 := 0 to 5 do
begin
y0 := i0*40;
for i := 1 to 50 do
begin
x1 := i*7;
x2 := x1 + 6;
y1 := 1 + y0;
y2 := y1 + 30;
New(RegionRecord);
RegionRecord^.rgn := CreateRectRgn(x1,y1, x2,y2);
RegionRecord^.id := ' Hello from rectangle region ' +
IntToStr(i0*100+i);
RegionList.Add(RegionRecord);
end;
end;
ShowOrHide(Sender);
Label1.Caption := 'Rectangle regions created.'
end;

{ The next procedure will iterate through the regions


checking if a MouseDown event occured inside a region.
If so, the caption of Label1 is changed to show the id
of the region. }
procedure TForm1.Image1MouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X,Y: Integer);
var
i: Integer;
begin
Label1.Caption := '';
for i := 0 to RegionList.Count - 1 do
begin
RegionRecord := RegionList.Items[i];
if ptInRegion(RegionRecord^.Rgn, x, y) then
Label1.Caption := RegionRecord^.id;
end;
end;

{ When the Show button is selected, this procedure prepares


a bitmap and draws outlines on it that correspond to the
regions that have been created. }
procedure TForm1.ShowOrHide(Sender: TObject);
begin
Bitmap := Tbitmap.Create;
Bitmap.Width := Image1.Width;
BitMap.Height := Image1.Height;
Image1.Picture.Graphic := Bitmap;
if RadioButtonShow.Checked then
begin
ImageHandle := Image1.Canvas.Handle;
for i := 0 to RegionList.Count - 1 do
begin
RegionRecord := RegionList.Items[i];
FrameRgn(ImageHandle, RegionRecord^.Rgn,
RedBrush, 1, 1);
end;
Image1.Invalidate;
Label1.Caption := '';
end;
Bitmap.Free;
end;

end.

End Listing Five

DECEMBER 1995 Delphi INFORMANT ▲ 37


DBNavigator
Delphi / Object Pascal

By Cary Jensen, Ph.D.

Finding Your Keys


Locating Records in Paradox Tables

ost database applications provide a technique to select a particular

M record. For example, in a customer database, it’s necessary to locate


a specific record based on a customer's name, account number, street
address, or some other unique piece of information.

There are several techniques you can use to find a particular record in a table using a Table
component. However, most of these only permit you to locate a record based on the fields of a
table’s index. This month's DBNavigator looks at the basics of locating records, including the
use of the TTable methods SetKey, FindKey, and FindNearest, and sequential record searches.

SetKey and GotoKey


The first technique involves the dsSetKey state. A Table component's state identifies what
is happening to it. For instance, if a table is in the dsInsert state, a record has been insert-
ed, but has not been posted. Likewise, if a table is in the dsEdit state, the current record
is being modified.

The table state, dsSetKey, is a special state that permits you to identify the index field values of
the record you want to move to. You can enter this state in one of two ways, using the SetKey
or EditKey method.

The Table method SetKey is used most commonly. When you call this method, the table is
placed into a state where assignments to its individual fields are not applied to it, but are
stored in the search key buffer. This buffer contains values only for index fields.

Once values are assigned to one or more indexed fields within the dsSetKey state, you
can call one of four methods. The method GotoKey is used to move to the record whose
indexed field values match those in the search key buffer. GotoNearest is used to move
to the record that most closely matches the buffer values. The last two methods,
FindKey and FindNearest, do not require the dsSetKey state and will be discussed later in
this article.

The use of SetKey, GotoKey, and GotoNearest is demonstrated in the project named
DEMO1 shown in Figure 1. This project uses the NEWCUST.DB table and is pointed
Figure 1: The DEMO1 project demon-
to by the alias DBDEMOS. (DBDEMOS is created by default by the Delphi installa-
strates using the GotoKey and GotoNearest tion program. NEWCUST.DB is based on the CUSTOMER.DB table that ships with
methods on the NEWCUST.DB table. Delphi.) NEWCUST.DB has been modified to include two additional indexes:

DECEMBER 1995 Delphi INFORMANT ▲ 38


DBNavigator

Company, a secondary, case-sensitive index similar to the Using FindKey


ByCompany index defined by CUSTOMER.DB, and The methods, FindKey and FindNearest, provide essentially the
ByCityState, a two-field, case-insensitive index. same capabilities as GotoKey and GotoNearest, respectively.
However, there are two major differences. First, it’s not necessary
DEMO1 contains an Edit component that allows the user to to place a table into the dsSetKey state before calling FindKey or
enter a search value. The code attached to the button labeled FindNearest. And second, you do not explicitly assign values to
GotoKey places the Table component associated with NEW- the search key buffer — this is done by FindKey and FindNearest.
CUST.DB, Table1, into the dsSetKey state. In this form,
Table1 is using the primary index of NEWCUST.DB, which Both the FindKey and FindNearest methods require a single
is based on the CustNo field. If a value entered into the Edit argument, which consists of a comma-delimited array of val-
component exactly matches one of the values in the CustNo ues to search. The first element of this array is associated with
field, clicking GotoKey moves you to that record. Here’s the the search value for the first field of the index, the second
OnClick event handler for Button1: array element with the second field, and so on.
procedure TForm1.Button1Click(Sender: TObject);
begin
The DEMO2 project, shown in Figure 2, performs the same
Table1.SetKey; searches as DEMO1, except DEMO2 uses FindKey and
Table1.FieldByName('CustNo').AsString := Edit1.Text; FindNearest instead of GotoKey and GotoNearest. For example,
if Table1.GotoKey then
DBGrid1.SetFocus;
the following code is associated with the OnClick event han-
end; dler for the button labeled FindKey:

procedure TForm1.Button1Click(Sender: TObject);


The first statement begin
if Table1.FindKey([Edit1.Text]) then
Table1.SetKey; DBGrid1.SetFocus;
end;
places the table pointed to by Table1 into the dsSetKey state.
Next, the value from the Text property of the Edit compo-
nent, Edit1, is assigned to the CustNo field. This assignment
looks as though data is being written to the table. However, Figure 2: The DEMO2
because of the dsSetKey state, it’s written to the search key project demonstrates
the use of FindKey and
buffer. Next, the GotoKey method of Table1 is called. This is a FindNearest on the
function that returns a Boolean value of True if an exact NEWCUST.DB table.
match is found, and False otherwise.

If the GotoKey method returns True, a call to the SetFocus


method for the DBEdit component is executed. When you
click on the button, it receives focus. In addition, if the
GotoKey method returns True (meaning a match is located), This code segment is associated with the OnClick event handler
the statement for the button labeled FindNearest:
procedure TForm1.Button2Click(Sender: TObject);
DBGrid1.SetFocus;
begin
Table1.FindNearest([Edit1.Text]);
moves the focus onto the DBGrid and places the cursor on DBGrid1.SetFocus;
end;
the located record.

Button2 on DEMO1, labeled GotoNearest, is associated with Case-Insensitive Searches


an event handler similar to that assigned to Button1. The pri- Whether you use GotoKey, GotoNearest, FindKey, or
mary difference is that the GotoNearest method (a procedure) is FindNearest, the table searched for must use an index. In the
used rather than GotoKey (a function). GotoNearest doesn’t preceding examples, the index consisted of a single field,
return a value because it’s always successful in moving to the based on the field CustNo. This is the primary index, and
record that most closely matches the values stored in the search because NEWCUST.DB is a Paradox table, it’s case-sensitive
key buffer. Here’s the OnClick event handler for Button2: and unique. (A unique index cannot have two records with
the same combination of values on the indexed fields.)
procedure TForm1.Button2Click(Sender: TObject);
begin Whether your searches are case-sensitive or not depends on
Table1.SetKey;
Table1.FieldByName('CustNo').AsString := Edit1.Text;
the index. Searching on a case-sensitive index always produces
Table1.GotoNearest; a case-sensitive search. Similarly, if your Table component has
DBGrid1.SetFocus; a case-insensitive index, a case-insensitive search is performed.
end;

DECEMBER 1995 Delphi INFORMANT ▲ 39


DBNavigator

This is demonstrat- The DEMO5 project is shown in Figure 5. The Table com-
ed in DEMO3, ponent's IndexName property has been assigned
shown in Figure 3. ByCityState, a two-field index. Since City is the first field in
This project, which this index, a search is only successful if a value is assigned to
uses the dsSetKey this field of the search key buffer. Searches that include only a
state, permits selec- value for the City field locate the first record that matches
tion of a case-sensi- that city. To search for a particular city-state combination,
tive index for both the City and State fields must be assigned a value.
NEWCUST.DB
(using an index
named Company)
or a case-insensitive Figure 3: Searches are case-sensitive
when the Table component being searched Figure 5: The DEMO5
search (using an uses a case-sensitive index. Searches are project demonstrates
index named case-insensitive when a case-insensitive using GotoKey on a
ByCompany). Each index is used, as demonstrated by the multi-field index.
time you click on DEMO3 project.
the checkbox
labeled Case Sensitive Search, you modify the
IndexName property of the Table component, as shown in
this event handler:

procedure TForm1.CheckBox1Click(Sender: TObject); Here’s the OnClick event handler for the button labeled
begin
if CheckBox1.Checked then GotoKey on the DEMO5 project:
Table1.IndexName := 'Company'
else procedure TForm1.Button1Click(Sender: TObject);
Table1.IndexName := 'ByCompany'; begin
end; Table1.SetKey;
Table1.FieldByName('City').AsString := Edit1.Text;
Table1.FieldByName('State').AsString := Edit2.Text;
The event handlers associated with the two search buttons if Table1.GotoKey then
contain the same code as DEMO1. DBGrid1.SetFocus;
end;

This code assigns values to both the City and State fields of the
Figure 4: The DEMO4
search key buffer. In this instance, if the user does not enter a
project demonstrates
using FindKey and city name, Edit1 contains a blank string, and the search is only
FindNearest in case- successful if there’s a record with a blank city value and the
sensitive and case- matching state value.
insensitive searches.
FindKey and FindNearest can also be used with multi-field
indexes. This is demonstrated by the DEMO6 project. This
code is associated with the FindKey button on DEMO6:

procedure TForm1.Button1Click(Sender: TObject);


The DEMO4 project, shown in Figure 4, demonstrates case- begin
sensitive and case-insensitive searches with FindKey and Table1.FindKey([Edit1.Text,Edit2.Text]);
FindNearest. DBGrid1.SetFocus;
end;

Multi-Field Indexes EditKey Versus SetKey


Searching on multi-field indexes is slightly more involved.
Closely related to the SetKey method is EditKey. When you
Using GotoKey and GotoNearest, values can be assigned to
call SetKey, the search key buffer is cleared of the previous
one or more fields of the index. However, if values are
search contents. In contrast, calling EditKey leaves the search
assigned to less than all of the index fields in the search
key buffer intact. This permits you to modify some search
key buffer, you must follow this rule: A field in a later
values. If you perform a second search, and some of the index
position in the index cannot be assigned a value unless all
field values for this search are identical to those in the first
earlier position index fields are assigned values. For exam-
search, you can call EditKey to enter the dsSetKey state, and
ple, with a four-field index, a value can be assigned to the
then assign search values to the modified search key buffer. If
third field only if the first two fields have also been
you called SetKey prior to a second search, all search key
assigned values. Likewise, if you assign a value to only one
buffer fields would need an assigned value. Therefore, the dif-
field of the index, it must be the first field.

DECEMBER 1995 Delphi INFORMANT ▲ 40


DBNavigator

ference between SetKey and EditKey is only important when


working with multi-field indexes. procedure TForm1.Button1Click(Sender: TObject);
var
GotoRecord: TBookMark;
Sequential Searches begin
It’s not always possible, or practical, to search a table based on if Table1.State in [dsEdit,dsInsert] then
if MessageDlg('Post record?',mtConfirmation,
an index. For example, you may want to provide the user [mbOK,mbCancel],0) <> mrOK then
with the ability to search any field, or combination of fields, Exit
and it’s very unlikely that an index for such a search will else
Table1.Post;
always be available.
GotoRecord := Table1.GetBookMark;
Although there are several techniques used to look for a par- Self.Cursor := crHourGlass;
Table1.DisableControls;
ticular record in a table, the most straightforward is the
sequential search. To use it, move to the first record (if you try
are not required to begin from the current one) and access if not CheckBox2.Checked then
Table1.First;
each record to test for the desired field values. If a match is while not Table1.EOF do
found, the search is successful and can be terminated. begin
if Table1.FieldByName(ComboBox1.Text).AsString =
Edit1.Text then
This technique is demonstrated in the DEMO7 project, begin
shown in Figure 6, where the user selects the value to search GotoRecord := Table1.GetBookMark;
for and the field to search for it in. Break;
end
else
Table1.Next;
end;
Figure 6: finally
The DEMO7 Table1.GotoBookMark(GotoRecord);
project Table1.FreeBookMark(GotoRecord);
demon- Table1.EnableControls;
strates one Self.Cursor := crDefault;
DBGrid1.SetFocus;
technique
end;
for scanning
a table end;
sequentially.

Figure 7: The OnClick event handler for the Search button.

Within the while loop, each record is tested for the search value.
Figure 7 shows the Object Pascal code associated with the If a match is found, the bookmark is assigned to the record and
OnClick event handler for the Search button. This code the while loop is terminated using a Break statement.
begins by declaring a variable of the type TBookMark that
defines which record to move to at the end of a search. The final part of this event handler appears in the finally
The first statement in the body of the procedure is a test clause of the try..finally block. The statements in the finally
of the Table component's state. If the current record has clause are executed even if an exception is raised within the
not been posted, the user is asked to confirm the posting try clause. The last steps are performed in the search: mov-
of the record before continuing. ing to the record pointed to by the bookmark, freeing the
bookmark, enabling the Table's data-aware controls, restor-
Next, a bookmark is assigned to the current record, the ing the form's cursor, and setting focus to the DBGrid.
form's cursor is changed to an hourglass, and the Table com-
ponent's DisableControls method is called. Calling this This sequential-search technique is simple but limited.
method prevents the user from seeing the accessed records First, it’s not advisable to use it with SQL tables. These
while locating the specified value, and speeds the search by tables are set-oriented, not record-oriented. Therefore, it’s
avoiding a repaint of any data-aware controls. better to use SQL statements to locate records in SQL
tables. Second, this technique is affected directly by the
The search begins after disabling the Table component’s con- number of records in the table being searched, and the
trols. If the checkbox labeled Continue Search From Current location of a matching record within that table. For exam-
Record has been checked, a call to the Table's First method ple, searching a 200,000 record table for a value found in
moves to the first record in the table. Next, a while loop is the last record requires just over one minute to complete
entered that repeats until all records have been processed based on a 100MHz Pentium with 24MB of RAM. By compari-
on the EOF (end-of-file) property of the table. son, a search on the same table using an index and

DECEMBER 1995 Delphi INFORMANT ▲ 41


DBNavigator

GotoNearest was practically instantaneous. However, in a


small table of less than 5,000 records or so, the speed of a Table1.FieldByName('CustNo').AsString := Edit1.Text;
Table1.GotoNearest;
sequential search is acceptable. DBGrid1.SetFocus;
end;
Conclusion end.

Whether your tables are indexed or not, you have several options End Listing Six
for locating specific records. Whenever possible, you should try Begin Listing Seven — Demo2 Project
to use indexes. In fact, one of the main reasons for creating program Demo2;
indexes is to provide fast searches based on commonly searched uses
Forms, Demo2u in 'DEMO2U.PAS' {Form1};
fields. However, if indexes are not practical, a sequential search {$R *.RES}
can always be used, even though it is far less efficient. ∆ begin
Application.CreateForm(TForm1, Form1);
Application.Run;
The demonstration projects referenced in this article are available end.
on the 1995 Delphi Informant Works CD located in
INFORM\95\DEC\DI9512CJ. unit Demo2u;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes,
Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database Graphics, Controls, Forms, Dialogs, StdCtrls, Grids,
development company. He is a developer, trainer, and author of numerous books on DBGrids, DB, DBTables;
database software. You can reach Jensen Data Systems at (713) 359-3311, or type
through CompuServe at 76307,1533. TForm1 = class(TForm)
DataSource1: TDataSource;
Table1: TTable;
DBGrid1: TDBGrid;
Begin Listing Six — Demo1 Project Button1: TButton;
program Demo1; Label1: TLabel;
uses Edit1: TEdit;
Forms, Demo1u in 'DEMO1U.PAS' {Form1}; Button2: TButton;
{$R *.RES} procedure Button1Click(Sender: TObject);
begin procedure FormCreate(Sender: TObject);
Application.CreateForm(TForm1, Form1); procedure Button2Click(Sender: TObject);
Application.Run; end;
end. var
Form1: TForm1;
unit Demo1u; implementation
interface {$R *.DFM}
uses procedure TForm1.Button1Click(Sender: TObject);
SysUtils, WinTypes, WinProcs, Messages, Classes, begin
Graphics, Controls, Forms, Dialogs, StdCtrls, Grids, if Table1.FindKey([Edit1.Text]) then DBGrid1.SetFocus;
DBGrids, DB, DBTables; end;
type procedure TForm1.FormCreate(Sender: TObject);
TForm1 = class(TForm) begin
DataSource1: TDataSource; Table1.Open;
Table1: TTable; end;
DBGrid1: TDBGrid; procedure TForm1.Button2Click(Sender: TObject);
Button1: TButton; begin
Label1: TLabel; Table1.FindNearest([Edit1.Text]);
Edit1: TEdit; DBGrid1.SetFocus;
Button2: TButton; end;
procedure Button1Click(Sender: TObject); end.
procedure FormCreate(Sender: TObject); End Listing Seven
procedure Button2Click(Sender: TObject);
end; Begin Listing Eight — Demo3 Project
var program Demo3;
Form1: TForm1; uses
implementation Forms, Demo3u in 'DEMO3U.PAS' {Form1};
{$R *.DFM} {$R *.RES}
procedure TForm1.Button1Click(Sender: TObject); begin
begin Application.CreateForm(TForm1, Form1);
Table1.SetKey; Application.Run;
Table1.FieldByName('CustNo').AsString := Edit1.Text; end.
if Table1.GotoKey then DBGrid1.SetFocus;
end; unit Demo3u;
procedure TForm1.FormCreate(Sender: TObject); interface
begin uses
Table1.Open; SysUtils, WinTypes, WinProcs, Messages, Classes,
end; Graphics, Controls, Forms, Dialogs, StdCtrls, Grids,
procedure TForm1.Button2Click(Sender: TObject); DBGrids, DB, DBTables;
begin type
Table1.SetKey; TForm1 = class(TForm)

DECEMBER 1995 Delphi INFORMANT ▲ 42


DBNavigator

DataSource1: TDataSource;
Table1: TTable;
DBGrid1: TDBGrid;
Button1, Button2: TButton;
Label1: TLabel;
Edit1: TEdit;
Table1CustNo: TFloatField;
Table1Company: TStringField;
Table1Addr1: TStringField;
Table1Addr2: TStringField;
Table1City: TStringField;
Table1State: TStringField;
Table1Zip: TStringField;
Table1Country: TStringField;
Table1Phone: TStringField;
Table1FAX: TStringField;
Table1TaxRate: TFloatField;
Table1Contact: TStringField;
Table1LastInvoiceDate: TDateTimeField;
CheckBox1: TCheckBox;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Table1.SetKey;
Table1.FieldByName('Company').AsString := Edit1.Text;
if Table1.GotoKey then DBGrid1.SetFocus;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Table1.Open;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Table1.SetKey;
Table1.FieldByName('Company').AsString := Edit1.Text;
Table1.GotoNearest;
DBGrid1.SetFocus;
end;
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
if CheckBox1.Checked then Table1.IndexName := 'Company'
else Table1.IndexName := 'ByCompany';
end;
end.

End Listing Eight

DECEMBER 1995 Delphi INFORMANT ▲ 43


At Your Fingertips
b y d a v i d r i p p y
Delphi / Object Pascal

W e must either find a way or make one.


— Hannibal

How can I conditionally change the font color of a row


in a DBGrid?
This is one of the most
frequently asked ques-
tions I receive. By chang-
ing the color of selected
rows in a DBGrid,
entries can be found
quickly without having
to study the values in the
columns. An example of
this feature is shown in Figure 1: Values in the OwesMe col-
Figure 1. This form con- umn greater than US$25 are displayed
in red.
tains a DBGrid display-
ing the names of several friends and the amount of money they
owe me. If the amount owed is greater than US$25, the font
color for that row becomes red. Conversely, if the amount owed Figure 3: Pressing the Run Calculator button executes the
Windows Calculator.
is less than or equal to US$25, the font color remains black.
Figure 2: This This is an easy feature to implement with Delphi. Simply call
code is attached the Windows API function WinExec. The syntax for using
to the DBGrid’s WinExec is:
OnDraw-
DataCell event
handler. WinExec( CmdLine: PChar; CmdShow: Word ) : Word;

WinExec accepts two parameters: CmdLine, a PChar contain-


The code that adds this functionality to the DBGrid is ing the name of the program to execute; and CmdShow, a
attached to the grid’s OnDrawDataCell event (see Figure 2). Word specifying how the program should run (minimized,
First, the OwesMe column from Table1 is examined. If its maximized, etc.). For a listing of the values for CmdShow,
value is greater than 25, the font color for the row is search for “ShowWindow” in Delphi’s Windows API on-line
changed to the color constant clRed. The cell is then drawn help. WinExec returns a Word that indicates the result of the
on the screen with the DefaultDrawDataCell command. function call. A return value of less than 32 indicates the
Not bad for two Object Pascal statements! — D.R. function call was unsuccessful.

How can I run another Windows application within If CmdLine does not include a path, WinExec will search
my program? for the executable in the following order: the current direc-
Users will often ask for the ability to execute other programs tory, the Windows directory, the Windows system directo-
within your Delphi application. For example, they may want ry, the directory containing the executable of the current
to access the Windows Calculator without returning to application, all directories in the path, and the directories
Windows itself (see Figure 3). mapped on a network.

DECEMBER 1995 Delphi INFORMANT ▲ 44


At Your Fingertips

Figure 4 shows the


OnClick event handler
for the BitButton
labeled Run
Calculator.
CALC.EXE was speci- Figure 4: This code is attached to the
BitButton’s OnClick event handler.
fied as the CmdLine
parameter, and SW_SHOWNORMAL as the CmdShow para-
meter. Because CALC.EXE is contained in the Windows directo-
ry, no path was specified. SW_SHOWNORMAL will display
the calculator in its normal state (i.e. not minimized, hidden,
etc.). — Russ Acker, Ensemble Corporation

How can I play a .WAV file in my application?


You might be tempted to use the MediaPlayer component to
play a .WAV file, but it’s like driving a nail with a sledgeham-
mer. With the ability to play MIDI, .AVI, .DAT, and other
files, MediaPlayer is probably my favorite component in
Delphi. However, this functionality adds a lot of unnecessary
overhead to your application if you simply want to play a
.WAV file. A more efficient way to play a .WAV is to call the
Windows API function sndPlaySound.

sndPlaySound is part of the Windows Multimedia extensions


and is contained in the MMSYSTEM.DLL. Delphi has already
encapsulated this function for us in the MMSYSTEM unit. To
gain access to this function, simply append MMSystem to the
end of your unit’s uses statement, shown in Figure 5.

Figure 5: Append
MMSystem to the
unit’s uses clause
to access the
sndPlay-Sound
Figure 6 (Top): This code is attached to the button’s OnClick event
function.
handler. Figure 7 (Bottom): To tile windows vertically down the
screen in the Database Desktop, hold down S while selecting
Window | Tile.
Figure 6 shows a form with a button labeled Play Chimes.
When the button is pressed, the familiar Windows chimes Quick Tip
.WAV file is played. Examine the code attached to the button’s In the Database Desktop, you’ve probably used the Window |
OnClick event. sndPlaySound accepts two parameters: a PChar Tile command to arrange your desktop windows horizontally
for the name of the .WAV file, and a Word for the mode to across the screen. Did you know that holding V down
play the .WAV. In this example, CHIMES.WAV is specified as while selecting Window | Tile will arrange your windows ver-
the .WAV file, and the constant snd_Async is used for the play tically down the screen? Check out Figure 7. ∆ — Russ Acker,
mode. The most commonly used values for the play mode Ensemble Corporation
parameter are snd_Sync and snd_Async. Additional parameters
can be found in the MMSYSTEM unit located in the \DEL-
PHI\SOURCE\RTL\WIN directory. Specifying snd_Sync will The demonstration projects referenced in this article are available
suspend your application until the .WAV has completed play- on the 1995 Delphi Informant Works CD located in
ing. Specifying snd_Async will play the sound in the back- INFORM\95\DEC\DI9512DR.
ground while your application continues running.

Note that the constants mentioned in MMSYSTEM.PAS for the


sndPlaySound function are flags and can be combined to produce David Rippy is a Senior Consultant with Ensemble Corporation, special-
effects different from just these two. Note also that the MMSYS- izing in the design and deployment of client/server database applica-
tions. He has contributed to several books published by Que, and is a
TEM.HLP help is available in the \DELPHI\BIN directory for contributing writer in the Paradox Informant. David can be reached on
reference. However, it isn’t “hooked up” with the rest of the CompuServe at 74444,415.
Delphi help system so you’ll have to call it directly. — D.R.

DECEMBER 1995 Delphi INFORMANT ▲ 45


Visual Programming
Delphi / Object Pascal

By Blake Versiga

Breed and Rank


Using the Outline Component to Represent a Family Tree

any developers consider Delphi’s Outline control the de-facto stan-

M dard for representing hierarchical relationships. Outline controls are


ideal for visually displaying document components, family trees,
organizational relationships, object models, etc. Delphi itself uses the Outline
control to implement its ObjectBrowser. [For an in-depth discussion of this
tool, see Douglas Horn’s article “The ObjectBrowser” in the November 1995
Delphi Informant.]

This article will reveal some of the mysteries behind Delphi’s Outline control, and discuss
various ways to work with it. For instance, it can be completely configured at design-time or
run-time (or variations of the two) by querying a database. In addition, owner-draw capabili-
ties will be explored. The example program provides an efficient user interface for displaying
the family tree of Great Pyraneese dogs.

Distinct Markings
The Outline control (TOutline class) is on the Additional page of the Component Palette.
This control is an array of OutlineNodes (TOutlineNode class) with indexes from 1 to the
number of items in the list. For this reason, it’s important to distinguish between the Outline
control and each item (or OutlineNode).

Both Outline and OutlineNode have individual sets of properties, methods, and events. The
attributes of Outline affect it as a whole. On the other hand, attributes of the OutlineNode affect
only the specific nodes of the Outline. Each item in the Outline can have 0 to 16368 sub-items.

Its large number of properties and methods make Outline a flexible component. For example,
the PictureClosed, PictureLeaf, PictureMinus, PictureOpen, and PicturePlus properties contain
standard File Manager-like graphics that can be customized for a particular look. If five graph-
ics will not meet the challenge, however, the Outline component comes to the rescue with its
owner-draw capabilities.

Opening Eyes
Let’s begin with a small example program to demonstrate some important Outline compo-
nent concepts. First, create a new project. Place an Outline component on the form and then
drag its handles to enlarge it.

DECEMBER 1995 Delphi INFORMANT ▲ 46


Visual Programming

Next, double-click on the Outline’s Lines property in the Therefore, changing this property does not affect the con-
Object Inspector to display the String list editor. Each line trols when the Style property is set to otStandard. Style can
represents a node in the Outline. Press CF or s be set to otOwnerDraw to activate the ItemHeight property.
to add one level of indention to the node. If spaces are used, If there is no code in the OnDrawItem event, Delphi uses
the String list editor will convert them to tabs before saving. the default drawing method. This allows the standard pic-
tures to be enlarged without the additional complexity of a
The sample application initializes each node to include the full owner-draw control.
name of the dog followed by a character denoting its gender
(see Figure 1). This is used to determine which bitmap should Barking at the Moon
be associated with the node when the Style property is set to Now that we have a functioning application, it’s essential to
otOwnerDraw. After adding entries in the String list editor, capture events and respond to the user’s interaction with the
run the application by pressing 9. The Outline will expand control. Typically, the developer must perform some tasks
and collapse without any additional code (see Figure 2). when the user clicks on a node of the Outline. The following
example modifies the form’s caption with the Outline index
and the full text path of the selected item. The index is a one-
based long integer indicating the ordinal position of the item
with respect to the entire list.

The full text path represents the text and separators of the
nodes the user must navigate to arrive at the selected node.
The ItemSeparator property determines the character string
used to delimit items in the FullPath property. To illustrate
these properties, simply add this line of code to the Click
event of the Outline control:

Form1.Caption :='Outline Index ' +


IntToStr(Outline1.SelectedItem) + '. ' +
Outline1.Items[Outline1.SelectedItem].FullPath;

Howlin’ with the Hounds


If adventure runs in your blood, the owner-draw style is for
Figure 1 (Top): The String you. The owner-draw Outline style offers the most control
list editor is used to define
the genealogy of Great
and is the ultimate in flexibility. (Again, Delphi’s
Pyraneese dogs. ObjectBrowser is an excellent example.) The concept behind
Figure 2 (Left): The sample owner-draw controls is that the programmer controls all
project in action. Clicking on aspects of a control’s appearance, including tree lines, pic-
the yellow folders causes this tures, text, and indention. Although this sounds daunting,
“object browser” to expand
the genealogy of the dogs.
it’s quite simple if you avoid a few pitfalls.
The name and gender of
each dog in this canine To receive events essential for owner-draw operations, set the
family are shown. Style property to otOwnerDraw. This instructs the Outline con-
trol to call the OnDrawItem event whenever it needs to draw a
node of the Outline. Note: In early versions of the VCL, the
Outline component contained a bug that suppressed the
OnDrawItem message if the ScrollBars property was set to
ssHorizontal or ssBoth. To avoid this, set the ScrollBars property
The OutlineStyle and Picture properties can be used to to ssVertical or ssNone. Any version of Delphi subsequent to ver-
modify the appearance of the Outline control and allow sion 1.0 (i.e. from the first patch onward) fixed this problem.
for customization without code. It’s also important to
remember that the bitmap’s size is fixed. The default First, add StdCtrls to the form’s uses clause. This module
bitmaps are 14 pixels high and 14 pixels wide, and reside adds the needed definitions for the TOwnerDrawState. Next,
in the \DELPHI\IMAGES\DEFAULT directory (if images add any variables required of type TBitmap to the implemen-
are installed). tation section of the unit. This will declare bitmaps that are
accessible throughout the module.
The ItemHeight property works in conjunction with owner-
draw controls, for example: Next, code must be added to create and free the bitmaps.
Select the Events page in the Object Inspector and double-
Style := otOwnerDraw

DECEMBER 1995 Delphi INFORMANT ▲ 47


Visual Programming

click in the Values column of the OnCreate event. Now Information about the
enter a statement to allocate storage space on the stack for node is used to determine
each bitmap, for example: exactly how it will be
painted. In our example,
BitmapVariable := TBitmap.Create;
the current node is drawn
highlighted and a different
Next, initialize each bitmap. If the bitmaps reside on disk, the
bitmap is used depending
LoadFromFile method can be used:
on the dog’s gender. A
BitmapVariable.LoadFromFile('Bitmap.bmp'); bitmap with an underlined
M in the lower left corner
The bitmap is now allocated on the stack, so the memory represents male dogs, and
must be freed when it’s no longer needed. To accomplish females are denoted with
this, use this statement in the OnClose event: an underlined F (see
Figure 3). The example
BitmapVariable.Free; project at runtime is
shown in Figure 4, and the
Finally, the Outline node must be drawn when indicated project source is shown in
by the Outline control. As mentioned earlier, the Outline Listing 9 on page 49.
control calls the OnDrawItem procedure whenever a cell
requires repainting. This routine should clear the canvas Conclusion
and paint any pictures, graphics, or text. The OnDraw Now that we have mas-
procedure is also responsible for showing hierarchical tered the fundamentals of
indention, if necessary. Here’s the prototype for the owner-draw outlines, we Figure 3 (Top): The male and
female Pyraneese bitmaps. Not just
DrawItem procedure: can apply these techniques pretty faces, these bitmaps demon-
to other owner-draw con- strate how the DrawItem procedure
procedure TForm1.Outline1DrawItem(Control: TWinControl; trols that are handled in a and GetItem method are used to
Index: Integer; Rect: TRect; State: TOwnerDrawState); properly paint the appropriate node.
similar manner. Additional Figure 4 (Bottom): The sample
owner-draw controls project at run-time.
The Control parameter contains a reference to the Outline
include TComboBox,
control. Index is the position of the item in the Outline con-
TDBComboBox, TDBListBox, TListBox, and TOutline.
trol. Note that this does not correspond to the node’s index,
but represents the ordinal index of visible items within the
Owner-draw controls offer tremendous flexibility to the pro-
Outline. Rect is the area available to the node. The State is
grammer. These techniques can be used to display anything
defined as:
from hierarchical data relationships to sections of text (e.g.
TOwnerDrawState = set of (odSelected,odGrayed,odDisabled, the Microsoft Word for Windows Outline feature). The
odChecked,odFocused); Outline control is a master of distilling tremendous amounts
of information into manageable chunks of data. The key is
The State parameter will contain information required to creativity. Now it’s your turn to bark up a storm. ∆
determine how a node will be drawn. Within the
DrawItem procedure, the code should paint the node. A The Delphi project referenced in this article is available on the
typical scenario involves clearing the area that will be 1995 Delphi Informant Works CD located in
painted, drawing the graphic, and finally, painting the text. INFORM\95\DEC\DI9512BV.
As mentioned earlier, the index passed to the OnDraw
event does not represent the node Index that will be
drawn. To obtain the actual Index, the GetItem method is Blake Versiga is a Systems Engineer at Computer Language Research. He has been
used. It translates any X,Y point to a node Index. Given involved in producing corporate solutions ranging from Pen-based computing to large
the node Index, all the pieces have come together. client/server applications. He can be reached on CompuServe at 73123,2737 or via
Internet mail at 73123.2737@compuserve.com.

DECEMBER 1995 Delphi INFORMANT ▲ 48


Visual Programming

s: string;
Begin Listing Nine — Outline Demo Program x,y,
program Project1; Offset: Integer;
ItemIndex: LongInt;
uses Node: TOutlineNode;
Forms,
Unit1 in 'UNIT1.PAS' {Form1}; begin
ItemIndex := Outline1.GetItem(Rect.Left, Rect.Top);
{$R *.RES} Node := Outline1.Items[ItemIndex];

begin with Outline1.Canvas do


Application.CreateForm(TForm1, Form1); begin
Application.Run; if State = [odSelected,odFocused] then
end. begin
Brush.Color := clHighlight;
Pen.Color := clHighlightText;
unit Unit1; end
else
interface begin
Brush.Color := Outline1.Color;
uses Pen.Color := clWindowText;
SysUtils, WinTypes, WinProcs, Messages, Classes, end;
Graphics, Controls, Forms, Dialogs, Grids, Outline,
StdCtrls, ExtCtrls, Buttons; FillRect(Rect);
Rect.Left := Rect.Left + (Node.Level * 15);
type
TForm1 = class(TForm) { First draw the icon }
Outline1: TOutline; if Copy(Node.Text,length(Node.Text),1) = 'F' then
Image1: TImage; BrushCopy(Bounds(Rect.Left,Rect.Top,35,25),
Label1: TLabel; bmpFemalePyr,Bounds(0,0,35,25),clLtGray)
Image2: TImage; else
BitBtn1: TBitBtn; BrushCopy(Bounds(Rect.Left,Rect.Top,35,25),
procedure Outline1Click(Sender: TObject); bmpMalePyr,Bounds(0,0,35,25),clLtGray);
procedure Outline1DrawItem(Control: TWinControl;
Index: Integer; Font := Outline1.Font; { Set the font }
Rect: TRect; { Then draw the string }
State: TOwnerDrawState); s := copy(Node.Text, 1, Length(Node.Text) - 2);
procedure FormCreate(Sender: TObject); { Indent and center Text }
procedure FormClose(Sender: TObject; x := Rect.left + bmpMalePyr.Width + 7;
var Action: TCloseAction); y := Rect.Top + (((Rect.Bottom-Rect.Top) —
procedure BitBtn1Click(Sender: TObject); abs(Font.Height)) div 2);
private
{ Private declarations } TextOut(x, y, s);
public end;
{ Public declarations } end;
end;
procedure TForm1.FormCreate(Sender: TObject);
var begin
Form1: TForm1; { Load the bitmap from the current directory }
bmpMalePyr := TBitMap.Create;
implementation bmpMalePyr.LoadFromFile('MalePyr.Bmp');
bmpFemalePyr := TBitMap.Create;
var bmpFemalePyr.LoadFromFile('FemPyr.Bmp');
bmpMalePyr, end;
bmpFemalePyr: TBitMap;
procedure TForm1.FormClose(Sender: TObject;
{$R *.DFM} var Action: TCloseAction);
begin
procedure TForm1.Outline1Click(Sender: TObject); bmpMalePyr.Free;
begin bmpFemalePyr.Free;
Form1.Caption := 'Outline Index ' + end;
IntToStr(Outline1.SelectedItem) +
'. ' + procedure TForm1.BitBtn1Click(Sender: TObject);
Outline1.Items[Outline1.SelectedItem].FullPath begin
end; Form1.Close;
end;
procedure TForm1.Outline1DrawItem(Control: TWinControl;
Index: Integer; end.
Rect: TRect;
State: TOwnerDrawState); End Listing Nine
var

DECEMBER 1995 Delphi INFORMANT ▲ 49


New & Used
BY Bill Todd

ReportPrinter
Nevrona Designs’ New Report Writer

very now and then a whole new category of

E product comes along that makes you say, “Wow.


Why hasn’t somebody done this before?”
ReportPrinter from Nevrona Designs is just such a
product. What can be so different about a report
writer? The answer is control, integration, and size.

Reports are created in ReportPrinter by writing Object Pascal


code, not by using a visual designer such as those in
ReportSmith and Crystal Reports. At first this may sound like a
big step backward, but it’s really a step to the side — and a step
into control over report design that you’ve never had before.

To understand the control and flexibility of ReportPrinter,


look at the two-page report illustrated in Figures 1 and 2.
This is one of the reports produced by the demonstration
program that comes with ReportPrinter. The two pages in
this report are completely different. Figure 1 contains multi-
ple tables in varying formats and locations, while Figure 2
contains a collection of graphic shapes intermixed with text.

You simply cannot create a report like this in a traditional


Windows report writer.

Let’s Compare
Is ReportPrinter a replacement for traditional report writers
like Crystal Reports and ReportSmith? Definitely not. Each
has its place defined by its strengths and weaknesses. With
ReportPrinter you get:
• Integration. ReportPrinter is a set of native Delphi
components to add to your application. Everything is
compiled into the .EXE file. There are no separate
.DLLs or report files to distribute. And since the report Figure 1 (Top): A ReportPrinter report containing multiple tables
is created entirely by compiled Pascal code, you get the in various formats. Figure 2 (Bottom): The second page of the
output fast. report shown in Figure 1 containing mixed graphics and text.

DECEMBER 1995 Delphi INFORMANT ▲ 50


New & Used

Crystal Reports Delphi Control

Now available, Crystal Reports version 4.5 is it allows you to set the title for the report methods. The “readme” file is no help
a welcome addition for Delphi developers. It preview window. either. It is simply a list of changes and bug
ships with a native Delphi component that fixes to prior versions of the component.
provides an easy interface to the Crystal print Once you have set the required properties, You may expect this in the readme for a
engine through a host of properties and run the report by setting the Action property beta version of a shareware product, but
methods. to 1. While these are the properties you will not in the shipping version of a commercial
use most, the Crystal component includes product. The best advice I can offer is to
Basic report printing and viewing is very over three pages of properties you can set look at the documentation for the VBX in
easy. After adding the Crystal component via the Property Inspector, and even more the Crystal Reports Developer’s Reference
to the Delphi Component Palette you will that you can set at run-time in your code. and use that as a guide for getting started
find it located on the Data Access page. This provides access to just about all the with the Delphi component.
Just drop a TCcrpe component on your flexibility for which the Crystal print engine
form and set the following properties to is renowned. The version of the Delphi component that
print your report: ships with Crystal Reports 4.5 is 1.06. By
Unfortunately this component is seriously the time you read this review, version 1.08
CopiesToPrinter. This property defaults to 1 flawed by bad design and poor documen- should be available on the Crystal bulletin
so you will only need to change it if you need tation. The component is supposed to pro- board and on their CompuServe forum. The
more than one copy of your report. vide a native Delphi alternative to the pre-release copy of version 1.08 that I saw
Crystal VBX that shipped with earlier ver- includes a much more complete help file
Destination. This property determines where sions of Crystal Reports (which you may and a demonstration program that shows
your report will go. You can select from any have been using in your Delphi applica- you how to use many of the component’s
of the following constants: tions). However, the designers changed the features. This new version is worth the
• toWindow names of some of the properties and many download time just for the help file and the
• toPrinter of the constants used to set properties. For demonstration program.
• toFile example, the VBX property that stores the
• toEmailViaMAPI name of the report to run is The Crystal Reports component is a welcome
• toEmailViaVIM ReportFileName, while in the Delphi com- tool that makes it easy to use this powerful
ponent it is ReportName. Another example report writer in your Delphi programs, while
ReportName. The name of the file that con- is that the PrintReport method from the VBX giving you access to all the features of the
tains the Crystal report to print. is not supported in the Delphi component. Crystal print engine. Although it will take
This means that converting your existing some time, it is worth your while to convert
WindowState. You need only be concerned programs from the VBX to the Delphi com- your programs from the VBX to the Delphi
with this property if the Destination property is ponent is more work than it should be. component. You will no longer need to dis-
set to toWindow. To control the size of the tribute the VBX file, and you’ll be ready to
report preview window, WindowState can be set Learning to use the component is a chal- compile your program with Delphi32 where
to wsMaximized, wsMinimized, or wsDefault. lenge too. The only documentation is a VBXes are not supported.
Windows help file and it is woefully incom-
WindowTitle. This property is optional, but plete. While it does include the component
if you are sending the report to a window, properties, it does not include any of the — Bill Todd

• Small size. Adding a report to an application increases the With a traditional report writer you get:
.EXE file size by about 30KB for the first report, and less for • Fast, easy, WYSIWYG development. You can see what the
subsequent reports. Contrast that with the megabytes of finished report will look like as it’s created. This saves time.
additional files distributed with a conventional report writer. • Instant calculations. Traditional report writers provide
• Control. You can put anything, anywhere, on any page, in fast, easy sorting, grouping, and subtotaling. Most offer
any order to get the report you want. Since the position is other functions such as counts and averages. You must
specified in your code in inches or centimeters, each write code to perform these calculations in
object on the report prints exactly where you want it. ReportPrinter.
• Direct access to data. Since a report is part of an appli- • A large run-time environment. This is the drawback to
cation, you access data using Delphi’s Table, Query, and traditional report writers. To create reports with a tradi-
StoredProc components. There is no separate database tional report writer, you must distribute its run-time
connection for your reports, which is especially conve- environment with your application. This means 2 to
nient for printing them based on the current record. 4MB of DLLs and supporting files, plus at least one
• Slower development time. This is the drawback to file for each report.
using ReportPrinter. Because you create each report by
writing code, it takes longer to design most reports Consider the Options
using ReportPrinter than with a traditional report When you need to include reports with custom applications,
writer. This is particularly true if the report requires consider which tool is best suited for the job, as well as the
numerous groups and subtotals. time you are willing to invest creating the reports.

DECEMBER 1995 Delphi INFORMANT ▲ 51


New & Used

Do not feel limited because you have to create ReportPrinter


reports by writing Object Pascal code. ReportPrinter supports
tabular reports, snaked columns, memos, graphics, line and
shape drawing, and fonts. To create business graphs in your
reports, pass the ReportPrinter canvas handle to the ChartFX
component that ships with Delphi.

ReportPrinter consists of four Delphi components that afford


the functionality you require.
• The ReportPrinter component lets you send a report to
the current printer.
• ReportFiler is virtually identical to the ReportPrinter
component except it sends a report to either a disk file or
memory stream.
• ReportFiler lets you create the report; then send it to the
printer using the FilePrinter component, or to the screen
using the FilePreview component. Figure 3: A sample ReportPrinter report based on a table using the
• FilePreview supports zooming, panning, and printing from OnPrintPage event.
the preview window. You can either create your own pre-
view form, or use the one included in ReportPrinter’s On the other hand, the demonstration
demonstration program. program is excellent. It includes five
reports and an excellent general purpose
The ReportPrinter and ReportFiler components make creat- preview form. To use this powerful tool ReportPrinter version 1.1 by
ing a report easy with a rich suite of events. You can attach effectively, be prepared to spend time Nevrona Designs is a report writer
for Delphi. Consisting of four Delphi
the reporting code to: working through the code in the components, it enables developers
• OnBeforePrint demonstration program. ReportPrinter to use Object Pascal to create cus-
tom reports. Delphi applications that
• OnAfterPrint also ships with full source code. integrate a report created with
• OnNewColumn ReportPrinter are more size-efficient
than applications distributed with
• OnNewPage Conclusion conventional report writers.
• OnPrint ReportPrinter is a must-have tool ReportPrinter also gives developers
more control over reports, and direct
• OnPrintPage for the serious Delphi developer. It access to data using the Table,
• OnPrintHeader lets you do things you cannot do Query, and StoredProc components.
• OnPrintFooter with any other report writer. Nevrona Designs
Whether you need reports compiled 2581 East Commonwealth Circle
Chandler, AZ 85225
The OnPrintPage event is particularly well suited to reports into a program for easy distribu- Voice: (602) 899-0794
where each page has the same layout, while OnPrint is the event tion, flexible page layout that varies Fax: (602) 530-4823
E-Mail: CIS: 70711,2020 or
to use for reports where the layout varies from page to page. from page to page, or precise posi- Internet: info@nevrona.com
Figure 3 shows one of the sample reports based on a table. tioning for pre-printed forms, Price: US$99
ReportPrinter is the tool that han-
The Documentation dles almost any reporting task. ∆
If the current version of ReportPrinter has a shortcoming, it’s
the documentation. The manual is only 56 pages, which is
mostly a reference for the component methods and proper- Bill Todd is President of The Database Group, Inc., a consulting firm based near
ties. Only six pages are devoted to the tutorial, and that is not Phoenix. Bill is co-author of Delphi: A Developer’s Guide (M&T Books, 1995). He is
also a member of Team Borland and a speaker at all Borland database conferences.
enough introduction to ReportPrinter’s features. Bill can be reached at (602) 802-0178, or on CompuServe at 71333,2146.

DECEMBER 1995 Delphi INFORMANT ▲ 52

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