0% found this document useful (0 votes)
49 views36 pages

Delphi Informant 95 2001

This document provides summaries of new products and solutions for Delphi developers: 1. softSENTRY 2.1 is an enhanced version of a trialware and software protection tool that works by injecting directly into executable files or calling a dynamic link library. It provides additional security and more complex password definitions. 2. eAuthor Help 3.10 is an updated version of a template-based RAD authoring tool for large-scale web sites and HTML help projects, introducing new features like WYSIWYG editing and XML document creation. 3. BS/1 Small Business is an ActiveX control that provides a cost-effective accounting system that software developers can include in their own custom applications for one

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)
49 views36 pages

Delphi Informant 95 2001

This document provides summaries of new products and solutions for Delphi developers: 1. softSENTRY 2.1 is an enhanced version of a trialware and software protection tool that works by injecting directly into executable files or calling a dynamic link library. It provides additional security and more complex password definitions. 2. eAuthor Help 3.10 is an updated version of a template-based RAD authoring tool for large-scale web sites and HTML help projects, introducing new features like WYSIWYG editing and XML document creation. 3. BS/1 Small Business is an ActiveX control that provides a cost-effective accounting system that software developers can include in their own custom applications for one

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

May 1999, Volume 5, Number 5

Cover Art By: Darryl Dennis

ON THE COVER 26 On the ’Net


5 The Template Method Pattern — Xavier Pacheco An HTML Generator: Part I — Keith Wood
The May issue begins with an in-depth discussion of application frameworks, If you need to create HTML from Delphi, you’ll be very interested in Mr
their relationship to patterns, and a pattern suited for their creation, the Wood’s set of components that employ the interface object (available in
Template Method pattern. In brief, Mr Pacheco provides us with a continuing Delphi version 3 and higher) for a simpler, more object-oriented approach.
education of design patterns — with supporting examples you can put to
use in your applications. 31 DBNavigator
Delphi Database Development: Part VIII — Cary Jensen, Ph.D.
After a comprehensive discussion of data validation, Dr Jensen provides an
FEATURES in-depth demonstration of the four types of client-side data validation:
13 Undocumented keystroke-level, field-level, record-level, and database-level.
From the Shell: Part II — Kevin J. Bluck and James Holderness
Messrs Bluck and Holderness return with more Win32 dialog boxes (e.g.
Find, Properties, etc.) provided as Delphi components. They also provide
an explicit description of the inner workings of the Windows shell.

19 Algorithms DEPARTMENTS
Map Coloring — Rod Stephens
Mr Stephens presents two algorithms — four- and five-color — for col- 2 Delphi Tools
oring maps, or any surface with discrete areas that must be visually 4 Newsline
unique. Demonstration programs are available for download, of course. 36 File | New by Alan C. Moore, Ph.D.

1 May 1999 Delphi Informant


20/20 Software Releases softSENTRY 2.1
Delphi 20/20 Software, Inc. released password definitions.
T O O L S softSENTRY 2.1, an enhanced User input strings,
version of its trialware and such as “name” and
New Products software protection tool that “serial number,” can be
and Solutions works by injecting directly used in password for-
into executable (.exe) files, or mulas to ensure that
by calling a dynamic-link the correct registration
library (.dll) file. information must be
Protected programs can query entered for the pass-
internal softSENTRY data, such word to work. Version
as System ID, values of user 2.1 also adds 99 new
input strings, values of limitation operators to further
counters, and the active mode in increase the difficulty of breaking 20/20 Software, Inc.
which the program is running. a password algorithm. Price: US$249 for either 16- or 32-bit Lite
Protected programs can also dis- Additional security measures versions; US$695 for complete version
play a registration form at any have been incorporated to protect (includes 16- and 32-bit functionality).
time. Enhanced formulas increase against attacks on the “footprint” Phone: (800) 735-2020
the complexity and flexibility of softSENTRY puts on a computer. Web Site: http://www.twenty.com
HyperAct Announces eAuthor Help 3.10
HyperAct, Inc. announced sites and HTML Help projects. which allows eAuthor to be
version 3.10 of eAuthor Help, This release introduces new extended with plug-ins, COM-
its template-based RAD author- features, such as WYSIWYG based templates, and more.
ing tool. eAuthor was designed editing, hard-copy documenta- With the new ActiveScript
to provide a rich authoring tion creation, XML document support, repetitive documenta-
environment for large-scale Web creation, and the eAuthor SDK, tion creation tasks can be
scripted using JavaScript or
external application via
automation.
In addition, enhancements
were added to features available
before, such as an improved
raw-HTML editor, a faster
editing environment, improved
spell checker, thesaurus, drag-
and-drop capabilities, improved
documentation, and more.

HyperAct, Inc.
Price: US$250
Phone: (402) 891-8827
Web Site: http://www.hyperact.com
Davis Business Systems and Psi Computer Consultants Offer BS/1 Small Business
Davis Business Systems Ltd. programs designed for use by to include a fully functioning
and Psi Computer small- to medium-sized busi- accounting system in their
Consultants Pty Ltd. have nesses. BS/1 is an ActiveX con- own custom application.
developed BS/1 Small trol that provides software BS/1 Small Business ActiveX
Business, a suite of accounting developers a cost-effective way can be applied in a wider
range of languages than a
native VCL. A developer can
use it for one client’s VB appli-
cation and for another client’s
Delphi application, without
purchasing two versions of the
same software.

Davis Business Systems Ltd./Psi


Computer Consultants Pty Ltd.
Price: From US$79 for a single-user license.
Phone: (604) 462-9007
Web Site: http://www.dbsonline.com

2 May 1999 Delphi Informant


Delphi Realsoft Releases SofTrak
Realsoft Development compatible using the Advantage for each entry; tracking of prod-
T O O L S announced the release of SofTrak engine. Client/server options are uct serial numbers and quantity
(in beta version at press time), an available for larger networks. on hand; custom reports for ver-
New Products integrated system for customer Registration components are sions, renewals, and invoices;
and Solutions tracking, support logging, help- ActiveX and native Delphi, and and more.
desk management, invoicing, registration codes use a secure
licensing, registration coding, four-stage encryption. Realsoft Development
version control, and more. SofTrak’s tracking features Price: SofTrak Lite (single user), US$249;
SofTrak was written with include dual-pane Customer SofTrak Professional (up to five users),
Delphi 4 using native compo- Database and filtered Tracking US$495; additional discounts are available
nents for Win95/98/NT4 and Database; auto date/time and for multiple users.
has a Microsoft Outlook-style username stamping for each Phone: (800) 929-3991
interface. Databases are xBase- entry; unlimited notes section Web Site: http://www.realsoftdev.com
MathTools Announces MIDEVA
MathTools Ltd. announced ed development environment. environment for developing and
MIDEVA, its scientific integrat- MIDEVA presents a complete running scientific applications.
MIDEVA includes an m-files
interpreter, syntax-highlighting
editor, source-level debugger,
optimizer, and online reference
guide. MIDEVA also provides
the ability to compile m-files
into executables,
Delphi/Visual Basic/Excel
DLLs, and Debug/Release
modes. It is compatible with
MATLAB 4.x and 5.
Linder Software and Albert’s
Ambry Announce LSPzip MathTools Ltd.
Linder Software, in association Price: Single commercial license, US$999;
with Albert’s Ambry, has released academic license, US$299.
the latest version of LSPzip, a com- Phone: (212) 208-4476
pression library for developers
working in Delphi, C++Builder, Web Site: http://www.
Clarion, Visual Basic, or Visual mathtools.com
C++ environments.
LSPzip comes with two com- Watergate Announces ActiveX and CGI in PC-Doctor
pression libraries: LSPack and Watergate Software, Inc. trol will allow PC-Doctor to JavaScript or VBScript to com-
LSZip. The LSZip libraries are announced the integration of be used in conjunction with municate with PC-Doctor’s
compatible with the latest PKZIP ActiveX and CGI technologies Web-based applications by diagnostic and system infor-
archive format.
LSPzip costs US$169 for a single- into its PC-Doctor line of diag- enabling them to display PC- mation capabilities online in
user license. A trial version may be nostic software products. Doctor diagnostic data. Web- real time. PC-Doctor CGI is
downloaded from the Albert’s The PC-Doctor ActiveX con- based applications can use an HTTP server-callable pro-
Ambry Web site at gram that allows for a com-
http://www.alberts.com/ RightDoc Releases RightDoc 1.0 mand line-driven, Web-based
authorpages/00013316/
prod_698.htm. RightDoc Co. announced merging, conditional formatting, interface to PC-Doctor.
RightDoc 1.0, an XML-based and conditional processing, PC-Doctor’s Modular Core
content management and pub- RightDoc is application data- Technology contains over 300
lishing engine that creates per- driven. Companies can create test functions optimized for
sonalized document content sets of “Smart” documents, with implementation throughout
from applications to view, print, each document having the abili- each stage of the product life
and generate HTML4, PDF, ty to alter itself based on appli- cycle. Each primary product —
and PostScript formats. cation data provided. PC-Doctor Factory, PC-Doctor
RightDoc provides for the Form-based built-in editors for Windows, PC-Doctor for
creation of XML documents in create and modify processing Windows NT, and PC-Doctor
Enterprise or Web server-based tags, styles and style properties, Service Center — provides a
Delphi, Visual Basic, Visual and XML entities. specific diagnostic application
C++, Visual J++, Excel, Access, of the core technology.
and others. It creates personal- RightDoc Co.
ized forms, reports, billing Price: US$249 per development seat Watergate Software, Inc.
statements, legal contracts, let- (royalty-free). Price: Contact Watergate for pricing.
ters and more. Phone: (509) 464-1059 Phone: (510) 596-2080
With built-in variable text Web Site: http://www.rightdoc.com Web Site: http://www.ws.com

3 May 1999 Delphi Informant


News InterBase Releases InterBase 5.5 for SCO
Scotts Valley, CA — InterBase
Software Corp. announced the
availability of InterBase 5.5 for
been improved by adding such
features as protection for online
metadata updates of Triggers and
Performance enhancements
include more efficient memory
usage and a new multi-threaded
L I N E the Santa Cruz Operation (SCO) Stored Procedures by the strength ODBC 3.0 driver that adds sup-
UNIX OpenServer operating sys- of the InterBase 5.5 versioning port for SQL-92 ROLES and
May 1999 tem. InterBase 5.5 for SCO offers engine. User Defined Functions international character sets.
performance improvements and (UDFs) have added safety fea- For more information on
enhanced stability, delivering a tures in Windows, and the UDF InterBase, visit http://www.
solution for SCO developers and library has been expanded. interbase.com.
value added resellers (VARs).
InterBase 5.5 for SCO
Inprise Strengthens AS/400 Global Partnership with
includes version 1.5 of SystemObjects
InterClient, an all-Java JDBC Amsterdam, Netherlands — develop, support, and market
driver. InterClient 1.5 adds Inprise Corp. announced a Inprise’s Windows-based visual
direct international support for long-term, world-wide exclusive development tools for the IBM
user-specified character sets. licensing agreement with AS/400 platform.
Stability in InterBase 5.5 has SystemObjects Corp. to further SystemObjects is a key Inprise
partner, having helped develop
HREF Presents Live eSeminars Delphi/400 and
Santa Rosa, CA — HREF Tools sentations with attendees from C++Builder/400. Under the
Corp. announced its new live all over the world, including new agreement, SystemObjects
SkyLine Tools Announces eSeminars using U-VU Network’s the US, Brazil, Lithuania, assumes the future development,
the Programmer of Internet Conference Service Mexico, Norway, and Australia. maintenance, and marketing of
The Year Award Software. U-VU is a built-with- HREF’s series “WebHub Tech Delphi/400 and
SkyLine Tools Imaging pre- WebHub system that enables the Talk Radio” brings technical con- C++Builder/400 internationally.
sented the Programmer of the
presenter to broadcast a live, tent to developers every two Later this year, SystemObjects
Year Award to Shelley
Emmerson, Imaging Technology interactive presentation while par- weeks. “WebHub Sizzle” presenta- plans a Java development solu-
Expert for the Royal Canadian ticipants watch visual screens and tions, for developers evaluating the tion for the AS/400 based on
Mounted Police. listen to an audio stream. Online power and usability of WebHub, Inprise’s JBuilder.
Using the SkyLine Tools participants log in, see the slide also air on an ongoing basis. For more information on
Imaging Corporate Suite and
VideoLab Pro, Emmerson creat- show, listen to the presenter, ask For more information and a SystemObjects, visit
ed an application that could questions by typing them in, and complete schedule, visit http://www.systemobjects.com,
input, store, and organize the chat (using text) with other users http://www.href.com/present. or call (800) 586-5516.
thousands of images, docu- before and after the presentation.
ments, and video footage result-
HREF is also partnering with
Oracle Expands Relationship with Inprise for
ing from the investigation of the
tragic 1998 Swissair Flight 111 other Delphi developers who VisiBroker CORBA
crash, which claimed 225 lives want to use the U-VU technology Scotts Valley, CA — Inprise press time, VisiBroker has been
when the aircraft went down in to create their own eSeminars. Corp. announced it signed a integrated into Oracle8i, Oracle
more than 200 feet of water The presentations run for licensing agreement with Oracle Application Server, and other
near Halifax, Nova Scotia.
Emmerson put the application about an hour and focus on Corp. Under the terms of the Oracle products.
together in 12 hours. topics of interest relating to multi-year agreement, Oracle Oracle8i extends Oracle’s tech-
Other tools used were Multi- Web development using has selected Inprise’s VisiBroker nology leadership in the areas of
Edit from American Cybernetics, HREF’s line of WebHub prod- as one of its worldwide stan- transaction processing, data ware-
InfoPower 4 from Woll2Woll,
InterBase 5 SQL relational data-
ucts and related utilities. dards for CORBA object request housing, mobile computing, and
base from InterBase Software, HREF has completed five pre- broker (ORB) technology. As of high-availability systems.
and Crystal Reports from Seagate
Software. A Windows NT work- Inprise Creates Separate Divisions
station was utilized as the appli- Scotts Valley, CA — In conjunc- designed to increase operating sulting and training capabilities
cation server. tion with its fiscal year 1998 and efficiency. borland.com offerings for the enterprise integration
fourth quarter earnings announce- will include Borland Delphi, marketplace.
ment, Inprise Corp. announced a C++Builder, JBuilder, and Jim Weil, currently President
new business structure with the InterBase. of Inprise subsidiary InterBase,
formation of two divisions: Solutions offered by the has been named President of
Inprise and borland.com. Inprise division will be sold the Inprise division, which will
borland.com plans to become a through its direct sales organiza- be headquartered in San
premier destination Web site that tion and partner channel, and Mateo, CA. John Floisand,
will serve individual developers’ will include Inprise Application Senior Vice President of world-
needs for a range of advanced Server, JBuilder for AppServer, wide sales, has been appointed
Internet products and technolo- AppCenter, VisiBroker, ITS and President of borland.com,
gies, including those from third Entera. The division will contin- which will remain in Scotts
parties. The restructuring will ue to expand its professional ser- Valley. Both division heads will
include a streamlining of facilities, vice organization to provide report to Delbert W. Yocam,
headcount, and product lines comprehensive integration con- Inprise chairman and CEO.
4 May 1999 Delphi Informant
On the Cover
Template Method / Frameworks / OOP

By Xavier Pacheco

The Template Method Pattern


Building a UI Application Framework

I n the March, 1999 issue of Delphi Informant, we discussed the Singleton pattern. This
month, we’ll create a simple application framework using a commonly used pattern,
the Template Method. This article will present two concepts: designing a framework
around which you develop a user interface, and using patterns to accomplish this.

In the following section, I’ll explain the concept of So how do patterns and frameworks relate to one
frameworks, what they are, and how they help us another? A developer uses patterns to build frame-
in designing applications. I’ll also explain what works for software. Patterns and frameworks aren’t
frameworks have to do with patterns. In this and the same; the GoF have defined three major dif-
future articles, I’ll make reference to the book ferences. Design patterns are:
Design Patterns: Elements of Reusable Object- more abstract than frameworks;
Oriented Software [Addison-Wesley, 1994] by smaller architectural elements than frame-
Erich Gamma, Richard Helm, Ralph Johnson, works; and
and John Vlissides, otherwise known as the Gang less specialized than frameworks.
of Four (GoF).

OOP Frameworks The first difference emphasizes that patterns


The GoF define a framework as “... a set of coop- focus more on abstractions or methodologies.
erating classes that make up a reusable design for a Although you can create a concrete framework
specific class of software.” A framework defines that structures a specific domain, the same isn’t
the constraints within which a product can be true for a pattern. Frameworks exist in actual
constructed. In other words, frameworks define code that may be copied and reused in different
the architecture or structure of the final product. applications. The same isn’t true for patterns, as
Although it might seem a bit restrictive, this is an they must be implemented each time they’re
extremely beneficial practice to use in software used. As for patterns, it’s the pattern that’s
development. Developers need not focus on the reused, not the implementation of that pattern.
flow or structure of the user interface (UI), so they Framework implementations, on the other
can instead focus on specific functionality. hand, may be reused.

With a tool like Delphi, it’s easy to fall into the The second difference emphasizes how patterns are
trap of designing the UI without careful fore- smaller in size. A framework may be composed of
thought as to how it’s supposed to work. A typi- many patterns, whereas a pattern would never be
cal UI may start out as a main form with a composed of frameworks. A pattern is more like an
menu. Later, a page control is added to separate algorithm used in a framework, and the framework
logical functionality. As the UI develops, more is what defines the architecture of a system.
functionality is added, removed, and moved into
separate forms. Typically, the layout and flow of The third difference emphasizes how frame-
the UI isn’t decided upon until much later in works are written to a specific application
the development process. By first designing a domain. For example, you might define a
framework, one can reduce the amount of re- framework for a graphics manipulation applica-
coding and redesign that occurs during UI tion. There isn’t a pattern that specifically
development. A UI framework acts as a plug-in addresses this domain, although you might use
mechanism to facilitate a more structured a pattern or combination thereof to construct
approach to UI development. this specific framework. The key is that the

5 May 1999 Delphi Informant


On the Cover
framework would only be useful for this type of application, I’m surprised we don’t see Template Class or Abstract Class pre-
whereas the patterns used to construct the framework may be sented as discrete patterns.
used on any type of application.
Another very good book on patterns is The Design Patterns Smalltalk
The Template Method Pattern Companion by Kyle Brown, Bobby Woolf, and Sherman R. Alpert
As the GoF state in Design Patterns, a Template Method pattern is [Addison-Wesley, 1998]. It presents four types of methods that
intended to: “Define the skeleton of an algorithm in an operation, might appear in an abstract class: template, concrete, abstract, and
deferring some steps to subclasses. Template Method lets subclasses hook. In Delphi, I’d suggest a fifth type, which I’ll refer to as the
redefine certain steps of an algorithm without changing the algo- event (see Figure 2).
rithm’s structure.”
Implementing Application Frameworks
The Template Method is probably the most widely used pattern In my earlier description of the initial approach to UI design, I
by Delphi developers. It’s based on the concept of inheritance; in described the typical process that a developer might use to put togeth-
fact, you’ll see examples of the Template Method strewn through- er the UI. Although this might be fine for a small application, or an
out the VCL code. Figure 1 depicts the structure of the Template application being developed by one person, it presents a problem with
Method pattern. applications being developed as functional pieces. It’s also a problem
where different people are working on different parts of the UI.
Template Method vs. Abstract Class
The Template Method pattern and abstract classes aren’t one in the A better approach would be if you separated the UI/functional
same. An abstract class provides the declaration of a class interface pieces and brought them together later — during a build process —
while deferring its implementation to its subclasses. Therefore, the after developers have finished developing their parts. This technique
various implementations of that abstract class provide the ability to is made possible by using a framework. The code that programmers
create different behaviors for the client. write must adhere to that framework.

A Template Method accomplishes the same at the method/algorith- The example in Figure 3 depicts the modularity of a framework.
mic level. A Template Method is defined and implemented in an The Shell Application owns a View Manager. The View Manager
abstract class to provide structure for an action performed by that
class. All descendant classes (concrete classes) override and imple- AbstractClass
ment smaller pieces — abstract methods — of the class to help give Primitive Method1
it identity. Therefore, a Template Method is actually part of an TemplateMethod PrimitiveMethod2
abstract class even though it’s usually fully defined and implement- PrimitiveMethod1
ed. Look at it as a way to extend the control in which you attempt PrimitiveMethod2
to define an abstract class. Ideally, the Template Method isn’t
declared as virtual or dynamic. The purpose of this method is to
establish a consistent process among the descendant classes. For
example, a Template Method might look something like this:
ConcreteClassA ConcreteClassB
procedure TSomeObject.TemplateMethod;
var
PrimitiveMethod1 PrimitiveMethod1
i: Integer; PrimitiveMethod2 PrimitiveMethod2
begin
PrimitiveMethod1; Figure 1: The Template Method pattern structure.
for I := 0 to 10 do
PrimitiveMethod2;
Type Description
PrimitiveMethod1;
end; Template A combination of the remaining four methods forming
an algorithm. Subclasses typically don’t change this
method and simply override the methods they call.
In this example, the TemplateMethod method defines a simple tem-
Concrete A method hard-coded by the abstract class that
plate by which two primitive methods are invoked. The implemen-
doesn’t get overridden by the subclasses. In Delphi,
tations of these primitive methods may vary so that they perform
this is a static method.
entirely different actions. For this to work, we must use object
Abstract A method declared by, and implemented by, each
inheritance to allow for the varying behaviors for the primitive
subclass. In Delphi, the keyword abstract is used to
methods. Therefore, it can be assumed that the Template Method
define this type of method.
pattern is dependent on abstract classes, i.e. they go hand in hand.
Hook A method that provides default functionality, and
that may be overridden by subclasses. In Delphi,
Method Types
this is the virtual or dynamic method.
We’ve nailed down the issue of Template Methods being routines
Event A pointer to a method. This method may be provided
with behaviors that change internally without the client knowing
by the client of the abstract class. It will be called if it
of this change. Because this method is contained in a class, it fol-
exists by the abstract class. When a method is provid-
lows we must provide an inheritance model to allow us to create
ed to the method pointer (e.g. OnClick event proper-
different subclasses to which we provide the differing method
ty) it’s referred to as an “event handler.” Component
behaviors. I’ll focus less on the Template Method itself, and more
writers will be familiar with this method type.
on the abstract class used to implement a Template Method. In
fact, because this is a technique so common in OOP development, Figure 2: Method types in an abstract class.

6 May 1999 Delphi Informant


On the Cover
procedure TWinControl.CreateWnd;
Shell Application View Manager
var
1..1 Params: TCreateParams;
TempClass: TWndClass;
ClassRegistered: Boolean;
1..1 begin
CreateParams(Params);
with Params do begin
TViewForm // Code removed.
if not ClassRegistered or
(TempClass.lpfnWndProc <> @InitWndProc) then begin
// Code removed.
end;
// Code removed.
CreateWindowHandle(Params);
// Code removed.
ConcreteViewForm1 ConcreteViewForm2 end;
end;

Figure 4: Skeleton of the TWinControl.CreateWnd Template


Figure 3: An example UI framework. Method.

is the intermediary between the Shell Application and each TWinControl = class(TControl)
TViewForm. TViewForm is an abstract class that defines the inter- procedure CreateParams(
var Params: TCreateParams); virtual;
face with which the View Manager and Shell Application interact.
procedure CreateWindowHandle(
Developers create implementations of TViewForm that adhere to const Params: TCreateParams); virtual;
the interface defined by TViewForm. Because each TViewForm end;
descendant is an independent form, it can be developed apart from
the Shell Application. When the functionality for a TViewForm is
complete, it’s incorporated with the main application. Both methods are used in the TWinControl.CreateWnd method, which
looks similar to Figure 4. I removed much of the code to conserve
Defining the Abstract TViewForm space. You’ll see that both the CreateParams and CreateWindowHandle
Listing One (on page 9) shows the source for the abstract TViewForm methods are called in the context of the CreateWnd method. It’s possi-
(this and all other source is available for download; see end of article ble for descendant classes to override both these methods to provide
for details). This class declares the methods and properties that enable different behaviors described by the Template Method pattern. In fact,
it to be embedded as a child window. Specifically, it declares two con- I override the CreateParams method to ensure that TViewForm can be
structors. The constructor that takes a TWinControl parameter, embedded as a child window. Other than its name, this Template
AParent, assigns the value passed in as AParent to a temporary variable. Method pattern is nothing new to most Delphi developers. The same
This will be used in the overridden Loaded method that sets up the is true for an event method as shown here:
appropriate property values required by this form to be embedded.
procedure TCustomForm.DoClose(var Action: TCloseAction);
TViewForm also declares three hook methods that may be overridden begin
if Assigned(FOnClose) then FOnClose(Self, Action);
by its descendants. These are “reader” methods for the properties
end;
ViewDescription, ViewMenu, and ViewToolbar. Whenever you declare a
virtual class containing reader methods for properties, it’s a good idea to
not keep these methods as abstract, because the descendants of the class This procedure shows the TCustomForm.DoClose method.
may not need to implement them. For example, not all TViewForm TCustomForm is another TViewForm ancestor. This method checks
descendants will return a TToolBar, TPopupMenu, or string description. to see that its field, FOnClose, is referring to a method. FOnClose is
Instead of forcing the descendant class to implement these methods, a method pointer that, if valid, gets called. Again, this technique
we’ll just implement them ourselves to provide default nil/empty values, isn’t new to component developers.
which is what the descendant class is going to have to do anyway.
Creating a TViewForm Subclass
TViewForm declares one abstract method, CanChange. Because Listing Two (on page 10) shows TViewForm2, one of three
TViewForm descendants are to be embedded into a TTabControl, you TViewForm descendants accompanying this article’s example pro-
may want to prevent the user from switching tabbed pages until a gram. I’m illustrating this form because it’s more complete. This
certain operation is complete. The Shell Application doesn’t know form is a simple database form containing a database connection, a
what functionality the TViewForm provides and, therefore, can’t pre- TDBGrid to display the data, a TToolbar for navigation, and a pop-
vent the tab switch from occurring. However, the Shell Application up menu. This form can function independently from the main
can call the CanSwitch method to determine if the tab switch is valid. application. It can also be integrated with the main application,
because it adheres to the interface defined by TViewForm.
TViewForm provides only hook and abstract methods, although it
carries along concrete, template, and event methods from its own Notice how I’ve overridden the methods declared by TViewForm, and
ancestors. For example, one of TViewForm’s ancestors is provided the proper results for the Shell Application. For example,
TWinControl, which defines two virtual methods that may be over- GetViewDescription returns a valid string, and GetViewMenu returns a
ridden by its descendants, CreateWindowHandle and CreateParams. valid TPopupMenu for integration with the Shell Application. Also,
The declarations of these methods are shown here: examine the CanChange method. This method passes False if Table1 is

7 May 1999 Delphi Informant


On the Cover

Figure 5: The Shell Application main form. Figure 7: TViewForm2 at run time.

Figure 6: TViewForm1 at run time. Figure 8: TViewForm3 at run time.

in a mode other than dsBrowse, thus forcing the user to save his or her When the user attempts to change a tab on the TTabControl, the
work. There are similar forms in the example you can study. OnChanging event handler for the TTabControl is invoked, and
the AllowChange parameter is set to the value of the currently
Creating the Shell Application loaded TViewForm.CanChange method. You can see how you
The Shell Application for this framework is shown in Listing Three might prevent a user from changing tabs and the current
(beginning on page 10). Figure 5 shows the main form for the Shell TViewForm. If the change is valid, tctrlMainChange,
Application. Notice that the primary pieces to the main form are the TTabControl.OnChange, is invoked. This method performs several
TTabControl, TCoolBar, and TMainMenu. The TTabControl contains a steps. First, it un-merges the current TViewForm’s menu. Then, it
TPanel, pnlContainer, that will serve as the container to the TViewForm retrieves the next TViewForm and merges its menu and toolbar
descendants. When the user selects a new tabbed page, the TViewForm with the main form. The reason I don’t have an UnMergeToolBar
corresponding to that tab will be loaded, and the previous form will be method is because the TViewForm’s toolbar is still owned by
unloaded. TCoolBar contains two bands. The first band is an applica- TViewForm. In the process of freeing the TViewForm, its
tion level band. The TToolButton objects on this band each correspond TToolBar is also freed. Figures 6, 7, and 8 show the main form
to one of the three TViewForm descendants and invokes them as nor- with the three TViewForm descendants contained in pnlContainer.
mal modal forms. The second band, which appears empty, contains a
TPanel, pnlViewTB, that will contain the TToolBar for the TViewForm The main form retrieves the TViewForm descendants using one
that’s returned by the TViewForm.ViewToolBar property. Finally, the of two methods. One is illustrated in the tctrlMainChange
pop-up menu returned by the TViewForm.ViewMenu property is method in the call to the TViewManager.GetViewForm methods
merged into the main form’s main menu. I don’t use the standard merg- shown below:
ing capabilities of TMainMenu, because I’m going to have multiple lev-
els of merging when I extend this framework in my next article. FViewManager.GetViewForm(tctrlMain.TabIndex, pnlContainer);

As you examine Listing Three, you’ll notice another class, TViewManager.GetViewForm is an overloaded method that creates a
TViewManager. This class represents another pattern, which I’ll discuss TViewForm instance and assigns the second parameter, in this case
in my next article. For now, just know that this is the class with which pnlContainer, as the parent. TViewManager does this by calling the
the main form interacts to reference the TViewForm descendants. appropriate constructor of TViewForm. The TViewManager
TViewManager is responsible for creating, freeing, and managing the method is called in TMainForm.ShowViewFormModal:
TViewForm descendants. I’ll discuss TViewManager’s functionality in
the context of the main form interacting with TViewForms. ViewForm := FViewManager.GetViewForm(ViewIndex);

8 May 1999 Delphi Informant


On the Cover

procedure Loaded; override;


function GetViewDescription: string; virtual;
function GetViewMenu: TPopupMenu; virtual;
function GetViewToolBar: TToolBar; virtual;
public
// Constructor to create as a normal form.
constructor Create(AOwner: TComponent); overload;
override;
// Constructor to create as a child form.
constructor Create(AOwner: TComponent;
AParent: TWinControl); reintroduce; overload;
function CanChange: Boolean; virtual; abstract;
property ViewDescription: string
read GetViewDescription;
property ViewMenu: TPopupMenu read GetViewMenu;
property ViewToolbar: TToolBar read GetViewToolBar;
end;

Figure 9: Modal TViewForm2. implementation

{$R *.DFM}
This method returns a reference to a TViewForm. ShowViewFormModal
demonstrates how you can show the same form previously embedded in { TviewForm. }
a TPanel as a separate modal form (see Figure 9). Finally, notice that the constructor TViewForm.Create(AOwner: TComponent);
main form’s OnCreate event handler invokes the tctrlMainChange begin
method to load the first TViewForm. FAsChild := False;
inherited Create(AOwner);
end;
Conclusion
The Template Method pattern is really an “OOP Patterns” term for constructor TViewForm.Create(AOwner: TComponent;
something most Delphi programmers have been doing since Delphi AParent: TWinControl);
begin
1. In this article, I discussed the Template Method pattern and started
FAsChild := True;
on the initial design of an application framework. I’ll use this frame- FTempParent := aParent;
work in my next article to discuss another useful and commonly used inherited Create(AOwner);
pattern, the Builder pattern. I’ll also illustrate how to extend this end;
framework to embed fully functional, run-time modules by using
procedure TViewForm.Loaded;
add-in packages. In closing, I’d like to thank David Streever and Anne begin
Pacheco for their technical and grammatical review of this article. ∆ inherited;
if FAsChild then begin
The files referenced in this article are available on the Delphi Informant align := alClient;
BorderStyle := bsNone;
Works CD located in INFORM\99\MAY \DI9905XP.
BorderIcons := [];
Parent := FTempParent;
Position := poDefault;
end;
Xavier Pacheco is the president and chief consultant of Xapware Technologies Inc., end;
where he provides consulting services and training. He is also the co-author of
procedure TViewForm.CreateParams(
Delphi 4 Developer’s Guide [SAMS Publishing, 1998]. You can write Xavier at
var Params: TCreateParams);
[email protected], or visit http://www.xapware.com. begin
Inherited CreateParams(Params);
if FAsChild then
Params.Style := Params.Style or WS_CHILD;
end;

Begin Listing One — TViewForm function TViewForm.GetViewMenu: TPopupMenu;


unit xwViewFrm; begin
Result := nil;
interface end;

uses function TViewForm.GetViewToolBar: TToolBar;


Windows, Messages, SysUtils, Classes, Graphics, Controls, begin
Forms, Dialogs, ComCtrls, Menus; Result := nil;
end;
type
{ TViewForm serves as the abstract class for Views within function TViewForm.GetViewDescription: string;
the ShellApp framework. } begin
TViewForm = class(TForm) Result := EmptyStr;
private end;
FAsChild: Boolean; // Form created as child indicator.
FTempParent: TWinControl; // Temporary parent window. end.
protected
procedure CreateParams(var Params: TCreateParams);
override; End Listing One

9 May 1999 Delphi Informant


On the Cover

Begin Listing Two — TViewForm2 Table1.Prior;


end;
unit ViewFrm2;
procedure TViewForm2.ToolButton3Click(Sender: TObject);
interface begin
inherited;
uses Table1.Next;
Windows, Messages, SysUtils, Classes, Graphics, Controls, end;
Forms, Dialogs, xwViewFrm, ComCtrls, ToolWin, Grids,
DBGrids, Db, DBTables, Menus; procedure TViewForm2.ToolButton4Click(Sender: TObject);
begin
type inherited;
TViewForm2 = class(TViewForm) Table1.Last;
Table1: TTable; end;
DataSource1: TDataSource;
DBGrid1: TDBGrid; procedure TViewForm2.PopupMenuClick(Sender: TObject);
ToolBar1: TToolBar; begin
ToolButton1: TToolButton; inherited;
ToolButton2: TToolButton; ShowMessage((Sender as TMenuItem).Caption);
ToolButton3: TToolButton; end;
ToolButton4: TToolButton;
PopupMenu1: TPopupMenu; end.
InsertRecord1: TMenuItem;
EditRecord1: TMenuItem;
DeleteRecord1: TMenuItem;
End Listing Two
procedure ToolButton1Click(Sender: TObject);
procedure ToolButton2Click(Sender: TObject); Begin Listing Three — The Shell Application
procedure ToolButton3Click(Sender: TObject);
procedure ToolButton4Click(Sender: TObject); unit MainFrm;
procedure PopupMenuClick(Sender: TObject);
protected interface
function GetViewToolBar: TToolBar; override;
function GetViewMenu: TPopupMenu; override; uses
function GetViewDescription: string; override; Windows, Messages, SysUtils, Classes, Graphics, Controls,
public Forms, Dialogs, ExtCtrls, ComCtrls, Menus, ToolWin,
function CanChange: Boolean; override; xwViewFrm, ImgList;
end;
type
var TViewManager = class(TObject)
ViewForm2: TViewForm2; private
FCurrentView: TViewForm;
implementation public
constructor Create;
{$R *.DFM} destructor Destroy; override;
{ GetViewForm retrieves the View, giving it a parent
{ TViewForm2. } window into which it embeds itself. }
function TViewForm2.CanChange: Boolean; function GetViewForm(const ViewIndex: Integer;
begin AParent: TWinControl): Boolean; overload;
if Table1.State <> dsBrowse then { GetViewForm retrieves a specified view by index. This
ShowMessage( method retrieves a reference to a view instance which
'Cannot change pages until you save the record'); does not embed itself. }
Result := Table1.State = dsBrowse; function GetViewForm(const ViewIndex: Integer):
end; TViewForm; overload;
{ CloseCurrentViewForm closes and destroys the current
function TViewForm2.GetViewDescription: string; embedded view form. }
begin procedure CloseCurrentViewForm;
Result := 'View Form 2'; { MergeViewToolBar merges the current views toolbar
end; with the AViewToolBarParent parameter. }
procedure MergeViewToolBar(
function TViewForm2.GetViewMenu: TPopupMenu; AViewToolbarParent: TWinControl);
begin { MergeViewMenu merges the current views menu with the
Result := PopupMenu1; main menu specified by AAddMenu. AMainMenu is used as
end; the owner of the newly created menu. }
procedure MergeViewMenu(AMainMenu: TMainMenu;
function TViewForm2.GetViewToolBar: TToolBar; AAddMenu: TMenuItem);
begin { UnmergeViewMenu unmerges and destroys the menu added
Result := ToolBar1; by the ViewMenu. }
end; procedure UnmergeViewMenu(AMenuItem: TMenuItem);
end;
procedure TViewForm2.ToolButton1Click(Sender: TObject);
begin TMainForm = class(TForm)
inherited; mmMain: TMainMenu;
Table1.First; mmiFile: TMenuItem;
end; mmiExit: TMenuItem;
stbrMain: TStatusBar;
procedure TViewForm2.ToolButton2Click(Sender: TObject); tctrlMain: TTabControl;
begin pnlContainer: TPanel;
inherited; CoolBar1: TCoolBar;

10 May 1999 Delphi Informant


On the Cover
ToolBar1: TToolBar;
ToolButton1: TToolButton; procedure TViewManager.MergeViewMenu(AMainMenu: TMainMenu;
ToolButton2: TToolButton; AAddMenu: TMenuItem);
ToolButton3: TToolButton; var
pnlViewTB: TPanel; MenuItem: TMenuItem;
mmiView: TMenuItem; i: Integer;
mmiHelp: TMenuItem; begin
mmiAbout: TMenuItem; if FCurrentView <> nil then
ilMain: TImageList; if FCurrentView.ViewMenu <> nil then begin
procedure mmiExitClick(Sender: TObject); for i := 0 to FCurrentView.ViewMenu.Items.Count-1 do
procedure tctrlMainChanging(Sender: TObject; begin
var AllowChange: Boolean); MenuItem := TMenuItem.Create(AMainMenu);
procedure FormCreate(Sender: TObject); MenuItem.Caption :=
procedure FormDestroy(Sender: TObject); FCurrentView.ViewMenu.Items[i].Caption;
procedure tctrlMainChange(Sender: TObject); MenuItem.OnClick :=
procedure ToolButton1Click(Sender: TObject); FCurrentView.ViewMenu.Items[i].OnClick;
procedure ToolButton2Click(Sender: TObject); AAddMenu.Add(MenuItem);
procedure ToolButton3Click(Sender: TObject); end;
private AAddMenu.Visible := True;
FViewManager: TViewManager; end;
procedure ShowViewFormModal(const ViewIndex: Integer); end;
end;
procedure TViewManager.UnmergeViewMenu(
var AMenuItem: TMenuItem);
MainForm: TMainForm; var
i: Integer;
implementation begin
for i := AMenuItem.Count - 1 downto 0 do
uses AMenuItem.Delete(i);
ViewFrm1, ViewFrm2, ViewFrm3; AMenuItem.Visible := False;
end;
{$R *.DFM}
procedure TViewManager.MergeViewToolBar(
{ TViewManager. } AViewToolbarParent: TWinControl);
procedure TViewManager.CloseCurrentViewForm; begin
begin if FCurrentView.ViewToolbar <> nil then begin
if FCurrentView <> nil then begin FCurrentView.ViewToolBar.EdgeBorders := [];
FCurrentView.Free; FCurrentView.ViewToolbar.Parent := AViewToolbarParent;
FCurrentView := nil; end;
end end;
end;
{ TMainForm. }
constructor TViewManager.Create; procedure TMainForm.FormCreate(Sender: TObject);
begin begin
inherited Create; FViewManager := TViewManager.Create;
FCurrentView := nil; tctrlMainChange(nil);
end; end;

destructor TViewManager.Destroy; procedure TMainForm.FormDestroy(Sender: TObject);


begin begin
if Assigned(FCurrentView) then FViewManager.Free;
FCurrentView.Free; FViewManager := nil;
inherited Destroy; end;
end;
procedure TMainForm.mmiExitClick(Sender: TObject);
function TViewManager.GetViewForm(const ViewIndex: Integer; begin
AParent: TWinControl): Boolean; Close;
begin end;
CloseCurrentViewForm;
case ViewIndex of procedure TMainForm.tctrlMainChanging(Sender: TObject;
0: FCurrentView := TViewForm1.Create(nil, AParent); var AllowChange: Boolean);
1: FCurrentView := TViewForm2.Create(nil, AParent); begin
2: FCurrentView := TViewForm3.Create(nil, AParent); AllowChange := FViewManager.FCurrentView.CanChange;
end; end;
FCurrentView.Show;
Result := True; procedure TMainForm.tctrlMainChange(Sender: TObject);
end; begin
FViewManager.UnmergeViewMenu(mmiView);
function TViewManager.GetViewForm( FViewManager.GetViewForm(tctrlMain.TabIndex,
const ViewIndex: Integer): TViewForm; pnlContainer);
begin FViewManager.MergeViewToolBar(pnlViewTB);
Result := nil; // Default. FViewManager.MergeViewMenu(mmMain, mmiView);
case ViewIndex of stbrMain.Panels[1].Text :=
0: Result := TViewForm1.Create(Application); FViewManager.FCurrentView.ViewDescription;
1: Result := TViewForm2.Create(Application); end;
2: Result := TViewForm3.Create(Application);
end; procedure TMainForm.ShowViewFormModal(
end; const ViewIndex: Integer);

11 May 1999 Delphi Informant


On the Cover
var
ViewForm: TViewForm;
begin
ViewForm := FViewManager.GetViewForm(ViewIndex);
try
ViewForm.ShowModal;
finally
ViewForm.Free;
end;
end;

procedure TMainForm.ToolButton1Click(Sender: TObject);


begin
ShowViewFormModal(0);
end;

procedure TMainForm.ToolButton2Click(Sender: TObject);


begin
ShowViewFormModal(1);
end;

procedure TMainForm.ToolButton3Click(Sender: TObject);


begin
ShowViewFormModal(2);
end;

end.

End Listing Three

12 May 1999 Delphi Informant


Undocumented
Undocumented Win32 API / Delphi 2, 3, 4

By Kevin J. Bluck and James Holderness

From the Shell


Part II: More Undocumented Shell Dialog Boxes

L ast month, we looked at several useful dialog boxes whose functions are not provided
in Comdlg.dll, nor are they clearly documented. Instead of trying to duplicate interfaces
by building a dialog box manually, we showed how to access several commonplace system
dialog boxes you may need, but are difficult to find. To close out this two-part series, we’ll
show you several more functions and how to use them.

A sample program accompanies this series and The SearchRoot parameter allows you to begin a
demonstrates each dialog box function (see Figure 1). search in a particular folder. This is the same effect
The first article covered five dialog boxes: Browse for you get if you select Find on the context menu of a
Folder, About Windows, Format, Change Icon, and folder you’ve right-clicked. You may pass this para-
Run. This month we tackle the rest. meter as nil to allow the user to begin searching at
the Desktop. The SavedSearchFile parameter
Finding Files allows you to specify a file saved from a previous
Many people want the Find dialog box. It’s the search (a .FND file) that will initialize the dialog
handy utility provided by the shell to find files box to match the saved state. This is the effect you
based on a variety of criteria (see Figure 2). The would get from opening a .FND file in the
ability to spawn this dialog box from your own Explorer. You may also pass this parameter as nil.
application is provided by the SHFindFiles func- Passing both parameters as nil produces the dialog
tion. It’s exported from Shell32.dll, and the box you would get from selecting Find | Files or
ordinal value is 90: Folders from the Start menu.

function SHFindFiles(SearchRoot: PItemIDList; If you specify a non-nil SearchRoot PIDL, it’s your
SavedSearchFile: PItemIDList): LongBool;
responsibility to free that PIDL after calling
stdcall;
SHFindFiles. However, if you pass a non-nil
SavedSearchFile PIDL, you mustn’t try to free that
PIDL if the function succeeds, as an error will
occur if you do. We can only hope the shell will
free it when it’s done doing whatever it does with
it. If the function fails, however, you must free the
PIDL yourself. [For a description of PIDLs and
their use, see “Shell Notifications” by Kevin J.
Bluck and James Holderness in the March, 1999
Delphi Informant.]

Unlike most dialog box functions, this function is


non-modal. Instead, it starts the dialog box in a
separate thread, then returns immediately. The
return value will be True if the dialog box was suc-
cessfully spawned, False if there was an error. If
the user doesn’t explicitly close the dialog box, it
will automatically close when your process termi-
nates. Keep in mind that you have no direct way
of telling what the user does with the dialog box.
The best way for your application to become
Figure 1: The demonstration program. aware of files the user eventually finds is to sup-

13 May 1999 Delphi Informant


Undocumented

Figure 2: The Find dialog box.

port OLE drag-and-drop, so the user can drag found files from the
dialog box into your application.

Finding Computers
Another function closely related to SHFindFiles is SHFindComputer. This
function shows the same dialog box you would get if you clicked the Start
menu and selected Find | Compute. Its interface is identical to
SHFindFiles, except it completely ignores the parameters you send it.
Apparently, they have been reserved for future expansion. Just pass nil to
both parameters. The return values are identical to SHFindFiles, and like
that function, the dialog box is non-modal. So the function returns Figure 3: The Properties dialog box — in this case for a drive.
immediately while the dialog box remains open, and there is no direct
means of telling what the user did with the dialog box. SHFindComputer ignored, and the path from the FileName parameter is used instead.
is exported from Shell32.dll by the ordinal value of 91: The DefaultExtension parameter points to a buffer containing the
default extension the dialog box will search for. The Filter parameter
function SHFindComputer(Reserved1: PItemIDList; points to a buffer containing pairs of null-terminated filter strings
Reserved2: PItemIDList): LongBool; stdcall; that will be shown in the Files of Type drop-down list. The Caption
parameter points to a string to be shown in the title bar of the dialog
Browsing for Files box. For further details on all these parameters, see the documenta-
There really isn’t any compelling reason why you need to use this tion on the Windows OPENFILENAME data structure.
next function. GetFileNameFromBrowse is nothing more than a sim-
plified wrapper around the GetOpenFileName function, which is the If the user selects a file to open, the return value is True. It’s False if
function you call when you want to display the standard Open dia- an error occurs, the user chooses the Cancel button, or the user
log box. Obviously, you already have access to everything this func- chooses the Close command from the System menu.
tion can do by using the standard VCL TOpenDialog component or
the GetOpenFileName API function. However, for some applica- Displaying Object Properties
tions, it might be nice to be able to browse for a file with a single Another handy undocumented dialog box function is
function call without the tedious process of filling in all the mem- SHObjectProperties. This function can be used to display the
bers of the OPENFILENAME structure, or instantiating an Properties dialog box for a drive, folder, or file (see Figure 3). It
instance of OpenDialog. The function is exported from Shell32.dll can also be used to display the properties for a printer object.
by ordinal value 63: The function is exported from Shell32.dll by ordinal value 178:

function GetFileNameFromBrowse(Owner: HWND; function SHObjectProperties(Owner: HWND; Flags: UINT;


FileName: Pointer; MaxFileNameChars: DWORD; ObjectName: Pointer; InitialTabName: Pointer):
InitialDirectory: Pointer; DefaultExtension: Pointer; LongBool; stdcall;
Filter: Pointer; Caption: Pointer): LongBool; stdcall;

The Owner parameter identifies the window that owns the dialog box.
Most of the parameters to this function correspond directly with The Flags parameter specifies the type of object whose name is passed
members of the OPENFILENAME structure. The Owner parameter in the ObjectName parameter. These are the possible flag values:
identifies the window that owns the dialog box. The FileName para-
meter points to a buffer that contains a file name used to initialize the OPF_PRINTERNAME = $01;
dialog box’s Edit control. When the function returns, this buffer con- OPF_PATHNAME = $02;
tains the full path of the selected file. It’s advisable to provide a buffer
capable of storing MAX_PATH characters plus a null terminator. The ObjectName parameter points to a string containing the path
The MaxFileNameChars parameter specifies the size, in characters, of name, or the printer name whose properties will be displayed. If a
the buffer pointed to by the FileName parameter. The InitialDirectory printer is local, you may use only the actual printer name. If a print-
parameter points to a string that specifies the initial file directory er is from the network, you need to use the entire UNC-style name,
when the dialog box appears. If the FileName parameter contains a in the form \\COMPUTERNAME\PRINTERNAME. The
fully qualified file name with path, the InitialDirectory parameter is InitialTabName parameter points to a string containing the name of

14 May 1999 Delphi Informant


Undocumented
Shutting Down the System
The next two functions, ExitWindowsDialog and RestartDialog,
deal with the problem of shutting down and restarting the oper-
ating system. They may seem out of place in this article because
they’re not really much more than extensions of the
ExitWindowsEx API function, but they both produce dialog
boxes as part of the process. Both functions are exported from
Shell32.dll. The ordinal export value for ExitWindowsDialog is
60, and the ordinal for RestartDialog is 59. These function decla-
Figure 4: The Map Network Drive dialog box.
rations are shown in:

the tabbed page that will initially be shown in the dialog box. If the procedure ExitWindowsDialog(Owner: HWND); stdcall;
InitialTabName parameter is nil, or the string doesn’t match the
name of any tab, the first tab on the property sheet will be selected. and:

If the function succeeds, the return value is True. If the function function RestartDialog(Owner: HWND; Reason: Pointer;
fails, the return value is False. To get extended error information, call ExitType: UINT): DWORD; stdcall;
the API function GetLastError. Note that this dialog box is non-
modal, similar to the SHFindFiles dialog box, so when the function ExitWindowsDialog is probably the less useful of the two. It’s the
returns, the dialog box will almost certainly still be open. There is dialog box displayed when you select Shut Down from the Start
no way of knowing when the user has closed the dialog box. menu. The dialog box doesn’t always seem to actually use Owner as
a parent. On Windows 95, the owner window will receive a
Networking WM_QUIT message if the operation is successful. On Windows
The next two functions allow your user to connect to network NT, the owner window doesn’t appear to be used at all. There is
resources. SHNetConnectionDialog (see Figure 4) is available on no return value for the function, so you have no way of knowing
Windows 95 and NT, and is exported from Shell32.dll by ordinal 160: what the user selected, or whether the operation was canceled.
Presumably, your application will be receiving shutdown messages
function SHNetConnectionDialog(Owner: HWND; from Windows fairly soon if the user decided to quit, but there is
ResourceName: Pointer; ResourceType: DWORD): no way to know for sure at the time of the function call.
DWORD; stdcall;

RestartDialog is used when changes are made to the system that


SHStartNetConnectionDialog is available only on NT. It shows the require a shutdown or restart before they can take effect. The Owner
same dialog box as SHNetConnectionDialog, but starts it non- parameter identifies the window that will own the dialog box. The
modally in another thread and returns immediately. This function Reason parameter points to a string that is displayed in the dialog box,
is exported from Shell32.dll only on NT by ordinal value 215: explaining the reason for the shutdown. The ExitType parameter speci-
fies the type of shutdown that will be performed if the user selects the
function SHStartNetConnectionDialog(Owner: HWND; Yes button. You can use a subset of the values used by ExitWindowsEx
ResourceName: PWideChar; ResourceType: DWORD): in addition to a few new values. The following is the complete list:
DWORD; stdcall;

EWX_LOGOFF = $00;
The parameter lists are basically identical. The Owner parameter EWX_SHUTDOWN = $01;
EWX_REBOOT = $02;
takes the handle of the window that will own the dialog box. The
EW_RESTARTWINDOWS = $42;
ResourceName parameter points to a null-terminated string specify- EW_REBOOTSYSTEM = $43;
ing the fully qualified UNC path of the network resource to connect EW_EXITANDEXECAPP = $44;
to. Specifying this parameter results in a dialog box that is “pre-set”
to the named resource and doesn’t allow the user to change the
resource. If you pass nil to this parameter, the dialog box allows the The return value is IDYES if the user chose to perform the shut-
user to specify the resource. down. It is IDNO if the operation was canceled.

The ResourceType parameter can be set to one of two values: There are a couple of other points about RestartDialog you should
RESOURCETYPE_DISK or RESOURCETYPE_PRINT. These val- note. The reason displayed in the dialog box always has some default
ues will produce different dialog boxes. The first allows you to assign a text appended to it asking the user to confirm the operation. It’s
drive letter to a network disk resource, while the second allows you to therefore advisable you always end your reason text string with a
map a parallel port name, such as LPT2, to a network printer. space, or a CR/LF. The title of the dialog box is always set to
However, for some reason, RESOURCETYPE_PRINT doesn’t work “System Settings Change.” Finally, the return value cannot be used
on NT. If you pass this value on NT, the function fails. There are also to determine the success of the operation. It only signifies the choice
some other constants in the RESOURCETYPE_XXX family, but made by the user. If the restart operation failed for some reason, the
none of the others work for this function on any platform. return value will still be IDYES.

If the function succeeds, the return value is NO_ERROR. If the Note that for either of these functions to work correctly, users must
user cancels the dialog box, it returns –1 ($FFFFFFFF). If the func- have the SE_SHUTDOWN_NAME privilege enabled in their profile.
tion fails, the return value is some other error code. To get more This is usually not an issue in Windows 95, but some installations of
detailed error information, call the GetLastError API function. NT are set up to prevent certain users from shutting down the system.

15 May 1999 Delphi Informant


Undocumented
This is particularly common on server machines, which would deny tion gives the user the opportunity to empty their Recycle Bin,
other users needed services if shut down at the wrong time. This can hopefully freeing up a significant amount of disk space. If there is
be an insidious bug, because developer accounts typically have “Local no Recycle Bin on the specified drive, or the Recycle Bin is already
Administrator” privileges and can shut down the system, but when a empty, this function does absolutely nothing. This function is
user tries the same thing, the privilege is unavailable. Be sure to test exported from Shell32.dll by ordinal value 185:
your application using typical user accounts before release.
procedure SHHandleDiskFull(Owner: HWND;
Out of Memory! Drive: UINT); stdcall;

Here’s an undocumented dialog box function of dubious value, but


we mention it anyway for the sake of completeness. The Owner parameter identifies the dialog box’s owner window. It
SHOutOfMemoryMessageBox is the standard shell dialog box used seems to make no difference if you pass 0 to this parameter. The
when the system is low on memory. It’s exported from Shell32.dll by Drive parameter specifies the zero-based number of the drive that is
the ordinal value of 126: running out of space. This is the same scheme used in
SHFormatDrive, where 0 = A:, 1 = B:, and so on.
function SHOutOfMemoryMessageBox(Owner: HWND;
Caption: Pointer; Style: UINT): Integer; stdcall; The thing that concerns us about this function is that it’s not clear
how one should use it. It really can’t be used as a standard error dia-
It makes a call to the standard Windows API function log box whenever a disk runs out of space, because the dialog box
MessageBox, passing its three parameters along with the standard won’t show at all when there is nothing in the Recycle Bin, and there
system error message ERROR_OUTOFMEMORY, i.e. “Not is no return value to provide information about whether the dialog
enough storage is available to complete this operation,” or the box was ever shown. It’s also difficult to know whether the applica-
local language equivalent. tion can immediately retry the operation after this procedure returns,
because there’s no direct way to know if the user has freed any space.
The Owner parameter specifies the parent window for the dialog box. The user may have chosen to merely open the Recycle Bin, or may
The Caption parameter points to a null-terminated string used for the have done nothing. It seems the application will have to monitor disk
dialog box title. If Caption is nil, the title of the parent window is used free space on its own, and merely use this dialog box as an attempt at
instead. The Style parameter can be set to any combination of the a “quick fix” second chance before resorting to failing the operation.
MB_XXX constants used by the MessageBox function, but it’s typically
set to (MB_OK or MB_ICONHAND). The return values are identical to Generic Shell Message Boxes
those for MessageBox. Check the documentation for that function if you The last set of functions we’ll cover is the family of generic message
would like full details on the MB_XXX constants and the return value. dialog box functions provided by the shell. ShellMessageBox is just a
wrapper around the Windows API function MessageBox that allows
When the actual MessageBox call is made, MB_SETFOREGROUND is you to use either string resource identifiers, or standard null-terminat-
added to the Style flags, but if that first call fails, a second MessageBox call ed strings, as well as allowing additional inclusion strings in the mes-
is made, this time with MB_SYSTEMMODAL added to the Style flags. sage string in a manner similar to the Windows API FormatMessage
MB_SYSTEMMODAL, in combination with MB_ICONHAND, function. ShellMessageBox is exported from Shell32.dll by ordinal 183:
should cause the message box to display regardless of available memory.
Theoretically, anyway; in practice, we’ve observed a bug in the function function ShellMessageBoxA(Module: THandle; Owner: HWND;
that prevents the second call from ever being made. In the event the sys- Text: PChar; Caption: PChar; Style: UINT;
Parameters: array of Pointer): Integer; cdecl;
tem really is out of memory, this function will likely be incapable of dis-
playing anything. However, it still returns the result of the MessageBox
call, so you should be able to tell when the function has failed by check- Technically, this function is called ShellMessageBoxA, as it is an
ing for a return value of zero. ANSI-only variant, even on NT. There is also a UNICODE variant
called ShellMessageBoxW, which is exported by ordinal 182, but this
Out of Space! variant is available only on NT:
Another resource-oriented function is SHHandleDiskFull. Its name
is a bit of hyperbole in our opinion. It certainly can’t handle a full function ShellMessageBoxW(Module: THandle; Owner: HWND;
disk all by itself. It can provide a useful tool, however, to deal with Text: PWideChar; Caption: PWideChar; Style: UINT;
Parameters: array of Pointer): Integer; cdecl;
users who never empty their Recycle Bins (see Figure 5). This func-
tion is generally called by an application in response to a disk opera-
tion that is failing because of insufficient free disk space. When The Module parameter takes the handle of the module that provides
called, if the user has anything in that disk’s Recycle Bin, this func- the string resources for the dialog box. You should use the
GetModuleHandle Windows API function to retrieve that handle. The
Owner parameter is the usual handle to the owner window. The Text
parameter points to a null-terminated string containing the text you
would like displayed in the dialog box. It may alternatively be the
resource ID of a string resource contained in the module identified by
the Module parameter. This text may include “escape sequences,”
which the function will replace with the additional text parameters
passed in the Parameters parameter in the same manner provided by
the API function FormatMessage. These escape sequences take the form
“%#”, where “#” is the ordinal position of the extra string parameter in
Figure 5: The Hard Disk is Full dialog box. question. For example, “%1” will be replaced by the first string in the

16 May 1999 Delphi Informant


Undocumented
Parameters open array, “%3” will be replaced by the third, and so on. Windows constants in the middle of other code, we’ve implemented
The Caption parameter points to a null-terminated string that specifies “translation” functions that handle this task. This ensures that trans-
the text shown in the dialog box title bar. Again, a resource ID may be lation is done only in one easily identifiable place in the unit.
used instead of a string pointer. If this parameter is left nil, the caption
of the window specified in the Owner parameter is used instead. The The common denominator for all dialog-wrapping components is
Style parameter is a bit-mask of flags, the same ones used in the the Execute method. In every case, the meat of the API functionality
Windows API MessageBox function. This parameter can be set to any is in that method. All the other code associated with the component
combination of the MB_XXX constants used by the MessageBox func- is strictly in a supporting role, implementing the mechanics of the
tion. The return value is also identical to that of the MessageBox API Delphi component paradigm. If you want to see a practical example
function. Check the documentation for that function if you would like of how a particular API call is used, check the corresponding compo-
full details on the MB_XXX constants and the return value. nent’s Execute method in the code accompanying this article (again,
see end of article for download details).
As for the Parameters parameter, alert readers have probably already
noticed that the Microsofties have done something very naughty. We’ve added a few stand-alone functions to invoke the simpler dia-
They’ve exported a function using the cdecl calling convention instead log boxes, or to invoke default instances of the more complicated
of the standard stdcall. Plus, to add insult to injury, they’ve made use of ones. Sometimes, a developer just wants to call a single function for
C language-specific variable parameter lists! This was quite lazy on their a general dialog box rather than monkey with a component. If you
part. FormatMessage shows they know how to do the same thing in a want to do any significant customization, however, you should use
more language-independent fashion, by passing an array of 32-bit val- the component. Of course, you’re also welcome to call the API func-
ues that reference values to insert in the formatted message. This cdecl tions directly, if you like.
situation, of course, makes it rather difficult to translate these functions
to Delphi, because Delphi doesn’t directly support variable parameter Browsing Magic
lists. Well, that’s what we get for messing with undocumented func- By far, the most complex of these dialog box components is
tions. To deal with this problem, the Parameters parameter is typed as TkbBrowseForFolderDialog (discussed last month). The
an open array of Pointer. An open array parameter is the closest simula- SHBrowseForFolder API function has a complicated initialization
tion available in Delphi to the concept of variable parameter lists in C. process and a callback mechanism to be implemented. Here, we pre-
Interested readers are welcome to examine the inline assembly code sent the highlights of encapsulating this function.
required to set up the cdecl call stack correctly in the source code
included with this article (see end of article for download details). At The most difficult aspect of this function for the uninitiated is that it
any rate, this is where pointers to the “extra” strings required to replace deals with the dreaded PIDL, formally known in Delphi as the
any escape codes are provided. Note that because of the mechanics of PItemIDList record type, defined in the VCL unit ShlObj. Basically, a
open arrays, you must specify at least one pointer value here. If you PIDL is the shell version of the DOS path. Every file system object can
have no escape sequences to replace, and therefore no additional strings be represented either as a PIDL or a path. In addition, many non-file
to pass, simply stick one nil value into the brackets. system objects also exist, such as Control Panel, which can’t be identified
by anything but a PIDL. Because the SHBrowseForFolder dialog box
Componentization allows you to select folders that are not part of the file system, it uses
In general, wrapping a component around a system dialog box is one PIDLs for input and output. For the purposes of this article, you may
of the simplest tasks in component development. The only dialog box consider a PIDL to be a pointer supplied by the shell that points to arbi-
component with any real complexity to it in this project is the one trary data that should not be modified in any way. Functions have been
encapsulating SHBrowseForFolder. RunFileDlg is slightly complex, as it provided in the unit kbsdPIDL that will convert a file system path to a
incorporates a notification message scheme. All the rest are quite PIDL and vice versa, assuming the PIDL in question points to a file
straightforward. In general, they consist of little more than a construc- system object. The most unusual aspect about PIDLs is that you should
tor to initialize data, some properties that correspond directly to the never free a PIDL using the usual RTL functions like FreeMem; you
parameters expected by the underlying API function, and an Execute must use only the FreePIDL function provided in kbsnPIDL.
method to invoke the dialog box at run time. Some of them, such as
ExitWindowsDialog, are so simple that they aren’t even worth a com- This leads directly into the problem of specifying the Browse dialog
ponent. We’ll just mention a few general design principles common to box’s root folder. It’s possible to limit the scope of the user’s browse by
all these dialog box components, rather than bore you with the excru- specifying a root folder lower in the hierarchy than Desktop. For
ciating details of their rather simple and repetitive implementations. example, if you specify the C:\ folder, the user will be unable to
browse anything that isn’t part of the C: drive. The tricky part is that
The efficiency-minded reader may question why we bothered overriding this root folder must be specified as a PIDL. Furthermore, the root
the constructors for many of these components. The compiler automati- folder isn’t necessarily a file system path. How do you allow the user to
cally zero-initializes all TObject descendants’ data storage upon creation, specify both file and non-file root folders in the design-time editor?
so there is no real reason to do so in a constructor; that we have done so
in many cases is unnecessary. We believe, however, that the gain in code The solution is to use two properties. One property is a new enu-
readability and ability to see the exact expected default values are well merated type, TkbsdSpecialLocation, which encapsulates the list of
worth the trivial amount of extra code. Windows API constants that correspond to various “special” folders,
such as Control Panel. These constants can be used with the
We have converted all flag types to enumerated and set types. This SHGetSpecialFolderLocation API function to obtain a PIDL to that
serves two purposes: It enables rigorous type-checking by the com- folder. By setting a property to one of these enumerated values, spe-
piler, and it prevents the user from having to remember “magic cial non-file folders can be specified without requiring the developer
numbers” for use in the design-time editor. To avoid translating to type in the data for that folder’s PIDL. One of the values of
these enumerated types back and forth to the corresponding TkbsdSpecialLocation is kbsdPath. Setting this value will enable a sec-

17 May 1999 Delphi Informant


Undocumented
ond property to allow the developer to enter a specific file system function, and therefore has no Self variable to refer to. A second issue
path to use as the root folder. You can see the implementation of was making this “alien” function capable of calling the component’s
this scheme in the Execute method fragment shown in Figure 6. event dispatch methods. These are protected access, and technically
not available from outside the component. To solve this problem, we
Once this root folder problem is solved, the remaining initialization exploited a little-known feature of Delphi. Regardless of the specified
is straightforward. We simply go through the required list of para- access level, all of an object’s data members and methods are available
meters in TBrowseInfo, and translate the corresponding component to any code contained in the same unit. This allows the callback to
property values into each data member. Once TBrowseInfo is ready, directly call the TkbBrowseForFolderDialog class’ protected event dis-
we call the function. patch method. This would not have been possible if the callback was
implemented in a different unit.
While the dialog box is displayed, it’s issuing calls to a callback func-
tion that we must implement. Our implementation is shown in The fact that the callback function receives the dialog box’s handle
Figure 7. The sole purpose of this callback is to invoke the compo- allows us to cheat the system a little more. The SHBrowseForFolder
nent’s event dispatch methods, passing the necessary data to each. A function has no means of directly specifying the dialog box’s title bar
couple of items are worthy of mention. We’ve used the “user-defined” caption. However, because we can store the handle after the
parameter of the callback to pass a pointer to our own component. BFFM_INITIALIZED callback invocation, we can use the
This is important, because the callback is not an object member SetWindowText API function to set the dialog box’s caption to our
desired value. This is exactly what we do in the component’s Initialize
{ If the RootFolder property specifies to use the Path method, before calling the event handler — a little sly, and it works very
property as the root, fetch the PIDL for that path and
well. Having the window handle offers a great deal of leverage to a
load it into the pidlRoot member of BrowseInfo. }
if (Self.RootFolder = kbsdPath) then knowledgeable Windows programmer. In fact, you could exploit this to
begin further modify the dialog box and its various child windows if you like
BrowseInfo.pidlRoot := GetPIDLFromPath(Self.RootPath); resizing, changing text, sending messages, receiving notifications, and so
end { if }
forth. The details of these enhancements will be left as an exercise.
{ If the specified root is Desktop, just set a nil PIDL. }
else if (Self.RootFolder = kbsdDesktop) then
begin Conclusion
BrowseInfo.pidlRoot := nil; As we stated last month, these standard shell dialog boxes provide the
end { else if } ability to integrate your application with the Windows shell. You can
{ If RootFolder isn't specifying a path for the root, try
use them as drop-in solutions to common problems, saving time and
to fetch the PIDL for some special folder. If the folder
is not recognized, just leave the root PIDL nil to get a improving the polish of your applications. The component and func-
default tree. } tion wrappers give you these almost unknown function interfaces in
else convenient Delphi style, making these useful dialog boxes almost
begin
trivial to incorporate. Have fun, and write something amazing! ∆
BrowseInfo.pidlRoot :=
GetSpecialLocationPIDL(Self.RootFolder);
end; { else } The files referenced in this article are available on the Delphi Informant
Works CD located in INFORM\99\MAY \DI9905KB.
Figure 6: Setting the root folder for SHBrowseForFolder.

function BrowseForFolderCallback(DialogHandle: HWND;


MessageID: UINT; PIDL: LPARAM; ComponentPointer: LPARAM):
Integer; stdcall; Kevin J. Bluck is an independent contractor specializing in Delphi development.
var
He lives in Sacramento, CA with his lovely wife Natasha. He spends his spare
DialogComponent: TkbBrowseForFolderDialog;
begin
time chasing weather balloons and rockets as a member of JP Aerospace
{ If the value we expect to point to the dialog component (http://www.jpaerospace.com), a group striving to be the first amateur organiza-
is not nil... } tion to send a rocket into space. Kevin can be reached via e-mail at
if (ComponentPointer <> 0) then [email protected].
begin
DialogComponent :=
TkbBrowseForFolderDialog(ComponentPointer);
{ Based on which message is invoking the callback,
invoke the appropriate event dispatch method for
the referenced component. We are cheating a bit; James Holderness is a software developer specializing in C/C++ Windows
these are actually protected methods, but we can
access them from outside the class because this
applications. He also runs a Web site on undocumented functions in Windows 95
code is in the same unit. } (http://www.geocities.com/SiliconValley/4942). He is currently working for
case (MessageID) of FerretSoft LLC (http://www.ferretsoft.com), where he helps create the Ferret line
BFFM_INITIALIZED: of Internet search tools. James can be reached via e-mail at
DialogComponent.Initialize(DialogHandle); [email protected] or [email protected].
BFFM_SELCHANGED:
TkbBrowseForFolderDialog(DialogComponent).Change(
DialogHandle, PItemIDList(PIDL));
end;
end;
{ Always return 0. }
Result := 0;
end;

Figure 7: Setting the root folder for SHBrowseForFolder.

18 May 1999 Delphi Informant


Algorithms
Delphi 1-4 / Graphics

By Rod Stephens

Map Coloring
A Look at Four- and Five-color Algorithms

A map can be an incredibly useful tool, particularly if you don’t like to stop and ask
for directions. A complicated map can be quite confusing, however. For hundreds of
years, cartographers have been making maps easier to read by displaying different
countries or regions in different colors. If no two adjacent regions have the same color,
it’s much easier to see where one ends and the next begins.

In 1853, F. Guthrie speculated that it was possi- large, this can be a huge number of combina-
ble to color any map this way using, at most, tions. For example, if R is only 10, R 4 is 20,000.
four colors. It wasn’t until 1976, 123 years later, Because the program searches all these combina-
that this fact was proven by Appel and Haken. tions in a rather simple-minded manner, this is
Unfortunately, the proof is computer assisted. It called an exhaustive search.
uses a program to exhaustively examine a huge
number of graphs, and doesn’t provide an effi- The first step in any map-coloring algorithm is
cient algorithm for four-coloring a map. to convert the map into an adjacency graph. Each
node in the graph corresponds to a region on the
This article describes two algorithms for color- map (the two terms are used interchangeably in
ing maps (both are available for download; see the rest of this article). Two nodes in the graph
end of article for details). The first is a some- are connected with a link if their regions share a
what inefficient algorithm that four-colors a border in the map. Figure 1 shows a small map
map. While the algorithm is theoretically ineffi- and its corresponding adjacency graph.
cient, in practice, it is quite fast for maps with a
reasonably large number of regions. The example program Color4 uses an exhaus-
tive search to four-color maps (see Figure 2).
The second alternative is an efficient algorithm that Much of the program’s code is dedicated to
colors a map using five colors. Whether you color a loading, editing, drawing, and otherwise manip-
map with four or five colors usually doesn’t matter. ulating maps. This code is long and irrelevant
The only reason you might need to use exactly four to the map-coloring algorithm, so it’s not
colors is if you have a somewhat unusual computer described here.
that can display only four colors, or if you are try-
ing to impress your friends with your algorithmic Color4 uses the TRegion class to store informa-
prowess. In these cases, use the inefficient four- tion about regions. The code for this class is
coloring algorithm, and be patient. shown in Figure 3, with region loading, editing,
and other irrelevant routines omitted. TRegion’s
Exhausting Work points array stores the points that define the
One way to four-color a map is to simply exam- region’s borders. The program uses this array to
ine all the possible combi- draw the region. It also compares the points in
nations of colors for two regions’ borders to see if the regions are adja-
the regions until cent. The neighbors TList contains a list of the
you find a com- adjacent regions. This list gives the links between
bination that the nodes in the adjacency graph. The color_number
works. If there variable holds the index of the color for the region.
are R regions on When the algorithm is finished, this will be a num-
the map, there ber between 1 and 4.
are R 4 possible
combinations of As far as the map-coloring algorithm is con-
Figure 1: A map and its adjacency graph. colors. If R is cerned, TRegion provides only two interesting

19 May 1999 Delphi Informant


Algorithms
methods: HasLine and CheckNeighbor. HasLine returns True if regions are neighbors, so the procedure adds the two regions to
the region’s border contains a specific line segment. each other’s neighbor list.
CheckNeighbor uses HasLine to see if a specified region is adja-
cent to this region. For each border segment in this region, The program’s TMapForm class includes only two interesting class vari-
CheckNeighbor calls the other region’s HasLine function to see if ables: regions and colors. The variable regions is a TList object that holds a
the regions share the segment. If HasLine ever returns True, the list of all the regions on the map. The colors array is an array of color
values that the program uses to color the regions. The value colors[0] is a
background color used for regions that haven’t yet been assigned colors.
For example, when you first load a map, all the regions have this color.

TMapForm has three routines that deal with map coloring:


AssignColors, FindNeighbors, and AssignOneColor.

When the user invokes the Color Nodes command from the Color
menu, the program invokes the AssignColors procedure, shown in
Figure 4. AssignColors resets the regions’ colors and clears their
neighbor lists. It then calls FindNeighbors to find the regions’ cur-
rent neighbors. AssignColors then assigns an arbitrary color to node
0 in the graph. It doesn’t matter which color the program uses for
this first node. The color helps determine the colors of the other
nodes, but a solution is possible no matter what color is used first.

Figure 2: The example program Color4 uses an exhaustive The procedure then calls function AssignOneColor, passing it the
search to four-color maps. parameter 1. That tells AssignOneColor to begin assigning colors
type
// Array of points. x1 := points^[1].X;
TPointArray = array [1..1000000] of TPoint; y1 := points^[1].Y;
// Pointer to array of points. for i := 2 to num_points do begin
PPointArray = ^TPointArray; x2 := points^[i].X;
y2 := points^[i].Y;
// Map region class. if ((x1=a1) and (y1=b1) and (x2=a2) and (y2=b2)) then
TRegion = class Exit;
public if ((x2=a1) and (y2=b1) and (x1=a2) and (y1=b2)) then
// # points on the region's border. Exit;
num_points : Integer;
// The points on the region's border. x1 := x2;
points : PPointArray; y1 := y2;
// List of neighboring TRegions. end;
neighbors : TList;
// Region number starting with 0. // We didn't find the line.
number : Integer; Result := False;
// Color number for the region. end;
color_number : Integer;
// See if this region is adjacent to the indicated one.
constructor Create; virtual; // If so, add the regions to each other's adjacency lists.
... procedure TRegion.CheckNeighbor(rgn : TRegion);
// Region loading, editing, etc. declarations omitted. var
i, x1, y1, x2, y2 : Integer;
function HasLine(a1, b1, a2, b2: Integer): Boolean; begin
procedure CheckNeighbor(rgn: TRegion); x1 := points^[1].X;
procedure AddNeighbor(nbr: TRegion); y1 := points^[1].Y;
end; for i := 2 to num_points do begin
x2 := points^[i].X;
// Set some defaults. y2 := points^[i].Y;
constructor TRegion.Create; if (rgn.HasLine(x1, y1, x2, y2)) then
begin inherited Create; begin
num_points := 0; // They are neighbors. Add them to each other's
neighbors := TList.Create; // adjacency lists.
number := -1; neighbors.Add(rgn);
color_number := 0; rgn.neighbors.Add(Self);
end; Exit;
end;
// Return True if the region contains this line segment. x1 := x2;
function TRegion.HasLine(a1, b1, a2, b2: Integer): Boolean; y1 := y2;
var end;
i, x1, y1, x2, y2 : Integer; end;
begin
// Assume we will find it. // Region loading, editing, etc. routines omitted.
Result := True; ...

Figure 3: The TRegion class.

20 May 1999 Delphi Informant


Algorithms
to the nodes, starting with node 1. How that assigns colors to all has already used that color. In that case, the program cannot also
the nodes is described a little later. assign the color to this region.

The FindNeighbors procedure, shown in Figure 5, examines every If the color isn’t already used by any of the region’s neighbors,
pair of regions. It invokes one of the pair’s CheckNeighbor proce- AssignOneColor assigns that color to this region. It then recursively calls
dures to see if the two regions are neighbors. If they are, itself to assign a color to the region with the next index. If that call to
CheckNeighbor adds them to each other’s neighbor lists. the function returns True, the program has found a valid four-coloring,
so this call to AssignOneColor sets its return value to True and exits.
The heart of the exhaustive search is the AssignOneColor function,
shown in Figure 6. This function takes as a parameter the index of If the recursive call returns False, the program cannot find a valid color-
the next region it should consider. If that index is greater than the ing with this region having the assigned color. The function continues to
largest index of any region, all the nodes have been assigned colors try the other colors. If the function cannot find a valid coloring using
successfully. That means the current assignment of colors is a valid any of the four colors for this region, it resets the region’s color, sets its
four-coloring. Function AssignOneColor sets its return value to True return value to False, and exits. As the calls to AssignOneColor return
to indicate that it found a valid coloring, then exits. back up the call stack, they will make new color assignments for the pre-
viously colored regions, and try again to color this region. The recursion
If it has not yet found a valid coloring, the function tries to give the will continue to try color combinations until it finds one that works.
region it is considering each of the four colors in turn. For each
color, the function determines whether one of the region’s neighbors The Color4 program uses this code to four-color maps. Use the File
menu to open a map file, or draw your own map. Click the left
// Four-color the map.
procedure TMapForm.AssignColors; mouse button to start a new region or add to the current region.
var Click the right button to finish the region. To make two regions
rgn_i : Integer; neighbors, use the same end points for at least one of their edges.
rgn : TRegion;
begin // Assign a color for the node with the indicated index.
// Reset all the colors and clear the neighbor lists. // Recursively assign colors for the other nodes. Return
for rgn_i := 0 to regions.Count - 1 do begin // True if we find a valid assignment.
rgn := regions.Items[rgn_i]; function TMapForm.AssignOneColor(rgn_i: Integer): Boolean;
rgn.neighbors.Clear; var
rgn.color_number := 0; color_num, nbr_i : Integer;
end; rgn, nbr : TRegion;
// Make the adjacency lists. begin
FindNeighbors; // If rgn_i >= regions.Count, then all regions have been
// Only continue if there are regions. // assigned and we have a valid solution.
if (regions.Count > 0) then if (rgn_i >= regions.Count) then
begin begin
// Assign the first region a color. Result := True;
rgn := regions.Items[0]; Exit;
rgn.color_number := 1; end;
// Assign the other colors using an // Try each possible color for this region.
// exhaustive search. rgn := regions.Items[rgn_i];
if (not AssignOneColor(1)) then for color_num := 1 to 4 do begin
ShowMessage( // See if this color is available for this region.
'Error: Could not find a valid coloring.'); for nbr_i := 0 to rgn.neighbors.Count - 1 do begin
// See if this neighbor has used the color already.
end;
nbr := rgn.neighbors.Items[nbr_i];
end;
if (nbr.color_number = color_num) then Break;
Figure 4: The AssignColors procedure four-colors the map. end;
// See if the color is usable.
if (nbr_i >= rgn.neighbors.Count) then
// Create the adjacency lists for the regions.
begin
procedure TMapForm.FindNeighbors;
// Assign this color to the region.
var
rgn.color_number := color_num;
i1, i2 : Integer;
// Recursively assign colors to the other regions.
rgn1, rgn2 : TRegion;
if (AssignOneColor(rgn_i + 1)) then
begin
begin
// Compare each region to every other and see if they // We found a valid assignment.
// are adjacent. Result := True;
for i1 := 0 to regions.Count - 2 do begin Exit;
rgn1 := regions.Items[i1]; end;
for i2 := i1 + 1 to regions.Count - 1 do begin end;
rgn2 := regions.Items[i2]; // If everything worked and we found a complete
rgn1.CheckNeighbor(rgn2); // assignment, we have already exited. Otherwise
end; // we continue trying other colors.
end; end;
// Blank our color so we can try again later.
// Display the neighbor lists if desired. rgn.color_number := 0;
if (mnuShowNeighborLists.Checked) then // We found no valid assignments.
ShowNeighborLists; Result := False;
end; end;

Figure 5: The FindNeighbors procedure finds the neighbors for Figure 6: The AssignOneColor procedure recursively assigns
each region. colors to regions until it finds a valid four-coloring.

21 May 1999 Delphi Informant


Algorithms

A
X AB
B

Figure 8: Combining nodes in an adjacency graph.

shown that there is a node X with exactly five neighbors, where two
neighbors, A and B, are not neighbors of each other, and each has at
most seven neighbors of its own. For example, the graph on the left
Figure 7: The example program Color5 five-colors maps. in Figure 8 shows such nodes A, X, and B. Node X has five neigh-
bors. Its neighbors A and B are not neighbors of each other, and
they each have no more than seven neighbors.
When you have loaded or created a map, select the Color Nodes
command from the Color menu, or press 9. Because nodes A and B are not neighbors, the program can assign
them the same color. To continue processing the graph, the program
Planar Postulates removes node X and combines nodes A and B into a single node. It
The four-coloring algorithm used by Color4 exhaustively examines color then continues coloring the remaining nodes. Figure 8 shows this
combinations until it finds one that works. For reasonably small prob- combination process. Notice that nodes that were neighbors of both
lems, this algorithm is fast, and the program has no trouble. If a map nodes X and node A or B have fewer neighbors than they did before.
contains many regions, however, exhaustive search can be impractical. Now there are some nodes with fewer than five neighbors, so the pro-
gram can use the first observation to continue processing the graph.
If the map contains R regions, there are R 4 possible color combinations.
If R is 1000, R 4 is one trillion. If your computer can examine one mil- When it has finished processing the smaller graph, the program
lion combinations per second, it will take more than 11 days to search restores nodes X, A, and B. It gives nodes A and B the color it gave
them all. The program will probably find a valid coloring before it the combined node in the smaller graph. Because node X has exactly
searches all the combinations, but there is no guarantee of quick success. five neighbors, and nodes A and B have the same color, X’s neigh-
In cases like this, you can use the five-coloring algorithm demonstrated bors can have used at most four of the five colors available. Pick one
by the Color5 program, shown in Figure 7. This program doesn’t always of the remaining colors and assign it to node X.
find a four-coloring, but it’s faster for very large maps.
Five-coloring
In many ways, the example program Color5 is similar to the example The example program Color5 uses these two facts to five-color maps.
program Color4; both use the same TRegion class and the same code As it removes nodes from the graph, it places information about the
to load and manipulate maps. Their differences are in how they find nodes on a stack. When the graph is empty, the program removes the
map colorings. Color5 uses two key facts about planar graphs to pro- items from the stack in last-in-first-out (LIFO) order, using the infor-
duce the five-coloring. A graph is planar if it can be drawn in a flat mation to assign colors to the nodes. In English, the algorithm is:
plane without any links crossing each other. Adjacency graphs, such
as the one shown in Figure 1, are always planar. 1) While the graph is not empty, do:
a. If there is a node N with fewer than 5 neighbors, then:
Fact 1 i) Add node N and its current neighbor list to the stack.
The first useful fact about planar graphs is that nodes with fewer ii) Remove node N from the graph.
than five neighbors are easy to color. First, remove the node from b. Else:
the graph and color the remaining nodes. When they are colored, i) Find a node X with exactly five neighbors, two of which (A
restore the removed node and look at its neighbors. Because the and B) are not adjacent and have at most seven neighbors.
node has fewer than five neighbors, the neighbors cannot have used ii) Add node N and its current neighbor list to the stack.
up all five colors. Pick an unused color and assign it to the node. iii) Remove node N from the graph.
iv) Add nodes A and B to the stack.
This fact alone would be enough to color the entire graph if every node v) Combine nodes A and B by adding node B’s neighbors to
had fewer than five neighbors. In fact, removing a node reduces the node A’s neighbor list.
number of neighbors of each of its neighbors, so some of them may vi) Remove node B from the graph.
now have fewer than five neighbors. The program can continue like this 2) While the stack is not empty, do:
indefinitely unless it eventually reaches a state where every node has at a. If the next pair of objects on the stack is a node and its neigh-
least five neighbors. For example, every node in the graph shown in bor list saved in step 1.a.i or step 1.b.ii, then:
Figure 1 has exactly five neighbors. i) Examine the node’s neighbors and assign the node a color
that is not used by its neighbors.
Fact 2 b. Else (the next pair contains two nodes A and B saved in step
The second fact about planar graphs is useful when every node in 1.b.iv):
the graph has at least five neighbors. When that is the case, it can be i) Assign node B the same color already assigned to node A.

22 May 1999 Delphi Informant


Algorithms
The main difference between Color4 and Color5 is the AssignColors size. Because it produces a slightly better result and is much simpler than
procedure, shown in Listing One (beginning on this page). This ver- the other algorithm, exhaustive search is usually a better choice. ∆
sion of the procedure starts by creating two TList objects. The
orig_regions list will contain a copy of the original region list. The The files referenced in this article are available on the Delphi Informant
procedure uses this list to restore the graph after it has torn it apart Works CD located in INFORM\99\MAY \DI9905RS.
during the color calculations. The second TList object is the stack
object. The program copies the region list into orig_regions, resets
each region’s color, and empties each region’s neighbor list. It then
calls procedure FindNeighbors to create the new neighbor lists. Rod Stephens is the author of several books, including Ready-to-Run Delphi 3.0
Algorithms [John Wiley & Sons, 1998]. You can reach him at RodStephens@
While the regions list holds at least one region, the program delphi-helper.com, or see what else he’s up to at http://www.delphi-helper.com.
removes a node from the graph. It first searches for a node with
fewer than five neighbors. If it finds one, it adds the node and its
neighbor list to the stack. It then calls the region’s
RemoveFromGraph procedure to remove the node from the neighbor
lists of its neighbors. The program leaves the node’s neighbor list
alone so it will later be able to find the node’s neighbors. Begin Listing One — AssignColors
// Five-color the map.
If the program cannot find a node with fewer than five neighbors, it procedure TMapForm.AssignColors;
calls the FindNonAdjacent function for regions until it finds a node with var
five neighbors, two of which are non-adjacent with at most seven neigh- orig_regions, stack, nbrs : TList;
i, rgn_num, n1_num : Integer;
bors of their own. It saves that region and its neighbor list on the stack, rgn, n1, n2 : TRegion;
and removes the region from the graph. It then calls the AssociateWith color_used : array [1..5] of Boolean;
procedure to associate one of the neighbor nodes with the other. It adds obj : TObject;
both neighbors to the stack, and removes the first from the graph. begin
// Prepare the lists we need.
orig_regions := TList.Create;
Finally, when the region list is empty, the program empties the stack. stack := TList.Create;
When it finds a node and its neighbor list on the stack, the program // Save a copy of the region list because we will
assigns the node a color not already taken by its neighbors. When it // mess it up. Also reset the region's colors and
finds two nodes, it assigns the second node the same color as the first // clear their adjacency lists.
for i := 0 to regions.Count - 1 do begin
node. AssignColors finishes by restoring the original region list. rgn := regions.Items[i];
orig_regions.Add(rgn);
The last new pieces of Color5 are support routines provided by the rgn.color_number := 0;
TRegion class, shown in Listing Two (beginning on page 24). The rgn.neighbors.Clear;
end;
RemoveFromGraph procedure searches a node’s neighbors and // Make the adjacency lists.
removes the node from the neighbors’ neighbor lists. The FindNeighbors;
FindNonAdjacents function searches a node’s neighbors for two non- // Push regions onto the stack.
adjacent neighbors that have at most seven neighbors. The function while (regions.Count > 0) do begin
// Look for a region with degree < 5.
returns True if it finds two such neighbors. rgn := nil;
for rgn_num := 0 to regions.Count - 1 do begin
The AddNeighbor procedure checks whether a node is already the neigh- rgn := regions.Items[rgn_num];
bor of another node. If it isn’t, the routine adds each node to the other’s if (rgn.neighbors.Count < 5) then Break;
end;
neighbor list. Finally, the AssociateWith procedure associates one node // If we found node with fewer than 5 neighbors,
with another. For each neighbor in the first node’s neighbor list, the rou- // add it and its neighbor list to the stack.
tine uses AddNeighbor to add the second node to the neighbor’s list. This if (rgn_num < regions.Count) then
merges the two nodes in the graph, as shown in Figure 8. The example begin
// Push rgn and its neighbor list onto the stack.
program Color5 uses this code to five-color maps. The interface is very stack.Add(rgn);
similar to that of the example program Color4. Load or create a map, stack.Add(rgn.Neighbors);
and press 9 to five-color it. // Remove rgn from the graph.
rgn.RemoveFromGraph;
regions.Delete(rgn_num);
Conclusion end
These two algorithms are useful tools for any amateur cartographer. else
The five-coloring algorithm used by Color5 is quite fast. At each begin
step, it removes one node from the graph, so if the graph contains N // There is no node with degree < 5. Search
// for one with degree = 5 and 2 non-adjacent
nodes, the program can only run for N steps. The steps are some- // nodes n1 and n2 with degree <= 7.
what complicated, but they are far shorter than the steps that may be for rgn_num := 0 to regions.Count - 1 do begin
needed by an exhaustive search. rgn := regions.Items[rgn_num];
if (rgn.FindNonAdjacents(n1, n2)) then Break;
end;
Eventually, the five-color algorithm empties the graph, and can use the // If we still did not find one, something
information in its stack to color the nodes one at a time. Even for enor- // is wrong.
mous maps, this is fast, and the algorithm can finish in a reasonable if (rgn_num >= regions.Count) then
amount of time. The exhaustive search used by Color4 can be slow for begin

large maps. In practice, however, it is quite fast for maps of reasonable

23 May 1999 Delphi Informant


Algorithms
ShowMessage('Error finding node to remove.'); // Restore the original region list.
Break; regions.Destroy;
end; regions := orig_regions;
// Save rgn and its adjacency list. end;
stack.Add(rgn);
stack.Add(rgn.neighbors);
// Remove rgn from the graph. End Listing One
rgn.RemoveFromGraph;
regions.Delete(rgn_num); Begin Listing Two — TRegion
// Associate n1 and n2.
n1.AssociateWith(n2); // Remove the node from the graph.
// Push n1 and n2 onto the stack. procedure TRegion.RemoveFromGraph;
stack.Add(n1); var
nbr_num : Integer;
stack.Add(n2);
nbr : TRegion;
// Remove n1 from the graph.
begin
n1.RemoveFromGraph;
// Remove links from neighbors to here.
// Find n1 in the region list.
for nbr_num := 0 to neighbors.Count - 1 do begin
for n1_num := 0 to regions.Count - 1 do begin
nbr := neighbors.Items[nbr_num];
n2 := regions.Items[n1_num];
nbr.RemoveNeighbor(Self);
if (n1 = n2) then
end;
begin
end;
regions.Delete(n1_num);
Break;
// Find two mutually non-adjacent neighbors with
end;
// degree <= 7, if they exist.
end;
function TRegion.FindNonAdjacents(
end; // End if we did not find a node with degree < 5.
var n1, n2: TRegion): Boolean;
end; // End while (regions.Count > 0) do.
var
n1_num, n2_num, n3_num : Integer;
// The graph is empty. Produce the coloring.
n3 : TRegion;
non_adjacent : Boolean;
// Pop regions off the stack and color them.
begin
while (stack.Count > 0) do begin
// If this node has more than 5 neighbors, it won't do.
// See if the next item in the stack is a
if (neighbors.Count > 5) then
// neighbor list or a region.
begin
obj := stack.Items[stack.Count - 1];
Result := False;
if (obj.ClassNameIs('TList')) then
Exit;
begin
end;
// Get the neighbor list. for n1_num := 0 to neighbors.Count - 2 do begin
nbrs := stack.Items[stack.Count - 1]; // See if n1 has degree <= 7.
stack.Delete(stack.Count - 1); n1 := neighbors.Items[n1_num];
// Get the corresponding region. if (n1.neighbors.Count <= 7) then
rgn := stack.Items[stack.Count - 1]; begin
stack.Delete(stack.Count - 1); // n1 has degree <= 7. Find another.
// Assign rgn a color different from those for n2_num:=n1_num+1 to neighbors.Count-1 do begin
// used by its neighbors. // See if n2 has degree <= 7.
for i := 1 To 5 do n2 := neighbors.Items[n2_num];
color_used[i] := False; if (n2.neighbors.Count <= 7) then
for n1_num := 0 to nbrs.Count - 1 do begin begin
n1 := nbrs.Items[n1_num]; // See if n1 and n2 are non-adjacent.
color_used[n1.color_number] := True; non_adjacent := True;
end; for n3_num := 0 to
// See which color is left. n1.neighbors.Count-1 do begin
for i := 1 to 5 do n3 := n1.neighbors.Items[n3_num];
if (not color_used[i]) then Break; if (n3 = n2) then
// If we did not find an unused color, begin
// something is wrong. // They are adjacent.
if (i > 5) then non_adjacent := False;
ShowMessage('Error finding color for node.') Break;
else end;
rgn.color_number := i; end;
// End if the next item in the stack // If the nodes are non-adjacent, we're done.
// is a neighbor list. if (non_adjacent) then
end begin
else Result := True;
begin Exit;
// The next item in the stack is a region. end;
// Get the region. end; // End if (n2.neighbors.Count <= 7) ...
n1 := stack.Items[stack.Count - 1]; end; // End for n2_num = n1_num + 1 to ...
stack.Delete(stack.Count - 1); end; // End if (n1.neighbors.Count <= 7) then
// Get the associated region. end; // End for n1_num = 0 to neighbors.Count - 2 do
n2 := stack.Items[stack.Count - 1]; // We did not find a usable pair of nodes.
stack.Delete(stack.Count - 1); Result := False;
// Assign n2 the same color as n1. end;
n2.color_number := n1.color_number;
end; // End if neighbor list/node ... // If this region is not yet in our neighbor list, add it
end; // End while (stack.Count > 0) do // and add us to its list.
procedure TRegion.AddNeighbor(nbr : TRegion);

24 May 1999 Delphi Informant


Algorithms
var
n1_num : Integer;
n1 : TRegion;
begin
// Examine our neighbors.
for n1_num := 0 to neighbors.Count - 1 do begin
n1 := neighbors.Items[n1_num];
// If the node is in the neighbors list, do nothing.
if (n1 = nbr) then Exit;
end;
// Update the neighbor lists.
neighbors.Add(nbr);
nbr.neighbors.Add(Self);
end;

// Associate this node with the target. Copy this node's


// neighbors into target's neighbor list.
procedure TRegion.AssociateWith(target : TRegion);
var
n1_num : Integer;
n1 : TRegion;
begin
// Examine all neighbors.
for n1_num := 0 to neighbors.Count - 1 do begin
n1 := neighbors.Items[n1_num];
if (n1 <> target) then
begin
// Add n1 to target's neighbor list and
// vice versa.
n1.AddNeighbor(target);
end;
end;
end;

End Listing Two

25 May 1999 Delphi Informant


On the ’Net
Delphi 3, 4 / Interfaces / OOP / HTML

By Keith Wood

An HTML Generator
Part I: Putting the Delphi Interface Construct to Work

In the May, 1996 issue of Delphi Informant, I introduced the THTMLWriter component in
the article “An HTML Generator.” This component allowed us to generate HTML from a
Delphi program, with the full power and flexibility that a Delphi program provides. The
approach used a single component to encapsulate the required behavior, and defined
methods to generate the HTML.

Since the release of Delphi 3, we’ve had an alter- ition of a set of methods that an object can
nate way to implement the generation of HTML express. In this article, we’ll redesign the HTML
— a more object-oriented approach. Delphi 3 generator as a set of objects, the IHTML collec-
introduced us to the interface, an abstract defin- tion, that encapsulates one or more HTML tags
(this article assumes a basic knowledge of
{ Base class that implements the IHTMLProducer interface. }
HTML). We’ll also update these objects to take
THTMLBase = class(TObject, IHTMLProducer) into account changes for HTML 4.
private
FStyle: string; Interfaces
FId: string;
An interface defines a set of methods that deter-
FTagClass: string;
FLanguage: string; mines the interactions expected of an object.
FDirection: THTMLDirection; The methods are not implemented in the inter-
FTitle: string; face (in this way, it’s similar to an abstract class),
FAccessKey: Char; but must be coded for each object that expresses
FTabIndex: THTMLNumber;
FOtherAttributes: string;
that interface. Other differences from abstract
protected classes are that interfaces can only have method
function QueryInterface(const IID: TGUID; out Obj): and property declarations, and all properties
HResult; stdcall; must be accessed through functions or proce-
function _AddRef: Integer; stdcall;
dures. Also, all attributes must be public, and
function _Release: Integer; stdcall;
property Style: string read FStyle write FStyle; interfaces can have no constructor or destructor.
property Id: string read FId write FId;
property TagClass: string read FTagClass write FTagClass; Delphi has a single inheritance model, which
property Language: string read FLanguage write FLanguage; means that each class can be derived from only
property Direction: THTMLDirection
read FDirection write FDirection;
one other class, inheriting the latter’s properties
property Title: string read FTitle write FTitle; and methods. Interfaces allow us to simulate
property AccessKey: Char multiple inheritance through an object, express-
read FAccessKey write FAccessKey; ing one or more of these sets of definitions.
property TabIndex: THTMLNumber
Like the normal class hierarchy, interfaces form
read FTabIndex write FTabIndex;
property OtherAttributes: string their own hierarchies and are all ultimately
read FOtherAttributes write FOtherAttributes; derived from IUnknown. This interface defines
function BaseAttributesAsHTML: string; the basic functionality required to discover what
public interfaces are available in an object, and to ref-
function AsHTML: string; virtual; stdcall;
end;
erence count accesses to them.

Figure 1: The THTMLBase class declaration, which is the basis of Our interface, IHTMLProducer, consists of a
the HTML-generating hierarchy. single method, AsHTML, that returns the con-

26 May 1999 Delphi Informant


On the ’Net
tents of the implementing object formatted for inclusion in an { Base container class for HTML. Any number of HTML
HTML document. Its definition is as follows: producers can be added to this container and each will
generate its HTML in turn. }
THTMLContainer = class(THTMLBase)
// The HTML producing interface; a single function that
private
// returns HTML formatted text.
lstHTML: TList;
IHTMLProducer = interface(IUnknown)
FOwnContents: Boolean;
['{ 1265C6A2-5791-11D2-A65A-0000C08699E7 }']
function GetCount: Word;
function AsHTML: string; stdcall; function GetItems(Index: Integer): TObject;
end; public
constructor Create;
destructor Destroy; override;
Positioning the cursor at the correct spot and pressing
function AsHTML: string; override; stdcall;
CSG G G enters the GUID (globally unique identifier) that function ContentsAsHTML: string;
identifies this interface. This value is required because later, we’ll procedure Add(objHTML: TObject);
be testing for the existence of this interface within an object. The procedure Clear;
property Count: Word read GetCount;
AsHTML function should be declared with the stdcall directive, property Items[Index: Integer]: TObject
because it may be called across process boundaries. read GetItems; default;
property OwnContents: Boolean
read FOwnContents write FOwnContents;
Any class that expresses this interface must define a function that end;
implements this one routine. We don’t require anything else from
that class, and don’t care what else it can or can’t do. Figure 2: The THTMLContainer class declaration, which is the
basis for HTML tags that contain other tags.
THTMLBase
The base of our HTML-generating hierarchy is THTMLBase. This
class implements the IHTMLProducer interface and declares the items currently held and to access each in turn. As an object is
basic attributes that exist in most of the HTML tags (see Figure 1). added to the internal list, it’s checked to ensure that it expresses
It’s derived directly from TObject, and we make use of the inherited the IHTMLProducer interface. An exception is raised if this isn’t
GetInterface method to implement the QueryInterface function the case.
required by the IUnknown interface.
The ContentsAsHTML method calls the AsHTML method of
Normally, interfaces are reference counted, i.e. the number of ref- each of the objects in the list in turn (hence the need to check
erences to each is tracked, and the object that implements the their type on adding), and combines the results. It’s declared as
interface is destroyed when no one can access it any longer. This protected so it’s available to subclasses without being generally
works well when you’re dealing with the objects only through their visible. The AsHTML method for this object simply calls the
interfaces. For our purposes, however, we’re creating objects, ContentsAsHTML method.
changing properties specific to them, then generating the HTML
through the interface. We control the creation and destruction of To facilitate memory management, we add another property to
the objects and don’t want the interface’s scheme to interfere with control what happens to the contained objects when the contain-
that. To this end, we implement the _AddRef and _Release func- er is cleared or destroyed. When OwnContents is True, all the
tions defined in the IUnknown interface to return a value of -1, objects in the list are freed when the list is cleared. When it’s
disabling any processing dependent on them. False, the contained objects are left alone. By default, the
OwnContents property is set to True. Because the HTML docu-
The basic HTML attributes declared in the THTMLBase class are ment itself is a container, we only need to keep a reference to it,
set up as simple properties, directly referencing internal variables. and free it when we’re finished. The document object, in turn,
We aren’t concerned with changes to these values until we actually frees all the objects it contains, removing the need to track each
generate the HTML. They’re defined as protected so they’re not individually.
externally visible, but they can be exposed by subclasses. The
BaseAttributesAsHTML method formats these values as HTML tag Generating HTML
attributes, and returns that string. Again, it’s protected so that it can The actual HTML generation is done through the AsHTML
be used only by subclasses. function, which is declared in the interface. Each subclass over-
rides this method to produce HTML appropriate to the tag it
At this stage, the AsHTML method required by the interface does encapsulates. Because the object generates all the HTML it
nothing, returning a blank string. This method is overridden by requires, there’s no need to keep track of opening and closing tags
each subclass to generate the HTML tag that it encapsulates. (with the possibility that these will become unsynchronized).

THTMLContainer As an example, let’s look at the paragraph tag. This is wrapped in


Many HTML tags, such as the paragraph and table tags, contain the THTMLParagraph class (see Figure 3). It declares two prop-
other HTML tags. Because this is common, we create a new class erties of its own, Alignment and Text, and exposes several of the
that provides this functionality. Therefore, any class derived from basic properties from THTMLBase. Its constructor allows us to
this one automatically inherits the containership abilities. create a basic paragraph that contains some text, while initializ-
ing the Alignment property to the default value.
THTMLContainer extends THTMLBase and maintains an inter-
nal list of objects that it contains (see the class declaration in THTMLParagraph subclasses THTMLContainer, which means we
Figure 2). It contains methods to add objects to the list and to can add other tags to the paragraph. All these different elements are
clear it out, as well as properties to determine the number of combined within the AsHTML function. It first specifies the tag

27 May 1999 Delphi Informant


On the ’Net
header for an HTML paragraph (<p), and follows this with the value straight text to the paragraph, and then the HTML from the tags
of the Alignment property (if not the default), and those of the basic contained within this one. We use the inherited ContentsAsHTML
attributes that have been set (through the BaseAttributesAsHTML method to generate the latter. Note that the text is passed through a
method). This completes the opening paragraph tag, denoted by >. routine that converts special characters to their HTML equivalents.
Finally, we add the closing paragraph tag, </p>.
The contents of the paragraph are added next. This comes in two
parts: First, the Text property, which provides a quick way of adding THTMLDocument
Producing an HTML document then becomes the task of the
{ An HTML paragraph. } THTMLDocument object. It has properties that allow us to specify
THTMLParagraph = class(THTMLContainer)
private
the header details for the document, including the title (mandatory),
FText: string; a base document, and a style sheet. Methods allow us to add addi-
FAlignment: THTMLAlignmentHoriz; tional tags to the header block; these can be link specifications,
public metadata, scripts, or objects.
constructor Create(sText: string); virtual;
function AsHTML: string; override; stdcall;
property Text: string read FText write FText; Additional properties provide for the setting of the document’s color
property Alignment: THTMLAlignmentHoriz scheme (although this approach is now deprecated) and/or a back-
read FAlignment write FAlignment; ground image. The THTMLDocument object is derived from
property Style; THTMLContainer, and so the body text and other content are
property Id;
property TagClass;
placed with the Add method.
property Language;
property Direction; Because of the default behavior that containers free the objects they
property Title; contain on their own destruction, we only need to keep track of the
property OtherAttributes;
document itself. Everything we add to it will be released once we
end;
free the main document. Thus, we could generate a simple docu-
{ Initialise. } ment with the code in Figure 4. The resulting page is being copied
constructor THTMLParagraph.Create(sText: string); into a memo field. The full hierarchy of objects provided by the
begin IHTML collection is shown in Figure 5.
inherited Create;
FText := sText;
FAlignment := ahDefault; Merging Documents
end; Rather than having to generate the entire document within our
Delphi program, we can have the common HTML text in an exter-
{ Return formatted HTML. }
nal file, and simply substitute for the parts that change. This is
function THTMLParagraph.AsHTML: string;
begin achieved through the THTMLStream object.
{ Check for errors.}
if (heStrictHTML4 in HTMLErrorLevel) and It takes a stream as input, and parses it for special tags to be
(Alignment <> ahDefault) then replaced. These tags are identified by starting with a pound sign
raise EHTMLError.Create(
Format(sDeprecatedAttribute, ['p']));
(#), the same convention that Delphi uses for its page producer
{ Generate HTM. } components. For each such tag found, an event is generated that
Result := '<p' + AlignHorizAttribute('align',Alignment) + allows us to specify the new value. One such tag value is built
BaseAttributesAsHTML + '>' + EncodeSpecialChars(Text) + into the object itself: the <time> tag that inserts the current date
ContentsAsHTML + '</p>'#13#10;
and time. It takes an optional format attribute that can specify
end;
the Delphi formatting string to be used. So, if we code the fol-
Figure 3: The THTMLParagraph class encapsulates an HTML lowing in the HTML document:
paragraph.
<#time format="d mmmm, yyyy hh:nn ampm">

var
htm: THTMLDocument; it might appear as:
begin
try 6 November, 1998 08:23 PM
htm := THTMLDocument.Create(
'IHTML Objects Demonstration');
As an extension to this scheme, we can include another file into the
htm.AddHeaderTag(THTMLMetadata.Create(
'Keywords', 'IHTML;Demonstration')); first one by following the pound sign with ^ (caret) and the name of
htm.AddHeaderTag(THTMLComment.Create( that file. The new file is also parsed in the same manner, allowing
'This page demonstrates the IHTML objects')); for further substitutions.
htm.Add(THTMLHeading.Create(
1, 'IHTML Objects Demonstration'));
htm.Add(THTMLParagraph.Create(
Data Tables
'Welcome to the demonstration of the IHTML objects.'));
To facilitate the generation of HTML tables from information in
memHTML.Text := htm.AsHTML; a database table, we have the THTMLDataTable object. This is
finally declared in the IHTMLDB unit, because it doesn’t encapsulate
htm.Free; one of the basic HTML tags.
end;
end;
It derives from the THTMLTable object, and uses its inherited
Figure 4: Generating a simple HTML document. abilities to generate the actual table HTML. The main new prop-

28 May 1999 Delphi Informant


On the ’Net
erty is DataSet, which allows us to indicate where to obtain the
THTMLBase data. Additional properties allow for the inclusion or exclusion of
THTMLComment a header row, as well as the specification of its color scheme. The
THTMLContainer inherited abilities provide for the usual customization of the table.
THTMLAnchor
THTMLButton The dataset attached to this object determines what data is dis-
THTMLDivision played. All the visible fields from that source are displayed in
turn. Their formatting can be controlled through the usual
THTMLDocument
mechanisms for manipulating dataset fields. Memos have their
THTMLFieldSet
entire contents added to the table, while all other fields show the
THTMLForm
DisplayText value. All fields can have a default alignment, or use
THTMLFrameSet the alignment from the field itself by setting the UseFieldAlign
THTMLHeading property to True.
THTMLImageMap
THTMLLabel Events allow for the properties of an entire row, or for each cell to
THTMLList be overridden. For cells, this includes any alteration of the displayed
THTMLListItem text itself. To ease the process of creating links within this table, we
THTMLNoScript can specify fields to be used as the hot-spot text, LinkField, and for
THTMLObject the destination, LinkTarget. These are then automatically formatted
THTMLParagraph as the table is generated.
THTMLSelectField
THTMLSelectGroup Error Reporting
THTMLTableBase Each IHTML object encapsulates an HTML tag, so it can perform
its own error checking. Before generation of the HTML, the objects
THTMLTable
check for problems, such as missing mandatory fields and deprecat-
THTMLDataTable
ed tags and/or attributes.
THTMLTableCellBase
THTMLTableDetail Setting the global variable HTMLErrorLevel determines the level of
THTMLTableHeading error checking. This value is a set of the levels of error reporting
THTMLTableColumnGroup required. The levels are heStrictHTML4, which checks for deprecat-
THTMLTableRow ed items in HTML 4, and heErrors, which reports missing mandato-
THTMLTableRowGroup ry attributes. The default value is heErrors. Any errors found are
THTMLText notified by raising an EHTMLError exception. This derives directly
THTMLFrame from Exception without adding anything new.
THTMLHorizRule
THTMLImage Delphi 4 Bonuses
THTMLImageMapArea Delphi 4 also introduced changes to the Object Pascal language.
THTMLInlineFrame These include the overloading of method declarations, and the pro-
vision of default values for parameters in method calls.
THTMLInputField
THTMLButtonField
To enhance the abilities of the IHTML objects we just described,
THTMLCheckboxField
we can make use of these new capabilities. Using conditional
THTMLFileField compiles, we can set up extensions to be used when compiled
THTMLHiddenField under Delphi 4. But first we need to be able to identify when
THTMLImageField this occurs.
THTMLPasswordField
THTMLRadioField All versions of Delphi define a standard symbol to identify that
THTMLResetField version: In Delphi 2 it’s VER90, in Delphi 3 it’s VER100, and in
THTMLSubmitField Delphi 4 it’s VER120. (Whatever happened to VER110?) Using
THTMLTextField this symbol in a conditional compiler directive allows us to target
THTMLLineBreak the enclosed code for that version. We extend the parameter lists
THTMLLink for the constructors of some of the IHTML objects under Delphi
THTMLMetadata 4, allowing the more common additional attributes to be set. By
THTMLObjectParam supplying default values for these, we don’t require the user to
enter them all, if they’re not necessary. For example, in the
THTMLScript
THTMLParagraph constructor, we add parameters for the class,
THTMLSelectOption
ID, and inline style of the paragraph:
THTMLStream
THTMLStyleSheet { $IFDEF VER120 } { Delphi 4 }
THTMLTableColumn constructor Create(sText: string; sTagClass: string = ' ';
THTMLTextareaField sId: string = ''; sStyle: string = ''); virtual;
{ $ELSE }
constructor Create(sText: string); virtual;
Figure 5: The hierarchy of IHTML objects. { $ENDIF }

29 May 1999 Delphi Informant


On the ’Net
Then, under Delphi 4, we could code any of the following: TP = class(THTMLParagraph)
public
htm.Add(THTMLParagraph.Create('A basic paragraph.')); { $IFDEF VER120 } { Delphi 4 }
htm.Add(THTMLParagraph.Create( constructor New(sText: string; sTagClass: string = '';
'A formatted paragraph.', 'format1')); sId: string = ''; sStyle: string = ''); virtual;
htm.Add(THTMLParagraph.Create( { $ELSE }
'An individually formatted paragraph.', 'format1', constructor New(sText: string); virtual;
'para1')); { $ENDIF }
htm.Add(THTMLParagraph.Create( end;
'A specially formatted paragraph.', 'format1','',
'background-color: red')); { $IFDEF VER120 } { Delphi 4 }
constructor TP.New(sText: string; sTagClass: string = '';
sId: string = ''; sStyle: string = ''); virtual;
Lazy Wrappers begin
The objects described in this article provide the functionality necessary Create(sText, sTagClass, sId, sStyle);
end;
to generate HTML from a Delphi program, but the class names can
{ $ELSE }
be lengthy, which requires more typing. A solution to this is to pro- constructor TP.New(sText: string); virtual;
duce a wrapper unit that reduces these names and makes them easier begin
to enter. Each class to be abbreviated is simply assigned to the new Create(sText);
shortened class name. Here’s an example: end;
{ $ENDIF }

type Figure 6: Subclassing THTMLParagraph.


TP = THTMLParagraph;
TH = THTMLHeading;
Lastly, we show how HTML templates can be combined with
TA = THTMLAnchor;
substituted text, or other documents, to generate new pages. An
event is attached to handle the replacement of the marked tags.
Then we would be able to add a new paragraph to an HTML docu- In each case, except for the frames example, the Delphi source
ment with the following statement: code that generated the document is available through a link at
the bottom of the created page. In the frames example, the source
htm.Add(TP.Create('A shorter paragraph call')); code appears directly in one of the frames.

To make the code even shorter, we could subclass the original class and Conclusion
replace its methods with abbreviated versions as well. For example, we The interfaces introduced in Delphi 3 allow us to employ a more
could subclass THTMLParagraph, as shown in Figure 6. The imple- object-oriented approach to the generation of HTML from a Delphi
mentation of these constructors is just a call to the longer original ver- program. Instead of the monolithic component created in the previous
sions. Now we can have the following: effort, we now have a set of smaller, integrated objects that encapsulate
specific tags within an HTML document. Properties allow us to cus-
htm.Add(TP.New('An even shorter paragraph call')); tomize the tags without forcing us to fill in lots of unnecessary parame-
ters in a method call. Furthermore, if the objects presented here don’t
Feel free to apply this technique to whichever of the IHTML work the way you would prefer, the object-oriented approach allows you
objects you desire. to extend or replace them without affecting the rest of the hierarchy.

Demonstration Using interfaces means we can add the HTML generating abilities to
The demonstration program that accompanies this article allows any object we desire, and have it interact seamlessly with the objects
you to generate five documents. (The demonstration program is previously described. These new objects can appear anywhere within
available for download; see end of article for details.) In each case, the class hierarchy and have any sort of inherited abilities. Next
the HTML page is displayed as source on the screen, and is saved month, we’ll look at more applications for this approach, including
to a file that can be opened in your browser. The name of the file programs for generating Pascal source code to HTML, converting a
is given each time. directory structure to HTML, and producing a frameset definition
document in a more visual way. ∆
The first example illustrates many of the common tags used in
HTML documents. These include headings, formatted text, lists, The files referenced in this article are available on the Delphi Informant
images, and forms. The second demonstrates the use of tables and Works CD located in INFORM\99\MAY \DI9905KW.
draws a chessboard complete with pieces (assuming that the sup-
plied graphics are located in a subdirectory called images). Next,
there is an example of a frameset document. It is based on the
Keith Wood is an analyst/programmer with CCSC, based in Atlanta. He started
example in the HTML specification and displays three frames, two
using Borland’s products with Turbo Pascal on a CP/M machine. Often working
with images and the last with the source code.
with Delphi, he has enjoyed exploring it since it first appeared. You can reach him
via e-mail at [email protected].
HTML tables can also be generated from database tables, as the
fourth example demonstrates. The contents of the Biolife table
(minus the graphic) are displayed within the browser. Note that
the generating object automatically handles memo fields. Two
events are attached to the table creation to color every second row
light blue and the length columns red.

30 May 1999 Delphi Informant


DBNavigator
Data Validation / Delphi 2, 3, 4

By Cary Jensen, Ph.D.

Delphi Database Development


Part VIII: Validating Data

F or most of the past year, this column has taken a systematic look at Delphi
database development. This month’s installment continues this series with a
look at client-side data validation.

Data validation, as defined here, involves con- data is coming from client applications in a
firming that data entered by the user is acceptable client/server environment, being inserted from
before permitting it to be posted to a database, an application server, or being imported directly
e.g. the data entered into a field (column) is with- to the database, these rules are respected. The
in an acceptable range of values, and that data has drawback to this approach is that any change to
been supplied for all required fields. It also the back end requires all data-based triggers and
involves verifying that data within a given record constraints to be redefined for the new server,
is consistent, e.g. when a payment type field indi- e.g. when your company replaces Microsoft
cates that a credit card was used, the correspond- SQL Server with Oracle.
ing credit card number field has been entered.
The advantage of storing the rules on the appli-
Overview of Data Validation cation server is that all thin-client applications
There are a number of ways that data validation using the application server will use the rules.
can be implemented. If a database server is being The drawback is that the rules are not enforced
used (e.g. InterBase, Oracle, or Microsoft SQL until the client application sends the user’s data
Server), you can define constraints and triggers updates. This may occur after the user has made
on the server. Server-based validation ensures numerous inserts, updates, or deletions. If the
that all applications storing information on the user has repeatedly violated the same business
server must abide by the rules defined there. If rule, this may not be discovered until after the
the data is being accessed in an n-tier environ- user has made extensive invalid edits.
ment, such as that supported by Inprise’s
MIDAS (Multi-tier Distributed Application Client-side validation can be used in any situa-
Services) technology, these business rules can be tion, including those stand-alone applications
defined on the application server. Finally, it’s that don’t involve a database server or an appli-
possible to define the data validation rules on cation server. Furthermore, client-side validation
the individual client applications. can be applied without involving a network
round trip. The drawback to client-side valida-
Each of these approaches has its advantages and tion is that it must be repeated for each client
disadvantages. For example, placing business application. This introduces the potential for
rules on the database (whether it’s a remote inconsistent application of business rules.
database or a local one) has the advantage that Likewise, client-side validation involves binding
the rules are applied regardless of how the data the user interface to business logic, an associa-
is being added to the database. Whether the tion many developers want to avoid.

31 May 1999 Delphi Informant


DBNavigator
From a maintenance standpoint, it’s ideal when all validation can In addition to simply rejecting or accepting characters, edit masks
be placed in only one of these three layers. In reality, though, can also be used to perform run-time case conversions of character
effective validation often involves placing it in several layers. For data. For example, using an edit mask, you can ensure that a partic-
example, although a majority of business rules may be applied at ular sequence of characters is accepted as upper-case characters, even
an application server layer, it might be desirable to create some val- if the user entered them in lower case.
idation on the client to decrease network traffic. Likewise, a pri-
mary key on a remote database table has the effect of reinforcing To create an edit mask, you must either instantiate TField descen-
record uniqueness — regardless of rules defined in other layers. dants for your fields at design time (by right-clicking your DataSet
component and using the Fields Editor to instantiate the fields), or
Writing triggers and constraints on the database server is a you must assign the EditMask property of a TField at run time
server-specific topic. For information on applying business rules (using either a DataSet’s Fields property or FieldByName method).
at this layer, refer to your database documentation. Writing Using design-time instantiated fields is the easiest, because it permits
business rules for the application server layer is a MIDAS the property to be configured at design time.
topic. You can find some discussion of these issues in Bill
Todd’s article “Delphi 4 Multi-tier Techniques” in the January, The following is a simple edit mask:
1999 issue of Delphi Informant, as well as the online Help.
The remainder of this article focuses on the application of client- >LL<
side validation.
The > and < parts of this mask convert the entered characters into
Validating Data on the Client upper case, while the two LL characters require that exactly two
Client-side validation is that which is defined in your Delphi letters be entered. This mask is useful for applications requiring
application with which the user interacts. In general, there are the entry of a US state name abbreviation. While this technique
four types of client-side validation: keystroke-level, field-level, doesn’t ensure a valid state abbreviation is entered, it does ensure
record-level, and database-level. Keystroke-level validation it’s in the correct form.
involves accepting or rejecting individual keystrokes. Field-level
validation is applied as each field is entered into a record. Field-level Validation
Record-level validation occurs when an individual record is Field-level validation involves evaluating the contents of an indi-
being posted. Database-level validation is applied to sets of vidual field as its value is being updated in the underlying record
records as they are being applied to the underlying database. buffer. The record buffer is a holding area that stores an image of
Each of these types of validation is introduced separately in the a record while it’s being edited. It allows the user to modify a
following sections. record in memory without affecting the underlying database as
each field (column) is updated.
Keystroke-level Validation
Keystroke-level validation involves evaluating each character as If a user enters invalid data into a field for which field-level vali-
it is entered into a field, and the rejection of those characters dation is defined, they are prohibited from leaving the field until
that are invalid. There are two ways to apply keystroke-level the data is corrected. While not as intrusive as keystroke-level
validation: validation, field-level validation tends to interrupt the flow of the
Write an event handler that executes after each character is user’s work.
entered, and raise an exception (silent or otherwise) when an
invalid character is encountered. There is only one way to provide field-level validation with
Use the EditMask property of a TField. Delphi. This technique involves adding an OnValidate event han-
dler to the TField associated with the field for which validation is
Writing an event handler to evaluate every keystroke entered by needed. From within this event handler, you evaluate the contents
the user is the most intrusive of all data validation, and is there- of the field. If your code determines the value is invalid, you raise
fore the least often used. This type of validation is usually an exception. Raising an exception within an OnValidate event
achieved by adding an OnKeyPress event handler to an individual handler has the effect of preventing the value entered into the
DBEdit control (or similar single-field data control), and evaluat- field from being updated to the underlying buffer. Furthermore,
ing each character as it is entered. This typically means that even whatever action the user was attempting to perform is prevented,
if the field is displayed in a DBGrid (or some other multi-field whether it’s navigation to a different field in the same record, or
control), the user is not permitted to edit it there. Instead, the some action that would have otherwise resulted in the current
user is required to select from a menu or click a button to dis- record being posted. Only after the user corrects the invalid data
play a modal dialog box in which the single field control appears. are they permitted to proceed.
(A modal dialog box is one that must be accepted before the user
can return to the form from which the dialog box was invoked.) Field-level validation is demonstrated in the VALID project
Only after an acceptable value has been entered is the underlying (available for download; see end of article for details.) The pro-
field updated by your code. ject’s main form, shown in Figure 1, contains a table, named
Table1, that demonstrates a variety of validation techniques. All
A more common, and more easily applied form of keystroke-level fields for this table were instantiated at design time using the
validation involves the use of an edit mask. An edit mask is a Fields Editor. To display the Fields Editor, right-click the Table
pattern defined for an individual TField. At run time, Delphi component and select Fields Editor. To instantiate TField compo-
ensures that only characters the mask permits are accepted. Any nents for each field associated with the table, press CA. (In
character not conforming to the mask is automatically rejected, versions of Delphi before Delphi 4, you must right-click the
without raising an exception. Fields Editor and select Add Fields. This results in the display of

32 May 1999 Delphi Informant


DBNavigator
the Add Fields dialog box, wherein you select all
field names and press R).

When the fields have been instantiated, select the field


to which you want to add field-level validation, and
add an OnValidate event handler. The following is the
OnValidate event handler added to the Company field
from the VALID project:

procedure TForm1.Table1CustNoValidate(Sender:
TField);
begin
if CustNo.AsInteger < 1000 then
raise EInvalidCustNoException.Create(
'Customer numbers must be greater than
1000');
end;

From within this event handler, the value of the


Customer number field, CustNo, is evaluated. If the Figure 1: The main form of the VALID project.
value of this field is less than 1000, an exception is
raised. As a result, the user is prevented from entering a value that These techniques are clearly examples of record-level validation,
is not one of the acceptable values. Furthermore, because this vali- because the rules you define are only applied if the corresponding
dation is applied at the field level, the user cannot continue to record is being posted.
work with the record until this invalid value is correct.
Constraints
There is another feature of this code worth noting. Specifically, Constraints permit you to define Boolean SQL statements that are
instead of raising a generic exception, a custom exception was executed before the record is posted. If the expression evaluates to
raised. This exception was defined by the following statements, False, an internal exception is raised, preventing the record from
which appear in a type clause in the project’s main form unit: being posted.

ECustomException = class(Exception); There are two ways to place constraints. You can use the
EInvalidCustNoException = class(ECustomException); CustomConstraint string property of individual TField compo-
nents, or the Constraints TCheckConstraints property of your
In general, whenever you raise an exception in code, it’s consid- TTable or TQuery components. (TStoredProc components don’t
ered good form to raise a custom exception, i.e. one defined by have a Constraints property. Furthermore, do not confuse this
you. Doing so permits any future exception handling code you Constraints property with the TControl.Constraints TSizeConstraints
add to distinguish between exceptions raised by you and those property, which controls the size of visible components.)
generated by Delphi’s components. In this case, a class named
ECustomException is defined, and all explicitly raised exceptions The example project VALID employs the CustomConstraint prop-
are declared to descend from it. erty to ensure the Contact field isn’t left blank. The value of the
CustomConstraint property for the Table1Contact TField is
Record-level Validation “Contact IS NOT NULL.” When a record is being posted, and
Record-level validation is used to prevent a record from being one of the TField CustomConstraint SQL expressions evaluates to
posted when it contains invalid data. Unlike keystroke-level and False, an exception is raised, and the string associated with the
field-level validation, the user is not prevented from entering TField ’s ConstraintErrorMessage is displayed to the user. In this
invalid data. Indeed, a user may move freely throughout a record, example, the ConstraintErrorMessage property contains the string
entering invalid data all over the place. Before the user can post “You must enter a contact name.”
the record, however, this invalid data must be corrected. While
some may argue this is not efficient, imagine how difficult it Using the CustomConstraint property requires you either instanti-
would be for you to complete a written form, such as your ate TFields at design time, or assign this property to TFields at run
income taxes, one field at a time, with each field needing to be time. By comparison, the Constraints property for TTable and
correct before you could continue to the next. TQuery components permits you to define one or more constraints
without working with individual TFields. The Constraints property
Delphi provides three ways to create record-level validation. Two contains one or more Constraint objects, each of which defines a
involve properties, and one makes use of an event handler. Each of CustomConstraint and an ErrorMessage. (The Constraints property
these is discussed in the following sections. also permits you to import constraints defined by a remote server.
This is a useful feature for propagating server-side constraints to
Before doing so, however, a comment is in order. The first two the client side, which can reduce network traffic. Using imported
techniques, constraints and the Required property, can be applied constraints is outside the scope of this article.)
at the field level. This might lead you to treat them as field-level
validation, but doing so is incorrect. Field-level validation is You add a constraint to the Constraints property by displaying the
applied on a field-by-field basis, which occurs during navigation, Constraints property editor, shown in Figure 2. After the
but does not necessarily involve the current record being posted. Constraints property editor is displayed, click the Add New

33 May 1999 Delphi Informant


DBNavigator
value in the current record, as well as examine data in other
datasets, before deciding if the record is valid. If you determine
the record is valid, you do nothing, and permit the default post-
ing behavior to execute. If your code finds the record is invalid,
you raise an exception, which has the effect of preventing the
record from being posted.

Following is an example of code that appears in the VALID project.


This code uses a second Table component to point to the Customer
table; however, this one is sorted by Company name. If the record is
being posted in an inserted record, this code verifies that the
Figure 2: The Constraints property editor. Company name doesn’t already appear in the database. If it does, a
custom exception is raised, and an error message describing the
problem is displayed:

procedure TForm1.Table1BeforePost(DataSet: TDataSet);


begin
if Table1.State = dsInsert then
if Table2.FindKey([Table1Company.AsString]) then
raise EDuplicateCustomer.Create(
Table1Company.AsString +
' is already in the Customer table');
end;
Figure 3: The error message displayed when a required
field is left blank. This code demonstrates that you can reference multiple tables
from a BeforePost event handler. However, it’s unlikely you would
button and define a CustomConstraint and an ErrorMessage write this specific test. Company name uniqueness can be
string for the new Constraint. You can add as many constraints assured using a unique index. Also, in some databases it might be
as you like, and each constraint can reference one or more fields perfectly valid to have two companies with the same name. This
in the table or query. database would otherwise permit such a duplication because
record uniqueness is ensured through the use of a unique
In the VALID project, a single constraint is added to Table1’s Customer number.
Constraints property. The value of the CustomConstraint property of
this constraint is “(Country = ‘US’) AND NOT (State IS NULL).” Database-level Validation
When this expression returns False, the following error message is Client-side database-level validation involves caching the user’s
displayed to the user: “When Country is US you must supply a edits to two or more records, then evaluating this work before
value for the State field.” permitting it to become a permanent part of the database.
Unlike the other types of validation described here, database-
The Required Property level evaluation can involve data in multiple tables. A complete
The example of a TField.CustomConstraint property in the preceding discussion of database-level evaluation is beyond the scope of
section demonstrates how to require a user to supply a value for a this article. Therefore, this section will serve to describe the
field. However, there is an easier way of doing this. Instead, simply basic approach.
set the Required property of a TField to True. This will cause Delphi
to verify that the associated field has been assigned a value before There are two parts to database-level validation. The first is that
permitting the record to be posted. If the field is null, Delphi raises all changes, including inserts, deletions, and modifications, must
an exception and displays an error message. be cached on the client side until validation is ready to take
place. This can be accomplished in one of two ways. You can use
The Table1 Company field in the VALID project has the Required cached updates or the TClientDataSet component. Cached
property set to True. If you attempt to enter a new Customer record updates can be used by any Delphi developer using Delphi 2 or
and fail to enter a value in the Company field, the error message later. The TClientDataSet component is only available in the
shown in Figure 3 is displayed. client/server versions of Delphi 3 and 4. (I hope borland.com
will make this tremendously useful component available in all
Because setting the Required property is easier than defining a versions of Delphi with Delphi 5.)
CustomConstraint for a field, you might wonder why I introduced
Constraints first. The answer is that the error message displayed by
Delphi when a Required field is left blank is automatically gener- The second part of database-level validation is that when changes
ated. By comparison, when you define a CustomConstraint, you are applied, either from cache or from a client dataset, they are
also get to define the ConstraintErrorMessage property, permitting done without the context of a transaction. The transaction, which
you to control the text of the message displayed to the user. is applied using a TDatabase, permits all the user’s edits to be can-
celled, if necessary.
BeforePost
While constraints and the Required property are useful, the most Conclusion
flexible technique is to write a BeforePost event handler for your Client-side validation permits your code to evaluate data before
datasets. From a BeforePost event handler, you can evaluate any it’s applied to the underlying database, and to reject values that

34 May 1999 Delphi Informant


DBNavigator
are not acceptable. While not appropriate for all applications,
client-side validation nonetheless deserves a place in the reper-
toire of all Delphi database developers. ∆

The files referenced in this article are available on the Delphi Informant
Works CD located in INFORM\99\MAY \DI9905CJ.

Cary Jensen is president of Jensen Data Systems, Inc., a Houston-based database development
company. He is co-author of 17 books, including Oracle JDeveloper [Oracle Press, 1998],
JBuilder Essentials [Osborne/McGraw-Hill, 1998], and Delphi in Depth [Osborne/McGraw-Hill,
1996]. He is a Contributing Editor of Delphi Informant, and is an internationally respected
trainer of Delphi and Java. For information about Jensen Data Systems consulting or training
services, visit http://idt.net/~jdsi, or e-mail Cary at [email protected].

35 May 1999 Delphi Informant


File | New
Directions / Commentary

Delphi on the Web: Off the Beaten Path

L ast month we examined some general sites of interest to Delphi developers. This month, we’ll examine some
Delphi sites you may not know about: sites devoted to components, tools, techniques, and/or code; news sites;
and special interest sites.

Developer’s Corner Journal (http://www.dcjournal.com) is similar to Would you like to be an advocate for Delphi? Check out The
some of the Pascal sites we examined last month. However, this site Delphi Advocacy Group at http://www.tdag.org. They define them-
focuses on two Inprise tools: Delphi and C++Builder. Particularly selves as Delphi users and professionals who promote the most
rich in content, DCJ includes sections on beginners’ issues, the advanced Windows development tool.
Windows GUI, Internet programming, experts, database develop-
ment, and much more. You may recall my partner in the TAPI articles, Major Ken Kyler (see
the July, August, and September 1998 issues of Delphi Informant).
Brad Stowers’ Delphi Free Stuff (http://www.delphifreestuff.com) Ken is sponsoring new lists: a Delphi moderated list, a moderated
is just that. It includes over a dozen of Brad’s components, as well Delphi Database list, Delphi Talk, and more. You can find out how to
as those of other developers. It also includes links, experts, tips, subscribe at http://www.kyler.com. His page includes information on
and examples demonstrating advanced techniques, including work- Web page authoring, technical writing, and other topics.
ing with the Windows API. There’s an open invitation and willing-
ness to provide a home for other “freeware components that need a The Tomes of Delphi Support Site at http://www.cyberramp.
distribution point.” net/~jayres/ is a lot more than its title suggests. It does, of course,
provide a good deal of updates and information related to the
Conrad Herrmann’s DAX FAQs at http://pweb.netcom.com/ important series of books of which John Ayers (the owner of the
~cherrman/daxfaqs.htm is devoted to Delphi’s ActiveX capabilities. It site) is the principle author. But the most impressive aspect is an
includes information and code examples related to Delphi’s ActiveX exhaustive page of links to third-party tools.
Class Framework, covering Delphi 3 and 4. It covers bugs in IE4.01
and Delphi 3.02, as well as work-arounds. Advanced Delphi Programming at http://members.tripod.com/
~delphipower/index.htm is also worth visiting. It has information on
The Delphi Pages at http://www.delphipages.com is an attractive several 32-bit Delphi programming topics, such as working with shell
site with a wealth of tools. The Delphi News portion, central to extensions in Delphi; displaying the Properties page for a file, folder, or
the site, is excellent. It includes pages devoted to applications, drive; and using SHFileOperation to copy files in Delphi.
tips, components, chat, a Delphi forum, links, and more.
Last year, I discussed Project Jedi, a project to develop and make avail-
Delphi user groups can be an effective means of sharing informa- able conversions of Windows APIs otherwise unavailable to Delphi
tion and programming techniques. But what about those who don’t developers. Of course, the Project Jedi site at http://www.delphi-jedi.org
live close enough to such a group to participate? Enter the Virtual has information on this important endeavor. But the site also includes a
Delphi User Group, located at http://balticsolutions.com/vdug. good deal of additional information and links. There is an excellent
VDUG provides a number of useful services. In addition to its tutorial written by Andreas Prucha on converting C headers; it provides
monthly newsletters, it provides information on Delphi compo- a wealth of information on this difficult topic. This site also includes
nents and Internet sites. links to user groups and an interview with John Ayers.

The Search Site for Software Developers at http://developers.href.com I hope to revisit this important topic. In the meantime, I continue
is indispensable for Delphi developers. It provides a powerful means to invite your helpful input. ∆
to search many Usenet and vendor newsgroups. You can find informa-
tion on obscure topics, assessments of programming tools, and — Alan C. Moore, Ph.D.
answers to tough questions. It also provides access to files on the
Delphi Super Page and links to other top sites. Alan Moore is a Professor of Music at Kentucky State University, specializing
in music composition and music theory. He has been developing education-
Richey’s Delphi-Box at http://inner-smile.com/delphi4.htm con- related applications with the Borland languages for more than 10 years. He
tains a wealth of information and links. In addition to the has published a number of articles in various technical journals. Using
expected links to Web sites, FTP sites, and Inprise sites, there are Delphi, he specializes in writing custom components and implementing
sections devoted to less-usual topics: Delphi user groups and multimedia capabilities in applications, particularly sound and music. You
Delphi job offers. can reach Alan on the Internet at [email protected].

36 May 1999 Delphi Informant

You might also like