Delphi Informant 95 2001
Delphi Informant 95 2001
ON THE COVER
7 Objects in the Stream — Alan Ciemian Cover Art By: Doug Smith
The Delphi VCL includes powerful capabilities for storing
and loading components to and from streams, but these
capabilities are difficult to implement and are nearly 44 At Your Fingertips — David Rippy
undocumented — until now. Mr Ciemian presents a set Mr Rippy, our favorite Object Pascal trickster, returns with
of classes that provide the framework for a stream-based techniques for: changing the color of selected rows in a
persistent object library. DBGrid, executing other programs from a Delphi applica-
tion, an efficient way to play .WAV files, and a nifty
Database Desktop quick tip.
FEATURES
46 Visual Programming — Blake Versiga
18 Informant Spotlight — Douglas Horn Mr Versiga gets creative with the Delphi Outline com-
The MRU list is a standard fixture of shrink-wrapped ponent and uses it to track the lineage of — yes —
Windows applications, but is not in Delphi’s ready-made Great Pyraneese dogs. It’s an introduction to the many
bag of tricks. This is no longer a problem however, uses of the Outline component including its owner-
because Mr Horn shows us how to present users with a list draw capabilities.
of their most recently used files.
REVIEWS
23 Useless Stuff — David Faulkner
Need a break? How about an electronic poker game? This 50 ReportPrinter — Product review by Bill Todd
month, Mr Faulkner introduces a CardDeck component and Consisting of four Delphi components, Nevrona Designs’
then uses it to create a little bit of Las Vegas. And despite ReportPrinter 1.1 is a compact alternative to Delphi’s
this column’s name, you’re bound to glean one or two ReportSmith, but is it up to the task of heavy-duty report-
useful techniques. ing? Mr Todd reviews the new tool, as well as the Delphi
component included as part of Crystal Reports 4.5.
33 Sights & Sounds — Sedge Simons, Ph.D.
Need hotspots? That is, do you need to create irregularly-
shaped mouse-sensitive areas on a form? Need them to DEPARTMENTS
be invisible? No problem. Dr Simons shows us how to use 2 Editorial
Windows API regions to create unorthodox interfaces. 3 Delphi Tools
5 Newsline
38 DBNavigator — Cary Jensen, Ph.D.
When it comes to databases, locating specific records
quickly — with or without an index — is what it’s all
about. And Delphi offers a wealth of methods for the task.
Dr Jensen shares seven demonstration projects, and you’ll
want to hit CompuServe to get them all.
here was no violent reaction per se to Vince Kellen’s article “Why Do Programmers Love Delphi?”
T in our October 1995 issue, but reaction there was. Let’s start with this note from Mr Sarasalo:
Vince Kellen, number of developers who are mak- Send flames to: Dominique Léger, in any of the trade magazines, and I
I think you are right on the money ing it their #1 coding tool. Down Québec could not find anyone at Borland who
on how Delphi feels "thinner". with VB! Long live Delphi! could help me. Is Local InterBase
Thinner than VB, ObjectPAL, Access Tim Bacon (York, England) PS: I love the Delphi Informant. It’s included in the BDE distribution rights
or Actor, but no thinner than C/SDK. pages are filled with useful informa- that comes with the client (desktop)
Comparing it to ObjectPAL, how- And finally, from what may tion and helpful hints. Can’t leave version of Delphi? ...To use InterBase
ever, I think there is another now be a sovereign nation, the office without it! is clearly more pain and hassle than to
aspect: encapsulation. Object Pascal this message from Mr Léger: use Paradox tables. Yet the long-term
enables, and if you do more than Love is a strong word, and benefits of InterBase are clear. ... Are
just add features to forms, encour- Dear Editor, deeply appreciated in this there other independent software
ages good interface design between I am writing this letter in response context. Perhaps there is developers and shareware authors out
different parts of a software project. to Vince Kellen’s article “Why Do more passion at work here there who find this Local InterBase
So, in my experience, while Programmers Love Delphi?” in the than you admit? Many matter troubling? Please comment.
ObjectPAL is great for small jobs October 1995 issue of Delphi thanks in any case, and here Many thanks, Paul
and in-house productivity enhance- Informant. ... I generally agree with is Vince’ reply: (P. K. Winter, Toronto, ON)
ments, when one is working on a his arguments but I feel he missed
larger project that needs to be the most important feature of Dominique brings up a great point Yes there are. This is a com-
maintained over time, Delphi's Delphi: Delphi Is Easy. about how easy it is to understand the mon question, and now,
Object Pascal is far superior. Nothing can beat the ease of use Object Pascal language, but I have to thanks to InterBase Product
Then code maintenance with when one can type disagree with Dominique when he Marketing Manager Keith
Delphi's editor is much easier than MyWindow.Width := 100 or says that programmers aren’t looking Bigelow, I have an answer.
with ObjectPAL's editor. MyWindow.Caption := ‘My Title’. for a challenge of learning a new tool It takes the form of a news
Wilhelm Sarasalo, Pacific Software We switched from C to Delphi like Delphi. I am using the word chal- item and appears on page
because it’s easy to program, fast to lenge in a positive sense, not a nega- 10. And thank you Paul for
This message from Mr Bacon compile, and generates self-con- tive one. I have found that many (not the kind words about my
ends with some sloganeering: tained executables. Personally, I all) programmers get tired of working two favorite magazines.
prefer C syntax, but since we are in in the same environment unless there
Vince, a very competitive market, efficient is some challenge. Whether it’s learn- Thanks for reading.
I saw your article ( "101 reasons tools can’t be ignored. In fact, give ing the complexities of writing serious
why programmers prefer Delphi", or me a Delphi tool with C syntax and custom components or delving into
some such title ) and felt compelled I’m switching back ... Windows events, Delphi provides a
to respond. You dismissed the idea I don’t think programmers are smooth ramp to give programmers of
that the incredible depth and breadth looking for the challenge of learning all skill levels a challenge.
of the Object Pascal language was the a new tool like Delphi. I don’t want Vince Kellen
reason why Delphi was such a cool to fight with my knife when I’m eat- Jerry Coffey,
product. I have to disagree! ing a steak. I just want the meat cut, And in closing, I have anoth- Editor-in-Chief
All the coders at this site love nothing more. Mr Kellen is mistaken er topic I’d like to address
Delphi because of Object Pascal: it here. I believe the challenge lies in this month. CompuServe: 70304,3633
hides and simplifies the complicated the greater possibilities offered to Internet: 70304.3633@com-
stuff by default, but when you need the programmer when he is devel- Dear Jerry: puserve.com
the power routines you can switch oping an application because he can Just as I continue to enjoy the Fax: 916-686-8497
them in easily. The IDE is really easy do much more in less time. Delphi is Paradox Informant magazine, I Snail: 10519 E. Stockton
to work with, and what better exam- a sharp knife. find the Delphi Informant a gold Blvd., Ste. 142,
ple code could you get than the mine of news, ideas, and tech- Elk Grove, CA
source of the product itself? As is your logic Dominique. niques. Keep it coming! 95624
Borland has really come up with a But does his postscript With respect to Delphi, I have a con-
great product, and it shows in the betray him? cern which I have not seen addressed
By Alan Ciemian
This article presents a set of classes that provide the framework for a stream-based persistent
object library. In some ways, the classes resemble the streaming facilities of Borland’s OWL
(Object Windows Library), but they take full advantage of Delphi’s updated class model and
class registration facilities, and integrate well with the VCL.
Overview
Before getting into the details, I will present a quick survey of the class-
es, their relationships to each other, and their relationships to the VCL
hierarchy (see Figure 1).
type
TacStreamable = class(TPersistent)
protected
{ Centralized field initialization }
Figure 1: The procedure InitFields; virtual;
custom classes { Stream interface }
presented in this constructor CreateFromStream(Stream: TacObjStream);
article and their procedure SaveToStream (Stream: TacObjStream);
position in the virtual; abstract;
procedure ReadFromStream(Stream: TacObjStream);
VCL hierarchy.
virtual; abstract;
{ Property methods }
function GetAsString: string; virtual;
public
{ Constructors }
constructor Create;
constructor CreateClone(const Other: TacStreamable);
{ Properties }
class of TStrings and TacStreamable. Since it does not, property AsString: string
read GetAsString;
TacObjStringList acquires its list characteristics through anoth- end;
er form of code reuse — namely containment.
{ TacStreamable implementation }
TacObjStream is an abstract class that defines the required constructor TacStreamable.Create;
interface for streams that read and save TacStreamable objects. begin
It provides the functionality for saving and reading objects to inherited Create;
InitFields;
any TStream subclass. It’s abstract because it does not include end;
the logic for managing the actual TStream.
constructor TacStreamable.CreateClone
( const Other : TacStreamable );
TacFileObjStream is a TacObjStream subclass that implements
a file-based stream for persistent objects. In another example begin
of containment, the file stream functionality is provided by a Create;
Assign(Other);
privately managed instance of TFileStream. end;
The protected virtual method, InitFields, provides for class- Figure 2: The source listing for TacStreamable.
specific private initialization. All three constructors declared
for the TacStreamable class call this method either directly or TacStreamable subclasses must override either the
indirectly. Since TacStreamable does not contain any data TPersistent.AssignTo or the TPersistent.Assign method to
fields, the default implementation of InitFields is empty. define the copy semantic of the class.
The Create constructor creates an instance with a default CreateFromStream is a protected constructor called by
state by calling the inherited constructor and the virtual TacObjStream to create an instance from a stream. After call-
InitFields method. ing the Create constructor to initialize the instance to a
default state, it calls the ReadFromStream method to update
The CreateClone constructor creates an instance from the state from the stream.
another assignment-compatible instance. It relies on the
Create constructor to initialize the object to a default state, The public property, AsString, returns the string representa-
and the Assign method, inherited from TPersistent, to copy tion of a persistent object. The protected virtual method,
the other object’s state. CreateClone is analogous to a C++ GetAsString, provides the implementation. TacObjStringList
copy constructor. Since it depends on TPersistent.Assign, instances use the string representation as a means of refer-
encing and displaying their contained objects. By default, time, each instance of the original class is associated with an
GetAsString returns an empty string. object of a second class, to which it delegates, or forwards, all
related method calls.
TacObjStringList: Managing Persistent Objects
If your application only uses a few or a fixed number of persis- Specifically, the contained list reference determines the list behav-
tent objects, saving and reading them one by one is probably ior for TacObjStringList instances. Depending on the actual class
sufficient. For applications that create a large or indeterminate of the contained list object, individual TacObjStringList instances
number of persistent objects, the ability to manage objects in can behave as if inherited from TStringList, TListBoxStrings,
heterogeneous lists is critical. The TacObjStringList class is a base TComboBoxStrings, or any other subclass of TStrings.
class for persistent object containers. As previously discussed, the
TacObjStringList class is a subclass of TacStreamable and sup- Although the TacObjStringList class uses string lists internal-
ports streaming the entire list as a single persistent object. ly, it does not directly expose these to its clients. Items are
inserted and removed as TacStreamable objects only. The
The TacObjStringList class acquires its list characteristics value of each object’s AsString property determines its associ-
through containment. Containment is an alternative ated string. This string identifies objects in the list and visu-
means of code reuse that allows a class to use the services ally represents objects in lists associated with visible controls.
of another class by maintaining a reference to an instance
of that class. The TacObjStringList class supports object ownership and also
provides insertion and deletion notifications for its subclasses.
Containment has advantages and disadvantages as com-
pared to inheritance. To expose the methods of the con- One last note before examining the implementation: the
tained object, the class must declare its own corresponding design of TacObjStringList facilitates its use in conjunction
methods that eventually call the methods of the contained with list controls. Consider the following source:
object. The resulting advantages include:
• control over method visibility, var
ObjList : TacObjStringList;
• the ability to redefine the interface to the contained ...
object’s services, and { ListBox refers to a TListBox component placed on a form }
• the ability to modify the behavior of any of the contained ObjList := TacObjStringList.Create(ListBox.Items,False);
object’s methods.
This code creates a TacObjStringList object, ObjList, that refer-
The resulting disadvantages include: ences the string list associated with a specific list box, ListBox.
• the added function call required, and Used in this manner, ObjList also acts as a list manager for
• the inability to access the contained object’s protected ListBox. You can perform most list manipulations, including
interface. insertions, deletions, and persistent storage, directly on the
ObjList and have the results automatically reflected in the visible
Delphi includes two distinct types of list classes: the familiar list. This reduces coupling the application’s logic to the interface,
string lists (subclasses of TStrings) used by numerous VCL limiting the effects of interface changes (such as changing from
components and the more generic TList class. Each item in a a list box to a combo box) to a few lines of code.
string list consists of a string and an associated object. Each
item in a TList is a generic pointer. The implementation of TacObjStringList can logically be
divided into two parts: list management and persistence.
The TacObjStringList class uses a contained list reference of Figure 3 shows the source listing for the class interface. The
type TStrings. Using the TStrings class instead of the TList following discussion covers the major aspects of the class.
class results in a more versatile class that works well in com- Refer to the complete source for the remaining details.
bination with Delphi’s components (especially list-based
controls such as TListBox and TComboBox). TStrings is an The TacObjStringList Class
abstract class that defines the interface for all of Delphi’s Five private fields relate to list management. The FList field is a
string lists, including TStringList, TListBoxStrings, and reference to the contained list object. FOwnList is a Boolean flag
TComboBoxStrings. Since the list reference type is TStrings that specifies ownership of the list referenced by FList.
instead of a particular subclass, the TacObjStringList class FOwnObjects, another Boolean flag, specifies ownership of the
can work directly with any type of string list. objects contained in the list. In both cases, ownership means the
TacObjStringList object has responsibility for freeing the owned
Using the TStrings reference in this manner, TacObjStringList object(s). The FOnInsert and FOnDelete fields are method refer-
simulates a limited form of delegation. Delegation, in an ences for the insertion and deletion notification events.
object-oriented sense, refers to a form of per-object inheri-
tance that allows different instances of a single class to behave Public properties provide access to the TStrings object (read-
as if they were each inherited from a different class. At run- only), the count of objects in the list (read-only) and the list
type
TacObjListIndex = Integer; { Indexing into lists } public
TacObjListCount = LongInt; { For saving list count { Construction/Destruction }
to stream. } constructor Create(const Strings : TStrings;
const OwnObjects : Boolean);
type { for TacObjStringList notifications } destructor Destroy; override;
TacObjListNotifyEvent = { List object access }
procedure (Idx: TacObjListIndex) of object; function AtIndex(const Idx: TacObjListIndex):
TacStreamable;
type function AtName(const Name: string): TacStreamable;
TacObjStringList = class(TacStreamable) { Standard list methods }
private procedure BeginUpdate;
{ Reference to contained list } procedure EndUpdate;
FList : TStrings; function Add(const Obj: TacStreamable):
{ Flag for list ownership } TacObjListIndex;
FOwnList : Boolean; procedure Insert(const Idx: TacObjListIndex;
{ Flag for list item ownership } const Obj: TacStreamable);
FOwnObjects : Boolean; procedure Move(const FromIdx: TacObjListIndex;
{ Delete notification } const ToIdx: TacObjListIndex);
FOnDelete : TacObjListNotifyEvent; { Deletes delete the objects if they are owned }
{ Insert notification } procedure DeleteIdx(const Idx: TacObjListIndex);
FOnInsert : TacObjListNotifyEvent; procedure DeleteObj(const Obj: TacStreamable);
procedure ResetList(const Strings : TStrings; procedure DeleteName(const Name: string);
const OwnObjs : Boolean); procedure DeleteAll;
procedure CloneContents(const OtherList: { Removes NEVER delete the objects }
TacObjStringList); function RemoveIdx(const Idx: TacObjListIndex):
procedure FreeList; TacStreamable;
procedure FreeObjects; function RemoveObj(const Obj: TacStreamable):
{ Property access methods } TacStreamable;
function GetCount: TacObjListIndex; function RemoveName(const Name: string):
protected TacStreamable;
{ TPersistent overrides } { ObjStringList specific methods }
procedure AssignTo(Dest: TPersistent); override; procedure UpdateObjectName(const Idx: TacObjListIndex);
{ TacStreamable overrides } { Public properties }
procedure InitFields; override; property Strings: TStrings
procedure ReadFromStream(Stream: TacObjStream); read FList;
override; property Count: TacObjListIndex
procedure SaveToStream (Stream: TacObjStream); read GetCount;
override; property OwnObjects: Boolean
{ Protected properties } read FOwnObjects
property OnObjDelete: TacObjListNotifyEvent write FOwnObjects;
read FOnDelete property OwnList: Boolean
write FOnDelete; read FOwnList
property OnObjInsert: TacObjListNotifyEvent write FOwnList;
read FOnInsert end;
write FOnInsert;
and object ownership settings. Protected properties allow The Destroy destructor override frees owned objects and lists
derived classes to install handlers for the insertion and dele- before calling the inherited destructor.
tion notifications.
The public methods, AtIndex and AtName, provide access to
The private methods ResetList, CloneContents, FreeObjects, the objects in the list. AtIndex returns the object at a specific
and FreeList manage the list reference. ResetList updates the index, and AtName returns the object with a specific string
list reference and the list and object ownership flags. representation.
CloneContents rebuilds the list to duplicate the contents of
another resulting in a list containing copies of the objects The UpdateObjectName method updates an object’s string
in the source list, not references to the same objects. representation from its AsString property. This method
FreeObjects and FreeList are helper methods for freeing all should be called for any object in the list that is modified in
the objects in the list and the list itself. a way that could potentially affect its string representation.
The constructor, Create, takes two parameters — a Most of the remaining public methods are concerned with
TStrings object reference and an object ownership flag. The list manipulation. Most of these map directly or indirectly
TStrings reference identifies an existing list object to use as to methods of the TStrings class. In some cases, the
the list delegate. If the reference is nil, a new TStringList TacObjStringList methods augment the TStrings behavior
object is created to serve as the list delegate. The object with additional error checking and support for notifications
ownership flag determines whether or not the list owns the and object ownership. For details, refer to the complete
contained objects. source code listing.
Implementing TacObjStringList
The implementation of TacObjStringList is also a good exam-
ple of building a persistent class. There are five requirements
for a fully functional persistent class:
1) It must be a descendant of TacStreamable.
2) It must override the AssignTo method to support assign-
ment and copy construction.
3) It must override the InitFields method to perform class
specific initialization.
4) It must override the SaveToStream and ReadFromStream
methods.
5) It must be registered if objects will be created directly from it.
Figure 5: TacObjStringList assignments.
In addition, the class should override the GetAsString method
to support string-based list access or display in list controls. course, subclasses can easily override this behavior as needed.
For valid assignments, the destination object is cast to a
The following discussion approaches these requirements in TacObjStringList and the CloneContents method is called to
the context of the TacObjStringList class. As previously stated, duplicate the source list.
TacObjStringList is a subclass of TacStreamable.
The InitFields implementation calls the inherited version
Figure 4 shows the source listing of the AssignTo method. and then initializes the private list reference to nil and the
First, you must verify that the instance is not being assigned ownership flags to False.
to itself. This is more than an efficiency issue. Allowing
AssignTo to proceed with itself as the destination object would The SaveToStream method saves the count of the objects in
result in memory leaks and nasty general protection faults. the list to the stream and then iterates through the list, saving
The next concern is with the class of the destination object. each object with a call to the stream’s SaveObject method. The
At first glance, it might appear that the type checking per- ReadFromStream method reverses this process. It reads the
formed by the as operator when casting the destination from count of the objects in the list from the stream and then cre-
TPersistent to TacObjStringList would be sufficient to guaran- ates each object with a call to the stream’s ReadObject method.
tee assignment compatibility. However, the as operator only
requires that the destination object’s class be TacObjStringList TacObjStringList does not override the GetAsString method.
or one of its subclasses. Considering the source object’s class It’s unlikely that string representations of TacObjStringList
may also be TacObjStringList or one of its subclasses, this objects will be required.
would allow all the assignments (see Figure 5).
That’s it. TacObjStringList is now a fully functional persistent
Unfortunately, in cases (A) and (B), the assignment may not be class. I have intentionally not yet registered the class. Registration
proper. In both cases, the destination class may include fields is required only for classes used to create actual objects. Because
and behaviors unknown to the source class. Therefore, the any given application may or may not create direct instances of
implementation of TacObjStringList performs an explicit check TacObjStringList, the application is responsible for registration.
to restrict assignments to the type indicated by case (C). Of
TacObjStream: Building a Home for Persistent Objects
procedure TacObjStringList.AssignTo(Dest : TPersistent); TacObjStream is the base class for persistent object streams.
var Before examining its particulars, it’s important to understand
DestStringList : TacObjStringList;
begin
how TacObjStream organizes data in the stream. Conceptually,
if ( Dest = self ) then Exit; the stream consists of four areas:
1) the Stream Header
if ( (Dest is TacObjStringList) and
(Self is Dest.ClassType) ) then
2) the User Header
begin { Assigning to same or superclass } 3) the Object Data
DestStringList := ( Dest as TacObjStringList ); 4) the Class Table
DestStringList.ResetList(DestStringList.FList,True);
DestStringList.CloneContents(self);
end Figure 6 illustrates this format. The stream and user head-
else ers are fixed length areas. More specifically, the size of the
begin { TPersistent will process error }
inherited AssignTo(Dest);
header for any particular stream must remain constant for
end; the life of the stream, although it may vary from stream to
end; stream. The stream header begins every stream created
from the TacObjStream class. It includes a signature and
Figure 4: The TacObjStringList.AssignTo procedure. version number for identifying the stream, and an offset to
the class table. TacObjStream The private methods, SaveStreamHeader and ReadStreamHeader,
subclasses can define and man- manage the stream header. After processing the standard stream
age the optional user header header, these methods call the SaveHeader and ReadHeader
area as needed. methods, respectively. Subclasses override these protected virtual
methods to define and manage a user header. By default, their
The object data area contains implementation is empty.
the persistent object data. A 2-
byte class ID, written by The class table is managed by the following private meth-
TacObjStream, prefixes each ods: PrepareClassTable, SaveClassTable, ReadClassTable, and
object’s data. The class ID iden- AddClassRef. The organization of the class table is depen-
tifies the object’s class type indi- dent on the stream’s access mode. The PrepareClassTable
rectly through the class table. method is responsible for properly configuring the class
The class table stores the class table prior to its use.
type information for each class
represented in the stream. The For input mode streams, the class table is organized as an
class table consists of a counter, unsorted string list of class names ordered by class ID. The pri-
specifying the number of class vate ReadClassTable method reads each entry from the stream
entries, followed by the class and inserts the class name into the class table at a position
Figure 6: The TacObjStream
entries in the form of <class determined by the class ID. It also calls Delphi’s FindClass pro-
file format. name, class id> pairs. cedure to determine the class type reference for the class name
and stores that in the entry’s corresponding Objects property.
The initial implementation of TacObjStream did not include a
class table. Instead of writing a class ID before each object, it For output mode streams, the class table is organized as a
wrote the class name. Delphi uses a similar procedure when sav- sorted string list of class names, each associated with an inte-
ing components. However, since each class name can occupy up ger class ID. The table builds incrementally as objects are
to 64 bytes (they are 63 character strings), this scheme can be saved to the stream. Each saved object results in a call to the
space inefficient, especially for streams containing large numbers stream’s AddClassRef method. A call to the object’s ClassName
of small objects. Performance may also suffer, as it requires a method returns the class name. If the class name is already in
class type lookup for each object. the table, AddClassRef returns the previously assigned class
ID. If the class name is not in the table, AddClassRef creates a
The class table implementation avoids these problems and new class table entry, assigns it the next class ID, and returns
is more efficient in almost all cases. The class table stores the class ID. The private method, SaveClassTable, appends the
each referenced class name (just once) along with its asso- class table to the stream before the stream is closed.
ciated class ID. As a result, the per-object overhead
decreases from a possible 64 bytes to a constant 2 bytes. It’s important to realize that ClassName always returns the
Also, reading from the stream requires a class type lookup class name of the object’s actual class, not of the reference
only once per class instead of once per object. Although variable’s class. Specifically, even though AddClassRef sees all
the class table does add complexity, I think the benefits objects passed to it as TacStreamable references, if an object of
more than justify its use. class TMyStreamableObject (a subclass of TacStreamable) is
passed, the call to ClassName returns “TMyStreamableObject,”
Figure 7 shows the source listing for the TacObjStream class not “TacStreamable.”
interface. TacObjStream includes three private fields. The
FMode field stores the stream’s current access mode. It’s of For append mode streams, the class table is initialized by
type TacObjStreamMode. TacObjStream provides three access ReadClassTable, but must be organized as if in output mode.
modes: input, output, and append. Input mode is for reading This ensures the class table is properly updated if new classes
some or all the objects in a stream, output mode is for saving of objects are appended to the stream.
an entire stream, and append mode is for adding objects to
the end of an existing stream. As previously stated, TacObjStream does not implement the
physical stream management. This is left to its subclasses.
The remaining two fields relate to the stream’s on disk for- Instead, it performs all stream operations through a reference
mat. The FHeader field contains the stream header. The to Delphi’s abstract stream class TStream. This is similar to
FClassTable field is a reference to the TStringList object that using a TStrings reference in TacObjStringList; it makes it pos-
maintains the in-memory class table. sible to transparently support multiple types of streams.
The Create constructor merely initializes the private fields. TacObjStream declares three abstract protected methods that
The Destroy destructor override ensures the stream is closed define its required internal stream interface. Subclasses must
and the class table string list is freed. override these methods to produce a fully working stream
type
TacObjStream = class(TObject)
{ Only 63 chars of identifiers are significant }
private
TacStreamableClassName = string[63];
FMode : TacObjStreamMode; { Access mode }
{ Identifies class of streamed objects }
FHeader : TacObjStreamHeader;{ Stream header }
TacStreamableClassId = Integer;
FClassTable : TStringList; { In-memory class
{ Index into class list }
lookup table }
TacStreamableClassIdx = Integer;
{ Stream header management }
procedure SaveStreamHeader;
type
procedure ReadStreamHeader;
TacObjStreamMode =
{ Class table management }
(
procedure PrepareClassTable(const Mode:
osmClosed, { Stream not open }
TacObjStreamMode);
osmInput, { For reading only }
procedure SaveClassTable;
osmOutput, { For writing only, starts with
procedure ReadClassTable;
empty stream }
function AddClassRef(const Obj: TacStreamable):
osmAppend { For writing only, starts with
TacStreamableClassId;
current contents }
protected
);
{ Abstract internal stream interface }
TacObjStreamModes = set of TacObjStreamMode;
function GetStream: TStream; virtual; abstract;
procedure OpenStream(const Mode: TacObjStreamMode);
{ Standard stream header. Starts every TacObjStream. }
virtual; abstract;
type
procedure CloseStream; virtual; abstract;
TacObjStreamHeader = record
{ Error handling }
Signature : array[0..7] of Char;
procedure ValidateStreamMode(const Modes:
Version : LongInt;
TacObjStreamModes);
ClassTableOffset : LongInt;
procedure ObjStreamError(Exc: Exception); virtual;
end;
{ Placeholders for user added headers }
procedure SaveHeader; virtual;
const
procedure ReadHeader; virtual;
DefaultObjStreamHeader : TacObjStreamHeader =
public
(
{ Construction/Destruction }
Signature : 'ACSTREAM';
constructor Create;
Version : $00000000;
destructor Destroy; override;
ClassTableOffset : $00000000
{ Opening and closing stream }
);
procedure OpenForInput;
procedure OpenForOutput;
type { TacObjStream exception classes }
procedure OpenForAppend;
EacObjStream = class(Exception)
procedure Close;
{ Base class for TacObjStream Exceptions }
{ Save and Read methods for streaming objects }
end;
procedure SaveObject(const Obj: TacStreamable);
function ReadObject(const Obj: TacStreamable):
EacObjStreamInvalid = class(EacObjStream)
TacStreamable;
{ Unexpected stream format, header unrecognized }
{ Methods used by objects to read/write their data }
end;
procedure SaveBuffer(const Buffer; Count: Longint);
procedure ReadBuffer(var Buffer; Count: Longint);
EacObjStreamWrongMode = class(EacObjStream)
procedure SaveCStr(const CStr: PChar);
{ Stream is in the wrong mode for requested operation }
function ReadCStr: PChar;
end;
end;
type
class for TacStreamable objects. The GetStream method only ReadFromStream methods of TacStreamable subclasses.
needs to return a reference to the actual stream object. This Their implementation is trivial. The calls are simply for-
reference provides access to the actual stream for data inser- warded to the TStream object returned by GetStream.
tion and extraction. The other two required methods, SaveCStr and ReadCStr are helper functions that simplify
OpenStream and CloseStream, impose a standard protocol for the handling of C-style, null-terminated strings.
opening and closing the stream that is not necessarily linked
to the stream’s creation and destruction. This minimizes the The most interesting methods are SaveObject and ReadObject.
limitations resulting from TacObjStream’s inability to sup- SaveObject writes a persistent object to the stream, and
port simultaneous input and output on an open stream. ReadObject recreates a persistent object from the stream. Because
of the previously discussed infrastructure, the implementation of
The public interface of TacObjStream includes methods for SaveObject and ReadObject is relatively simple (see Figure 8).
opening and closing the stream. The OpenForInput,
OpenForOutput, and OpenForAppend open the stream and SaveObject saves the class ID, returned by a call to AddClassRef,
prepare it for reading, saving, and appending, respectively. and then saves the object with a call to its SaveToStream method.
The Close method closes any open stream.
ReadObject reads the class ID and determines the associated
The SaveBuffer and ReadBuffer methods are provided class type by using the class ID as an index into the class table.
mainly for implementing the SaveToStream and Since the class type reference is stored as a TObject reference, it
{ Create a new object of the proper class from the The class declares two private fields, FFilename and
stream data }
NewObj := ObjType.CreateFromStream(self);
FFileStream. FFilename stores the name of the disk file so the
file can be opened and closed as needed. FFileStream is a ref-
if ( Assigned(Obj) ) then erence to the actual TFileStream object created to access the
begin { Assign created object to passed object and
return object }
file associated with the filename.
try
obj.Assign(NewObj); As required, TacFileObjStream overrides the protected meth-
Result := Obj;
finally
ods GetStream, OpenStream, and CloseStream. GetStream
NewObj.Free; returns the FFileStream stream reference. OpenStream creates a
end; TFileStream object and saves the reference in the FFileStream
end
else
field. The desired TacObjStream access mode, passed in as a
begin { Just return created object } parameter, determines the file access mode of the created
Result := NewObj; stream. CloseStream frees the TFileStream object referenced by
end;
end;
FFileStream and resets the field to nil. The corresponding disk
file is automatically closed by the TFileStream instance before
Figure 8: The TacObjStream SaveObject and ReadObject functions. it’s destroyed.
must be cast back to a TacStreamableClass reference before it’s This class has a single constructor, Create, that takes a filename
assigned to the local class reference variable, ObjType. as a parameter. It calls the inherited constructor and then saves
TacStreamableClass is a class reference type that can reference the filename in the FFilename field. The inherited destructor,
any class descended from TacStreamable. This cast should Destroy, is overridden. It calls the inherited destructor and then
always be valid since all objects saved to the stream must be frees the TFileStream instance, if it still exists.
instances of TacStreamable subclasses.
Streams and Persistent Objects in Action
A call to the CreateFromStream constructor with ObjType as the The remainder of this article presents a complete, albeit
class reference creates a new object from the stream. This call somewhat trivial, application that exercises most of the
causes Delphi to create an instance of the actual TacStreamable library. The name of the application is Resource Logger. It
subclass referenced by the ObjType variable. Thus, even though exists solely for periodically logging the system’s resource
the CreateFromStream constructor is a static method of status. It optionally tracks free Windows resources, avail-
TacStreamable, when it calls the virtual ReadFromStream method able memory, and available disk space.
type
TacFileObjStream = class(TacObjStream)
private
FFilename : TFilename;
FFileStream : TFileStream;
protected
{ Required internal stream interface }
function GetStream: TStream; override;
procedure OpenStream(const Mode: TacObjStreamMode);
override;
procedure CloseStream; override;
public
{ Construction/Destruction }
constructor Create(const Filename: TFilename);
destructor Destroy; override;
{ Properties }
property Filename: TFilename
read FFilename;
end;
implementation
constructor TacFileObjStream.Create
( const Filename : TFilename );
begin
inherited Create;
FFilename := Filename; Figure 10 (Top): The Resource Logger form at run-time. Figure 11
end; (Bottom): Resource Logger class hierarchy.
destructor TacFileObjStream.Destroy;
begin
Figure 11 shows the relevant class hierarchy for the applica-
inherited Destroy; tion. The TLogItem subclass of TacStreamable is the base class
{ Postponed stream Free so TacObjStream can close for all types of log entries. A distinct subclass of TLogItem is
it up, if needed }
FFileStream.Free;
declared for each of the three types of resources. TOptions is
end; another subclass of TacStreamable used for saving the logging
options. The TLog class is a simple subclass of TacObjStringList
function TacFileObjStream.GetStream: TStream;
begin
that adds a default array property for accessing the log items
Result := FFileStream; with array-like syntax. While active, the application manages
end; the log with an instance of TLog, referencing the list associated
procedure TacFileObjStream.OpenStream
with the form’s list box. The TLogStream subclass of
( const Mode : TacObjStreamMode ); TacFileObjStream manages the application’s data file.
var
StreamFileMode : Word;
begin
All the application-specific persistent classes are declared and
case Mode of implemented in the EXAMPLE unit. The implementations
osmInput : StreamFileMode := are all similar. Figure 12 shows the TMemoryLogItem class
fmOpenRead or fmShareDenyWrite;
osmOutput : StreamFileMode := fmCreate;
source listing as an example.
osmAppend : StreamFileMode :=
fmOpenReadWrite or fmShareDenyWrite; The TLogStream class is an example of subclassing
end;
FFileStream := TFileStream.Create(Filename,
TacFileObjStream to add a user-defined stream header (see
StreamFileMode); Figure 13). The SaveHeader method override saves the appli-
end; cation specific signature, and the ReadHeader method over-
procedure TacFileObjStream.CloseStream;
ride reads the signature and verifies it. ReadHeader raises an
begin exception if the signature is invalid.
FFileStream.Free;
FFileStream := nil;
end;
The EXAMPLE unit registers the TResourceLogItem,
TMemoryLogItem, TDiskLogItem, TOptions, and TLog classes
Figure 9: The TacFileObjStream class. in its initialization section. Only objects of these five classes
are instantiated and streamed.
The application consists of a single form (see Figure 10). A
list box displays the logged entries and a group of check boxes The main form unit contains all the application’s major func-
provides control over logging for each type of resource. The tionality. Up to this point, the discussion has focused on
three command buttons start and stop logging, clear the log, implementing the persistent and stream classes. The remainder
and summarize the logged entries. examines the form unit as an illustration of using the classes.
end;
type
end;
{ TMemoryLogItem handles memory usage log entries. }
TMemoryLogItem = class(TLogItem)
procedure TMemoryLogItem.InitFields;
private
begin
FFreeSpace : LongInt; { Amount of free memory (KB) }
{ Allow TLogItem to initialize }
FLargestBlock : LongInt; { Size of largest available
inherited InitFields;
block (KB) }
protected
FFreeSpace := GetFreeSpace(0) div 1024;
{ TPersistent overrides }
FLargestBlock := GlobalCompact(0) div 1024;
procedure AssignTo(Dest: TPersistent); override;
end;
{ TacStreamable overrides }
procedure InitFields; override;
procedure TMemoryLogItem.SaveToStream(Stream:
function GetAsString: string; override;
TacObjStream);
procedure SaveToStream(Stream: TacObjStream);
begin
override;
{ Allow TLogItem chance to save }
procedure ReadFromStream(Stream: TacObjStream);
inherited SaveToStream(Stream);
override;
public
Stream.SaveBuffer(FFreeSpace,SizeOf(FFreeSpace));
{ Properties }
Stream.SaveBuffer(FLargestBlock,SizeOf(FLargestBlock));
property FreeSpace: LongInt
end;
read FFreeSpace;
property LargestBlock: LongInt
procedure TMemoryLogItem.ReadFromStream(Stream:
read FLargestBlock;
TacObjStream);
end;
begin
{ Allow TLogItem chance to read }
implementation
inherited ReadFromStream(Stream);
procedure TMemoryLogItem.AssignTo(Dest: TPersistent);
Stream.ReadBuffer(FFreeSpace,SizeOf(FFreeSpace));
begin
Stream.ReadBuffer(FLargestBlock,SizeOf(FLargestBlock));
if ( (Dest is TMemoryLogItem) and
end;
(Self is Dest.ClassType) ) then
begin
function TMemoryLogItem.GetAsString: string;
inherited AssignTo(TLogItem(Dest));
begin
with Dest as TMemoryLogItem do
{ Get Timestamp string from TLogItem }
begin
Result := inherited GetAsString;
FFreeSpace := self.FFreeSpace;
FLargestBlock := self.FLargestBlock;
{ Append memory descriptions }
end;
AppendStr(Result,' Mem ');
end
AppendStr(Result,Format(' Free[%dKB]',[FreeSpace]));
else
AppendStr (Result,Format (' Largest[%dKB]',[LargestBlock]));
begin
end;
inherited AssignTo(Dest);
The form class declares three private fields. The FOptions The application saves its option settings and the active log
field is a reference to the TOptions object used to save the entries into a single stream. The SaveLog and ReadLog meth-
state of the logging options. The FLog field is a reference ods are responsible for the stream management. Figure 14
to the TLog object used to contain and manage the active shows their source listing.
log entries. The FLogStream field is a reference to the
TLogStream object used to manage the application data ReadLog opens FLogStream for input, calls the ReadObject
file. All three objects are created in the FormCreate method method twice (once for the options object and once for the
and freed in the FormDestroy method. list object), and then closes FLogStream. Since both objects
were previously created, their references are passed to
The Start Logging button toggles logging on and off. ReadObject. As discussed earlier, this alternative use updates
When logging is enabled, a Timer component triggers a the existing objects instead of creating new ones. For the
periodic event, and log entries for the selected resources options object, this is mainly for convenience, as it avoids
are created and added to the log with a call to FLog.Add. having to free the current options object. Regarding the log,
however, it’s critical. FLog uses the form’s list box as its stor-
The Clear Log button clears all entries from the log with a age list. Freeing the FLog object and letting ReadObject create
call to FLog.DeleteAll. Since the list owns the log entry a new one would break the association.
objects, it automatically frees them.
SaveLog is nearly identical to ReadLog. The only differences
The Summarize button reports the maximum usage for each are that it opens FLogStream for output and calls SaveObject
resource. This button’s handler, BtnSummarizeClick, demon- in place of ReadObject. In both cases, the list object referenced
strates iteration of a TacObjStringList instance and assignment of by FLog handles reading and saving the individual log entries,
TacStreamable objects. regardless of their specific class.
type
Conclusion
{ TLogStream represents the log data file } The TacStreamable, TacObjStringList, TacObjStream, and
TSignature = string[6]; TacFileObjStream classes provide the minimum functionality for a
TLogStream = class(TacFileObjStream) useful persistent object library. For many applications, this func-
private
FSignature : TSignature; tionality may be sufficient. For others, it should provide a reason-
protected able starting point for more sophisticated implementations.
procedure ReadHeader; override;
procedure SaveHeader; override;
end; There are many enhancements to consider. TacObjStream
could be extended to support simultaneous reading and writ-
implementation ing, or random object access. The accompanying source code
const includes a TacObjStream subclass for memory-based streams.
LOG_SIGNATURE : TSignature = 'LOGDAT'; Support for other types of streams might also be useful.
Iterators for TacObjStringList will be my next enhancement. I
procedure TLogStream.ReadHeader;
begin would also like to have other types of object containers, such
ReadBuffer(FSignature, SizeOf(FSignature)); as stacks and queues.
{ Throw exception if invalid signature }
if ( FSignature <> LOG_SIGNATURE ) then
begin Whether you use the library as-is or as a starting point for
raise Exception.Create('Unrecognized data file'); your own, there are no more excuses for avoiding persistent
end; objects and streams in your Delphi applications. ∆
end;
{ ReadLog reads the options and log from the data file } The demonstration application referenced in this article is avail-
procedure TMainForm.ReadLog;
begin able on the 1995 Delphi Informant Works CD located in
if ( FileExists(FLogStream.Filename) ) then INFORM\95\DEC\DI9512AC.
begin
with FLogStream do
begin
OpenForInput;
ReadObject(FOptions);
Alan Ciemian is a contract programmer and software developer specializing in Windows
ReadObject(FLog);
Close;
development using C++, Delphi, and Paradox for Windows. He can be reached on
end; CompuServe at 70134,611 or on the Internet at 70134.611@compuserve.com.
end;
end;
{ SaveLog saves the options and the log to the data file }
procedure TMainForm.SaveLog;
begin
with FLogStream do
begin
OpenForOutput;
SaveObject(FOptions);
SaveObject(FLog);
Close;
end;
end;
By Douglas Horn
Delphi MRU
Adding a Shortcut to Your Most Recently Used Files
I that lists the last files accessed by an application. Clicking on any of these file-
names opens the corresponding file immediately, allowing users to quickly pick
up where they left off. These MRU (or most recently used) lists are popular time
savers, and are available in most commercial Windows applications (see Figure 1).
Unfortunately, MRU lists are not standard in Delphi — you must create them.
Starting Off
MRU lists can be added just as easily to new or existing applica-
tions. To illustrate this, we’ll add an MRU list to one of the sample
applications that ships with Delphi.
Adding MRU List Objects The MRU list is a TStringList component that maintains a list of
To create an MRU list, you must add two types of objects to an MRU files while the application is running. To add this list, first
application: the menu items to access the list, and the list itself. declare the MRUList variable under FrameForm’s public part of
The menu items are any number of blank items positioned prop- the interface section.
erly on the main menu. This sample application uses four,
public
because it’s the standard number and is easy to work with. MRUList: TStringList;
However, a list can hold up to nine items. In some Windows
applications, such as Microsoft Word 6.0 for Windows, the user This allows access by the application’s other form, EditForm.
can configure the number of MRU items. Using more than nine
is possible, but not recommended, because it makes the menus Now that MRUList is declared, it must be put into action. Since it
long and the program runs out of single-digit numbers to use as will be active for the life of the application session, it should be
menu shortcut keys. created in the OnCreate event when the application starts.
MRUList is a global variable. It will be accessible throughout the
The proper position of the MRU list on a menu is under the application and automatically freed when the application is closed:
File menu group, above Exit, in its own subsection. Mnemonic
shortcut keys for MRU list items are always listed in numerical procedure TFrameForm.FormCreate(Sender: TObject);
order (beginning with 1), corresponding to the number of MRU begin
MRUList := TStringList.Create;
items (again, see Figure 1). end;
To add the MRU menu items to TEXTEDIT, first open the menu Managing the .INI File
editor for the application’s main (MDIParent) form, FrameForm. The container for the MRU list (the StringList object, MRUList)
Starting just below the lowest separator (at Exit), insert five new only contains its contents while the application is running.
menu items. From top to bottom, name these items MRU1, Between program sessions, it stores the list to an .INI file.
MRU2, MRU3, MRU4, and MRUSeparator. Leave the captions
for items MRU 1 through 4 blank. These will be filled by code at The best time to load values into MRUList is when the list is creat-
run-time. The caption for MRUSeparator should be set to a ed. Since this application uses four items, a loop should be added to
hyphen ( - ) to create a menu separator line. Set the Tag property the FormCreate procedure to read four values from the MRU section
of the items as shown in Figure 2. of the .INI file, TEXTEDIT.INI (in the C:\WINDOWS directory).
They should then be added to the string list. If the .INI file does
When the menu items on the MDI parent form (FrameForm) are not exist, it’s created automatically, and blank values are added to
complete (see Figure 3), repeat the process on the application’s the list. Figure 4 shows the updated FormCreate procedure.
MDI child form (EditForm) using the same names and proper-
ties. This does not create a conflict, because although the items The FormCreate procedure loads the contents of the .INI file into
are merged at run-time, they exist on different menus. the MRUList string list at startup. To have anything to load, howev-
er, the application must save the contents of the list upon closing.
Name Caption Tag Figure 2 (Left): Setting the Tag To accomplish this, the FormDestroy procedure is made similar to
MRU1 blank 0 properties for individual lines in FormCreate, except FormDestroy (see Figure 5) copies the contents
our example. of the string list back to the .INI file for storage (see Figure 6) until
MRU2 blank 1
Figure 3 (Bottom): New MRU
MRU3 blank 2 the program is run again.
list menu items added to the
MRU4 blank 3 FrameForm main menu via the
Menu Editor. Finally, for Delphi to access the .INI file (and for the application
MRUSeparator - 0
to run), IniFiles must be added to the uses statement in the
interface section of the FrameForm code:
To perform this, the MRUUpdate must be called from the applica- The most efficient way to set the Visible property to True if the
tion’s Open and Save procedures. In the sample TEXTEDIT appli- menu item is not blank, is to use the compound statement:
cation, these procedures are TEditForm.Open and
MRU1.Visible := (MRUList[0] <> '');
TEditForm.Save1Click. The Open procedure receives a filename
from the OpenDialog procedure called in TFrameForm.OpenChild.
By adding the line: This sets the Visible property to True if the List item contains a
filename (i.e. if MRUList[x] is not blank). Otherwise it sets the
FrameForm.MRUUpdate(Self,Filename); property to False, hiding the item.
before the procedure’s end keyword, we can tell the procedure to The code for TFrameForm.MRUDisplay is shown in Figure 8.
update the MRU list by using the open file’s name as the new The TEditForm.MRUDisplay procedure is identical, except it’s in
addition to the list. the EditForm code, and references the MDI child menu items.
Although the routines are straightforward, they must be called at Figure 9 (Top): The OpenChild procedure.
the correct time to display the current MRU list and avoid possi- Figure 10 (Bottom): The EditForm.Save1Click procedure.
ble errors. The EditForm version is the busiest, as the EditForm’s
menu is displayed whenever files are open. The FrameForm ver- must also be added to the end of the EditForm.Save1Click
sion of the menu is only called when the application is first acti- procedure as shown in Figure 10. This allows the program to
vated, and when an MDI child form is closed. This updates the display the current MRU information when a file has been
parent form’s menu in case the child form being closed is the last saved and the MRU list is updated.
one and the parent form’s menu will be displayed again. To
enable these functions, add: Finally, to update the MRU menu items in MDI child windows,
MRUDisplay(Sender);
the application must be instructed to call EditForm.MRUDisplay
whenever the focus switches between forms. Otherwise, each child
to the last line of the FrameForm.FormCreate procedure, and add: window displays the MRU list items as they were when the form
was last updated, and not necessarily the current information. To
FrameForm.MRUDisplay(Sender); enable this display update, set the EditForm’s OnActivate event to
MRUDisplay, using the Events Page of the Object Inspector.
to the last line of the EditForm.FormClose procedure.
Opening Files from the MRU List
The EditForm.MRUDisplay procedure is called by the The last task in adding an MRU list to an application is actually
FrameForm.OpenChild procedure by adding the statement: opening the files with the click of a button. The TEXTEDIT
application can already open a child window using the
EditForm.MRUDisplay(Sender); FrameForm.OpenChild procedure. This procedure obtains the
name of the file to open from an Open File common dialog
at the last line of the procedure. This procedure defines and box. Since the MRU list already contains the name of the
creates the child form. Since EditForm is declared as a local desired file, the dialog box is unnecessary.
variable in this FrameForm procedure, the procedure can call
EditForm.MRUDisplay, even though it is invisible to the rest of The FrameForm.MRUOpenChild procedure (see Figure 11) must
the program. The complete OpenChild procedure is shown in only reference the MRU list and obtain the filename associated
Figure 9. (Note that all lines but: with the selected MRU menu item. To do this, the procedure uses
the Sender’s Tag property (set when the MRU menu items were
EditForm.MRUDisplay(Sender);
created in step one), and assumes the Sender is a TMenuItem.
already exist in the TEXTEDIT sample application.)
Once this event handler is written, it should be set as the OnClick
event handler for all four MRU menu items (i.e. MRU1, MRU2,
The same statement:
MRU3, and MRU4) on the FrameForm main menu. The corre-
EditForm.MRUDisplay(Sender);
sponding event handler for the EditForm MRU menu items should
Figure 11 (Left): The FrameForm.MRUOpenChild procedure. Figure 12 (Right): The TEXTEDIT application with its new MRU list
displayed.
be called MRUClick. The procedure EditForm.MRUClick references as files are opened and saved. Add procedures to display the con-
FrameForm.MRUOpenChild: tents of the string list to the menu items on both parent and child
forms. Finally, include a simple event handler to open files from
procedure TEditForm.MRUClick(Sender: TObject); the string list when the corresponding menu item is clicked.
begin
FrameForm.MRUOpenChild(Sender);
end; So there you have it! Five steps to a more professional, user-
friendly application. ∆
Ready to Run
The MRU list in TEXTEDIT.DPR is now ready to run (see
Figure 12). It will keep track of the files and allow those used last The demonstration project referenced in this article is available on
to be instantly opened. the 1995 Delphi Informant Works CD located in
INFORM\95\DEC\DI9512DH.
It should now be easy to add an MRU list to any new or existing
Delphi application by following these steps. First, add the menu
objects and string list to the application. Next, add simple proce- Douglas Horn is a free-lance writer and computer consultant in Seattle, WA. He spe-
dures for moving data back and forth between the string list and cializes in multilingual applications, particularly those using Japanese and other Asian
an .INI file. Then, write an event handler to update the string list languages. He can be reached via e-mail at internet:horn@halcyon.com.
By David Faulkner
This article presents a playing card component, TCardDeck, and a video poker game (see Figure 1)
easily made from that component. We’ll start by learning how to use the component, and then deal
with some technical details of its implementation. (And if you download this component, you could
try cutting in on the lucrative Solitaire market for Windows 95.)
TCardDeck Informant
When writing a component, you must first decide which existing
component to descend from. In TCardDeck’s case, descending
from TGraphicControl seems logical. It already has some essential
properties, such as Position and Size, and has a canvas that makes
drawing easy. Additionally, TGraphicControl has events such as
Paint and drag-and-drop support that are necessary in the
TCardDeck component.
TCardDeck was created with the Component Expert (see Figure 6).
To open this dialog box, select File | New Component. When you
press the OK button, Delphi builds a unit with the necessary class
declaration and register procedure. Now, complete the code to
make the component functional.
Constants
CARDDECK.PAS begins by declaring many constants. Each card
name, color, and card are assigned numeric values. A number of
subrange types are created to simplify programming with
TCardDeck. For example, the constants:
Figure 3 (Top): The TCardDeck component on the DI page of the
cdSpades = [1..13];
Component Palette. Figure 4 (Bottom): The component on a form.
cdHearts = [14..26];
cdClubs = [27..39];
The Value property is an integer that represents the card face dis- cdDiamonds = [40..52];
cdBlackCards = [1..13,27..39];
played. 1 is the Ace of Spades, 2 is the 2 of Spades, 14 is the Ace cdRedCards = [14..26,40..52];
of Hearts, 27 is the Ace of Clubs, 40 is the Ace of Diamonds,
and 52 is the King of Diamonds. You can work with numbers or make it easy to determine a card’s color or suit.
a set of constants that adds readability to programs using
TCardDeck components. Figure 4, for example, shows the con- A set of TCardEntries is also configured as a constant. It’s
stant cdAceOfSpades in the Object Inspector. declared as:
The last 10 “cards” are Card 53 (cdJoker), the Joker, and cards 54 type
TCardEntry = record
to 62 (cdCardBack1..cdCardBack9) with bitmaps representing the Value: cdCardDeck;
back of each card. Name: PChar;
end;
const
TCardDeck Utility Functions Cards: array[1..62] of TCardEntry =
The ability to display card faces on a form makes it easy to ((Value: cdAceofSpades;
write card games, but TCardDeck goes further with some utility Name: 'cdAceofSpades'),
Value: cd2ofSpades;
functions. When a TCardDeck component is placed on a form, Name: 'cd2ofSpades'),
Delphi automatically includes the CARDDECK.PAS unit in
your form’s unit. By using TCardDeck, you automatically have Using the constant Cards array, it’s simple to map the string
access to the functions shown in Figure 5. version of a card’s value to its integer value. This is necessary
to set up TCardDeck’s Value property which accepts a string or
There is no built-in help for these functions or the extra proper- an integer.
ties defined by TCardDeck. Therefore, you may want to keep the
source code open on an editor tab. To do this, select File | Open TCardDeck.Paint
File from Delphi’s menu. Then, select CARDDECK.PAS from TCardDeck’s most important feature is its ability to display
the appropriate directory. You’ll find a component’s source code bitmaps that represent card faces. To do this, TCardDeck over-
is as good as any help system. rides TGraphicComponent’s Paint procedure:
We often have several tabs open in the editor to Delphi’s type
units (buy the VCL source if you don't have it). Note that TCardDeck = class(TGraphicControl)
although the File Open dialog box starts with a *.PAS filter, protected
the editor can be used to view any text file (such as those { Protected declarations }
written for an import routine, or HTML source code that procedure Paint; override;
may be interfaced).
Function Description
function IsCardRed(Value: cdCardDeck) : Boolean; Accepts a card value. Returns True if card is red, otherwise False.
function IsCardBlack(Value: cdCardDeck) : Boolean; Accepts a card value. Returns True if card is black, otherwise False.
function IsFaceCard(Value: cdCardDeck) : Boolean; Accepts a card value. Returns True if card is a face card,
othewise False.
function AreCardsSameColor(Value1, Accepts two card values. Returns True if cards are the same color,
Value2: cdCardDeck) : otherwise False.
Boolean;
function AreCardsSameSuit(Value1, Accepts two card values. Returns True if cards are the same suit,
Value2: cdCardDeck) : otherwise False.
Boolean;
function AreCardsSameValue(Value1, Accepts two card values. Returns True if the cards have the same
Value2: cdCardDeck) : face value and the same color, otherwise False. For example, if Figure 5: Each function used
Boolean; the 2 of spades and 2 of diamonds are passed, True is returned. by TCardDeck.
function CardColor(Value: cdCardDeck) : integer; Returns an integer representing the color of the card passed to it.
CARDDECK.PAS defines the following constants for card colors:
cdBlack = 1;
cdRed = 2;
function CardSuit(Value: cdCardDeck) : integer; Returns an integer representing the suit of the card passed to it.
CARDDECK.PAS defines the following constants for card suits:
cdSpade = 1;
cdHeart = 2;
cdClub = 3;
cdDiamond = 4;
function CardValue(Value: cdCardDeck) : integer; Returns an integer representing the face value of the card passed
to it. The number 1 represents an Ace, 2 represents a 2, and 13
represents a King.
Simply stated,
whenever procedure TCardDeck.Paint;
var
Windows requests BitMap:TBitMap;
TCardDeck to FacePChar: array [0..25] of char;
paint itself, the begin
strpcopy(FacePChar,CardToString(Value));
TCardDeck.Paint BitMap := TBitMap.create;
procedure is called. try
See Figure 7 for an BitMap.handle := LoadBitMap(HInstance,FacePChar);
if not BitMap.empty then
abbreviated ver- Figure 6: The Component Expert dialog box. begin
sion. The code not if Stretch then
shown paints the corners of the cards the same color as the canvas.stretchdraw(clientrect,BitMap)
else
background. The entire code listing for the project begins on Canvas.draw(0,0,BitMap);
page 27. end;
finally
BitMap.free;
Some important information should be noted within this proce- end;
dure. Once the variable BitMap is created, the code uses a end;
try..finally block. This protects the component from leaking
memory if a GPF occurs. The BitMap.Create command (or any Figure 7: An abbreviated version of the TCardDeck.Paint procedure.
Create command) allocates memory. As a component designer,
you must free this memory, even if the component causes an the dollar symbol ( $ ) on a blank line in the code editor and
exception during processing. In this case, if something goes press 1. You can now surf the help system for information.
wrong while painting the component, the finally clause calls With CARDDECK.RES included in the .EXE, this code is used
BitMap.Free to release the allocated memory. to extract a bitmap from the resource file:
The bitmaps for card faces are stored in CARDDECK.RES, a stan- BitMap.handle := LoadBitMap(HInstance,FacePChar);
With the bitmap loaded, this code paints the screen: the SetValue procedure:
The paValueList constant tells the Object Inspector that a drop- procedure Register;
down list is available. The paMultiSelect constant instructs the begin
RegisterComponents('DI',[TCardDeck]);
Object Inspector to continue displaying this property if more RegisterPropertyEditor(TypeInfo(cdCardDeck),TCardDeck,
than one TCardDeck component is selected. This allows you to 'Value',TCardValueProperty);
select multiple instances of a TCardDeck object and set their val- end;
implementation TCardDeck(FindComponent('CardDeck'+
inttostr(iCardCount))).value:=x;
{$R *.DFM} if bSound then sndPlaySound('CARD.WAV',2);
aUsedCards[x]:=True;
procedure TForm1.DealOrDrawClick(Sender: TObject); aHand[iCardCount]:=x;
var Tlabel(FindComponent('Hold'
x,y,z: byte; { general purpose } inttostr(iCardCount))).visible := False;
iCurrentCount: byte; { # of matching cards in hand } end;
iCardCount: byte; { used to step through 52 cards } DealOrDraw.Caption:='&Deal';
bTwoOfAKind, { two of a kind }
bTwoPair, { two pair } { Now score the whole thing }
bThreeOfAKind, { three of a kind } bTwoOfAKind := False;
bFourOfAKind, { four of a kind } bTwoPair := False;
bJacksOrBetter, { a pair of jacks or better } bThreeOfAKind := False;
bFlush, { a flush } bFourOfAKind := False;
bStraight, { a straight } bJacksOrBetter := False;
bStraightFlush, { a straight flush } bFlush := True;
bFullHouse, { a full house } bStraight := True;
bRoyalFlush: Boolean; { is this a really lucky player } bFullHouse := False;
aFaceCopy: TCardHand; { a copy of the current hand } for x:=1 to 5 do
aUsedTag[x] := False;
begin
if bSound then { compare face value of each card to other
sndPlaySound('Deal.wav',3); cards counting matches }
for x:=1 to 5 do begin
Timer1.enabled := False; if aUsedtag[x] then continue;
if iWinnings<>0 then begin iCurrentCount:=0;
Pot.caption:=IntToStr(StrToInt(Pot.caption)+iWinnings); for y:=x+1 to 5 do begin
iWinnings:=0; if (CardValue(aHand[x])=CardValue(aHand[y])) and
end; (not aUsedTag[y]) then begin
aUsedTag[y] := True;
if bFirstDeal and (StrToInt(Pot.caption)=0) then begin aUsedTag[x] := True;
MessageDlg('Busted, You have no money left to bet!', inc(iCurrentCount);
mtWarning,[mbOK], 0); if isFaceCard(aHand[x]) then
exit; bJacksOrBetter := True;
end; end;
end; { end of inner for loop }
if bFirstDeal then begin
for x:=1 to 52 do { initialize deck } case
iCurrentCount of
aUsedCards[x] := False; 3:bFourOfAKind := True;
DealOrDraw.Caption := '&Draw'; 2:bThreeOfAKind := True;
HoldOrDraw.Caption := 'Hold or Draw'; 1:if bTwoOfAKind then
Pot.caption := IntToStr(StrToInt(Pot.caption)-5); bTwoPair := True
{ Pick out 5 new cards } else
for iCardCount:=1 to 5 do begin bTwoOfAKind := True;
TButton(FindComponent('HoldButton' + end; { End of CurrentCount case }
inttostr(iCardCount))).enabled:=True; end; { End of outer loop }
TLabel(FindComponent('Hold' +
inttostr(iCardCount))).visible:=False; { it's a flush if CardSuit of all five cards is the same }
repeat { Pick a random card } for x:=2 to 5 do
x := random(51)+1; bFlush := (CardSuit(aHand[1])=
until not aUsedCards[x]; CardSuit(aHand[x])) and bFlush;
TCardDeck(FindComponent('CardDeck' +
inttostr(iCardCount))).value := x; { now detect a straight, start by creating a sorted
if bSound then sndPlaySound('CARD.WAV',2); copy of hand using copy so can 'blink' winning cards }
aUsedCards[x] := True; for x:=1 to 5 do
aHand[iCardCount] := x; afacecopy[x] := CardValue(ahand[x]);
end;
end { just a little bubble sort }
else for x:= 1 to 4 do begin
begin for y:=x to 5 do begin
HoldOrDraw.caption := ''; if afacecopy[y] > afacecopy[x] then begin
for iCardCount := 1 to 5 do z := afacecopy[x];
begin { deal new cards for non held cards } afacecopy[x] := afacecopy[y];
TButton(FindComponent('HoldButton'+ afacecopy[y] := z;
inttostr(iCardCount))).enabled := False; end;
if Tlabel(FindComponent('Hold'+ end;
inttostr(iCardCount))).visible then end;
continue;
repeat { if it’s a straight then each consecutive card will
x := random(51)+1; have face value of one more than previous card }
until not aUsedCards[x]; for x:=2 to 5 do
Hotspots
Creating Mouse-Sensitive Areas on Your Forms
These examples illustrate hotspots — areas of an image that are sensitive to mouse events.
These hotspots usually coincide with what Delphi developers consider objects: shapes on a
drawing or features on a map.
Regions
Regions are a flexible means of creating hotspots. They are
independent of any on-screen visual image and are stored as
data structures. The relationship between regions and an image
is decided by the programmer. The Windows API includes a
full suite of functions for creating and manipulating regions of
practically any size or shape. Under Windows 3.1, the only
Figure 1: Region functions as defined in Delphi’s Windows API restriction is that each region is limited to a size of 32,767 by
on-line help. 32,767 units, or 64KB of memory, whichever is smaller.
A region can be viewed as a set of points. Normally, these points procedure TForm1.ButtonEllipseClick(Sender: TObject);
will coincide with pixels in an image. A function in the var
Windows API tests a point to determine if it’s within a specified x1,x2,y1,y2: Integer;
begin
region. This function can also map mouse events from an on- FreeRegions;
screen image to a set of previously defined regions. Figure 1 ImageHandle := Image1.Canvas.Handle;
shows a list of Windows API region functions. Nregions := 5;
for i := 1 to Nregions do
begin
Using Regions x1 := i*40;
The typical steps for using regions are to: x2 := x1 + 35;
y1 := i*30;
• allocate memory y2 := x1 + 25;
• create and store regions New(RegionRecord);
• respond to events RegionRecord^.rgn := CreateEllipticRgn(x1,y1,x2,y2);
RegionRecord^.id := ' Hello from elliptic region ' +
• free memory IntToStr(i);
RegionList.Add(RegionRecord);
To illustrate these steps, the example application defines end;
ShowOrHide(Sender);
regions that correspond to various shapes shown in a Label1.Caption := 'Elliptic regions created.'
TImage object. It then responds to MouseDown events in end;
the image (see Figure 2).
Figure 3: The custom ButtonEllipseClick procedure.
space in the 64KB of memory used for the data and stack
segments (a Windows 3.1 limitation). In this example, the
Figure 2:
Our sample records are stored in a TList.
application.
The shapes The Object Pascal code shown in Figure 3 creates five
shown in the regions when the user clicks the button labeled Elliptic
TImage object
Regions. A record is stored for each region. Similar code,
correspond to
regions defined shown in Listing Five on page 35, creates regions of differ-
by the code in ent shapes in response to clicks on the other buttons.
Listing Five.
It’s important to remember that the regions are completely
independent of what is shown on screen. In this example, the
ShowOrHide procedure is called after the regions are created.
Allocate memory. When a region is created, the Windows If the Show radio button is checked, ShowOrHide will draw
API allocates memory for the data structure, then returns a shapes on the TImage representing the regions. Otherwise the
handle. It’s your responsibility to track these handles in the image remains blank. Regardless of what the image displays,
Delphi application, and to free corresponding objects when the regions exist and are ready to use.
they become unnecessary.
Respond to events. From a practical standpoint, something on
Create and store regions. Typically, information about the screen will almost always coincide with a region you create. This
region is stored with the region handle. For example, if each is done by executing independent drawing commands when cre-
region represents a record in a table, storing that record’s key ating a region, or creating it first and then drawing a frame
with the region handle allows information to be displayed around it in a specified device context. The latter method repre-
from the table in response to a mouse click. One way to do sents the most versatile code and is implemented in the
this is to define a record object that contains the region han- ShowOrHide procedure (see Figure 4).
dle and any other useful fields.
Interacting with regions means responding to events. In
In this example, the record is defined as: Figure 4, we want to retrieve data from the region record in
response to mouse clicks in the TImage control. Therefore,
arec = record
rgn: HRgn;
the code is attached to the TImage MouseDown event that will
id: string; attempt to map the coordinates of the mouse event to one of
end; the regions we have defined.
In this record rgn is the region handle, and id is a simple The Windows API function, PtInRegion, maps events to
string we create and store as a short description of the region. regions. It returns True if the specified point is inside the
specified region. In response to a MouseDown event on
The records of each region can be stored in an array or in a Image1, we simply iterate through the list of regions, testing
TList. An array may be more convenient, but it requires each to see if the event occurred inside it (see Figure 5).
RegionList.Clear;
begin
end;
FreeRegions;
ImageHandle := Image1.Canvas.Handle;
for i0 := 0 to 5 do
begin
y0 := i0*40;
for i := 1 to 50 do
begin
x1 := i*7;
x2 := x1 + 6;
y1 := 1 + y0;
y2 := y1 + 30;
New(RegionRecord);
RegionRecord^.rgn := CreateRectRgn(x1,y1, x2,y2);
RegionRecord^.id := ' Hello from rectangle region ' +
IntToStr(i0*100+i);
RegionList.Add(RegionRecord);
end;
end;
ShowOrHide(Sender);
Label1.Caption := 'Rectangle regions created.'
end;
end.
There are several techniques you can use to find a particular record in a table using a Table
component. However, most of these only permit you to locate a record based on the fields of a
table’s index. This month's DBNavigator looks at the basics of locating records, including the
use of the TTable methods SetKey, FindKey, and FindNearest, and sequential record searches.
The table state, dsSetKey, is a special state that permits you to identify the index field values of
the record you want to move to. You can enter this state in one of two ways, using the SetKey
or EditKey method.
The Table method SetKey is used most commonly. When you call this method, the table is
placed into a state where assignments to its individual fields are not applied to it, but are
stored in the search key buffer. This buffer contains values only for index fields.
Once values are assigned to one or more indexed fields within the dsSetKey state, you
can call one of four methods. The method GotoKey is used to move to the record whose
indexed field values match those in the search key buffer. GotoNearest is used to move
to the record that most closely matches the buffer values. The last two methods,
FindKey and FindNearest, do not require the dsSetKey state and will be discussed later in
this article.
The use of SetKey, GotoKey, and GotoNearest is demonstrated in the project named
DEMO1 shown in Figure 1. This project uses the NEWCUST.DB table and is pointed
Figure 1: The DEMO1 project demon-
to by the alias DBDEMOS. (DBDEMOS is created by default by the Delphi installa-
strates using the GotoKey and GotoNearest tion program. NEWCUST.DB is based on the CUSTOMER.DB table that ships with
methods on the NEWCUST.DB table. Delphi.) NEWCUST.DB has been modified to include two additional indexes:
This is demonstrat- The DEMO5 project is shown in Figure 5. The Table com-
ed in DEMO3, ponent's IndexName property has been assigned
shown in Figure 3. ByCityState, a two-field index. Since City is the first field in
This project, which this index, a search is only successful if a value is assigned to
uses the dsSetKey this field of the search key buffer. Searches that include only a
state, permits selec- value for the City field locate the first record that matches
tion of a case-sensi- that city. To search for a particular city-state combination,
tive index for both the City and State fields must be assigned a value.
NEWCUST.DB
(using an index
named Company)
or a case-insensitive Figure 3: Searches are case-sensitive
when the Table component being searched Figure 5: The DEMO5
search (using an uses a case-sensitive index. Searches are project demonstrates
index named case-insensitive when a case-insensitive using GotoKey on a
ByCompany). Each index is used, as demonstrated by the multi-field index.
time you click on DEMO3 project.
the checkbox
labeled Case Sensitive Search, you modify the
IndexName property of the Table component, as shown in
this event handler:
procedure TForm1.CheckBox1Click(Sender: TObject); Here’s the OnClick event handler for the button labeled
begin
if CheckBox1.Checked then GotoKey on the DEMO5 project:
Table1.IndexName := 'Company'
else procedure TForm1.Button1Click(Sender: TObject);
Table1.IndexName := 'ByCompany'; begin
end; Table1.SetKey;
Table1.FieldByName('City').AsString := Edit1.Text;
Table1.FieldByName('State').AsString := Edit2.Text;
The event handlers associated with the two search buttons if Table1.GotoKey then
contain the same code as DEMO1. DBGrid1.SetFocus;
end;
This code assigns values to both the City and State fields of the
Figure 4: The DEMO4
search key buffer. In this instance, if the user does not enter a
project demonstrates
using FindKey and city name, Edit1 contains a blank string, and the search is only
FindNearest in case- successful if there’s a record with a blank city value and the
sensitive and case- matching state value.
insensitive searches.
FindKey and FindNearest can also be used with multi-field
indexes. This is demonstrated by the DEMO6 project. This
code is associated with the FindKey button on DEMO6:
Within the while loop, each record is tested for the search value.
Figure 7 shows the Object Pascal code associated with the If a match is found, the bookmark is assigned to the record and
OnClick event handler for the Search button. This code the while loop is terminated using a Break statement.
begins by declaring a variable of the type TBookMark that
defines which record to move to at the end of a search. The final part of this event handler appears in the finally
The first statement in the body of the procedure is a test clause of the try..finally block. The statements in the finally
of the Table component's state. If the current record has clause are executed even if an exception is raised within the
not been posted, the user is asked to confirm the posting try clause. The last steps are performed in the search: mov-
of the record before continuing. ing to the record pointed to by the bookmark, freeing the
bookmark, enabling the Table's data-aware controls, restor-
Next, a bookmark is assigned to the current record, the ing the form's cursor, and setting focus to the DBGrid.
form's cursor is changed to an hourglass, and the Table com-
ponent's DisableControls method is called. Calling this This sequential-search technique is simple but limited.
method prevents the user from seeing the accessed records First, it’s not advisable to use it with SQL tables. These
while locating the specified value, and speeds the search by tables are set-oriented, not record-oriented. Therefore, it’s
avoiding a repaint of any data-aware controls. better to use SQL statements to locate records in SQL
tables. Second, this technique is affected directly by the
The search begins after disabling the Table component’s con- number of records in the table being searched, and the
trols. If the checkbox labeled Continue Search From Current location of a matching record within that table. For exam-
Record has been checked, a call to the Table's First method ple, searching a 200,000 record table for a value found in
moves to the first record in the table. Next, a while loop is the last record requires just over one minute to complete
entered that repeats until all records have been processed based on a 100MHz Pentium with 24MB of RAM. By compari-
on the EOF (end-of-file) property of the table. son, a search on the same table using an index and
Whether your tables are indexed or not, you have several options End Listing Six
for locating specific records. Whenever possible, you should try Begin Listing Seven — Demo2 Project
to use indexes. In fact, one of the main reasons for creating program Demo2;
indexes is to provide fast searches based on commonly searched uses
Forms, Demo2u in 'DEMO2U.PAS' {Form1};
fields. However, if indexes are not practical, a sequential search {$R *.RES}
can always be used, even though it is far less efficient. ∆ begin
Application.CreateForm(TForm1, Form1);
Application.Run;
The demonstration projects referenced in this article are available end.
on the 1995 Delphi Informant Works CD located in
INFORM\95\DEC\DI9512CJ. unit Demo2u;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes,
Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database Graphics, Controls, Forms, Dialogs, StdCtrls, Grids,
development company. He is a developer, trainer, and author of numerous books on DBGrids, DB, DBTables;
database software. You can reach Jensen Data Systems at (713) 359-3311, or type
through CompuServe at 76307,1533. TForm1 = class(TForm)
DataSource1: TDataSource;
Table1: TTable;
DBGrid1: TDBGrid;
Begin Listing Six — Demo1 Project Button1: TButton;
program Demo1; Label1: TLabel;
uses Edit1: TEdit;
Forms, Demo1u in 'DEMO1U.PAS' {Form1}; Button2: TButton;
{$R *.RES} procedure Button1Click(Sender: TObject);
begin procedure FormCreate(Sender: TObject);
Application.CreateForm(TForm1, Form1); procedure Button2Click(Sender: TObject);
Application.Run; end;
end. var
Form1: TForm1;
unit Demo1u; implementation
interface {$R *.DFM}
uses procedure TForm1.Button1Click(Sender: TObject);
SysUtils, WinTypes, WinProcs, Messages, Classes, begin
Graphics, Controls, Forms, Dialogs, StdCtrls, Grids, if Table1.FindKey([Edit1.Text]) then DBGrid1.SetFocus;
DBGrids, DB, DBTables; end;
type procedure TForm1.FormCreate(Sender: TObject);
TForm1 = class(TForm) begin
DataSource1: TDataSource; Table1.Open;
Table1: TTable; end;
DBGrid1: TDBGrid; procedure TForm1.Button2Click(Sender: TObject);
Button1: TButton; begin
Label1: TLabel; Table1.FindNearest([Edit1.Text]);
Edit1: TEdit; DBGrid1.SetFocus;
Button2: TButton; end;
procedure Button1Click(Sender: TObject); end.
procedure FormCreate(Sender: TObject); End Listing Seven
procedure Button2Click(Sender: TObject);
end; Begin Listing Eight — Demo3 Project
var program Demo3;
Form1: TForm1; uses
implementation Forms, Demo3u in 'DEMO3U.PAS' {Form1};
{$R *.DFM} {$R *.RES}
procedure TForm1.Button1Click(Sender: TObject); begin
begin Application.CreateForm(TForm1, Form1);
Table1.SetKey; Application.Run;
Table1.FieldByName('CustNo').AsString := Edit1.Text; end.
if Table1.GotoKey then DBGrid1.SetFocus;
end; unit Demo3u;
procedure TForm1.FormCreate(Sender: TObject); interface
begin uses
Table1.Open; SysUtils, WinTypes, WinProcs, Messages, Classes,
end; Graphics, Controls, Forms, Dialogs, StdCtrls, Grids,
procedure TForm1.Button2Click(Sender: TObject); DBGrids, DB, DBTables;
begin type
Table1.SetKey; TForm1 = class(TForm)
DataSource1: TDataSource;
Table1: TTable;
DBGrid1: TDBGrid;
Button1, Button2: TButton;
Label1: TLabel;
Edit1: TEdit;
Table1CustNo: TFloatField;
Table1Company: TStringField;
Table1Addr1: TStringField;
Table1Addr2: TStringField;
Table1City: TStringField;
Table1State: TStringField;
Table1Zip: TStringField;
Table1Country: TStringField;
Table1Phone: TStringField;
Table1FAX: TStringField;
Table1TaxRate: TFloatField;
Table1Contact: TStringField;
Table1LastInvoiceDate: TDateTimeField;
CheckBox1: TCheckBox;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Table1.SetKey;
Table1.FieldByName('Company').AsString := Edit1.Text;
if Table1.GotoKey then DBGrid1.SetFocus;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Table1.Open;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Table1.SetKey;
Table1.FieldByName('Company').AsString := Edit1.Text;
Table1.GotoNearest;
DBGrid1.SetFocus;
end;
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
if CheckBox1.Checked then Table1.IndexName := 'Company'
else Table1.IndexName := 'ByCompany';
end;
end.
How can I run another Windows application within If CmdLine does not include a path, WinExec will search
my program? for the executable in the following order: the current direc-
Users will often ask for the ability to execute other programs tory, the Windows directory, the Windows system directo-
within your Delphi application. For example, they may want ry, the directory containing the executable of the current
to access the Windows Calculator without returning to application, all directories in the path, and the directories
Windows itself (see Figure 3). mapped on a network.
Figure 5: Append
MMSystem to the
unit’s uses clause
to access the
sndPlay-Sound
Figure 6 (Top): This code is attached to the button’s OnClick event
function.
handler. Figure 7 (Bottom): To tile windows vertically down the
screen in the Database Desktop, hold down S while selecting
Window | Tile.
Figure 6 shows a form with a button labeled Play Chimes.
When the button is pressed, the familiar Windows chimes Quick Tip
.WAV file is played. Examine the code attached to the button’s In the Database Desktop, you’ve probably used the Window |
OnClick event. sndPlaySound accepts two parameters: a PChar Tile command to arrange your desktop windows horizontally
for the name of the .WAV file, and a Word for the mode to across the screen. Did you know that holding V down
play the .WAV. In this example, CHIMES.WAV is specified as while selecting Window | Tile will arrange your windows ver-
the .WAV file, and the constant snd_Async is used for the play tically down the screen? Check out Figure 7. ∆ — Russ Acker,
mode. The most commonly used values for the play mode Ensemble Corporation
parameter are snd_Sync and snd_Async. Additional parameters
can be found in the MMSYSTEM unit located in the \DEL-
PHI\SOURCE\RTL\WIN directory. Specifying snd_Sync will The demonstration projects referenced in this article are available
suspend your application until the .WAV has completed play- on the 1995 Delphi Informant Works CD located in
ing. Specifying snd_Async will play the sound in the back- INFORM\95\DEC\DI9512DR.
ground while your application continues running.
By Blake Versiga
This article will reveal some of the mysteries behind Delphi’s Outline control, and discuss
various ways to work with it. For instance, it can be completely configured at design-time or
run-time (or variations of the two) by querying a database. In addition, owner-draw capabili-
ties will be explored. The example program provides an efficient user interface for displaying
the family tree of Great Pyraneese dogs.
Distinct Markings
The Outline control (TOutline class) is on the Additional page of the Component Palette.
This control is an array of OutlineNodes (TOutlineNode class) with indexes from 1 to the
number of items in the list. For this reason, it’s important to distinguish between the Outline
control and each item (or OutlineNode).
Both Outline and OutlineNode have individual sets of properties, methods, and events. The
attributes of Outline affect it as a whole. On the other hand, attributes of the OutlineNode affect
only the specific nodes of the Outline. Each item in the Outline can have 0 to 16368 sub-items.
Its large number of properties and methods make Outline a flexible component. For example,
the PictureClosed, PictureLeaf, PictureMinus, PictureOpen, and PicturePlus properties contain
standard File Manager-like graphics that can be customized for a particular look. If five graph-
ics will not meet the challenge, however, the Outline component comes to the rescue with its
owner-draw capabilities.
Opening Eyes
Let’s begin with a small example program to demonstrate some important Outline compo-
nent concepts. First, create a new project. Place an Outline component on the form and then
drag its handles to enlarge it.
Next, double-click on the Outline’s Lines property in the Therefore, changing this property does not affect the con-
Object Inspector to display the String list editor. Each line trols when the Style property is set to otStandard. Style can
represents a node in the Outline. Press CF or s be set to otOwnerDraw to activate the ItemHeight property.
to add one level of indention to the node. If spaces are used, If there is no code in the OnDrawItem event, Delphi uses
the String list editor will convert them to tabs before saving. the default drawing method. This allows the standard pic-
tures to be enlarged without the additional complexity of a
The sample application initializes each node to include the full owner-draw control.
name of the dog followed by a character denoting its gender
(see Figure 1). This is used to determine which bitmap should Barking at the Moon
be associated with the node when the Style property is set to Now that we have a functioning application, it’s essential to
otOwnerDraw. After adding entries in the String list editor, capture events and respond to the user’s interaction with the
run the application by pressing 9. The Outline will expand control. Typically, the developer must perform some tasks
and collapse without any additional code (see Figure 2). when the user clicks on a node of the Outline. The following
example modifies the form’s caption with the Outline index
and the full text path of the selected item. The index is a one-
based long integer indicating the ordinal position of the item
with respect to the entire list.
The full text path represents the text and separators of the
nodes the user must navigate to arrive at the selected node.
The ItemSeparator property determines the character string
used to delimit items in the FullPath property. To illustrate
these properties, simply add this line of code to the Click
event of the Outline control:
click in the Values column of the OnCreate event. Now Information about the
enter a statement to allocate storage space on the stack for node is used to determine
each bitmap, for example: exactly how it will be
painted. In our example,
BitmapVariable := TBitmap.Create;
the current node is drawn
highlighted and a different
Next, initialize each bitmap. If the bitmaps reside on disk, the
bitmap is used depending
LoadFromFile method can be used:
on the dog’s gender. A
BitmapVariable.LoadFromFile('Bitmap.bmp'); bitmap with an underlined
M in the lower left corner
The bitmap is now allocated on the stack, so the memory represents male dogs, and
must be freed when it’s no longer needed. To accomplish females are denoted with
this, use this statement in the OnClose event: an underlined F (see
Figure 3). The example
BitmapVariable.Free; project at runtime is
shown in Figure 4, and the
Finally, the Outline node must be drawn when indicated project source is shown in
by the Outline control. As mentioned earlier, the Outline Listing 9 on page 49.
control calls the OnDrawItem procedure whenever a cell
requires repainting. This routine should clear the canvas Conclusion
and paint any pictures, graphics, or text. The OnDraw Now that we have mas-
procedure is also responsible for showing hierarchical tered the fundamentals of
indention, if necessary. Here’s the prototype for the owner-draw outlines, we Figure 3 (Top): The male and
female Pyraneese bitmaps. Not just
DrawItem procedure: can apply these techniques pretty faces, these bitmaps demon-
to other owner-draw con- strate how the DrawItem procedure
procedure TForm1.Outline1DrawItem(Control: TWinControl; trols that are handled in a and GetItem method are used to
Index: Integer; Rect: TRect; State: TOwnerDrawState); properly paint the appropriate node.
similar manner. Additional Figure 4 (Bottom): The sample
owner-draw controls project at run-time.
The Control parameter contains a reference to the Outline
include TComboBox,
control. Index is the position of the item in the Outline con-
TDBComboBox, TDBListBox, TListBox, and TOutline.
trol. Note that this does not correspond to the node’s index,
but represents the ordinal index of visible items within the
Owner-draw controls offer tremendous flexibility to the pro-
Outline. Rect is the area available to the node. The State is
grammer. These techniques can be used to display anything
defined as:
from hierarchical data relationships to sections of text (e.g.
TOwnerDrawState = set of (odSelected,odGrayed,odDisabled, the Microsoft Word for Windows Outline feature). The
odChecked,odFocused); Outline control is a master of distilling tremendous amounts
of information into manageable chunks of data. The key is
The State parameter will contain information required to creativity. Now it’s your turn to bark up a storm. ∆
determine how a node will be drawn. Within the
DrawItem procedure, the code should paint the node. A The Delphi project referenced in this article is available on the
typical scenario involves clearing the area that will be 1995 Delphi Informant Works CD located in
painted, drawing the graphic, and finally, painting the text. INFORM\95\DEC\DI9512BV.
As mentioned earlier, the index passed to the OnDraw
event does not represent the node Index that will be
drawn. To obtain the actual Index, the GetItem method is Blake Versiga is a Systems Engineer at Computer Language Research. He has been
used. It translates any X,Y point to a node Index. Given involved in producing corporate solutions ranging from Pen-based computing to large
the node Index, all the pieces have come together. client/server applications. He can be reached on CompuServe at 73123,2737 or via
Internet mail at 73123.2737@compuserve.com.
s: string;
Begin Listing Nine — Outline Demo Program x,y,
program Project1; Offset: Integer;
ItemIndex: LongInt;
uses Node: TOutlineNode;
Forms,
Unit1 in 'UNIT1.PAS' {Form1}; begin
ItemIndex := Outline1.GetItem(Rect.Left, Rect.Top);
{$R *.RES} Node := Outline1.Items[ItemIndex];
ReportPrinter
Nevrona Designs’ New Report Writer
Let’s Compare
Is ReportPrinter a replacement for traditional report writers
like Crystal Reports and ReportSmith? Definitely not. Each
has its place defined by its strengths and weaknesses. With
ReportPrinter you get:
• Integration. ReportPrinter is a set of native Delphi
components to add to your application. Everything is
compiled into the .EXE file. There are no separate
.DLLs or report files to distribute. And since the report Figure 1 (Top): A ReportPrinter report containing multiple tables
is created entirely by compiled Pascal code, you get the in various formats. Figure 2 (Bottom): The second page of the
output fast. report shown in Figure 1 containing mixed graphics and text.
Now available, Crystal Reports version 4.5 is it allows you to set the title for the report methods. The “readme” file is no help
a welcome addition for Delphi developers. It preview window. either. It is simply a list of changes and bug
ships with a native Delphi component that fixes to prior versions of the component.
provides an easy interface to the Crystal print Once you have set the required properties, You may expect this in the readme for a
engine through a host of properties and run the report by setting the Action property beta version of a shareware product, but
methods. to 1. While these are the properties you will not in the shipping version of a commercial
use most, the Crystal component includes product. The best advice I can offer is to
Basic report printing and viewing is very over three pages of properties you can set look at the documentation for the VBX in
easy. After adding the Crystal component via the Property Inspector, and even more the Crystal Reports Developer’s Reference
to the Delphi Component Palette you will that you can set at run-time in your code. and use that as a guide for getting started
find it located on the Data Access page. This provides access to just about all the with the Delphi component.
Just drop a TCcrpe component on your flexibility for which the Crystal print engine
form and set the following properties to is renowned. The version of the Delphi component that
print your report: ships with Crystal Reports 4.5 is 1.06. By
Unfortunately this component is seriously the time you read this review, version 1.08
CopiesToPrinter. This property defaults to 1 flawed by bad design and poor documen- should be available on the Crystal bulletin
so you will only need to change it if you need tation. The component is supposed to pro- board and on their CompuServe forum. The
more than one copy of your report. vide a native Delphi alternative to the pre-release copy of version 1.08 that I saw
Crystal VBX that shipped with earlier ver- includes a much more complete help file
Destination. This property determines where sions of Crystal Reports (which you may and a demonstration program that shows
your report will go. You can select from any have been using in your Delphi applica- you how to use many of the component’s
of the following constants: tions). However, the designers changed the features. This new version is worth the
• toWindow names of some of the properties and many download time just for the help file and the
• toPrinter of the constants used to set properties. For demonstration program.
• toFile example, the VBX property that stores the
• toEmailViaMAPI name of the report to run is The Crystal Reports component is a welcome
• toEmailViaVIM ReportFileName, while in the Delphi com- tool that makes it easy to use this powerful
ponent it is ReportName. Another example report writer in your Delphi programs, while
ReportName. The name of the file that con- is that the PrintReport method from the VBX giving you access to all the features of the
tains the Crystal report to print. is not supported in the Delphi component. Crystal print engine. Although it will take
This means that converting your existing some time, it is worth your while to convert
WindowState. You need only be concerned programs from the VBX to the Delphi com- your programs from the VBX to the Delphi
with this property if the Destination property is ponent is more work than it should be. component. You will no longer need to dis-
set to toWindow. To control the size of the tribute the VBX file, and you’ll be ready to
report preview window, WindowState can be set Learning to use the component is a chal- compile your program with Delphi32 where
to wsMaximized, wsMinimized, or wsDefault. lenge too. The only documentation is a VBXes are not supported.
Windows help file and it is woefully incom-
WindowTitle. This property is optional, but plete. While it does include the component
if you are sending the report to a window, properties, it does not include any of the — Bill Todd
• Small size. Adding a report to an application increases the With a traditional report writer you get:
.EXE file size by about 30KB for the first report, and less for • Fast, easy, WYSIWYG development. You can see what the
subsequent reports. Contrast that with the megabytes of finished report will look like as it’s created. This saves time.
additional files distributed with a conventional report writer. • Instant calculations. Traditional report writers provide
• Control. You can put anything, anywhere, on any page, in fast, easy sorting, grouping, and subtotaling. Most offer
any order to get the report you want. Since the position is other functions such as counts and averages. You must
specified in your code in inches or centimeters, each write code to perform these calculations in
object on the report prints exactly where you want it. ReportPrinter.
• Direct access to data. Since a report is part of an appli- • A large run-time environment. This is the drawback to
cation, you access data using Delphi’s Table, Query, and traditional report writers. To create reports with a tradi-
StoredProc components. There is no separate database tional report writer, you must distribute its run-time
connection for your reports, which is especially conve- environment with your application. This means 2 to
nient for printing them based on the current record. 4MB of DLLs and supporting files, plus at least one
• Slower development time. This is the drawback to file for each report.
using ReportPrinter. Because you create each report by
writing code, it takes longer to design most reports Consider the Options
using ReportPrinter than with a traditional report When you need to include reports with custom applications,
writer. This is particularly true if the report requires consider which tool is best suited for the job, as well as the
numerous groups and subtotals. time you are willing to invest creating the reports.