DELPHI 2 Unleashed PDF
DELPHI 2 Unleashed PDF
This edition of Delphi 2.0 contains a vast amount of new material. Here are some of the major new sub-
jects covered in this edition of the book:
■ Win32 Programming: An in-depth ■ Delphi Under Win32: A lengthy chapter
discussion of threads, multitasking, Win32 explains the new Delphi long strings and
memory management, critical sections, shows how they differ from PChars and
mutexes, and memory-mapped files. The from the 256-character short strings used in
text helps you understand how the Delphi 16. The text also covers variants,
Windows 95 and Windows NT 32-bit variant arrays, and the impact that 32-bit
architectures impact Delphi programming. programming has had on fundamental
■ Windows API Programming: How does Delphi types such as integers and pointers.
the Windows API fit into a Delphi ■ The New Database Features: Delphi 2.0
program? Examples show how to construct includes multi-record objects, a
DLLs and executables entirely out of TDataModule object for storing nonvisual
Windows API calls, without using the VCL. components such as TTable and TQuery, a
This section gives you a close look at the Database Explorer, a Data Dictionary, new
API that forms the basis for all Windows lookup combo boxes and list boxes, new
programming, and which underlies the grids that support colors and embedded
structures at the heart of the VCL. combo boxes, a new filter and searching
■ Internet Programming: This part show technology, as well as the new 32-bit
how to write CGI applications using Delphi Borland Database Engine. All of these
objects, and gives other techniques for features are explored in easy-to-use sample
tapping into the full power of the Web. programs.
This part of the book explains how you can ■ Much more: There are many other new
create database programs that interact with features in this edition of Delphi Unleashed.
users while they are signed on to your Web Sample programs on the CD show how to
site. use form inheritance, the Delphi memory
■ Using the Component Object Model manager, the DllEntryPoint, cached
(COM): This section shows how to updates, the TThread object, the new and
construct objects that can be used by C++ improved TOleContainer object, and
programs, as well as how to access C++ hundreds of other examples that show the
objects inside Delphi programs. You can right way to construct robust 32-bit
share these same objects with Visual Basic programs.
and other programming environments. Delphi 2.0 is an almost unbelievably powerful pro-
■ Using the Games SDK: The Games SDK gramming environment. In this book I try to look
is written using COM, so its objects are beneath the surface of Delphi to see how it works. You
fully accessible from inside a Delphi can use this knowledge to help you construct robust
program. This book includes Mike Scott’s programs that take advantage of the full power of
interface files that enable you to access all Windows NT and Windows 95.
the features of the DirectDraw API. Also
included is a sample 3-D maze built into On the CD are 15 chapters that were dropped from
Delphi 2.0 using the Games SDK. the text in the second edition of this book. These
■ OLE Automation: This part of the book chapters include much of the information you need
shows how to write Automation clients and to get started using Delphi. All of the sample pro-
servers using the built-in Delphi objects, grams from the first edition of the book are also in-
and how to write IDispatch interfaces so cluded on the CD.
that you can access Ole Automation on your This is a big book crammed with lots of information
own terms. This text shows how you can and hundreds of sample programs. I’ve tried to in-
control other programs from inside Delphi, clude the information you need to access the most
and how you can write Automation servers powerful and advanced features of Delphi 2.0.
that can be controlled by other programs.
Delphi2
UNLEASHED Charles Calvert
Overview
vi
UNLEASHED
Foreword xxi
Part II Win32
7 Multitasking and Threads 197
8 Win32 Memory Management 245
Part IV Databases
15 Using TTable and TDataSet 497
16 SQL and the TQuery Object 537
17 Fields and Database Tools 565
18 Real-World Databases 603
19 Working with the Local InterBase Server 653
20 Advanced InterBase Techniques 679
21 ReportSmith 727
Part V Objects
22 Handling Messages 763
23 Objects and Inheritance 795
24 Objects, Encapsulation, and Properties 831
25 Polymorphism 861
26 Creating Components 905
27 Creating Nonvisual Components 941
Contents
viii
UNLEASHED
Part II Win32
7 Multitasking and Threads 197
Processes and Memory ................................................................. 198
Multitasking and Threads ............................................................ 201
Preemptive and Nonpreemptive Multitasking ......................... 203
Part IV Databases
15 Using TTable and TDataSet 497
Database Basics ............................................................................ 499
The DataModule ..................................................................... 501
The Object Repository ................................................................. 503
The Database Explorer ................................................................. 505
Understanding the TDataSet Class............................................... 506
Opening and Closing Datasets ..................................................... 508
Navigational Routines .................................................................. 509
Checking for the End or Beginning of a DataSet .......................... 511
Fields ........................................................................................... 513
Changing Data ............................................................................. 516
Using SetKey or FindKey to Search Through a File ....................... 519
Filtering the Records in a DataSet with ApplyRange ...................... 522
Filtering with the OnFilterRecord Event ...................................... 524
Using the Refresh Function ........................................................... 526
Bookmarks ................................................................................... 527
TTable ......................................................................................... 528
Part V Objects
22 Handling Messages 763
Delphi Events .............................................................................. 764
Understanding Events.............................................................. 769
Using Sets to Track Messages ....................................................... 771
Tracking the Mouse and Keyboard .............................................. 776
Foreword
Delphi 2 Unleashed is a great way to learn all of the programming techniques that Delphi of-
fers. Whether you’re a seasoned Delphi programmer looking to get the latest tips and tech-
niques of OLE for Delphi 2.0 or a newcomer learning database fundamentals, this book offers
what you need to be productive, because it’s written by someone who’s worked with Delphi
from the start.
I remember back in early 1993, Charlie came to see me because he was concerned about what
was going on in Borland’s languages group. Charlie was manning the tech support phones and
had written two very successful books, Turbo Pascal 101 and Teach Yourself Windows Program-
ming in 21 days (published by Sams).
He wanted to know where we were going to take the technology in the future. “We’ve got to
make it easier, Zack. It’s too hard to program for Windows.”
So I took Charlie aside and told him about a new product under development. It was so top-
secret it didn’t really have a name, and no one outside of a core group of developers knew about
it. Sometimes we called it “Visual Foo” since we didn’t have a name; a few months later we
came up with the codename “Delphi,” and it stuck.
Although Delphi 1.0 was still under development, Charlie joined the Developer Relations group
at Borland to become a Delphi evangelist and guru. One of his goals was to make sure that we
had a large number of third-party consultants, system integrators, and developers supporting
Delphi when it was released. As a result, Charlie was involved with Delphi since its earliest
incarnations and has breadth and depth of knowledge that puts him in a unique position as an
instructor.
Before we shipped Delphi 1.0, preliminary versions of this book were used to train thousands
of consultants, system integrators, and third-party developers worldwide. Charlie refined the
material in a three-day seminar that he taught to audiences of programmers in Scotts Valley,
Amsterdam, and Munich.
Charlie is also known to many developers from his Delphi technical presentations and tutori-
als at dozens of user groups and industry conferences such as Software Development and
Comdex.
One of the things you’ll notice as you read this book is that Charlie has a friendly style that is
open and accessible. Charlie explains the fundamentals of Delphi programming as well as ad-
vanced topics without ever sounding condescending. Maybe it’s all the time he’s spent on the
phones helping customers find and fix bugs in their code, or perhaps its the live experience of
teaching audiences, but the end result is that throughout this book Charlie conveys a feeling of
a one-on-one tutorial.
Charlie has worked closely with the development, quality assurance, and documentation teams
responsible for creating Delphi. In the final months before Delphi 1.0 shipped, we called on
Charlie to help write material that would be part of the database manual. He came through
with information that has helped hundreds of thousands of customers understand how to build
PC LAN and client/server database applications in Delphi. And just a few months prior to the
release of Delphi 2.0, I called on Charlie again, this time begging for some material on OLE
Automation. Charlie worked closely with Anders Hejlsberg, the chief architect of Delphi, to
explain the inner workings of OLE automation. Charlie took a complex topic and made it
approachable. He tells what you need to know to make it work using practical tips and clear
examples. You’ll find additional expanded material on OLE automation in this book.
Even if you have the first edition of this book, you’ll find plenty of new material explaining
how to take advantage of new Windows 95 and NT features such as OCXs and OLE automa-
tion, multi-threading, the Direct Draw API, COM (Common Object Model) objects, and new
advanced data controls.
You’ll enjoy learning to use Delphi 2.0 to create high-performance desktop and client/server
applications. With Charlie’s book you’ve got a guide to take you there.
–Zack Urlocker, Director of Delphi Product Management at Borland International, January
1996
Acknowledgments
Several Borland employees answered many questions for me while I was working on this book.
My thanks to Steve Trefethen, Nimish Vora, Dan Daley, Jason Sprenger, Steve Teixeira, David
Intersimone, Gary Whizin, Allen Bauer, Zack Urlocker, Scott Clinton, Lar Mader, Scott Frolich,
and Bruneau Babet. In particular, I want to thank Danny Thorpe for several lengthy conver-
sations in which he provided remarkable insights into complex subjects.
There are others who I work with day to day who were a big help to me. They include Karen
Giles, Nan Borreson, CJ Martin, Lisa Coenen, Yolanda Davis, Nancy Collins, and Christine
Sherman.
At Sams, Chris Denny, Angelique Brittingham, and Mary Inderstrodt were, as always, a tre-
mendous help. Chris, Angelique, Mary, and I have worked together on several projects, and I
owe them all a big debt of gratitude for their good work, patience, and perseverance.
My deepest and most heartfelt thanks, however, are reserved for my wife Margie, who has
cheerfully endured a rather hectic year and a half in which I got way, way, way too much writ-
ing done. Talk about the man who went too far! I should perhaps also add a note of thanks to
my cats Valentine and Rupert. As is often the case, Rupert is here, literally in my lap, as I write
this, and Valentine (incredibly enough) chose this very moment to run across the keyboard,
leaving his own cryptic message on the screen. And after I summarily deleted it, who can blame
him if he’s now waiting patiently at the door to be let out?
I
IN THIS PART
Object Pascal
This section of the book covers several fundamental Object Pascal program-
ming issues. It looks into some reasonably technical subject matter
regarding arrays, pointers, strings, and files. Other matters covered in this
section include new Delphi types and how some of the old types made the
port to 32 bits.
Visual Programming
Is RAD 1
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
4
Part I
Welcome to the second edition of Delphi Unleashed. Over fifty percent of the material in this
book is new, including new chapters on the Internet, the Windows API, database tools, COM
objects, the Microsoft Games SDK, and Win32 programming.
Not all the material that comes with this package is evident from the pages you hold in your
hands. There is also a CD loaded with megabytes of code and 15 chapters of text explaining
the fundamentals of Object Pascal. These chapters were removed from the first edition of the
book to make room for more advanced and up-to-date material. However, some of the chap-
ters were favorites with readers, so they are included on the CD.
You will find that the text of the book is all about Windows 95 and Windows NT program-
ming, whereas some of the chapters on disk address Windows 3.1 programming. Almost all
the chapters on Delphi 2.0 cover intermediate or advanced material, whereas some of the chapters
included only on the CD, cover material of interest to newcomers. As a result, many facets of
the Delphi programming world are available to you in one large package that is loaded with
information.
I have also called on three experts to write about the Internet, ReportSmith, and the Games
SDK. These are subjects that interest me, but that I didn’t have time to write about in my own
words. As a result, you get four bonus chapters in this book from people who make their living
working full-time in these cutting edge fields. I’ll talk more about Ann Lynnworth, Mike Scott,
and Andy Fearns later in this chapter, but their expert opinions and long professional experi-
ence should prove useful to most readers.
Delphi 2.0
Before beginning a discussion of the contents of this book, it is perhaps worth taking a mo-
ment to talk about Delphi 2.0. I will make this portion of the chapter very brief, as I’m sure
you are already impressed with Delphi, or you wouldn’t be interested in this text.
What’s important to me about Delphi is not the fact that it has won so many awards, garnered
so many kudos, or sold hundreds of thousands of copies. These things are nice, and they are
part of what makes a product successful. However, Delphi’s strongest features become appar-
ent when one sees the way programmers react to daily use of a true object-oriented compiler.
Serious programmers love Delphi because of its powerful, high performance compiler. It’s a
tool for professionals, for those who care about the quality of the products they use and make.
Once again, I don’t want to simply list off all the great features of Delphi 2.0 because I don’t
think they are what’s really important. You might be impressed by the Database Explorer, by
Visual Form Inheritance, by the OCX support, and indeed these features are important. But
they aren’t the core of what makes Delphi great.
The best things about Delphi are its compiler, its superb object-oriented architecture, and its
support for native components. Its key features are the simple elegance of Object Pascal itself;
the power, size, and speed of its compiled code; and its ability to support every last nook and
cranny of the Windows API.
The closer I get to the core of Delphi, the more impressed I am by its features. It’s not the
flashy things that you notice at first blush that make this product great, nor is it the esoteric
features we show off at demos and trade shows. It’s the core of the product that’s important.
The key elements are the 32-bit Windows API friendly compiler, the support for inheritance
and polymorphism, and the support for reusable native components with their easy-to-use prop-
erties. These are the features that make Delphi great, and they are the ones that will be pushed
to the fore, most often in this book.
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
6
Part I
■ Part V, “Objects:” The fifth section of the text plunges into the heart of object-
oriented programming. It includes chapters on inheritance, encapsulation, and
polymorphism, as well as a description of how to create both visual and nonvisual
components. This section, and the chapters on the Windows API, contain much of
my favorite material.
■ Part VI, “OLE and COM:” The sixth section explores OLE and the COM object
model. There are chapters on DDE, object linking and embedding, OLE automation,
the method for implementing the Component Object Model in Delphi, and the
Microsoft Games SDK. There is also some general discussion of OCXs, although this
topic is not explored in depth.
■ Part VII, “Multimedia and Games:” The chapter on the Games SDK was written by
multimedia expert Mike Scott. He was one of Delphi’s earliest adopters, and he has
proven his expertise in a number of settings, including construction of a popular
Delphi WinG add-on.Creating multimedia programs with visual tools is the subject
matter of the two chapters in this section of the book. The first multimedia chapter
probably contains the easiest to understand material in this text. The second chapter
in this section, on advanced multimedia, takes an in-depth look at the MCI interface
to the Windows multimedia services. This chapter can be viewed as either a behind
the scenes look at the code for the Delphi TMediaPlayer component, or an introduc-
tion to the techniques you need to write directly to the the most important Windows
multimedia interface.
■ Part VIII, “The Internet:” The final section of the book, by guest author Ann
Lynnworth, explores the Internet. Ann and business partner Michael Ax have used
Delphi to create a series of Web-based tools that give you powerful access to some of
the most advanced features in contemporary programming. Ann shares many of her
technical secrets with you and gives you a working copy of her code so you can see
how to program the Internet.
You might notice that this book is loaded toward the middle and the end, rather than toward
the front. A lot of programming books put their best chapters up front, which is not the case
with this particular book. Instead, I start with some general chapters on Object Pascal and then
plunge into some of the more interesting chapters as the book wears on. The book is arranged
this way on purpose, and it is best to read it from beginning to end. You should remember,
however, that some of the most interesting material is found in the last third of the book.
Overall, this text is aimed at readers who are looking for insight into how Delphi and Win-
dows are constructed and who want to know something of the theory behind this architecture.
There are sections of the book that have a “how-to” structure to them, but this is not the main
thrust of the book. As a rule, I have tried to stick to subjects that I feel are of general interest to
a broad range of Delphi programmers.
I’m primarily interested in teaching deep-rooted knowledge that applies to a broad range of
situations, rather than specific techniques for resolving particular problems. My feeling is that
if you understand how and why Delphi works, you will always be able to find the solution to
your current programming challenge. It’s a little like the old saying about it being better to
teach someone how to fish, than to merely give them a fish. I want this book to be more than
a list of tips and tricks—I want it to show how to think about, design, and construct Delphi
programs.
NOTE
You should also look on the CD for additional programs that might not be included in
the text. As a rule, I try to organize all the related code in the appropriate chapters. For
instance, if I have code on COM programming that I never got a chance to write up, I
put it in the same place on the CD where I store the standard COM programs men-
tioned in the text. Check the ReadMe file on the CD for additional information.
Some readers may have a legitimate need to get in touch with me. If you have pressing
questions about this text or its accompanying code, you can reach me on CompuServe
at 76711,533, on AOL at CHARLIECAL, and on the Internet at [email protected]
.com. Additional information might appear at www.borland.com or at the Sams Web
site, http://www.mcp.com. On CompuServe, type GO SAMS to reach a site where infor-
mation related to this book might appear. I can supply updates and bug fixes if you
write me directly. If key updates or fixes need to be published, I will put them in a file
called UNL2UP.ZIP and place them online at the various sites.
If you have general technical questions about Delphi, you can get support online at:
Compuserve:
GO DELPHI
Internet:
comp.lang.pascal.delphi.misc
comp.lang.pascal.delphi.components
comp.lang.pascal.delphi.databases
comp.lang.pascal
comp.lang.pascal.borland
alt.lang.delphi
alt.comp.lang.borland-delphi
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
8
Part I
In particular, the VCL has a tendency to hide the exact implementation of certain objects, thereby
preventing people from changing the code’s default behavior. For instance, Delphi makes it so
easy to put up a list box that it’s hard to see how you can use some of the fancy Windows API
messages that enable you to create columns in a list box or access other advanced features.
The point isn’t that Delphi’s tools don’t work. The issue is that sometimes they work all too
well! Delphi presents such a smooth, powerful, interface that it’s hard to see that right beneath
the surface is the key to Delphi programming excellence: the Windows API!
One of the goals of this book is to look beyond the VCL to the underlying API code. The VCL
is built on the Windows API; and if you want to understand the VCL, you also have to under-
stand the Windows API.
There is only one Windows API function that you cannot call from Delphi, and that routine’s
functionality is duplicated both in the Windows API itself and also internally in Delphi. In
other words, there is nothing involving Windows programming that you can’t do with Delphi.
But to know how to do it, you have to know the Windows API.
NOTE
The Windows API function you can’t call from Delphi is called wvsprintf. The
Windows API function that duplicates its functionality is called wsprintf, and the
internal Object Pascal call that duplicates its functionality is called Format. There are
no other Windows API functions you can’t call from Delphi. You can, of course,
handle all the Windows API messages from inside Delphi.
The point here is that besides Delphi’s wonderful strengths as a RAD tool and as a database
tool, it also gives you full access to the Windows API. It can do this while producing code that
is similar in quality to the code produced by the Borland C++ compiler and yet sometimes
smaller and more compact. In fact, Delphi and C++ use the same compiler. Each language
parses the syntactical elements of the code and then passes a series of tokens on to the same
32-bit optimizing compiler.
In short, Delphi is an excellent tool to use if you want to make full use of the Windows API.
The only problem with this scenario is that many Delphi programmers don’t know how to dig
beneath the surface of the Delphi RAD tools to use the full power of the Windows API. One
of the major goals of this book is to remedy that situation.
There is an entire section of this book, six chapters long, that digs into the Windows API. The
section on Win32 also presents many of the key features that need to be understood by Win-
dows programmers. After reading these eight chapters, you will understand how to use Delphi
to create tiny executables that contain nothing but Windows API code. These programs are
about one-tenth the size of a standard Delphi RAD program. You will also see how to create
tiny DLLs using the Windows API.
Most importantly, however, the emphasis on the Windows API in this book enables you to dig
beneath the surface of Delphi and tap into its real power. If you want to create sophisticated
Delphi components, you have to know the Windows API. If you want to stretch Delphi to its
limits, using the full capability of both the RAD tools and the powerful Windows 95 and
Windows NT operating systems, you have to know the Windows API.
If you understand the Windows API, everything else is possible for you. If you don’t under-
stand the Windows API, you will keep running into brick walls.
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
10
Part I
revealing book. I want to bore beneath the surface of Windows and Delphi and show you how
and why they work. Armed with that wealth of knowledge, you will find that tasks that previ-
ously seemed complicated or beyond your reach are suddenly easy to accomplish.
In particular, you will find that there are many fewer occasions when you simply can’t accom-
plish a goal. Whereas before you might have hit a brick wall, after reading this book you should
have enough theoretical knowledge to know how to solve many problems that used to present
an opaque and confusing surface to you. If you understand the theory behind the tools, you
will understand what they can and can’t do, and how to go about reaching your goal.
The calls shown here are from MMSYSTEM.PAS. Notice that the mciGetDeviceID function is
declared three times. Once it is declared as used in ANSI code, once as used in Unicode, and
the third time as used in standard Delphi applications. The first and third declarations are syn-
tactically identical in this case, the only difference being that the first has an A appended to its
name. You should get used to seeing declarations that have an A appended to them in ANSI
form and a W appended to them to show that they are the Wide, Unicode declarations.
The important point to remember in this regard is that Unicode takes double-wide characters
where ANSI takes only single-byte characters. In particular, Unicode uses a 16-bit value where
ANSI uses only an 8-bit value. This allows Unicode to use a much wider range of characters—
65,000 in all. ANSI characters, on the other hand, are limited to only 256 possible elements.
In general, the only time you want to use Unicode is when you are working with COM or
OLE programs. The Delphi VCL is not Unicode-aware at this time.
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
12
Part I
Despite the brief discussion of Unicode that appears here, the main thing I want to communi-
cate in this section is that readers of this book will be better off if they have a copy of the VCL
source. It is available from Borland or from third-party venders such as the Programmer’s Shop
or Programmer’s Paradise.
audience are required because there are four bytes in an Integer. If the CPU wants to work
with a Word value, only two members of the audience are required. For working with
ShortStrings, the number of participants is 256, and so on.
Once again, the memory theater is not a perfect analogy, but it takes the events that go on
inside a computer out of the realm of the abstract and makes them tangible. It’s easy to imag-
ine people sitting in the seats of a theater. Everyone can grasp that concept. It’s harder, how-
ever, to understand what it means to manipulate a byte of memory in RAM. Therefore, for the
sake of making the concepts introduced in this book as clear and simple as possible, I intro-
duce a small conceit called the Memory Theater. If you want a detailed look at this analogy,
view the basic chapters on types that come on the CD.
NOTE
The Component Palette is the place on the upper-right side of the Delphi IDE where
you can snag TEdit, TButton, TTable and other controls before dragging them onto
the main form. If this concept doesn’t make sense to you, you might want to consider
backing away from this book for a bit and reading a more introductory text. There are,
in fact, many more introductory chapters on the CD of this book, and also at your
local computer book store.
In general, I don’t regard this as a particularly difficult book. Almost all the key
technologies in Delphi are made readily available to you. As a result, there is no code in
this book that is brutally difficult to understand. However, the text does assume that
you are already up and running with Delphi and that you know some programming
basics.
By the way, the word IDE stands for Integrated Development Environment, and it is
the term I use to refer to the complex of Windows you see when you load Delphi into
memory. The IDE includes the toolbar, Component Palette, Object Inspector, editor,
integrated debugger, the watch window, and anything else you access from Delphi
proper. The Delphi IDE stands in contrast to the Delphi command-line tools such as
DCC32.EXE or BRCC32.EXE.
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
14
Part I
One good way to come to terms with the Component Palette is to take it out of the environ-
ment and see what you have left once it’s gone. To do this, first make sure Delphi is shut down,
then open up the Windows Explorer and find the CMPLIB32.DCL file in the ..\DELPHI 2.0\BIN
subdirectory. Use the Explorer to move this file out of the BIN subdirectory and into a
subdirectory you create called BAK.
NOTE
I caution you to proceed with extreme care whenever you work with
CMPLIB32.DCL. This file is vital to Delphi, and you should always keep at least one
good copy of the file in a subdirectory on your hard drive. This is a large file, so you
might not be able to put it on a floppy without first zipping it up. If you work on a
laptop, you should be absolutely certain that you keep a backup of this file on your
hard drive. If worst comes to worst, you can always reinstall Delphi, or simply copy
CMPLIB32.DCL off the install CD.
There is some chance that this file is not called CMPLIB32.DCL on your system. To
check this, first open Delphi, then choose Component | Install from the menu. The
Library filename is listed at the top of the Install Components dialog box.
After shutting down Delphi and moving CMPLIB32.DCL into the BAK subdirectory, you
should then launch Delphi. You will get an error message saying that Delphi can’t find or load
CMPLIB32.DCL. Don’t let this worry you. Just click the OK button and proceed.
If you try to compile a default Delphi project under these circumstances, you will get an error
of one kind or another, probably one reading: File Not Found: ‘Unit1.DFM’. To remedy this
situation, bring up the Project Manager and remove, without saving, UNIT1.PAS from your
project. Go to Tools | Options | Preferences and make sure you have not chosen to Autosave
your projects. You don’t want to try to save this file. Select View | Project Source so you can
take a look at the DPR file for the program.
Now modify the DPR file for your program so it looks like this:
program Project1;
uses
SysUtils;
begin
WriteLn(‘Creeds and schools in abeyance’);
ReadLn;
end.
Go to the Project | Options | Linker menu choice and select Generate Console Application.
Now close the Project Options dialog box and, if you like, you can also remove the uses clause
from your program. (My copy of the Delphi IDE gets flustered if my main file doesn’t have a
uses clause.)
Now compile and run your application. You will see that it pops up a small console applica-
tion in a DOS box. In the box you see a brief quotation from Walt Whitman’s “Song of
Myself.” If you want, you can open up a stand-alone DOS box and run this program from
the command prompt.
Don’t let the appearance of this program deceive you. It’s not a DOS program; it’s a full 32-bit
Windows program that runs from the command prompt. If you want, you can add Windows
to the uses clause and call MessageBox from this application:
program Project1;
uses
Windows;
begin
MessageBox(0, ‘Test balloon!’, ‘Hello from the command line!’, mb_ok);
end.
You can also allocate huge arrays of over 64 KB in applications of this type. It is true 32-bit
code.
What has this tiny example demonstrated? Two key things:
■ You can’t do any visual programming without the Component Palette.
■ You can, however, compile and run an application.
In particular, you can see that the Component Palette has nothing to do with Delphi’s
compiler. This is a good thing. It’s a sign of integrity. I would have a hard time trusting a pro-
gramming environment that was so intimately wedded to its visual tools that it could not run
without them. You want the sense that something solid lies at the heart of your programming
tool, and, in this case, you can see that Delphi’s world class compiler is still there even if the
visual tools are absent. This is the same compiler that drives Borland C++ 4.52 and Borland
C++ 5.0. It’s as good a compiler as you will find anywhere on the market, and it was designed
by one of the best minds in the computer industry.
Re-Creating CMPLIB32.DCL
Now that you have seen how to remove CMPLIB32.DCL from the environment, the next
step is to see how to put it back in place. One simple way to do this is to simply copy
CMPLIB32.DCL from the BAK subdirectory you created earlier into the BIN subdirectory.
Now choose Component | Open Library from the menu and select the CMPLIB32.DCL file.
Click the OK button and everything is back to normal. This is how you should proceed if
anything ever causes any damage to your CMPLIB32.DCL file. (Remember that if you don’t
have the file in a BAK subdirectory, you can still retrieve it from the install CD.)
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
16
Part I
Set the Library filename equal to the name you want to assign to your new library:
c:\Program Files\Borland\Delphi 2.0\bin\mylib.dcl
You can call it CMPLIB32.DCL if you like, or give it a new filename if you think you might
want to keep it around. It is, in fact, possible that you will want to keep the file because Delphi
lets you custom craft Library files that have special uses.
NOTE
I don’t really recommend installing Delphi into the preceding path, but my version of
the product installed itself there by default. On my main machine, I moved Delphi
into the C:\DELPHI32 subdirectory. To move the product, it’s best to uninstall first
from the Control Panel, then reinstall into the new subdirectory. This is not a pressing
issue, however, and you probably shouldn’t bother moving Delphi at this particular
stage of the game.
Click the Add button and browse into the ..\DELPHI 2.0\LIB subdirectory and add
STDREG.PAS, or STDREG.DCU, to your library. The Install Components dialog box should
now look as it does in Figure 1.1.
FIGURE 1.1.
The Install Components
dialog box as it appears
when you are first creating
a library from scratch.
After you click the OK button, Delphi huffs and puffs in the background for a while as it works
on compiling your library. When it’s finished, you should see the Standard, Additional, and
Win95 pages of the Component Palette back in place and more or less in working order. (It’s
not important if your version of Delphi has different pages in STDREG.PAS—all that matters
is that you are seeing some of the core pages from the Component Palette. See Figure 1.2 to see
how this looks on my system.)
FIGURE 1.2.
The Component Palette
when only a few of the
standard components are
installed.
Go back to the Windows Explorer now, and take a look at MYLIB.DCL, or whatever file you
have created. On my system, it’s about 654 KB in size, which is considerably less than half the
size of the standard CMPLIB32.DCL file that ships with Delphi.
Go ahead and drop a few components on the form designer and then compile and run your
application. Does it compile any faster when the library file is smaller? On my system, there is
a bit less swapping and a little faster response, but the result might be more significant if you
are working on a system where memory is particularly tight.
Feel free to experiment at this point. You could, for instance, add the SYSREG.PAS file and
see what new components are registered by that module. Next, you might want to add the
DBREGS.PAS file, which contains the database tools.
At this point, you might be curious about what’s to be found inside each of these XXXREG.PAS
files. If you have the source, open them up, and you will find that they each contain one key
method called Register. Here is an excerpt from STDREG.PAS:
RegisterComponents(LoadStr(srStandard), [TMainMenu, TPopupMenu, TLabel,
TEdit, TMemo, TButton, TCheckBox, TRadioButton, TListBox, TComboBox,
TScrollBar, TGroupBox, TRadioGroup, TPanel]);
This line of code registers most of the controls found on the Standard page of the Component
Palette. If you want, you might try experimenting a little here and commenting out all but this
one set of lines from the Register Procedure. Now choose Component | Install, and remove
every other unit from the library except for STDREG.PAS. When you are done, you will have
a very stripped-down library file, but it is still fairly large because a good chunk of VCL just sits
in the background doing grunt work.
The point here is simply that CMPLIB32.DCL is not a mysterious entity. It’s a relatively com-
prehensible chunk of code over which you have complete control, down to almost the lowest
level. If you think your development process will benefit from it, you should go ahead and
create a series of custom Component Palettes for use in different circumstances, or for use by
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
18
Part I
different teams in your company. Customize these palettes to your heart’s content. The devel-
opers of Delphi want you to be able to do this; they went to considerable effort to make it easy
for you to gain control over the Component Palette.
NOTE
Before leaving this section, don’t forget to restore the original CMPLIB32.DCL file,
if you so desire, and to remove any comments that you don’t want lingering in the
STDREG.PAS file. If all else fails, simply reinstall Delphi.
In truly desperate situations you can use the Add / Remove programs applet from the
Control Panel to remove Delphi, making sure that all Delphi-related files are taken off
your system. Check the Software section of HKEY_LOCAL_MACHINE in the registry. Make
sure no references to Delphi are left. Then try a reinstall.
I have found it’s best never to save any of my own source files in the Delphi 2.0
directory. That area is for Delphi, and you should create a separate area for your own
files. That way you can totally delete the Delphi 2.0 directory without affecting your
own files.
If you have a CD, it’s relatively painless to let the install program churn away in the
background while you go take a break. There is no reason why anything I have
outlined in this section should damage your system, but just in case the worst happens,
I want to be sure you know how to put things back together. The number one safety
tip when working with Delphi, however, is to be sure to back up your library file!
Remember that the Component Palette is the place to turn if you need to add third-party tools
to Delphi. Mike Scott’s work with the Games SDK requires that you do this, and at one point
in the book I have you add a few components to the environment. One of them, called
TFileIterator, plays an especially important role in some key programs found in this book.
The salient point here is that if you understand how the Component Palette works, you can
always add a few new third-party components when needed.
That’s all I want to say about the Component Palette. As with many portions of this book, I
have dealt more with the theory behind this tool than with specific implementations. Some
readers might want to now construct component libraries that are tailored to database work, or
to graphics work, or maybe one library for each chore. Or perhaps you will never want to play
with the Component Palette again. The point is to understand the tool and to dig in and see
what it can do for you.
NOTE
While I’m talking about important aspects of the IDE, I might mention that on the
Run menu is a choice called Program Reset. You can often use this option when
you accidentally create a program that locks up on you. This terminates the errant
program.
For many programmers this might not be of great account, but it’s one of my favorite new
features in the compiler. Perhaps it’s my background in C/C++, but I find these kinds of com-
ments handsome, easy to write, and easy to read. Old hands at Object Pascal might be sur-
prised when they find this syntax in my code, so I mention it here to prepare the unwary!
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
20
Part I
Of course, some general-purpose routines get passed around from programmer to program-
mer and don’t really belong to anyone anymore. Almost all the routines in STRBOX.PAS,
MATHBOX.PAS, UTILBOX.PAS, and so forth, belong in this category. Feel free to use any
of these routines as you see fit. It would not be right, however, to take one of these units whole-
sale, or in large part, and include it in a book on Delphi.
If you want to use this book in an academic or corporate setting, you should feel free to do so,
as long as you are buying copies of the book for the people you are teaching. Don’t just copy
chapters from the book and hand the copied pages out to your classes.
This might seem like an unusual section to include in an opening chapter of a book, but I get
a surprisingly large number of questions along these lines. The key point to remember is that
if you are writing your own programs, you can use any of the code from this book as you see
fit. I wouldn’t want you to release one of the programs unmodified under your own name, but
you can use the code in your programs without concern for copyright issues, as long as you are
not another author selling the same wares that I sell. I publish the code because I want other
programmers, professional and hobbyists, to use it. That’s why it’s there. I’m in the business of
supplying information. It’s both avocation and vocation.
Overall, this book contains an unusually large amount of totally new material for a second edi-
tion. As I said earlier, the reason for including this new material was simply to raise the bar, to
provide material for those readers who are interested in advanced programming issues. It’s not
my way, however, to write a book that focuses solely on the most advanced, most obscure
material. I tried to find subject matter that is of general interest to all readers but that is not
likely to be covered in books aimed at beginning or intermediate programmers.
For those who miss the material that was cut, and for those who wish for more fundamental
information about the basics of Delphi programming, I have put all the chapters that were cut
from this edition on the CD. There are probably about 400–500 of these pages. The material
is in Word 6.0 format, which can be read inside the WordPad program that ships with
Windows 95.
As a rule, the chapters on disk have not been updated for Windows 95 or NT, but nearly all
the programs discussed in those chapters should compile fine in Delphi 2.0. Delphi 2.0 is com-
pletely backwards-compatible with Delphi 1.0, so there is no reason why the code from the
first edition wouldn’t work under Delphi 2.0. Some of the discussion about types is a little
outdated, but the second chatper of this book, “Delphi Types Under Win32,” talks about how
the types changed from Delphi 1.0 to Delphi 2.0.
For many readers of the first edition, the early chapters of Delphi Unleashed, the ones that are
now on the CD, were the best parts of this book. As a result, I think the quality of the material
on the CD is every bit as good as the material between the covers of the book.
Perhaps you should really think of this book as having overflowed onto the CD. If it were possible
to bind that much material into one volume, the publishers would have done so, and the scope
of this book would have been simply enormous. Indeed, there is a tremendous amount of
material in this package, ranging from relatively easy-to-understand material for beginners, all
the way through advanced material on the Windows API and COM objects. In fact, if you
include the material on the CD, this might be the largest single volume computer book on
your shelves.
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
22
Part I
ReportSmith is a great tool, especially if you are working in a large corporation that processes
vast quantities of data. ReportSmith is not tailored for small tables or small chores, although it
performs them with excellence. Its real area of expertise is as an industrial-strength tool that
can tackle huge jobs with relative ease. If you have a smaller program that works with relatively
small data sets, then you might use the TQuickReports object that ships with Delphi. How-
ever, if you have a big job to do, and need a powerful and easy-to-use tool to complete the job,
you should read Andy’s excellent chapter on ReportSmith. Andy Fearns has the experience and
technical expertise to lead you through some fairly grueling projects, and I’m deeply indebted
to him for bringing his expertise to this book.
Ann Lynnworth is one of the more talented, and if she’ll forgive me, unusual people I have met
in the computer industry. No one who meets her is ever likely to forget her. She has the power
to hold large audiences in the palm of her hand, and to bring laughter and goodwill every-
where she goes. She was a very early adopter of both Delphi and the Internet, and she managed
to leverage both technologies into a thriving business called HREF tools. She and her business
partner, Michael Ax, have a considerable store of technological tips that they share with you.
Mike Scott is from Scotland. He has become a well-known expert on graphics and multimedia
technology. His enthusiasm shows through in his chapter, and his technical expertise is be-
yond question. In fact, Mike is one of those programmers whose code has formed the basis for
many other professional programming projects. There are just a few people in the industry with
his level of expertise, and he fortunately is generous enough to share his knowledge with all of
us. I have learned a lot from his code over the years, and so have many other programmers.
He’s an expert that the experts turn to when they need advice.
■ Why did this technique work under one set of circumstances but not under the
current set of circumstances?
■ Is it really legal to do this, or am I just getting away with something?
These are the kinds of questions I ask when I don’t understand the way a technology works.
The only way to get these kinds of questions answered is to dig beneath the surface and find
out how the tools I’m using are put together! Only then can I unleash the full potential inside
both the product and myself.
Some programmers are born with phenomenal skills. They have brains that work like finely
tuned clocks. Most of us will never be as good as these extremely gifted men and women.
However, there is a second way to become a very good programmer, and that is to learn how
the system works and why it works.
If you know how to do a particular programming chore, you can get a particular job done. If
you understand why Windows and Delphi work, and the principles upon which they are based,
then you can figure out how to do anything that involves Delphi and Windows. One approach
teaches you how to perform a particular list of actions; the second approach teaches you the
skills you need to figure out how to do anything.
This book is in the second school of thought. Much of it is about the theory behind Windows
and the theory behind Delphi. I have written this kind of book because I believe the most prac-
tical way to become a good programmer is to understand how the operating system itself works,
and how Delphi is put together.
Maybe a short story from my life will help illustrate this point. When I first came to work at
Borland’s technical support department, I knew a lot about how to program in Turbo Pascal.
I quickly learned, however, that simply memorizing a list of facts or techniques was not enough.
The phone was going to ring, and someone was going to ask me a question about a tool that
could manipulate the entire PC. There were too many facts to hope to memorize. I had to
know how Windows worked, how DOS worked, how protected mode worked, how Turbo
Vision worked, how OWL worked, how assembly language programming worked, and so forth.
There was no way to memorize that much information.
I survived because I learned from the best programmers at Borland, from people who are now
almost all on Object Pascal or C/C++ research and development. What they taught me was
not how to perform a particular action, but why the computer worked as it did. Once I under-
stood how memory was constructed, and why it was constructed that way, I could answer any
question about memory and pointers. Sometimes I had to do a little work first and then call
the person back, but I was never clueless about memory issues because I understood how the
system worked.
2/6 sqc3 Delphi Prog Unleashed #30499-6 andy 3/3/96 CH01 LP#4
Object Pascal
24
Part I
The same applied to object-oriented programming. The key was not to memorize a list of com-
mands, but to understand the theory behind objects. Once I knew what a VMT was and how
virtual methods are called, OOP ceased to be a mystery, and I could simply think in terms of
how things had to be implemented if they were going to work.
Understanding the theory behind objects, components, memory, the GDI, or any other topic
does not mean that all your problems magically disappear. But it is the most practical way to
get to be a good all-around programmer.
As you spend more and more time working with computers, you get to be a better program-
mer. Experience helps to shorten the learning curve that plagues new programmers. Because
you keep learning more about computers, it takes you less time to learn new skills.
With experience you learn not only a list of techniques, but also an understanding of the theory
behind the machines and tools that you work with day in and day out. This theoretical knowl-
edge is the most practical knowledge in the world because it helps you get each individual task
done in the shortest period of time.
Really talented programmers have a vision of how the system works, and they use that vision
every time they sit down to code. This book is about that vision of how the system works.
■ Reference Counting 46
■ Variants 52
■ Currency Type 62
In this chapter you get a look at the new types associated with Delphi 32-bit programming.
There is also a short overview of the old types, and the names and values they have under Win32.
In particular, you look at the following subjects:
■ The new long Pascal strings that have no significant limitation on their length.
■ The Variant type, which can be used anywhere in your programs, but which is
designed for use with OLE Automation. It is also a big help when you are working
with databases.
■ All the standard integer types and their values in the Win32 programming environ-
ment.
■ A discussion of generic and fundamental integer types. Generic types (Integer,
Cardinal) differ in kind between Win16 and Win32 implementations. The funda-
mental types (ShortInt, SmallInt, LongInt, Byte, Word) are the same in both 16- and
32-bit Delphi. The Currency type will also be discussed in this chapter.
■ A brief discussion of WideChars and AnsiChars, as well as related types such as Chars,
and the pointers PChars, PWideChar, and PAnsiChar.
This chapter is not designed to teach you about typed variables and how to use them in an
Object Pascal program. If you are interested in that kind of information, see the chapters on
that subject which appear only on the CD. The goal of this chapter is to give you an overview
of the standard types found in Win32. In other words, this chapter highlights the differences
between Object Pascal types under Win32 and Object Pascal types under Windows 3.1. See
the DOC files on the CD-ROM for general information about types.
NOTE
These changes have nothing to do with the C-style PChar strings found in Borland
Pascal 7 (BP7), in Delphi 16, and still found in this version of Delphi. PChars are very
different animals than either the old-style Pascal strings or the new long strings. The
long strings act very much like standard Pascal strings, only they are null-terminated
and no longer have a length limitation.
I should perhaps add some additional information here for readers who might be
interested in knowing how changes to the Delphi language come about. If you are used
to programming in C, you know that a change to the language definition would have
to be approved by the ANSI committee before it could appear in a major compiler.
Fortunately, Object Pascal is not controlled by a committee, and is therefore not
bound by restrictions that prevent it from growing or that force it to adhere to stan-
dards that might make little sense in the light of rapidly changing computer technolo-
gies. (There is an ANSI committee for Pascal, but the Borland Pascal compilers have
never attempted to align themselves with it.)
As a result, the quality of the product, and of the language on which it relies, is very
much a direct outgrowth of the talents of the men and women on the Delphi team.
You will have to judge for yourself what you think of this scheme, but certainly there
are some advantages to the arrangement as long as the caliber of the people working on
the team remains high.
Pascal strings have very few limitations and can be of virtually any length. For a more precise
explanation of exactly what I mean by “virtually any length,” see Part II of this book, which is
entitled simply, “Win32.” For now, I can state briefly that whatever limitations you do en-
counter will be dictated not by limitations in Object Pascal, but by the amount of memory on
your target machines and the size of memory allocated to your program by the system. The
amount of memory on your machine is unlikely to exceed 64 MB plus what ever free disk space
is available on your hard drive, and the Win32 operating system would probably prevent you
from allocating much over 2 GB for the structures inside your program.
The old style of strings can still be used in Delphi. They are called ShortStrings and they have,
naturally enough, a 255-character limitation. For instance, the following is the correct way to
declare and use an old-style string:
procedure TForm1.Button1Click(Sender: TObject);
var
S: ShortString;
begin
Edit1.Text := IntToStr(High(S));
end;
Note that this code uses the High function, which at the time of this writing is associated with
old strings but not with the new strings. The High function returns the maximum number of
characters that can be stored in a string. In the preceding example, High will return 255.
The new strings are referred to as long strings, or more technically, AnsiStrings. The following
function explicitly uses new strings:
procedure TForm1.Button2Click(Sender: TObject);
var
S: AnsiString;
begin
SetLength(S, 35);
end;
Note that this procedure uses the SetLength function, which is used to explicitly designate the
length of a string. This function works only in Delphi 2.0 and will not work in Delphi 1.0.
Here is how to issue the same command with a short string from Delphi 1.0, or an explicitly
declared ShortString in Delphi 2.0:
S[0] := #35;
The preceding code would be an error if you were working with long strings because long strings
have no 0th element. The first character in a long string is the first letter of the actual string you
want to reference. Consider the following code fragment:
var
S: string
Ch: Char;
begin
S := ‘Delphi’;
Ch := S[1];
end;
This code would have the same result if S were a long string or a ShortString. In cases like this,
S[1] always references the first letter of the string, which in this case is D. However, if S were a
ShortString, then you could also reference S[0], which is the length byte. Referencing S[0]
with a long string would be an error, and the length byte is stored at a negative offset, along
with the size of the allocation and its reference count.
NOTE
Let me reiterate that the SetLength function will not work with ShortStrings in 16-bit
Delphi, although it will work with them in 32-bit Delphi. If you need to implement
the SetLength code in Delphi 16, then the following routine will do the trick:
procedure SetLength(var S: string; i: Integer);
begin
S[0] := Chr(i);
end;
This function takes a string and an integer as parameters. If you want to set the string
to a certain length, say 10 bytes, then you can pass in the value 10 in the second
parameter. The function will then automatically set to 10 the length byte of the string
in the first parameter. The Chr function translates integer values to character type
values. Unlike the SetLength function used with long strings, the Chr function does not
allocate or deallocate any memory. It does, however, provide a reasonable method for
reusing code in the 16- and 32-bit versions of Delphi. Note that you can use the built-
in Win32 conditional definition to tell if you are in 16- or 32-bit Delphi:
{$ifdef WIN32}.
Although the developers tried to make both types of strings work the same in Delphi 2.0, clearly
it matters whether you use AnsiStrings or ShortStrings. Some commands work with one type
of string, others don’t, and some commands work with both types. Given these circumstances,
one wonders about the implications of making the following declaration:
var
S: string
Declaring code in this way is legal, but does it result in a long string or a ShortString?
It turns out that two different factors determine how the compiler views the preceding code. If
you turn to the Project | Options | Compiler page you will find that there is a compiler option
called Huge Strings. If this option is checked, then the preceding declaration will be consid-
ered an AnsiString. If the option is unchecked, then it will be a ShortString. By default, the
option is turned on.
The next question, of course, is why the compiler option is called Huge Strings rather than
AnsiStrings or long strings? The issue here is that you can turn on Huge Strings by writing the
following compiler option in your code:
{$H+}
When they created the compiler option codes, there were only so many letters in the alphabet
available to the developers of Delphi. Therefore, it’s a safe bet that the developers chose $H because
options such as $A (AnsiStrings) and $L (long strings) were not available to them. H is a good
choice because Huge Strings forms at least a reasonable mnemonic to help you keep track of
your options in this alphabet soup.
If you have the $H+ directive turned on, you can still use ShortStrings if you declare your string
variables in one of two ways:
{$H+}
var
S1: ShortString; // Declaration One
S2: string[50]; // Declaration Two
Both of these declarations produce ShortStrings. The first because it explicitly declares the S1
variable as a ShortString, and the second because it explicitly declares S2 as being less than 256
characters in length.
In this section of the chapter you have been introduced to the AnsiString and ShortString
types. Here are the key points covered in this section:
■ ShortStringsare identical to the old Pascal strings used in Delphi 1.0 and in the
Turbo Pascal products.
■ AnsiStrings are a new type of string that act very much like ShortStrings except when
you try to treat them as an array.
■ When treated as an array, an old style string has the length byte of the string in its 0th
element, but AnsiStrings have no element 0, and instead start with element 1, which
is the first character of the string.
■ By default, Delphi has the Huge Strings ($H+) compiler directive turned on. This
means that all strings that are not explicitly declared as ShortStrings will be consid-
ered to be long strings.
■ There are two ways to explicitly declare a ShortString even when the Huge Strings
compiler directive is turned on. One is to declare the variable to be of type
ShortString, and the second is to give it a length of less than 256 characters:
MyString[255].
NOTE
The analysis of the StrMem program in Listing 2.1 gives you a look inside the current
implementation of long strings. It is important to understand that in the shipping
version of Delphi, long strings might be defined to have a certain behavior, not a
certain implementation. In other words, it’s possible that the developers of Delphi will
never document the internal methods they use to represent long strings. They will,
however, document the way in which long strings are expected to behave.
Good programmers almost always attempt to learn the way a particular feature in the
compiler is implemented. This can help them write better code. However, the imple-
mentation of a feature is not always as important as the documented description of its
behavior. Having said this, I have to add that I can rarely do anything useful with a
programming feature until I have some conception of how it is implemented. If you
always rely on the documentation for a feature, then you are forced to memorize a list
of possible behaviors. If you understand how a feature works, then you don’t have to
memorize anything, but can instead rely on your understanding of how the object’s
implementation dictates its possible behaviors.
The point here is that you must be careful to understand the potential consequences of
building your code around undocumented features of the product. In particular, using
undocumented code might force you to make changes to your code when the compiler
is updated.
Listing 3.1. The StrMem program is used to illustrate some of the low-level
technical differences between AnsiStrings and ShortStrings.
unit main;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
A quick look at the source code for this program shows that it does nothing more than assign
some values to a string, and then display that string in the main caption of the program. The
reason for this paucity of creativity is that the program is meant to be used as a testing ground
in which you can learn about long and short strings. It’s valuable as a testbed, not as an ex-
ample.
To me, looking at a data type in the debugger is always very helpful. If I can actually see the
way it is structured, then I feel as though I have a better grasp of how it operates. To get a good
look at a long string, you need to load the StrMem program in the stand-alone debugger.
NOTE
Your version of Delphi 2.0 might not come with the stand-alone debugger. The
debugger is called TD32.EXE, and is valuable for its capability to peer deep into
memory.
To examine a program in TD32, you must compile it with debug information. In
particular, you should open up the Project | Options menu item in Delphi 2.0, and
turn to the linker page. There you will see an option to “Include TDW debug Infor-
mation.” Make sure this is checked, and then close the Options dialog box and select
Project | Build All.
You should now launch TD32 from the subdirectory in which your program resides,
passing it the name of your executable as a parameter. (If this fails, use the debugger’s
File menu to change to the directory where the executable is stored, and then explicitly
load the program into TD32.) If you have done everything correctly, the Object Pascal
source for your program will appear in the stand-alone debugger. If you find yourself
looking at machine code rather than Pascal code, then you probably forgot to turn on
debug information in Delphi before rebuilding.
Note that the stand-alone debugger does not run in a normal window. Instead, it runs
in a DOS box. You can maximize the DOS box if you want to get a clearer view of
your code. When you run the stand-alone debugger with certain settings, you might
find that you have to use the Alt+Tab key to switch back and forth between the
debugger and your program. If you think the debugger or your program has locked up,
trying switching back and forth to see if
1. Your program is active but the debugger is not
2. The debugger is active but your program is not
In other words, under normal operating situations, there are times when you can use
the debugger but not your program, or you can use your program but not the
debugger. At other times you can use both. This is not a flaw in the debugger, but a
natural consequence of the act of debugging, and one that also occurs in the integrated
debugger.
When you first load the program into the stand-alone debugger, you will be looking at the first
page of code in your program, which is the DPR file. Right-click the screen to bring up a local
menu, and choose Module. A dialog box will appear listing the modules in your project. Select
MAIN.PAS. Now you will be able to see the code for the main module of the StrMem pro-
gram, as shown in Figure 2.1.
FIGURE 2.1.
A Windows 95 session with
Turbo Debugger loaded in
a window on the desktop.
In the stand-alone debugger, use the F2 key to set a break point at the beginning of the
Button1Click procedure.
From the View menu, choose CPU. A window will appear in your program; it can be used to
see the machine code that is generated for your program. The general scene should look like
the one depicted in Figure 2.2.
Run StrMem from inside the debugger by pressing F9 or by selecting the appropriate option
from the Run menu. Once your program is launched, press the button on the program’s main
form. Pause for a moment when the code reaches the first line in the assignment statement, the
one with the lea command in it.
MAIN.32: S := ‘5’;
:0041F14C 8D45FC lea eax,[ebp-04] ; Pause here
:0041F14F BAA8F14100 mov edx,0041F1A8
:0041F154E84F2DFEFF call @LStrLAsg
FIGURE 2.2.
The stand-alone debugger
as you start stepping
through your code.
Remember that the code won’t look exactly like the code shown here, but it will have the same
general structure. In particular, it is unlikely that any of your addresses will be identical to mine,
but the assembly code will have the same format.
In the watch window at the bottom of the stand-alone debugger, enter the expression @S, and
take a look at its contents.
@S :0073F8DC : POINTER
Move over to the dump window at the bottom left of the CPU window. Right-click the win-
dow to pop up a local menu, and then select Goto. A dialog box called Enter Address to Goto
appears. Enter the variable S in this window.
When you first start stepping through your code, you will see something like the following
values for S:
:0073F8DC 00 00 00 00 48 F9 73 00 H•s
Notice that the address on the left side of the display is the same as the address of the variable
S as displayed in the Watch List window. On my machine, the address of the variable is
$0073F8DC. The first four bytes, that is, the 32-bit value of the pointer, is currently nil: 00 00 00
00.
Step through your code two lines, until the code pointer is past the call to LStrLAsg.
MAIN.32: S := ‘5’;
:0041F14C 8D45FC lea eax,[ebp-04]
:0041F14F BAA8F14100 mov edx,0041F1A8
:0041F154E84F2DFEFF call @LStrLAsg ; Go past this line.
After you call the LStrLAsg function, you will see that the four bytes of data in your string pointer
contain an address. This address might look something like this:
:0073F8DC A8 F1 41 00 48 F9 73 00 H•s
The first four bytes, A8 F1 41 00, is the relevant data in this example. This is the address in
memory where your string can be found. Pop up the Goto menu by clicking the right mouse
button in the Dump window. Enter the address shown in the first four bytes of your string
variable. Remember that the Intel architecture places its bytes in reverse order. Therefore, if
your string variable is at the address shown in the preceding line of code, then you should enter
$0041F1A8 and then press OK. The result will be that you can view your string in memory.
:0041F1A8 35 00 00 00 FF FF FF FF 5 ____
In this case you set the string equal to the number 5. The number 5 is character $35 in the
ANSI character set, and that value is displayed as the first byte of your string. The second byte
of your string is a null character designating the end of your string. On the far right of the
display is the number five written out in standard characters, and then followed by some gar-
bage characters, which in my case happen to be displayed as underlines.
So far, you have learned two things by peering into the debugger:
■ Long strings are pointers. If you go to the address of the string, there is nothing there
but a pointer to another location in memory.
■ You can find the string contained in your AnsiString variable by going to the address
referenced by your long string variable.
It surprised me when I first saw this implementation because I didn’t necessarily feel as though
I were working with a pointer when I worked with a long string. The compiler conceals from
you the true nature of the type. Nevertheless, if you want to get good at handling these little
puppies, then you need to understand that they are really pointer variables.
As mentioned earlier, one of the interesting traits of long strings is that you cannot assign or
read values from element zero in the string, which is where the length byte appeared in Delphi
16. By looking at the memory dumps shown earlier, you can see how Delphi’s string architec-
ture has changed. In particular, the first character of each string is no longer a length byte, but
is now the first character of the actual string. In other words, element zero of the string has
disappeared. It no longer exists.
This does not mean, however, that the length byte has actually disappeared. To see it, move
the dump window back one line so that you can see the line above the one where your string
starts. This string might look something like this:
:0041F1A0 FF FF FF FF 01 00 00 00 _ _ _ _
If your string had the word Fred in it, instead of the string 5, then it would look like this:
:0041F1AC FF FF FF FF 04 00 00 00 _ _ _ _
Notice that in the first case, the last 32-bit chunk of the string has the number 1 in it, while in
the second case it has the number 4. This is because the string 5 has one character in it, whereas
the string Fred has four characters in it.
Obviously, the length byte of long strings is kept at a negative offset from the beginning of the
string. Furthermore, the value is no longer an eight-bit byte capable of addressing only 255
characters. Now it is a 32-bit value, which would have a limit of 4GB. As such, it would prob-
ably be appropriate to call it a length value, or length code.
For strings that are allocated on the heap, the next two 32-bit values going backward from the
length value are also part of the string variable. One of these 4-byte values designates the allo-
cated length of the string, while the other designates the reference count. For string constants
there is no allocated length value, and the reference count is always -1. (Once again, remember
to check the documentation on the shipping version of Delphi to see if these facts are docu-
mented. If they are not documented, then they might change in future versions of the prod-
uct.)
In this section you had an overview of long strings. You have seen that they are not the same as
the standard Pascal strings. Instead of being represented by an array, they are actually null-
terminated strings referenced by a pointer. These null-terminated strings are unusual, in that
they are preceded by a 32-bit value that references the length of the string.
Run to the breakpoint and then examine the address of S in the watch window:
@S :0074FB60 : POINTER
Once again, your machine is unlikely to show the exact same address, but the general format of
the display will be similar. Choose View | CPU from the menu, and move to the dump win-
dow and right-click it to bring up a local menu (pressing Alt+F10 has the same effect). Choose
Goto, and enter S in the Enter Address to Position to dialog box. When you first look at the
memory associated with the variable S it will be full of garbage.
0074FB60 3F 01 02 00 44 16 F7 BF ? D_+
Notice that the address of S is the same in the dump window as in the watch window. Although
the number will almost surely differ on your system, at the time of this writing it was $0074FB60
on my system. In the preceding line, everything to the right of the address is random garbage,
and will almost certainly differ on your system.
Step through the line of code that assigns the value 5 to the variable S.
:00420704 66C78500FFFFFF+mov word ptr [ebp-00000100],3501
Once again, only the general format of this statement will look the same on your machine.
Now, look back down at the dump window.
:0074FB60 01 35 02 00 44 16 F7 BF 5 D_+
Notice that the first byte of the string variable is set to 1, and the second byte is set to $35,
which is the ASCII value for 5.
01 35
The rest of the string is full of garbage characters and will probably look different on your sys-
tem. The first two bytes, however, will be identical. The key point here is that the first byte
shown is the length byte, which is set to 1 because the string 5 has only one character in it. The
second byte is 35, which is the offset of the ASCII character representing the number 5.
In the last two sections you have had a close look at long strings and ShortStrings. By now you
should have a very well-grounded understanding of the difference in structure between these
two string types. It should be obvious, for instance, that these two types are radically different.
The developers of Delphi have gone to great lengths to enable you to treat the two types of
strings in a very similar manner. However, underneath this area of similarity, you have seen
that AnsiStrings and ShortStrings are two extremely different beasts.
Table 2.1. These routines can be used with both AnsiStrings and
ShortStrings.
Routine Description
Delete Deletes a substring from a string
DisposeStr Deallocates a string from the heap
FmtLoadStr Loads a string from a program’s resource string table
Insert Inserts a substring into a string
IntToHex Converts an integer to a hexadecimal
IntToStr Converts an integer to a string
IsValidIdent Returns TRUE if the given string is a valid identifier
Length Returns the dynamic length of a string
LoadStr Loads the string resource from the application’s executable file
LowerCase Lowercases the given string
NewStr Allocates a new string on the heap
Pos Searches for a substring in a string
SetLength Sets the length of a string
Str Converts a numeric value to a string
StrToInt Converts a string to an integer
StrToIntDef Converts a string to an integer or default value
UpperCase Uppercases the given string
Val Converts a string value to its numeric representation
Two very important routines not listed in Table 2.1 are called Format and FmtStr. Both rou-
tines are nearly identical, but the former returns the result from a function call, whereas the
latter returns the result as a parameter. The following pseudocode focuses on the differences
between the two calls:
Result := Format(...)
FmtStr(Result, ...);
As you can see, the FmtStr procedure takes the result as a parameter, and the Format function
returns the result. I tend to use the Format function more than the FmtStr function, so I will
use that routine in the following discussion. However, both routines are based on similar prin-
ciples, and if you understand one then you understand the other.
In Listing 2.2 you will find the main unit from the Formats program. This program shows
several simple examples of how to use the Format function.
Listing 2.2. The Formats program shows how to use the Format function.
unit main;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Buttons;
type
TForm1 = class(TForm)
ListBox1: TListBox;
StrInput: TButton;
IntHex: TButton;
AllThree: TButton;
DiskAvail: TButton;
IntInput: TButton;
bFloat: TBitBtn;
bCommas: TBitBtn;
bScience: TBitBtn;
procedure IntInputClick(Sender: TObject);
procedure StrInputClick(Sender: TObject);
procedure IntHexClick(Sender: TObject);
procedure AllThreeClick(Sender: TObject);
procedure DiskAvailClick(Sender: TObject);
procedure bFloatClick(Sender: TObject);
procedure bCommasClick(Sender: TObject);
procedure bScienceClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
uses
UtilBox;
{$R *.DFM}
continues
end.
This program, shown in Figure 2.3, enables you to press various buttons that prompt the user
for input. The data entered by the user is then run through the Format function for processing,
and the resulting string is displayed in the list box at the top of the program. Many of the but-
tons in the program don’t do anything very useful because this program is designed for pro-
grammers, not end users. Its purpose is just to show you how the Format function works.
FIGURE 2.3.
The Formats program
prompts a user for input,
which is processed and
displayed in a list box.
The Format function is a lot like the printf series of functions in C. In particular, it takes two
parameters.
function Format(
const Format: string; // String plus format specifiers
const Args: array of const // Arguments to embed in first paramter
): string; // Function returns the formated string
The first parameter, called Format, is a string in which you can embed various specifiers. Each
of these specifiers can hold the place of an argument that will be embedded in the string.
Consider the following example:
S := Format(‘This is an integer: %d’, [23]);
This code fragment sets S to read: This is an integer: 23. In this example, %d is a format
specifier and 23 is an argument. Notice that the arguments are placed inside brackets that de-
lineate an array of const.
NOTE
An array of const is a special construction in Object Pascal that enables you to work
with a variable number of arguments. The classic use of an array of const is to pass a
variable number of arguments as parameters to a procedure. The following code
demonstrates how that works:
procedure Foo(A: array of const);
var
i: Integer;
S: string;
begin
i := High(A) + 1;
S := Format(‘Num arguments passed: %d’, [i]);
ShowMessage(S);
end;
There are a number of different format specifiers used with the Format function. Each of the
specifiers is preceded by a percentage sign, and a variable number of internal arguments, as
shown in the following list:
d: integer
e: scientific
f: fixed
g: general
n: number
m: Money
p: Pointer
s: string
x: Hex
Most of these arguments can be used to translate a numerical value into a string value. For
instance, the p specifier will translate a computer address into a string value, as shown in the
following code:
procedure TForm1.Button2Click(Sender: TObject);
var
S: string;
begin
S := Format(‘$%p’, [@S]);
ShowMessage(S);
end;
The preceding example prints out the address of the variable S in string format. For example,
when I run the function on my computer it displays the value $0065F8DC in a message box. The
actual result will probably differ on your machine.
Because the Args parameter to the Format function is an array of const, you can use it to pass in
multiple arguments to the first parameter, as shown in the following code:
procedure TForm1.DiskAvailClick(Sender: TObject);
var
Free, Size: Integer;
S: string;
begin
Free := DiskFree(3);
Size := DiskSize(3);
S := Format(‘DiskSize: %d. DiskFree: %d’, [Size, Free]);
ListBox1.Items.Add(S);
end;
In this example, the integer values Size and Free are passed in as arguments to the first param-
eter. The format specifiers in that parameter enable Delphi to convert the integer values into
string format and properly imbed them in a string. The result is a string that might look some-
thing like this:
DiskSize: 523526144, DiskFree: 28868834
Sometimes, you might want to display floating-point numbers, and in particular, you might
want to control the way the decimal portion of these numbers is displayed. There are a number
of ways to do this, but the following fragment shows one common way to handle the problem:
procedure TForm1.bFloatClick(Sender: TObject);
var
i: Integer;
f: Double;
S: string;
begin
GetInteger(‘Enter a number and I will find its square root: ‘, i);
f := Sqrt(i);
S := Format(‘The result: %f, To 5 decimal places: %12.5f, and as money: %m’,
[f, f, f]);
ListBox1.Items.Add(S);
end;
This code asks the user to enter an integer value. The square root of the number entered is then
calculated and displayed to the user in three different ways. The first way simply displays the
number with some default formatting. The second method specifies that the resulting string
will be at least 12 characters in length, and that 5 digits will appear after the decimal point.
The third specifier translates a floating-point number into a monetary value. For instance, it
converts 35.3 into $35.30.
The Formats program shown in Listing 2.2 gives the Format function a reasonable workout. If
you’ve never played with this type of function before, you should run the program so you have
a general feeling for what it can do for you. In general, you will find that it solves a particular
class of problem quickly and easily. For further information on the format specifiers used with
this function, see the “Format Strings” entry in the online help.
Delphi provides three trim functions rather than just one in order to optimize your use of these
functions. For instance, there is no need to expend clock cycles removing spaces from both
ends of a string if it is unlikely that the spaces appear in more than one position.
The Trims program found on the Unleashed CD-ROM provides some quick examples of how
to use the new trim functions. For instance, the following is the Trim function in action:
procedure TForm1.Button1Click(Sender: TObject);
var
S, Temp: string;
begin
S := ‘ Sam ‘;
Temp := S;
S := Trim(S);
S := ReplaceChars(S, #32, ‘*’);
Temp := ReplaceChars(Temp, #32, ‘*’);
ListBox1.Items.Add(S);
ListBox1.Items.Add(Temp);
end;
A string constant with preceding and succeeding spaces is assigned to the variable S. It then
assigns S to another string variable called Temp. The third step in the procedure is to remove the
spaces from the beginning and end of the string by calling Trim. At this point, the variable S is
equal to the word “Sam” with no spaces on either end. The variable Temp still has spaces in it.
To make this difference visible to you, the code replaces all the spaces in the Temp string with
asterisks and then shows you the string in a list box. The ReplaceChars function is a custom
routine found in the StrBox unit found on the Unleashed CD-ROM. You learn more about
ReplaceChars in the next chapter, “Strings and Text Files.” For now, just include the StrBox
unit, which ships with this book, in your uses clause.
NOTE
The following is the output from the Button1Click function shown above:
Sam
***Sam***
The TrimRight function is demonstrated in the following method from the Trims program:
procedure TForm1.Button2Click(Sender: TObject);
var
S, Temp: AnsiString;
begin
S := ‘ Sam ‘;
Temp := S;
s := TrimRight(S);
S := ReplaceChars(S, #32, ‘*’);
Temp := ReplaceChars(Temp, #32, ‘*’);
ListBox1.Items.Add(S);
ListBox1.Items.Add(Temp);
end;
This code strips the spaces from the right part of the string S, but not from the left. The output
in the list box therefore looks like this:
***Sam
***Sam***
The Trim, TrimRight, and TrimLeft functions are easy to use. They are mentioned here only so
you will know that they are available. Readers of the previous versions of this book should note
that these functions replace or have similar functionality to the CleanString, StripBlanks, and
StripFromFront functions in the StrBox unit. I have kept all three functions in the StrBox unit,
however, in part for compatibility with old code, and in part because the routines might still
be useful under particular circumstances.
The preceding example is interesting because MessageBox is a Windows API function that takes
a PChar in the second parameter. In this particular rather contrived case, it was more conve-
nient to use a string than a PChar. In the past, a decision of this type forced the programmer to
call StrPCopy, which would translate a Pascal string into a PChar. The new long strings, how-
ever, enable you to perform this translation by making a simple typecast. This is possible, of
course, because long strings are really just pointers to null-terminated strings with a few extra
traits thrown in for good measure.
If you want to move in the other direction, from a PChar to string, then you can simply make
a direct assignment. Consider the following declarations:
var
P: PChar;
S: AnsiString;
If you have two variables of these types, then you can write
S := P;
Reference Counting
Delphi performs reference counting on the long strings you create. This means that if you as-
sign one AnsiString to another, then Delphi does not necessarily copy the string over into a
new place in memory. Instead, it simply increments a variable that keeps track of the number
of users of a string. If one of the users of the string then goes out of scope, the memory associ-
ated with the string is not deallocated. Instead, the reference count is merely decremented by
one. If the total number of users of the string goes down to zero, then the memory associated
with the string is deallocated.
If you assign two variables to the same long string, and then change the value of the string
associated with one of the string variables, the data associated with the changed string will be
copied over to its own area of memory; the reference count of the other string will then be
decremented by one. The new string, of course, will have a count of one.
Let me repeat the concepts outlined in the last paragraph just to be sure that what I am saying
is clear. Consider the following example:
var
S1, S2: string;
begin
S1 := ‘Sam’; // One string in memory.
S2 := S1; // Still one string in memory.
S2[3] := ‘p’; // Copy on change! Now there two strings in memory!
end;
After assigning the string “Sam” to S1, you have one AnsiString variable that points at an area
in memory that contains a string. If you assign a second Delphi AnsiString variable to the first
variable, then Delphi does not make a copy of the string for either the first or second variable.
Instead, both variables will point at the same area in memory. Thus you have two variables,
but only one string. As a result, the string’s reference count will be set to two. If you then change
the string associated with one of the variables, Delphi will proceed to make a copy of the string
associated with the variable referenced at the time of the change. At this point there will be two
variables and two strings. The reference count of each string will then be set to one. This con-
cept is called “copy on change.”
NOTE
Traditional Delphi characters are represented by 8-bit characters called Chars. In fact,
8-bit characters have been the standard in all Intel-based computer languages for years.
The problem with 8-bit values, however, is that they can represent only 256 possible
characters, which is not nearly enough for Asian languages or for many of the math-
ematical or other types of symbols that you might want to store.
Because 8-bit characters have limitations, the industry has decided to support a new
16-bit standard that will have not 256 possible characters, but 64KB worth of possible
characters.
The new 16-bit characters used in Unicode applications are called WideChars. If you are
working with a string of these characters, then you are working with a variable of type
PWideChar. A WideChar has two bytes per character, whereas a standard old-style
character, called an AnsiChar, has one byte per character. If you declare a variable of
type Char, then it will be an AnsiChar by default in Delphi 2.0. You cannot count on
the fact that Chars will always be considered 8-bit characters, and in future versions of
Delphi, 16-bit Chars will probably become the default.
So far you have seen that Delphi provides support for Unicode, but that it is not yet hosted
fully by all of Delphi’s visual controls. Given these circumstances, the thing you want to be
able to do is convert into AnsiStrings any Unicode strings that you get, so that you can use
them in your controls. You will also need a routine to convert the strings back the other way.
The program in Listing 2.3, WIDECHAR.DPR, shows you how to perform these conversions.
Listing 2.3. How to convert a Unicode string to an AnsiString, and vice versa.
unit main;
{
-- WideChar functions from SYSTEM.PAS:
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Ole2, ExtCtrls;
type
TForm1 = class(TForm)
BCLSID: TButton;
bWideString: TButton;
Edit1: TEdit;
Panel1: TPanel;
bDelCLSID: TButton;
bDelphiWideString: TButton;
Edit2: TEdit;
procedure BCLSIDClick(Sender: TObject);
procedure bWideStringClick(Sender: TObject);
procedure bDelphiWideStringClick(Sender: TObject);
procedure bDelCLSIDClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
uses
OleAuto;
{$R *.DFM}
continues
// ClassIDToString is in OLEAUTO.PAS
procedure TForm1.bDelCLSIDClick(Sender: TObject);
begin
Edit2.Text := ClassIDToString(IID_ICLASSFACTORY);
end;
end.
The Wides program has two buttons on it. One button can be used to convert a Unicode string
into an AnsiString, and the other button shows how to move in the opposite direction. In
particular, one button calls an OLE function that converts an OLE CLSID from the registry
into a string. Unfortunately, the OLE functions usually work with Unicode strings, in part
because OLE is a technology that looks toward the future.
When you call OLE string functions, you will often need to convert the Unicode strings used
by OLE into AnsiStrings used by the Delphi visual controls. The Wides program shows you
how to proceed. The other button on the form has a more contrived functionality: it simply
converts from ANSI to Unicode and back, with no claim to real-world functionality.
The most useful function in the preceding program is probably UnicodeToAnsi:
function UnicodeToAnsi(S: PWideChar): string;
var
S1: PChar;
i: Integer;
begin
i := lstrlenw(S) + 1;
GetMem(S1, i);
i := WideCharToMultiByte(CP_ACP, 0, S, i, S1, i * 2, nil, nil);
Result := S1;
FreeMem(S1, i);
end;
This function calls the WideCharToMultiByte Windows API function. I’m wrapping this func-
tion in a Delphi routine because it becomes a bit complex to call, due primarily to the number
of parameters it takes. Here is the declaration for WideCharToMultiByte:
int WideCharToMultiByte(
CodePage: Cardinal; // code page
dwFlags: Integer; // performance and mapping flags
lpWideCharStr: PWideChar; // address of wide-character string
To call WideCharToMultiByte, all you usually have to do is pass a PWideChar and its size, as well
as a preallocated PChar and the size of the memory allocated for it. The specific amount of
memory allocated for the PChar should be included in the call. The rest of the parameters can
usually be set to zero, or to nil. The first parameter can receive a constant named CP_ACP, which
designates the code page you want to work with. In short, the WideCharToMultiByte function
is more powerful than anything you will need under normal circumstances. As a result, I have
wrapped it in a Delphi function that will simplify its use. For more information, see the online
help.
When you want to go the opposite way, from an AnsiString to a Unicode string, then you can
call the following function from the Wides program:
function AnsiToUnicode(S: string; var NewSize: Integer): PWideChar;
var
Size: Integer;
P: PWideChar;
begin
Size := Length(S);
NewSize := MultiByteToWideChar(CP_ACP, 0, PChar(S), Size, P, 0);
P := VirtualAlloc(nil, Size, Mem_Commit, Page_ReadWrite);
MultiByteToWideChar(CP_ACP, 0, PChar(S), Size, P, NewSize);
Result := P;
end;
This function allocates memory for the PWideChar that it returns. This means that you will be
responsible for destroying the memory associated with the routine. The routine returns the
size of the allocation in the NewSize parameter so that you will know how much memory to
destroy.
AnsiToUnicode calls the MultiByteToWideChar function, which performs the opposite task from
WideCharToMultiByte. Here is the declaration for MultiByteToWideChar:
function MultiByteToWideChar(
CodePage: UINT; // Code Page
dwFlags: DWORD; // Character-type Options
const lpMultiByteStr: LPCSTR; // Address of string to map
cchMultiByte: Integer; // Number of characters in string
lpWideCharStr: LPWSTR; // Address of WideChar buffer
cchWideChar: Integer): // Size of WideChar bugger.
Integer; stdcall; // Number of characters written to WideChar
The function MultiByteToWideChar can also return the number of bytes that need to be allo-
cated for the PWideChar variable. To get the routine to perform this function, pass zero in the
cchWideChar variable. Because the routine performs two functions, I call it twice in
the AnsiToUnicode routine. The first time I am just getting the length of the PWideChar, and
the second time I am getting the actual string.
That’s all you will learn about long strings in this chapter. Hopefully, these pages have given
you an introduction to the topic and will help you use them effectively in your own programs.
Take special note of the built-in Delphi calls to StringToWideChar, WideCharToString, and
ClassIDToString that are referenced in the code. These are the easiest ways to convert WideChars
to AnsiStrings, and vice versa.
Variants
Variants are a new type in Delphi that can be assigned to a wide range of variable types. Some
people jokingly refer to variants as a typeless type, because it can be used to represent a string,
an integer, an object, or several other types.
The following is a simple procedure that illustrates the flexible nature of variants:
procedure TForm1.Button2Click(Sender: TObject);
var
V:Variant;
begin
V := 1;
V := ‘Sam’;
end;
This procedure compiles fine under Delphi 2.0. As you can see, you are allowed to assign both
a string and an integer to the same variant variable.
The following procedure, however, is not legal:
procedure TForm1.Button1Click(Sender: TObject);
var
V: Variant;
begin
V := 1;
Caption := ‘Sam’ + V;
end;
The attempt to assign Edit1.Text to a variant concatinated with a string will be flagged as a
type mismatch by the compiler because a variant takes on some of the attributes of an integer
after being assigned to that type.
Underneath the surface, Variant types are represented by a 16-byte structure found in
SYSTEM.PAS. At the time of this writing, that structure looks like this:
PVarData = ^TVarData;
TVarData = record
VType: Word;
Reserved1, Reserved2, Reserved3: Word;
case Integer of
varSmallint: (VSmallint: Smallint);
varInteger: (VInteger: Integer);
varSingle: (VSingle: Single);
varDouble: (VDouble: Double);
varCurrency: (VCurrency: Currency);
Notice that this is a variant record; that is, the various types represented in the case statement
are overlaid in memory. (Variant records have nothing to do with the Variant type. The oc-
currence of the two names in the same context is just a coincidence.) The structure ends up
being 16 bytes in size because the VType field is two bytes, the three reserved fields total six
bytes, and the largest of the types in the variant section is the double, which is eight bytes in
size (2 + 6 + 8 = 16). Once again, the case statement in the declaration is not a list of separate
fields, but a list of different ways to interpret the eight bytes of data contained in the second
half of the space allocated for the record.
The following are the declarations for the values used with variants:
varEmpty = $0000;
varNull = $0001;
varSmallint = $0002;
varInteger = $0003;
varSingle = $0004;
varDouble = $0005;
varCurrency = $0006;
varDate = $0007;
varOleStr = $0008;
varDispatch = $0009;
varError = $000A;
varBoolean = $000B;
varVariant = $000C;
varUnknown = $000D;
varByte = $0011;
varString = $0100;
varTypeMask = $0FFF;
varArray = $2000;
varByRef = $4000;
NOTE
When you study these declarations, it’s important to understand that Delphi defines a
certain set of behaviors to be associated with variants, but might not guarantee that the
implementation will remain the same from version to version. In other words, you can
almost surely count on the fact that assigning both a string and an integer to the same
variant will always be safe. However, you might not necessarily be sure that variants
will always be represented by the same constants and record shown above. To check
whether these implementations have been blessed as being permanent, refer to your
Delphi documentation.
If I were writing the documentation for Delphi, I might decide not to show you the
specific record shown previously because these kinds of details might change in later
versions of the product, just as the internal representation for long strings might
change. However, in this book I do not claim to be defining the language in a set of
official documents. Instead, I am explaining the product to you, using the techniques
that seem most useful. In this particular case, I think it helps to see behind the scenes
and into the particulars of this implementation of the Variant type. Whether or not
this will become a documented fact or an undocumented trick is not clear at the time
of this writing.
It should be clear from the preceding code examples that knowing what type is represented by
a variant at a particular moment can be important. To check the variant’s type, you can use the
VarType function, as shown here:
var
Atype: Integer;
V: Variant;
begin
Atype := VarType(V);
...
end;
In this example, the Integer variable AType will be set to one of the constants shown above. In
particular, it will probably be assigned to varEmpty, because in the preceding example this vari-
able has not yet been assigned to another type.
If you want to get a closer look at a variant, you can typecast it to learn more about it, as shown
in the following example:
procedure TForm1.Button1Click(Sender: TObject);
var
V: Variant;
VarData: TVarData;
begin
V := 1;
VarData := TVarData(V);
Atype := VarData.vType;
end;
In this particular case, AType will be set to the same value that would be returned from a call to
VarType.
To understand more about how all this works, see Listing 2.4, in which you can find the code
for the VarDatas program. This code sets a single variant to a series of different values, and
then examines the variant to discover the current value of its VType field.
Listing 2.4. The VarDatas program lets you examine variant types.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
public
procedure ShowVariant(V: Variant);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
continues
end.
FIGURE 2.4.
The VarDatas program
uses the TVarData
structure to examine how
variants are put together.
The data shown in the list box in Figure 2.4 represents the value and internal representation of
a single variant that is assigned a series of different types. It’s important to understand that
Variant can’t simultaneously represent all types, and can instead at any one moment take on
the characteristics of only one particular type.
The code that assigns different types to a variant is easy to understand, as is shown here:
procedure TForm1.Button1Click(Sender: TObject);
var
V: Variant;
S: TObject;
begin
ShowVariant(V);
V := Null;
ShowVariant(V);
V := 1;
ShowVariant(V);
V := ‘Sam’;
ShowVariant(V);
V := 1.25;
ShowVariant(V);
end;
The code that reports on the current type of a variant is somewhat more complex, but still
relatively straightforward, as shown next:
function GetVariantType(V: Variant): string;
var
VarData: TVarData;
S: string;
begin
VarData := TVarData(V);
case VarData.VType of
varEmpty: S := ‘varEmpty’;
varNull: S := ‘varNull’;
varSmallint: S := ‘varSmallInt’;
varInteger: S := ‘varInteger’;
varSingle: S := ‘varSingle’;
varDouble: S:= ‘varDouble’;
varCurrency: S := ‘varCurrency’;
varDate: S := ‘varDate’;
varOleStr: S := ‘varOleStr’;
varDispatch: S := ‘varDispatch’;
varError: S := ‘varError’;
varBoolean: S := ‘varBoolean’;
varVariant: S := ‘varVariant’;
varUnknown: S := ‘varUnknown’;
varString: S := ‘varString’;
varTypeMask: S := ‘varTypeMask’;
varByRef: S := ‘varByRef’;
end;
Result := S;
end;
This code first converts a variant into a variable of type TVarData. In doing so, it is merely sur-
facing the true underlying type of the variant. However, a variable of type TVarData will not act
the same as a variable of type Variant. This is because the compiler provides special services for
variants that it would not provide for a simple record type such as TVarData.
It’s important to note that there are at least two ways to write the first lines of code in this
function. For instance, I could have written
function GetVariantType(V: Variant): string;
var
I: Integer;
S: string;
begin
i := VarType(V);
case i of
varEmpty: S := ‘varEmpty’;
varNull: S := ‘varNull’;
...
end;
end;
This code works the same as the code shown in the actual program found on the Unleashed
CD-ROM.
However you decide to implement the function, the key point is that variants can take on the
appearance of being of a certain type. The chameleon-like behavior of Variant is sparked by
the type of variable to which it is assigned. If you want, you can think of a variant as being a
chameleon that hides itself from view by assuming the coloration of the variable to which it is
assigned. A variant is never of type Variant, it’s always either empty, NULL, or the type of the
variable to which it is assigned. In the same way, a chameleon has no color of its own, but is
always changing to adapt its color to the environment around it. Either that or it is unborn,
dead, non-existent, or has no color at all!
The following routines can all be used with variants. To learn more about these routines, look
in the online help.
procedure VarClear(var V: Variant);
procedure VarCopy(var Dest: Variant; const Source: Variant);
procedure VarCast(var Dest: Variant; const Source: Variant; VarType: Integer);
function VarType(const V: Variant): Integer;
function VarAsType(const V: Variant; VarType: Integer): Variant;
function VarIsEmpty(const V: Variant): Boolean;
function VarIsNull(const V: Variant): Boolean;
function VarFromDateTime(DateTime: TDateTime): Variant;
function VarToDateTime(const V: Variant): TDateTime;
Here are some routines to use with Variant arrays, which are discussed in Chapter 29, “OLE
Automation Basics.”
function VarArrayCreate(const Bounds: array of Integer;
VarType: Integer): Variant;
function VarArrayOf(const Values: array of Variant): Variant;
procedure VarArrayRedim(var A: Variant; HighBound: Integer);
function VarArrayDimCount(const A: Variant): Integer;
function VarArrayLowBound(const A: Variant; Dim: Integer): Integer;
function VarArrayHighBound(const A: Variant; Dim: Integer): Integer;
function VarArrayLock(const A: Variant): Pointer;
procedure VarArrayUnlock(const A: Variant);
function VarIsArray(const A: Variant): Boolean;
Before closing this section I want to make it clear that variants are not meant to be used broadly
in your program whenever you need to work with a variable. I have no doubt that many people
will in fact program that way, but I want to emphasize that the developers didn’t really want
variants to be used that way, mainly because they have some overhead associated with them
that can slow down the compiler. (Some tricks performed by variants, such as string manipu-
lation, happen to be very highly optimized. However, you should never consider a variant to
be as fast or efficient as a standard Object Pascal type such as an Integer, Cardinal, or string.)
Variants are almost certainly best used in OLE and database applications. In particular, vari-
ants were brought into the language because they play a role in OLE automation. Further-
more, much of the structure and behavior of variants are defined by the rules of OLE. These
unusual types also prove to be useful in database applications. As a rule, there is so much over-
head involved in referencing any value from a database that a little additional variant manipu-
lation code is not going to make a significant difference.
NOTE
The word integer has two related meanings in Object Pascal. There is the Integer type,
which is used in declarations. It is a 16-bit unsigned value in Delphi 16, and a 32-bit
unsigned value in Delphi 32. There are also the integer types, which represent a generic
way of referring to all ordinal, scalar types used in Delphi. In particular, Integer, Word,
ShortInt, LongInt and Byte are all integer types. I will capitalize the word “integer”
when I am referring to a specific type, and I will leave the word in small letters when I
am referring to the whole family of integer types.
There are two types of integers in Delphi 2.0. The fundamental types remain the same in all
versions of Object Pascal. Here is a short table showing the fundamental types:
ShortInt -128 .. 127 Signed 8-bit
Byte 0 .. 255 Unsigned 8-bit
SmallInt -32768 .. 32767 Signed 16-bit
Word 0 .. 65535 Unsigned 16-bit
LongInt -2147483648 .. 2147483647 Signed 32-bit
If you are in Delphi 16 and use a ShortInt, then it will be an 8-bit signed value. If you are in
Delphi 32 and use a ShortInt, then it will still be an 8-bit signed value.
The generic integer types are not the same in each version of Delphi. Here is a table showing
the generic types:
Integer -32768 .. 32767 Signed 16-bit
Integer -2147483648 .. 2147483647 Signed 32-bit
Cardinal 0 .. 65535 Unsigned 16-bit
Cardinal 0 .. 2147483647 Unsigned 32-bit
Generic types will have a 16-bit value in Delphi 16, and a 32-bit value in Delphi 32. There are
two kinds of generic integers: signed and unsigned.
You should note that Cardinals in 32-bit land use only 31 of the 32 bits available to them.
That is, the top number they store is 2GB, rather than the expected 4GB. The Delphi com-
piler assumes that all integer operations are going to fit into 32 bits. If the developers allowed
Cardinals to use all 32 bits, there would be a good chance that you could add two numbers
together to create a sum larger than what can be fit into 32 bits. This would create an overflow
condition that would be difficult for the compiler to detect. As a result, you are limited to 31
bits in Cardinals and LongInts. You can still store 32 bits of information in a Cardinal and
reliably perform bitwise operations on all 32 bits, but you could not safely use the number in
a mathematical expression such as addition or multiplication. This is a limitation in the com-
piler that will hopefully be overcome in later editions of Delphi. The Delphi Comp type can be
used if you need to perform integer math on values up to 64 bits in size.
Generic integers have the habit of changing their size to adapt to the current operating system.
A 16-bit operating system is going to be very efficient when working with 16-bit values, and a
32-bit operating system will be very efficient when working with 32-bit values. Therefore, it
can be convenient to have a type that switches back and forth between 16- and 32-bit values in
order to adapt itself to the operating system or the current machine.
NOTE
The fact that 16-bit operating systems work best with 16-bit numbers means that it is
sometimes more efficient to use a Word or an Integer under Delphi 16 than it is to use
a Byte or ShortInt. In other words, smaller isn’t necessarily better when it comes to
producing fast integer math. The Holy Grail is usually not size, but matching the type
to the CPU and the operating system.
Having said this, I have to admit that I rarely give such matters much consideration
when I program. I try to never use a type that might prove too small for the values I
wish to store in it, and otherwise I don’t tend to give the matter much thought. Still,
there is some value in the idea that it’s best to always use the Integer or Cardinal
generic types, unless you have a specific reason for using the other types.
When you are dealing with simple integer types—and indeed with all Delphi types—it is
worthwhile to spend some time browsing the WINDOWS.PAS unit, and in particular, the
top few pages of it. There you will find a number of interesting declarations that might prove
useful to you when you work with Windows API functions. Here is a sample selection of dec-
larations:
WPARAM = Longint;
LPARAM = Longint;
LRESULT = Longint;
LPSTR = PAnsiChar;
LPCSTR = PAnsiChar;
LPWSTR = PWideChar;
LPCWSTR = PWideChar;
DWORD = Integer;
BOOL = LongBool;
PBOOL = ^BOOL;
PByte = ^Byte;
PINT = ^Integer;
PSingle = ^Single;
PWORD = ^Word;
PDWORD = ^DWORD;
LPDWORD = PDWORD;
UCHAR = Byte;
PUCHAR = ^Byte;
SHORT = Smallint;
UINT = Cardinal;
PUINT = ^UINT;
ULONG = Cardinal;
PULONG = ^ULONG;
LCID = DWORD;
LANGID = Word;
THandle = Integer;
PHandle = ^Thandle;
When you see Windows API books written about the C/C++ programming language, you will
find many of the types just shown. For example, the UINT, DWORD, and LPSTR types are among
the most frequently used types in Windows code. It is also interesting to note that commonly
used Pascal types such as THandle simply resolve into an Integer.
Many Object Pascal programmers spend a good deal of time browsing Windows API books
written about the C/C++ programming language. As a result, it is often useful for program-
mers to refer function parameters by the same names as they are given in C/C++ reference books.
Therefore, it’s tempting, under certain circumstances, to refer to a DWORD rather than an
Integer, and a UINT rather than a Cardinal.
Despite the usefulness of these C/C++ -like declarations, I have to confess that I don’t like the
game of creating a type that is, in effect, little more than an alias for another type. I know that
there are times when this technique is very useful, and indeed, sometimes it can save hours of
work when you need to move back and forth between different platforms. In such cases, you
probably should use these kinds of declarations. However, I prefer to keep my code as simple
and lucid as possible. Everyone who understands the rudiments of Object Pascal will immedi-
ately understand the following declaration:
var
I: Integer;
S: string;
It is simple in the extreme, and its simplicity is an enormous virtue. The following declara-
tions, on the other hand, are a bit confusing:
var
i: LCID;
s: LPCSTR;
An LCID, for instance, translates into a DWORD, which in turn translates into an Integer. This
whole business of having a type that is really an alias for another alias is the kind of thing on
which C/C++ is built. Macros in C++ are frequently layered three or four levels deep, and this
is indeed fine under certain circumstances. Object Pascal, however, typically draws a line in
the sand around these types of issues. If you want the syntactical convenience and complexity
of multiple inheritance, then you should use C/C++. If you want the sometimes artful ambi-
guity of operating overloading, then you should use C/C++. If you want to create multiple
layers of aliases in order to achieve certain syntactical effects, then you probably ought to use
C/C++.
Object Pascal, on the other hand, tends to be about simplicity and elegance. Indeed, well-
written Object Pascal code comes close to being self-documenting. It says what it means. It’s
the opposite of a language such as C/C++, where even relatively simple functions are usually
burdened under the weight of numerous, very necessary, comments.
Obviously, matters of this type are strictly matters of opinion, and perhaps it is objectionable
for a writer to abuse the privileges of authorship by promoting a particular, possibly prejudiced,
viewpoint. Nevertheless, I would still assert that it is in the nature of Object Pascal to attempt
to be as simple, sparce, and elegant as possible. The classic Object Pascal method is about six
lines long, takes only one simple parameter, and has only one local variable. It goes against the
general tenor of the language to introduce complex syntactical conceits such as multiple inher-
itance, macros, or variable declarations that end up resolving into multiple layers of aliases.
There is, in my opinion, a place for this kind of thing, but it’s in C/C++, not in Object Pascal.
Currency Type
The Currency type represents the resolution to a long-term problem in Object Pascal. The is-
sue here is that the binary representation of a decimal floating-point value is approximate in
the majority of cases. As a result, they are not a good way to keep track of monetary values.
Integer numbers, on the other hand, are always accurate. However, integer values are not a
good way to track money because they don’t have a convenient system for tracking dollars and
cents; that is, there is no way to use them with decimal points or fractional parts of a dollar.
The Currency type resolves this problem because it is simultaneously an integer type and a
floating-point type. Beneath the surface, Currency types are really integer values similar to the
Comp type. However, they also support the concept of a decimal place. In particular, they have
an implied decimal point that assumes 4 decimal places.
Consider the following example:
procedure TForm1.Button1Click(Sender: TObject);
var
B: Currency;
begin
B := 2.1 + 12;
Panel1.Caption = Format(‘Money: %m’, [b]);
end;
This function uses the Currency type to add two numbers together and display them in the
caption of Panel. The output will be Money: $14.10, as shown in Figure 2.5.
FIGURE 2.5.
The output from
the Money program.
Because the Currency type will not become inaccurate when you are performing math on basic
monetary transactions, it is the preferred type to use when you need to keep track of dollars
and cents.
Summary
In this chapter, you learned about some of the new types found in Delphi 2.0. You also learned
the changes made to some of the old types, and briefly learned a few of the issues you might
want to keep in mind when you use Delphi variables.
On the whole, Delphi makes porting from 16 to 32 bits almost ridiculously easy. Most of the
time you don’t need to think about the port at all; it just happens automatically. However,
there are some underlying issues that you need to consider if you want to delve deeply into the
new compiler and into portability-related issues. As a result, I have tried to introduce some of
the main topics in this chapter.
The following are the key points to remember from this chapter:
■ Delphi has a new string type that no longer has a 255-character limitation. The new
Delphi long strings are implemented as pointers to zero-terminated strings. They do
not, however, act like pointers when you use them in your programs. They travel
incognito, masquerading as normal strings.
■ The Variant type is useful in OLE automation and database applications. It is unique
in that it can be assigned several different types of data, although it is limited to
containing only one type of data at a time. In other words, you can assign it to
multiple types over time, but at any one time it is limited to only one type. This
strange behavior has led to it jokingly being called a “typeless type.” You should not
use variants in standard Pascal routines because of the overhead associated with them.
Use them only in OLE-based applications. They will also be used in database routines,
but most Variant-based database code is implemented by the developers of Delphi,
and you should usually just use regular variables in your database code.
■ The generic Cardinal and Integer types vary in their definition depending on whether
you are in the 16- or 32-bit version of the product. As a result, they tend to help you
optimize your code if you need to switch back and forth between these two environ-
ments. The fundamental types remain the same in both 16- and 32-bit Delphi. The
fundamental types are ShortInts, SmallInts, LongInts, Bytes, and Words.
■ Date-Based Filenames 76
■ ReplaceChars 78
This chapter takes an in-depth look at strings. You have already worked with strings in this
book, but there are a number of features associated with them that you haven’t yet had a chance
to explore. This chapter also explores text files, which provide a simple mechanism for storing
strings in a file.
The first part of the chapter demonstrates that strings, even the new long strings, are really
only a form of array. However, a number of interesting rules specific to strings do not apply to
arrays, and my goal is to explain the most important of them in very explicit detail. In particu-
lar, you will see how to search for a substring in a string and how to parse a lengthy string.
Once you understand how strings work, the next subject to approach is how to store them in
text files, and how to retrieve them again from those text files. While pursuing this subject, you
get a chance to see how one goes about storing numbers in a text file.
var
NearString: TNearString;
MyString: string;
In this code fragment, MyString has nearly all the traits of NearString, plus a few special quali-
ties. In other words, a string is a superset of an array of char, except that you can’t access ele-
ment zero in a long string. The first section of this chapter delineates exactly what special qualities
belong to a string that do not belong to an array of char.
NOTE
This subject is complicated by the fact that AnsiStrings and ShortStrings have a very
different internal structure. From reading Chapter 2, “Delphi Types Under Win32,”
you know that AnsiStrings are not simple arrays, but pointers to arrays. However, in
this chapter, I discuss AnsiStrings as if they were simple arrays of Char, except when I
absolutely need to clarify the situation for some particular reason.
All characters in an array of Char are equal. No one character has any special properties. In a
ShortString, however, the first character is called a length byte. The length byte designates how
many significant characters exist inside the string. Because the first character has this special
task, the first meaningful letter is always offset one in the string. In an AnsiString, there is no
character at element zero in the string. Instead, the length “byte,” which might now be more
appropriately called a length value, is stored in the first 4 bytes at a negative offset. Beyond that
is another 4 bytes that tells you the memory allocated for the string, as well as another 4 that
track the reference count.
NOTE
In the paragraphs that follow, you need to remember that most Chars can be repre-
sented in two different ways. For instance, the letter A can be printed verbatim or it
can be represented by the 65th member in certain character sets. If you want to refer to
the letter A by its place in a character set, you can write #65. The # in this example
designates that the item in question is a char, not a simple numerical value. Therefore,
the number 5 is represented as 5, but the fifth character in a character set is represented
by #5. If you are having trouble grasping these concepts, you might want to refer to the
chapter stored on the CD called, “Integer Types in Detail.” Note also that not all
character sets are identical. For instance, you can run the CharSet program that comes
with Windows 3.1 to confirm that the 65th character in the WingDings character set
is not an A.
Consider a string containing the word Hello. This string is five characters long, so the Length
function will return the number 5:
Len := Length(NearString);
In this case, Len is set to the number 5. The next character in the array would be an H:
NearString[1] := ‘H’; { H = #72 }
If you are working with a ShortString, the end result is an array of six characters that look like
this:
#5,’H’,’e’,’l’,’l’,’o’
If you are working with a long string, the end result looks like this:
‘H’, ‘e’, ‘l’, ‘l’, ‘o’,#0
In this case, the string is null-terminated. All AnsiStrings are null-terminated, and their length
byte is stored at a negative offset.
If the length byte of the ShortString in the preceding example was accidentally set to #6 in-
stead of to #5, then whatever letter happened to be in memory at position 7 would automati-
cally become part of the string, usually with disastrous results. The same thing would happen
if you used the SetLength function to set the length of the AnsiString (in the previous example)
to 6. For instance, the string Hello might suddenly become any of the following:
Hello1
Hellob
Hello#
Hello+
In short, the behavior you can expect to see is entirely undefined in such situations.
The code shown here will compile and run without error:
var
S: ShortString;
begin
SetLength(S, 5); // OldStyle: S[0] = #5;
S[1] := ‘H’;
S[2] := ‘e’;
S[3] := ‘l’;
S[4] := ‘l’;
S[5] := ‘o’;
Edit1.Text := S;
end;
This code prints the word Hello inside an edit control. You can view this code fragment as the
anatomy of a ShortString. It shows explicitly the various parts that go into making up the el-
ements of this type.
The following code behaves in a manner identical to the code shown earlier:
var
S: string;
begin
S := ‘Hello’;
Edit1.Text := S;
end;
This fragment is easier to write than the code shown previously. However, the fact that you
can write code like this is really a special feature of the compiler. What it actually does is shown
in the first example. However, it would be too much trouble to have you go through that lengthy
process every time you wanted to assign a value to a string. Therefore, the compiler lets you
write code like that in the second example.
The following short sample program, called EASYSTR, demonstrates the ideas presented in
this section. (See Listing 3.1.) Specifically, EASYSTR shows what happens if you don’t treat
the length byte in a string with care.
The form for the EASYSTR program includes two buttons and an edit control, as shown in
Figure 3.1.
FIGURE 3.1.
To create the form for the
EASYSTR program, simply
drop two buttons, a panel,
and a label onto a form.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TEasyString = class(TForm)
Panel1: TPanel;
Label1: TLabel;
Valid: TButton;
Bad: TButton;
procedure BValidClick(Sender: TObject);
procedure BBadClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
EasyString: TEasyString;
implementation
{$R *.DFM}
continues
end.
The EASYSTR program enables you to print a valid string and an invalid string inside an edit
control. The invalid string is flawed because it has an incorrect length byte. In particular, it sets
the length byte to 150, although the string you want to print is only five characters long.
This simple mistake would cause trouble in your program. However, I want to make sure you
get a sense of what is going wrong, so I have added another procedure to the function that
ensures that all the characters in a string get set to random values:
procedure ScrambleString(var S: ShortString);
var
i: Integer;
begin
for i := 1 to 255 do
S[I] := Chr(Random(255) + 1)
end;
This function sets all the characters in a string to random values between 1 and 255. It does
this by using the Random function, which returns a random number between zero and the value
passed in its sole parameter. In this case, however, the goal is not to produce a random num-
ber, but a random character. To achieve this end, the Chr function is used. Chr is used to con-
vert a numerical value into a character.
NOTE
There is no significant difference between using the Chr function and just typecasting
the value returned from the random function
S[i] := Char(Random(255));
This code produces the same result as when the Chr function is used.
Note also that this program works with ShortStrings. You would have to modify the
program slightly to get the same effects with long strings. In particular, you could not
start writing random values to a long string without first allocating memory for the
string! Or, to put the matter slightly differently, declaring a ShortString automatically
allocates 256 bytes for your string.
Because I’m using ShortStrings, I could have the ScrambleString function iterate from
0 to 255, but I think Delphi programmers should now always be in the habit of not
referencing element zero in a string.
Figure 3.2 depicts what can happen when you try to write a string to an edit control with the
length byte set to an arbitrarily large value. Clearly the result is less than optimal. Random
characters are scattered across the component like a series of strange hieroglyphics, and the word
Hello is discernible only after you study the output in some depth.
FIGURE 3.2.
If the length byte of a string
is set to the wrong value,
the results can be fairly
chaotic.
You can compare the general fiasco shown in Figure 3.2 with the orderly results shown in Fig-
ure 3.3. In this second case, the compiler is passed a valid length byte and the results are pre-
cisely defined and readily understandable.
FIGURE 3.3.
No matter how you
scramble the extra
characters in a string, the
result shown to the screen
will be fine as long as the
length byte is assigned a
valid value.
Remember that AnsiStrings differ from ShortStrings in that you can only manipulate the length
of the string through the use of the SetLength function. Because of this limitation, it is prob-
ably best to always use SetLength, and to never reference element zero in a string directly.
This code prints the word Hello in a neat and orderly fashion. To do this, it sets the first char-
acter of a PChar to the letter H and sets the sixth character to the value of the first member of the
currently selected character set. It is important to remember that you don’t end zero-
terminated strings with the number zero; you end them with #0. If you refer to the chapter on
the Unleashed CD called “Integer Types in Detail,” you will see that the number zero is usu-
ally the 48th member of a standard character set. It is entirely distinct from the first member of
that character set.
PChars are pointers to arrays of char, and as a result you must allocate memory for them before
you try to use them. For instance, in the following code I explicitly allocate 26 bytes of memory
for a PChar before filling it in with characters and displaying it on the screen. When I am fin-
ished, I then deallocate that memory.
procedure TForm1.BCharsClick(Sender: TObject);
var
S: PChar;
begin
GetMem(S, 26);
StrCopy(S, ‘Hello’);
Edit1.Text := S;
FreeMem(S, 26);
end;
When you allocate memory for a pointer, it is as if someone goes out into the memory theater
and specifically tells a certain number of people that they are now part of a particular alloca-
tion. In this example, for instance, 26 members of the audience are grouped together under
the aegis of a single PChar.
NOTE
If you don’t want to call GetMem and FreeMem, you can also allocate memory for a PChar
by writing the following type of code:
var
S: array[0..25] of Char;
In most situations, the preceding declaration would be equivalent to a PChar and
would be treated the same as a PChar. If you declared the array as being 1..26, instead
of 0..25, the compiler would not treat the array as a PChar.
Devotees of the C/C++ language will notice that Delphi has a function called StrCopy that mirrors
the job performed by strcpy in the land that AT&T made. That is, it copies one string into
another. There are also functions called StrCat, StrPos, StrCmp, and so on. The StrPas func-
tion was used in Delphi 1.0 to convert a PChar into a string, but it is no longer needed. The
developers were able to free us from the need to use that function perhaps in part because PChars
and long strings have a very similar structure.
If you want to learn a little more about PChars, take a look at the EASYSTR2 program, which
is on the Unleashed CD. It enables you to use the two functions shown previously.
For now, however, I’m going to back away from the subject of PChars and arrays of Char, con-
tent to have done little more than introduce the subject. My goal here is not yet to explain this
second type of string in any detail, but to introduce the topic so that no one is taken by surprise
when seeing these types of strings in day-to-day work.
In earlier versions of Delphi and Turbo Pascal, PChars were needed when you worked directly
with Windows API functions. In fact, they are still used in these functions, but the chore of
managing them is simplified because you can typecast an AnsiString as a PChar and assign a
PChar to an AnsiString:
var
P: PChar;
S: String;
begin
S := P;
P := PChar(S);
end;
Because PChars are native to many Windows API functions, sooner or later you will meet them
face to face in some dark coding alley. As a nod to their prominence in the language, I discuss
them in depth in the next chapter, “Pointers and PChars.”
At first glance you might expect this code to print the number 2.03 to the screen. However, it
will not because Str2Real is unable to handle the extra spaces appended after the characters
2.03.
It is likely that a problem similar to this could occur in a real-world program. For instance, a
programmer might ask the user to enter a string, and the user might accidentally append a series
of blanks to it, or perhaps the extra characters were added by some other means. To be sure
that your program will run correctly, you have to strip those extra blank characters off the end
of your string.
You, can, of course, use the Trim function to resolve this problem. However, it’s also useful to
have a more general-purpose routine that will strip off any specified character from the end of
a string. The following function, called StripEndChars, can be used to remove not only blanks,
but any other specified character from the end of a string:
function StripEndChars(S: string; Ch: Char): string;
var
i: Integer;
begin
i := Length(S);
This function does not change the string that you pass into it, but creates a second string that
it passes back to you as the function result. This means you must use this function in the fol-
lowing manner:
S2 := StripEndChars(S1);
where S1 and S2 are both strings. You also can write code that looks like this:
S1 := StripEndChars(S1);
This variable is set to the length of the string passed to the function:
i := Length(S);
The Length function is one of the simplest and fastest executing routines in the Object Pascal
language. In effect, it does nothing more than this, at least when working with ShortStrings:
function Length(S: ShortString): Integer;
begin
Length := Ord(S[0]);
end;
Of course, the actual routine in the runtime library is almost certainly written in assembler,
and it works with both ShortStrings and long strings. Nevertheless, like the one above, it re-
turns the value of the length byte that is the first character of a ShortString, or that resides at
a negative offset in a long string.
The next line in the function checks the value of the last character in the string under investi-
gation:
while S[i] = Ch do begin
More explicitly, it checks to see whether it is equal to the type of character you want to remove.
If it is, then the following code is executed:
Delete(S,i,1);
Dec(i);
The built-in Delete function takes three parameters. The first is a string, the second is an offset
into the string, and the third is the number of characters you want to delete from the first pa-
rameter. In this case, if you passed in the string ‘Sam ‘, which is the word Sam followed by
three spaces; the last space would be lopped off so that the string would become ‘Sam ‘, where
Sam is followed by two spaces.
The Delete function decrements the value of i and then returns to the top of the loop to see if
the next character is equal to the value passed in:
while S[i] = Ch do begin
If it is, that character is also deleted from the end of the string.
The entire process is repeated until the last character in the string is no longer a space. At that
point, the function ends, and a string is returned that is guaranteed not to have any spaces
appended on the end of it.
The StripEndChars function itself is not crucial to most programming endeavors, but becom-
ing familiar with functions of its type is essential. It isn’t really that this one particular function
is so important, although I do end up using it fairly often. What is crucial here is that
StripEndChars is the kind of function that solves a common problem likely to be encountered
by programmers, and furthermore, it does so by bearing down and manipulating a chunk of
data on a byte-by-byte basis.
Date-Based Filenames
This section presents another function for manipulating strings. This function is not likely to
be used every day by most programmers, but when you need it, it comes in very handy indeed.
The primary reason for showing it, however, is just to give an example of working with strings.
Programmers often end up making reports or gathering data on a daily basis. For instance, I
sign onto an online service nearly every day, and frequently I want to store the information I
glean from cyberspace inside a file that contains the current date. In other words, if I sign onto
CompuServe and download the current messages from the Delphi forum, I don’t want to store
that information in a file called DELPHI.CIS. I want a filename that includes the current date,
so that I can easily tell what files were downloaded on a particular day. In short, I want to au-
tomatically generate filenames that look like this: DE022595.TXT, PA022695.TXT,
DE022795.TXT, and so on, where 022795 is a date of the type MMDDYY.
NOTE
Given the long filename support in Windows 95, names such as those shown above
are becoming less common. However, I still use them heavily, in part because the
networks, command-line processors, and compression programs that I most often use
do not yet support long filenames.
This function, demonstrated online in EASYFILE.DPR, takes a two-letter prefix and a three-
letter extension, and creates a filename of the type shown previously. The function begins by
calling the built-in Delphi function Date, which returns a TDateTime structure. This, in turn, is
passed to the DecodeDate function from SysUtils unit, which returns the current year, month,
and day as Word values. If the date were Tuesday, March 25, 1994, the function would return
the following:
Year := 1994
Month := 3
Day := 25
Assuming that the user of this function passed in DE in the PRE parameter, and TXT in the EXT
parameter, it would be fairly easy to use the IntToStr function to create something like this:
DE3251994.TXT
There are several problems with this result, the biggest being that it is 12 characters in length,
which is too long for a legal filename that’s compatible with the old 16-bit filenaming conven-
tions. To solve the problem, it would be nice to be able to change the month to a number such
as 03, to keep the day as 25, and to strip the 19 from the year:
DE032594.TXT
To achieve that end, GetTodayName needs a special function that will not only convert a num-
ber to a string, but also pad it with an appropriate quantity of zeros. The following function
fits the bill:
{----------------------------------------------------
Name: Int2StrPad0 function
Declaration: Int2StrPad0(N: LongInt; Len: Integer): String;
Unit: MathBox
Code: N
Date: 03/01/94
Description: Converts a number into a string and pads
the string with zeros if it is less than
Len characters long.
----------------------------------------------------}
begin
FmtStr(Result, ‘%d’, [N]);
while Length(Result) < Len do
Result := ‘0’ + Result;
end;
This function first uses the built-in Pascal routine called FmtStr to convert a LongInt into a
string. If the string that results is longer than Len bytes in length, the function simply exits and
returns the string. However, if the string is less than Len bytes, the function appends zeros in
front of it until it is Len characters long. Here is the transformation caused by each successive
iteration of the while loop if N equals 2 and Len equals 4:
2 { First iteration }
02 { Second iteration }
002 { Third iteration }
0002 { Fourth iteration }
The point here is that the function checks to see if the string is four characters long. If it isn’t,
the function prepends a zero to the string with the following code:
S := ‘0’ + S;
In the case of the GetTodayName function, the value passed in the Len parameter is 2, because
the need is to translate a number such as 3 or 7 into a number such as 03 or 07.
The final trick in the GetTodayName function is to convert a year such as 1994 into a two-digit
number such as 94. Clearly, this can be easily achieved by merely subtracting 1900 from the
date. However, that sound of hoofbeats in the distance is the rapid approach of the year 2000.
Subtracting 1900 from 2001 would not achieve the desired result. The code therefore first
converts the year into a string and then simply lops off the first two characters with the Delete
function:
Year := Int2StrPad0(y, 4);
Delete(Year, 1, 2);
In this case, a 4 is passed to Int2StrPad0, because the year is originally a four-digit number.
As mentioned earlier, the Delete function is built into the Delphi language. It deletes charac-
ters from a string, starting at the offset specified in the second parameter. The number of char-
acters to be deleted is specified in the third parameter. See the online help for more details.
ReplaceChars
It might be helpful if you take a look at one more function that performs basic string manipu-
lation. The ReplaceChars function substitutes all instances of one character in a string with a
second character:
function ReplaceChars(S: string; OldCh, NewCh: Char): string;
var
Len: Integer;
i: Integer;
begin
Len := Length(S);
for i := 1 to Len do
if S[i] = OldCh then
S[i] := NewCh;
Result := S;
end;
The code first checks the length of the string. If it is zero, the function exits immediately; oth-
erwise, the routine iterates through the string, character by character, looking for the character
you want to replace. If the character is found, the new character is immediately written on top
of it. Note that the routine starts with offset one, not offset zero.
Given this array, the following command will set all the members of this array to #0:
FillChar(MyArray, SizeOf(MyArray), #0);
If you want to fill the array with spaces, you could use the following syntax:
FillChar(MyArray, SizeOf(MyArray), #32);
The key thing to remember when using FillChar is that the SizeOf function can help you be
sure that you are writing the correct number of bytes to the array. The big mistake you can
make is to write too many bytes to the array. That is much worse than writing too few. If you
think of memory as a theater with people, rather than 8-bit values in each seat, you can imag-
ine 10 members of the audience sitting together, all considering themselves part of MyArray.
Right next to them are two people who make up an integer. They are busy remembering the
number 25. Now you issue the following command:
FillChar(MyArray, 12, #0);
All the people who are part of the array will start remembering #0, which is fine. But the com-
mand will keep right on going past the members of MyArray and tell the two folks remember-
ing the number 25 that they should both now remember #0. In other words, the integer value
will also be zeroed out and a bug will be introduced into your program. The result described
here, you should understand, is a best-case scenario. Worst case is that the extra 2 bytes belong
to another program, which means that your program will generate a General Protection Fault.
The moral is that you should always use the FillChar procedure with care.
A function similar to FillChar is called Move. Its purpose is to move a block of data from one
place to another. A typical use of this function might be to move one portion of a string to a
second string, or to move part of an array into a string. The Copy function can also be used for
a similar purpose. The advantage of the Copy function is that it is relatively safe. The disadvan-
tages are that it is less flexible and can be somewhat slower under some circumstances.
Move takes three parameters. The first is the variable you want to copy data from, the second is
the variable you want to move data to, and the third is the number of bytes you want to move:
procedure Move(var Source, Dest; Count: Word);
Below you will find an example of a typical way to use the function. If you enjoy puzzles, you
might want to take a moment to see if you can figure out what it does:
procedure TForm1.Button1Click(Sender: TObject);
var
S1,S2: ShortString;
begin
S1 := ‘Heebee Gee Bees’;
Move(S1[12], S2[1], 4);
SetLength(S2, 4);
Edit1.Text := S2;
end;
The code shown here first sets S1 to a string value. It then indexes 12 bytes into that string and
moves the next 4 bytes into a second string. (Don’t forget to count the spaces when you are
adding up the characters in a string!) Finally, it sets the length of the string to 4, which is the
number of bytes that were moved into it. After executing this code, the final assignment state-
ment will write out the word Bees in Edit1.Text. Here is how to accomplish the same task
using the Pascal Copy function:
S1 := ‘Heebee Gee Bees’;
S2 := Copy(S1, 12, 4);
Edit1.Text := S2;
The first parameter to Copy is the string you want to get data from, the second is an offset into
that string, and the third is the number of bytes you want to use. The function returns a substring
taken from the string in the first parameter.
The Copy function is easier to use and safer than the Move function, but it is not as powerful. If
at all possible, you should use the Copy function. However, there are times when you can’t use
the Copy function, particularly if you need to move data in or out of at least one variable that is
not a string. Also, it is worth remembering that Move is very fast. If you have to perform an
action over and over again in a loop, you should consider using Move instead of Copy.
It’s interesting to note that the above function will not work if you are working with AnsiStrings.
Instead, you would have to write code that looks like this:
procedure TForm1.Button1Click(Sender: TObject);
const
Size = 4;
var
S1, S2: string;
begin
S1 := ‘Heebee Gee Bees’;
SetLength(S2, Size);
Move(S1[12], S2[1], Size);
Edit1.Text := S2;;
end;
The issue here is that you cannot move memory into S2 without first allocating memory for it.
In particular, recall that S2 is really a pointer to a string, and so you will generate an access
violation if you try to just move memory into it.
As easy as it is to write data to the wrong place using the FillChar statement, you will find that
the Move statement can lead you even further astray in considerably less time. It will, however,
get you out of all manner of difficult corners—as long as you know how to use it.
The following function puts the Move procedure to practical use. As its name implies, the
StripFirstWord function is used to remove the first word from a string. For instance, it would
change the following string
‘One Two Three’
The first line in this function introduces you to the built-in Pos function, which locates a
substring in a longer string. For instance, in this case, the Pos function is used to find the first
instance of the space character in the string passed to the StripFirstWord function. The func-
tion returns the offset of the character it is looking for.
More specifically, the Pos function takes two parameters. The first is the string to search for,
and the second is the string you want to search. Therefore the statement Pos(#32, S) looks for
the space character inside a string, where the string is represented by the variable S.
If you passed in the following line of poetry: The pure products of America go crazy, the Pos
function would return the number 4, which is the offset of the first space character in the pre-
ceding sentence. However, if you passed in a simpler string such as Williams, Pos would return
0 because there is no space character in the string. If the function does not find a space charac-
ter in the string, it returns the original string:
if i = 0 then begin
StripFirstWord := S;
Exit;
end;
The built-in Exit procedure shown here simply exits the function without executing another
line of code. This is the StripFirstWord function’s sole, and rather limited, exercise in error-
checking.
If the offset of a space character is returned by the Pos function, the Move function transfers
“length - offset” number of characters from the string that is passed in to a different location in
the same string:
i := Pos(#32, S);
...
Size := (Length(S) - i);
Move(S[i + 1], S[1], Size);
SetLength(S, Size);
The next line of code sets the length byte for the newly created string, which no longer con-
tains the first word in the sentence.
The first step is to determine the number of characters in the sentence after the first word is
removed. This is found by subtracting the number returned by Pos from the total length of the
sentence. StripFirstWord then moves the remaining portion of the string from a position “off-
set” characters deep in the following string:
She was a child and I was a child, In a kingdom by the sea
The extra characters, represented in this case by the second occurrence of sea, are then lopped
off by setting the length byte to the appropriate number of characters:
was a child and I was a child, In a kingdom by the sea
Most of the routines you have seen in this chapter are useful enough for you to want to save for
future use. With this thought in mind, I have placed them in the STRBOX unit, which is a com-
panion unit to the MATHBOX unit found on the Unleashed CD-ROM, and discussed in the
CD-based chapter “Units, Real Numbers, and Functions.”
The following program, called TESTSTR, gives you a chance to work with the StripFirstWord
function. This program takes any sentence you enter and separates it into a series of individual
words that are displayed in a listbox.
To avoid any problems that might arise from accidentally prepending spaces before a string,
the TESTSTR program makes use of the following function:
{---------------------------------------------------
Name: StripFrontChars function
Declaration: StripFrontChars(S: string; Ch: Char) : String;
Unit: StrBox
Code: S
Date: 03/02/94
Description: Strips any occurances of charact Ch that
might precede a string.
----------------------------------------------------}
function StripFrontChars(S: string; Ch: Char): string;
var
S1: string;
begin
while (S[1] = Ch) and (Length(S) > 0) do
S := Copy(S,2,Length(S) - 1);
StripFrontChars := S;
end;
This routine removes a particular character from the start of a string until there are no more
instances of the character prepended to the string, or until the string is completely empty. If
you pass it a string and #32, it will make sure there are no spaces preceding the string. (If you
want to remove only spaces from the front of a string, you can call TrimLeft.)
StripFrontChars works its magic by first checking to see if the initial character in the string has
the same value as Ch. If it does, it finds the second character in the string and copies it and the
remainder of the string back over the first character of the string, thereby accomplishing a task
similar to that undertaken by the second Move statement in StripFirstWord.
NOTE
Several places in this chapter I have shown you two ways of accomplishing the same
task. Experienced programmers know that writing code is a bit like writing prose:
There are always several different ways to say the same thing. Some people believe that
there are quantifiable ways of finding the best way to accomplish a particular task. To
my mind, there is no objective way to determine the best course of action in many
programming dilemmas. For instance, it is often true that one method is faster than
another, but it might also be more difficult to maintain. Which is better, the faster
code or the more maintainable code? It depends.
There are few objective means of determining the best way to write a block of code;
there are certain programmers who consistently write excellent code. Programming is
one field in which there is little substitute for raw talent. However, average program-
mers can accomplish a lot if they have patience, good toolboxes, and good compilers.
The TESTSTR program uses an edit control, a button labeled Parse, and a listbox. The form
for the program is shown in Figure 3.4, and the code in Listing 3.2.
FIGURE 3.4.
The form for the
TESTSTR program.
interface
uses
WinTypes, WinProcs, Classes,
Graphics, StdCtrls, Controls,
Forms;
type
TForm1 = class(TForm)
BParse: TButton;
Edit1: TEdit;
ListBox1: TListBox;
procedure BParseClick(Sender: TObject);
continues
var
Form1: TForm1;
implementation
uses
StrBox;
{$R *.DFM}
end.
The TESTSTR program uses the OnCreate event to specify a string and to pass it to the
BParseClick function. This function uses RemoveFirstWord to break the sentence into individual
words and to display each word in a listbox. One of the interesting aspects of the program is
that it shows how you can place routines in a unit such as STRBOX and then call them in a neat
and easily readable fashion.
There are hundreds of different functions you could write to help you perform certain string-
oriented tasks. The ones in this chapter should help you get started creating a library of your
own that you can turn to when you need a quick solution for a problem involving a string. The
final version of the STRBOX program that ships with this book contains a number of addi-
tional string manipulation routines that I have built up over the years. Readers of the first edi-
tion of this book should note that the implementation for this unit has been changed rather
heavily, in part because of the impact of long strings. The interface, however, is pretty much
the same as in the first version of this book, except for the fact that there are a few new rou-
tines.
This syntax tells the compiler that it needs to set aside only 31 seats for this particular string.
Thirty will hold the string itself, and the thirty-first holds the length byte. By declaring a string
this way, you save 256–31, or 225 bytes.
Delphi enables you to declare ShortStrings of any length between 1 and 255. For instance, all
of the following are valid ShortString declarations:
S: string[1];
S1: string[255];
S2: string[100];
S3: string[25];
Strings of certain lengths are declared so often that you might want to create special types for
them:
type
TStr20: string[20];
TStr25: string[25];
TStr30: string[30];
TStr10: string[10];
TStr80: string[80];
Given these declarations, you can write code that looks like this:
var
S1: TStr20;
S2: TStr30;
The syntax shown here can help you save memory without having to perform much extra work.
In fact, the types shown here are so useful that I have added them to the STRBOX unit so that
you can access them easily at any time. I have also added a few other string declarations that
can prove useful when you are working with files in 16-bit Windows:
DirStr = string[67];
PathStr = string[79];
NameStr = string[8];
ExtStr = string[4];
The only problem with limiting the length of strings through the technique shown here is that
it still requires you to know the length of a string at design time. Working with AnsiStrings, or
working with PChars and the GetMem function, it is possible to set the length of a string at runtime,
which means you can make strings that are exactly long enough to hold the characters stored in
them. (You can learn more about pointers and the GetMem function in Chapter 4, “Pointers
and PChars.”) You also can declare a pointer to a standard short string, predeclared by Delphi
as a PString, and then allocate just the amount of memory you need for it.
It might be easier to use the default behavior of AnsiStrings when you want to limit memory
usage rather than going to the bother of declaring the special types of strings shown above.
However, you should study the techniques shown here carefully—in case you ever need them.
Remember also that strings declared as part of an object or procedure are local to that object or
procedure, and will be in memory only as long as the object or procedure that owns them.
Variable F now represents a text file, and it can be used to specify where you want data to be
read or written.
NOTE
Pascal has traditionally used the identifier Text rather than TextFile in situations like
the one shown above. You can, in fact, still use the Text type. However, you will
probably also need to qualify the identifier Text with the name of the unit where it is
declared, which is the SYSTEM unit. This is necessary because the TForm object already
has a variable with this name inside it, thereby causing a name conflict, since you can’t
use a variable as a type. To remedy the situation, you need to specify which instance of
the identifier Text you want to reference. Specifically, you are saying that you want to
reference the Text type that is declared in the SYSTEM unit: SYSTEM.Text. The same
problem occurs with the identifiers Assign and Close.
Needless to say, the name conflict shown here never should have occurred, and its
presence in the shipping version of Delphi is a testament to the fact that even the
developers of Delphi are fallible.
After declaring the file variable, the next step is to associate it with a particular filename:
var
F: TextFile;
begin
AssignFile(F, ‘MYFILE.TXT’);
...
It is traditional to assign the extension TXT to a text file. Of course, many files don’t follow
this convention. For instance, INI files don’t use the TXT extension, and neither do files la-
beled READ.ME. However, most text files do have a TXT extension, and this is the accepted
and traditional way to treat them.
After the assignment statement, the next step is to open the file with the Reset, Rewrite, or
Append routine. The Reset procedure opens an existing file:
var
F: TextFile;
begin
AssignFile(F, ‘MYFILE.TXT’);
Reset(F);
...
end;
The ReWrite procedure creates a file or overwrites an existing file. The Append routine opens an
existing file. It does not overwrite its current contents, but it enables you to append new strings
to the end of a file:
var
F: TextFile;
begin
AssignFile(F, ‘MYFILE.TXT’);
Append(F);
...
end;
NOTE
There is no simple way to open a text file and insert a string into it at a particular
location. You can create a new file, overwrite an existing file, or append a string onto
an existing file. You can’t easily add a new string in the fourth line of a twenty-line text
file. I say you can’t do this easily, but there are ways to do this. They usually involve
opening the file in either binary or text mode, reading its entire contents into an array
or other complex data structure, inserting a string, and finally writing the file back out
to disk.
Once you have opened a text file, you can write a string to it with the Write or WriteLn proce-
dure:
var
F: TextFile;
begin
AssignFile(F, ‘MYFILE.TXT’);
ReWrite(F);
WriteLn(F, ‘Call me Ishmael.’);
CloseFile(F);
end;
Notice that the WriteLn statement begins by referencing the file variable as its first parameter.
The method shown here ends with a CloseFile statement. If you want to use the more tradi-
tional Close statement, you probably need to qualify it by referencing SYSTEM.PAS. (I say
probably, since you will not need to qualify it if you are in a stand-alone procedure that is not
part of an object that contains a method called Close.)
Bugs can be introduced into your program if you forget to call CloseFile. More specifically,
there is an internal buffer associated with each text file that will probably not be flushed if you
fail to call CloseFile. If the buffer isn’t flushed, a portion of the file will remain in memory
rather than being written to disk. As a result, it appears that your program is not writing prop-
erly to the file, and programmers can spend a long time looking for a memory corruption prob-
lem when the source of the trouble is simply failing to call CloseFile. See the Flush procedure,
found in the online help.
NOTE
The SYSTEM file is buried deep in the Delphi runtime library. Every Delphi program
you create will automatically have the system file linked into it, even if you don’t
explicitly reference it in your uses clause. Most of the SYSTEM unit contains assembly
language code for elemental routines such as WriteLn, Assign, ReWrite, Reset, and a
few other functions that have survived since the earliest versions of the Turbo Pascal
compiler. If you want to know more about this unit, you can look it up in the online
help or look for the source code on disk. The availability of the source code depends on
which version of Delphi you bought. That is, it comes with the client/server version,
but not the desktop version. If it exists on your system, it would be stored somewhere
beneath the source subdirectory. You can purchase the RTL separately from Borland.
If you want to read a string from a text file, you can use the Read or ReadLn statement:
var
F: TextFile;
S: String;
begin
AssignFile(F, ‘MYFILE.TXT’);
Reset(F);
ReadLn(F, S);
WriteLn(S);
CloseFile(F);
end;
Notice that this code uses the Reset procedure to open an existing file and then uses ReadLn to
retrieve the first string from this file.
You can also read and write numbers from a text file. For instance, the following code is en-
tirely legal:
var
F: System.Text;
S: String;
i: Integer;
begin
System.Assign(F, ‘MYFILE.TXT’);
ReWrite(F);
S := ‘The secret of the universe: ‘;
i := 42;
WriteLn(F, S, i);
System.Close(F);
end;
This code, which uses the word System as a qualifier, writes the following line into a text file:
The secret of the universe: 42
you could read the first line from this file with the following code:
var
F: TextFile;
i, j, k: Integer;
begin
AssignFile(F, ‘MYFILE.TXT’);
Reset(F);
ReadLn(F, i, j, k);
CloseFile(F);
end;
The code shown here would read the numbers 10, 101, and 1001 from the file. If you wanted
to read both lines from the file, you could write
var
F: TextFile;
i, j, k, a, b, c: Integer;
begin
AssignFile(F, ‘MYFILE.TXT’);
Reset(F);
ReadLn(F, i, j, k);
ReadLn(F, a, b, c);
CloseFile(F);
end;
You can use a function called EOF to determine if you are at the end of a text file. For instance,
if you had a file that contained several hundred lines of numbers like those shown in the small
file listed earlier, you could read the entire file in the following manner:
var
F: TextFile;
i, j, k, Sum: Integer;
begin
AssignFile(F, ‘MYFILE.TXT’);
Reset(F);
while not EOF(F) do begin
ReadLn(F, i, j, k);
Sum := I + j + k;
WriteLn(F, ‘i + j + k := ‘, Sum);
end;
CloseFile(F);
end;
The EASYFILE program, shown in Listing 3.3, demonstrates how to use the TTEXTREC struc-
ture to determine a file’s name and whether a file is open for input, open for output, or closed.
Specifically, you can typecast a variable of type Text so that you can test its state, as shown
here:
var
F: TextFile;
begin
if TTextRec(F).Mode := fmClosed then OpenTheFile;
end;
TTextRec is in the online help. The Mode constants are declared in SYSUTILS.PAS as follows:
fmClosed = $D7B0;
fmInput = $D7B1;
fmOutput = $D7B2;
fmInOut = $D7B3;
Listing 3.3. The EASYFILE program demonstrates some basic techniques for
handling TextFiles.
unit Main;
{
This program shows how to work with strings and
with text based files.
fmClosed = $D7B0;
fmInput = $D7B1;
fmOutput = $D7B2;
fmInOut = $D7B3;
As declared in SYSUTILS:
PTextBuf = ^TTextBuf;
TTextBuf = array[0..127] of Char;
TTextRec = record
Handle: Word;
Mode: Word;
BufSize: Word;
Private: Word;
BufPos: Word;
BufEnd: Word;
BufPtr: PTextBuf;
OpenFunc: Pointer;
InOutFunc: Pointer;
FlushFunc: Pointer;
CloseFunc: Pointer;
UserData: array[1..16] of Byte;
Name: array[0..79] of Char;
Buffer: TTextBuf;
end;
}
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
StdCtrls, SysUtils, ExtCtrls,
Dialogs;
type
TForm1 = class(TForm)
RunTest: TButton;
OpenInput: TButton;
OpenOutPut: TButton;
CloseFile: TButton;
Panel1: TPanel;
Panel2: TPanel;
Label1: TLabel;
Label2: TLabel;
procedure RunTestClick(Sender: TObject);
procedure OpenInputClick(Sender: TObject);
procedure OpenOutPutClick(Sender: TObject);
procedure CloseFileClick(Sender: TObject);
private
F: TextFile;
end;
var
Form1: TForm1;
implementation
continues
{$R *.DFM}
end.
When using this program, it is important that you push the buttons on the main form in the
order in which they appear. That is, push the top one first, the next button down, and so on.
If you press the buttons out of order, the program could raise an exception. You could, of course,
disable and enable buttons in order to force the user to proceed in a particular fashion, but I
decided against that approach in order to keep the code as simple as possible.
Notice that the following method uses exceptions to handle the errors that would occur if the
user pushed the buttons in the wrong order:
procedure TForm1.OpenInputClick(Sender: TObject);
begin
try
Reset(F);
except
on EInOutError do
MessageDlg(‘File must be created first’, mtInformation, [mbOk], 0);
end;
Label2.Caption := GetMode(F);
end;
The code first attempts to open the file with the Reset function. If this attempt fails, the user
is notified that an error occurred, and the function immediately terminates.
In this section, you learned the basics about text files. One of the most important points to
remember is that you can open a file one of three ways:
■ ReWrite opens a new file or overwrites an existing file.
■ Reset opens an existing file for reading.
■ Append opens an existing file and enables you to append text to the end of it. If the file
does not exist, an error condition results.
Summary
In this chapter, you explored strings and text files. You saw that strings are really just a special
form of array, and that you can manipulate them in myriad ways by treating them as vectors
filled with chars. You also saw that there are many functions used to manipulate strings. In
particular, you were exposed to the Move, FillChar, Delete, Copy, and Pos routines. You also
learned something about the differences between ShortStrings and AnsiStrings.
The last part of the chapter presented a basic introduction to TextFiles. You saw how to open
and close these files, how to read and write to them, and how to detect if you are at the end of
a text file. Look on the CD for programs showing how to parse text files.
In the 16-bit world, manipulation of pointers was probably the single most powerful skill in
the repertoire of experienced programmers. However, the need to understand pointers is not
quite as pressing in the 32-bit world. In fact, you can write many very good programs without
understanding pointers. Nevertheless, pointers still lie at the heart of most advanced program-
ming techniques. If you really understand pointers, little in computer programming is likely to
be beyond you.
I don’t mean to imply that understanding pointers makes computer programming easy. That
is not at all the case. Pointers tend to be a relatively difficult subject, even in their easiest and
most comprehensible forms. Furthermore, the advent of 32-bit flat model, programming for
PCs means that the need for pointers in your code has gone way down. You had to use pointers
in the 16-bit world, but you don’t have to in the 32-bit world. If you are in doubt about whether
or not you need to use pointers, don’t use them.
Pointers are worth the struggle, however, because they enable you to directly manipulate
memory, or at least simulate the act of directly manipulating memory. When you are dealing
with pointers, there are few barriers between you and the raw bytes of memory that exist in
RAM, or in other portions of the computer’s memory. With pointers, you are able to manipu-
late the raw material that forms the life blood of every computer.
NOTE
Once again, pointers are not nearly as important in Delphi 2.0 as they were in Delphi 16. I
suggest that under most circumstances you shy away from using either pointers or PChars. Avoid
them as much as possible because they can lead to bugs! It’s easy to make a mistake using pointers.
For the speed demons in the crowd who want to use pointers for performance reasons, please
note that all Delphi objects, as well as AnsiStrings, are actually disguised pointers. That is, they
are pointers masquerading as static values. The apparent absence of pointers in your program
does not mean that your code is taking a speed hit.
NOTE
In the 16-bit world, pointers were bound by the 64KB limitation imposed by the 8086 and
286 architectures. In that world, pointers consisted of two different pieces: the first referencing
a particular 64KB segment in the first megabyte of memory and the second referencing an offset
in that segment. 16-bit, protected-mode implementations did some hand waving that allowed
programmers to access memory outside of the first megabyte, but the actual numbers out of
which the pointers were made were still bound by extreme limitations. In particular, you could
not reference more than 64 KB of memory at one time. That was the limit; a 16-bit number
simply can’t address more than 64 KB of memory. (The HUGE pointers in C made it appear
that we were addressing more than 64 KB at a time, but this was never more than a rather
tenuous mirage.)
In the 32-bit world, the limitation is no longer 64 KB, but 4 GB. That is, the maximum num-
ber of addressable bytes in a single pointer is 65,536 in the 16-bit world, and 4,294,967,296 in
the 32-bit world. The maximum amount of addressable memory in real mode is 1,048,576
bytes, in 16-bit protected mode 16,777,216, and in 32-bit protected mode 4,294,967,296.
The switch to 32 bits turns the computer world on its head. Before we had more memory than
we could easily address. Now we have more address space than memory—much more address
space.
To step from the 16-bit world to the 32-bit world is a bit similar to the northern Europeans
starting to circumnavigate the globe with ships. Before that, the world was limited to just a few
small countries, and then all at once the entire world was within reach. On a more modest
scale, the same thing happened when planes and cars were developed. Before these technolo-
gies were available, most people lived entire lives within a 50-square-mile radius of their home.
Afterward, nearly everyone began to travel across entire countries, and many now travel regu-
larly between continents. The same thing will happen if we ever begin to seriously explore space.
The limitations of our reach will switch from a single planet to an entire solar system, and perhaps
some day to an entire galaxy.
When you are thinking in terms of pointers, the switch from the 16-bit to the 32-bit world is
a vast reorientation in space and time. The world isn’t just one or two times bigger, it’s thou-
sands of times bigger.
I mention all this because it has a rather strong impact on the memory theater. In the 16-bit
world, you could imagine the dimensions of the Memory Theater. It was huge, holding at least
640,000 seats. That’s bigger than any known existing theater, but it’s still an imaginable num-
ber. The million man march, for instance, was probably approximately that big. Four gigabytes
worth of seats, however, is just beyond the range of imagining. It’s just plain flat-out incom-
prehensible, at least from my point of view.
So, the first thing to grasp about 32-bit pointers is that they reference spaces that are almost
unimaginably large. You have to start thinking in terms of interplanetary, perhaps even inter-
stellar, distances before you can find analogies for the size of the numbers addressed by a 32-bit
operating system.
A second key point about 32-bit pointers is that they almost all address virtual memory. Under
Windows 95, for instance, most programs are loaded into memory at the 4MB mark and can
access memory up to the 2GB mark. Clearly, none of the machines currently running Win-
dows 95 have anything like 2 GB of memory on them.
To have a program access memory at the 2GB mark clearly involves a certain amount of hand-
waving. In particular, there is a table in memory called a descriptor table. Each address used in
Win32 is really a reference into this table, and not a reference into a real place in memory. To
find the actual address of a variable residing in memory, you would have to first take the ad-
dress you are working with, which is a virtual address, and then see what that address really
points to by looking it up in the descriptor table. In other words, addresses aren’t really ad-
dresses; they are just references to a look up in the descriptor table. The descriptor table con-
tains the actual addresses of places in RAM. Under normal circumstances you never look at
these real addresses, and there is rarely any practical reason to do so.
Well, even that last paragraph is a bit of a simplification. Windows actually has a paging scheme
that complicates matters even further, but this is not the time to go into these details. If you
want to find out the specifics, you should read Jeffrey Richter’s Advanced Windows, by Microsoft
Press, and Extending DOS, by Ray Duncan, et al, Addison Wesley Press. I will address some of
the issues in Chapter 8, “Multitasking, Threads, and Memory: The Windows 95 Environment.”
This is not the part of this book where I want to dig deeply into the nature of memory under
Windows 95. Instead, I want to make clear that the memory theater analogy is strained by the
appearance of Win32. However, underneath all the descriptors, and all the paging, all the vir-
tual memory, Microsoft and Intel still want you to be able to think of memory as if it were
something tangible. The operating system goes to enormous lengths to make memory seem
simple, to make it appear to be as straightforward as it seems when I discuss the memory the-
ater.
In the memory theater, the CPU addresses the audiences and asks them to participate in the
play currently being enacted. It’s an audience-participation theater. In particular, the CPU,
from its position on the stage, might tell four bytes out in the audience that it is their job to
remember an integer value. It takes four members of the audience to remember a single integer
value because an integer is four bytes in size.
The purpose of this section, then, has been twofold:
■ I want to make it clear that the memory theater analogy is less than perfect when you
are working under Win32. Don’t take it too literally.
■ Windows goes to great lengths to make the virtual addresses you use in your program
seem as if they reference actual places in RAM, and not just a series of tables and
pages. You are running your program inside a virtual machine that emulates in
software a real-mode, 4GB machine. Most of the time it is a good idea to buy into this
illusion.
Maybe under Win32, the memory theater should be given a new name: the virtual memory
theater. When you play Doom, Heretic, or Phantasmagoria, you don’t think you are moving
through a real world. Nevertheless, the illusions created by these games are clever enough to
make users grasp the rules of the virtual world in which they play, and to see how it has paral-
lels to the real world. In the same way, Windows makes programmers feel as though the ad-
dresses they get are real addresses. It’s a virtual memory theater.
In real mode memory the CPU addressed the actual bytes sitting in the audience. It talked to
“real” bytes of memory. In Win32, the CPU addresses a set of virtual bytes. The programs
running in the theater think they are standing in front of an audience 4 GB in size, but they are
actually sitting in front of an elaborate virtual display that simulates a theater four gigabytes in
size.
Of course, there is real RAM in a computer, but your program never addresses it directly. In-
stead, it addresses virtual RAM, and the operating system then moves beyond the scenes to
translate these simulated actions into actual events that effect RAM, your hard disk, video
memory, or some other part of your computer. Your program, however, very rarely actually
directly touches any of these things. It’s working with a simulation of memory, not with real
memory.
The memory theater under Win32 is based upon an illusion. There isn’t a one-to-one analogy
between the seats in a theater and the bytes of memory in RAM because programmers no longer
address actual bytes of memory in RAM. Instead, programmers address descriptor tables, they
address virtual pages of memory. However, it appears to the programmer that they are address-
ing actual bytes of memory, just as it appears that you walk through a real house when you play
Phantasmagoria. To the imaginative it can appear that the virtual bytes of memory in Win32
are like seats in a vast theater. The whole thing is an illusion within an illusion, yet it can still
help you understand how memory works—as long as you don’t start taking it too literally!
If all this doesn’t come home to you right away, don’t worry. I’m going to talk a lot about
memory in this book, and for most readers a clear conception of memory under Win32 will
probably sink in only slowly, over a considerable period of time. In particular, I spend copious
amounts of time discussing these subjects in Chapter 7, “Multitasking and Threads,” and
Chapter 8, “Win32 Memory Management.”
If you’ve come out of your reading of the last few paragraphs with an understanding that there
is an important difference between virtual memory and real memory, you probably have enough
knowledge to go on from here. Later on, however, you will want to be sure that you have a
clearer conception of the exact nature of virtual memory.
NOTE
During this discussion you should stop trying to see through virtual memory and
should instead allow yourself to be fooled by the illusion it presents. There is no danger
in doing this because the illusion is very carefully constructed and follows a very clearly
laid-out set of predefined rules. You should now believe that virtual memory is real
memory, just as an air traffic controller comes to believe that the blips on a screen are
actual airplanes.
Suppose you have a list of 100 sentences, each of which is between 100 and 150 characters in
length. Furthermore, suppose this list of sentences is not part of the body of a text, but is the
list of error messages displayed by your program. In other words, you don’t necessarily want to
show this entire list to the user at once, but you might want to grab the 12th string, or 23rd
string, or some other particular string, and show it to the user.
Given this scenario, it might be helpful to keep a set of references to these strings, rather than
wandering through the seats of the memory theater trying to find a particular string at a par-
ticular time. As it happens, there are a number of different ways you can keep these references,
but one convenient way to do it would be by keeping pointers to these strings.
In the 32-bit Intel architecture, pointers occupy four bytes. That is, they take up four seats in
the memory theater. These four bytes contain the address of a particular location in the memory
theater. In other words, the four seats occupied by a pointer contain the reference number, or
address, of other seats in the memory theater.
Suppose the first of the 100 strings you want to reference is located in seat 255A. One simple
way to reference this number would be to create the following simple piece of syntax:
$255A
Though this syntax might not make much sense to a total stranger, it is easily understood by
someone who recognizes it purpose. It’s the hexadecimal number for a seat in the memory the-
ater. You just start numbering the seats: The first one is $1, the second is $2, and so on.
NOTE
Before the emergence of Win32, all the seats in the memory theater were referenced by
segment and offset. For instance, a typical 16-bit addresses might look like this: 25:5A.
In the memory theater analogy, the number before the colon referred to a section
number and the number after the colon referred to a seat number. In the real world,
the first number referenced a 64KB segment of memory, and the second number
referenced an offset in that 64KB block. This is no longer the system. Now the world
is greatly simplified, and each of the 4GB worth of seats in the memory theater has its
own unique number, with $FFFFFFFF being the largest possible number. (Ironically, the
above number is -1, if you considered it to be a signed number.)
The addresses of the seats in the 32-bit memory theater are very simple. They are just hexa-
decimal numbers. Besides its simple syntax, another advantage of this construction is that it
can easily be stored in a small area. For instance, it takes only four members of the audience to
remember the entire number. In particular, each person is remembering an 8-bit value, which
can be as high as 256, which is $FF. The result would be that the entire 32-bit number could be
memorized by four members of the audience: $FF FF FF FF.
NOTE
To understand exactly what is going on here, you need to recall that while one member
of the audience can remember a number between 0 and 255, two members of the
audience working together can remember numbers between 0 and 65,364. Four
members working together can remember a number between 0 and 4,294,967,296.
Using a system in which four members of the audience remember the seat number of another
group of people in the audience, it is possible to have a relatively manageable block of 100 sets
of pointers, where each pointer is really just four seats in the memory theater.
100 × 4 is equal to 400, which is a fairly large number of seats. However, if each of the strings
being referenced averaged 125 characters in length, the approximate number of bytes in the
group is 100 × 125, or 12,500 seats. 12,500 is a large number of seats to have to sort through,
even for a computer. Obviously, it would be helpful to be able to refer to a smaller lookup
table, consisting of 400 seats, that can be broken down into groups of 4 seats each. This is what
pointers do for you. Because each pointer is the same size, you can jump to the tenth pointer
by moving in 40 bytes. You could not, however, jump to the 10 string by moving in any par-
ticular number of bytes because you don’t know the length of each string ahead of time.
Suppose you want to find the 20th string in the group of strings. One way to find it would be
to start wandering through the section where the strings are stored, looking for the beginning
and end of each string, and keep looking until you finally find the string you want. A second
method would be to go to the 20th pointer to see which seat number is stored there. Then you
could go directly from the pointer to the string itself. In effect, the people who sit in the four
seats of the 20th pointer can stand up and point directly at the seat that represents the begin-
ning of the string you want to find, as shown in Figure 4.1.
FIGURE 4.1.
Four members of the
memory theater save the
seat number where a string
is stored.
String Pointer
It doesn’t take much imagination to see that pointers can help speed up operations inside a
computer. By using pointers, you can avoid lengthy searches through hundreds of thousands
of bytes, and instead be directed quickly and efficiently to the seat you want to find.
Furthermore, if you want to change the order of the strings you are referencing, you don’t actually
have to move a 150-character string from one place to another. Instead, you can simply change
the addresses stored in the pointers. For instance, if one pointer references a string starting in
seat $00001A2B and the next pointer references a string in seat $00005A6B, reversing the order of
these strings is a simple matter of swapping two 4-byte pointers. This is obviously much faster
than trying to move hundreds of bytes around.
Some readers might be inclined at this point to take this whole matter back one step further
and ask what it is that points at the pointer. In other words, it is easy enough to imagine how
the pointers described here can help you reference a particular place in memory, but how does
one get to the pointer in the first place?
The short answer is that the locations of certain pointers are stored in your executable. In other
words, when you write a program, code is generated that says, in effect, go to this spot, find a
pointer, and then do something with the memory referenced by the pointer. The code usually
points to a place in your address space or stack segment where the pointers you declare are
stored. However, you also can have a pointer in your program’s data that points to a pointer on
your heap, which points to yet another pointer on the heap, and so on.
You can trace this answer back even further, to the moment when a computer is first switched
on. At that moment, pointers are loaded into memory that reference instructions for booting
up a computer and loading the operating system. These instructions and data in turn use pointers
to reference other places inside the computer, and so on. What really ends up happening is
that a computer follows an endless stream of pointers, each of which keeps referencing some
other place in memory where data or instructions are stored.
NOTE
Pointers are sometimes referred to as addresses because that is really all that a pointer
represents. For instance, you might hear someone say, “Pass the address of variable J to
the FooBar function.” What this means is that you should pass a pointer to the variable
J to the FooBar function. Pointers are really nothing more than addresses. Furthermore,
you can obtain the address of any variable by appending the @ symbol before it:
var
A: Integer;
P: Pointer;
begin
Caption := Format(‘$%p <<=>> $%p’, [@A, P]);
end;
The code shown here would write the address of the variable A to the caption at the top
of a window. It would also write the pointer value P to the caption. The result might
look something like this: $0065F8DC <<=>> $0065F938, where the <<=>> symbol is just a
randomly chosen separator to help you keep the two address separate in your mind.
Remember that addresses are normally written in hex notation, so I prepend a $ to the
above result. Another way to see a meaningful version of the address of the variable A is
to type @A in the Watch Window. Be sure that the variable A is in scope when you view
it in the integrated debugger.
Once again, I want to stress that you don’t have to understand all this in order to write code.
However, if you can begin to see what I am driving at, or if this material is already old hat to
you, you are sitting in the catbird’s seat, with all the vast realms of the computer spread out
before you. Pointers are the keys to the kingdom, and with them you can unlock virtually any
door.
Finally, I want to remind you once again that the addresses in memory are not real addresses,
they are virtual addresses. The memory theater is not a perfect analogy, but it does create a
virtual world that you can play within your mind. Because you never actually touch real memory
in a protected-mode operating system, there is something appropriate about creating a virtual
memory theater to help you imagine the inside of a computer. Programming is like a game.
Everything about memory and computer languages is an illusion, just as games like Phantas-
magoria or Doom are illusions. Computer-based illusions, however, can be both interesting
and useful. In truth, I have always found programming more interesting in the long run than
even the cleverest computer games and, conversely, what really interests me in computer games
is how they are programmed.
My point is that virtual memory might not be real memory, but that doesn’t make it any less
powerful or valuable! At some level, it’s hard to give up a slide rule and start using a calculator.
It’s difficult to move from a typewriter to a word processor. It’s hard to give up eye-to-eye con-
tact and instead rely on sonar or radar. But in the long run, all but the most starry-eyed ludite
would agree that you are better off making all of these changes. In the same way, you are better off
moving from real memory to virtual memory. It’s all an illusion, but a very useful one.
need room for an integer. Please reserve four seats for me.” Once you make this request, the
owners of the memory theater are honor-bound to reserve those seats until you tell them you
want to dispose of them.
It is very important that whenever you allocate memory, you eventually come along and
deallocate it. The problem is that there are only a certain number of seats in the memory the-
ater. If you allocate memory and then forget to deallocate it, eventually you will go to the ticket
office and ask for more seats, and you will be told that the house is all booked up, or more
likely, you will find that the request is honored, but that the performance on your system be-
comes very slow. In particular, protected mode provides a kind of virtual memory that can be
swapped to your hard disk. Therefore, it can keep swapping between RAM and your hard disk
for a very long time before it actually runs out of potential memory. However, long before all
the possible bytes are allocated, there will come a time when you machine’s performance will
become unacceptably slow, or when some particular resource on your system will be depleted.
However, you will not actually run out of memory until you run out of hard disk space. If you
have 2 GB of empty hard drive space on your system, you have at least 2 GB of memory avail-
able to your program.
NOTE
One of the key problems that has haunted the worlds of DOS and Windows has been
that there is very little to keep an ill-behaved program from taking up more seats than
it reserved. Some programs simply write over the seats reserved by another program.
That is, they start acting as if they owned those seats, when the seats were clearly
reserved for someone else. If Windows is able to detect that this is happening, it
generates a General Protection Fault (Windows 3.1) or an Access Violation (Windows
95 or NT), which says that some program is trying to write to memory that it does not
own, and that the operating system is going to try to protect those violated seats by
shutting down the program that is out of line.
This is not the place to do much more than hint at this subject, but the move toward
Windows NT, Windows 95, and OS/2 is fueled in large part by the desire to use
protected-mode operating systems that are good at protecting the memory that belongs
to a program. In particular, the goal is to give each program its own virtual memory
theater with 4 GB worth of seats in it. The program doesn’t really have its own
memory theater, but it has the illusion that it has its own memory theater. The theater,
of course, isn’t really 4 GB in size, it’s really probably only some 8-, 16-, or 32MB in
size, plus whatever free space is available on the hard drive. The illusion is so complete,
however, that the program thinks it has 4 GB worth of memory allocated to it, and
that all of it is in RAM. Your program doesn’t know or care that it might be reference
memory that has been paged to disk. All the addresses that it can access belong to that
one program and that one program alone. Therefore (at least in theory), it can’t access
memory that belongs to another program.
If you call New to allocate memory, the operating system literally sets up a table where it keeps
track of your request, and it will not give the memory you have reserved to anyone else until
you come back and say, “Okay, I’m through with this memory. You can deallocate it.” (The
table in question is called a descriptor table, and it can be thought of as an internal database or
spreadsheet used to keep track of pointers.)
If a programmer comes to the operating system and requests memory that is not available, the
request fails and the user usually sees a sign that says “Out of Memory,” or more likely, “Out
of Resources.” In these cases, the operating system absolutely will not give up any reserved seats
no matter how important the need. The issue, of course, is that information vital to the user or
to the integrity of the operating system might be stored in any particular allocation. As a result,
it is not possible to give it up until the module that allocated the memory gives the sign okay-
ing the deallocation.
NOTE
Memory allocation and maintenance inside Windows can become quite complex. For
instance, as mentioned previously, it is possible for Windows to swap memory to disk.
In terms of the memory theater, this is a bit like telling a group of people in one set of
seats that they will have to step out into the lobby for a time, so that other people can
come in and participate for a bit. The issue is that there are 4 GB of potential seats, but
the theater itself might hold only 8 MB or 16 MB, or however much RAM you have in
your machine. When the first set of people is needed again, they can be swapped back
into the memory theater, and the second set of people is sent out into the lobby—that
is, they are swapped to disk. These kinds of trades can and do continue indefinitely
until the seats in question are deallocated. Furthermore, it is possible for the program-
mer to specify that certain seats can’t be swapped in and out of memory in this
manner, although this capability is greatly limited in Win32 when compared to
Windows 3.1.
However, this is not the time to get into all these esoteric issues. For now it is simplest
to imagine the audience in the memory theater as being entirely static. There will be
time enough later to get into all the tricks people have created to expand the capabili-
ties of the memory theater. The facts of the matter, however, are very simple: There are
x number of seats in the memory theater. You can reserve these seats by allocating
memory. If you reserve many seats and do not ever deallocate them, you will run out of
seats and produce an error condition.
Listing 4.1. The SimpMem program shows how to perform simple memory
allocations and deallocations.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
When creating this program, I set the color of the form to white, turned off the button’s Par-
ent Font property, and set the font of the form to Arial, 24-point, bold-italic. The result is
shown in Figure 4.2.
FIGURE 4.2.
The main form for the
SimpMem project.
I spend several paragraphs discussing this program, so if it doesn’t make complete sense to you yet,
don’t worry. I want to start by describing the program once at a fairly high speed, go through the
whole program again very slowly, and finally come back and summarize what has been said.
The program starts by declaring a new type, which is a pointer to a ShortString:
type
PShortString = ^ShortString;
Notice that I am not declaring a pointer to an AnsiString, since an AnsiString is really a pointer.
You can work with pointers to pointers if you want to, but this isn’t the place to start an exami-
nation of this topic. So I am working with pointers to a ShortString—that is, a pointer to an
array. I am not working with pointers to AnsiString. Note also that there is an internal Object
Pascal type called PString, which is a pointer to an AnsiString. The native Pascal PString type
is declared in the System unit. I’m not referencing that type here, but only the type that is
explicitly declared previously as a pointer to a ShortString.
The program declares a variable of the type PShortString:
var
MyString: PShortString;
In the body of the code, memory is allocated for the variable, and then the variable is assigned
a value and is written to the screen:
New(MyString); { Allocate 256 bytes for the string }
MyString^ := ‘Hello’; { Assign a value to string }
Canvas.TextOut(10, 10, MyString^); { Write the value to the screen }
Finally, the memory that was allocated is disposed—that is, it is given back to the operating
system:
Dispose(MyString); { Deallocate the memory }
In the preceding paragraph I gave you a quick overview of the program, but many readers prob-
ably need the action to be slowed down much further so that they can get a careful look
at exactly what happened. As I said earlier, pointers are a complicated subject, so it is very
appropriate that they be covered in the clearest possible terms. I therefore slow the camera way
down and consider each frame of the film in detail.
In chapters found on the CD, you work with variables of type Integer, type Word, or type string.
You can declare these types of variables with syntax that might look something like this:
var
A: Word;
B: Integer;
C: ShortString;
If you wanted to change the third variable from a string to a pointer to a string, you would
write
var
C: ^ShortString;
In this code fragment, the caret symbol (^) identifies the variable as a pointer.
If you wanted to read the line aloud, you would say, “C is a pointer to a string,” or, if you
prefer, “C colon up-caret string.” Of the two options, I much prefer the former. The syntax
itself is merely a convention. In other words, the colon and the caret were symbols that were
picked more or less at random by the developers of Object Pascal. They don’t have any par-
ticular significance. What is significant is the actual meaning of the syntax, which is best ex-
pressed by the sentence “C is a pointer to a string.” Here are a few more examples of bits of
syntax, and a reasonable method for speaking them aloud:
A: ^Word;
“A is a pointer to a word.”
B: ^Char;
“B is a pointer to a character.”
Of course, if you would rather say “B colon up-caret Char,” there is no harm in it. I simply
happen to prefer a more literal translation of the syntax. Experienced programmers will readily
comprehend either manner of speaking.
Now it is possible to go back and take a second look at the declarations at the top of the
SimpMem program:
type
PShortString = ^ShortString;
var
MyString: PShortString;
The type declaration shown here states that a PShortString is a pointer to a string. The var
statement then declares a variable called MyString that is of type PShortString.
The classic mistake made in cases such as this would be to write the following:
type
PShortString = ^ShortString;
var
MyString: PShortString;
begin
MyString := ‘Hello’;
Caption := MyString;
end;
The are two problems with this code. The first is that no allocation is made for the MyString
variable, and the second is that the variable is not dereferenced when it is used.
Dereferencing a variable is a fancy way of talking about using the caret syntax. This is a
dereferenced variable:
MyString^
Often, the compiler will catch you up if you forget to dereference a variable. You will get a
compiler error telling you that you have a type mismatch or some other problem. The way to
fix the problem is to deference the variable involved.
Consider this statement:
MyString := ‘Hello’;
Given the declarations shown here, you know that MyString is a pointer, and the string literal
Hello is a string. You can’t assign a string to a pointer. It is a type mismatch. You can, however,
write the following:
MyString^ := ‘Hello’;
This statement says that the memory that MyString points to is going to be assigned a certain
value. Because MyString points to a string, it’s okay to assign a string to it.
NOTE
Many programmers like to append Ptr to pointer variables so that they can easily be
recognized. For instance, these programmers might write
MyStringPtr: PShortString;
Other programmers prefer to prepend letters designating the type of variable being
declared and the fact that it is a pointer:
lpstrMyString: PShortString;
This syntax states that MyString is a long pointer—that is, a 32-bit pointer—to a
string. I usually don’t use syntax of this type, primarily because I think it makes my
code harder to read. However, there are very good arguments on the other side of this
issue, and you should take the path that you find most comfortable. (You will, how-
ever, find that I sometimes distinguish one type of component from another type by
using syntax closely akin to this Hungarian notation. Like certain politicians, I tend to
waffle a bit on this issue!)
As I said previously, the compiler will usually warn you if you forget to dereference a variable.
There are exceptions, such as when you are using the Move command, or when you are using
the BlockRead or FillChar commands. These functions enable you to use a pointer without
dereferencing it, which can lead you down the primrose path to some fairly serious crashes.
The point here is not that BlockRead, FillChar, and Move are poorly designed functions, but
merely that they are flexible enough to enable you to make some fairly serious mistakes. As a
result you should always use these powerful functions with care.
var
MyString: PShortString;
begin
MyString^ := ‘Hello’;
WriteLn(MyString^);
end.
In fact, there are even occasions when you will be able to run code like this without ever receiv-
ing an error message. However, the fact that the preceding code does not allocate any memory
for MyString is a very serious bug, and it will eventually catch up with a programmer and cause
real trouble, if it doesn’t surface right away by causing the program to produce an access viola-
tion. Access violations, shown in Figure 4.3, are caused when you try to access memory that
does not belong to your program or that you do not have rights to access.
FIGURE 4.3.
Access violations cause
Delphi to act in a
disgruntled and irritable
manner.
I want to emphasize, however, that you can sometimes use a pointer for which you have not
allocated memory without causing an Access Violation. This is called blind luck, and it would
happen if the address of the variable you are referencing happens to point to valid memory.
Note that before you allocate memory for a pointer, it will contain a random address, and it’s
possible that this address might point at valid memory. However, a mistake of this kind is very
serious, and it will almost certainly catch up with you sooner or later. Furthermore, unless more
powerful debugging tools appear in the future, there is no way to ensure that you never make
this mistake. There currently are no tools that test to see if this is the case. The only way to
check for these errors is to comb over your code carefully looking for slip-ups!
The short program shown in Listing 4.2 will help you get a feeling for what happens when you
forget to allocate memory for a pointer.
{$R+,S+}
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
PAnsiString = ^string;
TForm1 = class(TForm)
bMayRaiseException: TButton;
bWillRaiseException: TButton;
procedure bMayRaiseExceptionClick(Sender: TObject);
procedure bWillRaiseExceptionClick(Sender: TObject);
private
NilString: PAnsiString;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
When you run this program, it will probably produce an access violation. However, you might
be able to run it without error because MyString might accidentally reference valid memory. If
this happens, you could try adding more units to the uses clause, or new variables in the var
section. These might alter the program’s image so that the MyString variable will point at in-
valid memory. Most of the time, however, this program will automatically generate an access
violation. (Though it has similar results, this is not exactly the same thing as using the Access
database program. Access violations, Access databases—related concepts, different implemen-
tations!)
NOTE
As mentioned earlier, one of the insidious things about pointer errors is that the line of
code that causes them will often compile and run just fine. On occasion, a detectable
problem will not surface until much later in your program. As a result, it appears that
the error occurs in a portion of your code that is actually error-free. Tracking down
pointer errors can be a task strewn with red herrings.
In the 16-bit version, you could set HeapLimit equal to zero in the first line of your
program, which would effectively turned off the suballocator. You would then have a
better chance of finding the error in the location it occurred. However, this is not an
option in the 32-bit version. In fact, there are no panaceas when it comes to pointer
problems. Many of them are, by definition, insidious and hard to track down. One of
the ways that good programmers earn their reputation is by finding pointer errors in
record time.
In the future, there might be a Code Guard or Bounds Checker for Delphi. If this type
of product appears, this would be a big boon for programmers. However, in its
absence, you can do little more than concentrate hard on writing good code, and use
the Microsoft debug kernel if it’s available.
Another thing you should notice about the Buggy1 program is that it includes the following
compiler directives:
{$R+,S+}
This code turns on stack-checking and range-checking, both of which can help you detect pointer
errors at the places they occur.
The key point about the BUGGY1 program is that it does not include any code to allocate
memory for a pointer. It might seem as if I am spending an awfully long time on this point,
but you will find that even the most experienced programmers frequently forget to allocate
memory for pointers. It is a simple mistake to fix once you have found the error, but tracking
down pointer errors is not always easy.
Here is a second version of the body of the BUGGY1 program:
begin
New(MyString);
MyString^ := ‘Hello’;
WriteLn(MyString^);
end.
This version of the program will compile and run just fine. However, it also contains a very
common, and very serious, error.
Many alert readers are probably aware that this code is missing a statement to deallocate the
memory assigned to MyString. When you are thinking about this kind of thing, it is simple
enough to see what is wrong. The problem, however, is that in a program containing tens of
thousands of lines of code, it is easy to forget to deallocate memory in one place or another.
Once you make this mistake, it can be very difficult to track it down and eradicate it. The issue
being, of course, that it will not produce an immediate error condition in your program.
Here is how the body of BUGGY1 should look:
begin
New(MyString);
MyString^ := ‘Hello’;
WriteLn(MyString^);
Dispose(MyString);
end.
If you accidentally left off the last line and failed to deallocate memory, the program would
still run fine. However, omitting the last line of this program is the type of error that can slowly
drain the system of all its available memory.
NOTE
The very important Drain program, found only on the Unleashed CD, shows what
happens when you allocate memory and don’t free it. It includes working with the
THeapStatus record from the Windows API:
THeapStatus = record
TotalAddrSpace: Cardinal;s
TotalUncommitted: Cardinal;
TotalCommitted: Cardinal;
TotalAllocated: Cardinal;
TotalFree: Cardinal;
FreeSmall: Cardinal;
FreeBig: Cardinal;
Unused: Cardinal;
Overhead: Cardinal;
HeapErrorCode: Cardinal;
end;
In particular, suppose I have written a small utility that runs constantly on my system. Sup-
pose further that inside that utility there happens to be an error like the one shown earlier. The
first time the utility runs, I lose about 256 bytes, which on most systems happens to be no
major problem. However, if that keeps executing in a loop, after a few hours or days, my sys-
tem will crash with an “Out of Memory” or “Out of Resources” error.
The error, of course, could occur while I am running some other program, which means that
it would be very difficult to figure out where the error was coming from. Even if I did track the
error back to its home program, it might still be difficult for me to sift through a few thousand
lines of code looking for the one place where I forgot to call Dispose.
Once again, I am belaboring this point because I want to make it absolutely clear that you have
to remember to call New and Dispose nearly every time you use a pointer. Furthermore, it is
very natural for the soggy little computer residing between our ears to occasionally forget to
call New or Dispose, even if it thoroughly understands the importance of the calls. As a result,
there are times when you have to track this error down at great expense in terms of time and
sanity.
NOTE
The GetMem and FreeMem routines are closely related to New and Dispose. In fact, in their
implementation, New and Dispose call GetMem and FreeMem. The difference between the
two sets of routines is that one set automatically calculates the size of the variables for
which you want to allocate memory, whereas the other asks you to specifically state
how much memory you need:
var
S, S1: ^ShortString;
begin
New(S);
GetMem(S1, 256);
Dispose(S);
FreeMem(S1, 256);
end;
In the code shown here, the calls to GetMem and the calls to New perform exactly the
same function; that is, they each allocate 256 bytes on the heap.
In Chapter 7, “Multitasking and Threads,” you will learn about ways to allocate memory by
calling into the operating system. In particular, you will learn about calling VirtualAlloc and
HeapAlloc, and about memory-mapped files.
I should also add that all programs running under Windows 3.1 and most 16-bit programs
running under Windows 95 will end up sharing the same address space. This means that if one
program is corrupting memory, it can affect all the other programs in its address space. Any
corruption or any unallocated memory associated with that program will live on, even after the
program closes. That is, it will live on until the Windows 3.1 session is shut down or until all
the other 16-bit programs run under Windows 95 are shut down. 32-bit programs, on the other
hand, do not operate in this fashion. After you close a 32-bit program under Windows 95 or
Windows NT, all the memory associated with that program will be disposed by the operating
system. This is not, however, a license to forget about deallocating memory. Any time you don’t
dispose a pointer, you are courting trouble. If you know what you are doing, you can get away
with it in a 32-bit program, but it is a very bad habit to develop.
In this section you have learned some of the basic facts about pointers. The next section goes
on to show you one very practical use for the new tool you have been studying.
However, it is vital to understand that Edit1 is really a pointer to an object. Under normal
circumstances, you would have to write
Edit1^.Text := ‘Sam’;
You don’t have to do this because the developers of Delphi have intentionally tried to conceal
the fact that Edit1 is a pointer. They don’t want to burden you with that difficulty, so they
never force you to dereference an object. Furthermore, it is not possible to create an instance of
an object that is not a pointer. All objects are pointers in Delphi.
I stressed earlier in this chapter that it is very important to allocate and deallocate memory for
pointers. This is usually not a problem when you are dealing with objects because the system
will allocate and deallocate the memory for you. In particular, memory gets allocated for an
object when you pull it off the Component Palette and drop it on a form. When a program
ends, its forms are automatically disposed. Furthermore, each form knows how to dispose the
objects that are placed on it. This subject will be discussed in depth in the chapters on compo-
nents and objects.
There are times, however, when you do need to explicitly create and destroy objects. For in-
stance, if you are using a TStringList variable, you must allocate memory for it and deallocate
that memory when you are done:
var
L: TStringList;
begin
L := TStringList.Create;
// Do something with the string list.
L.Free;
end;
In the code shown here, notice that there is a special syntax for allocating memory for objects.
In particular, you don’t call New or GetMem. It would normally be an error, or lead to an error,
if you tried to call New or GetMem on an object. Instead, you call the Create method of the ob-
ject. Furthermore, you don’t dispose an object by calling Dispose or FreeMem. Instead, you call
the object’s Free method.
That’s all I’m going to say about objects in this chapter. The subject is covered in depth in Part
5, “Objects.”
This code in effect zeros out the pointer value itself. It sets the pointer to zero. This does not
mean that it sets the memory associated with the pointer to zero, it means that the pointer
itself is set to zero. It does not affect what the pointer is pointing at, since all nil pointers by
definition are not pointing at anything.
By definition, nil pointers have no memory associated with them. That is, they have not been
allocated. Further, you can test to see if a pointer is set to nil:
var
P: Pointer;
begin
...
if P = nil then
GetMem(P, 1000)
else
Move(P^, MyVariable, 1000);
...
if P <> nil then Dispose(P);
end;
This code illustrates the main reason for adding the value nil to the Object Pascal language.
Specifically, it allows you to explicitly set a pointer to nil and then test to see if it has that
value. If it is set to nil, it cannot have any memory associated with it. In other words, you can
check to see if a pointer has been allocated by seeing whether it is equal to nil. If it is equal to
nil, it has no memory associated with it. If it is not equal to nil, it might have a valid address
in it or it might have garbage characters that just look like an address.
If you declare a pointer as a local variable in a function and do not explicitly set it to nil, it will
probably contain a randomly chosen address. That is, it will not necessarily automatically be
set to nil, and might appear to be a valid pointer. However, the address in the variable is prob-
ably random garbage unless you have explicitly declared memory for it. Pointer values that you
declare inside an object declaration will automatically be zeroed out—that is, be set to nil.
The compiler will not stop you from setting a valid pointer to nil. This will not cause an error
in your program, but it will strand the memory associated with the pointer. That is, you will
not be able to access that memory, and it will not be possible to easily dispose it.
NOTE
The developers of Delphi go out of their way to ensure that you can write complex, full-
featured programs without ever having to use a PChar. That is, they intentionally make sure
that you can use Delphi strings to access most of the basic functionality of a computer, includ-
ing the Windows API functions. The reasoning behind this decision is simply that strings are
much easier to use than PChars. However, whenever you decide to step outside the native Delphi
calls and start accessing the Windows API directly, you might decide to use PChars, although
this is no longer necessary.
NOTE
The PChar type is a special kind of pointer. You need to allocate memory for it, but
you usually do not need to dereference it. This can cause some confusion at first, but
the key thing to remember about PChars is that they are pointers, so you have to be
sure that memory is allocated for them and that the memory associated with them is
later freed.
There are two ways to create a PChar. The first is simply to declare a variable as a PChar:
var
S: PChar;
The most fundamental difference between the two declarations shown here is that the second
has memory allocated for it and the first does not. In other words, the character array shown
here has 101 bytes allocated for it, whereas the PChar has no bytes allocated for it. Otherwise,
the two declarations are nearly identical. This identity comes about because the compiler has a
special set of rules for handling PChars and zero-based arrays of Char. In other words, the ar-
ray of Char shown earlier has special properties associated with it, just as a standard Delphi
string is really just an array of Char with special properties.
In Chapter 3, “Strings and Text Files,” you were introduced to some of the basic facts about
PChars. In that chapter, you learned that both strings and PChars are really just arrays of Char.
ShortStrings, however, track their length in the first byte of their data structure, AnsiStrings
track them at an undocumented negative offset, and PChars place a NULL character (#0) at
their end to signal their length. Because Delphi ShortStrings use a length byte, they are limited
by definition to only 255 characters. PChars, on the other hand, have no inherent limitations
under the 32-bit version of Delphi. AnsiStrings also have no inherent limits, and they also have
a null character (#0) at their end. They differ from PChars in that you usually don’t have to
explicitly allocate memory for them; and when you allocate memory for them, you do so with
the SetLength function. Furthermore, AnsiStrings have undocumented data stored at negative
offsets.
When working with strings, you can use the Length function to determine their size. With
PChars, however, you must use the StrLen function, which is stored in the SysUtils unit:
uses
SysUtils;
The code first calls StrCopy to assign a string to the PChar called MyString. It then calls StrLen
to find out the length of the string. StrLen returns a Word, so that value must be translated
into a string before it can be shown in an edit control.
Here is a list of the commonly used string functions found in the Strings unit:
StrAlloc Allocates a buffer for a null-terminated string with a maximum length
of Size –1
StrBufSize Returns the maximum number of characters that may be stored in a
string buffer allocated by StrAlloc
StrCat Appends a copy of one string to the end of another and returns the
concatenated string
StrComp Compares two strings
StrCopy Copies one string to another
StrDispose Disposes a string on a heap
StrECopy Copies one string to another and returns a pointer to the end of the
resulting string
StrEnd Returns a pointer to the end of a string
StrFmt Formats a series of arguments
StrLFmt Formats a series of arguments; the result contains a pointer to the
destination buffer
StrLCat Appends characters from one string to the end of another and returns
the concatenated string
StrIComp Compares two strings without case sensitivity
StrLComp Compares two strings, up to a maximum length
StrLCopy Copies characters from one string to another
StrLen Returns the number of characters in Str
StrLIComp Compares two strings, up to a maximum length, without case
sensitivity
begin
StrCat(S1, ‘Hello’);
StrCat(S2, ‘, gentle reader’);
StrCat(S1, S2);
end;
The code shown here might not perform as expected, because there may be random characters
in S1. As a result, the word Hello will be appended onto the end of these random characters
rather than being copied into the first six letters of string S1. The correct way to handle the
situation shown earlier is this:
var
S1, S2: array[0..100] of Char;
begin
StrCopy(S1, ‘Hello’);
StrCopy(S2, ‘, gentle reader’);
StrCat(S1, S2);
Edit1.Text := S1;
end;
This code always ends up with the string Hello, gentle reader in string S1. The first example
might end up with this string in S1, but it is not guaranteed.
The StrNew and StrDispose functions are very useful ways of allocating memory for a string:
var
S1: PChar;
begin
S1 := StrNew(‘Hello’);
Edit1.Text := S1 + ‘ Length: ‘ + IntToStr(StrLen(S1));
StrDispose(S1);
end;
When using StrNew, you never have to specify exactly how many bytes you want to allocate for
string S1. Instead, the compiler makes the calculation for you. This technique can be very use-
ful when you want to make sure you are not wasting memory by preallocating arbitrarily large
blocks of memory at design time. The StrDispose function provides a simple way to dispose of
memory that was allocated with StrNew. In fact, you must use StrDispose whenever you use
StrNew. Notice that you don’t have to tell the compiler how many bytes to deallocate. You
should also look at the StrAlloc function, which is clearly explained in the online help.
If you are working in Delphi 16, the code shown previously would need to use the StrPas func-
tion, which converts a null-terminated string into a Delphi string:
Edit1.Text := StrPas(S1) + ‘ Length: ‘ + IntToStr(StrLen(S1));
To use StrPas, simply pass it the PChar you want to convert, and the string equivalent will be
returned by the function. Nothing could be simpler.
The opposite of the StrPas function is StrPCopy, which converts a Delphi string into a null-
terminated string. In both Delphi 16 and Delphi 32, you must use StrPCopy when working
with arrays of Char:
var
S1: String;
S2: array[0..100] of Char;
begin
S1 := ‘Confront the difficult while it is still easy’;
StrPCopy(S2, S1);
Edit1.Text := S2;
end;
In Delphi 2.0, however, you can, with the aid of typecast, simply assign a PChar to a string, as
long as Huge Strings is turned on in the Compiler Options page:
procedure TForm1.Button3Click(Sender: TObject);
var
S1: string;
S2: PChar;
begin
S1 := ‘Confront the difficult while it is still easy’;
S2 := PChar(S1);
Edit1.Text := S2;
end;
Notice that the code shown here does not need to allocate memory for S2 because it is being
assigned to the same pointer as s1. This chore is handled for you automatically by the
compiler. Please understand, however, that some strange things are happening when you as-
sign S1 to S2, that is, when you write S2 := PChar(S1). If you are an experienced assembly
language programmer, you should use the standalone debugger to see if you can suss out ex-
actly how the shipping version of Delphi handles these situations.
NOTE
There are subtle differences between the ways that the compiler handles PChars and
arrays of Char. By definition, they should be identical as long as extended syntax is
turned on {$X+}, which is the default setting. This is not always the case, however, as
shown above.
All of the code you have seen in the last few pages can be found on the CD in a program called
PCHAR1. I don’t bother to reproduce that program here, because it is nothing more than a
series of buttons, each one of which is associated with one of the examples shown previously.
The form for the PCHAR1 program is shown in Figure 4.4.
FIGURE 4.4.
The PCHAR1 program
demonstrates some of the
basic traits of null-
terminated strings.
Before closing this chapter, there is one final point I want to make about PChars. If you create
a procedure that returns a PChar, you should always make the routine that calls the procedure
allocate the memory for the PChar. For instance, suppose you wanted to return a string from
a procedure that supplies the current date. The following implementation would be incorrect:
procedure GetDate(Date: PChar);
begin
GetMem(Date, 100);
StrCopy(Date ’11/01/94');
end;
The problem with this code fragment is that it allocates memory inside a function and then
returns the string to a place that lies outside the scope of the current function. As a result, there
is no guaranteed way to ensure clean up of the memory. The correct way to handle a situation
like this is to insist that the calling routine allocate memory:
procedure GetDate(Date: PChar);
begin
StrCopy(Date, ’11/01/94');
end;
A simple way to call this routine would be to write a procedure that looks like this:
procedure CallDate;
var
S: array[0..100] of char;
begin
GetDate(S);
Edit1.Text := S;
end;
or like this:
procedure CallDate;
var
S: PChar;
begin
S := StrAlloc(100);
GetDate(S);
Edit1.Text := S;
StrDispose(S);
end;
In this case, the CallDate procedure handles memory allocation for the string that will hold a
date. This is always the correct way to handle this type of situation. The golden rule is that a
procedure that returns a modified PChar should never be responsible for allocating its memory.
The calling procedure should always handle this task.
There is a program on the CD called PCHARFUN that illustrates the ideas discussed in the
preceding paragraphs. The key point to remember is that any function that returns a PChar
should have the calling function perform any necessary memory allocation. It is always a mis-
take for the called function to return a PChar that it has allocated. If you make this mistake,
one of two things will happen:
■ The returned value will no longer be valid because it was a pointer that was allocated
on the function’s stack.
■ The returned value will probably never be properly deallocated because there is no one
who owns it and who therefore has responsibility for its deallocation.
Summary
In this chapter you have learned the basics about pointers and PChars. Pointers are one of the
most elemental types supported by a computer. Delphi enables you to write a good deal of
code without ever forcing you to think about pointers.
This chapter also explored PChars. PChars can be very confusing to newcomers, in part be-
cause they are a special kind of pointer that rarely needs to be dereferenced. As a result, it is
easy to make a mistake when using this type.
The next chapter takes a more in-depth look at pointers. In particular, you will get a look at
linked lists and the way addresses look when you explore them with the debugger. You will
also see how to perform arithmetic on pointers.
In the last chapter you learned the basic facts about pointers. In this chapter you get a chance
to explore the subject in more depth. In particular, you will look at
■ Pointer arithmetic
■ Linked lists
■ Using the stand-alone debugger to explore pointers.
As in the last chapter, much of this material does not carry the burden of extreme importance
that it had in the 16-bit version of Delphi. However, this is still significant material, and it is
worthy of careful perusal by serious students of Delphi. In particular, it is now possible to be a
reasonably respectable, intermediate level programmer without having a good understanding
of pointers. It’s not recommended, and it would be considered idiosyncratic by most profes-
sionals, but it is now possible to write useful professional level code that makes no explicit use
of pointers. However, if you want to be an expert, you have to understand pointers on the deepest
level of your consciousness. Nobody could lay claim to mastery of any field of Delphi pro-
gramming without a thorough, and instinctive, understanding of pointers. In short, you can
use Delphi without understanding pointers, but you will never be a really good Delphi pro-
grammer until you have the subject under your belt.
Listing 5.1. The PMath program shows how you can do some simple pointer
math using PChars.
unit Unit1;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
This program lets you perform simple arithmetic on a PChar in order to iterate through its
characters. After setting B equal to A with an assignment statement (B := A), the string B is
pointing at a sentence that reads: “This-long-sentence”. The next line:
Inc(B, 2);
moves the pointer value along two characters so that the sentence now reads: ‘is-long-sentence’.
You could have done the same thing by writing code like
B := B + 2;
It’s important to understand that this is an unusual thing to do with pointers. It’s a feature
supported by C, so some folks tend to see it as a natural action; but it’s quite peculiar and will
work in Delphi only in particular circumstances, such as when you are working with PChars.
Some programmers tend to avoid this kind of syntax since it is a bit counter-intuitive and can
lead to confusing and hard-to-read code. When you increment the value of a pointer, you are
moving the place in memory that a pointer points to up by a certain value. For instance, the
pointer might initially have pointed at seat 500 in the memory theater. If you increment it by
two, it will point at seat 502. This is a simple concept, but the syntax that accomplishes these
ends is not necessarily transparent to a reader of the code.
Linked Lists
It’s time now to look at linked lists, which can be used to store lists of any type of item in a
relatively small area of memory. Linked lists are known for their power and flexibility.
Conceptually, linked lists are fairly simple data structures. The basic idea behind them is that
they are used for stringing a series of records together out on the heap. The main advantages of
the system are twofold:
■ Linked lists are a very quick and convenient way to store many records in a single data
structure.
■ Linked lists are very flexible data structures that enable you to allocate memory on the
fly. If you don’t know how many records of a particular type you need, linked lists can
easily grow and shrink to meet your current demands.
NOTE
Delphi has a number of built-in objects such as TStrings, TStringList, and TList, all
of which are really just object-oriented variations of linked lists. Obviously, it usually
makes more sense to use these built-in structures than to create tools of your own.
Customized linked lists of various kinds, however, are so useful, and have played such a
crucial role in computing over the years, that I can’t really imagine finishing this book
without covering them in at least some detail. If you are a good programmer, you can
use linked lists to provide small, flexible solutions to a large number of problems.
Listing 5.2 shows the code for the LinkLst1 program. It demonstrates the core facts about linked
lists. This is a console application. To make it run, go to the Project | Options | Linker menu
option and check the Generate Console Application option. I have chosen to make this a con-
sole application not because this code couldn’t be incorporated into a standard VCL project,
but simply because I wanted it to be as simple as possible.
Listing 5.2. Basic details about allocating and deallocating memory for linked
lists are shown in the LINKLST1 program.
program LinkLst1;
uses
SysUtils; // Added to suppress IDE errors
type
PMyRecord = ^TMyRecord;
TMyRecord = Record
Name: String;
Next: PMyRecord;
end;
var
Total: Integer;
continues
var
First,
Current: PMyRecord;
begin
Total := 1;
DoFirst(First, Current);
repeat
Inc(Total);
Add(Current);
until Total > 20;
Show(First);
FreeAll(First);
end.
LinkLst1 displays a simple list of strings that look something like this:
Sam1
Sam2
Sam3
Sam4
etc...
The list is comprised of strings that were parts of a very simple record:
PMyRecord = ^TMyRecord;
TMyRecord = Record
Name: String;
Next: PMyRecord;
end;
This record has two fields. The first is a string, and the second is a pointer to a record of type
TMyRecord. I explain this latter field in a moment.
In this program, the Name field of the records used in the program gets set to values such as
Sam1, Sam2, Sam3, and so forth. The code that creates these strings is very simple and should be
easy for most students of Object Pascal to read:
All that is happening here is that a variable called Total is being incremented inside a loop, and
each time it is incremented, the function GetData is called. GetData sets the Name field of the
record to the word Sam plus the value of the variable Total after it has been translated into a
string.
The Name field of TMyRecord is easy enough to understand. The tricky part is the second field,
the one called Next. The Next field is declared to be of type PMyRecord. In other words, it is a
pointer to a record of the same type as the record to which it belongs.
If you are new to linked lists, you might mistakenly be tempted to think of them as a series of
Chinese boxes, each of which contains another box of its same type, only slightly smaller. A
better image for a linked list might be a series of helium-filled balloons each attached to the
next by a string, with the very first member tied down to a stake fixed firmly in the ground.
Picture a metal stake with a ring on the top of it fixed into an area of ground where your ex-
ecutable resides in memory. This stake has a string tied to it, and on the end of the string is a
balloon. At the very bottom of the balloon, right next to the place where the string is attached,
there is a second string, which in turn leads off to a second balloon. Tied to the bottom of this
balloon is a third string, which leads to a third balloon, which in turn leads to a fourth string,
and so on, for ten, twenty, even a thousand or more balloons.
At the very bottom of each linked list is a pointer, which is fixed firmly in the solid earth of
your program’s EXE image. This pointer points off like a long string that leads to a record that
is floating out on the heap. Inside this record another pointer exists, which in turn leads off
like a long string to the next member in the list. This process continues until you have strung
together all the records you want to work with at any one time. The only limits you have to
wrestle with are those imposed by the operating system or the amount of available virtual
memory. The key image I want you to grasp here is that the pointer field, which in this case is
called Next, acts exactly like a string that leads you to the next record in the list of records that
comprise what is called a linked list. The pointers link the list together the same way the strings
link the balloons together.
Suppose you had 20 of these records linked together. If you wanted to find the tenth record,
you could start at the pointer in your executable’s data, feel your way along to the first record,
find the pointer field in that record, feel your way along to the second record, and so on, until
you had iterated through ten records. Then you would be standing smack dab in the middle of
the record you wanted to find.
Furthermore, if you had a list of this type, you could count the total members in the list by
starting at the first record and proceeding until you came to a point where there was no string
to lead you to another record. The question is how can you tell that you are out of records?
How do you know that the next pointer you look at is the end of the road and doesn’t point at
another record?
The answer to this question is fairly simple. Every pointer can be set to one of three states. It
can contain a valid address, a nonsense address, or an address that is set to zero. It is very dif-
ficult to tell the difference between a valid address and a nonsense address, because they both
simply contain a set of numbers. However, pointers that are set to zero are clearly defined as
invalid pointers. Therefore, you can mark the end of a linked list by setting the Next field of the
record to zero, which is called setting a pointer to nil. You can set a pointer to nil by writing
the following:
MyRecord^.Next := nil;
CAUTION
I stated earlier that it is hard to tell from just looking at it whether a pointer contains a
valid address or just some randomly chosen set of numbers that happened to be in a
particular section of memory. The inability to make this distinction is a very serious
problem.
In particular, if a program is feeling its way along the length of a linked list and it
suddenly stumbles across a Next pointer that points off at a random place in memory,
the computer will try to feel its way along that string to the imaginary or nonsensical
location. Because there is no valid memory at this new location, the computer becomes
effectively lost the moment it arrives at the end of its string. Once a computer gets lost,
it is very difficult for it ever to get back on track again. Jumping to an invalid address is
an excellent way to crash a computer, or at least to cause a process running under a
multitasking system to be frozen and ultimately terminated by the system.
As a result of the problem outlined here, it is absolutely essential that you construct all
linked lists entirely out of valid addresses, and that you mark the end of each linked list
with the value nil. Assigning nil to the last Next field in a linked list clearly marks that
node as being the end of the road.
This procedure starts by allocating memory for the node with the New procedure. The code
then sets the Next pointer to nil and sets the Name field to an empty string. After this procedure
is through, the node passed to it is fully initialized.
As stated earlier, the very first node in a linked list is attached to the data in your executable. As
a result, it is an exception and needs to be treated differently than all the other nodes in a list.
Here is one way to create the first node in a linked list:
procedure DoFirst(var First, Current: PMyRecord);
begin
CreateNew(Current);
GetData(Current);
First := Current;
end;
DoFirst calls CreateNew, and then GetData, and sets the variable First equal to Current. You
have already heard about both CreateNew and GetData, but it will still take a moment to ex-
plain the last line in the DoFirst procedure.
In addition to the Total variable, the LINKLST1 program lists the following global variables:
var
First,
Current: PMyRecord;
These two variables are used to keep track of the members of the linked list. The First variable
always points to the member of the list that is staked into the data or stack segment. The
Current variable always points to the currently selected member of the linked list, which in this
particular program will always be the last member of the linked list. In more complex linked
list programs, a variable like Current might address a node that is in the middle of the list.
The DoFirst procedure first creates a valid node, fills it with valid data, and finally sets First
equal to this node. In other words, the following line of code
First := Current;
is equivalent to the act of driving the stake attached to the first balloon into the ground. After
this, the linked list is attached to the data in your executable, by means of the variable called
First.
After getting the list started, all other nodes are added onto the list by means of the Add proce-
dure:
procedure Add(var Current: PMyRecord);
var
Prev: PMyRecord;
begin
Prev := Current;
CreateNew(Current);
GetData(Current);
Prev^.Next := Current;
end;
Add gets passed a copy of the currently selected member of the linked list. Remember that in
this linked list, Current could just as easily have been called Last.
NOTE
It’s important to understand that in a different type of linked list, the Current item
might not necessarily be the Last item. In fact, there are many different kinds of linked
lists, and you need to be aware that some of the comments I make in this chapter refer
to this particular implementation of a very simple form of linked list, and not necessar-
ily to all linked lists.
Other types of linked lists include circular linked lists and double linked lists. In
circular linked lists the last item points back to the first item, rather than being marked
as nil. In double linked lists, each item contains a pointer to both the next item in the
list, and the previous item in the list. There is also an entire series of structures called
binary trees that can be built out of the concepts used in constructing the simple linked
list found in this chapter. However, thoroughly describing all these types of data
structures would take up hundreds of pages of text, and there is no room for that kind
of undertaking within the limits of the current book.
Remember, the main point of this chapter is not so much to teach you about a specific
type of linked list, as it is to make sure that you understand pointers. Of course, linked
lists also happen to be a very important type of data structure, and most programmers
use them in a wide variety of circumstances. In fact, no programmer could ever lay
claim to being an expert without having a solid understanding of linked lists.
The Add procedure declares a second pointer to PMyRecord, called Prev. Prev is set equal to Cur-
rent, and then CreateNew is called. In other words, Prev remembers what Current was pointing
at, then Current is used to hold a newly created node, which is then immediately filled with
data by the GetData procedure. Finally, the new Current variable is tied on to the end of the
linked list with the following piece of code:
Prev^.Next := Current;
There are many, many ways to implement a linked list. The following routine represents a second
take on the Add procedure:
procedure Add2(var Current: PMyRecord);
var
Temp: PMyRecord;
begin
CreateNew(Temp);
GetData(Temp);
Current^.Next := Temp;
Current := Temp;
end;
In this example, a variable called Temp is used when a new node is created. The Next field of
Current is set equal to Temp, and finally the Current variable is set equal to the newly created
node. Both examples take the same amount of time to execute, and both accomplish the same
goal. However, looking at a second version of the Add procedure will help you understand ex-
actly how linked lists work and exactly what needs to be accomplished to add a new node to
the end of a list.
The main body of the LINKLST1 program uses the following code to create 21 nodes:
repeat
Inc(Total);
Add(Current);
until Total > 20;
NOTE
The main body of a Delphi program is always encapsulated in a begin..end pair that is
terminated with a period. Most Delphi programs use this main block simply to
initialize one or more forms and then call Application.Run. The LINKLST1 program
uses this main block far more extensively than VCL programs, but there is no differ-
ence in the way the compiler handles this section of the program. In other words, it
simply executes the code between the begin..end pair, regardless of whether that code
reads
Application.Run,
or whether it reads
CreateNew(Current, First);
After the list has been created, the following procedure shows it to the user:
procedure Show(Head: PMyRecord);
begin
while Head^.Next <> nil do begin
WriteLn(Head^.Name);
Head := Head^.Next;
end;
WriteLn(Head^.Name);
end;
First is passed to Show because the procedure simply displays each record in the list, starting
with the first and traveling through to the end. As you will see in a moment, it is not possible
to iterate backwards through this particular linked list. Instead, you must always iterate through
the list by starting at the beginning and moving one link at a time.
Here is how to move from the currently selected item to the next item in the list:
Head := Head^.Next;
If you pass First to the Show procedure, calling this line once will move you from the first item
in the list to the second item. Calling it a second time will move you from the second item to
the third item, and so forth.
The trick in the Show procedure is to know when you are at the end of the list. This is accom-
plished with the while statement, which enables the line shown previously to be called until
the moment when the current record has a Next field that is set equal to nil. The significance
of the nil Next pointer was described earlier:
while Head^.Next <> nil do begin
There are two other important points about the Show procedure. The first is that after the while
loop ends, one more item still must be displayed to the user, so the code does that in its last
line. Furthermore, you should note that First is passed to the Show procedure, not as a var
parameter:
procedure Show(var Head: PMyRecord);
That is, it is passed by value, not by reference. This is done because you don’t want to change
the node addressed by First.
Because this is an important point, let’s look at the same issue again from a second angle. You
want to be able to pass Show the first member of the list:
Show(First);
to iterate from the first member of the list to the second member, you don’t want the variable
called First to be affected. As a result, the correct thing to do is pass First by value, rather
than by reference. This means a copy of the pointer will be passed to the function and not the
First pointer itself.
The purpose of this procedure is to dispose of all the nodes in the linked list. In other words,
it deallocates all the memory allocated by the CreateNew procedure.
Just like the Show procedure, FreeAll is passed First, and it iterates through the linked list in
exactly the same manner as Show. However, as each node is reached, the FreeAll procedure
disposes of it. In order to do this correctly, it must first keep a copy of the next member of the
list, so that it has something to hang on to after it has deleted the current member. Then the
current member, called Head in this procedure, is set equal to what was the second member of
the list. In this way, the procedure iterates through the whole list, deleting the member behind
it as it goes. Note that it is safe to pass First to free all as a var parameter.
The 16-bit version of the LINKLST1 program used Delphi’s built-in MemAvail procedure to
list the currently available memory both at the beginning and at the end of a run. This func-
tion is not used in the 32-bit version because there is no sensible way to keep track of the exact
amount of available memory in a 32-bit system. One reason for this is that more memory can
often be created by simply swapping RAM to disk. (In fact, there was no really sensible way to
keep track of the amount of available memory under Windows 3.1 either, but the developers
still tried to make it possible.)
If you took the LinkLst1 program and removed the uses clause, or changed the uses clause so
that it referenced Crt instead of SysUtils, you could then run this program from the DOS
prompt as an old style Turbo Pascal program. To do so, you would, of course, have to compile
it with an old version of Turbo Pascal, if you have one available. You could then call MemAvail,
and you would find that the LINKLST1.EXE program created by this code will report the
same amount of memory available at startup as at the end.
NOTE
It’s important to understand that neither the 16-bit nor the 32-bit versions of Delphi
are meant to produce DOS code. Some clever programmers have been able to coax this
kind of code out of Delphi 16 and there might be ways to get console applications to
run under a 32-bit DOS DPMI extender, but this is not one of the goals set for the
compiler by the developers. There is no definitive answer to the question of whether or
not the developers will someday create a version of Turbo Pascal for DOS that sup-
ports all, or some major subset, of the Delphi syntax. Certainly it is possible that such a
tool will be created, but you should not, by any means, count on it.
I mention all this in part because it is hard to tell whether a Win32 program is leaking
memory. With a real mode program, however, you can use the MemAvail command to
get an exact reading of program memory usage. Sometimes it’s worthwhile taking a
piece of code like a linked list back into real-mode DOS, and testing it to see that it is
absolutely clean. Of course, there are tools that test this kind of thing in Windows 95
and Windows NT, but a quick real-mode compile can be one way to check your work.
A real-mode compile can be particularly appealing if you are operating on a limited
budget, or if your company is a bit slow approving funding requests!
FIGURE 5.1.
The main form for the
LINKLST2 program
enables you to browse the
list and delete records.
FIGURE 5.2.
The second form in the
LINKLST2 program is
used for entering new data
into the list.
With the capability to add, delete, browse, and search for records, the LINKLST2 program
gives you most of the functionality you want in a simple linked list. The one thing that is miss-
ing is the capability to move both backward and forward through the list. This capability is
granted by a data structure called a double-linked list. An example of a double-linked list is avail-
able on the CD in the file DBLIST.PAS. DBLIST is stored in the UNITS subdirectory on
the CD.
Listing 5.2. The LINKLST2 program gives you access to adding, deleting, and
searching for records.
unit Main;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
Menus, StdCtrls, SysUtils,
Entry;
const
FileName = ‘LinkExp.dta’;
type
TDataForm = class(TForm)
MainMenu1: TMainMenu;
File1: TMenuItem;
CreateList1: TMenuItem;
Save1: TMenuItem;
Lists1: TMenuItem;
Add1: TMenuItem;
Delete1: TMenuItem;
ReadData1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
N2: TMenuItem;
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
First: TButton;
Next: TButton;
Count1: TMenuItem;
bAdd: TButton;
Last: TButton;
Find1: TMenuItem;
Help1: TMenuItem;
procedure CreateList1Click(Sender: TObject);
procedure ReadData1Click(Sender: TObject);
procedure FirstClick(Sender: TObject);
continues
var
DataForm: TDataForm;
implementation
uses
Dialogs;
{$R *.DFM}
procedure CreateTextFile;
const
Max = 100;
begin
Assign(F, ‘data.txt’);
ReWrite(F);
for i := 1 to Max do begin
WriteLn(F, ‘Sam’ + IntToStr(i));
WriteLn(F, i);
WriteLn(F, Days[i Mod 7]);
end;
Close(F);
end;
procedure TDataForm.CreateNewBinaryFile;
var
F: File of TMyNode;
Head: PMyNode;
begin
Head := FirstNode;
System.Assign(F, FileName);
ReWrite(F);
while Head^.Next <> nil do begin
Write(F, Head^);
Head := Head^.Next;
end;
Write(F, Head^);
System.Close(F);
end;
continues
procedure TDataForm.ShowRecord;
begin
Edit1.Text := Current^.Name;
Edit2.Text := IntToStr(Current^.Flight);
Edit3.Text := Current^.Day;
end;
FreeAll(FirstNode);
CreateTextFile;
ReadTextFile(FirstNode, Current);
Current := FirstNode;
ShowRecord;
end;
procedure TDataForm.Working;
begin
Edit1.Text := ‘Please wait’;
Edit2.Text := ‘Working’;
Edit3.Text := ‘’;
Application.ProcessMessages;
end;
continues
procedure TDataForm.GoLast;
begin
while Current^.Next <> nil do
Current := Current^.Next;
end;
var
S: String;
begin
S := ‘’;
InputQuery(‘Search’, ‘Search for: ‘, S);
Current := Find(S);
ShowRecord;
end;
end.
Listing 5.3. The Entry module allows you to add records to the
LNKLIST2 program.
unit Entry;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
StdCtrls, Buttons, MakeData,
SysUtils;
type
TEntryForm = class(TForm)
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
BitBtn1: TBitBtn;
BitBtn2: TBitBtn;
private
{ Private declarations }
public
function Add(var Current: PMyNode): Boolean;
end;
var
EntryForm: TEntryForm;
implementation
{$R *.DFM}
end.
The LINKLST2 program reads and writes data from both a binary and a text file. The binary
file is the main storage for the program. The text file exists as an option for you to manually
enter data into a text file when you are starting the program. You can then automatically read
the text file into the program’s linked list. In other words, the text file is just part of a utility,
and the binary file is the main data storage. The ReadData method will call the text file routines
only if there is no binary file for it to read.
The LnkLst2 program can automatically create its own text file. The only reason I’m leaving
open the possibility of needing to read in a text file is because it is a common operation that
occasionally confuses some programmers. I therefore include an example of it here, on the
grounds that it might be helpful to some people. However, there is no reason why this pro-
gram could not have automatically generated a binary file, and skipped the text file altogether.
In short, the text file is there only as an added service, and you shouldn’t think of it as a neces-
sary part of this type of program.
The File menu item in the LINKLST2 program has the following entries:
Read Data
Create New List
Save
Exit
The capability to create a new list is granted to the CreateData function in the MakeData unit:
procedure CreateData;
var
F: Text;
i: Integer;
begin
Assign(F, ‘data.txt’);
ReWrite(F);
for i := 1 to Max do begin
WriteLn(F, ‘Sam’ + Int2Str(i));
WriteLn(F, i);
WriteLn(F, Days[i Mod 7]);
end;
Close(F);
end;
This function creates a simple text file that looks like this:
Sam1
1
Monday
Sam2
2
Tuesday
Sam3
...
The file goes through as many iterations as you specify in the Integer constant called Max. The
first time you run the program, you need to call this function.
The following rather plodding function reads the text file into a linked list:
function TDataForm.ReadTextFile(var FirstNode, Current: PMyNode): Boolean;
var
F: System.Text;
Prev: PMyNode;
i: Integer;
begin
Result := False;
System.Assign (F, ‘data.txt’);
try
Reset(F);
except
on E:EInOutError do begin
ShowMessage(E.Message + ‘: Data.Txt. Use Create New List.’);
Exit;
end;
end;
NewNode(Current);
ReadLn(F, Current^.Name);
ReadLn(F, Current^.Flight);
ReadLn(F, Current^.Day);
FirstNode := Current;
i := 0;
while not Eof(F) do begin
Prev := Current;
NewNode(Current);
ReadLn(F, Current^.Name);
ReadLn(F, Current^.Flight);
ReadLn(F, Current^.Day);
Prev^.Next := Current;
Inc(i);
end;
System.Close(F);
CreateNewBinaryFile;
Result := True;
end;
You should call one of the preceding functions via the menu each time you load the program—
except the very first time, when it should be the second function you call, after the CreateData
routine. After having read this list the first time from a text file, LINKLST2 stores its data in a
binary file of the type first seen at the end of Chapter 13, “The Windows API and DLLs.” The
CreateNewFile procedure is used to actually write the binary data to disk:
procedure TDataForm.CreateNewFile;
var
F: File of TMyNode;
Head: PMyNode;
begin
Head := FirstNode;
Assign(F, FileName);
ReWrite(F);
while Head^.Next <> nil do begin
Write(F, Head^);
Head := Head^.Next;
end;
Write(F, Head^);
System.Close(F);
end;
You can call this function from the Save menu item. This procedure iterates over all the nodes
in the linked list and writes each one to disk. The entire file is rewritten from scratch each time
the procedure is called. Like all the procedures in the LINKLST2 program, it would not mat-
ter if there were 5 or 5,000 records in the list. The procedures would work exactly the same
under either condition. The only limit to the size of the list is the amount of available memory
on your system.
The LINKLST2 program knows how to count all the nodes in the linked list:
procedure TDataForm.Count1Click(Sender: TObject);
var
i: Integer;
Head: PMyNode;
begin
i := 1;
Head := FirstNode;
while Head^.Next <> nil do begin
Head := Head^.Next;
Inc(i);
end;
MessageDlg(‘Total = ‘ + IntToStr(i), mtInformation, [mbOk], 0);
end;
while doing nothing more than incrementing the variable i after visiting each node. At the
end, i will be equal to the total number of records in the list, and the MessageDlg procedure is
then called on to display that number to the user.
The Find procedure searches through all the records in the list until it finds one that has a Name
field equal to a string entered by the user:
function TDataForm.Find(S: String): PMyNode;
var
Head,
Temp: PMyNode;
begin
Head := FirstNode;
Temp := nil;
while Head^.Next <> nil do begin
if Head^.Name = S then begin
Temp := Head;
Break;
end;
Head := Head^.Next;
end;
Notice that when the sought-after record is found, the routine exits the while loop by calling
Break. Delphi provides two commonly used ways for getting out of a loop:
■ A call to the Exit procedure breaks you out of the loop and also out of the current
procedure. If the current procedure is the main body of a program, a call to Exit ends
the entire program.
■ A call to Break causes the code to exit the current loop but not the entire procedure.
For instance, in the preceding example, Break exits the while loop, but the following
two lines are still executed:
if Head^.Name = S then Temp := Head;
Find := Temp;
Notice that the lines that exist outside the loop are necessary for special cases—for instance,
when there is only one node.
Perhaps the most important, and most difficult, procedure from the LINKLST2 program is
the DeleteNode procedure:
procedure TDataForm.DeleteNode(var Node: PMyNode);
var
Temp: PMyNode;
begin
Temp := FirstNode;
Current := Temp;
There are three different sections in this routine. The first section checks to see if the node to
be deleted is the first node in the list. If so, the code iterates down to the second node of the
list, sets FirstNode equal to it, and then deletes the node that was at the head of the list.
The second section in the DeleteNode procedure checks to see if the node to be deleted is some-
where in the middle of the list. If it is, the Next field in the node before the one to be deleted is
set equal to the node after the one to be deleted. The node between these two locations is dis-
posed of.
The third section of the DeleteNode method checks to see if the last node in the list is the one
that needs to be deleted. If it is, the Next field in the next to last node is set to nil, and the final
node is disposed of.
The logic in the DeleteNode procedure is interesting primarily because it shows how you can
use if statements to track down special cases. If the first or last nodes in the list need to be
deleted, each one needs to be handled as a special case; otherwise, a default method is used for
deleting any of the nodes in the middle of the list.
I have also included a third program, called LINKLST3, that shows the LINKLST2 transformed
into a console application. This third example is provided as an afterthought, and I do not
mention it again, other than to say that it is available on disk. After you start the program,
choose Create New List from the menu before trying to iterate through any of the records.
Listing 5.4. The POINTER5 program explores the strange world of pointers and
the Intel addressing scheme.
program Pointer5;
var
A: Pointer;
B: PChar;
begin
A := Ptr(HInstance); { A “fake” pointer to nothing, }
B := Ptr($000045FF); { There is no memory allocation! }
A := nil; { Set pointer to nil }
B := nil; { Set pointer to nil }
New(A); { Pointer will STILL equals nil! }
Dispose(A); { Does nothing }
continues
You can see that POINTER5 contains an unusually large number of comments. I have done
this because I want to give you something concrete to hang on to while I discuss some pretty
abstract concepts. There is, however, a second version (POINTER5A) of the program avail-
able on disk that does not have any comments. If you find the comments confusing, switch to
the other version.
A quick look at this program reveals that it doesn’t do anything useful. In fact, it does strange
things such as create pointers that don’t point at valid data. When it finally does allocate some
memory, it never uses it in any conventional sense but quickly proceeds to dispose of it.
I wrote this program primarily to give you a chance to watch pointers in the stand-alone
debugger. You will probably be able to follow the first part of this discussion if all you have is
the integrated debugger, but the real meat of the discussion requires the stand-alone tool.
NOTE
When you compile for the stand-alone debugger, you must make sure that the “In-
clude TDW Debug Info” switch is turned on. To set this switch, choose the Project |
Options menu selection and turn to the Linker page. The switch is near the bottom of
the window, and it is turned on when there is an X in front of it. Make sure also that
the Debug Information and Compiler Symbols options are turned on in the Compile
page. You must also be sure that Optimizations is turned off ! The Optimizations
option is also found on the Project | Options | Compiler page.
For safety’s sake, it is probably a good idea to choose Build from the Run menu now.
The debug symbols won’t be embedded into the executable unless the program is
rebuilt. Once the settings are correctly identified, and the entire project has been
rebuilt, the next step is to pop up the stand-alone debugger by choosing the Tools |
Turbo Debugger menu option, or else by starting the debugger from the command
line and passing it the POINTER5.EXE program as a parameter.
Turbo Debugger may not ship with your version of Delphi 2.0. You can, however,
purchase it from Borland as a stand-alone product. It usually comes bundled with
TASM, which is Borland’s assembler. If you have Borland C++ 4.5 or later, you may
already have a copy of a Delphi-compatible, stand-alone debugger, which is called
TDW.EXE. The only way to know for sure whether your version of TDW or TD32
works with Delphi is to try it. Remember that it won’t work properly unless you
compile with debug symbol info. You must pass the executable you want to run as a
parameter to TD32.EXE, or else you must load your program from the File menu.
The first time you step through the program, you should set the following watches in the Watch
Window of either the integrated or the stand-alone debugger. Use the stand-alone debugger if
possible, because it is designed from the bottom up for this kind of analysis of a program or
code fragment.
A
B
@A
@B
A^
B^
The @ symbol tells the debugger to return the address of a variable. You can use the @ symbol
inside the code of your own programs if you want to reference, not the contents of a variable,
but its address.
After pressing F8 for the first time, the Watch Window at the bottom of the screen should
look something like this:
B^ #158 : CHAR
A^ @:00000000 : UNTYPED
@B :00406528 [Pointer5.B] : POINTER
@A :00406524 [Pointer5.A] : POINTER
B :00000000 : PCHAR
A :00000000 : POINTER
Of course the actual addresses for the first, third and fourth entries you see in the Watch Win-
dow might not be identical to those you see here, but the general layout should be similar.
(However, in Win32 it is at least probable that the numbers you see will be identical to the
ones I see, particularly if we happened to be using the same shipping version of the compiler,
which in this case is unlikely. The issue here is that programs are loaded at a prespecified vir-
tual address, and the data is stored at offsets from that virtual address.)
NOTE
Some curious readers may wonder how I copied the information from the Watch
Window in the debugger. Actually, it isn’t difficult—you just need to know your way
around. One way is to show the debugger as a child window on the desktop, and then
choose Edit from the System menu. A second method involves the Log Window,
which can be accessed from the View menu in the stand-alone debugger. Simply follow
these steps:
1. After opening the Log Window, click it once with the right mouse button.
2. Choose “Open Log File” and click the OK button to accept the default name
given to you by the debugger.
3. Select the window whose contents you want to save.
4. Pull down the Edit menu and choose “Dump Pane to Log.”
5. Click the right mouse button again on the Log window and close the file.
When you return to DOS, you will find a file saved under the default name containing
the contents of the window that was focused when you chose “Dump Pane to Log.”
You probably won’t have to do this kind of thing very often, but when the time finally
comes around, the need is usually fairly urgent.
Two of the addresses in the Watch Window are important to use during this initial stage of the
program analysis:
@B :00406528 [Pointer5.B] : POINTER
@A :00406524 [Pointer5.A] : POINTER
Notice that there is no segment and offset format for the display of these pointers. Instead, you
have a single flat model address: $00406524. This is just a simple hexadecimal value which,
when translated into base ten, is revealed to be slightly larger than 4 megabytes: 4,220,196.
The pointer here is that there is no longer any data segment in your program. Win32 has a
non-segmented architecture. Your executable is loaded into memory at a particular place, usu-
ally 4 MB, and its data will usually appear at some virtual address slightly higher than that. You
can specify the exact virtual address at which your program will be loaded by selecting the Project
| Options | Linker page, and checking the Image Base entry.
In this particular case, variable A is stored $00406524. Four bytes further on you can find B,
which resides at $00406528. Once again, this should not surprise anyone, because the vari-
ables were declared right next to each other in the code, and they are each four bytes in size. In
other words, B is found 4 bytes further into the program’s image than A because the interven-
ing space is taken up by pointer A, which is four bytes in size.
NOTE
Two quick points might be helpful for some readers. First, if you ever forget how large
a variable is, you can run the SizeOf function on it. Generally you don’t have to
recompile your code but can simply type SizeOf(A) in the Watch Window, and the
debugger will tell you the size of the object, which in this case is 4.
Second, don’t forget that Windows ships with a calculator that can help you translate
hex numbers into decimal numbers. The calculator is kept in the Accessories group,
and you can start it the way you would any other Windows program. Once it’s
running, choose the View | Scientific menu item. Type 170 into the calculator and
click the button labeled Hex to see its hex value, which is AA. Conversely, you can first
click the Hex button, then type AE, and the calculator will translate the number into its
decimal value if you click the Dec button.
Once again, you don’t need to know any of these things to program in Delphi.
However, if you want to get really serious about programming, you ought to know this
material, or at least aspire to know it one day.
The key point that has been established so far is that the pointers A and B both reside in the
main executable image for the program. When you load a program, it is sucked into memory
by the operating system via a mechanism called a memory-mapped file. In effect, the entire
executable is mapped into a series of virtual addresses which I am referring to as the executable’s
image.
In the simplest case scenario, the program is literally read into one area of memory in one fell
swoop, without any concern for creating the segmented areas that were common in 16-bit
Windows. Of course, in some cases portions of an executable may reside on disk even after
they have been mapped into virtual memory. The point of memory mapped files is that you
can treat data on disk as if it were residing in RAM. That is, the file on disk has been mapped
into virtual addresses, and can be accessed the same way you would access any area in memory,
and with the same commands. That is, you can call functions like StrCat or Move on files that
reside on disk. It is of no concern to you whether this memory is actually in RAM or actually
swapped to disk. The operating system lets you treat the memory exactly the same, regardless
of its real location.
There is a big difference between the address at which A and B reside and the items they point
at. In fact, at this time, A and B aren’t pointing at anything. Their value is currently nil.
NOTE
Delphi initializes all global variables to nil, or 0, on start up. If you are following along
on a DOS version of Pascal which doesn’t do this, you should add two lines of code to
the program, which will initialize these values to nil.
Here comes the important part. A and B point at nil. That much is clear. But what exactly does
this mean? What specifically does it mean to say that a pointer is equal to nil? To find out, you
need to open the Dump Pane from the Watch Window. Click it once with the right mouse
button and select the option that says Goto. (See Figure 5.3.) A dialog box will appear and you
can type the letter A into it and press Enter. This takes you to a pictorial representation of the
place in the executable image where A actually resides:
:00406524 00 00 00 00 00 00 00 00
:0040652C 00 00 00 00 00 00 00 00
:00406534 00 00 00 00 00 00 00 00
:0040653C 00 00 00 00 00 00 00 00
As you can see, the four bytes shown immediately after $00406524 are all zeros. This is what
it means to say that A is equal to nil. Just so you’re sure to understand what I mean, let me
show you the line again, with the relevant four bytes in brackets:
$00406524 [00 00 00 00] 00 00 00 00
The last four bytes in the preceding line also happen to be set to zero, which once again should
not come as a surprise, because they belong to B. As you saw earlier, B is also set to nil, so of
course the four bytes in its portion of the data segment are set to zero.
FIGURE 5.3.
Searching for an address in
the Dump Pane.
To verify the value of B, you can click once on the right mouse button, select Goto, and enter
B. The Dump Pane you see will look something like this:
:00406528 00 00 00 00 00 00 00 00
:00406530 00 00 00 00 00 00 00 00
:00406538 00 00 00 00 00 00 00 00
:00406540 00 00 00 00 00 00 00 00
Once again, it is the first 4 bytes after $00406528 that are important. As you can see, they are
set to zero, and that means that B has the value of nil.
After spending all this time getting set up, and after carefully examining the initial state of the
program, it is at last time to step through the first real lines in the program:
The Ptr function creates a pointer that addresses a particular place in memory. Specifically, the
preceding code sets the first two bytes at the address $00406524 to the address of the place in
memory where the program is loaded:
:00406524 00 00 40 00 FF 45 00 00 @ _E
:0040652C 00 00 00 00 00 00 00 00
:00406534 00 00 00 00 00 00 00 00
:0040653C 00 00 00 00 00 00 00 00
Here is the significant line again, with the relevant address offset in brackets:
:00406524 [00 00 40 00] FF 45 00 00 @ _E
HInstance is always set to the place in memory where your program is loaded. This is specified
in the Project | Options | Linker page from the menu, and should normally be set to $00400000.
The most obvious facts about the preceding line are that the numbers in the address are re-
versed. Specifically, the high byte is where you would expect the low byte to be, and vice versa.
If this strikes you as peculiar, you are a considerably calmer and more controlled person than
myself. Even after years of working with the Intel architecture, I am still completely floored by
the fact that such a topsy-turvy memory scheme managed to win out over more logical alterna-
tives. The simple moral of this story is that it is not reason, but the free market, that dictates
the major decisions made in the computer industry. All in all, the free market is probably a
respectably admirable arbiter in matters like this, yet it does yield some surprising results at
times. (Given the flap over the floating point Pentium errors, I should perhaps add that I think
Intel is a fine company, although I don’t understand why they had to leave us with such a topsy-
turvy method for storing addresses in memory.)
NOTE
Connoisseurs of the DOS EXE format should use the Dump Pane to Goto the actual
address in memory pointed to by both HInstance and A:
00400000 4D 5A 70 00 05 00 00 00 MZp .
00400008 20 00 00 00 FF FF 47 00 __G
00400010 00 02 00 00 5C 02 00 00 . \ .
00400018 40 00 00 00 01 00 FB 50 @ . _P
Notice the familiar MZ signature! Memory-mapped files just suck the executable into
virtual memory!
At any rate, the second line of code shown previously looks like this:
B := Ptr($000045FF); { There is no memory allocation! }
After it is run, the place in the data segment where B resides looks like this:
00406528 FF 45 00 00 00 00 00 00 _E
00406530 00 00 00 00 00 00 00 00
00406538 00 00 00 00 00 00 00 00
00406540 00 00 00 00 00 00 00 00
Once again, here are the relevant portions marked with parentheses around the segment and
brackets around the offset:
00406528 [FF 45 00 00] 00 00 00 00 _E
As you can see, the address is obviously set to $45FF, with the bytes reversed, as dictated by the
whims of the good folks at Intel.
To sum up what has been said here, you can look down at the Watch Window, which now
shows A and B equal to the following:
B :000045FF : PCHAR
A :00400000 : POINTER
This representation of the address is much more readable than what is seen in the Dump Pane.
However, the view in the Dump Pane is perhaps more important, because it shows exactly what
is happening in the memory that belongs to the program. Specifically, it shows that the values
that had been set to nil are now pointing at particular addresses.
NOTE
The List Pane used to examine the validity of pointers will probably not be included in
the shipping version of Delphi. As a result, I have cut that section of this chapter.
However, it might be of use to some readers, so I placed it in the POINTER5 direc-
tory on disk, in a text file, and in a Word 7.0 (WordPad) file.
When you step through these lines, you should set up the Dump Pane so that it gives you a
view of the place in the data segment where A and B reside. If you do this, you will see them
both being set back to zero.
NOTE
Under normal circumstances you should never set the value of a valid pointer to nil
without first deallocating the memory associated with it. However, this time it is okay
to do this with variable A, because it was simply set equal to memory that had already
been allocated by the system. In other words, A was not the owner of the memory it
addressed, it was simply borrowing it for a time. As a result, there is no need to
deallocate the memory that A addresses, and there is no harm in setting its value back
to zero.
This seems like a reasonable thing to do, because you know that the New function allocates
memory for pointer values, and A is indubitably declared as a pointer.
The problem with this code is that the New function simply allocates enough memory to cover
the area addressed by a particular pointer. Because an ordinary pointer variable by default does
not address any memory, a call to New in this case will not result in a change to the status of A.
That is, it will still equal nil. If you want to allocate memory for a variable of type Pointer, you
need to call the GetMem function. Use new on pointers that point to data structures, such as a
pointer to a ShortString, a pointer to an array, or a pointer to a record.
Before looking at the GetMem function, pop up the CPU window again and step through the
assembler code that gets executed as a result of a New(A) statement:
Pointer5.17: New(A); { Pointer will STILL equals nil! }
:00404EF2 33C0 xor eax,eax
:00404EF4 E85FD3FFFF call @GetMem
:00404EF9 A324654000 mov [Pointer5.A],eax
As you can see, the compiler generates code that zeros out EAX:
xor eax, eax
and then calls a function that allocates memory from the operating system. When this call re-
turns, EAX holds the address of some allocated memory. However, the value in EAX is still zero,
so A is still set to nil.
Compare the preceding example with the following code:
Pointer5.19: GetMem(A, 100); { Allocate a hundred bytes }
:00404F0A B864000000 mov eax,00000064
:00404F0F E844D3FFFF call @GetMem
:00404F14 A324654000 mov [Pointer5.A],eax
If you remember that decimal 100 and hex 64 are equivalent, you can see that the following
code first loads the value 100 into EAX, then calls the function that allocates memory. When
it returns, EAX no longer contains zero, so this time A is not set to nil.
The address that is listed at $00406524 in the program’s image now references real memory
on the heap that belongs to variable A. To prove this to yourself, put the address shown in the
Watch Window next to variable A into the List Pane. You will see that the debugger lists it as
belonging to a valid selector that has nothing to do with your program’s executable image. (See
Figure 5.4.) This address is to be found somewhere out on the heap, but it is now a portion of
the heap that temporarily belongs to your program.
FIGURE 5.4.
Take the address ($XXXX)
shown in the Watch
Window and enter it in the
List Pane to see if you have
a valid selector.
The code shown here deallocates the memory associated with A and B. After these pointers are
deallocated, you can put them in the List Pane to see that they are no longer considered valid
pointers. Furthermore, this shows two different ways of setting a variable to nil. The first method
simply makes an assignment:
A := nil; { Set pointer to nil }
The second method uses Delphi’s built-in assembler to move zero into the bytes occupied by
the variable B:
asm { Set pointer to nil (B := nil;) }
mov longint ptr B, 0; { Zero out the pointer }
end; { Close ASM block }
This discussion has been fairly involved, but it should help you begin to grasp exactly how
pointers are constructed, and how you can use the stand-alone debugger to spy on their inner
workings.
One of the main points I’m trying to stress in the second half of this chapter is that the stand-
alone debugger is a good tool to use when you need to track down pointer bugs. In the last few
sections of the chapter you have seen how valid pointers, nil pointers, and invalid pointers look
in the debugger. Use this knowledge to help you track problems if something goes wrong with
your program.
Summary
This chapter presented you with several full-length linked lists, and it showed you how you
can manipulate these lists and how you can use them to store large amounts of data. In particu-
lar, you learned how to iterate through a linked list, how to count the nodes in a list, how to
add and delete data from a list, and how to search for a record stored in a linked list. You also
got a good look at some code that read a linked list from disk, and which enabled you to modify
and then restore the list to its original location on disk.
Most of the time, built-in Delphi objects or tools such as TStringLists, TLists, and Paradox
or dBASE tables will provide the simplest possible means for tracking long lists of data. How-
ever, linked lists can still be convenient if you need to create a custom list with particular prop-
erties, or if you want to write portable code that can be used both in Delphi and at the DOS
prompt. The biggest advantage linked lists can bring you, however, is that they have almost no
overhead. As a result, if memory usage has become a big problem in your program, you can use
linked lists. However, under normal circumstances you will probably use a database table,
TStringList, or TList. In particular, serious Delphi programmers should take the time to ex-
plore the TList object. On the CD, you will find the OBJECT4 program and the EXPLORER
program, both of which use the TList object.
The text of this lengthy chapter also discussed the particulars of the Intel addressing scheme,
and it showed you how you can use the debugger to learn about the pointers in your programs.
Exceptions
6
In this chapter, you will learn how to add error-handling to your programs. This is done al-
most entirely through a mechanism called exceptions.
In particular, the following subjects are covered:
■ Exception-handling theory
■ Basic exception classes
■ try..except blocks
■ try..finally blocks
■ Raising exceptions
■ Creating your own exception classes
■ Saving error strings in resources and retrieving them with LoadString
■ Internationalizing your program with string tables
■ Overriding the default exception handler
To a large degree, Delphi and the VCL make it possible for you to write programs that almost
entirely ignore the subject of error-checking. The reason for this is that exceptions are built
into most classes and standalone routines so that they will be raised automatically whenever
something goes wrong. However, professional programmers will want to go beyond even this
level of safety and add additional error-checking to their code, or else change the default error
handling performed by Delphi. Also, your programs might need to raise their own errors, so
you will need to add and raise new exception classes.
NOTE
Right at the start, it’s important to recognize the difference between Delphi exceptions,
which cover language issues, and hardware exceptions, which involve hardware and
hardware interrupts. You can (and Delphi often does) wrap a hardware exception
inside a Delphi exception, and handle the event that way, but hardware exceptions are
different than Delphi exceptions.
For instance, running out of paper in a printer causes a hardware exception. Object
Pascal doesn’t get an exception in this case, but a component might raise an exception
if the situation is detected. Raising the exception does not cause the hardware error; it
already exists outside the program.
Traditionally, error-handling has been a matter of setting flags and then responding to a flag
that designates an error condition. For instance, if you tried to open a file that does not exist,
an IOResult or DosError flag would be set and you could detect the condition by checking the
flag. Under the new system, instead of a flag being set, an exception is raised.
The easiest way to start appreciating the advantages that exceptions bring to your code is to
imagine a situation in which three different levels of code are called. Suppose, for instance,
that you wanted to display data in a grid, where the data in question is stored in a text file. You
might have the following set of calls, all nested inside each other:
function DisplayDataInGrid: Word;
function RetrieveData: Word;
function OpenFile: Word;
function ReadFile: Word;
function CloseFile: Word;
In this scenario, DisplayDataInGrid calls RetrieveData, and RetrieveData calls OpenFile, ReadFile,
and CloseFile. After CloseFile was called, control would return to DisplayDataInGrid, and
the actual data would be shown to the user.
If something went wrong during the time the file was being opened, OpenFile would have to
pass that information back to RetrieveData. RetrieveData would have to include code ensur-
ing that ReadFile and CloseFile didn’t get called; then it would have to pass the error condi-
tion back to DisplayDataInGrid. In turn, DisplayDataInGrid would have to ensure that it did
not try to display data which was never correctly retrieved. This whole process of passing infor-
mation in a daisy chain forces you to write complicated, confusing, and error-prone code.
As a child, you might have played a game called “telephone.” In particular, you might have
had a teacher, camp counselor, or friend who arranged a large group of people in a circle and
then whispered something to the person on his or her left. That person in turn whispered the
message to someone on his left, and so on, until the message went all the way around the circle.
When the message had made the whole trip, it almost always ended up changing radically from
the time that it started, until the time when the trip was complete. The same thing can happen
in a program if you use the daisy chain method of conveying error conditions. The worst case,
of course, is when one link in the chain is broken altogether and the rest of the code continues
merrily on its way, oblivious of the fact that something serious has gone wrong.
Exceptions don’t use the daisy chain theory of error processing. Instead, if an exception is raised
in the function OpenFile, the code automatically unwinds back to DisplayDataInGrid, where
you can write code that handles the error. Exceptions automatically pop procedures and data
off the stack, and they automatically ensure that no other routines behind or in front of it are
called until the code is found that is intended to handle the exception.
NOTE
When code is being popped off the stack, Delphi checks for one particular block of
code that needs to be executed, even if an exception has occurred. This special code is
enclosed in try..finally blocks, as you will see in the next-to last section of this
chapter.
If an exception occurs and you do not handle it explicitly, a default exception handler will process
the exception. Most exceptions are not handled explicitly. When the VCL default exception
handler is invoked, it displays a message dialog describing the exception to the user, and then
your program resumes its normal processing of Windows messages. Your program does not
return to the code that raised the exception. When an exception is raised, special code in your
program (in the system unit) checks the most recently entered try block on the call stack for a
suitable exception handler. If that try block has no exception handlers suitable for the current
exception, the next outward try block is checked, and so on, until an exception handler is found
or it reaches the default exception handler. Normal program execution resumes at the next
statement following the code that handles the exception.
There is another default exception handler below the VCL default handler, called the RTL
default exception handler. If an exception occurs in code that is not part of VCL’s normal event
or message handling (or in a Delphi program that doesn’t use VCL) and that exception goes
unhandled, it will be caught by the RTL default exception handler. The RTL default excep-
tion handler displays a rather technical error via a MessageBox and then terminates your
program.
For some reason, all this always sounds considerably more complex in theory than it is in prac-
tice. If what I have said here does not quite make sense to you, just forage on anyway, and I
think it will come clear in time. The previous few paragraphs lay out the theory behind a fairly
simple set of syntactical routines. Play with the syntax for awhile, using the descriptions below
as a guide, and then if you want, come back and read the theory a second time.
Exception Classes
Delphi comes with a rich set of built-in exception classes meant for handling a wide range of
exceptions. You can easily create your own exception classes for handling the key events in your
program that might be susceptible to error. Here is the base class for exception handling, as it
is declared in SYSUTILS.PAS:
Exception = class(TObject)
private
FMessage: string;
FHelpContext: Integer;
public
constructor Create(const Msg: string);
constructor CreateFmt(const Msg: string; const Args: array of const);
constructor CreateRes(Ident: Integer);
constructor CreateResFmt(Ident: Integer; const Args: array of const);
constructor CreateHelp(const Msg: string; AHelpContext: Integer);
constructor CreateFmtHelp(const Msg: string; const Args: array of const;
AHelpContext: Integer);
constructor CreateResHelp(Ident: Integer; AHelpContext: Integer);
constructor CreateResFmtHelp(Ident: Integer; const Args: array of const;
AHelpContext: Integer);
property HelpContext: Integer read FHelpContext write FHelpContext;
property Message: string read FMessage write FMessage;
end;
The key point to grasp here is that all exceptions have a message that can be displayed to the
user. You can pass in this message through a number of different constructors, and you can
retrieve it through the Message function.
NOTE
Delphi classes sometimes have multiple constructors because you may have more than
one way in which you want to create a class. Sometimes you might want to initialize a
class by passing in one type of string, and another time you might want to pass in a
second type of string or perhaps an integer. To give you the flexibility you need,
Delphi enables you to declare multiple constructors. Needless to say, you still call only
one constructor when creating a class; it’s just that you have a choice of which con-
structor you want to choose.
Here are some additional built-in exceptions, all quoted directly from SYSUTILS.PAS:
EOutOfMemory = class(Exception)
public
destructor Destroy; override;
procedure FreeInstance; override;
end;
EInOutError = class(Exception)
public
ErrorCode: Integer;
end;
EIntError = class(Exception);
EDivByZero = class(EIntError);
ERangeError = class(EIntError);
EIntOverflow = class(EIntError);
EMathError = class(Exception);
EInvalidOp = class(EMathError);
EZeroDivide = class(EMathError);
EOverflow = class(EMathError);
EUnderflow = class(EMathError);
EInvalidPointer = class(Exception);
EInvalidCast = class(Exception);
EConvertError = class(Exception);
EAccessViolation = class(Exception);
EPrivilege = class(Exception);
EStackOverflow = class(Exception);
EControlC = class(Exception);
EVariantError = class(Exception);
EPropReadOnly = class(Exception);
EPropWriteOnly = class(Exception);
You can see that there are error codes for divide by zero errors, file I/O errors, invalid type
casts, and various other conditions both common and obscure.
The preceding list, however, is far from complete. Many other exceptions classes are declared
in other modules of the VCL. To get a feeling for their complete scope, you should use the
Browser and also the online help. Figure 6.1 shows the Browser as it looks when it is in the
thick of the exception hierarchy.
FIGURE 6.1.
The Browser is a good tool
to use when you want to see
if Delphi has a class for
handling a particular type
of exception.
by your program will cause the debugger to take you as near as possible to the place in your
code where the exception occurred. Only after you start running again will you see the error
message as it will be reported to the user. As a result, you should probably keep Break on Ex-
ception set to False, except when you explicitly want to step through your code during an ex-
ception.
The SIMPEXEP program, referenced in Listing 6.1, gives four examples of how to raise and
handle exceptions in Delphi. The first two examples shown are of the simplest possible kind,
and are meant to get you started with the concepts involved in this process. The latter two
examples in this program are a bit more complex, but are still relatively straightforward.
Remember to turn Optimizations off when running this program. You can find the Optimiza-
tions settings in the Project | Options | Compiler menu option.
Listing 6.1. The SIMPEXP program demonstrates basic techniques for handling
exceptions.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons;
type
TForm1 = class(TForm)
DelExcept: TBitBtn;
UserExcept: TBitBtn;
Declare: TBitBtn;
CalcAdd: TBitBtn;
procedure DelExceptClick(Sender: TObject);
procedure UserExceptClick(Sender: TObject);
procedure DeclareClick(Sender: TObject);
procedure CalcAddClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
continues
{$R *.DFM}
end.
This program contains four TBitBtns that, when pressed, raise exceptions. In the first case, the
code lets Delphi’s built-in routines handle the exception; in the rest of the examples, a custom
error handler is invoked.
Here is the simplest way to raise and handle an exception:
procedure TForm1.DelExceptClick(Sender: TObject);
var
i, j, k: Integer;
begin
j := 0;
k := i div j;
end;
The code shown here causes a divide by zero error, and it will automatically pop up the dialog
shown in Figure 6.2.
FIGURE 6.2.
The default mechanism for
handling a divide by zero
error.
The error message shown in Figure 6.2 is useful to programmers, since it provides an address
you can use in the Search | Find Error menu choice. However, it is not a particularly friendly
message to send to a user of your program.
The following code demonstrates how to set up a try..except block that gives you a place to
handle an error:
procedure TForm1.UserExceptClick(Sender: TObject);
var
i, j, k: Integer;
begin
j := 0;
try
k := i div j;
except
on EDivByZero do
MessageDlg(‘Division by zero error detected!’,
mtError, [mbOk], 0);
end;
end;
The code that you want to test appears right after the reserved word try. In this case, all that
goes on in this section is that you force a divide by zero error. When the error occurs, the code
after the word except is executed. In this section, you designate that you want to handle
EDivByZero messages explicitly, and you do so by popping up an error message.
In the first part of the try block, you could make a procedure call. That procedure could in
turn call another procedure, and so on, for six, ten, or however many levels of nested calls. If
any of the routines in that block raised an EDivByZero error, the code would automatically jump
to the MessageDlg shown in the preceding code.
Or, to state the same fact somewhat differently, if the code looked like this:
j := 0;
try
k := i div j;
DoSomething;
DoSomethingElse;
DoThis;
DoThat;
except
on EDivByZero do
MessageDlg(‘Like wow! Don’t divide by zero, man!’,
mtError, [mbOk], 0);
end;
then DoSomething, DoSomethingElse, DoThis, and DoThat would never be executed. Instead, the
code would jump immediately from the division statement to the except section.
You can combine the convenience of having Delphi report an error with the luxury of being
able to define your own error strings:
procedure TForm1.DeclareClick(Sender: TObject);
var
i, j, k: Integer;
begin
j := 0;
try
k := i div j;
except
on E:EDivByZero do
MessageDlg(Self.ClassName + ‘: ‘ + E.Message,
mtError, [mbOk], 0);
end;
end;
In this code, Delphi is in effect allowing you to map an identifier onto the already existing
exception object instance that was raised. This is not a variable declaration, since no new stor-
age is being allocated. It’s simply giving you a convenient way to access the exception object
instance so that you can extract additional information carried by the object instance, such as
the error message string:
E.Message
where Message returns the string that Delphi would associate with this error. To understand
where Message comes from, refer to the declaration of TException shown previously.
After you have access to the message associated with an exception, you can add your own infor-
mation to it. For instance, in this case I snag the name of the object whose method is raising
the exception, thereby helping me know right away where the problem has occurred:
S := Self.ClassName + ‘: ‘ + E.Message,
FIGURE 6.3.
An exception that shows a
string created in part by
Delphi and in part by the
programmer.
You also might want to display the actual address at which an exception is raised. The built-in
ExceptAddr function returns this address:
This code displays the regular error message associated with an EDivByZero exception and then
immediately converts the result of the ExceptAddr function into a string so that you can see
that it returns the same address displayed by a Delphi exception. You can use this address in
the Search | Find Error menu option, which will take you to the place in your code where the
error occurred.
To convert a pointer into a string, you can use the following function from the StrBox unit:
function Address2Str(Addr : Pointer) : string;
begin
Result := Format(‘%p’, [Addr]);
end;
This function uses the Format function to perform a routine string operation that returns a
string version of an address.
In this section, you learned how to handle simple exceptions. The actual logistics of handling
these situations can be quite complex at times, but you are now armed with the basics needed
to go into battle and wage war with the compiler. Note that I arranged the examples in this
section in increasing order of complexity, where the first two are more typical of the code you
will use in most programs, whereas the latter two are useful when you want to use advanced
techniques.
Listing 6.2. The MYEXCEPT program shows how to create and raise custom
exceptions.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons;
type
ESillySpellingError = class(Exception);
TForm1 = class(TForm)
ERead: TBitBtn;
EReadAddr: TBitBtn;
DelphiRaise: TBitBtn;
Edit1: TEdit;
Label1: TLabel;
procedure RaiseEReadClick(Sender: TObject);
procedure EReadAddrClick(Sender: TObject);
procedure DelphiRaiseClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
StrBox;
{$R *.DFM}
end.
To use this program, just press any of the three buttons on the form. The third button won’t
raise an exception unless the edit control is set to the misspelled string occured.
Exceptions occur because they are explicitly raised. For instance, here is another version of the
Str2Int routine:
This procedure uses the Delphi Val procedure to convert a string into an integer. If all is suc-
cessful, Result is set equal to the transmuted string and E is set to 0. If there is a problem, E is
non-zero. When an error condition exists, the Str2Int routine raises an EConvertError and
passes in a string that explains what has gone wrong.
TIP
Val is one of many built-in routines for handling strings and numbers. If you are trying
to find out whether Delphi contains a particular type of function, you can search on
“Procedures and Functions (Categorical)” in the online help.
EConvertError is a built-in Delphi type meant to be used in situations where an error occurs in
a conversion routine. If you look in SYSUTILS.PAS, you will find that Delphi uses this excep-
tion quite a bit, but tends to raise it via the good graces of the ConvertError routine.
There is nothing in Delphi that forces you to use a particular class of exceptions in a particular
situation. For instance, in the following method I raise an EReadError, even though nothing
has gone wrong in the program:
procedure TForm1.RaiseEReadClick(Sender: TObject);
begin
raise EReadError.Create(‘Fake EReadError has occurred’);
end;
Exceptions are triggered when you use the reserved word raise and then construct an instance
of a particular type of exception. In and of themselves, exceptions have nothing to do with
errors, and indeed you could use them for some entirely different purpose. In other words,
exceptions are a good means of reporting errors, but they do not occur because an error occurs;
they occur because you use the reserved word raise!
Many of the exception classes that are built into Delphi may be useful to you at times. For
instance, you might need to convert some variable from one type to another; or there may be
an occasion when you need to read some value. If errors occur during such tasks, it would make
sense to raise an EConvertError or an EReadError. That way, the code that depends upon your
conversion routines doesn’t have to worry about what to do when the conversion routines fail.
On failure, they will raise an exception and will never return bad data or error codes to the
caller. That can go a long way toward simplifying your code.
Despite the usefulness of many of Delphi’s built-in exception classes, there are many occasions
when you are going to need to create exception classes of your own. To do so, you should first
declare a new class:
type
ESillySpellingError = class(Exception);
Hopefully, writing about the error in this book will help me remember that there are two Rs in
the word—not just one!
At any rate, you can see that it is very easy to create your own exceptions and to raise them.
Whenever you feel that you need to describe a new type of error, you can do so by just creating
the simple type shown in the preceding code. If you want, you can create more complex excep-
tion types:
EInOutError = class(Exception)
public
ErrorCode: Integer;
end;
The EInOutError adds an ErrorCode to its object declaration. You can then reference this error
code in an except block:
try
..
except
on E:EInOutError do
Code := E.ErrorCode;
end;
Remember that the need for different types of errors becomes evident, not when you are rais-
ing them but in the except portion of try..except blocks. You might want to handle a particu-
lar type of error explicitly and let other errors be handled by the default handler. To do this,
you must have a variety of different exception classes to raise so that your exception handlers
can be set up to distinguish between the different error situations.
If you look carefully at the SYSUTILS unit, you will see that Delphi rarely hard codes a string
into the code that raises exceptions. Instead, it constantly calls functions with names such as
LoadStr or FmtLoadStr. These functions call the Windows API routine LoadString, which re-
trieves a string from a resource:
function LoadString(Instance: THandle; ID: Word;
Buffer: PChar; BufferMax: Integer): Integer;
The LoadString function is akin to the LoadResource function you saw in the chapter on DDE
and OLE. In particular, you pass in the following:
Instance: HInstance variable of the current module. This variable is declared and
assigned automatically by the system. You don’t have to do anything but use the
variable, which is passed on to you gratis by both Windows and Delphi. (You can load
strings from a resource DLL simply by using the DLL’s module handle for the
Instance parameter to LoadString.)
ID: A number referencing a particular string in a string resource.
Listing 6.3. The RESERROR program shows how to read error strings from a
resource file.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons;
type
EErrorOne = class(Exception);
EErrorTwo = class(Exception);
TForm1 = class(TForm)
TestLoadStr: TBitBtn;
RaiseErrorOne: TBitBtn;
Edit1: TEdit;
Edit2: TEdit;
RaiseErrorTwo: TBitBtn;
procedure TestLoadStrClick(Sender: TObject);
procedure RaiseErrorOneClick(Sender: TObject);
procedure RaiseErrorTwoClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
{$R RESMAIN.RES}
{$I RESMAIN.INC}
end.
Listing 6.4. The RESMAIN.INC file lists the errors used by the RESERROR program.
const
ErrorOne = 1;
ErrorTwo = 2;
Listing 6.5. The RESMAIN.RC file can be converted in a RES file by typing BRCC32
-r RESMAIN.RC at the DOS prompt.
#include “resmain.inc”;
STRINGTABLE
{
ErrorOne, “First error”
ErrorTwo, “Second error”
}
The RESERROR program loads error strings from a resource and displays them to the user
when an exception is raised. One button on the main form of the program is used to test to see
that the strings found in the resource can be retrieved, and the other two buttons actually raise
exceptions.
NOTE
String tables can help you during development and during maintenance by allowing
you to change strings in one place and see the changes reflected throughout your
program. Needless to say, this technique can be very useful when you are trying to
create multiple versions of a program for internationalization purposes.
When foreign languages are involved, you might store your string resources in a DLL,
so that you can change the strings without having to recompile your main program. In
short, you can create DLLs that contain nothing but string resources. Each DLL will
contain strings from a different language so that you have a version that is French,
another that is German, and so on. You can then ship a different DLL with the
products that you ship to different countries. When using these DLLs, call LoadString,
but pass in the HInstance of the DLL where the string is stored.
Note also that the string resources in your EXE can be removed and replaced without
recompiling the EXE. All you have to do is use BRCC32.EXE to tack the new re-
sources onto the precompiled EXE.
To create a string resource, you could use the following syntax in an RC file:
STRINGTABLE
{
1, “First error”
2, “Second error”
}
where 1 and 2 are the IDs of the strings, and the strings themselves are shown in double quotes.
Notice, however, that RESERROR does not hard-code numbers into its string table:
#include “resmain.inc”;
STRINGTABLE
{
ErrorOne, “First error”
ErrorTwo, “Second error”
}
You can compile a RC file into a RES file by typing this line at the DOS prompt:
brcc32 -r resmain.rc
BRCC32.EXE uses the C-like #include to reference the include file created. You reference the
same include file in a Delphi program by writing this:
{$I ‘resmain.inc’}
NOTE
Windows 95 can make life miserable for denizens of the command line. In particular,
programs are sometimes installed in deeply nested paths that contain long filenames.
For instance, my current version of Delphi is installed in the c:\program
files\borland\delphi32 subdirectory. Given this rather bleak scenario, the correct
command line for compiling the RES file would be:
c:\progra~1\borland\delphi32\bin\brcc32.exe -r resmain.rc
The issue here, of course, is that the directory ‘program files’ is referenced as shown
above under DOS 7.0, though it may be referenced differently on your system. I have
heard that 4DOS for Windows 95 supports long filenames. Without command line
support for long file names, the best approach may be for you to create a batch file
with the above line in it. (See GO.BAT on the CD-ROM.)
Once you have created a RES file and an include file, you can load the strings into your pro-
gram with a relatively simple routine:
function GetError(ID: Integer): string;
const
Max = 150;
var
S: array[0..Max] of Char;
begin
LoadString(HInstance, ID, S, Max);
Result := S;
end;
LoadString is part of the Windows API, so it works with PChars. PChars will be automatically
translated into strings for ease of use inside Delphi. Notice that once again, I avoid hard-
coding numbers into the program but instead declare a constant called Max. Even if you refer-
ence a number only once, it’s still a good habit to declare it as a constant. The reasoning, of
course, is that you can change the number in one place and be sure that all references to that
number will change. Yes, it is overkill in this one case, but still it is good programming prac-
tice.
It’s worth noting that the VCL and Delphi use resources to store error strings. If you open up
SYSUTILS.PAS, you will find that it makes heavy use of a more complex version of the logic
shown in RESERROR.DPR. In general, string resources are one of the more valuable tools in
the repertoire of any Windows programmer, and you should consider making use of them
whenever you need to hard-code strings into your program.
The following excerpts from SYSUTILS.RC show a portion of a string table used by Delphi to
aid in reporting errors:
#include “sysutils.inc”
STRINGTABLE
{
SInvalidInteger, “‘%s’ is not a valid integer value”
SInvalidFloat, “‘%s’ is not a valid floating point value”
SInvalidDate, “‘%s’ is not a valid date”
SInvalidTime, “‘%s’ is not a valid time”
SInvalidDateTime, “‘%s’ is not a valid date and time”
STimeEncodeError, “Invalid argument to time encode”
SDateEncodeError, “Invalid argument to date encode”
SOutOfMemory, “Out of memory”
...
}
Notice that this table obviously relies on Format and related functions to enable the program to
insert data into a string before showing it to the user.
The following constants are excerpted from a Delphi source file called SYSUTILS.INC and
are used as IDs that reference the errors in the string table shown previously:
const
SInvalidInteger = 65408;
SInvalidFloat = 65409;
SInvalidDate = 65410;
SInvalidTime = 65411;
SInvalidDateTime = 65412;
STimeEncodeError = 65413;
SDateEncodeError = 65414;
SOutOfMemory = 65415;
...
These string constants end up being linked into most of your programs, so you might want to
open up SYSUTIL.RC and see if you can use any of them in your own code.
try..finally Blocks
Sometimes you need to ensure that a particular block of code is executed even if something
goes wrong in the code that proceeds it. For instance, you may allocate memory, perform sev-
eral actions, and finally intend to deallocate the memory. However, if an exception is raised
between the time you allocate memory and the time you want to deallocate the memory, the
code that deallocates the memory might never get executed. To ensure that this doesn’t hap-
pen, you can use a try..finally block.
Here is a second way to think of try..finally blocks. As you know, a try..except block can
cause the execution pointer for your program to jump from the place that an error occurs
directly to the place where you handle that error. That is all well and good under most circum-
stances, but sometimes you want to make sure that some code between the error and the
exception handler is executed regardless of circumstances. try..finally blocks represent
solution to this problem.
The code shown in Listing 6.6 demonstrates how to use a try..finally block. The program is
run in a simple form that contains a button and an edit control.
Listing 6.6. The FINAL program shows how to use a try..finally block.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons;
type
TForm1 = class(TForm)
BitBtn1: TBitBtn;
Edit1: TEdit;
Label1: TLabel;
procedure BitBtn1Click(Sender: TObject);
private
GlobalString: string;
procedure RunCalculation;
end;
var
Form1: TForm1;
continues
{$R *.DFM}
procedure TForm1.RunCalculation;
var
i,j,k: Integer;
begin
j := 0;
try
k := i div j;
Label1.Caption := ‘Sam’;
finally
GlobalString := ‘RunCalculation called!’;
end;
GlobalString := ‘Toot’;
end;
end.
The key point to grasp about the FINAL program is that it nests a try..finally block inside
a try..except block. Of course, in a Delphi program, all try..finally blocks are always nested
inside a try..except block, because each Delphi program in fact occurs inside a try..except
block. However, it helps to see a small example that shows exactly how try..execpt blocks and
try..finally blocks are nested one inside the other. You might want to duplicate code like
that shown in the Final program in many places in your own programs. Furthermore, some-
times try..except and try..finally blocks are nested in a single procedure. Remember to turn
Optimizations off when running this program.
The FINAL program contains a private field of TForm1 called GlobalString. At the start of the
method, this string is zeroed out and displayed in the edit control:
BitBtn1Click
try
GlobalString := ‘’;
Edit1.Text := GlobalString;
RunCalculation
except
on EDivByZero do
MessageDlg(‘You divided by zero again!’, mtError, [mbOk], 0);
end;
Edit1.Text := GlobalString;
This function forces an exception to be raised, but it does so inside a try..finally block. Nor-
mally, the code pointer jumps from the place where the exception is raised straight to the place
it is handled. The try..finally block, however, slows this process down long enough to en-
sure that the GlobalString variable will be set to a new value.
If there were code after the place where the exception is raised, that code would not be ex-
ecuted—and yet anything in the finally block would be executed. Consider this example:
j := 0;
try
k := i div j;
Label1.Caption := ‘Foo’;
finally
GlobalString := ‘RunCalculation called!’;
end;
Here, the code that changes the caption would not get executed, but the code that changes the
GlobalString would. If you changed the preceding code so that it first sets j to 1 instead of to
0, both the label and the GlobalString would be changed.
It’s probably worthwhile for me to make a few brief comments on execution speed. As a rule,
try..finally blocks are not particularly expensive in terms of clock cycles. As a result, you
should feel free to scatter them through your code with a good degree of liberalism. On the
other hand, try..except blocks can be fairly costly in terms of clock cycles, so you should show
some caution when deciding how often you want to insert them into your programs.
NOTE
In my opinion, it is always better to play it safe than to try to optimize your code to the
final and ultimate clock cycle. If you’ve been using a product like Visual Basic or
PowerBuilder, then the move to Delphi is going to give you huge gains in speed. Let
the developers put in all the optimizations; you should just concentrate on writing
programs that work! Of course, many expert programmers can afford to ignore this
advice, but I personally strive first to write reliable code, and I worry about optimiza-
tions only if I’m sure everything is already working correctly.
Listing 6.7. The code for the ONEXCEPT program shows how to create a generic
error handler for all exceptions raised in your program.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
DivByZero: TButton;
ReadError: TButton;
procedure DivByZeroClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure ReadErrorClick(Sender: TObject);
private
procedure HandleExcepts(Sender: TObject; E: Exception);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
The ONEXCEPT program will handle all exceptions in a single routine defined by the pro-
grammer. It is not meant as an example of how to write exceptions or how to construct a pro-
gram. However, it does provide advanced programmers with an illustration of how to use the
OnException event.
Normally, the code for an OnXXX handler is created for you automatically when you click the
Events page of the Object Inspector. In this case, however, TApplication never appears in the
Object Inspector, so you must manually create the call as a method of TForm1:
TForm1 = class(TForm)
...
procedure HandleExcepts(Sender: TObject; E: Exception);
end;
After declaring the procedure, you can assign it to the TApplication OnException property in
the FormCreate method:
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnException := HandleExcepts;
end;
The HandleExcepts method will now be called when an exception occurs as long as you don’t
declare any intervening try..except blocks. In the example shown here, the HandleExcepts
procedure explicitly handles EDivByZero errors but responds to all other errors through a ge-
neric handler:
procedure TForm1.HandleExcepts(Sender: TObject; E: Exception);
begin
if E is EDivByZero then
MessageDlg(‘EDivByZero’, mtError, [mbOk], 0)
else
MessageDlg(‘Other error’, mtError, [mbOk], 0)
end;
Needless to say, it is usually not a very good idea to create OnException handlers. You should
use them only if you are absolutely sure you know what you are doing and why you want to
do it.
Listing 6.8. The CreateExp program shows how to reraise an exception, and how
to ensure that allocated memory is properly deallocated.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
bAutoRaise: TButton;
bMemRelease: TButton;
procedure bAutoRaiseClick(Sender: TObject);
procedure bMemReleaseClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
The main form for this program has two simple buttons on it. The first is called bMemRelease
and the bAutoRaiseClick. If you press either of these buttons then code will execute which
demonstrates some important feature of exceptions.
Reraising an exception involves nothing more than making a simple call to the raise function.
The above example intentionally creates an EDivByZero error. (Be sure that Optimizations is
turned off so that this code will execute properly.)
After the exception is raised, the program performs some error processing, which in this case
involves nothing more than setting k to –1. Though setting k to 0 has no significant impact in
this program, it might be helpful in other programs as a way of signaling that k contains not a
valid result, but is instead in an error condition. In other words, setting k to –1 is a signal to the
rest of the program that k should not be used, and that some kind of error has occurred.
After setting k to 0, you may still want to tell the user what kind of error has occurred. One way
to do this would be to display a MessageBox containing your own string, or the string found in
the Message field of the EDivByZero object. However, you may not want to handle things this
way, particularly if you are not sure exactly which exception occurred. If this is the case, you
can first set k to –1, and then reraise the exception by calling raise. When you reraise the ex-
ception, the default Delphi handlers kick in, and the error will be reported to the user auto-
matically.
Reraising an exception allows you to get the best of both worlds. You get to perform some
custom handling, and you also allow Delphi to automatically inform the user exactly what has
gone wrong. One nice aspect of this kind of handling is that it frees you from trying to explic-
itly handle all possible exceptions. Instead, you can simply do some default handling that en-
sures your program knows about and handles a generic error condition. Once you have set
everything up, you can then let the system specify the exact type of error that occurred.
One of the most common uses for exceptions is to ensure that memory that has been allocated
will be deallocated:
procedure TForm1.bMemReleaseClick(Sender: TObject);
var
F: TFileStream;
S: PChar;
begin
F := TFileStream.Create(‘c:\autoexec.bat’, fmOpenRead);
try
GetMem(S, F.Size + 1);
F.Read(S^, F.Size);
S[F.Size] := #0;
MessageBox(Handle, S, ‘Info’, mb_Ok);
finally
F.Free;
end;
end;
In the example shown here, memory is allocated for the variable F. In a protected mode 32-bit
development environment there isn’t much chance that anything is going to go wrong while
an object is being created. In other words, it’s unlikely that you will run out of memory. If,
however, something does go wrong, then there are internal Delphi exception handlers which
will almost certainly catch the error, deal with it correctly, and then inform the user of what
went wrong.
So it’s fairly save to assume that the creation of the stream in the bMemReleaseClick method is
going to succeed. If it did happen to fail, you can rest assured that Delphi has internal routines
for handling and reporting the error. However, once the stream has been created, there is still
a likely chance that an error will occur while the function is attempting to read from a file. If
something did go wrong, Delphi would probably automatically catch the error. However, af-
ter the exception is raised, the program still needs to be sure that the memory for the stream is
deallocated. The purpose of the try..finally block in the above routine is to ensure that the
memory for the TFileStream object will be deallocated even if something goes wrong while
reading from the file.
In particular, if something goes wrong during the execution of this block of code:
GetMem(S, F.Size + 1);
F.Read(S^, F.Size);
S[F.Size] := #0;
MessageBox(Handle, S, ‘Info’, mb_Ok);
The mechanism that ensure that the Free method gets called is nothing more than a simple
try..finally block. This is a very common usage of the try..finally syntax, and it is one you
should be sure you understand.
The above code also demonstrates how to open up a simple TFileStream object and read in
some data. The newly collected data is then displayed to the user via the auspices of the
MessageBox function. Once the stream is no longer needed, it is destroyed by a call to Free.
The Create method for a stream takes two parameters. The first is a string specifying the name
of the file you want to read. The second is a constant that informs that system what kind of
operation you want to perform. The following are the constants associated with Delphi streams,
as found in the online help:
fmOpenRead Open the file for reading only
fmOpenWrite Open the file for writing only
Summary
In this chapter you learned about exceptions. Exceptions are a vital part of all well-written pro-
grams. However, you should remember that Delphi declares a large number of exceptions for
handling the errors that occur in your program. As a result, many errors will be handled grace-
fully without your intervention. For instance, the error popped up in the first EDivByZero ex-
ample shown in this chapter is perfectly suitable for many programs. That exception was raised
without you having to write special error-handling syntax.
Exceptions are one of the most important parts of Delphi’s syntax, and you should dedicate
considerable time to learning how they work. Overall, the mechanics, of exceptions are fairly
simple. However, there is an art to learning exactly when and how to use exceptions. Gaining
a skill in this art takes time, and will in most cases evolve over a period of months or years,
rather than hours or days.
II
IN THIS PART
PART
Win32
Part II of this book covers Win32. These pages include a long look at
multitasking, threading, and memory management. Specific information
about mutexes, critical sections, virtual memory, heaps, memory-mapped
files, and other Win32 programming issues are examined in both text and
in sample programs.
Multitasking and
Threads 7
If you want to create a process, call CreateProcess. This function does what the WinExec func-
tion did in Windows 3.1. WinExec is now obsolete, and you should use CreateProcess in its
stead.
Processes don’t do anything. They just exist. The thing that does something inside a program
is called a thread. Each program has at least one thread, and most programs that currently run
on Windows have only one thread. All Windows 3.1 programs by definition have only one
thread. The idea that each program can have its own thread, or that a program can have mul-
tiple threads, is specific to fancy operating systems like UNIX, OS/2, Windows NT, and Win-
dows 95.
When Windows loads a process, it simply opens up a memory-mapped file and sucks the con-
tents of an executable file or DLL into memory. Processes are inert. They don’t do anything.
Threads do things. Processes merely exist.
All processes have an HInstance associated with them. This parameter is declared globally in-
side all Delphi programs. You don’t have to do anything to get hold of this variable. It’s there
automatically at program startup. Object Pascal programs have an advantage over C programs
in that WinMain never has to be called, and the variables that are passed to it never have to be
pushed on the stack. Startup is very fast in an Object Pascal program, yet HInstance is there for
you as a global variable if you need it.
The HInstance of an application is the base memory address where it is loaded. In Windows
95, this value is typically 10×00400000, or 4,194,304. In other words, the program is usually
loaded in at the 4 MB mark. It then ranges from that point up to the 2 GB mark. This means
a Windows 95 program has 2 GB minus 4 MB worth of room in which to stretch out. Both
the program’s code and its data are loaded into this 2 GB address space. This means the oper-
ating system gives you virtually unlimited resources in which to run your program. (Your hard-
ware might not, however, share the operating system’s largesse.)
The specific value at which your program is loaded is under your control. If you select the Project
| Options | Linker page from the menu, you will see that this value is accessible to you as the
Image base.
NOTE
What does it mean to say that a program is loaded into a 4 GB address space? If you have only
a few megabytes of memory on your machine, what sense does it make to talk about such huge
amounts of memory?
The answer is that each program is assigned not an actual area in physical RAM, but only a
virtual address space. That is, it’s assigned a range of addresses. When you need to access that
memory, physical addresses are assigned to it. The act of assigning physical addresses to virtual
memory is called “mapping.”
Windows is constantly mapping virtual memory into physical address spaces. That is, it’s as-
signing physical addresses to virtual memory. Because of this system, people with only 8 MB
of memory on their system can have variables, or even entire processes, that have addresses in
the 3 to 4 GB range. Every program thinks it’s running in its own private 4 GB address space,
and that it can use 2 GB of this space as it pleases. What’s really happening in some cases is that
pages of that virtual 4 GB space are being mapped into the actual 8 MB of memory available
on your system.
NOTE
Because each process runs in its own 4 GB address space, the global hPrevInstance
variable from Windows 3.1 is meaningless in Win32. There can’t be a previous
instance of a program because each program is running in its own virtual world, and
there is only one virtual world in all the universe. (Actually, there is more than one
world inside a computer. But each process is convinced that it is alone, the only
virtual world in existence. Its world is 4 GB in size, guaranteed, and the inhabitants
of that world are prepared to prove it to you. And it’s the only one there is. Any sci-
ence fiction writers want to pick up this ball and run with it?)
A page of memory is typically 4 KB in size, which is 4,096 bytes. Windows 95 is always map-
ping pages of virtual memory into physical address spaces. When this happens, the address your
program sees does not change. There is another table that Windows maintains that translates
these virtual addresses into physical addresses. This is called indirection, or perhaps more accu-
rately, double indirection. (The pointer itself is one level of indirection, the virtual address a
second level.) The addresses that you see in your program are never direct references to real
memory. Instead they are indirect references. You see one address in your program, and then
Windows maintains a table that enables the virtual addresses you see to be mapped into physi-
cal memory that resides in RAM. RAM is physical memory; the addresses you see are virtual
memory. Virtual memory is not real memory any more than virtual reality is reality.
Why does Windows maintain this elaborate fiction of having virtual addresses? The main rea-
son is to protect one process from another. Each process is running in its own 4 GB address
space. It thinks it owns the whole computer. It has no idea that there are other processes on the
computer.
Because each process is totally separated from the other processes on the computer, there is
much less chance that they will overwrite one another. This means it is unlikely that an invalid
memory access by one program will overwrite another program, or overwrite some part of the
operating system. Each process is boxed away in a safe virtual world where it can’t get at any
other programs. This makes interprocess communication very different than it was in Win-
dows 3.1. However, it also makes it much less likely that your computer will crash.
Furthermore, when you close down a Win32 program, all the resources allocated to it are au-
tomatically closed out. By definition they can reside only inside that program’s one virtual world.
When the virtual world is closed out, all the resources associated with that process are also closed
out. If you forget to deallocate memory, or if you forget to deallocate resources, all your mis-
takes are covered up the moment you close your program. (This is not, however, a theory on
which to base program memory management! You should at least try to handle memory cor-
rectly. However, if you make a mistake, you are not permanently sullying the operating system’s
environment the way you did in Windows 3.1.)
NOTE
All 16-bit Windows programs that run on Windows 95 by default share the same
address space. It’s as if they are all running on their own miniature version of Windows
3.1. That means errors made by one 16-bit process can affect other 16-bit processes.
However, when you close out all the 16-bit applications, their virtual world disappears,
and all the resources dedicated to it are reclaimed. If one 16-bit program is giving you
trouble, don’t just close it. Close all the 16-bit programs in your Windows 95 session,
then reopen the ones that you feel are running properly. That one program dirtied the
water that all the other 16-bit programs used. The only thing to do in that case is to
close all the 16-bit programs, then create a new, pristine virtual 16-bit world by
reopening one of them. Remember, this is Windows 95 I’m talking about here, not
Windows NT.
That’s all I’m going to say about processes. The key point is that a process is a loaded program.
It’s not running; it’s not executing. Processes don’t run; they don’t execute. They just exist in
a vast 4 GB virtual address space. They have direct access to about 2 GB of that space, and the
operating system owns the rest. Threads run. Threads execute. Threads are the doers.
Each thread on the system has something called a context. A TContext record is a data structure
that contains information about the state of a thread. More specifically, it contains informa-
tion about the state of the registers in the CPU when that thread is running. If you want to see
one of these structures, look for the TContext declaration in WINDOWS.PAS:
PContext = ^TContext;
TContext = record
ContextFlags: DWORD;
Dr0: DWORD;
Dr1: DWORD;
Dr2: DWORD;
Dr3: DWORD;
Dr6: DWORD;
Dr7: DWORD;
FloatSave: TFloatingSaveArea;
SegGs: DWORD;
SegFs: DWORD;
SegEs: DWORD;
SegDs: DWORD;
Edi: DWORD;
Esi: DWORD;
Ebx: DWORD;
Edx: DWORD;
Ecx: DWORD;
Eax: DWORD;
Ebp: DWORD;
Eip: DWORD;
SegCs: DWORD;
EFlags: DWORD;
Esp: DWORD;
SegSs: DWORD;
end;
The record quoted here is stripped of all its comments, which are by far the most enlightening
part of the declaration. The comments are extensive, however, and of true interest to only a
very small number of readers. The rest of us can get by with only a quick glance at the general
lineaments of the structure.
For the moment, think of all the threads in a computer as if they were placed on a big wheel,
like the wheels that spin around at a gambling casino. (Now, now! That analogy is not meant
to be a commentary on the Windows 95 operating system!) As the wheel spins, there is a time
when each of the threads is near the top of the wheel. Starting a few degrees before it reaches
the top of the wheel, and extending a few degrees after, is the period of time when a thread is
actually executing. The moment the thread comes into that range, its context is loaded. That
is, the CPU register values that it stores in its TContext record are loaded into the physical CPU
found on the computer. Then that thread begins executing at the point where its instruction
pointer is currently located. It executes for a few cycles, until the wheel that owns it spins out
of range. Just before it goes out of range, its TContext record is updated with the current state
of the CPU.
This is one way to think of how multitasking works under Windows 95. The key point is that
each thread is given a time slice. It executes during the duration of its time slice, and then
another thread gets a chance. While it is running, the thread has its own unique set of register
values. When its time slice is over, the values are stored in a TContext record, and the next thread’s
CPU values are loaded into memory.
No two threads are ever running at the same time (unless you have more than one processor on
the machine). However, it seems to you as if they are all running at the same time. That’s
because computers are fast. Like most sleight-of-hand tricks, a computer’s multitasking capa-
bility is based on speed and dexterity. Computers don’t really do the impossible; they just seem
to be doing the impossible.
NOTE
It’s important to understand that threads can be assigned priorities. A thread with a
high priority gets called more frequently than a thread with a low priority. This means
you can (at least theoretically) start a thread that performs a task entirely in the
background. It never takes up any of the system’s time unless all the other threads are
idle.
There’s a lot of loose talk going on about Windows 95 right now. Some of it is valid. However,
don’t let anyone convince you that Windows 95 is not a real multitasking operating system.
This book teaches you how to write 32-bit programs. 32-bit programs that run on Windows
95 are really multitasked. (The 16-bit programs in their single address space are not really
multitasked, but you don’t have to run them if you don’t want to. Also, they can’t steal the
show entirely from 32-bit applications—only from each other. Therefore you are not so bad
off if you run only one 16-bit process at a time.)
NOTE
For what it’s worth, I don’t know any operating system that is truly crash proof. Some
UNIX systems have a reputation for being very solid, but somehow the ones I have
been connected to do seem to crash from time to time. Windows NT is also a very
stable operating system, but I have definitely managed to bring it weeping to its knees.
Windows 3.11 was a much more stable operating system than many people gave it
credit for, but still, one ill-behaved 16-bit application could knock the wind out of its
sails with relative ease. Windows 95 seems to lie somewhere between Windows 3.11
and Windows NT. If you have a new computer with 16 or more MB of memory on it,
Windows 95 is usually pretty stable, but it too will crash given the right set of circum-
stances. My point, once again, is that no operating system that runs on a PC is truly
crash-proof.
This new system of preemptive multitasking is better from two different perspectives. First, it
means that users have a better chance of recovering if something goes wrong. Second, program-
mers don’t have to work so hard to make sure that their programs are polite and well-behaved.
That’s enough theory for now. I discuss Win32 memory-related issues more carefully after you
get some hands-on experience with threads. Remember, the stuff I’m talking about here re-
gards advanced 32 programming issues. Intermediate programmers don’t have to know about
this stuff. However, you will be a better programmer if you do learn how processes, threads,
multitasking, and memory work.
you know it you have about twenty balls up in the air at the same time. The same is true of
threads. Just starting a single thread is simple. Not quite as simple as a blank form Delphi RAD
application, but still simple.
It’s only when you get multiple threads running at the same time, and only when they are
concurrently trying to access the same data, that things can get a bit more complex. Thread
synchronization, thread local storage, and related topics do have their complex sides. How-
ever, I think you will be surprised at how easy it is to add threads to your programs. Even some
of the more advanced thread synchronization-related topics such as critical sections and mutexes
are both fairly simple, at least in principle.
As usual, it’s hard to get specific without having an example program to examine. Listing 7.1
shows how to create a simple thread. Go ahead and run the program, then come back and read
about how it works.
Remember: Don’t worry about this one. Basic thread theory is simple.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
bUseThread: TButton;
bNoThread: TButton;
procedure bUseThreadClick(Sender: TObject);
procedure bNoThreadClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
continues
if hthread = 0 then
MessageBox(Handle, ‘No Thread’, nil, MB_OK);
end;
end.
When this program begins, it displays the simple window shown in Figure 7.1. There are two
buttons on the main window. If you select the one labeled Use Thread, the program counts
from 1 to 100,000 using a thread. While this process is going on, you can use the mouse to
move or resize the main window. If you press the second button, the program also counts from
1 to 100,000. However, this second counting process is not run in a separate thread, so you
cannot move or resize the window until the loop that performs the counting is finished.
FIGURE 7.1.
The Thread1 program
counts from 0 to 100,000
when the user selects either
button on its main form.
The structure of the Thread1 program is very simple. There are two button response methods.
The first runs a function called ThreadFunc as a separate thread, and the second runs the same
function as part of the main thread of the program. If you press the first button, the program
has two threads running simultaneously; if you press the second button, the program has only
its main thread running.
if hthread = 0 then
MessageBox(Handle, ‘No Thread’, nil, MB_OK);
This code does nothing more than attempt to create a thread. If something goes wrong, it pops
up a message box informing the user that there has been an error.
This is the CreateThread syntax:
function CreateThread(
lpThreadAttributes: Pointer; // address of thread security attributes
dwStackSize: DWORD; // size of stack for thread
lpStartAddress: TFNThreadStartRoutine; // address of thread function
lpParameter: Pointer; // argument for new thread
dwCreationFlags: DWORD; // creation flags
var lpThreadId: DWORD): // Returned thread identifier
THandle; stdcall; // Returns handle to thread
The first parameter takes a series of security attributes. If this parameter is nil, the default se-
curity attributes are used. On Windows 95, it is standard to set this parameter to nil. The only
time you might want to vary from this pattern is if you want child processes to inherit the thread.
If the second parameter for the thread is 0, the stack size for the thread is the same as the stack
size for the application. In other words, the primary thread and the thread you are starting both
have stacks that are the same size. The stack automatically grows, if necessary. In short, you
can usually set this parameter to 0.
The lpStartAddress parameter is the most important portion of the function call. It is where
you specify the name of the thread function that is called when the thread begins execution.
Just enter the name of the function in this field and put an at sign (@) in front of it.
WINDOWS.PAS declares a TFNThreadStartRoutine like this:
TFarProc = Pointer;
TFNThreadStartRoutine = TFarProc;
In short, you can pass in just about anything in this parameter. However, if you want your call
to succeed, you should pass in the address of function like the ThreadFunc routine described
earlier.
If you want to pass a parameter to your function, you specify it in lpParameter. Typically, you
create a structure and pass in its address in this parameter. The variable you use does not have
to be a structure—it could be a string or some other type of variable.
dwCreationFlags enables you to pass in certain flags that are associated with your thread. The
Microsoft documentation I have seen so far specifies only one possible flag for this field. That
flag is called CREATE_SUSPENDED. If you create a suspended thread, the thread itself is created, its
stack is created, and its TContext structure is filled with CPU values, but the thread is never
assigned any CPU time. It’s all set to go, but it won’t execute until you call ResumeThread. If
you want, you can then suspend the thread again by calling SuspendThread. This whole subject
of suspended threads is really an advanced topic relating to thread synchronization. For now,
you should just pass in 0 in this parameter.
The final parameter, lpThreadID, holds a var parameter that is assigned a unique ID by the
system. On Windows 95 this parameter can be 0, and indeed most of the time you won’t use
the thread ID, so you might have an inclination to set it to 0. On Windows NT, however, this
parameter cannot be 0. If you are interested in portability between Windows 95 and Windows
NT, you should not set this value to 0. In other words, if your application goes into general
release, you should pass in 32-bit DWORD and should not set this value to 0. Remember, you are
not passing anything to Windows in this parameter, rather you are passing in a variable that is
assigned a value by Windows.
Here’s an example:
var
hThread: THandle;
ThreadID: DWord;
begin
CreateThread(nil, 0, ThreadFunc, nil, 0, ThreadID);
After you have created a non-suspended thread, the function you passed to it is automatically
called. In other words, after you create a thread, the function representing the entry point for
that thread is called almost immediately. Here is the thread for the Thread1 program:
It would not be safe to use the Canvas object for Form1 in this routine, because most of the
visual objects in the VCL are not thread-safe. As you will see in the discussion of the TThread
object, there is a way to safely use the VCL inside threads. However, as a general rule, you should
consider using the Windows API exclusively when working with threads. This does not mean
that your program cannot be VCL-based. In fact, there is no restriction on the number or kind
of VCL objects you use in multithreaded programs. However, if the code in one of your threads
wants to talk to a VCL object, it should use the Windows API, and in particular, it should use
SendMessage or, occasionally, PostMessage.
NOTE
The six chapters of this book numbered Chapters 9 through 14 are dedicated to a
discussion of the Windows API. In those chapters, and particularly in Chapter 14, you
learn about using SendMessage and PostMessage to talk to Windows controls. This
subject is also broached on several occasions in this chapter.
I want to emphasize that the TThread object provides a way to call the VCL from inside
threads. That object is explored later in this chapter. Furthermore, certain key com-
mands, such as TQuery.Open, are safe to perform inside threads. Lastly, if you have a
little knowledge of the Windows API, you can talk to your Delphi VCL objects using
the SendMessage command. This enables you to build complex VCL-based applica-
tions, and to use threads inside them where necessary. One of the big goals of this book
is to provide you with sufficient understanding of the VCL to use it with threads, and
to use it in the countless other places in Windows development where that kind of
knowledge can prove invaluable.
One last comment needs to be made in this context. There are many parts of the VCL
that you can use inside a thread function. It’s mostly just TComponent descendants that
you need to watch out for, and then only within certain parameters. This topic is
addressed in more depth in the section on the TThread object.
The routine shown earlier first gets the device context from the system, then uses the device
context to write 100,000 short strings to the surface of the form. When the loop is finished,
the device context is returned to the system.
While this function is operating, there are two threads running simultaneously in the Thread1
program. If you resize the main window, or if you move it, then you are using the main thread
for your program. The code that increments and paints numbers to the screen is part of the
second thread. You can switch back and forth between the two threads just by moving the form
or resizing it.
If you call the ThreadFunc routine directly, rather than as a thread, you cannot move or resize
the main window until the loop is finished. That is because the program is entirely occupied
with the loop and has no second thread available to handle other tasks. The point of this ex-
ample program is to show how you can get one program to do two things at the same time.
Part of the program is painting numbers to the screen, but the other part is free to handle resizing
or any other task you might give it.
As you can see, there isn’t anything very complicated about threads when you are looking at
them from the most elemental level. What’s complicated is when you have multiple threads
trying to access the same data at the same time. When that happens, you need to find a way to
get the threads to synchronize with one another, so that they are working together and not
working at cross purposes.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Menus;
const
Margin = 20;
type
PData = ^TData;
TData = record
XPos: Integer;
YPos: Integer;
end;
TForm1 = class(TForm)
MainMenu1: TMainMenu;
Start1: TMenuItem;
Edit1: TEdit;
bSquare: TButton;
procedure FormPaint(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure StartMenu(Sender: TObject);
procedure FormResize(Sender: TObject);
procedure bSquareClick(Sender: TObject);
private
EarthMap: HBitmap;
procedure DrawBitmap(PaintDC: HDC; Bitmap: HBitMap;
XVal, YVal: Integer);
end;
var
Form1: TForm1;
AWidth, AHeight: Integer;
implementation
uses
MathBox;
{$R *.DFM}
{$R earth.res}
continues
New(Data);
Data^.xPos := 0;
Data^.YPos := AHeight;
hThread2 := CreateThread(nil, 0, @ThreadFunc,
Data, 0, ThreadID);
New(Data);
Data^.xPos := AWidth;
Data^.YPos := AHeight;
hThread3 := CreateThread(nil, 0, @ThreadFunc,
Data, 0, ThreadID);
end.
When you first run this program, a bitmap appears in the upper-left corner of the screen. If
you select the program’s sole menu item, three other copies of this bitmap are slowly painted
in, as shown in Figure 7.2. There is also a button and edit control in the upper-right corner of
the screen. You can enter data in this edit control and then press the button to find its square
root.
FIGURE 7.2.
The Thread2 program after
three copies of the original
bitmap were made by three
separate threads.
What’s interesting about the program is that you can perform mathematical calculations at the
same time that the program is very busy painting three bitmaps to the screen. The program
appears to be doing four things at once, which would have been impossible under Windows
3.1. (You could have faked this same kind of capability in Windows 3.1 by using a series
of PostMessage commands, but that kind of code is awkward to implement and difficult to
maintain.)
You can compile it by entering the following command on the DOS prompt:
brcc32 -r earth.rc
EARTH.RC is the name of the RC file. Note that the resource file that will be created is explicitly
declared in the source:
{$R earth.res}
Don’t try to combine EARTH.BMP with the default THREAD2.RES resource file. Let Delphi
handle THREAD2.RES, and you can create your own resource.
NOTE
If do not have any experience with resource files, this subject is covered in some depth
in Chapter 10, “Dialogs and Resources.” The preceding resource file is so simple that
you are unlikely to have trouble with it. However, if you can’t get it to compile, just
use the copy of the EARTH.RES file that ships with this book. If you can’t find
EARTH.RES, you could also try copying EARTH.RC and EARTH.BMP into the
Delphi 2.0\bin subdirectory, and then running BRCC32.EXE from there. Once again,
the whole subject of compiling from the command line is covered in Chapter 10.
Here is the code that loads the bitmap from the resource file:
EarthMap := LoadBitmap(hInstance, ‘Earth’);
GetObject(EarthMap, sizeof(Windows.TBitMap), @BStruct);
AWidth := BStruct.bmWidth;
AHeight := BStruct.bmHeight;
The LoadBitmap function is a Windows API routine that snags a bitmap from the resource that
resides in the executable whose hInstance you pass in the first parameter. The name you gave
the bitmap in the resource file is passed in the second parameter. For instance, here is the code
for the resource:
Earth BITMAP “earth.bmp”
In this case you called the resource Earth, which is the first word in the preceding line. You
therefore retrieve this resource by passing the word Earth in the second parameter to LoadBitmap.
The call to GetObject retrieves the following structure, declared in WINDOWS.PAS:
TBitmap = packed record
bmType: Longint; // Type of bitmap, always zero
bmWidth: Longint; // The width in pixels of the bitmap
bmHeight: Longint; // The height in pixels of the bitmap
bmWidthBytes: Longint; // Number of bytes in each scanline
bmPlanes: Word; // The number of color planes
bmBitsPixel: Word; // Number of bits per pixel
bmBits: Pointer; // A pointer to the actual bits
end;
In this case, the Thread2 program is concerned only with the width and height of the current
bitmap, so it saves those values in global variables called AWidth and AHeight.
This original bitmap from the resource is shown in the upper-left corner of the screen. It is
placed there by a call to DrawBitmap from the OnPaint handler:
procedure TForm1.DrawBitmap(PaintDC: HDC; Bitmap: HBitMap;
XVal, YVal: Integer);
var
MemDC: HDC;
OldBitmap: HBitmap;
begin
MemDC := CreateCompatibleDC(PaintDC);
OldBitmap := SelectObject(MemDC, Bitmap);
BitBlt(PaintDC, XVal, YVal, AWidth,
AHeight, MemDC, 0, 0, SRCCOPY);
SelectObject(MemDC, OldBitmap);
DeleteObject(MemDC);
end;
This is the traditional method of painting a bitmap, and it is relatively fast. There is, however,
no reason not to use a Delphi TBitmap object if you find that approach easier. The preceding
code starts by creating a device context compatible with the screen’s device context:
MemDC := CreateCompatibleDC(PaintDC);
This means that it creates a device context that knows how to work with your video card and
your monitor.
The next step is to select the bitmap into this newly created device context:
OldBitmap := SelectObject(MemDC, Bitmap);
Notice that the code saves the original bitmap stored in the new device context. This original
bitmap is probably only a very small one pixel by one pixel image, but you need to conserve the
resources associated with the bitmap.
The next step is to Blt the bitmap from the newly minted device context to the screen’s device
context:
BitBlt(PaintDC, XVal, YVal, AWidth,
AHeight, MemDC, 0, 0, SRCCOPY);
After blitting the bitmap to the screen, you need to first restore the old bitmap into the newly
created device context
SelectObject(MemDC, OldBitmap);
and then dispose of the new device context because it is no longer needed:
DeleteObject(MemDC);
Remember, you wouldn’t ever want to dispose of the screen device context, because it is going
to be needed throughout the life of the program.
The preceding call to DrawBitmap could be easily converted into a general purpose utility rou-
tine by changing the header so that it accepted the width and height of the bitmap:
procedure DrawBitmap(PaintDC: HDC; Bitmap: HBitMap;
XVal, Yval, AWidth, AHeight: Integer);
A copy of this procedure is found on disk in UTILBOX.PAS from the UNITS subdirectory.
for j := 0 to AHeight do
for i := Margin to AWidth + Margin do begin
P := GetPixel(DC, i, j);
SetPixel(DC, i + Data^.Xpos, Data^.YPos + j, P);
end;
This time-consuming process has the side benefit of being fun to watch.
So now you understand the core of the Thread2 program. A bitmap is blitted to the screen.
Three threads are started. Each of the threads laboriously copies the original bitmap to the screen,
one pixel at a time.
You probably noticed that the Thread2 program also created a button and an edit control. If
you enter a number in the edit control, you will find that pressing the program’s button gives
you its square root. In fact, the program starts out with an outrageously high number (4 GB)
in the edit control. If you press the button several times, this number has its square root taken
multiple times, until you start approaching the number 1.
What’s interesting about the Thread2 program is not that it knows how to get a square root.
(It’s a given that a computer can do that for you.) What’s amazing is that you can be asking it
to calculate square roots at the same time that all three threads are busy copying the original
bitmap.
The point here is that there is no apparent degradation in the way the button and edit control
respond. The computer is obviously very busy copying pixels, yet it always lets you type in
numbers and calculate square roots almost exactly as it would if you had the entire CPU to
yourself. This is what preemptive multitasking is all about. When the time slices for the sec-
ondary threads are up, the processor is given over to the other threads on the system. No delay
or degradation of performance is apparent to the user. The computer appears to be doing at
least four things at the same time. In fact, for all practical purposes, it is doing four things at
the same time!
The Thread2 program takes advantage of the lpParameter parameter of CreateThread, as shown
by the Data parameter in the following code sample:
hThread1 := CreateThread(nil, 0, @ThreadFunc,
Data, 0, ThreadID);
The thread function is called ThreadFunc, and the parameter passed to it is of type PData:
PData = ^TData;
TData = record
XPos: Integer;
YPos: Integer;
end;
The x and y fields of this structure are used to designate the point at which each thread should
start its copy of the picture.
Now here’s the interesting part. Suppose you declared Data to be a global variable. Then sup-
pose you assigned this global variable a starting x and y location for a picture, and passed it to
a thread. Then suppose you assigned this same variable a new value and started a new thread.
When you assigned the global variable a new value, it automatically changed the value passed
to the first thread. This means that the first thread would now start painting at the place where
the second picture should be, rather than in the place you originally asked it to paint. (One
way around this type of problem is to use something called thread local storage, but that is be-
yond the scope of this book.)
As you saw in the last paragraph, simply declaring a global variable is not a solution to the
problem presented by these three threads. A second possible idea would be to declare the Data
variable as local to the StartMenu method, which is the method called when the Start menu
item is selected. However, if you did this you would get into even deeper trouble. Specifically,
you would assign the variable a value, then pass it to the thread. At that point the StartMenu
method would go out of scope, and the value that the thread was trying to use would suddenly
disappear from underneath it. So this alternative won’t work either.
A third alternative, and the one used in this program, involves using pointers. First allocate
memory for a variable, then pass it to the first thread. Next allocate memory for a second vari-
able, and pass it to the second thread. Finally, you could allocate memory for a third variable,
and pass it in to the third thread. Each thread is responsible for disposing the memory passed
in to it. Each variable would of course contain a different set of coordinates:
procedure TForm1.StartMenu(Sender: TObject);
var
hThread1, hThread2, hThread3: THandle;
ThreadID: DWORD;
Data: PData;
begin
New(Data);
Data^.xPos := AWidth;
Data^.YPos := 0;
hThread1 := CreateThread(nil, 0, @ThreadFunc,
Data, 0, ThreadID);
New(Data);
Data^.xPos := 0;
Data^.YPos := AHeight;
hThread2 := CreateThread(nil, 0, @ThreadFunc,
Data, 0, ThreadID);
New(Data);
Data^.xPos := AWidth;
Data^.YPos := AHeight;
hThread3 := CreateThread(nil, 0, @ThreadFunc,
Data, 0, ThreadID);
The preceding code calls New three times, once for each copy of the PData structure. The calls
to New allocate memory for the PData structure. The code assigns values to the structure’s fields
and then passes them into the appropriate thread.
Given the scenario outlined earlier, each thread is then responsible for deallocating the struc-
ture passed in to it:
function ThreadFunc(Ptr: Pointer): LongInt; stdcall;
var
i, j: Integer;
P: TColorRef;
DC: HDC;
Data: PData;
begin
Data := PData(Ptr);
DC := GetDC(Form1.Handle);
for j := 0 to AHeight do
for i := Margin to AWidth + Margin do begin
P := GetPixel(DC, i, j);
SetPixel(DC, i + Data^.Xpos, Data^.YPos + j, P);
end;
ReleaseDC(Form1.Handle, DC);
Dispose(Data);
end;
The relevant call in this case is the last one, where the Data variable is disposed of.
The process described here shows one way of handling data that is passed in to a thread. The
issue here is that you have one thread but create three instances of it. This means you have to
create three separate instances of the data you pass in to it.
As it turns out, the basic problem of how a thread handles data is one that has many implica-
tions. As a rule, the issues surrounding this subject are categorized under a topic called thread
synchronization. The reason they are given this name becomes clear in just a moment.
Critical sections solve the problem described earlier by effectively blocking the second thread
from accessing the sensitive data when it is being used by the first thread. In particular, it stops
the second thread from executing by ensuring that it doesn’t get any time slices. The parallel in
the database world would be locking a record, but the analogy is not exact.
The mechanisms involved in this process are not hard to understand. Critical sections are a
simple solution to what sounds like a fairly complex problem. Go ahead and get the CritSect
program, shown in Listing 7.4, up and running. After you read the description of how it works,
you’ll see that this whole issue is easy to resolve.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Menus;
const
TotalCount = 20;
type
TForm1 = class(TForm)
ListBox1: TListBox;
ListBox2: TListBox;
MainMenu1: TMainMenu;
Options1: TMenuItem;
RunThread1: TMenuItem;
CritSects1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure RunThread1Click(Sender: TObject);
procedure CritSects1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
var
CritSects: Boolean;
Sect1: TRTLCriticalSection;
GlobalData: Integer;
/////////////////////////////////////////////////////////
// The thread routine
/////////////////////////////////////////////////////////
function ThreadFunc1(P: Pointer): LongInt; stdcall;
var
i, j: Integer;
S: string;
begin
Form1.ListBox1.Items.Clear;
/////////////////////////////////////////////////////////
// The thread routine
/////////////////////////////////////////////////////////
function ThreadFunc2(P: Pointer): LongInt; stdcall;
var
i, j: Integer;
S: string;
begin
Form1.ListBox2.Clear;
Result := 0;
end;
continues
end.
The CritSect program has a single window with two list boxes and a menu. If you select the
Run Thread menu item, the list boxes are filled with numbers, as shown in Figure 7.3.
FIGURE 7.3.
The CritSect program as it
appears when its threads
are not synchronized.
As you can see, the two columns of numbers are not identical. For reasons that are explained in
just a moment, this is the way the program appears when its threads are not synchronized. If
you selected the Toggle CritSections menu and then choose Run Thread a second time, you
see that the two columns are now identical in appearance, as shown in Figure 7.4. For reasons
that are explained shortly, this is the way program looks when its threads are synchronized.
FIGURE 7.4.
The CritSect program as it
appears when its threads
are synchronized.
The CritSect program has a single global variable called GlobalData, which is of type Integer.
Each thread in the program manipulates this global data and then restores it to its original value.
For instance, the first thread adds 3 to this number, then sets a second variable called i to the
new value GlobalData - 3, and then shows the number in the first list box:
Inc(GlobalData, 3);
i := GlobalData - 3;
S := Format(‘Information: %d’, [i]);
SendMessage(Form1.ListBox1.Handle, lb_AddString, 0, LongInt(S));
Dec(GlobalData, 3);
If you add 3 to a number, and then subtract 3 from that same number, it should be restored to
its original value. In fact, one would have to be living in a very non-Euclidean, non-linear world
if adding 3 to a number and then subtracting it out did not take you back to the original num-
ber. 100 plus 3 equal 103, and 103 minus 3 equals 100. That’s all there is to it, and there’s no
point in talking about the matter any further—except for the fact that when the CritSect pro-
gram tries to do this, sometimes it gets 100, and sometimes it gets 97.
NOTE
In the example shown above, I can typecast S as a LongInt because it is a long string.
You cannot use the exact same syntax with ShortStrings, the issue being that
ShortStrings have a length byte at position zero. This length byte would confuse
Windows.
The other thread in the program, the one that writes to the second list box, does the exact
opposite thing. It subtracts 3 from 100, and then adds 3 back to the new number. Obviously
this process would have to yield 100 as a result each and every time. That’s the way the uni-
verse is put together, and that’s the way things have to be. Only it doesn’t turn out that way.
Instead of getting the result you would expect, there are times when you do not get 100, but
103. Clearly there is something very wrong here.
What’s going on is not some metaphysical curiosity, but only the kind of complication that
ensues when two threads are accessing and manipulating the same data. Specifically, one thread
adds 3 to the global data, thereby setting it equal to 103. Then the second thread accesses the
data and subtracts 3 from it, thereby setting it equal to 100. When the second thread then adds
back the 3 that it took away, the result is not 100, but 103. Or conversely, when the first thread
subtracts 3 back out, it gets not 100, but 97. It’s all a matter of timing.
As you can see, what’s happening here is the same type of problem that I described when I
talked about two threads simultaneously accessing the same record. The problem is one of syn-
chronization. One thread needs to be able to tell the other that the global data is in a sensitive
or critical state and that it shouldn’t be touched!
Clearly, this is what critical sections are all about. They are a way of designating that a particu-
lar piece of data is currently off-limits and can’t be touched.
The implementation of the solution to this problem is extremely literal minded. What you do
is mark the blocks of code that manipulate the data as “critical sections.” Before the code in
these sections can be executed, the computer has to check a global record that records whether
another thread is currently in the middle of a critical section. (Furthermore, it checks to see if
this critical section is related to the first critical section. That is, you could have different sets of
critical sections in the same program.)
Here’s how it works. In the OnCreate handler, the CritSect program initializes a variable of
type TRTLCriticalSection:
Sect1: TRTLCriticalSection;
InitializeCriticalSection(Sect1);
NOTE
It’s not really important what TRTLCriticalSection looks like, but you can in fact find
the structure defining its type in WINDOWS.PAS:
TRTLCriticalSection = record
DebugInfo: PRTLCriticalSectionDebug;
// The following three fields control entering
// and exiting the critical
// section for the resource
LockCount: Longint;
RecursionCount: Longint;
OwningThread: THandle;
LockSemaphore: THandle;
Reserved: DWORD;
end;
...
LeaveCriticalSection(Sect1);
The moment EnterCriticalSection is passed Sect1, it sets one or more of its field to reflect
the fact that a critical section is active. Now, if a second critical section is called by another
thread, EnterCriticalSection can see that a critical section is active, so it puts the second thread
to sleep:
EnterCriticalSection(Sect1);
...
// Second swatch of Critical code appears here ...
...
LeaveCriticalSection(Sect1);
While the second thread is asleep, it does not get any time slices. It’s marked as inactive and the
CPU wastes no time trying to call it. This is an extremely efficient system, and very few clock
cycles are wasted by a process that has been put to sleep.
When the first critical section exits, LeaveCriticalSection is called. The variables in Sect1 are
then changed, and the second thread is free to execute. The second thread immediately enters
its own critical section, thereby preventing the first thread from executing the offending code.
At this stage, the two critical sections in this program end up swapping back and forth, with
each given a chance to execute while the other waits.
By using the process described earlier, you can mark the areas where a thread accesses the
GlobalData variable as critical. Therefore, only one thread is able to access the data at a time.
This effectively solves the problem. When the critical sections are active, the program always
produces the results you would expect, as shown in Figure 7.4.
When you are through using a variable of type CRITICAL_SECTION, you should always delete it:
DeleteCriticalSection(Sect1);
This is an important step because it frees up system resources. However, you want to be sure
that you do not call EnterCriticalSection or LeaveCriticalSection with an invalid variable.
To do so causes an exception.
I have spent a fair amount of time on critical sections only because they are probably a new
concept to most readers. They were new to me when I first saw them in Windows 95. As a
result, I have tried to explain them in some depth, even though the concepts behind them are
quite simple. As I said earlier, the whole issue of synchronization at least seems like a fairly
complex problem. However, the solution to the problem is very simple. Critical sections are
easy to use.
or more threads in a single application, but two or more threads that might reside in separate
applications.
Mutexes get their name from the words “mutually exclusive.” Only one thread can own a mutex
at a time. If a thread owns that mutex, the mutex is said to be signaled. If no thread owns the
mutex, the mutex is said to be non-signaled. (This is confusing terminology for a very simple
concept. If you want, you can just think of the mutex as being owned or free.)
To create a mutex, you call CreateMutex. Here is the CreateMutex syntax:
function CreateMutex(
lpMutexAttributes: PSecurityAttributes; // address of security attributes
bInitialOwner: BOOL; // flag for initial ownership
lpName: PChar): // address of mutex-object name
THandle; stdcall; // If Result = 0, use GetLastError
The first parameter is a structure that defines the security attributes for the mutex. If you set
this field to nil, the mutex is assigned a default set of security attributes. For more information
on this field, see the online help.
The second parameter specifies whether the mutex is owned by the calling thread. An owned
mutex is signaled, and an unowned mutex is non-signaled. You can use this parameter to specify
whether the mutex should go immediately into the signaled state.
The final parameter is used to give the mutex a name. If one process creates a mutex and gives
it a name, then a second process attempts to create or open a mutex of the same name, it gets
the same mutex back in return. The two processes can then use the mutex to synchronize their
actions. You can pass in nil in this parameter if you like. This produces an unnamed mutex.
Here’s an example:
hMutex := CreateMutex(nil, FALSE, nil);
Listing 7.5 shows the Mutex program. It is nearly identical to the CritSect program. The only
difference is that the Mutex program uses mutexes rather than critical sections.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Menus;
const
TotalCount = 20;
type
TForm1 = class(TForm)
ListBox1: TListBox;
ListBox2: TListBox;
MainMenu1: TMainMenu;
Options1: TMenuItem;
RunThread1: TMenuItem;
UseMutex1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure RunThread1Click(Sender: TObject);
procedure UseMutex1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
var
UseMutex: Boolean;
hMutex: THandle;
GlobalData: Integer;
/////////////////////////////////////////////////////////
// The thread routine
/////////////////////////////////////////////////////////
function ThreadFunc1(P: Pointer): LongInt; stdcall;
var
i, j: Integer;
S: string;
begin
Form1.ListBox1.Items.Clear;
/////////////////////////////////////////////////////////
// The thread routine
/////////////////////////////////////////////////////////
function ThreadFunc2(P: Pointer): LongInt; stdcall;
var
i, j: Integer;
S: string;
begin
Form1.ListBox2.Clear;
continues
Result := 0;
end;
end.
As I stated earlier, the Mutex and CritSect programs are nearly identical. In fact, from the user’s
point of view, their behavior is indistinguishable.
Mutex calls CreateMutex in the part of the OnCreate handler where CritSect called
InitializeCriticalSection. Instead of calling EnterCriticalSection, the Mutex program calls
WaitForSingleObject. Instead of calling LeaveCriticalSection, the Mutex program calls
ReleaseMutex:
WaitForSingleObject takes two parameters. The first is the handle to a mutex, and the second
is the number of milliseconds the function should wait before returning. WaitForSingleObject
returns only if the mutex becomes signaled, or if the time specified has elapsed. That is, it re-
turns when it owns the mutex or when the time interval specified in the second parameter elapses.
If you pass in the INFINITE flag in this parameter, the routine returns only when the mutex is
signaled. (There is a function called WaitForMultipleObjects. The name says it all, although
you can look up the particulars in the online help.)
When a thread is finished with a mutex it should call ReleaseMutex, which takes a handle to a
mutex as a parameter. After the call to ReleaseMutex, the mutex is free, and other code that has
been waiting to own the handle can now execute. It’s a bit like passing a baton between teams
of runners. The ones that own the mutex run, and the others wait for it to be passed to them.
When you are completely done with a mutex and don’t need it any more, you should close it:
CloseHandle(hMutex);
NOTE
Both the Mutex and the CritSect program call the Sleep function. Sleep suspends the
execution of a thread for a specified number of milliseconds. A value of INIFINITE
causes the thread to sleep for an indefinite period of time.
I call Sleep in the Mutex and CritSect program because I want to exaggerate the two
threads’ innate lack of synchronicity. In other words, I use this call to force the two
threads out of sync so that it is almost certain that they will not both normally print
out 100 each time. Because I use the function, the chance of coincidental synchronicity
between the two threads is low. This means I have to use critical sections or mutexes to
get them in line.
NOTE
Code that is not thread-safe is code that will be corrupted if it gets called at random
times. The CritSect program, when run without the CritSects optioned turned on, is
not thread safe. The data it uses because corrupt, as you can see from the out put in the
listboxes. When you turn CritSects on, the program becomes thread safe, and the data
displayed in the listboxes is valid. This is, however, a relatively benign example of non-
thread-safe code. If you try to call the visual elements of VCL from inside a thread, you
will almost certainly end up crashing your program sooner or later. The crash will not,
however, necessarily occur right away.
As a rule, the only way you can call the VCL inside a thread is to use the Synchronize method.
In particular, Synchronize performs some hand waving that essentially makes your thread a
temporary part of the main thread of the application. During this time, you can access the VCL.
The moment you don’t need the VCL anymore, you should break out of the synchronized
section of your code. Your program will then once again have multiple threads.
The point is that most of the VCL is not thread-safe, so you can’t access it while you are in a
thread. Period. That’s it. The solution, then, is to call Synchronize, which in effect puts a
temporary end to you thread and makes it part of the main thread for the application. During
this time, you can call the VCL. The moment your call to the VCL is finished, you should
break out of the synchronized section of your code. The function gets its name because it syn-
chronizes your thread with the main thread of the application.
In general, descendants of TComponent are not thread-safe. There are, however, some big parts
of the VCL that do not descend from TComponent. For instance, there are no TComponent de-
scendants anywhere in CLASSES.PAS.
NOTE
This is one of the early occasions in this book when I am going to remind you of the
importance of having the source to the VCL. I return to this theme a number of times,
but the key point to grasp is that really good Delphi programmers almost all have a
copy of the VCL. The VCL ships with some versions of Delphi, and it is available as
an up-sell directly from Borland or possibly from some third-party shops such as the
Programmers Shop (1-800-421-8006) or Programmer’s Paradise (1-908-389-9228). It
is also possible that you have some files that have an INT extension on your system.
Look for CLASSES.INT, for instance. This file would have the interface to
CLASSES.PAS, but not the implementation.
It’s safe to use these classes, as well as TObject or the exception classes, in a thread. You could
even design your own thread-safe TComponent descendants if you were very careful and knew
exactly what you are doing.
If you think about it for a moment, it becomes obvious that at least part of the VCL has to be
thread-safe. For instance, the TThread object itself is part of the VCL, and obviously it is thread-
safe. It’s just a question of learning what you can and can’t do.
NOTE
Most of the rest of the VCL is out of bounds, with yet a few more key exceptions. One of the
most important exceptions is calling the open methods of TQuery, TDatabase, and TTable ob-
jects. If you create a session in a thread using the TSession object, you could hook a TTable
object to it and open that table in a thread. While you are opening the table, you should not
have the TTable object attached to anything else, including a TDataSource. The moment the
open procedure finishes, you could hook up the thread to a grid in a Synchronize method.
The point here is that you could compose a complex query, put it in the SQL property of a
TQuery object that is part of a thread, and then call Open. While all those calculations are going
on in the background, you could use the main thread of your program, or other threads of
your program, to interact with the user. The moment the SQL statement was through execut-
ing, you could pop the results into a grid or other data-aware control and show them to the
user.
Listing 7.6. The Thread1a program shows how to use the VCL object called
TThread.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
MyThrd1, MyThrd2, Buttons, ExtCtrls;
const
wm_ThreadDone = wm_User + 0;
type
TForm1 = class(TForm)
bGoodThread: TButton;
bBadThread: TBitBtn;
Memo1: TMemo;
Panel1: TPanel;
procedure bGoodThreadClick(Sender: TObject);
procedure bBadThreadClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure GoodThreadDone(Sender: TObject);
procedure BadThreadDone(Sender: TObject);
private
BadThread: TBadThread;
T2: TMyThread2;
procedure ButtonsOff(Setting: Boolean);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
continues
end.
interface
uses
Classes, Windows;
type
TBadThread = class(TThread)
private
procedure RunUpdate;
protected
procedure Execute; override;
end;
implementation
uses
Main, SysUtils;
{ TBadThread }
procedure TBadThread.RunUpdate;
var
i: Integer;
begin
for i := 1 to 20000 do begin
Form1.Panel1.Caption := IntToStr(i);
Form1.Panel1.Update; // Wouldn’t have to do this in good thread
end;
end;
procedure TBadThread.Execute;
begin
Windows.InvalidateRect(Form1.Handle, nil, True);
Synchronize(RunUpdate);
PostMessage(Form1.Handle, wm_ThreadDone, 1, 0);
end;
end.
interface
uses
Classes;
type
TMyThread2 = class(TThread)
private
FCount: Integer;
procedure RunUpdate;
protected
procedure Execute; override;
end;
implementation
uses
Main, SysUtils, Windows;
procedure TMyThread2.RunUpdate;
begin
Form1.Panel1.Caption := IntToStr(FCount);
end;
procedure TMyThread2.Execute;
var
i: Integer;
begin
Windows.InvalidateRect(Form1.Handle, nil, True);
for i := 1 to 20000 do
if i mod 10 = 0 then begin
FCount := i;
Synchronize(RunUpdate);
continues
end.
This program has two buttons on it. If you press the button labeled Bad Thread, you will start
a thread that is poorly designed. In particular, while it is running you will find that it takes up
all the cycles allocated to the program. You can’t do anything else in the program while this
thread is running. The second button in the program, labeled Good Thread, enables you to
move or resize the program while it is running. It is a well-behaved thread.
Here is the declaration for the TThread object in CLASSES.PAS:
TThread = class
private
…
protected
procedure DoTerminate; virtual;
procedure Execute; virtual; abstract;
procedure Synchronize(Method: TThreadMethod);
property ReturnValue: Integer read FReturnValue write FReturnValue;
property Terminated: Boolean read FTerminated;
public
constructor Create(CreateSuspended: Boolean);
destructor Destroy; override;
procedure Resume;
procedure Suspend;
procedure Terminate;
function WaitFor: Integer;
property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;
property Handle: THandle read FHandle;
property Priority: TThreadPriority read GetPriority write SetPriority;
property Suspended: Boolean read FSuspended write SetSuspended;
property ThreadID: THandle read FThreadID;
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end;
The Create method is meant to start and run the thread. It doesn’t just allocate memory for
the thread, it actually creates and calls the thread:
constructor TThread.Create(CreateSuspended: Boolean);
var
Flags: Integer;
begin
inherited Create;
… // Additional code
FHandle := CreateThread(nil, 0, @ThreadProc, Pointer(Self),
Flags, FThreadID);
end;
As you can see, the thread is actually running by the time the Create method has ended. No-
tice that the object passes itself as a parameter to the thread. The thread function (ThreadProc)
does little more than call the Execute routine.
If you go back and look at the declaration for Execute, you can see that it is declared as ab-
stract. This means that it is never implemented. It is up to you to implement the Execute method
by overriding it. In essence, the Execute method becomes the thread function itself!
Whenever you use a TThread object, you first create a descendant of it:
TMyThread2 = class(TThread)
private
FCount: Integer;
procedure RunUpdate;
protected
procedure Execute; override;
end;
Every time you create a descendant of a TThread object, you override the Execute method. That’s
a given. In the case shown here, I have also included an FCount variable and a procedure called
RunUpdate.
Here is the implementation of TMyThread2, which is the “good” thread in the Thread1a
example:
procedure TMyThread2.RunUpdate;
begin
Form1.Panel1.Caption := IntToStr(FCount);
end;
procedure TMyThread2.Execute;
var
i: Integer;
begin
Windows.InvalidateRect(Form1.Handle, nil, True);
for i := 1 to 20000 do
if i mod 10 = 0 then begin
FCount := i;
Synchronize(RunUpdate);
end;
PostMessage(Form1.Handle, wm_ThreadDone, 2, 0);
end;
The purpose of this code is simply to count from 1 to 100,000 while regularly updating the
user as to the status of the operation.
The thread begins by clearing the screen. It doesn’t do this by calling the internal VCL func-
tion called Invalidate. Instead, it calls the Windows API function called InvalidateRect. The
reason for this is simple: Invalidate is a method of TForm, and as such is out of bounds to call
inside a thread. That’s no big deal—you can just call InvalidateRect instead.
After clearing the screen, the code iterates through a loop 100,000 times. Each time it com-
pletes ten cycles of the loop it prints out the current iteration to the screen with a standard
VCL routine:
Form1.Panel1.Caption:= IntToStr(FCount);
It does not, however, call the VCL directly. Instead it wraps the function inside a method call
RunUpdate, and it wraps the call to that function inside a method call Synchronize:
Synchronize(RunUpdate);
While Synchronize is executing, your thread temporarily becomes part of the main thread for
the executable. This means that the main thread for the program is temporarily halted. It is not
running. Instead, your thread is running. As soon as the Synchronize method is finished, the
thread resumes.
The point here is that when Synchronize is called, the main thread is not running, and there-
fore the VCL cannot be receiving any other messages. This means that it is safe for you to call
the VCL during the time that Synchronize is executing.
The key point to remember is that you want to get out of the Synchronize procedure as quickly
as possible. The moment you are through executing the VCL statement, you get out of the
Synchronize method.
procedure TBadThread.Execute;
begin
Windows.InvalidateRect(Form1.Handle, nil, True);
Synchronize(RunUpdate);
PostMessage(Form1.Handle, wm_ThreadDone, 1, 0);
end;
In this case, the entire loop executes inside the call to Synchronize. This might seem like the
right thing to do, because most of the time spent in the loop is dedicated to updating the main
screen of the program. The flaw in this theory, however, is that the main thread of the applica-
tion never gets a chance to execute while Synchronize is being called. As a result, the program
doesn’t really have two threads running, it has just one. The secondary thread has taken over
the main thread, thereby rendering the whole enterprise worthless. It isn’t really a bug, and it
doesn’t threaten the stability of the program, but it’s a classic case of poor design. (Not that
you should feel bad if you have done this. Needless to say, I wouldn’t have the example here to
present if I hadn’t fallen in this trap once myself!)
There are a couple of other things I ought to say about the TThread object before closing this
section. Notice, for instance, that the code specifies a method to be called when the thread is
terminated:
T2.OnTerminate := GoodThreadDone;
Notice that the signature for the procedure takes a single parameter of type TObject. This is the
standard TNotifyEvent that is used in response to OnClick events, and in many other places in
Delphi. You create this function yourself, typing out the entire declaration by hand, rather than
having the IDE create it for you automatically. Inside the method you need do nothing more
than free the thread itself, and handle any other cleanup chores that you might want to specify.
In particular, the code shown above sets the buttons to valid values and uses a message box to
let the user know that the thread has terminated.
A trap that you need to avoid involves the WaitFor method of TThread. This method enables
the main program to wait until the thread finishes executing. It is a serious error to call WaitFor
if the thread is going to call Synchronize. Then you end up with a deadlock where the thread
waits for the main program and the main program waits for the thread. In the meantime, nothing
happens, and you are forced to shut the program down through the IDE or through
Ctrl+Alt+Del. In Delphi, a call to WaitFor under these circumstances should raise an
exception.
In this section you have seen how to use the TThread object to run a thread. The two key points
to remember about the TThread object are that it gives you a safe way to call the VCL while a
thread is running and it gives you way to create local variables that are used only by the thread.
These variables are the fields of your TThread descendant. Windows also provides a technique
for doing this called Thread Local Storage. TLS is a powerful technique, but is, literally, about
ten times slower than using a TThread object to obtain the same end.
The actual number of cycles a thread gets depends on its priority level and the priority class of
its process. You can call GetPriorityClass to get the priority of your process, and
SetPriorityClass to set the priority of your process. All of the functions mentioned in this
paragraph are Windows API routines.
The program shown in Listing 7.9 was written by David Intersimone, a fellow Borland em-
ployee. The code has some historical significance, as it was written on the flight to the Win-
dows 95 launch in Seattle. The fact that David was able to do some serious coding on a West
Coast shuttle flight shows that almost anything is possible if you set your mind to it!
Listing 7.9. David Intersimone’s Dreaded Sorts program. This program needs two
other blank forms saved in units called SecForm and ThForm.
unit main;
{
Dreaded Sorts
copyright (c) 1996 by David Intersimone
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ComCtrls, Buttons;
const
aMax = 300;
type
TForm1 = class(TForm)
Edit1: TEdit;
Label2: TLabel;
Label1: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
BitBtn1: TBitBtn;
BubbleTrackBar: TTrackBar;
QuickTrackBar: TTrackBar;
procedure Button1Click(Sender: TObject);
private
T1 : THandle;
T2 : THandle;
end;
var
Form1: TForm1;
a,b : array[0..aMax-1] of integer;
numItems : integer;
implementation
uses
secform, thform;
{$R *.DFM}
continues
form3.free;
form3 := TForm3.Create(self);
form3.top := 140;
form3.left := 320;
form3.clientheight := numItems+10;
form3.clientwidth := numItems+10;
form3.color := clBlue;
form3.caption := ‘Quick Sort’;
form3.show;
Randomize;
for i := 0 to numItems-1 do
begin
a[i] := random(numItems);
b[i] := a[i];
form2.canvas.pixels[a[i]+5,i+5] := clYellow;
form3.canvas.pixels[b[i]+5,i+5] := clLime;
end;
T1 := createThread(nil,0,@BubbleThread,nil,0,threadID);
setThreadPriority(T1, BubbleTrackBar.Position);
T2 := createThread(nil,0,@QuickThread,nil,0,threadID);
setThreadPriority(T2, QuickTrackBar.Position);
end
else
Form1.Caption := ‘Too Large!’;
end;
end.
The Dreaded Sorts program creates two threads. One thread runs a bubble sort and the second
runs a quick sort. If you want, you can use trackbars to adjust the priority of the bubble sort
upward, so that it gets lots of clock cycles, while simultaneously lowering the priority of the
quick sort. The end result is that the bubble sort finishes in about the same time as the quick
sort, even thought it is a much slower algorithm. The two extra blank forms used by the project
visually depict the progress of the sorts, as shown in Figure 7.5.
FIGURE 7.5.
The bubble sort, shown on
the left, is naturally slower
than the quick sort, shown
on the right.
Here you can see both sorts being created. Each thread is assigned a priority by the
SetThreadPriority function:
function SetThreadPriority(
hThread: THandle; // The handle of the thread.
nPriority: Integer): // The threads priority.
BOOL; stdcall; // Call GetLastError if the function fails
Here are the constants you can use to help make sense of the values shown earlier:
THREAD_BASE_PRIORITY_LOWRT = 15;{ value that gets a thread to LowRealtime-1 }
THREAD_BASE_PRIORITY_MAX = 2; { maximum thread base priority boost }
THREAD_BASE_PRIORITY_MIN = -2; { minimum thread base priority boost }
THREAD_BASE_PRIORITY_IDLE = -15;{ value that gets a thread to idle }
Related functions that you should look up in the online help include SetPriorityClass and
GetPriorityClass.
Summary
When working with threads, you saw how to create single threads. You also saw how to allo-
cate memory so that you can pass in data to a single thread that has multiple copies of itself
running at one time. You also saw the reverse of this process: how to synchronize multiple threads
so that they can all share access to a single block of data. In particular, you studied critical sec-
tions and mutexes.
You also got a good look at the TThread object that’s part of the Delphi VCL. The TThread
object enables you to use both the VCL and threads at the same time. This statement is
somewhat misleading, in that there are no inherent conflicts between the VCL and threads,
and indeed, the TThread object is very much a part of the VCL. However, most of the TComponent
descendants are not compatible with threads. There are some exceptions, such as the TOpenDialog
component, which works properly as long as it is created in code. The TQuery and TTable ob-
jects also work inside threads, as long as you handle them properly. However, most visual VCL
objects won’t work inside threads unless you use the TThread object.
Win32 Memory
Management 8
In this chapter you get a look at memory-management issues under Windows 95 and Win-
dows NT. In particular, you get a look at:
■ The structure of memory in the four-gigabyte (4 GB) Win32 address space
■ Creating, freeing, and using heaps
■ Memory-mapped files
■ Virtual memory and, particularly, the VirtualAlloc and VirtualFree functions
This chapter is a companion piece to the previous chapter, “Multitasking and Threads.” The
last chapter concentrated on multitasking and threads; this chapter concentrates on memory
management. Put the two sides of the story together and you have a fairly clear picture of the
most important features in Win32. These chapters show why Windows 95 and Windows NT
are so important, and how they differ from Windows 3.1.
The end of this chapter provides a brief bit of Object Pascal code that shows how to manipu-
late a Delphi memory manager. This code is relatively straightforward when compared to the
rest of the material in this chapter, but it helps to emphasize the power of Delphi, and the art-
fulness of its design. The subject fits in with the rest of the chapter because it involves memory
management.
■ Each program has only one type of heap, but it can have multiple copies of these
heaps, and can create them on-the-fly. This enables you to isolate one set of objects
from another. For instance, you can place a linked list in its own heap, where it can
endanger only itself and no other parts of your program. Contrast this with Windows
3.1, where each program had two heaps: the local heap and the global heap.
■ Each Win32 heap belongs to one, and only one, process. There is no such thing as a
global, shared heap in a 32-bit Windows 95 process. Contrast this with Windows 3.1,
where each application has a local heap and then shares a single global heap with every
other application on the system. The single Windows 3.1 global heap contained
memory allocations not only from the current task, but from all the tasks on the
system. It was a huge stew of different kinds of allocations from different kinds of
programs, all mixed together in one potentially dangerous concoction. (Note also that
the lack of Win32 local heaps means that DLLs no longer can be used to share
memory between processes—at least, not by using the same techniques exploited in
Windows 3.1.)
■ There is a powerful set of routines for manipulating virtual memory. Virtual memory
is treated just like normal memory, but you can keep it either in RAM or in a “paging
file” that is stored on disk. You can call the VirtualAlloc function to allocate memory
in that paging file. You then can read and write to this memory. Reserving virtual
memory and writing to it is called committing physical storage. Use VirtualAlloc to
reserve or commit physical storage, and VirtualFree to decommit physical storage.
■ Win32 supports memory-mapped files, which can help you share memory between
applications and also access files on disk. The system uses memory-mapped files to
load executables and DLLs. As their name implies, memory-mapped files enable you
to map a file on the disk into the address space of your program. It is then possible to
manipulate the file using standard library routines, such as memcpy and even strcpy.
These points make it clear that Win32 has not one, but three different ways of handling memory.
The first is through virtual memory, the second is through memory-mapped files, and the third
is through heaps. Large arrays of data can usually be best stored in virtual memory. If you need
to share memory between processes, or if you need to work with large streams of data (files),
you can use memory-mapped files. If you want to create a linked list, or some similar structure
that requires lots of small memory allocations, you probably should use the heap.
As you can see, there’s more to Windows 95 memory management than just the bare presence
of a 4GB address space. Furthermore, you can see that your program doesn’t just get loaded
into the bottom of this 4GB space. Nor does it then proceed to use the remaining 3.9 GB for
its personal playground. Instead, in Windows 95, your program usually gets loaded in at the
4MB mark, and at least half of the rest of the address space is reserved for the system. In Win-
dows NT, programs usually get loaded in at the 64KB mark, and can range up to within
64 KB of the 2GB mark. The two 64 KB blocks are used to trap errant pointers.
Windows 3.x had three possible modes of operation: real mode, standard mode, and enhanced
mode. Windows 95 does not support either real mode or standard mode when running Win-
dows applications. (It does support DOS real-mode programs running in a virtual DOS box.)
Take this limitation as part and parcel of your vision of Windows 95 as a true 32-bit operating
system. Real mode and standard mode are ways of handling 16-bit applications. By their defi-
nition they are tied to applications that have very limited address spaces. Windows 95 is about
wide-open address spaces.
You’ll be glad to hear that most of the key 16-bit Windows memory-management routines
port over to Windows 95 applications. However, they are available just for compatibility, and
are inefficient when compared to the new memory calls. In particular, the old GlobalAlloc call
has been replaced with HeapAlloc, VirtualAlloc, and memory-mapped files. For compatibil-
ity reasons, GlobalAlloc still works. However, it is not as efficient as HeapAlloc or VirtualAlloc.
Furthermore, there is no longer any distinction between GlobalAlloc and LocalAlloc. Both
calls end up allocating memory from the process’s primary heap. But I’m getting ahead of myself.
NOTE
People who argue that Windows 95 is just a thin veneer over a bunch of 16-bit DOS
calls are ignoring some very important aspects of this new operating system. Yes, there
still are portions of Windows 95 that are 16-bit. However, the memory model for
Windows 95 is totally different from Windows 3.1. What computer programs do is
manipulate memory. Every significant API or Delphi library call made on a computer
ends up manipulating memory in one way or another. You can’t even make the call
without manipulating memory. If the memory model has changed radically, then
everything has changed radically.
It’s interesting to study the similarities between Windows 95 and Windows 3.1, but
it’s a gross misstatement of the facts to claim that they are just two versions of the same
operating system. I can be as cynical about Windows 95 as the next guy. But look
again at the list of new Win32 memory features near the top of this section. Say what
you will, Windows is in the big leagues now. Windows 95 is a real operating system,
and a powerful one at that. The mere presence of some 16-bit code does not cancel out
the extraordinary developments in multithreading and multitasking described in
Chapter 7, “Multitasking and Threads,” and in memory management shown earlier.
This section has given you a general overview of some of the key points about Win32 memory
management. Further information is available in advanced books on Windows programming,
or on the Microsoft Developer Network CD.
NOTE
The only reason there’s a 1MB limit on the size of the stack is to catch runaway
recursive processes before they get totally out of control.
Recursive functions call themselves. Every time they call themselves, information is
pushed on the stack. A recursive process should end automatically after it reaches some
predefined goal. However, sometimes something goes wrong and the recursive process
keeps calling itself over and over again, pushing more data on the stack with each call.
Because the stack has a 1MB limit, the operating system has a chance to automatically
stop this process before it consumes all the memory on the system.
The key point to understand is that most Windows 95 programmers don’t have to worry a
great deal about memory management under most circumstances. Advanced programmers need
to understand this subject; indeed, an in-depth understanding of this subject is what defines
advanced Windows programmers. If you want to be really good, you should know this stuff
cold.
So yes, it’s always good to know about memory. However, if you don’t, you are not nearly as
likely to get bit by Windows 95 or Windows NT as you were by Windows 3.1. When com-
pared to Windows 3.1, Win32 gives you more power and more freedom, with less work and
less anxiety.
NOTE
You commit physical storage and reserve regions. All regions reserved by your program
begin on 64KB boundaries. The size of the region must be a multiple of the page size
on your computer. For the Intel architecture, the page size is 4 KB. Physical storage is
allocated in pages, where the finest granularity is 4 KB.
Remember that virtual memory is virtual memory whether it’s in RAM or swapped to disk.
Memory management under Windows 95 or Windows NT is radically different from what it
was under Windows 3.1. Win32 is even radically different from enhanced-mode Windows 3.1,
which had a swap file.
You can think of physical storage in Win32 as a huge file that resides not so much in RAM as
on disk. At times this huge paging file may have certain portions loaded into RAM, but it’s just
as much at home on your hard disk. In fact, it may be even more at home on your hard disk.
On the Intel architecture, paging files are divided up into pages of 4 KB each. If you ask for
only 2000 bytes of memory using VirtualAlloc, the system still returns a full 4096 bytes. That’s
the size of a page of memory under Windows 95 or Windows NT on the Intel platform. In
fact, all Win32 platforms except for the DEC Alpha work in 4KB chunks. The Alpha works in
8KB chunks.
NOTE
Your executable, or a DLL you create, does not reside in a paging file. Instead, it
resides in a memory-mapped file.
The allocations you make are from a paging file, but the actual code and data from
your program resides in a memory-mapped file. This memory-mapped file is simply
sucked into memory in one huge chunk when you first load the program. A region of
memory is reserved for your executable, and the committed physical storage for that
executable is simply the .EXE file residing on disk. This makes it easy for Windows to
commit the memory for an executable!
Each page of memory in a paging file has a protection attribute associated with it. These at-
tributes have names like PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE, and so on. You pass in
these attributes when you call VirtualAlloc.
Using VirtualAlloc
OK, you can take a deep breath now. The hard part is over. Actually, using VirtualAlloc is
not nearly as hard talking about what it does.
NOTE
Remember, you don’t have to do any of this stuff. If you want to allocate memory, you
can use New and Dispose, or you can just declare a big array and work with that. The
point of this chapter, and indeed of much of this book, is to let you look beneath the
surface to see how Windows and Delphi work behind the scenes. The goal is not just
to see how to do a particular job as quickly and easily as possible, but how to become a
better programmer!
The first parameter is a suggested location where you might want to allocate memory. In most
cases, just pass nil in this parameter, and let the system choose the location to allocate the
memory.
The second parameter is the size of the allocation. If you are asking to reserve memory, this
number is rounded up to 64 KB, even if you ask for less. If you are asking to commit memory,
the figure is rounded up to the nearest 4KB boundary, even if you asked for less. (It’s 8 KB on
an Alpha machine.)
The third parameter specifies the allocation type. You can either reserve memory or commit
memory. You cannot access memory that you have reserved. However, neither can any other
part of your program. This way you can arrange to reserve a contiguous range of memory with-
out necessarily having to commit it all. This might be useful if you need to allocate a 2MB
array, but don’t actually use more than a few thousands bytes of the array at one time.
NOTE
Suppose that you were mapping the battle of Gettysburg on a grid. Each cell in the
grid might represent a certain area of the battlefield that is 100 yards square. Some of
the areas are marked as containing Union troops and some as containing Confederate
troops. Most of the squares, however, would be blank. This is an ideal time to explic-
itly use VirtualAlloc. Don’t use New or GetMem in this case; it’s time to turn to the
Windows API. You can use VirtualAlloc to reserve memory for the entire grid, which
might be several megabytes in size. Then you can commit memory only for those areas
where there actually are troops. The result is that you can use the array syntax, but
actually commit only a relatively small portion of the memory to physical storage.
The third parameter describes the type of access you want to have to the memory. As of this
writing, Windows 95 supports only PAGE_NOACCESS, PAGE_READONLY, and PAGE_READWRITE.
If the operation succeeds, you get a pointer to some memory back. If it fails, the function re-
turns nil.
You don’t necessarily have to reserve memory first and then commit memory later. If you want,
you can perform both actions at the same time:
P2 := VirtualAlloc(nil, Size, mem_Commit or mem_Reserve, Page_ReadWrite);
In this case, I use an or operation on the mem_Commit and mem_Reserve flags, which is the ap-
proach suggested by the Microsoft documentation. However, I appear to have the same results
if I pass in only the mem_Commit flag.
The program in Listing 8.1, called VirtAll, performs some experiments with the VirtualAlloc
function. In particular, it allocates some memory, and then displays some facts about that
memory.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Grids, Buttons, ExtCtrls;
type
TForm1 = class(TForm)
SG: TStringGrid;
Panel1: TPanel;
bAllocate: TButton;
bbHInstance: TBitBtn;
bbMethod: TBitBtn;
fForm: TBitBtn;
bDLL: TBitBtn;
procedure bAllocateClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure bbHInstanceClick(Sender: TObject);
procedure bbMethodClick(Sender: TObject);
procedure fFormClick(Sender: TObject);
procedure bDLLClick(Sender: TObject);
private
procedure ShowPointer(P: Pointer; Col: Integer; S: string);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
continues
var
hLIb: THandle;
begin
hLib := LoadLibrary(‘USER32.DLL’);
ShowPointer(Ptr(hLib), 7, ‘USER32’);
FreeLibrary(hLib);
end;
end.
When you run this program, you see several buttons along the bottom of the form, as shown
in Figure 8.1. You can press the first button to view information about three addresses, labeled
P, P1, and P2. These three addresses are pointers allocated by the program. The HInstance
button lets you view information on the HInstance for the program, which is the address at
which the program is loaded into memory. The other three buttons enable you to view infor-
mation on the form, on a function used in the program, and on the HInstance of USER32.DLL,
as shown in Figure 8.2.
FIGURE 8.1.
The VirtAll program
reports information about
pointers allocated in a
method of Form1.
FIGURE 8.2.
The VirtAll program
reports information about
variables used by the
system.
Besides the calls to VirtualAlloc and VirtualFree, the VirtAll program also makes calls to
VirtualQuery. This function retrieves information on pointers that are passed to it, and any
contiguous memory beyond it that has the same attributes:
function VirtualQuery(
lpAddress: Pointer; // Pointer you want to know about
var lpBuffer: TMemoryBasicInformation; // Record containing requested info
dwLength: DWORD // Size of TMemoryBasicInformation
): DWORD; stdcall; // Num Bytes returned in lpBuffer
The call to VirtualQuery is trivial. What is a bit more difficult is parsing the information re-
trieved in lpBuffer:
TMemoryBasicInformation = record
BaseAddress : Pointer;
AllocationBase : Pointer;
AllocationProtect : DWORD;
RegionSize : DWORD;
State : DWORD;
Protect : DWORD;
Type_9 : DWORD;
end;
The BaseAddress is a rounded-off version of the address of the variable that you passed in to
VirtualQuery.
The AllocationBase is the address of the reserved area to which the memory belongs. Suppose
you first call VirtualAlloc with the mem_Reserve flag, and then call VirtualAlloc with the
mem_Commit flag. In that case, the BaseAddress holds the address at which the variable you are
asking about resides, and the AllocationBase shows the beginning of the reserved area to which
the allocation belongs. I return to this subject in a moment, but you can see examples of this in
the columns labeled P1 in Figure 8.1 and GetState in Figure 8.2.
The AllocationProtect field shows the protection given to the region when it was initially re-
served. This is the reserved region, not the committed physical storage. In other words, when
you call VirtualAlloc to reserve space for a region, you pass in certain protection flags. This
field reports on which flags you passed in.
The RegionSize is the size of the allocation.
The State field is set to mem_Reserve, mem_Commit, mem_Free, or some combination of these three
values.
The Protect field is the protection level of the actual memory in question. This is set to
PAGE_READONLY, PAGE_READWRITE, PAGE_NOACCESS, and so on.
The Type_9 field is set to either mem_Private, mem_Image, or mem_Mapped. In Windows 95, it is
always mem_Private, but in Windows NT you have more scope.
If you press the Allocate button on the main form of the VirtAll program, the following code
is the first to execute:
S := ‘Sam’;
Size := Length(S) + 1;
P := VirtualAlloc(nil, Size, mem_reserve, Page_ReadWrite);
ShowPointer(P, 1, ‘P’);
This code is reserving memory, not committing it. Nothing physical happens on the system
when you call this function. All it does is reserve a block of virtual memory for use by the pointer
variable P.
As you can see by looking at Figure 8.1, the function happened to reserve memory at
$00AD0000. The AllocationBase and the BaseAddress are the same in this case because the
pointer occupies the same address as its reserved area. The code asks for four bytes of memory,
but you can see in Figure 8.1 that VirtualAlloc actually set aside 64 KB. That’s the finest reso-
lution you can get when reserving memory. The state for the allocation is mem_Reserve, which
makes good sense under the circumstances.
The memory was allocated with the PAGE_READWRITE flag, but you can see the system paid no
attention to that request and gave the memory a PAGE_NOACCESS flag. This fact would be born
out if you tried to access the memory:
Move(S, P^, Size);
would raise an exception. It is, therefore, probably better to reserve memory with the
PAGE_NOACCESS flag:
If you now look at the second allocation made by the program, you see a quite different story:
P1 := VirtualAlloc(Ptr(Integer(P) + $5000), Size, mem_Commit, Page_ReadWrite);
ShowPointer(P1, 2, ‘P1’);
Move(S, P1^, Size);
This code allocates four bytes from a block of memory $5000 bytes into the 64KB block of
memory reserved by the first call to VirtualAlloc. I first cast P as an integer so that I can per-
form integer math on the pointer. I then add $5000 to the pointer, which takes me $5000
bytes into the pointer. The next step is to convert this integer value back into a pointer that can
be passed to VirtualAlloc. (This code is a stunning example of how cryptic Object Pascal can
get under the worst possible circumstances, but at least it does let us do some pointer math.)
When you look at the pointer returned by this call to VirtualAlloc, shown in Figure 8.1, you
see that its BaseAddress and AllocationAddress are different. In fact, they are separated by ex-
actly $5000 bytes, which is what you would expect in this case. Both the AllocationProtect
and the actual protect for the pointer are set to PAGE_READWRITE. Had I reserved the memory
for its region with PAGE_NOACCESS rather than PAGE_READWRITE, the AllocationAddress for both
this column and the P column would be set to PAGE_NOACCESS. However, the Protect field for
P1 would still be correctly set to PAGE_READWRITE, if that was what I requested. To test this,
modify the code on the Unleashed CD-ROM so that it passes PAGE_NOACCESS to the first call to
VirtualAlloc. By all means, experiment with this code. That’s why it’s there!
The last of the three allocations does not explicitly reserve any memory, but instead commits
four bytes of memory:
P2 := VirtualAlloc(nil, Size, mem_Commit or mem_Reserve, Page_ReadWrite);
In Figure 8.1 you can see that this appears to create a new allocation, only 4 KB in size. The
code asks for PAGE_READWRITE protection, so you get it for both the page that was allocated and
the reserved area to which it belongs.
The most interesting thing about this third call to VirtualAlloc is that occurs exactly 64 KB
further down the pike from the first call. This shows that Windows is indeed reserving a 64KB
block of memory for the pointer called P. VirtAll uses only four bytes of that memory, but still
the entire block is reserved. Remember, though, that only four bytes of physical storage were
ever committed from that 64KB block. This is virtual memory, and there’s a big difference
between virtual memory and the physical RAM on your system.
It’s important to note that if I allocate another block of memory only four bytes in size imme-
diately after the third allocation, its AllocationBase is set to 64 KB down from the last alloca-
tion:
P3 := VirtualAlloc(nil, Size, mem_Commit, Page_ReadWrite);
ShowPointer(P3, 4, ‘P3’);
The point is that P2 is really taking up (that is, reserving) 64 KB of virtual memory, although
it appears to be taking up only 4 KB. Once again, that’s 64 KB of virtual memory that is re-
served, but only 4 KB of physical storage is committed. It’s wasteful to expend reserved memory
to no good effect; nearly as serious as wasting physical storage. You have to grasp the difference
between reserving memory and committing memory!
Had I reserved a 64KB chunk of memory, and then committed two four-byte chunks inside of
that, it would have taken up only 64 KB of virtual memory rather than 128 KB of virtual
memory. In other words, the technique shown for committing the memory for P1 is the right
choice to use in this case, and the method shown for the variable called P2 is the wrong choice!
If you need to make only one allocation, it’s OK to use the P2 technique. If you’re going to
make several small allocations in a row, reserve some memory first, then commit portions of it
as needed!
The following code would place the second 4-byte memory allocation in the same 4KB page as
the second allocation shown earlier:
P3 := VirtualAlloc(Ptr(Integer(P) + $5004), Size, mem_Commit, Page_ReadWrite);
ShowPointer(P3, 4, ‘P3’);
That means the code allocates only 4 KB of physical storage for two four-byte allocations.
The VirtAll program clearly provides a fascinating platform for exploring virtual memory. Notice
that it also takes a look at the whole block of memory allocated for the code and data of your
program. It does this by converting HInstance into a pointer, then passing it in to VirtualQuery.
You also can obtain the HInstance for USER32.DLL, and then take a look at the memory al-
located for that key system file.
When you are through with the memory used in the VirtAll program, you should free it. You
can use the mem_Decommit flag to free the memory that you committed at an address inside the
first reserved, 64KB region:
Notice that you specify the exact amount of memory you want to free.
If you want to free the memory associated with the reserved pointer, you should not specify
the amount of memory you want to release:
if not VirtualFree(P, 0, mem_Release) then
ShowMessage(‘Error freeing memory’);
Take a moment to compare these two examples. The first uses the mem_Decommit flag, and the
second uses the mem_Release flag. One simply frees up some committed memory, while the
other actually returns a block of memory to the system. If you are releasing memory that has
been reserved, then you don’t need to specify the size of the block. The system knows how big
the block is and will perform the necessary calculations for you. If you are decommitting
memory, you should say how large an area inside the reserved space you want to free.
You also should free the memory that was committed without first reserving a region for it. Do
not specify the amount of memory to release:
if not VirtualFree(P2, 0, mem_Release) then
ShowMessage(‘Error freeing memory’);
As I explained earlier, heaps don’t necessarily play the same role in Windows 95 as they did in
Windows 3.1 or DOS. For one thing, you are not nearly as likely to run out of room in your
program’s address space for data or code. If there is plenty of RAM on your machine, there’s
plenty of room for even very large amounts of code and data in your program’s address space.
Even if there is a shortage of RAM, Windows usually can swap memory to disk to make room
for the core parts of your program. You code will run much slower in that case, but it still
will run.
If you do need additional room, there’s always the virtual memory in your paging file, or you
could use memory-mapped files, as described in this section. However, if you want to make a
series of relatively small allocations, your program’s heap would be a good place to make them.
Your program can have more than one heap. As I’ve mentioned, you can create and destroy
new heaps by calling CreateHeap and HeapDestroy. There are two primary reasons to create a
heap:
■ You might want to isolate a sensitive memory operation from the rest of the program
so that it cannot corrupt the core routines in your code. For example, you could put a
linked list in its own heap.
■ You might want to optimize routines by placing allocations of similar size in their own
unique heap. This helps prevent heap fragmentation, and increases the access time
when you need to get at the memory.
Despite the seemingly esoteric nature of this subject, the actual calls to HeapAlloc, HeapFree,
CreateHeap, and HeapDestroy are simple enough to master. Listing 8.2 contains a program that
exercises each of these calls.
Listing 8.2. The main module for the HeapMem program shows how to work
with Win32 heaps.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Buttons;
type
TForm1 = class(TForm)
bDoAlloc: TBitBtn;
bNewHeap: TBitBtn;
procedure bDoAllocClick(Sender: TObject);
procedure bNewHeapClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
HEAP_ZERO_MEMORY = $00000008; // Not in WINDOWS.PAS?
MaxSize = 256;
begin
MemStr := HeapAlloc(AHeap, HEAP_ZERO_MEMORY, MAXSIZE);
StrCopy(MemStr, PChar(S));
ASize := HeapSize(AHeap, 0, MemStr);
AllocStr := Format(‘Alloc Size: %d’, [ASize]);
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, 10, 10, MemStr, StrLen(MemStr));
TextOut(DC, 10, 40, PChar(AllocStr), Length(AllocStr));
ReleaseDC(hWindow, DC);
end.
This program enables you to print two simple strings to the screen, as shown in Figure 8.3.
When you select one of the program’s menu options, the memory for the strings is allocated
from the default process heap. When you choose the other option, the memory for the strings
is allocated from a new, custom heap.
FIGURE 8.3.
The HeapMem program
shows how to allocate
memory using native
Win32 routines.
The following code shows how to allocate memory for a string, how to check the size of the
allocation, and how to free the memory:
procedure DoAlloc(hWindow: hwnd; AHeap: THandle; S: string);
var
ASize: DWord;
DC: HDC;
MemStr: PChar;
AllocStr: string;
begin
MemStr := HeapAlloc(AHeap, HEAP_ZERO_MEMORY, MAXSIZE);
StrCopy(MemStr, PChar(S));
ASize := HeapSize(AHeap, 0, MemStr);
AllocStr := Format(‘Alloc Size: %d’, [ASize]);
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, 10, 10, MemStr, StrLen(MemStr));
TextOut(DC, 10, 40, PChar(AllocStr), Length(AllocStr));
ReleaseDC(hWindow, DC);
This routine calls HeapAlloc to allocate memory, HeapSize to check the size of the allocation,
and HeapFree to free the memory. Here’s the declaration for the HeapAlloc function:
function HeapAlloc(
hHeap: THandle; // The handle of the heap you want to use.
dwFlags: DWORD; // Flags such as HEAP_ZERO_MEMORY
dwBytes: DWORD // The amount of memory you need
): Pointer; stdcall; // On failure generates exception or returns nil
Use HeapAlloc to allocate memory, just as you use New or GetMem, or GlobalAlloc from Win-
dows 3.1. (Well, it’s not exactly the same, as you’re suballocating the memory from the heap.
But it’s similar.) The memory allocated may be larger than the amount you request, in which
case you can use the entire amount returned. Use the HeapSize function to find out how much
memory was actually allocated. You do not have to lock down this memory as you did in
Windows 3.1. However, this memory is nonmoveable.
The first parameter is the handle to the heap you want to use. To use the default heap for your
program, just call GetProcessHeap. If you want to create a new heap, call HeapCreate, as shown
later in this chapter.
There are three flags you can pass to this routine in its second parameter. One is
HEAP_GENERATE_EXCEPTION, which means that on failure the operating system generates an er-
ror rather than returning nil. The HEAP_ZERO_MEMORY flag causes the system to fill the block of
memory with zeros. You also can pass in HEAP_NO_SERIALIZE, but this option is beyond the scope
of this book.
The last parameter is the size of the block of memory you want to create. In principle, this
number can be as large as you want. However, if you want to request large chunks of memory,
you should consider calling VirtualAlloc rather than HeapAlloc. Furthermore, requests for large
chunks of memory usually are satisfied by the system calling VirtualAlloc automatically, even
though you made the request with HeapAlloc. The only concrete limitation on this number
occurs if you explicitly called CreateHeap, and you explicitly made the heap you created
nongrowable. In that case, this number should not exceed $7FFF8, which is 524,280.
Here’s an example:
MemStr := HeapAlloc(GetProcessHeap, HEAP_ZERO_MEMORY, 4096);
The HeapSize routine returns the size of the current heap allocation. In other words, if you
allocate 500 bytes with HeapAlloc and then call HeapSize, the value returned is 500. Remem-
ber that Windows sometimes returns more memory than you requested, in which case HeapSize
returns a value larger than the amount you requested. It’s the value returned by HeapSize that
is correct, not the value you requested.
Here is the declaration:
function HeapSize(
hHeap: THandle; // handle to the heap
dwFlags: DWORD; // heap size control flags
lpMem: Pointer // The memory you want to query
): DWORD; stdcall;
This is a simple function to use. Just pass in a heap in the first parameter, zero in the second
parameter, and the pointer you want to know about in the third parameter. (The second pa-
rameter can be set to HEAP_NO_SERIALIZE but, as I mentioned earlier, that’s an advanced topic
not covered in this book.)
Here’s the declaration for the HeapFree function:
function HeapFree(
hHeap: THandle; // Value returned by CreateHeap or GetProcessHeap
dwFlags: DWORD; // usually zero, can be HEAP_NO_SERIALIZE
lpMem: Pointer // pointer to the memory to free
): BOOL; stdcall; // returns False on failure
This function frees memory allocated with HeapAlloc. The first parameter is the handle to the
heap. The second usually is set to zero. The third parameter is a pointer to the memory you
want to free.
The function returns True when successful.
Here’s an example:
HeapFree(AHeap, 0, MemStr);
You can pass in zero in the first parameter, or one of the flags HEAP_GENERATE_EXCEPTIONS or
HEAP_NO_SERIALIZE. Use HEAP_GENERATE_EXCEPTIONS if you want the operating system to report
errors, rather than simply returning nil.
The second parameter is the amount of memory you want to allocate for the heap. If you pass
in zero in the third parameter, the heap grows automatically if you request more than
dwInitialSize bytes of memory through calls to HeapAlloc. If you do not pass in zero in
dwMaximumSize, the heap does not grow, and requests for memory beyond the limits of the heap
fail.
When you want to destroy a heap, call HeapDestroy and pass in the handle to the heap as the
sole parameter:
HeapDestroy(AHeap);
This section of the chapter telegraphed to you the information you need to manipulate native
Win32 heaps. You now know how to allocate memory, how to free it, and how to create and
destroy your own heaps.
Remember that this is not the only way to manipulate memory. Windows 95 and Windows
NT also provide memory-mapped files and VirtualAlloc. If you want to allocate very large
chunks of memory, then see the description of the VirtualAlloc function in this section. If
you want to create a series of small allocations, as in a linked list, use HeapAlloc and its related
functions. To get started with memory-mapped files, see the next (and last) section of this
chapter.
Listing 8.3 shows an example of memory-mapped files. I have to confess that this example does
not represent a complete examination of this subject. Instead, my goal is to introduce you to
the topic so that you understand what memory-mapped files are all about. After all, seeing is
believing—and in this case I think most programmers will be intrigued by what they see. If
you’re interested in exploring this subject in depth, you should get a book on advanced Win-
dows programming, or else study the Delphi online Help.
Listing 8.3. The main unit for the MemFile program shows how to work with
memory-mapped files.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Buttons;
type
TForm1 = class(TForm)
BitBtn1: TBitBtn;
OpenDialog1: TOpenDialog;
Memo1: TMemo;
procedure BitBtn1Click(Sender: TObject);
private
hMapping: THandle;
Data: Pointer;
continues
var
Form1: TForm1;
implementation
{$R *.DFM}
/////////////////////////////////////////////////////////
// Open a map file and prepare to access it
/////////////////////////////////////////////////////////
function TForm1.OpenMappedFile(FileName: string; var FileSize: DWORD): Boolean;
var
hFile: THandle;
HighSize: DWORD;
begin
Result := False;
CloseHandle(hFile);
SetWindowText(Memo1.Handle, S);
if not UnMapViewOfFile(Data) then
ShowMessage(‘No unmapping’);
if not CloseHandle(hMapping) then
ShowMessage(‘No closing’);
FreeMem(S, Size);
end;
end;
end.
The MemFile program, shown in Figure 8.4, has a standard window, a TMemo object, and
one button. If you select the menu item, the program opens a file in read-only mode, reads it
into memory, uses move to copy its contents into a string variable, and then sends the file to a
memo control with SetWindowText.
FIGURE 8.4.
The MemFile program
maps a file from your hard
drive into memory.
I should point out that one of the advantages of memory-mapped files is that you can use them
to work with huge blocks of memory. In other words, you can open up a file that is several
gigabytes in size, and manipulate it using the routines in the MemFile program.
NOTE
Memory-mapped files are powerful enough to work with huge files. However, the
MemFile program displays the contents of the file you choose in an edit control, which
means this particular program cannot work with huge files. If you want to use these
routines with really big files, combine this program with the RichText control.
You should note, however, that it usually doesn’t make any sense to map a really big
file into memory all at once. Instead, you want to read in sections of the file and then
work with these sections one at a time. (You’ll find that the routines I show you in the
HeapMem example have provisions for this kind of process, although I did not exploit
those routines.) In particular, you can specify the offset at which you want to begin
mapping a file. You can use this offset to map in chunks of a file at a time. Note,
however, the provisions mentioned later in this chapter regarding the granularity of
memory allocations.
There are two important routines in the MemFile program. The first prepares the memory-
mapped file, and the second reads it into memory and manipulates it. The first routine is called
OpenMappedFile. The second is just a button’s OnClick handler.
CloseHandle(hFile);
The routine shown here uses the CreateFile Windows API call to open the file. Because this
program displays its output in an edit control, there’s a limit on the size of the file you can
read.
This is the standard Windows routine for opening and closing files. You don’t just use it to
create a file: You also can use it to open an existing file. It can also be used with named pipes,
communications tools, storage devices, and consoles.
This is one of the most complex calls in the whole Windows API. In saying this, I don’t mean
that the function is necessarily hard to use; rather, it has many, many flags that can be passed
into it. If you want a full explanation of the routine, see the Delphi online Help. Here I’ll only
give you enough information so you can use the routine to open and close standard disk files.
The first parameter is simple, in that it takes a pointer to a filename. The second parameter can
have the following values:
0 You can query the device without actually accessing the device.
GENERIC_READ You can read the file and move the file pointer.
GENERIC_WRITE You can write to the file and move the file pointer.
The dwShareMode parameter can have one of the following values:
0 The file cannot be shared.
FILE_SHARE_READ Others can open the file for read access.
FILE_SHARE_WRITE Others can open the file for write access.
Security issues generally are relevant only to Windows NT, so Windows 95 programmers can
set the lpSecurityAttributes parameter to nil.
The dwCreationDistribution field can have the following values:
CREATE_NEW The function fails if the specified file already exists.
CREATE_ALWAYS The function overwrites the file if it exists.
OPEN_EXISTING The function fails if the file does not exist.
OPEN_ALWAYS If the file does not exist, the function creates it.
TRUNCATE_EXISTING The function truncates the file to 0 bytes. (Use this with
GENERIC_WRITE.)
The file handle returned by CreateFile is used to determine the file’s size, and to create a file-
mapping object.
Here is the CreateFileMapping function:
function CreateFileMapping(
hFile: THandle; // handle of file to map
lpFileMappingAttributes: PSecurityAttributes; //optional security attributes
flProtect, // protection for mapping object
dwMaximumSizeHigh, // high-order 32-bits of object size
dwMaximumSizeLow: DWORD; // low-order 32-bits of object size
lpName: PAnsiChar // name of file-mapping object
): THandle; stdcall; // zero on failure, use GetLastError
The first parameter of this function contains the handle of the file you want to map. You get
this handle by calling CreateFile.
You can pass in nil in the second parameter to use the default security attributes.
The flProtect parameter can have one of the following values, plus some additional values
described in the online Help:
PAGE_READONLY The committed part of the file has read-only access.
PAGE_READWRITE CreateFile must use GENERIC_READ and GENERIC_WRITE.
Page_WriteCopy CreateFile must use GENERIC_READ and GENERIC_WRITE.
If you set the dwMaximumSizeHigh and dwMaximumSizeLow parameters to zero, the system auto-
matically fills them in with the size of the file associated with the handle passed to this
function.
The last parameter contains the name of the file-mapping object. This parameter allows differ-
ent processes to enter the same name when calling this function, thereby gaining access to the
same file. This enables you to share memory between two applications. You can pass nil in
this parameter if you don’t need to specify a name.
Here’s an example:
hMapping := CreateFileMapping(hFile, nil, PAGE_READONLY,
0, 0, nil);
After successfully calling and using CreateFile and CreateFileMapping , you should call
CloseHandle to close out the files.
A call to CreateFileMapping does not actually allow you to use mapped files. It opens up a file-
mapping object, but does not return a pointer to the “memory” in that file. To get at the actual
contents of that file, call MapViewOfFile or MapViewOfFileEx.
Here is the declaration for a call to MapViewOfFile:
function MapViewOfFile(
hFileMappingObject: THandle; // Object returend by CreateFileMapping
dwDesiredAccess: DWORD; // Access mode (FILE_MAP_READ)
dwFileOffsetHigh, // high-order 32-bits of file offset
dwFileOffsetLow, // low-order 32-bits of file offset
dwNumberOfBytesToMap: DWORD // number of bytes to map
): Pointer; stdcall; // returns nil on failure
The first parameter is the mapping object you want to work with. This value is obtained by
calling CreateFileMapping. The next parameter specifies the kind of access you want. It can
contain the following values:
FILE_MAP_WRITE Read-write access.
FILE_MAP_READ Read-only access.
FILE_MAP_ALL_ACCESS Same as FILE_MAP_WRITE.
FILE_MAP_COPY Copy on write access.
See the online Help for additional information.
The dwFileOffsetHigh and dwFileOffsetLow are the high and low DWORDS of the value
specifying the offset into the file at which you want to read. The file has to be very large before
you need to use the dwFileOffsetHigh parameter. The dwFileOffsetLow parameter must be a
multiple of the system’s memory allocation granularity. You obtain this number using a call to
GetSystemInfo . If you want to start reading from the beginning of the file, set both
dwFileOffsetHigh and dwFileOffsetLow to zero.
The final parameter is the number of bytes to map. If you want to read the whole file, just set
this value to zero. Otherwise, set it to either the granularity value returned by GetSystemInfo or
some amount smaller than the granularity.
Once you have the pointer to the data in a file, you can use it to access the contents of the file.
To help illustrate this point, I use Move to copy the contents of the file into a string. I then set
the last byte in the string to 0, and send it off to the edit control so you can look at it:
if not OpenMappedFile(OpenDialog1.FileName, Size) then Exit;
GetMem(S, Size);
Move(Data^, S^, Size);
S[Size - 2] := #0;
SetWindowText(Memo1.Handle, S);
if not UnMapViewOfFile(Data) then
ShowMessage(‘No unmapping’);
if not CloseHandle(hMapping) then
ShowMessage(‘No closing’);
FreeMem(S, Size);
The point here is that file-mapping enables you to treat the contents of a file as if it were memory.
(You’re calling Move on the contents of a file.) If the last bytes in two files both were 0, you
could append them by using StrCopy or StrCat, or the simple string concatenation operator
(+). You could even compare two files with StrComp! If you had a routine that reversed the
bytes in a block of memory, or that scanned memory for data, you could use it with memory-
mapped files. Furthermore, you can allow two programs to share memory by giving them both
access to the same file.
When you’re through using a file, you should close out the variables you have been using:
UnmapViewOfFile(Data);
CloseHandle(hMapping);
Once again, the purpose of this discussion of memory-mapped files is not to give you an ency-
clopedic overview of the entire, rather complex, subject. Instead, I just want to give you a feel-
ing for what memory-mapped files can do for you. This is a taste of something powerful that’s
meant to inspire you to pursue the subject further.
Listing 8.4. The MemMgr program shows how to track the number of calls you
make to GetMem and FreeMem.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
bAllocate: TButton;
bCheckStatus: TButton;
procedure FormCreate(Sender: TObject);
procedure bAllocateClick(Sender: TObject);
procedure bCheckStatusClick(Sender: TObject);
end;
var
Form1: TForm1;
GetMemCount: Integer;
FreeMemCount: Integer;
ReallocMemCount: Integer;
OldMemMgr: TMemoryManager;
implementation
{$R *.DFM}
const
NewMemMgr: TMemoryManager = (
GetMem: NewGetMem;
FreeMem: NewFreeMem;
ReallocMem: NewReallocMem);
continues
end.
The code in Listing 8.4 tracks the number of times you call GetMem and the number of times
you call FreeMem. It then subtracts the one number from the other, thereby informing you that
you may have called GetMem more times that you called FreeMem. In particular, this program has
a button called Allocate, which allocates 500 bytes 10 times, while only calling FreeMem once.
The result is that you find a skewed balance of calls to GetMem and FreeMem.
Inside SYSTEM.PAS there’s a record called MemoryManager of type TMemoryManager:
PMemoryManager = ^TMemoryManager;
TMemoryManager = record
GetMem: function(Size: Integer): Pointer;
FreeMem: function(P: Pointer): Integer;
ReallocMem: function(P: Pointer; Size: Integer): Pointer;
end;
The instance of this record in SYSTEM.PAS contains pointers to three functions that perform
the basic memory-management routines for Delphi: GetMem, FreeMem, and ReAllocMem. In par-
ticular, you want to declare a record that looks like this:
NewMemMgr: TMemoryManager = (
GetMem: NewGetMem;
FreeMem: NewFreeMem;
ReallocMem: NewReallocMem);
This record contains a set of pointers to new routines that replaces the old memory-manager
routines.
By calling GetMemoryManager ,
you can get hold of the old MemoryManger ; by calling
SetMemoryManager,
you can put a new memory manager in place. You should therefore pass
your new memory manager record to SetMemoryManager, which makes the replacement offi-
cial.
In this case, the new memory manager simply increments a number and then calls the routines
in the old MemoryManger. As a result, it can track the number of times you call GetMem and FreeMem.
This technique can help you be sure that you are not committing memory without also taking
the time to free it. Because these calls to GetMem and FreeMem have a small overhead, you might
want to remove this memory manager when you ship your code.
Summary
This chapter introduced you to some of the advanced features of Windows 95. In particular,
you got a broad overview of Win32 memory-management issues, such as allocation, memory-
mapped files, and heaps.
Besides providing specific coding examples, in the last two chapters I presented an overview of
Windows 95 multitasking and memory management. These are complex subjects, introduced
here in broad, general terms. However, this material should make you aware of some of the
major theories behind Windows 95 and Windows NT. Hopefully, I’ve pointed you in the right
direction so you can see where to concentrate your future studies.
At the end of this chapter I also presented a short example of how to use the Delphi memory
manager. I tacked this on in part because it had no better home, and in part because it pro-
vided a rather light and easy way to end a difficult chapter. In particular, it emphasized the fact
that Delphi programming often is much simpler than Windows API programming.
III
IN THIS PART
In this chapter, you get a look at the core API code that makes Windows programming pos-
sible. My goal is to show you not only how to write a Windows program with only API calls,
but also how to find the role this same code plays in the VCL.
The VCL has a magical quality to it. It seems to create your forms out of whole cloth and present
them to you as complete entities fabricated by the Delphi environment or compiler. However,
the VCL is really just a wrapper around the Windows API. It’s a very sophisticated, very clever
wrapper, but it’s still just a wrapper. The goal of this chapter is to begin to show you how that
wrapper works.
A second theme, however, runs in the background of this entire chapter. I am of the opinion
that Object Pascal is an excellent language to use if you want full access to all the features of the
Windows programming environment. Delphi allows low-level access to the most esoteric as-
pects of the operating system. In short, if you want to do sophisticated system programming,
there is no need to switch to another tool. Delphi has everything you need.
If you are new to the material in this chapter, you may find it a bit overwhelming at first. One
of the key points to remember is that the code that forms the core of each Windows program
generally needs to be written only once. Each subsequent Windows application that you build
can reuse this code with only minor changes. It’s difficult to get the code right the first time,
and it can be confusing to understand, or at least it was confusing to me the first time I saw it.
Once I had the basics down, then I could reuse the code as often as I liked just by copying it
from one application to the next with only minor changes.
All Delphi RAD programs have to include the Windows API calls shown in this chapter.
However, the VCL intentionally goes to great lengths to hide this code from your sight. It’s
pushed down the hierarchy of VCL objects and out of the view of casual Delphi programmers.
Still, this is the core code that lies at the bottom of all Delphi programs, and no programmer
could ever claim to understand either Windows or the VCL without knowing this material
both inside and out.
Straight—No Chaser
If you are new to the Windows API, you may find the pace of this chapter fairly arduous. Be-
cause I need to cover a lot of ground in a limited amount of space, I’m going to deal out the
whole deck in one sitting. These are core programming issues under discussion in this chapter,
and half the fun is wagering more than a little on each hand that’s dealt.
NOTE
A number of books are available that discuss Windows API programming in depth.
My personal favorite, needless to say, is Teach Yourself Windows 95 Programming in 21
Days, Second Edition by Charlie Calvert, published by Sams. A well-known book that
covers this same material is Programming Windows, by Charles Petzold, published by
Microsoft Press. If the material I’m about to discuss comes at you too fast for you to
assimilate, you might turn to either of these books. They are both written about the C
programming language, but when you are down to the Windows API level, it’s hard to
tell the difference between Pascal and C. They are virtually identical at this level.
The great advantage of exploring the Windows API in depth is that it helps you understand
how Delphi and the operating system on which it is built really work. If you can see the core
techniques used to create Windows programs, you can extrapolate from that information to
see how Delphi really works and what it can and can’t do.
Many people use Delphi because it allows you to build complex applications with relative ease
and rapidity. This is an excellent use of the tool, and one that I myself take advantage of on
many occasions. However, if you want to create really powerful programs, or if you want to
create components, you need to know about more than just the high-level tools. You need to
understand how Delphi works and why it works. This chapter, and the remaining chapters in
this part of the book, show you the underlying theory behind Windows programming and the
Delphi tools. Armed with that knowledge, you will find that there is no computer-based task
you cannot conquer with the Object Pascal language.
uses
Windows;
continues
case AMessage of
wm_Destroy: begin
PostQuitMessage(0);
Exit;
end;
end;
Result := hWindow;
end;
var
AMessage: TMsg;
hWindow: HWnd;
begin
if not WinRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
hWindow := WinCreate;
if hWindow = 0 then begin
MessageBox(0, ‘WinCreate failed’, nil, mb_Ok);
Exit;
end;
You can compile this program simply by loading it into the IDE and pressing the green com-
pilation arrow. Alternatively, you can compile it from the DOS prompt by issuing the follow-
ing command:
DCC32 WINDOW1.DPR
Note that this application has no form during design time. It is, therefore, radically different
from standard Delphi applications. Probably the best way to create this application is in a text
editor such as Notepad. You can then save the file as WINDOW1.DPR and compile it from
the command prompt.
If you want to create the file in the IDE, perform the following steps:
1. First, start a new project.
2. Remove unit one from the project using the Project Manager found in the View
menu. (First select UNIT1 in the Project Manager, then delete it by pressing the
button with a red minus symbol on it.)
3. Select View | Project Source from the menu and edit the main module so that it looks
like the code in Listing 9.1.
When you are done, compile and run the program. The code pops up a simple window.
This program is roughly equivalent to the form you would see in the simplest possible stan-
dard RAD-based Delphi applications. That is, if you opened up Delphi, then compiled and
ran the default “PROJECT1” application, the result would be very similar to what you see
when you compile and run Window1.
NOTE
In this section of the book, I refer to “RAD” applications and “Windows API” applica-
tions. By the former, I mean applications that are written in the Delphi IDE using the
Component Palette, Form Designer, and Object Inspector. By the latter, I mean
applications like WINDOW1.DPR that do not use visual tools. This section of the
book is about Windows API-based applications, but I constantly compare them to
applications that are built using the RAD tools. Of necessity, this meant that I needed
to come up with terms I could use to distinguish the two types of applications.
There is one very significant difference between a default Delphi RAD-based application and
the Window1 application just shown. This difference will become obvious if you copy the
executables from both projects into a single directory and run the DIR command.
Volume in drive C is HOST FOR C Serial number is 1ADA:2A4F
Directory of c:\foo\*.exe
As you can see, PROJECT1.EXE is about 16 times larger than WINDOW1.EXE. In fact,
WINDOW1.EXE is probably the smallest stand-alone Windows executable you have ever seen.
Visual Basic produces small executables, but they depend on a large DLL nearly half a mega-
byte in size, so they are really some fifty times larger than a standard Delphi app. PowerBuilder
typically needs several megabytes worth of support files before its applications can be run. C
also produces small, tight executables, but even the smallest C-based Windows applications
are usually considerably larger than the Window1 executable produced by Delphi.
Why is WINDOW1.EXE so much smaller than PROJECT1.EXE? The key issue here is that
the Window1 source code makes no use of Delphi objects. In short, the VCL is not linked into
WINDOW1.EXE, except perhaps in some very vestigial form.
The VCL takes up a lot of space. This should be no secret to people who read computer maga-
zines. In fact, many magazine editors, in their infinite wisdom, complain about something they
call “code bloat.” They state that applications keep getting bigger and bigger, and many of them
say that object-oriented libraries such as the VCL, OWL, or MFC represent one of the major
reasons for this rapid increase in the size of executables.
When read from a particular perspective, these pundits of the computer press are right on tar-
get in their colorful fulminations against “ignorant programmers.” After all, the code created
for a standard Delphi application is much larger than the code produced by a typical non-
object-oriented project.
I probably don’t need to spend much time defending OOP for the typical reader of this book,
however. Objects, and particularly component-based objects, save months of development time
on most projects. RAD tools make it easier to quickly develop applications that are relatively
free of bugs. Windows API programs, on the other hand, have a tendency to attract bugs just
as flypaper attracts flies. All RAD-based Delphi programmers, as well as OWL and MFC pro-
grammers, trade performance and size for the benefits of objects. Tools like Delphi that add
Rapid Application Development to the mix are even more appealing to developers who want
to quickly create bug-free programs.
When you weigh the relative merits of these systems, compiled RAD tools like Delphi win
hands down over both the straight Windows API applications and the applications based on
non-RAD object libraries like MFC or OWL. After all, users will always choose a larger, slower,
currently available application over a smaller, faster application that is promised to be deliv-
ered “real soon now.”
So, you might now ask, why do I bother to show you this Windows API-based application?
After all, it’s not a RAD-based program. It’s one of the smaller, faster applications championed
by the pundits of the computer press. If the Windows API is so hard to work with, and if I
personally prefer RAD tools, why should I bother to discuss Windows API programs in this
book? Well, the reason is that everything done in the Delphi VCL is ultimately handled through
the Windows API. Remember, the VCL is just a wrapper around the Windows API! The
Windows API lies at the heart of Delphi and at the heart of every other Windows tool, utility,
or service. If you understand the Windows API, you will be well on your way to understanding
the VCL. In fact, all of Windows programming will start to open out in front of you. Under-
stand Windows API-based applications, and you will know much more about how to get Delphi
RAD applications to do anything you want!
In other words, I don’t think most readers of this book really want to develop applications using
the techniques shown in WINDOW1.DPR. The only people who should try to build
production-quality applications using this technique are experienced programmers who have a
very good reason to try to build the smallest, fastest application possible. Of course, program-
mers who are building VCL components may also have good reason to use this kind of code in
their tools, but it probably will not appear in exactly this same form.
No, my point is not to promote development of Windows API-based applications. Instead, I
want to show how Delphi really works. I want to take this subject down to the metal, so that
you can see what is really going on. One of the benefits of that knowledge will be the ability to
create really tiny executables if you need them. However, my main goal is not so much optimi-
zation as education. If you don’t already understand them, learn about Windows API applica-
tions, and then apply that knowledge to the RAD tools given to you by Delphi. You should
only build Windows API-based applications if you are sure you know what you are getting
into, and you are sure you know why you want to do it. You should, however, find many times
when this kind of code can help you construct VCL components. In those situations, you may
want to use variations on this kind of code, but you will also want to protect consumers of your
components from the complexity of your implementation.
C programmers know that their entry point for a Windows application looks roughly like this:
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInstance,
LPSTR lpszCmdParam, int nCmdShow)
In an Object Pascal application, you don’t have to set up a function that takes all these param-
eters. Instead, the startup code for your app saves this information in global variables called
hInstance, ParamStr, and CmdShow. A PrevInstance variable was available in 16-bit Delphi (and
may still be available in 32-bit Delphi), but it serves no purpose in Win32 because there can be
only one executable in each virtual address space. The C WinMain code already shown still ref-
erences hPrevInstance in Win32, but this is only for portability reasons, and its value will al-
ways be set to zero.
The point here is that the WinMain function in a C program is analogous to the main block of
code in a Delphi program. By the main block of code, I mean the body of code between the
begin..end pair that appears at the bottom of the main module of your program. The begin-
ning of this block of code is a begin statement, and the end is an end statement with a period
after it.
There is no reason to assume that there is any inherent advantage in having your compiler pass
parameters to WinMain, rather than saving them off earlier in global variables. In fact, it is prob-
ably actually slower to push these variables on the stack before executing a function call to
WinMain, rather than just saving the variables in the data segment and then calling the initial-
ization sections of your units, which is what happens in Object Pascal. In other words, my guess
is that the Object Pascal technique for starting a Windows program is probably better optimized
than the technique used in standard C programs. However, there is no significant difference
between the two techniques, and either is completely acceptable. After all, every program takes
several computer ages to be loaded into memory. The user is going to notice the load time, not
the overhead of a single function call!
In general, I would discourage hard-core Delphi developers from trying to claim that Delphi is
faster than C. The two languages perform more or less identically overall. There are areas in
which one language is faster than the other, but overall, neither has a significant difference in
terms of performance. (If you are the spirited type who is determined to champion Object Pascal
over C, then the most significant feature to push is the size of the binary files it produces.)
NOTE
Remember that the initialization sections of your units will be called before the
main body of your code. Consider what would happen if you added the following unit
to the Window1 project:
unit Unit2;
interface
uses
Windows;
implementation
initialization
MessageBox(0, ‘hi’, nil, mb_Ok);
end.
Given the presence of this unit, and a reference to it in the uses clause for the main
body of your program, then this MessageBox function would be called before the main
body of your code in the primary unit. This means that in multiunit projects, the first
line of executable code is likely to be the initialization section of some unit!
I am now going to take you through the code for the Window1 program in very general terms.
This is the 25,000-foot overview. To begin, here are the first few lines from the main program
block of the Window1 program:
if not WinRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
hWindow := WinCreate;
if hWindow = 0 then begin
MessageBox(0, ‘WinCreate failed’, nil, mb_Ok);
Exit;
end;
The WinRegister and WinCreate functions are two local routines that first register and then
create the main window for the program. These two functions must complete successfully, or
there is no point in trying to run your program! These two functions are a little bit like the act
of starting a car and putting it in gear. If you can’t complete these steps correctly, there is no
point in talking about trying to take a drive to a particular location.
After the main window is created, the ShowWindow command is used to make it visible, and the
UpdateWindow command is used to force it to paint itself. The code then enters a message loop
that captures certain kinds of incoming messages, translates them, and sends them off to the
WindowProc. This loop continues until the user closes the main window, at which time the
application terminates and passes Windows the wParam value of the current message as an exit
code:
Halt(AMessage.wParam);
Halt is a common Pascal call for terminating an application. You can look it up in the online
help if you are interested, though it plays no important part in the current discussion. It’s just
a way to guarantee that your application exits with a certain exit code that you can define. If
you omitted the call, your program would still end without mishap, but it would not be quite
as kosher.
This section has given you the first of three overviews of the Window1 program that you will
get in this chapter. This overview has been very brief, painting a general picture in the broadest
possible outlines. The next three sections go over this material in more detail and from a slightly
different perspective. After you have read once about the WinRegister, WinCreate, and WindowProc
functions, I will review the whole process one more time. I’m using repetition here to help
reinforce some fairly complex ideas that might not be well understood ahead of time by all
readers of this book. It’s essential that you come to terms with this material. If you understand
how to register a window, create a window, and run its WindowProc, then you have laid a good
foundation on which to build an in-depth understanding of Windows programming.
Registering a Window
The first thing the main program block does is call WinRegister:
function WinRegister: Boolean;
var
WindowClass: TWndClass;
begin
WindowClass.Style := cs_hRedraw or cs_vRedraw;
WindowClass.lpfnWndProc := @WindowProc;
WindowClass.cbClsExtra := 0;
WindowClass.cbWndExtra := 0;
WindowClass.hInstance := HInstance;
WindowClass.hIcon := LoadIcon(0, idi_Application);
WindowClass.hCursor := LoadCursor(0, idc_Arrow);
WindowClass.hbrBackground := HBrush(Color_Window);
WindowClass.lpszMenuName := nil;
WindowClass.lpszClassName := AppName;
The TWndClass structure contains a good deal of information about the type of window you
want to register with the operating system. For instance, it specifies the icon associated with
the window:
WindowClass.hIcon := LoadIcon(0, idi_Application);
It also specifies the window’s background color, menu, and class name. Note that the follow-
ing code gives the background of your main window the same color given to the background
for a standard Delphi RAD-based form:
WindowClass.hbrBackground := HBrush(Color_Window);
In particular, Color_Window is a variable set in the Display applet from the Control Panel. It
happens that you can typecast this constant as a brush in order to get this color for your main
window. Delphi performs a similar trick for the main form in a standard RAD application.
Perhaps the single most important thing that happens in the WinRegister method is that you
tell Windows where it should send the messages associated with a window. For instance, if the
user clicks the mouse on the window, what function should be called in order to give your
application a chance to respond to the mouse click? Well, in this case, all messages associated
with the main window are passed to the function called WindowProc. The specific line of code
that tells Windows to pass the message to looks like this:
WindowClass.lpfnWndProc := @WindowProc;
If you look at the source for the Window1 program, you will see the WindowProc procedure. In
just a little while, I will discuss that procedure and explain how it works.
NOTE
This book is not meant to explain Windows API programming from the ground up.
That’s a major challenge, and the last time I took it on, the book I wrote turned out to
be about 1300 pages in length. It’s simply too much to attempt an explanation of both
Windows API programming and Delphi OOP programming, all within the same
volume. Nevertheless, I want some of the major concepts of Windows programming to
come through to you in fairly clear and easy-to-understand terms. As a result, I will
dedicate portions of this and the next two chapters to the subject of messages. I will
frequently revisit the subject of the WindowProc and the role it plays in Windows API
programming. In short, you need not worry if you don’t yet fully grasp how Windows
handles messages. That topic will be covered carefully in this book. If you need
additional information, you can read Teach Yourself Windows 95 Programming in 21
Days, Second Edition or Programming Windows.
Once you have filled out all the fields of the TWndProc structure, you can pass it to the
RegisterClass procedure:
This function is not the same as the VCL RegisterClass function used in component creation.
Indeed, in a regular OOP-based Delphi application, you would have to qualify the call if you
did not want to get a type mismatch:
Result := Windows.RegisterClass(WindowClass) <> 0;
The RegisterClass function registers your window with the operating system. It returns an
atom that uniquely identifies the class that you plan to create. If the call fails, then the function
returns 0, so you can assume that it succeeded if the call does not equal zero. In fact, the pre-
ceding code line sets Result to True as long as the function does not return 0. (Note that this
code is not compatible with Win16 applications because 16-bit Windows defined the
RegisterClass as a Boolean function. I will touch briefly on a method for compiling Windows
API code under 16-bit Windows in Chapter 10, “Dialogs and Resources.”)
NOTE
What does it mean to say that Windows defines RegisterClass? Why do I word things
this way?
Well, the point is that RegisterClass is a Windows API function. It’s defined by the
operating system. You can call this same RegisterClass function from a C program if
you like. There is no difference in calling this function from a Delphi program or a C
program.
It happens that Delphi also defines an internal routine called RegisterClass that is part
of the VCL. This internal routine is not part of the operating system. One of the big
chores that a Delphi programmer must face is discovering which routines are part of
the operating system, which are part of the core Object Pascal language, and which are
part of the VCL.
Routines that are defined in SYSTEM.PAS and SYSUTILS.PAS are generally consid-
ered to be part of the core Object Pascal language. At the time of this writing, if you
have the source code to Delphi, you will find these core Object Pascal files in the
..\DELPHI32\SOURCE\RTL\SYS subdirectory.
Routines that are defined in WINDOWS.PAS are part of the operating system. These
are the core functions supplied by Windows to all programs that want to run under
Windows 95 and Windows NT. Other files such as OLE2.PAS, WINSOCK.PAS,
DDEML.PAS, COMMCRTL.PAS, COMMDLG.PAS, and so on are also calls that
are resolved by the operating system. At the time of this writing, these files are found in
the ..\DELPHI32\SOURCE\RTL\WIN subdirectory of the source code to Delphi.
In general, all the other files or routines found in the ..\DELPHI32\LIB subdirectory
that are not part of the two categories previously mentioned are by default part of the
VCL. The core units in the VCL are CLASSES.PAS, FORMS.PAS,
COMCTRLS.PAS, GRAPHICS.PAS, STDCTRLS.PAS, and EXTCTRLS.PAS.
There are many other units that make up the VCL, many of which are very important.
At the time of this writing, these VCL files are found in the
..\DELPHI32\SOURCE\VCL and ..\DELPHI32\SOURCE\LIB subdirectory.
All of the files mentioned in this note come precompiled with every copy of Delphi.
The compiled versions of these files are stored in the ..\DELPHI32\LIB subdirectory.
At the time of this writing, the interfaces for these files are found in the
..\DELPHI32\DOCS subdirectory. In other words, if you don’t have the source code
to Delphi, you can find out quite a bit about the core source files by looking in the
DOCS subdirectory for files that end with an INT extension. I strongly recommend
getting the source code if you don’t have it already.
The TWndClass structure (defined in WINDOWS.PAS) is fairly long and complex. Neverthe-
less, the actual act of filling it out and passing it to RegisterClass is fairly straightforward from
a conceptual point of view. Specifically, you need to register your window class with the sys-
tem. To do so, you first fill out a record that describes your window class, and then you pass
that record to RegisterClass. It can be a bit confusing if it’s the first time you’ve seen it, but
the basic principles involved in registering a class are really fairly straightforward. Once again,
here are the steps:
1. Fill out the TWndClass record.
2. Pass it to RegisterClass.
If the call succeeds, you can go on to create the window. If the call fails, you should abort your
application immediately, as shown in the main block of code for the Window1 program.
A Delphi programmer, however, can be confused by all this talk of classes and class registra-
tion. It’s crucial to understand that the RegisterClass method has nothing to do with the Delphi
VCL, and in fact, a window class is not a Delphi object. In and of itself, Windows is not
object-oriented, and it does not support polymorphism, inheritance, or encapsulation. The
TWndClass structure and the Windows.RegisterClass method belong to the Windows API, not
to the VCL. They are part of the operating system, and Windows is not an object-oriented
operating system. The Windows classes referenced by TWndClass and RegisterClass are not
real objects, but are only vaguely object-like. Delphi wraps these object-like entities inside real
objects.
Creating a Window
After the Window1 class is registered with the system, the next step is to create it:
function WinCreate: HWnd;
var
hWindow: HWnd;
begin
hWindow := CreateWindow(AppName, ‘Object Pascal Window’,
ws_OverlappedWindow, cw_UseDefault, cw_UseDefault,
cw_UseDefault, cw_UseDefault, 0, 0, HInstance, nil);
Result := hWindow;
end;
The core of this function is the call to CreateWindow. The other two functions, ShowWindow and
UpdateWindow, are really just very simple routines that get the window up on the screen and
make sure it is painted correctly. From the programmer’s point of view, these latter calls are
essential, but relatively trivial. The hard part is calling CreateWindow.
The first parameter passed to CreateWindow is the name of a registered class. In this case, the
window you want to create is called Window1, and you just finished registering it in the call to
WinRegister. There are, however, various preregistered classes such as edits, listboxes, richedits,
listviews, and so on, and you can use the CreateWindow call to create these windows without
having to explicitly call RegisterWindow. The distinction between windows you register your-
self and those that are preregistered by Windows can be confusing at first. Take your time with
this subject. It’s not really that hard; there is just a lot of detail to master.
The second parameter you pass to CreateWindow is the title of your window, and the next four
parameters are its dimensions. If you want the window to open in a default size and shape se-
lected by Windows, then you can pass in cw_UseDefault rather than a specific number in the
four dimension parameters.
The next three parameters are the Windows parent, its menu, and its hInstance. Because this
is the main window of the application, it will have no parent. Menus also play no role in this
simple application. The hInstance of the program is available as a global variable, per the ex-
planation given earlier in this chapter.
The final parameter can be filled with user-defined data, or it can play a particular role in the
creation of MDI windows. However, for the purposes of the current discussion, it can simply
be set to nil and then ignored.
Here’s how the call to CreateWindow is defined:
function
CreateWindow(ClassName: PChar; { address of registered class name }
WindowName: PChar; { address of window name }
Style: LongInt; { window style }
X: Integer; { horizontal position of window }
Y: Integer; { vertical position of window }
Width: Integer { window width }
Height: Integer; { window height }
WndParent: HWnd; { handle of parent or owner window }
Menu: HMenu; { handle of menu }
Instance: THandle; { handle of application }
Param: Pointer { address of window-creation data }
): HWnd;
Remember, these functions are not defined in the main Delphi help, but in a separate help file
accessible from the help menu. In the shipping version of Delphi, this file will probably be
called WINAPI32.HLP.
If you want to know more about the CreateWindow function, you can read the online help, you
can buy a book like Teach Yourself Windows 95 Programming in 21 Days, Second Editon, or
you can browse the Microsoft Developer’s CD. From my point of view, I find that it really
doesn’t matter much if the book or help file I am reading is meant for C programmers or Pas-
cal programmers. CreateWindow looks the same in C or in Pascal, with a few minor differences.
For instance, this is what the call looks like in C:
HWND CreateWindow(
LPCTSTR lpClassName, // address of registered class name
LPCTSTR lpWindowName, // address of window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle of parent or owner window
HMENU hMenu, // handle of menu or child-window identifier
HANDLE hInstance, // handle of application instance
LPVOID lpParam // address of window-creation data
);
To me, there is no significant difference in making this call from a C application or a Delphi
application. It’s the same function either way—it’s just that each language has slightly differ-
ent type names.
NOTE
If I seem to be making a big point of the similarities between C and Pascal, that’s just
because I have found that many people don’t seem to understand how similar the two
languages are to each other, especially when you are down at the level being discussed
in this chapter. Once again, my point is not that one is better than the other, but just
that there are many parallels between the two languages.
There is no hope for an application if it does not have a main window, so you need to shut
down your app if this call fails.
Before closing this section, I should point out that a call to CreateWindow causes Windows to
send a wm_Create message to the WindowProc associated with the window in question. This
wm_Create message gives you a chance to initialize data associated with the window. I will
explain more about this process later in this chapter and in several other places in this book.
case AMessage of
wm_Destroy: begin
PostQuitMessage(0);
Exit;
end;
end;
This procedure takes four parameters. This isn’t a matter of choice. It has to take four param-
eters, and they have to be of the exact same type shown. The name you give to the procedure
does not matter, and the names of the parameters you pass to it do not matter. However, the
types of the parameters passed to this procedure are set in stone.
NOTE
Remember that this procedure was assigned to the lpfnWndProc field of the TWndClass
structure. Your duty as a Windows programmer is to declare a procedure that takes the
types defined in the call to WindowProc. To create a standard Windows program, you
must declare a procedure of this type, and you must assign it to the lpfnWndProc field
of the TWndClass structure passed to RegisterClass.
There are other ways to write Windows programs, but 98 percent of all Windows
programs, including all Delphi RAD programs, are built in just this way. In other
words, Delphi RAD programs also have a WndProc, and it is identical in kind to the one
shown here. Delphi goes to great length to hide this fact from you, but at bottom,
there is no easy way to escape this architecture. Windows programs are built around
WndProcs; that’s all there is to it!
Please note that I use the term WndProc when I am referring to the generic concept of a
window procedure, but that I frequently name explicit instances of this procedure
WindowProc. The spelling WndProc comes from Microsoft type declarations in various C
header files, whereas the easier-to-read-and-use WindowProc is associated more with
explicit instances of the function. Both words are correct, and for most practical
purposes they are interchangeable. However, WndProc hints at a concept, whereas
WindowProc usually refers to a specific procedure in my code.
The signature of a WndProc function is predefined by Windows. You can even look it up in the
online help if you want. Just search in the help index for WindowProc, and you will find some-
thing like the following definition:
Function WindowProc(
Window: HWnd; /* handle of window */
AMsg: LongInt; /* message */
wParam: LongInt; /* first message parameter */
lParam: LongInt; /* second message parameter */
): LongInt;
The first parameter is the handle of your main window. The handle of your window is like the
handle of a file: it’s a number that uniquely identifies your window. Whenever you need to
talk to Windows about the traits of a particular window, you can identify the window in ques-
tion by passing the operating systems its handle, that is, its HWnd.
The second parameter of this function is the type of the message coming in. Each message in
Windows has its own unique identifier. For instance, when you move the mouse, the window
procedure gets a wm_MouseMove message. When you press a key, it gets a wm_KeyDown message.
When the window is about to close, it gets a wm_Destroy message. There are thousands of these
messages, and expert Windows programmers know many of them by heart. Macho dudes pump
iron, macho Window programmers memorize Windows messages, but I survive by mastering
the online help system! To each his own.
The last two parameters, wParam and lParam, hold data that will differ depending on the type of
message being sent. For instance, if you move the mouse, the X and Y coordinates of the mouse’s
new position will be passed in these parameters. Don’t worry about getting at that information
quite yet. I will explain more about message handling in several other places in this book.
The body of a window procedure consists primarily of a case statement that handles all the
messages sent to the window. Most of these messages can be passed on untouched to a default
internal window procedure referenced by the DefWindowProc function. However, there are usually
some messages that require explicit handling. In this case, the only one you need to worry about
is the wm_Destroy message. This message is sent to tell you that the user or operating system is
closing down this window. Because this is the only window in this application, the appropriate
response to this request is to close down the whole application. You can do this by calling
PostQuitMessage.
Once again: wm_Destroy messages are sent to your window when it is time for that window to
close. When the main window of an application is closed, its duty is to shut down the whole
application. You can close the application by calling PostQuitMessage. PostQuitMessage sets a
flag that tells the application it’s time to exit the message loop; that is, it causes Windows to
send a wm_Quit message:
while GetMessage(AMessage, 0, 0, 0) do begin
TranslateMessage(AMessage);
DispatchMessage(AMessage);
end;
GetMessage returns false after PostQuitMessage has been called, that is, after it retrieves a wm_Quit
message from the message queue. Once again: the message loop continues until it gets a wm_Quit
message. A call to PostQuitMessage causes a wm_Quit message to be placed in the thread’s mes-
sage queue.
A message queue is just a place where messages can be stacked up while they are waiting to be
processed. There is a function called SendMessage that sends messages directly to a WndProc.
There is a second function called PostMessage that places messages in the message queue. Each
thread in a Win32 application has its own message queue. Don’t worry if some of this doesn’t
quite make sense yet. Messages are a little complex at first, but the experienced programmers
who are reading this book could tell you that this subject is much simpler than it seems at first.
Before leaving this subject, I should point out that if there had been other memory allocations
associated with your window, then you could deallocate that memory in response to a wm_Destroy
message. In other words, the response to a wm_Destroy message can include code for disposing
memory:
wm_Destroy: begin
Dispose(MyRecord);
FreeMem(MyPointer, MySize);
PostQuitMessage(0);
Exit;
end;
I will touch on this subject again in the next chapter, “Dialogs and Resources.”
The call to DefWindowProc at the end of this window procedure is simply a way of letting Win-
dows handle the default behavior for your window. For instance, all windows know by default
how to handle certain aspects of being redrawn, and how to handle being resized. To make
sure that all this behavior occurs automatically, you must pass all unhandled messages through
to DefWindowProc. DefWindowProc passes your call on to an internal routine that can be thought
of as something like a mirror image of your own window procedure. Of course, the body of the
routine is different, but its structure and purpose are similar to your own window procedure.
As explained previously, its job is to execute any default behavior associated with your win-
dow.
If you explicitly handle a message, then you have a choice of passing it on to DefWindowProc or
not, depending on your circumstances, and depending on the nature of the particular window
in question. Some classic bugs are introduced into programs because someone handles a par-
ticular message and then forgets to pass it on to DefWindowProc.
In this section, you have had a first look at window procedures. These little jewels are the heart
and soul of most Windows programs. I will therefore go over this material several more times,
in several different contexts, in various different parts of this book. It’s okay if it doesn’t make
sense to you yet, but you will never really understand the Windows API until you understand
what WndProcs do, and how they should be constructed.
WindowClass.hbrBackground := HBrush(Color_Window);
WindowClass.lpszMenuName := nil;
WindowClass.lpszClassName := AppName;
These are very advanced programming issues and will not be given much consider-
ation in this book. If you want to futz around with the message queue, you should
read books by Jeffrey Richter, Andrew Schulman, Kim Crouse, or Matt Pietrek.
Don’t worry if all of this doesn’t make sense to you yet—it’s not supposed to. The point of this
section of the chapter is simply to give you an overview of a typical Windows program.
Right now, you only need to think about the broadest issues, such as the three steps in the
main block of the Window1 program:
1. Register the window.
2. Create the window.
3. Enter the message loop.
There it is, just as simple as one, two, three!
The other key part of the program is the WndProc procedure, which responds to messages a
program receives:
function WindowProc(Window: HWnd; AMessage, WParam,
LParam: Longint): Longint; stdcall; export;
begin
WindowProc := 0;
case AMessage of
wm_Destroy: begin
PostQuitMessage(0);
Exit;
end;
end;
The Window1 program only responds explicitly to wm_Destroy messages. All other messages
get passed on to DefWindowProc. The DefWindowProc procedure does nothing more than handle
the default behavior associated with a window. In other words, when you maximize a window
or minimize a window, the DefWindowProc procedure supplies the necessary know-how to ex-
ecute these maneuvers.
DefWindowProc doesn’t, however, handle wm_Destroy messages. That is your duty as a program-
mer. When the message comes down the pike, just call PostQuitMessage; this will cause a wm_Quit
message to be placed in the queue. After PostQuitMessage has been called, you can exit WndProc
with a return value of 0.
For now, that’s all you need to know about the Window1 program. To summarize:
■ There are two main parts in a program. The first is the main block of the program; the
second is the WndProc procedure.
■ The main block of the program has three parts. The first part registers the Window,
the second creates the window, and the third sends messages to the window through a
loop.
■ Any messages sent to a window pass through WndProc. This window procedure can
explicitly handle the messages, or it can pass them on to DefWindowProc, which is the
default message handler. In fact, some programs first handle the message, and then
pass it on to DefWindowProc for further processing.
I hope the repetition in the preceding sections hasn’t driven you totally insane. I’ve used it so
heavily here because I want to do everything I can to drive home some complicated, but very
important, issues. My hope is that even newcomers to this subject will finally throw up their
hands and say: “Okay, Charlie, I get it! You don’t have to say it again. It’s clear to me now!” If
that happens, then you can rest assured that you understand the overall flow of a typical Win-
dows program. Just get the broad issues clear in your mind, and then the details will become
clear in the future. Armed with this knowledge, there are many, many important Windows
programming issues that you are now prepared to grasp.
begin
WindowClass.hInstance := HInstance;
if Windows.RegisterClass(WindowClass) = 0 then
raise EOutOfResources.CreateRes(SWindowClass);
end;
FHandle := CreateWindow(WindowClass.lpszClassName, ModuleName,
WS_POPUP + WS_VISIBLE + WS_CLIPSIBLINGS + WS_SYSMENU + WS_MINIMIZEBOX,
GetSystemMetrics(SM_CXSCREEN) div 2,
GetSystemMetrics(SM_CYSCREEN) div 2,
0, 0, 0, 0, HInstance, nil);
SetWindowLong(FHandle, GWL_WNDPROC, Longint(FObjectInstance));
if NewStyleControls then
SendMessage(FHandle, WM_SETICON, 1, GetIconHandle);
end;
end;
The point here is that the Delphi VCL makes the same calls that are made in WINDOW1.DPR.
In fact, it has to make these calls! There is no other way to create a standard window and show
it to the user.
Let’s move away from the Forms unit, and take a look at the TWinControl implementation in
the Controls unit:
procedure TWinControl.CreateWindowHandle(const Params: TCreateParams);
begin
with Params do
FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,
X, Y, Width, Height, WndParent, 0, HInstance, Param);
end;
CreateWindowEx is just like CreateWindow, except that it takes one extra parameter that shows
up in the first position. The point here is that TForm, TEdit, TListBox, and many of the other
major controls are all descended from TWinControl. The CreateWindowHandle procedure dem-
onstrates that these core Delphi components all have their bases in the fundamental call to
CreateWindow that is shown in the Window1 example.
Going on just one step further, let’s look at the TCreateParams structure:
TCreateParams = record
Caption: PChar;
Style: Longint;
ExStyle: Longint;
X, Y: Integer;
Width, Height: Integer;
WndParent: HWnd;
Param: Pointer;
WindowClass: TWndClass;
WinClassName: array[0..63] of Char;
end;
This vitally important structure holds most of the parameters that are passed to CreateWindow.
As it happens, you have direct access to this structure through a call to a function called
CreateParam. Every form calls CreateParam before it is shown. This is a virtual method, and
you can override it if you want to change the default values passed in the TCreateParams struc-
ture. The Provider program, discussed later in this book, makes heavy use of this program-
ming technique. Here is an excerpt from that program:
procedure TCompanyForm.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
Params.Style := Params.Style and not ws_Caption;
Params.Style := Params.Style and not ws_ThickFrame;
Params.Style := Params.Style or ws_Border;
end;
This code gives you the same control over the parameters passed to RegisterClass as you would
get in a standard Windows API application. It’s not as fast, nor quite as clean, as standard API
code, but the control is there if you need it.
Overriding TForm.WndProc
Besides the CreateParam function, another TForm routine you might want to override is called
TForm.WndProc. All the messages sent to your application pass through this routine, just as all
the messages sent to the Window1 program pass through the WindowProc routine.
The NWndProc program, shown in Listing 9.2, demonstrates how to override the TForm.Wnd
procedure.
The NWndProc program gives you some of the clues you need to start digging into the struc-
ture of the Delphi VCL from the perspective of a Windows API programmer. You should use
this program as a starting point and beginner’s guide to the types of riches found when you
study the VCL. (See Listing 9.2.)
Listing 9.2. The NWndProc program shows how to override the window
procedure of a form.
unit main;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
procedure FormClick(Sender: TObject);
private
ShowMessages: Boolean;
protected
procedure WndProc(var Msg: TMessage); override;
end;
var
Form1: TForm1;
implementation
uses
MsgForm;
{$R *.DFM}
continues
end.
When you first launch the NWndProc program, it appears as a normal form with some text on
it. If you click it once, however, a second form pops up. This second form will process all the
messages sent to the main form and display them in a list box. (In Windows 95, it will be pos-
sible to fill up the list box, so run the program for only a few minutes at a time.)
The overridden WndProc procedure itself is very simple:
procedure TForm1.WndProc(var Msg: TMessage);
begin
if Msg.Msg = wm_Close then ShowMessages := False;
if ShowMessages then
HandleMessages(Msg);
inherited WndProc(Msg);
end;
This code first checks to see if the message passed in is wm_Close. If so, it is simply passed to the
inherited WndProc, which is roughly the equivalent of calling DefWindowProc. If the message is
not wm_Close, then it is passed on to HandleMessages, which displays the message to the user.
After it has been displayed, the message is then passed on to DefWindowProc through the inher-
ited WndProc call.
Note that the TForm.WndProc procedure takes only one parameter. This is clearly not the origi-
nal WndProc that handles four separate parameters. Instead, it is a secondary function called by
internal VCL routines. Nevertheless, you will find that all the information stored in lParam
and wParam has been encapsulated inside the TMessage structure and is there for your perusal if
you need it. In fact, you will find that the TMessage structures have parsed this information and
made it more readily available for your use. Here is the TMessage structure as it appears in
MESSAGES.PAS:
TMessage = record
Msg: Cardinal;
case Integer of
0: (
WParam: Longint;
LParam: Longint;
Result: Longint);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;
The HandleMessages routine explicitly handles certain very frequently delivered messages, and
then gives you the Hex value of any other message. If you want to find out what these other
messages are, you can look them up in MESSAGES.PAS, using the Hex value as a key to search
on. Here is a simplified version of the routine that highlights its structure:
procedure HandleMessages(var Msg: TMessage);
var
S: string;
begin
if Msg.Msg = wm_Paint then …
if Msg.Msg = wm_MouseMove then …
if Msg.Msg = wm_KeyDown then …
if Msg.Msg = wm_NCHITTEST then …
end else begin
S := Format(‘$%x’, [Msg.Msg]);
ShowMsg(S);
end;
Note that this function acts like a simplified version of the WinSight utility that ships with Delphi.
If you want, you can try to rewrite the Window1 program so that it acts in the same way as
NWndProc. If you try this latter idea, you should use the Windows TextOut function to dis-
play text on the main screen of the application.
I’m not going to spend any more time describing the NWndProc program. It’s not really that
complex, and it is meant only to show you the type of things you can do with a solid under-
standing of standard Windows programs. If you want to be a really good Delphi programmer,
then you should take the information you have seen in this chapter and plunge into the Delphi
source code with a will. This knowledge forms the basis for the skillsets of all the best Win-
dows programmers I’ve met.
If you already know this material from your experiences with C or Turbo Pascal, this chapter
has shown you how to apply that knowledge to Delphi applications. The key point to grasp is
that most Windows programs are the same underneath. Fortunately, Delphi offers you a trans-
parent object wrapper that lets you dig down to these core routines when you need them.
Summary
In this chapter, you have learned some of the key points about the Windows API. In particu-
lar, you have seen how to write a Delphi program that uses straight Windows API code in lieu
of VCL objects. You also saw how to apply your knowledge of Windows API code to VCL
objects. One of the main themes of this chapter is that the VCL builds applications using the
techniques shown in the Window1 program. If you understand Window1, then you under-
stand some of the core functions in the VCL.
This chapter has also showered the Object Pascal language with rather lavish praise. I want to
stress that my point is not to fight a language war, but only to point out that Pascal is an excellent
language for writing low-level system code. In fact, Delphi itself is written in Object Pascal
because there was no need to turn to another language for aid in accessing low-level routines.
Object Pascal is a very fast, very tight language with a clean syntax and a quick compilation
time. C is also a very fine language, with a number of great strengths, some of which have no
parallel in the Pascal language. However, you may find that you prefer working in Pascal, espe-
cially when you need to dig into the darkest, most obscure recesses of the Windows API. Pascal
is a very well-constructed language, and Delphi’s developers have found a way to polish it so
that it gleams in the sun and glows in the moonlight.
Dialogs and
Resources 10
This chapter contains several different topics, most of which are related to the Windows API,
dialogs, or resources. In particular, you will see the following:
■ How to create old style Windows dialogs in a straight API-based program.
■ How to compile Delphi applications from the command line.
■ How to write one set of Windows API code that compiles under both 16-bit Win-
dows and Win32.
The obvious themes of this chapter are dialog boxes and messages. However, the main focus is
still the Windows API, and I spend considerable time refining and explaining the way stan-
dard Windows API programs are designed and written. In particular, there is an introduction
to handling messages in the WndProc function and a discussion about how this task can be op-
timized in a Windows application. This basic subject is carried over into the next two chapters,
which serve to nail down the whole topic of the Windows messaging system.
This chapter spends a considerable amount of time discussing compiling from the command
line and creating batch and makefiles that you can use from the command line. This material
might appear to be not quite as earth-shattering in its importance as the rest of the code in this
chapter nor as the code you saw in the previous chapter or will see in the next chapter. How-
ever, this material has to be covered, because it is essential programming information. Have a
little patience with the command line material, but be sure not to miss the information on
dialog boxes and resources. That’s key information.
is not a significant difference, and in fact you will find that both types of dialog boxes look, act,
and feel exactly the same. The only difference is in the amount of memory they consume and
the speed at which they execute.
NOTE
One final note of clarification is needed here. Adding a VCL dialog box to a Delphi
program does not necessarily provide a 150KB hit to your program. If you put one
dialog box in a DLL, you will get a 150KB hit. If you put two VCL dialog boxes in
that DLL, the DLL remains essentially the same size. It’s the first hit that is costly. In a
standard Delphi program, all the code required to build a dialog box is also required to
build the main form for the program. So adding one dialog box to an existing Delphi
program is unlikely to make your code significantly larger. The first hit is costly, but
once you have added that code into your project you can reuse it at relatively little cost
to the size of your project. As a result, it’s primarily when you start putting dialog
boxes into DLLs that the technique shown here becomes valuable.
This chapter concentrates on creating dialog boxes and using resources. However, I offer sev-
eral examples of putting dialog boxes in DLLs. This code appears in Chapter 13, “The Win-
dows API and DLLs,” and in Chapter 14, “Windows Controls and Callbacks.”
FIGURE 10.1.
The main window, menu,
and dialog box for the
Window2 program.
uses
Windows, Messages; // The wm_XXX constants moved from Windows to Messages
{$I WINDOW2.INC}
{$R WINDRES.RES}
const
AppName = ‘Window2’;
wm_Command:
if (WParam = idOk) or (WParam = idCancel) then begin
EndDialog(Dialog, 1);
Exit;
end;
end;
Result := False;
end;
continues
wm_Destroy:
begin
PostQuitMessage(0);
Exit;
end;
end;
var
AMessage: TMsg;
HWindow: HWnd;
begin
if not DoRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
HWindow := Create;
if HWindow = 0 then begin
ShowWindow(hWindow, CmdShow);
UpdateWindow(hWindow);
Halt(AMessage.wParam);
end.
const
cm_About = 100;
#include “window2.inc”
// The Icon
WindowIcon ICON “WINDOW2.ICO”
// The Menu
WindowMenu MENU
BEGIN
MENUITEM “&About”, cm_About
END
continues
To compile this program you first need to compile its resource from the command prompt
using the following command:
BRCC32 -r windres.rc
This produces a file called WINDRES.RES. Once this file exists, the Window2 program com-
piles smoothly inside the IDE.
NOTE
A more detailed discussion of resource files is included later in the chapter, along with
a lengthy discussion of compiling from the command prompt. If you can’t get this
application to compile successfully right away, don’t despair. There are plenty of clues
as to what might be wrong scattered among the next few pages of this book.
When this program is first launched, it appears as a standard window with a simple menu at
the top. If you select the sole item in the menu, a dialog box appears. The dialog box has a
button and some text in it. If you press the button, the dialog box closes.
Using the Delphi RAD tools, it would be trivial to construct a program of this type. However,
the task is considerably more complex when you are working with the straight Windows API.
The primary benefit of the Window2 program, however, is that it’s about one tenth the size of
a standard Delphi VCL program of similar capability. To get started understanding how it
works, you first need to understand something about resources.
RC text files have code in them that follows a very simple syntax for defining which icons and
bitmaps you want to build into your application, as well as any menus, dialog boxes, or custom
resources you might want to add.
The Window2 program has one RES file called WINDRES.RES. The source for this file is
shown in Listing 10.3. It contains an icon, a simple menu, and a simple dialog box.
Here is the code found in WINDRES.RC for including a simple icon:
// The Icon
WindowIcon ICON “WINDOW2.ICO”
The first line of the file shown here contains a C style comment, which is the traditional way
to add comments to resource files. The second line of code tells BRCC32 to find a file called
WINDOW2.ICO. Furthermore, it states that the content of WINDOW2.ICO is an ICON,
and that it should be compiled into a resource and labeled WindowIcon:
■ Filename: Window2.ico
■ Resource Type: ICON
■ Identifier labeling resource: WindowIcon
As a rule, a word such as ICON is not processed by BRCC32—it is simply stored for later use.
In other words, resource files are just chunks of binary data sorted by name and type, where
anyone can define a new type whenever they want. Resource files store names like ICON so
that you can find out what kind of resource is available, not because they treat icons any differ-
ently than any other chunk of binary data you might pass to them.
The following code adds a menu to the resource:
// The Menu
WindowMenu MENU
BEGIN
MENUITEM “&About”, cm_About
END
The word WindowMenu is an identifier selected by the programmer to label the menu for later
retrieval. Writing
WindowMenu MENU
That is, the word WindowMenu is a variable declaration for a resource file, and the word MENU is
a type declaration for a resource file. (Obviously, the syntax for RC files has nothing to do with
Object Pascal.) The code tells the resource compiler that you want to define a menu, and that
you want the menu to be called WindowMenu.
The BEGIN..END pair that follows the opening line of the menu declaration is used exactly as a
BEGIN..END pair would be used in a Delphi application. In other words, it’s a syntactical con-
struct for designating the beginning and ending of a block of code. In RC files you can also use
C style curly braces:
WindowMenu MENU
{
MENUITEM “&About”, cm_About
}
From a performance point of view, no advantage adheres to either syntactical construct. You
can simply choose the technique that you feel is clearest and easiest to read.
The single line between the BEGIN..END pair defines the only visible part of this menu:
MENUITEM “&About”, cm_About
This code tells BRCC32 that you want to create a menu item with the text “About” in it. A
menu item is the visible, textual, part of a menu. In a typical pop-up File menu, menu items
include Open, Save, and Close.
NOTE
If you want to work with a more complex menu, you can view the code shown here:
MENU_1 MENU
BEGIN
POPUP “&File”
BEGIN
MENUITEM “&New”, CM_NEW
MENUITEM “&Open...”, CM_OPEN
MENUITEM “&Save”, CM_SAVE
MENUITEM “Save &as...”, CM_SAVEAS,
MENUITEM SEPARATOR
POPUP “&Print...”
BEGIN
MENUITEM “Print To Disk”, CM_PRNDISK
MENUITEM “Print LPT1”, CM_PRNLPT1
END
MENUITEM “Page se&tup...”, CM_PAGESETUP
MENUITEM “P&rinter setup...”, CM_PRINTERSETUP
MENUITEM SEPARATOR
MENUITEM “E&xit”, CM_EXIT
END
POPUP “&Cursors”
BEGIN
MENUITEM “&Round Cursor\tAlt+F1”, CM_ROUNDCURSOR
MENUITEM “&Diamond Cursor\tCtrl+F2”, CM_DIAMONDCURSOR
MENUITEM “&Draw Icon\tShift+F3”, CM_ICONCURSOR
END
POPUP “BitCursors”
BEGIN
MENUITEM “Item”, CM_BITMENU1
MENUITEM “Item”, CM_BITMENU2
MENUITEM “Item”, CM_BITMENU3
END
MENUITEM “&Help”, CM_HELP
END
This menu has a series of pop-up menus like the kind you would find in a standard
Windows program such as Notepad or WordPad. You could define the following
constants inside the RC file so that it would compile:
#define CM_NEW 101
#define CM_OPEN 102
#define CM_SAVE 103
#define CM_SAVEAS 104
#define CM_PRNDISK 151
#define CM_PRNLPT1 152
#define CM_PAGESETUP 105
#define CM_PRINTERSETUP 106
#define CM_EXIT 107
The menu item with a label that reads &About declared earlier has an ID associated with it. This
ID is represented by the constant CM_ABOUT. Whenever the user clicks this menu item, a mes-
sage with the value CM_ABOUT associated with it is sent to your WndProc.
cm_About is a standard constant defined like this:
const
cm_About = 100;
You should not define this constant inside the main body of WINDOW2.DPR, nor inside
the RC file. The issue is that the value needs to be seen by both WINDOW2.DPR and
WINDRES.RC. The best solution in cases like this is to start a small separate file meant for
storing global variables that will be used by two or more modules in your program.
In this case, the small global file is called WINDOW2.INC, and it looks like this:
const
cm_About = 100;
This is the entire include file, complete with all the syntax necessary to make it compile prop-
erly. In other words, there is no interface section, no implementation, no anything. It’s just a
code fragment.
You can link an include file into any Object Pascal program with the following syntax:
{$I WINDOW2.INC}
This code tells the compiler to insert the file called WINDOW2.INC at this place in the file.
In terms of the final binary executable produced by the compiler, there is absolutely no differ-
ence between placing code in an include file and placing code in a module at the exact location
where the {$I ...} directive is located.
In the preceding example, the include directive looks like this:
uses
Windows;
{$I WINDOW2.INC}
{$R WINDRES.RES}
const
AppName = ‘Window2’;
If I removed the include directive and inserted the constant declaration for cm_About directly
into the code, there would be no change in the binary imprint of the executable:
uses
Windows;
const
cm_About = 100;
{$R WINDRES.RES}
const
AppName = ‘Window2’;
In other words, the two code fragments shown here are, for all intents and purposes, identical.
One happens to use an include file and the other doesn’t, but they both say the exact same
thing, and the compiler interprets both code fragments as being identical.
If you want to link an include file into an RC file, you can use the following syntax:
#include “window2.inc”
This inserts the code from WINDOW2.INC into the RC file, just as the {$I WINDOW2.INC}
syntax inserts the code into the DPR file.
NOTE
You can also insert references to PAS files into RC files that are to be compiled by
BRCC32. (However, this won’t work with Microsoft’s resource compiler.)
For instance, you could create the following file in lieu of WINDOW2.INC:
unit Globals;
{ Name : GLOBALS.PAS
Author : Copyright (c) 1995 by Charlie Calvert
Description : Contains variables used in WINDOW2.DPR }
interface
const
cm_About = 100;
implementation
end.
You could then link this file into WINDOW2.DPR as you would link in any other
PAS file, and you could link it into WINDRES.RC with the following syntax:
#include “globals.pas”
So that you can have examples of both types of syntax, I have implemented this
technique in a version of the Window2 program that is included on disk in the
subdirectory called Window2a.
The following code fragment shows how to define a dialog box in a resource file:
// The About Dialog
About DIALOG 18, 18, 141, 58
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION “About Dialog”
BEGIN
PUSHBUTTON “Ok”, IDOK, 5, 39, 132, 12,
WS_CHILD | WS_VISIBLE | WS_TABSTOP
CTEXT “Object Pascal Example”, -1, 1, 9, 140, 8,
WS_CHILD | WS_VISIBLE | WS_GROUP
CTEXT “Copyright (c) World Community, Inc.”,
-1, 1, 23, 140, 10,
WS_CHILD | WS_VISIBLE | WS_GROUP
END
The opening line of this code gives the dialog box a name, declares its type so the resource
compiler knows what to do with it, and defines its dimensions:
About DIALOG 18, 18, 141, 58
The next line of code defines the styles associated with the dialog box. These are the same styles
you would pass in the third parameter of a call to CreateWindow.
The code that appears between the BEGIN..END pair defines the three controls that reside on the
dialog box’s surface. One is a button, of essentially the same type as created by the TButton
object found on the Component Palette. The other two controls are like the TLabel controls
found on the Component Palette. However, unlike the button controls, these controls are not
identical to the Delphi TLabel controls. They look, feel, and act very much the same, but at
bottom they are quite different. In particular, the CTEXT items shown earlier use the static class
defined by Windows.
TLabel components are defined inside Delphi. In particular, they are Delphi objects that have
no Window handle, and they are drawn on the surface of the object on which they lie. The
main point, however, is that they are not part of the operating system. TLabel objects are not
the same as static controls defined with the CTEXT directive.
The TButton, TEdit, TListBox, TComboBox, TRadioBox, and TCheckBox controls from the
Component Palette are all real Windows controls. For instance, the push button control in the
preceding dialog box is the same type of push button control used in Delphi RAD programs.
The TLabel and TShape controls are fabrications made up of Delphi objects. The developers
of Delphi chose to create the TLabel control because they could make something smaller, faster,
and less expensive to use than the standard static control displayed in Window2’s dialog box.
NOTE
You can compile many different resources into a single RES file. For instance, the
following short RC file would compile six bitmaps, an icon, and a string resource into
an RES file that could be linked into your executable:
Body BITMAP “body.bmp”
Head BITMAP “head.bmp”
Pattern BITMAP “pattern.bmp”
Grass BITMAP “grass.bmp”
Road BITMAP “road.bmp”
Road2 BITMAP “road2.bmp”
Icon ICON “file1.ico”
STRINGTABLE
BEGIN
0, “Woodnotes (Part 1)”
1, “by Ralph Waldo Emerson”
2, “”
3, “When the pine tosses its cones”
4, “To the song of its waterfall tones,”
5, “...”
6, “Wealth to the cunning artist who can work”
7, “This matchless strength. Where shall he find, O waves!”
8, “A load your Atlas shoulders cannot lift?”
END
You could retrieve the bitmaps at runtime by calling LoadBitmap and the strings from
the stringtable by calling LoadString.
That’s all I’m going to say about RC files for now. If you have had trouble compiling this ex-
ample, hold on just a moment longer, and I will discuss compiling from the command prompt.
This code tells Delphi to search for and load a resource file with the same name as the current
source module, but with an RES extension.
NOTE
I call the RES file for the Window2 program WINRES.RES. This is because Delphi,
by default, works with a resource file that has the same name as the executable. This
resource file belongs to Delphi, the programming environment, and it’s best not to
have a RES file that you have created with the same name as the executable itself. Just
consider the executable name as reserved when you are working with a resource file for
a Delphi program.
The following very short RC file, called CLIENT1.RC, defines the standard RES file that ac-
companies a Delphi application:
/* Client1 Icon */
Icon ICON “client1.ico”
These standard Delphi resource files contain nothing more than an icon. Normally these files
are automatically built for you by Delphi, but I am showing how to make one so that you will
understand the technology involved. You can use the Resource Workshop to examine a stan-
dard VCL resource file. Ask it to convert the file into an RC file. If you do this, you will see
that its code is identical to the code shown earlier.
BRCC32.EXE compiles the RC file if you enter the following command at the DOS prompt:
BRCC32 -r CLIENT1.RC.
This command produces a file called CLIENT1.RES that contains a single icon. You can ex-
amine this file in the Image Editor from the Tools menu. It can also be loaded into the Re-
source Workshop, if you have that tool available.
NOTE
Experienced Borland C++ and Borland Pascal programmers probably know about the
Resource Workshop, which can be used to simplify the process of working with RES
files. You can still use the Resource Workshop with Delphi exactly as you would have
used it with BP7 or BC45. If you don’t have a copy of this tool, you can almost
certainly buy it from Borland as an up-sell. Note that there is a difference between the
16- and 32-bit versions of the Resource Workshop. BC45 ships with a 32-bit version
of this tool.
BRCC32.EXE can do everything that the Resource Workshop can do except enable
you to browse through existing resources or executables. The advantage that Resource
Workshop has over BRCC32 is that it’s easier to use.
The Image Editor that ships with Delphi has much of the functionality of the Resource
Workshop built into it, but it does not handle version resources, dialog boxes, or string
tables. The very first versions of the Image Editor that shipped with Delphi 16 were
buggy. However, this product is much cleaner now, and I have found myself relying on
it more often with reasonably good results.
Besides the standard icon, you can add additional resources to standard Delphi RAD programs.
However, when you do this, you should not attempt to insert your resources into the default
RES file used to hold the program’s icon. For instance, if you had a program called CLIENT1,
Delphi would automatically create a RES file for it called CLIENT1.RES. In content, it would
be similar, or identical, to the resource file shown earlier. Do not attempt to add your own
resources to this file. Instead, add a second resource file to this project, for the reasons explained
earlier when I talked about resource naming conventions.
When adding additional resource files to a standard Delphi RAD project, I prefer to put them
in the main form, right next to the reference to the DFM file:
implementation
{$R *.DFM}
{$R MYRES.RES}
The code shown here adds a second resource file to a Delphi RAD project. The resource file is
called MYRES.RES, and it would typically be made with a copy of BRCC32.EXE and a text
file called MYRES.RC. The assumption here is that the executable for the preceding program
is named anything else besides MYRES.EXE, because you should not add a resource file to a
Delphi project that has the same name as the DPR file.
var
AboutProc: TFarProc;
begin
WindowProc := 0;
case AMessage of
wm_Command:
if LoWord(WParam) = cm_About then begin
DialogBox(HInstance, ‘About’, Window, @About);
Exit;
end;
…
end;
wm_Command messages are sent to a WndProc whenever the user chooses an item from a menu,
selects an accelerator key stroke, or performs certain other actions such as selecting a button.
When you receive a wm_Command message, WParam is set to the constant associated with the con-
trol that sent it. In this case, that value is cm_About.
In Win32, the high-order Word of WParam is set to the value 1 if the message is from an accel-
erator, or to 0 if the message is from a menu. The high order word specifies a notification code
if the message is from a control. Because this message is being sent by a menu, there is no need
to use the LoWord function. However, this is still the cautious thing to do, and it helps you form
the right habits. In Win16, there is no value specified in the high-order word of WParam, in part
because this is only a 16-bit value in Win16, and therefore there is no high-order Word.
Once you have parsed WParam and know what message you are going to receive, the next step is
to create the dialog box itself:
DialogBox(HInstance, ‘About’, Window, @About);
In Win32, calling the DialogBox procedure is trivial. In Win16, it can be complicated by the
need to call MakeProcInstance before the DialogBox call, and FreeProcInstance after the DialogBox
call. It is not an error to call MakeProcInstance and FreeProcInstance in Win32; however, these
calls do not do anything in Win32. I return to this subject later in the chapter, when you see
the Win232 program, which compiles in both Win16 and Win32.
DialogBox creates a modal dialog box. If you want to create a modeless dialog box, you should
use CreateDialog instead of DialogBox.
After creating the dialog box, the focus of the program moves to the About function, which is
to the About dialog box what the WindowProc is to the main window:
function About(Dialog: HWnd; AMessage, WParam:UINT;
LParam: LPARAM): Bool; stdcall; export;
begin
Result := True;
case AMessage of
wm_InitDialog: Exit;
wm_Command:
if (WParam = idOk) or (WParam = idCancel) then begin
EndDialog(Dialog, 1);
Exit;
end;
end;
Result := False;
end;
If you compare the About function and the WindowProc function, you will see that they take the
same parameters but return a different value. In general, the two routines are very similar, both
in appearance and in function.
You can use the wm_InitDialog message to perform any initialization needed for the dialog box.
The dialog box should be closed whenever the user presses the OK button or cancels out of the
dialog box. To close a dialog box, call the EndDialog procedure, passing in the HWnd of the dia-
log box in the first parameter. The second parameter of EndDialog contains the value you want
the DialogBox procedure to return.
This whole process is not really that complicated. However, if it seems a bit intimidating, re-
member that you can just cut and paste this code from one application to the next. All modal
dialog boxes are created the same way, and all dialog box procedures have the same basic struc-
ture as the About procedure. So whenever you need to create one of these dialog boxes, just cut
and paste the code from this program. Write once, use many. It’s not really that hard.
at the command line. If you don’t have the Resource Workshop, you have to compile the RC
files from the command prompt, but you can still compile the executables from inside the Delphi
IDE.
I’m discussing the command line now only because it can be helpful to know how to work
outside the IDE. It is not a necessity to work from the command prompt, and indeed I only do
so under unusual circumstances. However, it can be a very fast way to compile and test appli-
cations. The command prompt also enables you to easily automate procedures with batch or
makefiles, which can be a very convenient aid under certain circumstances of the type frequently
encountered by serious programmers.
NOTE
If you want to compile from the command line, it’s probably best not to have Delphi
installed in a subdirectory that includes long filenames anywhere in its path. In
particular, I would install it into C:\DELPHI32, if possible. If you decide to move
Delphi from its current directory to a new location, remember that you have to change
the entries in the registration database. Under most circumstances, this means that you
should probably first uninstall Delphi using the Control Panel, and then reinstall to a
new location where there are no long pathnames leading to the Delphi directory. If you
have a CD, and you choose the automated Uninstall from the Control Panel, it really
isn’t that difficult to move Delphi’s location.
You should note that the 4DOS command processors, version 5.51 and greater, do
understand long filenames. These tools are made by JP software and are available as
shareware on CompuServe and elsewhere. 4DOS makes life with long filenames
bearable, but in general, I still think it is a good idea not to install Delphi into a
subdirectory that uses long filenames.
Just to make my point clear, at the time of this writing I was not able to get Delphi
applications to compile cleanly from the command prompt unless I could eliminate
long filenames in the path leading to the Delphi directory. Perhaps this will be fixed in
the shipping version of the product, but for now I would recommend installing Delphi
in C:\DELPHI32, or in some other location that has a simple path leading to it. My
other programs can be buried behind the c:\program files\ directory, but Delphi seems
to work best if it has a simple path leading to it.
By now you know how to construct and compile a very simple RES file. The following code
shows a simple DOS batch file. It would compile the resources used by the Window2 pro-
gram, as well as the Window2 program itself:
BRCC32 -r windres.rc
dcc32 -b window2.dpr
This batch file enables you to compile Delphi programs from the command prompt, without
ever opening DELPHI32.EXE. The -b parameter specifies that you want to perform a build
rather than a simple compile. You must have the DELPHI32\BIN directory on your path for
this to work correctly. Furthermore, you should define a DCC32.CFG file that points to the
LIB directory:
-rC:\Delphi32\Lib
-uC:\Delphi32\Lib
The -r switch specifies the resource directory, and the -u switch designates the directory or
directories where your units are stored. The code shown here should be placed in a separate file
called DCC32.CFG and should be stored in the DELPHI32\BIN subdirectory. In the case
shown here, I’m assuming Delphi was installed in the c:\delphi32 subdirectory. That enables
me to avoid atrocities that look like this:
-uC:\Progra~1\Borland\Delphi~1.0\Lib
Note that the WINDRES.RC won’t compile successfully unless you have an icon called
WINDOW2.ICO in the same directory. There are icons that ship both with this book and
with Delphi, or you can build an icon with the Resource Workshop or with the Image Editor
from the Tools menu. If you don’t want to build your own icon, you can safely rename an
existing icon to WINDOW2.ICO, or copy an existing icon to a file of that name.
The WINDRES.RES file is automatically linked into your program because of the $R line found
in WINDOW2.DPR:
{$R WINDRES.RES}
This kind of batch file works fine for small projects. However, there are occasions when you
need to compile large, multifile projects. In those cases, you need a more sophisticated tool,
such as a makefile.
In one moment, I’ll show an example makefile. However, it’s at least arguable that a makefile’s
syntax is such that it should be used only as a last resort. For those who feel that way, the fol-
lowing is a useful batch file trick that can get you through even very complex projects.
The key point to consider when reading the following batch file is that the Borland compilers
set the DOS ERRORLEVEL to 1 if something goes wrong:
@echo off
BRCC32 -r wind232.rc
if ErrorLevel 1 goto Error
:Error
echo Error occured!
:AllDone
The batch file tries to compile each of the files in your project. If something goes wrong, the
batch file prints out “Error occurred!” and terminates. Even if you had hundreds of different
compilations specified in your batch file, the run would always either terminate successfully or
terminate with the last few lines showing the action that caused the error. It’s this error detec-
tion mechanism that makes these batch files useful.
Notice the change to the command line where DCC32.EXE is called. This change sets up aliases
for the WinProcs and WinTypes units. These aliases tell the 32-bit compiler to substitute the
word WINDOWS whenever it sees the words WinTypes or WinProcs. (Inside the IDE, you can
set up the same changes by selecting Tools | Options | Library | Aliases from the menu.) The
switch for declaring an alias is -A. Needless to say, you need to put WinProcs and WinTypes in
your uses clause for this option to make any sense.
Assuming that you don’t want to use batch files, here is a makefile that can be used to compile
the WINDOW2 project:
#------------------------------------------------------------------
# WINDOW2.MAK
# Copyright (c) 1996 by Charlie Calvert
# You must have MAKE.EXE on your path to run this file.
# Syntax: MAKE -f WINDOW2.MAK
#------------------------------------------------------------------
!if !$d(WIN16)
COMPILER = DCC32.EXE
RCCOMPILER = BRCC32.EXE
FLAGS = -B -AWINPROCS=WINDOWS -AWINTYPES=WINDOWS
!else
COMPILER = DCC.EXE
RCCOMPILER = BRC.EXE
FLAGS = -B
!endif
APPNAME = WINDOW2
# link EXES
$(APPNAME).exe: WINDRES.res
$(COMPILER) $(FLAGS) $(APPNAME).DPR
# resource
.rc.res:
$(RCCOMPILER) -r { $< }
Here is the syntax for using a makefile from the command line:
make -f window2.mak
This is not the time or place to get involved in a long discussion of makefiles. I’m including
this one mostly to let hard-core C/C++ programmers see how they can use some of their favor-
ite tools with Delphi. Note that this MAK file has a rule for building resources, and it provides
a mechanism for creating 16-bit files as well as 32-bit. If you want to create a 16-bit executable,
you should enter the following line at the command prompt:
make -DWIN16 -f window2.mak
The -D option shown here defines WIN16 so that it appears to be part of the environment.
The makefile sets up the COMPILER and RCCOMPILER macros differently when WIN16 is defined.
In the next section I describe how to change WINDOW2.DPR so it compiles under 16-bit
and 32-bit.
You cannot create the 16-bit Delphi executable unless you have Delphi 16 installed on your
system. Furthermore, you need to be able to switch your DOS path back and forth between
the DELPHI\BIN and DELPHI32\BIN subdirectories before switching targets.
NOTE
Next you find a batch file that I use to switch my path dynamically without having to
reboot my system or my Windows 95 DOS session. This file assumes you have a
command processor running that passes parameters to batch files. 4DOS does this for
you, as do all versions of DOS up to, but not including, version 7. Here is the batch
file:
@echo off
if %1==1 goto delphi
if %1==2 goto BC45
if %1==3 goto Delphi32
:delphi
path=c:\delphi\bin
goto mostpaths
:bc45
path=c:\bc45\bin\
goto mostpaths
:delphi32
path=c:\delphi32\bin
goto mostpaths
:mostpaths
path=c:\iblocal\bin;c:\windows\command;c:\idapi;%path%;
set BCROOT=c:\bc45
goto end
:end
Note that the %path% syntax enables me to reference the current DOS path. Therefore,
the path statement in MOSTPATHS prepends a few new subdirectories onto the path
statement defined in one of the earlier sections of the batch file. For example, the early
sections of the batch file might set the path to the following:
c:\delphi32\bin
On my system this batch file is called NEWP.BAT. If I type NEWP directly at the DOS
prompt with no parameters, I get a list of the available paths. If I want to switch to the
Delphi 16 path, I type NEWP 1. If I want to switch to the DELPHI32 path, I type NEWP
3. There is a call to NEWP 3 (CALL NEWP 3) in my AUTOEXEC.BAT file.
...
{$ifdef WIN32}
function WindowProc(Window: HWnd; AMessage, WParam,
LParam: Longint): Longint; stdcall; export;
{$else}
function WindowProc(Window: HWnd; AMessage, WParam: Word;
LParam: Longint): Longint; export;
{$endif}
var
AboutProc: TFarProc;
begin
WindowProc := 0;
case AMessage of
...
This code assumes that Win32 is defined automatically when you run DCC32 or DELPHI32.
In my opinion, a better way to handle this problem would be to declare types that can be rede-
fined in 16- and 32-bit Windows:
type
UINT = Cardinal;
WPARAM = UINT;
LPARAM = LongInt;
A UINT would then be declared as a 16-bit value in Win16 and a 32-bit value in Win32. The
same code would then compile in both versions without a need to resort to perhaps overly
obtrusive ifdefs. Some of these types might be predefined for you in the file WINDOWS.PAS
that ships with your version of Delphi 2.0. However, they won’t be defined in Delphi 16. The
complete code for the program is shown below in Listing 10.4.
Listing 10.4. A version of the Window2 program that compiles under both
Win32 and Win16.
program Wind232;
uses
WinTypes, WinProcs, Messages;
{$I WIND232.INC}
{$R WINDRES.RES}
const
AppName = ‘Wind232’;
type
UINT = Cardinal;
WPARAM = UINT;
LPARAM = LongInt;
begin
Result := True;
case AMessage of
wm_InitDialog: Exit;
wm_Command:
if (WParam = idOk) or (WParam = idCancel) then begin
EndDialog(Dialog, 1);
Exit;
end;
end;
Result := False;
end;
case AMessage of
wm_Command:
if WParam = cm_About then begin
AboutProc := MakeProcInstance(@About, HInstance);
DialogBox(HInstance, ‘About’, Window, AboutProc);
FreeProcInstance(AboutProc);
Exit;
end;
wm_Destroy:
begin
PostQuitMessage(0);
Exit;
end;
end;
{$ifdef WIN32}
continues
var
AMessage: TMsg;
Window: HWnd;
begin
if not DoRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
Window := Create;
if Window = 0 then begin
MessageBox(0, ‘Create failed’, nil, mb_Ok);
Exit;
end;
ShowWindow(Window, CmdShow);
UpdateWindow(Window);
Halt(AMessage.wParam);
end.
This program acts identically to the Window2 program. The only differences involve the places
where Win32 is defined. For instance, note the DoRegister procedure:
{$ifdef WIN32}
Result := RegisterClass(WindowClass) <> 0;
{$else}
Result := RegisterClass(WindowClass);
{$endif}
Register class is defined to return an Atom in 32-bit code, and to return a BOOL in 16-bit.
The code shown here compiles fine in both WIN16 and WIN32. The issue is that
MakeProcInstance and FreeProcInstance don’t do anything in WIN32:
This is the kind of code sample that makes you think, “Hey, maybe I could have written Win-
dows after all!” Of course, these routines are a little more complicated in their 16-bit versions.
All this dancing around when switching back and forth between 16 and 32 bits is necessary
only because the code shown here is Windows API code and not VCL code. One of the signifi-
cant functions of the VCL is that it makes it easy to move back and forth between 16 and 32
bits. The little exercises shown here should help to make it clear how much work is saved by
using the VCL.
Summary
In this chapter you got a look at dialog boxes, resources, and messages. In addition, you got a
good look at compiling applications from the DOS prompt, and at writing the appropriate
batch files and makefiles to complete this task.
Remember that if you absolutely need to see this material in more depth, you can pick up a
standard C reference on the Windows API such as Petzold’s Windows Programming, published
by the Microsoft Press, or Teach Yourself Windows 95 Programming, published by Sams. One
of the goals of this chapter is to make it easy for you to use the reference books on Windows
that are written in C. The key piece you need to grasp is how all that raw C API code relates to
Delphi, and the answer to that query is supplied in these chapters.
Remember that the discussions about DLLs and messages begun in this chapter are picked up
again in the next chapter. There is much more to be said about both topics, so you should tune
in to the next chapter for additional information. One of the best ways to get used to all this
Windows API code is simply through repetition. The first time you see a WinProc, it’s a bit
overwhelming. But once you have seen a dozen of them, they start to look awfully familiar.
They start by seeming alien, but they end up being old friends.
Messages and
Painting Text 11
MESSAGES.PAS
This chapter uses a powerful message-cracking technique built into Delphi that helps you write
clean, portable code. In particular, MESSAGES.PAS contains a series of special records that
help you handle messages in a simple, easy-to-read manner, while at the same time creating
code that is portable between Win16 and Win32.
Each message sent by the operating system is accompanied by two parameters of type LongInt
called WParam and LParam. In Win16, WParam is declared as a 16-bit value, but in Win32, it is
declared as 32-bit value. LParam is declared as a 32-bit value under both operating systems. These
changes mean that the information accompanying some messages is encoded differently in
Win32 than it was in 16-bit Windows. The message-cracking records in MESSAGES.PAS hide
these implementation details from you. More specifically, Delphi 16 and Delphi 32 ship with
two different versions of MESSAGES.PAS: one for 16-bit code and one for 32-bit code.
MESSAGES.PAS helps you avoid having to typecast most of your code that involves process-
ing LParam and WParam. Normally Windows programming requires a huge number of typecasts,
all of which circumvent the normal type-checking procedures that help you write safe code.
MESSAGES.PAS lets you avoid typecasts and reasserts the primacy of type checking.
What Is a Message?
Chapter 9, “Beyond the VCL: WinMain and WinProc,” presented a bird’s-eye view of a stan-
dard Windows program. It gave you a flyby over all the major portions of a Windows pro-
gram, including the WinMain, Register, Create, and WndProc functions, as well as the message
loop. The next step is to narrow the focus so that you get a closer look at the window proce-
dure and the messages that are sent to it.
By now you should have a feeling for the difference between a message-based (or event-driven)
program and a standard procedural program. In the former case, the operating system tells a
program that an event has occurred. In the latter case, the program queries the system to find
out what has happened.
Here’s a more in-depth look at the differences between the traditional DOS and Windows
programming worlds:
■ Windows message-based model: Once the program is launched, it simply waits for
messages to be sent to it and then responds accordingly. Windows itself detects if a
key has been pressed or if the mouse has been moved. When an event of this type
occurs, Windows sends a predefined message to the program, telling it what has
happened. The program usually has the option of either ignoring the message or
responding to the message.
■ DOS procedural model: DOS code usually executes linearly; that is, it starts at the
beginning of a program and advances through to the end by stepping through code
one line at a time or by branching or looping through various segments of code. The
program discovers the user’s commands by querying the system. That is, the program
calls interrupt-based subroutines that are built into the operating system or the
hardware. In return, these interrupts report whether a key has been pressed or the
mouse has been moved. Special APIs such as Turbo Vision lived in a never-never-land
halfway between DOS and Windows, but the standard DOS program followed the
system outlined in the first part of this paragraph.
It should come as no surprise to hear that messages are really just constants defined in
WINDOWS.PAS. Here, for instance, are the declarations for messages that handle keyboard
and mouse movements:
// Keyboard Messages
WM_KEYDOWN = $100; // Key was pressed
WM_KEYUP = 257; // Key was released
WM_CHAR = 258; // Processed keystroke
WM_DEADCHAR = 259; // Composite key
// Mouse Messages
WM_MOUSEMOVE = $200; // Mouse was moved
WM_LBUTTONDOWN = 513; // Left button pressed
WM_LBUTTONUP = 514; // Left button released
WM_LBUTTONDBLCLK = 515; // Double click of left button
WM_RBUTTONDOWN = 516; // Right button down
WM_RBUTTONUP = 517; // Right button released
WM_RBUTTONDBLCLK = 518; // Double click of right button
WM_MBUTTONDOWN = 519; // Middle button down
WM_MBUTTONUP = 520; // Middle button up
WM_MBUTTONDBLCLK = 521; // Double click of middle button
Don’t try to memorize these messages. Just look them over and become familiar with the way
they look and the kinds of services they provide.
NOTE
varNull = $0001;
varSmallint = $0002;
As you can see, this code follows a syntax similar to the one I have adopted. Perhaps
the internal rule on the Delphi team is that Windows constants follow C conventions,
and native Delphi declarations follow the style I prefer. At any rate, there are clearly
some conflicting schools of thought at work here, and as a result, I don’t place great
weight on this issue (although I do tend to avoid declaring anything in all caps unless it
is an acronym). However, you will undoubtedly find that I use both conventions in
various places in this book. This is perhaps regrettable, but it is not a serious issue
because the compiler simply does not care how I capitalize my code!
Clearly, nothing is very mysterious about the messages themselves. They are simply constants
with useful names that inform a program about the current state of the system. When an event
occurs, these messages are bundled with other useful bits of information and sent to one or
more appropriate window procedures. Exactly what should then be done with those messages
is the topic of this chapter.
NOTE
You don’t need to understand how Windows knows that the user moved the mouse or
pressed a key. All you need to do is trap messages and then decode them. For instance,
whenever a message with the number hexadecimal 200 comes down the pike, it’s an
indication that the user moved the mouse.
Accompanying the message are additional codes that tell something about the location
and state of the mouse. This information lies encoded in parameters called wParam and
lParam.
The three steps previously outlined are very important. Almost every piece of Windows API
code you write rests on the basic components created in Chapters 9 and 10. In other words,
that code is very much reusable. In fact, you might want to keep it in a separate subdirectory
where you can access it whenever you need to start a new program. As you probably know, the
idea of reusing packets of code is very important in most modern programming endeavors.
NOTE
Don’t try to copy old makefiles or DSK files from directory to directory without
carefully reviewing them. DSK files generated by the Delphi IDE often contain the
pathname of the files you add to a project. Don’t make the mistake of moving your
files to a new subdirectory and accidentally modifying WINDOW1.DPR (or some
other member of the original program) simply because it is still listed in your project
file.
It’s now time to take a look at the code from the EASYTEXT program, found in Listing 11.1.
Take the time to get this program up and running. It will help you understand the rest of this
chapter.
uses
Windows, Messages;
const
AppName = ‘EasyText’;
var
Directions: string;
//////////////////////////////////////
// The destructor handles WM_DESTROY
//////////////////////////////////////
procedure EasyText_OnCreate(hWindow: HWnd; var Msg: TWMCreate);
begin
Directions := ‘Try resizing this window.’;
Msg.Result := 0; // Continue. Return -1 on error
end;
//////////////////////////////////////
// The destructor handles WM_DESTROY
//////////////////////////////////////
procedure EasyText_OnDestroy(hWindow: HWnd; var Msg: TWMDestroy);
begin
PostQuitMessage(0);
end;
///////////////////////////////////////
// Handle WM_PAINT messages
// Show how to use TextOut and DrawText.
///////////////////////////////////////
SetBkMode(PaintDC, TRANSPARENT);
GetClientRect(hWindow, Rect);
EndPaint(hWindow, PaintStruct);
end;
//////////////////////////////////////
// The Window Procedure
//////////////////////////////////////
function WindowProc(Window: HWnd; AMessage, WParam,
LParam: Longint): Longint; stdcall; export;
var
AMsg: TMessage;
begin
AMsg.Msg := AMessage;
AMsg.WParam := WParam;
AMsg.LParam := LParam;
AMsg.Result := 0;
case AMessage of
wm_Create: EasyText_OnCreate(Window, TWMCreate(AMsg));
wm_Destroy: EasyText_OnDestroy(Window, TWMDestroy(AMsg));
wm_Paint: EasyText_OnPaint(Window, TWMPaint(AMsg));
else
WindowProc := DefWindowProc(Window, AMessage, WParam, LParam);
Exit;
end;
Result := AMsg.Result
end;
///////////////////////////////////////
// Register the Window Class
///////////////////////////////////////
function DoRegister: Boolean;
var
WindowClass: TWndClass;
begin
WindowClass.Style := cs_hRedraw or cs_vRedraw;
WindowClass.lpfnWndProc := @WindowProc;
WindowClass.cbClsExtra := 0;
WindowClass.cbWndExtra := 0;
WindowClass.hInstance := HInstance;
WindowClass.hIcon := LoadIcon(HInstance, ‘WindowIcon’);
continues
///////////////////////////////////////
// Create the Window Class
///////////////////////////////////////
function Create: HWnd;
begin
Result := CreateWindow(AppName, ‘Object Pascal Window’,
ws_OverlappedWindow, cw_UseDefault, cw_UseDefault,
cw_UseDefault, cw_UseDefault, 0, 0, HInstance, nil);
end;
var
AMessage: TMsg;
HWindow: HWnd;
begin
if not DoRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
HWindow := Create;
if HWindow = 0 then begin
MessageBox(0, ‘Create failed’, nil, mb_Ok);
Exit;
end;
ShowWindow(hWindow, CmdShow);
UpdateWindow(hWindow);
Halt(AMessage.wParam);
end.
The purpose of this program is to show you how to use message crackers and how to respond
to wm_Paint messages. The code pops up a standard window, shown in Figure 11.1, and prints
two pieces of text on it. Notice that one piece of text always stays in the middle of the window,
even when you resize it.
FIGURE 11.1.
EASYTEXT.EXE displays
two pieces of text: one in
the upper-left corner of the
window, and one centered
in the middle of the
window.
case AMessage of
wm_Create: EasyText_OnCreate(Window, TWMCreate(AMsg));
wm_Destroy: EasyText_OnDestroy(Window, TWMDestroy(AMsg));
wm_Paint: EasyText_OnPaint(Window, TWMPaint(AMsg));
else
WindowProc := DefWindowProc(Window, AMessage, WParam, LParam);
Exit;
end;
Result := AMsg.Result
end;
NOTE
The code I am showing you here is very close to compiling in both Win16 and Win32.
However, you would need to tweak the WindowProc header very slightly before the
code would recompile in 16-bit land. I covered the changes you would need to make in
the last chapter, in the Wind232 program. The basic structure of this program,
however, is designed so that it can run on either platform. The EZText16 program,
found on disk, compiles on both platforms. However, I wanted to avoid any ifdefs in
the code I’m showing you in this book.
If the message crackers in MESSAGES.PAS did not exist, the window procedure could be-
come an extremely formidable foe that Windows programmers would have to wrestle to the
mat on a daily basis. The source of the trouble is the notorious case statement found in win-
dow procedures. Even moderately complex Windows API programs that don’t use message
crackers contain WndProcs that tend to stretch on for page after page of mind-boggling code.
Now that message crackers are here, the case statement still exists, but it has been tamed by a
series of predeclared TWMXXX records that fairly effectively defang the serpent waiting within every
window procedure. The message crackers do this by finding a simple way to move the body of
your response to a message out of the WndProc and into functions that obey the basic rules of
structured programming. Of course, using message crackers isn’t an absolutely effortless pro-
cess, but I believe you’ll find them much easier to use than a lengthy case statement.
The EasyText program explicitly handles three messages:
wm_Create: EasyText_OnCreate(Window, TWMCreate(AMsg));
wm_Destroy: EasyText_OnDestroy(Window, TWMDestroy(AMsg));
wm_Paint: EasyText_OnPaint(Window, TWMPaint(AMsg));
As you can see, the messages are wm_Create, wm_Destroy, and wm_Paint. In the next few pages,
you’ll see how message crackers simplify the use of not only these messages, but all standard
API messages.
When reading this discussion, remember that the TWMXXX messages help shield you from com-
plexity. Unfortunately, I have to delve right into the heart of that complexity in order to ex-
plain how message crackers work. After you have a few basic ideas clear in your mind, you’ll
find that message crackers smooth the way for you over and over again.
NOTE
Message crackers help you use standard structured programming techniques through-
out your WndProc. When I first learned structured programming, it seemed a bit
complex. Over the years, however, it has saved me many, many frustrating days of
painful debugging. The same is true of message crackers—for many of the same
reasons.
All right. It’s time to gird your loins and focus your mind. The next few paragraphs contain a
good deal of important information.
To begin, you need to know that wm_Destroy messages are sent to a window whenever it’s about
to be closed. In the main window of an application, it’s crucial that you remember to call
PostQuitMessage in response to a wm_Destroy message. If you don’t do this, you’ll immediately
become involved in an inexplicable mass of bugs from which you’ll never extract yourself until
you finally figure out the nature of your error. If you forget to call PostQuitMessage, your ap-
plication is toast!
NOTE
PostQuitMessage sends a WM_QUIT message to Windows, which is the signal to break out
of the message loop back in WinMain. In other words, the message loop would continue
indefinitely were it not for the call to PostQuitMessage. Therefore, this call is absolutely
essential for proper application termination.
If message crackers were not being used, this whole process would have been handled in the
WndProc:
switch (Message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_PAINT:
etc..
}
With the TWMXXX records, however, programmers have the option of easily handling wm_Destroy
messages in a separate function. This helps promote good structured programming techniques.
In the source for MESSAGES.PAS, you can view the message cracker records. These nuggets
of code “pick apart” the parameters to WndProc. The macros then pass on the important finds
to your message handler function. The issue is that every message sent to a window can be
accompanied by additional information—which usually comes in the form of the hwnd, wParam,
and lParam parameters.
Three problems occur as the result of sending messages in the hwnd, wParam, and lParam param-
eters:
■ Not all messages use all three of these parameters. As a result, programmers always
have to look in their reference books to see which of these parameters are utilized by a
particular message.
■ Different pieces of information often are packed into a particular parameter. For
instance, a programmer might have to look in the first word of lParam to get a piece of
information, and look in the first and second words of wParam to get different pieces of
information. After engaging in this trying process, the programmer often needs to
typecast the information before it’s usable.
■ The way information is packed into lParam and wParam sometimes differs depending
on whether you are in 16-bit Windows or in Win32.
You have learned that message crackers are really just a series of records designed to pick apart
the various parameters passed to WndProc. The first stage in understanding this technique is to
take a look at the TMessage structure, used as the type for the AMsg local variable in the WndProc:
TMessage = record
Msg: Cardinal;
case Integer of
0: (
WParam: Longint;
LParam: Longint;
Result: Longint);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;
This is a typical variant record that can be interpreted in two different ways. In particular, you
can see it as containing three 32-bit values, six 16-bit values, or some combination of the two.
It’s up to you to interpret the structure as you please.
Delphi has its own internal ways of filling out TMessage structures, but in this case the code
simply makes some quick and dirty assignments:
AMsg.Msg := AMessage;
AMsg.WParam := WParam;
AMsg.LParam := LParam;
AMsg.Result := 0;
After these assignments are made, all the values sent to the WndProc are encapsulated in a single
structure. A default return value for the WndProc is also designated, although you could, if you
wanted, change this setting before passing it back to the operating system at the end of the
WndProc:
Result := AMsg.Result
end;
NOTE
Some programmers might get a little nervous when they see the assignment statements
shown here. After all, thousands of messages can come into a WndProc during the life of
a procedure, and the act of assigning values to the fields of TMessage seems a bit time-
consuming. However, if you look at the actual assembler generated for that code you
will see that it is not very lengthy:
mov [ebp-10],eax // AMsg.Msg := Amessage;
mov [ebp-0C],edx // AMsg.WParam := WParam;
mov [ebp-08],ecx // AMsg.LParam := LParam;
xor ebx,ebx // AMsg.Result := 0;
mov [ebp-04],ebx
On a Pentium, these few instructions take place almost instantaneously. Certainly they
aren’t worth fretting about.
The TMessage record type is 16 bytes in size. So is the TWMNoParams record shown here:
TWMNoParams = record
Msg: Cardinal;
Unused: array[0..3] of Word;
Result: Longint;
end;
What is the relationship between TWMNoParams and TMessage? Well, they are both the same size,
so you can effectively map them into the same memory area or copy the value of one to the
other through typecast:
var
A: TMessage;
B: TWMNoParams;
begin
A.Msg := wm_Destroy;
A.WParam := 3;
A.LParam := 3;
B := TWMNoParams(A);
end;
It’s okay to typecast a TMessage variable as a TWMNoParams variable because they both take up
the same number of bytes. There is no loss of information when you make the assignment.
What good is the TWMNoParams record? Why would you want to use it? In some cases, LParam
and WParam contain no information. They are simply empty. For instance, the wm_Destroy message
doesn’t take advantage of either LParam or WParam. These values are simply along for the ride.
As a result, MESSAGES.PAS associates wm_Destroy messages with TWMNoParams:
type
TWMDestroy = TWMNoParams;
You can now safely typecast the TMessage structure associated with wm_Destroy messages so that
it fills TWMDestroy records that can be passed to a function that handles wm_Destroy messages:
…
AMsg.Msg := AMessage;
AMsg.WParam := WParam;
AMsg.LParam := LParam;
AMsg.Result := 0;
…
wm_Destroy: EasyText_OnDestroy(Window, TWMDestroy(AMsg));
…
The EasyText_OnDestroy function will now receive a copy of a record that has automatically
parsed LParam and WParam. In this case, the parsing does nothing more than designate that LParam
and WParam are Unused:
TWMNoParams = record
Msg: Cardinal;
This might seem like a small accomplishment, but the point is that LParam and WParam have
now been automatically parsed for you in a way that prevents you from making an error. The
TWMDestroy record makes it impossible for you to accidentally try to use LParam and WParam.
The record informs you that these values do not exist for this record. They are Unused!
It’s now time to revisit this issue when looking at a second message called wm_Create:
TWMCreate = record
Msg: Cardinal;
Unused: Integer;
CreateStruct: PCreateStruct;
Result: Longint;
end;
In this case WParam is mapped into a variable called Unused, but LParam is mapped into a vari-
able of type PCreateStruct:
TCreateStruct = packed record
lpCreateParams: Pointer;
hInstance: HINST;
hMenu: HMENU;
hwndParent: HWND;
cy: Integer;
cx: Integer;
y: Integer;
x: Integer;
style: Longint;
lpszName: PAnsiChar;
lpszClass: PAnsiChar;
dwExStyle: DWORD;
end;
Now when you try to parse the WParam and LParam variables associated with a wm_Create mes-
sage, there is no guessing. Delphi automatically does the parsing for you and assigns the
PCreateParams parameter to lParam:
…
AMsg.Msg := AMessage;
AMsg.WParam := WParam;
AMsg.LParam := LParam;
AMsg.Result := 0;
case AMessage of
wm_Create: EasyText_OnCreate(Window, TWMCreate(AMsg));
…
■ The records can be parsed differently in 16 and 32 bits, without you having to think
about portability issues. In other words, Delphi can just define a different TWMCreate
method for 16-bit Delphi if necessary.
One key point to remember is that this technique works just as effectively in a Delphi RAD
program as it does in a Windows API program. For instance, in Chapter 25, “Handling Mes-
sages,” you see the following object definition:
TEvents = class(TForm)
… // Additional code
procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
… // Additional code
private
procedure MyMouseMove(var M: TWMMouse); message wm_MouseMove;
public
{ Public declarations }
end;
This code shows two ways of handling wm_MouseMove messages in a standard Delphi RAD ap-
plication. Notice that the second technique uses a TWMMouse message cracker parameter:
TWMMouse = record
Msg: Cardinal;
Keys: Longint;
case Integer of
0: (
XPos: Smallint;
YPos: Smallint);
1: (
Pos: TSmallPoint;
Result: Longint);
end;
As you can see, the TWMMouse parameter does the same job of parsing out the parameters passed
to it as the FormMouseDown event handler does. In fact, there is quite a bit in common between
the MyMouseMove function and the EasyText_OnXXX events shown earlier.
That’s all I’m going to say about message crackers for now. However, this subject comes up
again several times in the next few chapters. If it’s not already obvious, over time it will begin
to come clear to you how these message crackers work and what they can do for you.
The name of this function is designed to make it look like a method. I append the word EasyText
before the name, just as you would append Form1 before an event in a standard Delphi appli-
cation. This gives you the artificial sense that your code is being categorized and organized as
it would be in an object-oriented program.
The name of the event being handled is appended onto the name of the “object class”:
ClassName_OnMessageName
EasyText_OnCreate
This gives you the sense that you are just responding to an event the way you would in a Delphi
application. In general, the naming convention used here is designed to help you write the
cleanest, clearest possible code.
All the message handling functions should take two parameters:
EasyText_OnCreate(hWindow: HWnd; var Msg: TWMCreate);
The first is the HWnd of the window you are working with, and the second is the message crack-
ing record described earlier.
NOTE
There is actually an argument in favor of just declaring the HWnd as a global variable and
not bothering to pass it in to each function you declare. You could, for instance, pick
up the global HWnd in the Create method for the entire application in the place where
you call CreateWindow. You could then use that global variable in all your message
handlers. My tendency to expend a few extra clock cycles passing the parameter to
functions is all part and parcel of my belief that it is always best to write the clearest
possible code. This is a judgment call, however, and certainly I can see good arguments
on both sides of the issue.
The body of the EasyText_OnCreate method is fairly straightforward. First the global string
Directions is assigned a value, then the Msg parameter is given a result value:
begin
Directions := ‘Try resizing this window.’;
Msg.Result := 0; // Continue. Return -1 on error
end;
If this function returned -1, the whole application would shut down immediately. In particu-
lar, wm_Create messages get sent when you call CreateWindow. The call to CreateWindow fails if
the wm_Create handler returns -1. In this case, there is no reason why the string assignment
would ever be likely to fail, so the code returns 0 automatically. If you had more complicated
allocations to make, you might write code that could set Msg.Result to -1 if one of the alloca-
tions failed.
You can use the Msg.CreateStruct parameter to access any of the values in the TCreateStruct
structure passed to this procedure. If you look back a page or two, you will see that a
TCreateStruct record holds all the variables that were passed to CreateWindow. This mecha-
nism provides an easy way for you to access these variables, and to change them if necessary.
If it weren’t for the message cracker, you would have to handle the typecasting yourself, as follows:
CreateStruct = PCreateStruct(lParam);
This is a tedious and error-prone process that is particularly worrisome to beginners. The best
thing to do is to follow the template shown earlier and let the typecast occur automatically
when the TMessage value gets passed to an EasyText_OnXXX handler.
NOTE
It might be helpful for you to know that whenever I explicitly handle either wm_Create
or wm_Destroy messages, I try to do it at the very beginning of a window procedure.
That way, readers of my code can easily find the place where I handle the initialization
(wm_Create) and the destruction (wm_Destroy) of my window. Exactly why these two
processes are so important is probably obvious to you intuitively and will certainly
become apparent to you over time. For now, you need to remember only that I like to
handle these two chores first and then handle all other messages in alphabetical order.
The wm_Command message cracker works with HWnd, wParam, and lParam. As a result, you might
want to look it up in MESSAGES.PAS to see what a really complex message cracker looks like.
■ You can explicitly call DefWindowProc from inside the function that wants to pass the
message on.
■ You can change the signature of the message handle, turn it into a function, and have
it return False. This could change a variable called AllDone to False, which informs the
WndProc that there is additional processing that needs to take place.
NOTE
If you choose the latter technique, there is one last detail that needs to be taken care of.
On occasion, when you handle a message, you might change the values stored in
LParam and WParam. This means that you need to pass these changed values on to the
DefWindProc, which is the purpose of the following code:
if not AllDone then begin
WParam := AMsg.WParam;
LParam := AMsg.LParam;
Result := DefWindowProc(Window, AMessage, WParam, LParam);
Exit;
end;
Most of the time, you will either want to handle the message yourself or pass it on directly to
DefWindowProc. That kind of default processing is handled automatically by the WndProc pro-
vided earlier. However, there are occasions when you might want to pass your handled mes-
sage on to DefWindProc. I discuss these latter occasions in this section. I’m aware that neither of
the solutions provided in this section are particular elegant, but they would resolve the prob-
lem with only a minimum of fuss.
Painting Text
Every message sent to a window procedure is important. Yet somehow, the wm_Paint message
strikes me as being the central message around which most applications revolve. Certainly,
nobody could deny that wm_Paint messages perform a vital function in the life of nearly all
Windows applications.
wm_Paint messages are sent to a window every time it needs to redraw the contents of its client
area. When programmers speak of the client area of a window, they are usually referring to the
area inside a window’s borders and title bar (see Figure 11.2). The client area is the space that
programmers usually draw in, whereas Windows tends to take care of the rest of the applica-
tion.
FIGURE 11.2.
The client area is the
portion of a window
beneath the title bar and
inside the frame or border.
If there is a part of the client area that you do not specifically take control of, Windows fills it
in with the designated background color. That is, it does so if you don’t prevent it from getting
the required messages from the default window procedure. Specifically, trouble can occur if
you don’t pass some messages on to DefWndProc that need additional processing by the system.
This is a subject to which I return in Chapter 12, “The Mouse and the Keyboard.”
wm_Paint messages get sent to a window by the operating system whenever it needs to be drawn
or redrawn. Think for a moment what this means. If a window has been hidden under another
window and is suddenly uncovered, it receives a wm_Paint message. It gets the message because
it needs to redraw the area covered by the other window. If a window has been maximized or
minimized, it receives a wm_Paint message when restored to normal size. If a user switches back
from a full-screen DOS window, all the visible windows on the desktop receive wm_Paint
messages.
NOTE
SetBkMode(PaintDC, TRANSPARENT);
GetClientRect(hWindow, Rect);
EndPaint(hWindow, PaintStruct);
end;
To help you understand exactly what’s going on here, it might help if I strip the function of
everything but its most fundamental parts:
procedure EasyText_OnPaint(hWindow: HWnd; var Msg: TWMPaint);
var
PaintStruct: TPaintStruct;
PaintDC: HDC;
begin
PaintDC := BeginPaint(hWindow, PaintStruct);
EndPaint(hWindow, PaintStruct);
end;
This example can serve as a template from which you can build all your paint routines. The
basic elements are the calls to BeginPaint and EndPaint. Note also the variables called PaintDC
and PaintStruct.
The key role of the BeginPaint function is to give you access to a device context (HDC) for a win-
dow. Device contexts might be a totally new concept for some people who come from DOS.
As a result, you might want to pay special attention during this brief discussion to one of the
key concepts in Windows programming.
NOTE
In a RAD program, you can get hold of the device context for a window by accessing
Canvas.Handle. In particular, the following OnPaint handler method for a TForm object
is roughly parallel to the short version of the EasyText_OnPaint method shown earlier:
procedure TForm1.FormPaint(Sender: TObject);
var
PaintDC: HDC;
begin
PaintDC := Canvas.Handle;
end;
This code retrieves PaintDC from the Canvas object in much the way the
EasyText_OnPaint function retrieves it from the call to BeginPaint. For people with a
slightly more technical bent, the following code more precisely mirrors inside a RAD
program the code shown in the EasyText_OnPaint function:
type
TForm1 = class(TForm)
private
procedure wmPaint(var Msg: TWMPaint); message wm_Paint;
end;
implementation
Device contexts form a link between your program and certain external output devices that
can be attached to a computer. In particular, most device contexts form the interface between
your application and a printer or video card.
It’s probably safe to say that if device contexts did not already exist, somebody would have to
invent them. They form an essential part of any robust programming environment. Until
Windows came along, all DOS programmers either consciously or unconsciously felt their lack.
Device contexts make it easy to deal with a wide variety of video cards and printers. For in-
stance, there are VGA, EGA, CGA, Hercules, and who-knows-how-many different SuperVGA
cards lying in wait for unwary programmers. Printers are even more numerous and varied.
In the past, every application needed its own drivers to run these different devices. WordPerfect,
for instance, was famous for its wonderful printer drivers. Their existence gave the product a
kind of flexibility that users loved. But of course, not every start-up company that came down
the pike could afford to develop its own set of drivers; even if they could, all that redundant
effort would not make a great deal of sense.
To resolve this problem, Windows made it possible for manufacturers of hardware output devices
to write drivers that could be hooked into any Windows program. The key to this whole pro-
cess is the development of device contexts, which form the link between your application, a
device driver, and an actual hardware device. Their existence makes it possible for you to write
one set of code that can, at least in theory, use multiple devices.
It is quite feasible under Windows to write one set of code that works equally well with a CGA,
VGA, or EGA monitor. This can be done without ever having to write any device-specific code.
Any DOS programmer who has done serious work in the graphics world is bound to recognize
that this is a major accomplishment, even if it does come replete with a few caveats illustrated
at various points in this book.
By now, you should start to understand why it is so important to begin every paint procedure
with a call to BeginPaint. BeginPaint is the function that retrieves the device context for a
particular window, and without the device context you aren’t going to get very far when at-
tempting to display graphics.
NOTE
There are several different ways to get device contexts from the system. One of the
most convenient is called GetDC. To my mind, GetDC is so well named, and so easy to
use, that there is a temptation to use it in place of BeginPaint. Don’t ever do this—it
causes all kinds of inexplicable and undefined behavior. BeginPaint is specifically
designed to be used in conjunction with wm_Paint messages. If you try to work without
it, your window is likely not to respond correctly when brought to the top from behind
another window or when it needs to be repainted. Inside a RAD program, however,
you can almost always get the device context by just referencing Canvas.Handle.
Just as BeginPaint is the proper way to start a paint function, EndPaint is the proper way to
close it off. Be sure to encapsulate the heart of your response to every wm_Paint message within
these two functions.
BeginPaint and EndPaint both take an HWnd and a PaintStruct as parameters. Here are the fields
of a PaintStruct:
TPaintStruct = packed record
hdc: HDC;
fErase: BOOL;
rcPaint: TRect;
fRestore: BOOL;
fIncUpdate: BOOL;
rgbReserved: array[0..31] of Byte;
end;
■ rcPaint: The rectangle inside of which the drawing will occur. It is important to
understand that this rectangle is not synonymous with the area of the client rectangle.
The reason for this is that Windows is smart enough to tell a window exactly how
much of itself needs to be redrawn. This can be important when a window in the
GetClientRect(hWindow, Rect);
This code starts by setting the background mode to TRANSPARENT. This is necessary because the
background of the window can be any of a number of different colors as a result of this line
from the Register procedure:
WindowClass.hbrBackground := hBrush(Color_Window);
This code uses a system-wide color choice as the background of the main window. If you right-
click the desktop, you can access the Display Properties dialog box. Use the Item combo box in
this display to select the option called “Window,” and then set it to some color of your own
choosing. Thereafter, all the properly designed windows on your desktop will have the new
color that you selected. As a programmer, your job is to respect whatever choice the user makes
in the Display Properties dialog box. To do this, you set the background of your windows to
the COLOR_WINDOW constant and then set the background of the text you output to transparent:
SetBkMode(PaintDC, TRANSPARENT);
If you neglect to call SetBkMode, your text appears with a white background even if the sur-
rounding window is blue, gray, or some other color. The end result is not good, and you might
want to comment out the call to SetBkMode once in your code just to appreciate how bad this
looks. Of course, you must first set the COLOR_WINDOW constant to something other than white
if you want to see this effect. This issue also applies to Windows 3.1, but the use of non-white
background colors in that platform was less prevalent.
In the EasyText program, you can examine the two most common ways of displaying text in a
Windows program. The first is called TextOut, and the second is DrawText.
TextOut is the easiest to use of the two functions. All that is required is a device context, some
starting coordinates, a string, and the length of the string. The most common means of deter-
mining the string length is to call the Length function. If you happen to be working with PChar
rather than a long string, you can call on one of the Pascal language’s built-in functions, such
as StrLen, or Windows own lstrlen.
The higher level DrawText function takes a device context, a string, a string length, a RECT, and
some flags as parameters. If you pass DrawText a NULL-terminated string, you can pass -1 in
the third parameter, which lets Windows calculate the length of the string. Long strings are by
default NULL-terminated.
The RECT parameter designates the area inside which the string will be painted. In this case, I
have used the GetClientRect function to retrieve the area of the entire client window and then
passed in three flags that instruct Windows to center the string in the RECT. Figure 11.3 illus-
trates how Windows continues to center the string, regardless of what shape the window hap-
pens to assume. Notice that the string output by TextOut remains resolutely in place, regardless
of the shape or size of the window.
FIGURE 11.3.
The string output by
DrawText always finds the
center of the client area—
no matter how often you
drag or reshape its
boundaries.
I should point out that the previous paragraphs are really only an introduction to the subject of
outputting text in a Windows application.
relationship turns out to be surprisingly skewed. In the remainder of this chapter I provide a
few quick examples illustrating what I mean.
The Delphi Canvas.TextOut function makes internal calls to the Windows TextOut function:
procedure TCanvas.TextOut(X, Y: Integer; const Text: String);
begin
Changing;
RequiredState([csHandleValid, csFontValid, csBrushValid]);
Windows.TextOut(FHandle, X, Y, PChar(Text), Length(Text));
MoveTo(X + TextWidth(Text), Y);
Changed;
end;
The third line in this function is expected. Somewhere in the midst of this process the function
has to call the Windows API function TextOut. However, it’s not obvious that Delphi is also
going to call functions called Changing and Changed, as well as RequiredState.
The calls to Changing and Changed are really fairly trivial functions that just ensure that any one
method that hooks the OnChanging or OnChange event gets called. In essence, these are just courtesy
functions to let anyone who happens to be interested in the fact that the HDC is about to
change be notified that the change is going to occur.
The RequiredState function, however, ensures that the Handle, Font, and Brush associated
with the device context are valid and are set to the proper values. For instance, if the user has
changed the Font property associated with the Form, the proper font has to be created and
selected into the device context.
Finally, the call to MoveTo sets off another call to RequiredState, this time one that again checks
to be sure the device handle is valid. Once all is well, the Windows API MoveToEx function is
called.
The point here is that there is a huge difference between calling Windows.TextOut and calling
Canvas.TextOut. Canvas.TextOut provides many services for you that Windows.TextOut does not
provide. On the other hand, it also must take many more clock cycles to execute. To really
understand Delphi, you need to understand the difference between these functions, and you
need to understand that you have a choice. You can call Windows.TextOut, or you can call
Canvas.TextOut. However, the importance of your choice will not become clear to you until
you start writing the kind of Windows API program demonstrated by the EasyText example
shown earlier.
It’s noteworthy that there is no Delphi equivalent of the DrawText function, although you can
construct one if necessary:
procedure TForm1.FormPaint(Sender: TObject);
var
Rect: TRect;
begin
Windows.GetClientRect(Handle, Rect);
DrawText(Canvas.Handle, ‘The middle of the road’, -1, Rect,
DT_SINGLELINE or DT_CENTER or DT_VCENTER);
end;
After writing the code shown here, you would think you would be done. However, in my ver-
sion of Delphi, this code does not act as you would expect. In particular, it is not updated each
time the Window is resized.
To get this kind of code to act as you would expect, you need to give it a little push in a FormResize
method:
procedure TForm1.FormResize(Sender: TObject);
begin
InvalidateRect(Handle, nil, True);
end;
Once again, my point is that the Delphi VCL does not always act as you expect it to act. The
Delphi VCL is not necessarily as thin a wrapper around the Windows API as you might ex-
pect. In fact, sometimes the VCL seems to take over altogether and to in effect rewrite Win-
dows.
Using the DrawText example shown earlier, try playing with the font for the form. Notice that
when you use Canvas.Handle as your device context, the output for your program reflects changes
you make to the Form1.Font property. Now try the following code:
procedure TForm1.FormPaint(Sender: TObject);
var
Rect: TRect;
DC: HDC;
begin
DC := GetDC(Handle);
Windows.GetClientRect(Handle, Rect);
DrawText(DC, ‘The middle of the road’, -1, Rect,
DT_SINGLELINE or DT_CENTER or DT_VCENTER);
ReleaseDC(Handle, DC);
end;
This code doesn’t use the HDC kept by the TCanvas object. Instead, it gets the HDC directly from
Windows:
DC := GetDC(Handle);
In this situation, changes that you make to the Form1.Font property are not reflected in the text
that is output by the DrawText function. In particular, I’m setting things up so that Delphi
never gets a chance to call TCanvas.RequiredState. These seemingly minor details have a huge
impact on the way Delphi programs perform. It’s one thing to look at Delphi as a simple RAD
tool, and it’s another to start digging in and seeing how Delphi is really put together under the
hood.
Notice, for instance, that Delphi would make it very hard for you to use TCanvas.TextOut to
write multiple fonts to a window at the same time. However, if you grab hold of the DC di-
rectly, you can select what fonts you want into it, even if you are using the Delphi TFont object
to create the font. It’s all a question of knowing how the tools work, finding their limits, and
finding their special capabilities. Dig down to the Windows API level, and it all starts to make
sense.
Summary
In this chapter, you have had a chance to get an overview of messages, message crackers, and
the wm_Paint message handler function.
One of the most important passages in this chapter discusses the way MESSAGES.PAS uses
records to parse the otherwise somewhat cryptic parameters passed to the WndProc. Message
crackers also are important because they help to break WndProcs up into structured code that is
relatively easy to debug and to understand.
You have also learned about the necessity of handling wm_Destroy messages, and you have got-
ten a glimpse of wm_Create messages and the crucial role they can play in your application.
The final sections of the chapter concentrate on device contexts and the BeginPaint and EndPaint
functions. You have seen that text can be output to a window with either the TextOut or DrawText
functions.
One final key point needs to be emphasized. If you create the Delphi RAD equivalent of the
EasyText program, it will be about 150,000 bytes in size, assuming you have all the optimiza-
tions turned on. The EasyText program itself is about 15,000 bytes in size. That is, it’s one-
tenth the size of a standard Delphi application. Both applications are smaller than their equivalent
C applications. That is, EASYTEXT.DPR produces a smaller application than a similar C
application does, and the Delphi RAD equivalent produces a smaller application than a C++
OWL application. Despite the innate strength of the Delphi development environment, it’s
still interesting to see that you can have a tenfold improvement in size just by dropping to the
API level.
In this chapter, you learn how to use the Windows API to get control over the mouse and
keyboard. In particular, you will see the following:
■ How to detect when a key has been pressed
■ How to detect which key has been pressed
■ How to detect if the system (Alt) key has been pressed
■ How to track the position of the mouse
■ How to detect mouse button presses
■ How to detect double-clicks
In addition, you’ll learn a number of tips about painting text to the screen that complement
the knowledge you picked up in Chapter 11, “Messages and Painting Text.”
Once again, the reason for presenting this material is simply to introduce you to the Windows
API. Delphi can call any Windows API function and can respond to any Windows message.
Furthermore, it is possible to structure an Object Pascal program along the basic lines of a stan-
dard C Windows API program with WinMain and WndProc. The potential to tap into the full
power of Windows is there, but you can’t do it unless you understand how the code works.
These chapters show how Windows works and how to tap into its resources using Delphi.
Most of the material in this chapter isn’t particularly difficult, but it is important because it
covers many fundamental I/O services. When you finish this chapter, you should have a good
feeling for how to handle messages and how to structure a Windows API application.
Though they don’t follow each other in direct sequence, you should read this chapter in coor-
dination with Chapter 24, “Objects, Encapsulation, and Properties,” which is an entirely VCL-
based chapter that covers very similar material. If you understand both chapters, you can feel
confident that you truly understand Windows-based event-oriented programming. This is one
of the key aspects of both Delphi and Windows. Serious programmers will benefit on a daily
basis from understanding it thoroughly on both the API and VCL levels.
Listing 12.1. The KeyMouse program shows how to trap keyboard and mouse
input.
program KeyMouse;
//////////////////////////////////////
// Program Name: KEYMOUSE.DPR
// Programmer: Charlie Calvert
// Description: Show how to trap keyboard and mouse input.
// Date: Dec 30, 1995
//////////////////////////////////////
uses
Windows, Messages, SysUtils;
// --------------------------------------------------------
// Interface
// --------------------------------------------------------
// Some variables
const
XVal: Integer = 10;
YVal: Integer = 30;
AppName = ‘KeyMouse’;
var
hWindow: HWnd;
// TKeyMouse
procedure KeyMouse_OnDestroy(var Msg: TWMDestroy); forward;
procedure KeyMouse_OnChar(var Msg: TWMChar); forward;
procedure KeyMouse_OnKeyUp(var Msg: TWMKeyDown); forward;
procedure KeyMouse_OnKeyDown(var Msg: TWMKeyDown); forward;
procedure KeyMouse_OnLButtonDown(var Msg: TWMLButtonDown); forward;
procedure KeyMouse_OnLButtonDblClk(var Msg: TWMLButtonDblClk); forward;
procedure KeyMouse_OnLButtonUp(var Msg: TWMLButtonUp); forward;
procedure KeyMouse_OnMouseMove(var Msg: TWMMouseMove); forward;
procedure KeyMouse_OnPaint(var Msg: TWMPaint); forward;
procedure KeyMouse_OnSysKeyUp(var Msg: TWMSysKeyUp); forward;
procedure KeyMouse_OnSysKeyDown(var Msg: TWMSysKeyDown); forward;
// --------------------------------------------------------
// Initialization
// --------------------------------------------------------
//////////////////////////////////////
// Program entry point
//////////////////////////////////////
type
TWMChar2 = record
Msg: Cardinal;
CharCode: Word;
Unused: Word;
cRepeat: Word;
ScanCode: Byte;
KeyData: Byte;
continues
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 20, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
case Msg.CharCode of
vk_Left: S := ‘Left arrow pressed. ‘;
vk_Right: S := ‘Right arrow pressed. ‘;
vk_F1: S := ‘F1 key pressed. ‘;
vk_F12: S := ‘F12 key pressed. ‘;
vk_Clear: S := ‘Numeric keypad 5, NumLock off’;
vk_NumPad5: S := ‘Numeric keypad 5, NumLock on ‘;
else
S := ‘Out to lunch, back in five! ‘;
end;
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 140, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
//////////////////////////////////////
// The Window Procedure
//////////////////////////////////////
function WindowProc(Window: HWnd; AMessage, WParam,
LParam: Longint): Longint; stdcall; export;
var
AMsg: TMessage;
begin
AMsg.Msg := AMessage;
AMsg.WParam := WParam;
AMsg.LParam := LParam;
AMsg.Result := 0;
case AMessage of
wm_Destroy: KeyMouse_OnDestroy(TWMDestroy(AMsg));
wm_Char: KeyMouse_OnChar(TWMChar(AMsg));
wm_KeyDown: KeyMouse_OnKeyDown(TWMKeyDown(AMsg));
wm_KeyUp: KeyMouse_OnKeyUp(TWMKeyUp(AMsg));
wm_MouseMove: KeyMouse_OnMouseMove(TWMMouseMove(AMsg));
wm_LButtonDblClk: KeyMouse_OnLButtonDblClk(TWMLButtonDblClk(AMsg));
wm_LButtonDown: KeyMouse_OnLButtonDown(TWMLButtonDown(AMsg));
wm_LButtonUp: KeyMouse_OnLButtonUp(TWMLButtonUp(AMsg));
wm_Paint: KeyMouse_OnPaint(TWMPaint(AMsg));
wm_SysKeyup: KeyMouse_OnSysKeyUp(TWMSysKeyUp(AMsg));
wm_SysKeyDown: KeyMouse_OnSysKeyDown(TWMSysKeyDown(AMsg));
else
Result := DefWindowProc(Window, AMessage, WParam, LParam);
Exit;
end;
Result := AMsg.Result
end;
//////////////////////////////////////
// Handle wm_DESTROY
//////////////////////////////////////
procedure KeyMouse_OnDestroy(var Msg: TWMDestroy);
begin
PostQuitMessage(0);
end;
//////////////////////////////////////
// Handle regular keyboard hits
// Use if you want to trap the letter keys or number keys.
//////////////////////////////////////
procedure KeyMouse_OnChar(var Msg: TWMChar);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 20, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
//////////////////////////////////////
// Any key is released
//////////////////////////////////////
procedure KeyMouse_OnKeyUp(var Msg: TWMKeyUp);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
continues
SetBkColor(DC, GetSysColor(Color_BtnFace));
ReleaseDC(hWindow, DC);
end;
//////////////////////////////////////
// Handle a key press
// Don’t try to process letter or number keys here. Instead,
// use wm_CHAR messages.
//////////////////////////////////////
procedure KeyMouse_OnKeyDown(var Msg: TWMKeyDown);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
ReleaseDC(hWindow, DC);
end;
//////////////////////////////////////
// Click on left mouse button
//////////////////////////////////////
procedure KeyMouse_OnLButtonDown(var Msg: TWMLButtonDown);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 100, PChar(S), Length(S));
ReleaseDC(HWindow, DC);
end;
//////////////////////////////////////
// This function is called when the user double-clicks the mouse
//////////////////////////////////////
procedure KeyMouse_OnLButtonDblClk(var Msg: TWMLButtonDblClk);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 100, PChar(S), Length(S));
ReleaseDC(HWindow, DC);
end;
//////////////////////////////////////
// This function is called when the mouse button is released
//////////////////////////////////////
procedure KeyMouse_OnLButtonUp(var Msg: TWMLButtonUp);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
S := Format(‘wm_LButtonUp ==> x = %d y = %d F = %d ‘,
[Msg.XPos, Msg.YPos, Msg.Keys]);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 120, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
//////////////////////////////////////
// This function is called whenever the mouse moves
//////////////////////////////////////
procedure KeyMouse_OnMouseMove(var Msg: TWMMouseMove);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
S := Format(‘wm_MouseMove ==> x = %d y = %d keyFlags = %d ‘,
[Msg.XPos, Msg.YPos, Msg.Keys]);
if ((Msg.Keys and MK_CONTROL) = MK_CONTROL) then
SetTextColor(DC, RGB(0, 0, 255));
if ((Msg.Keys and MK_LBUTTON) = MK_LBUTTON) then
SetTextColor(DC, RGB(0, 255, 0));
if ((Msg.Keys and MK_RBUTTON) = MK_RBUTTON) then
SetTextColor(DC, RGB(255, 0, 0));
if ((Msg.Keys and MK_SHIFT) = MK_SHIFT) then
SetTextColor(DC, RGB(255, 0, 255));
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 80, PChar(S), Length(S));
continues
//////////////////////////////////////
// Handle wm_PAINT messages.
//////////////////////////////////////
procedure KeyMouse_OnPaint(var Msg: TWMPaint);
const
Message : array[0..5] of string =
(
‘wm_Char’,
‘wm_Key’,
‘wm_SysKey’,
‘wm_MouseMove’,
‘wm_MousdDown’,
‘wm_MouseUp’);
var
PaintStruct: TPaintStruct;
Rect: TRect;
PaintDC: HDC;
OldFont: HFont;
i: Integer;
begin
PaintDC := BeginPaint(hWindow, PaintStruct);
SetBkColor(PaintDC, GetSysColor(Color_BtnFace));
GetClientRect(hWindow, Rect);
Rect.top := 5;
DrawText(PaintDC, ‘MOUSE AND KEYBOARD DEMONSTRATION’, -1, Rect, DT_CENTER);
Rect.top := 25;
Rect.bottom := 45;
DrawText(PaintDC,
‘(Try experimenting with the mouse and keyboard)’,
-1, Rect, dt_Center);
SelectObject(PaintDC, OldFont);
for i := 0 to 5 do
TextOut(PaintDC, XVal, YVal + (20 * (i + 1)),
PChar(Message[i]), Length(Message[i]));
EndPaint(hWindow, PaintStruct);
end;
//////////////////////////////////////
// This function is called whenever the ALT key is lifted.
//////////////////////////////////////
procedure KeyMouse_OnSysKeyUp(var Msg: TWMSysKeyUp);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
S := Format(‘wm_SysKeyUp == > ‘ +
‘ vk = %d cRepeat = %d ‘,
[Msg.CharCode, LoWord(Msg.KeyData)]);
ReleaseDC(hWindow, DC);
end;
//////////////////////////////////////
// This function is called whenever the ALT key is pressed.
//////////////////////////////////////
procedure KeyMouse_OnSysKeyDown(var Msg: TWMSysKeyDown);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
ReleaseDC(hWindow, DC);
end;
///////////////////////////////////////
// Register the Window Class
///////////////////////////////////////
function DoRegister: Boolean;
var
WindowClass: TWndClass;
begin
WindowClass.Style := cs_hRedraw or cs_vRedraw or cs_dblClks;
WindowClass.lpfnWndProc := @WindowProc;
WindowClass.cbClsExtra := 0;
WindowClass.cbWndExtra := 0;
WindowClass.hInstance := HInstance;
WindowClass.hIcon := LoadIcon(0, idi_Application);
WindowClass.hCursor := LoadCursor(0, idc_Arrow);
WindowClass.hbrBackground := hBrush(Color_Window);
WindowClass.lpszMenuName := nil;
WindowClass.lpszClassName := AppName;
///////////////////////////////////////
// Create the Window Class
///////////////////////////////////////
function Create: HWnd;
continues
var
AMessage: TMsg;
begin
if not DoRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
hWindow := Create;
if hWindow = 0 then begin
MessageBox(0, ‘Create failed’, nil, mb_Ok);
Exit;
end;
ShowWindow(hWindow, CmdShow);
UpdateWindow(hWindow);
Halt(AMessage.wParam);
end.
Color_BtnFaceColor_BtnFaceColor_BtnFaceColor_BtnFaceColor_BtnFaceColor
➥_BtnFaceColor_BtnFaceColor_BtnFaceColor_BtnFaceColor_BtnFace
FIGURE 12.1.
KEYMOUSE.DPR shows
the information conveyed
by the most important
mouse and keyboard
messages.
NOTE
From a technical point of view, I have opted to go with a global copy of hWindow and therefore
to pass each of my message handlers only the TMessage variant that they need to process. I dis-
cussed this approach in the last chapter, and I implement it here so that you can compare the
two techniques. Notice that the global hWindow is declared at the top of the program that all the
message handler routines utilize. Other than this brief note, I’m not going to spend much more
time discussing how the message handlers are dispersed from the WndProc. If you want to re-
view that kind of material, see the previous chapter, “Messages and Painting Text.” Most of
this chapter focuses on the implementation of these message handlers, rather than how they
were set up or why they were created.
forward Declarations
One minor technical point you might want to consider when looking at the KeyMouse pro-
gram is that all the MessageHandler routines are predeclared with forward declarations:
// TKeyMouse
procedure KeyMouse_OnDestroy(var Msg: TWMDestroy); forward;
procedure KeyMouse_OnChar(var Msg: TWMChar); forward;
procedure KeyMouse_OnKeyUp(var Msg: TWMKeyDown); forward;
procedure KeyMouse_OnKeyDown(var Msg: TWMKeyDown); forward;
procedure KeyMouse_OnLButtonDown(var Msg: TWMLButtonDown); forward;
procedure KeyMouse_OnLButtonDblClk(var Msg: TWMLButtonDblClk); forward;
procedure KeyMouse_OnLButtonUp(var Msg: TWMLButtonUp); forward;
procedure KeyMouse_OnMouseMove(var Msg: TWMMouseMove); forward;
procedure KeyMouse_OnPaint(var Msg: TWMPaint); forward;
procedure KeyMouse_OnSysKeyUp(var Msg: TWMSysKeyUp); forward;
procedure KeyMouse_OnSysKeyDown(var Msg: TWMSysKeyDown); forward;
Needless to say, predeclaring all these functions is not necessary. I do it only because it pro-
vides a place in your code that serves the same purpose as an object declaration. In other words,
it lets you get an overview of all the functions in the KeyMouse family, just as the TForm object
at the head of the Delphi RAD module gives you an overview of the code in a PAS file.
Some readers might find this technique useful, primarily because it’s always a good idea to
consider carefully any technique that helps you write clean, easy-to-read, self-documenting code.
You can just glance at the preceding declarations and learn a lot about this program. If you are
new to the KeyMouse program, you can get a quick overview of the program by just glancing
at these forward declarations. C programmers are also going to find this technique familiar
because it is strongly encouraged in that language.
All that you are doing with the forward directive is stating that you might call some of the
functions before you have defined them. That is, you might call them at a point one-third down
the page of the source file, and yet you implement them down near the very bottom, as is done
in the KeyMouse program. By writing out the header and following it with the word forward,
you can legally do this without confusing the compiler. Once again, this is not a necessity in
this particular case, it’s just a technique that might have appeal to careful programmers who
want to make their code as easy to maintain as possible. (To make sure I’m not confusing any-
one, let me state the matter again slightly differently: There may be occasional times when it is
necessary to use the forward directive, but this is not one of them.)
Table 12.1. Windows messages associated with the keyboard and mouse.
Message Result
WM_CHAR A number or letter key was pressed.
WM_KEYDOWN A key was pressed.
WM_KEYUP A key was released.
WM_LBUTTONDOWN The left mouse button was pressed.
WM_LBUTTONUP The left mouse button was released.
WM_LBUTTONDBLCLK The left mouse button was double-clicked.
WM_MOUSEMOVE The mouse was moved.
WM_SYSKEYDOWN The Alt key was pressed.
WM_SYSKEYUP The Alt key was released.
The majority of the rest of this chapter is dedicated to discussing these messages in one form or
another. There are a few side trips to discuss matters such as painting the screen, but the main
focus is on these very important messages.
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 20, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
In just a moment, I talk about the actual purpose of the KeyMouse_OnChar function and about
how it handles wm_Char messages. Before I get to that subject, however, I want to make sure
you understand the crucial difference between the way the KeyMouse_OnChar and the
KeyMouse_OnPaint functions handle the device context. This is a point you have to get clear in
your head or you’ll never get your programs to act correctly.
The first thing the KeyMouse_OnChar function does is get a device context. It does this not by
calling BeginPaint and EndPaint but by snagging the device context with calls to GetDC and
ReleaseDC. The TextOut method uses this device context to output information to the screen,
just as the EasyText program did in Chapter 11.
To understand the difference between calling GetDC and calling BeginPaint, you only need to
drag another program over the KeyMouse window. If, for instance, you were to temporarily
obscure the left half of the KeyMouse window, the scene visible in Figure 12.2 would greet
your eyes when the KeyMouse window is again brought to the fore.
FIGURE 12.2.
The KeyMouse program
with some of its informa-
tion blotted out after it was
temporarily obscured by
another program.
If you understand what has happened here, you are definitely getting a feel for the mechanisms
employed by the Windows messaging system. A wm_Paint message is sent to a window every
time a portion obscured by another window is uncovered. Therefore, the title and the words
on the left portion of the KeyMouse program are visible whenever the window is visible. How-
ever, any text painted in, say, the KeyMouse_OnChar function, won’t be repainted. The reason
for this, of course, is that only wm_Char messages are sent to the KeyMouse_OnChar method. It
doesn’t know, or care, if a wm_Paint message is sent!
NOTE
In DOS programs, the contents of windows are frequently saved wholesale in a buffer
and then restored whenever a window is brought back on top. This isn’t the way
Windows usually works. Instead, the contents of a window are restored by sending a
wm_Paint message to the window that needs to be repainted. When a window gets a
wm_Paint message, it explicitly performs the acts necessary to repaint itself. The point
here is that KeyMouse gets wm_Paint messages when it needs repainting, but it doesn’t
get wm_Char messages when it needs repainting. Therefore, any painting done in the
wm_Char handler won’t be updated automatically if it gets obscured by another window.
Nothing about the mechanism I’m describing is terribly complicated. Here are the two sides
to the story:
■ When responding to wm_Paint messages, you should use BeginPaint and EndPaint.
■ At all other times, you should use GetDC and ReleaseDC.
Now that you have a feeling for the difference between BeginPaint and GetDC, the next step is
to tackle the wm_Char message. wm_Char messages are sent to a program whenever a user presses
one of the standard alphanumeric keys. That is, if a user presses the A key or B key, a wm_Char
message is sent. Notice that wm_Char messages are not sent when the user presses one of the
function keys or the arrow keys on the numeric keyboard.
Here is the TWMChar structure that is passed to the KeyMouse_OnChar routine:
TWMKey = record
Msg: Cardinal;
CharCode: Word;
Unused: Word;
KeyData: Longint;
Result: Longint;
end;
TWMChar = TWMKey;
In particular, notice that TWMChar messages are handled by the TWMKey record. If this isn’t clear
to you, open up a copy of MESSAGES.PAS or MESSAGES.INT, if you have them available,
and study the way they are put together. Remember, time spent studying the source code is
never wasted! It’s one of the most valuable ways you can expend your time. If you get paid by
the hour, you should ask your boss or contractor to pay you twice as much for the time you
spend studying the Delphi source code! (I’m pulling your leg just a bit here, but still, nothing
is more valuable than the time you spend studying the source code!)
The value associated with a keypress is sent to the KeyMouse_OnChar function in the Msg.CharCode
variable, which is declared to be a word. In other words, if the user presses the A key, Msg.CharCode
is set to ASCII value 97, which is the letter a. If the user holds down the Shift key while press-
ing A, the ASCII value 65 is sent in the Msg.CharCode variable. ASCII 65, of course, represents
the letter A.
If a key is pressed repeatedly between calls to wm_Char, the number of key presses that occurred
is sent in the low word of the Msg.KeyData variable. Although this information is usually not
very important, Windows still passes it on to you in case you have some use for it.
NOTE
It’s worth noting here that it would not be hard to break up the TWMKey record into
additional fields for use with TWMChar messages:
type
TWMChar2 = record
Msg: Cardinal;
CharCode: Word;
Unused: Word;
cRepeat: Word;
ScanCode: Byte;
KeyData: Byte;
Result: Longint;
end;
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 20, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
The code shown here breaks the KeyData field of TWMKey into three separate fields called
cRepeat, ScanCode, and KeyData. I know to do this by referring to the online help for
wm_Char, found in WIN32.HLP. There I learn that bits 0–15 of LParam contain the
repeat count, bits 16–23 hold the OEM scancode, and the top eight bits contain
various pieces of miscellaneous data, as shown here:
24 Specifies whether the key is an extended key, such as the right-hand Alt
and Ctrl keys that appear on an enhanced 101- or 102-key keyboard.
The value is 1 if it is an extended key; otherwise, it is 0.
25-28 Reserved; do not use.
29 Specifies the context code. The value is 1 if the Alt key is held down
while the key is pressed; otherwise, the value is 0.
30 Specifies the previous key state. The value is 1 if the key is down before
the message is sent; it is 0 if the key is up.
31 Specifies the transition state. The value is 1 if the key is being released;
it is 0 if the key is being pressed.
As you can see, I choose to ignore the feast of information in the high byte of LParam,
and instead concentrate on the lower 24 bits. The code shown in this note is available
in the copy of KeyMouse that appears on disk. You can call KeyMouse_OnChar2 just by
changing one letter in the wm_Char response in the WndProc:
WM_CHAR: KeyMouse_OnChar2(TWMChar(AMsg));
Note that there is nothing to prevent you from creating as many custom TWMXXX
messages as you might need. In fact, you can define your own messages and create your
own TWMXXX records to go with them. What I am showing you in this chapter is in part
a code resource found in MESSAGES.PAS and in part a coding technique that you can
exploit for your own purposes.
The KeyMouse_OnChar function serves as a set piece for the virtues of MESSAGES.PAS. If
KeyMouse didn’t take advantage of message crackers, it would have to parse the wParam and
lParam variables sent to the program’s WndProc to determine the correct value of the keypress
and the repeat count. To help get hold of this issue you might want to look up wm_KeyDown and
wm_Char in the online help, as shown in Figure 12.3.
FIGURE 12.3.
A Borland help screen
shows how information
about keypresses is packed
into lParam and wParam.
Thanks to MESSAGES.
PAS, detailed understand-
ing of this information is
no longer needed.
Before message crackers came along, programmers ferreted out all this information on their
own by reading the fine print in the reference manuals, and by studying the actual bits in wParam
and lParam.
NOTE
Before closing this section it might be worth taking a moment to review some key
points. You should use wm_Char messages for reading the alphanumeric keys. These key
presses are correctly translated by Windows before the wm_Char message is sent.
Therefore, wm_Char messages are an ideal way to tell if a number or letter is selected by
the user.
NOTE
Don’t try to use wm_Char to detect whether function keys and letter keys have not yet
been properly translated (via the TranslateMessage function in the message loop) when
wm_KeyDown messages are sent. Hence, you can easily misinterpret the value of the
keystroke associated with a wm_KeyDown message. More information about wm_KeyDown
messages is presented in the next section.
SetBkColor(DC, GetSysColor(Color_BtnFace));
ReleaseDC(hWindow, DC);
end;
Windows makes it very easy for programmers to discover when a right-arrow key, left-arrow
key, or function key has been pressed. This information is packed into the message handler’s
TWMKey record in the CharCode field. (Once again, TWMKeyDown is set equal to TWMKey in
MESSAGES.PAS.)
The CharCode parameter plays the same role in the KeyMouse_OnKey function that it played in
the KeyMouse_OnChar function. In other words, if the user presses the Α key, that value is passed
to the KeyMouse_OnChar function in the CharCode field. However, if the user presses the left-
arrow key, the VK_LEFT constant is passed to the KeyMouse_OnKey method via the CharCode pa-
rameter. By default, this information is passed along as a raw number, and it’s up to you to
know which numbers are associated with which keystrokes.
The VK_LEFT constant is one of several predefined constants declared in the WINDOWS.PAS.
Here is an excerpt from that list:
VK_END = 35;
VK_HOME = 36;
VK_LEFT = 37;
VK_UP = 38;
VK_RIGHT = 39;
VK_DOWN = 40;
VK_SELECT = 41;
VK_PRINT = 42;
VK_EXECUTE = 43;
VK_SNAPSHOT = 44;
VK_INSERT = 45;
VK_DELETE = 46;
VK_HELP = 47;
{ VK_0 thru VK_9 are the same as ASCII ‘0’ thru ‘9’ ($30 - $39) }
{ VK_A thru VK_Z are the same as ASCII ‘A’ thru ‘Z’ ($41 - $5A) }
VK_LWIN = 91;
VK_RWIN = 92;
VK_APPS = 93;
VK_NUMPAD0 = 96;
VK_NUMPAD1 = 97;
VK_NUMPAD2 = 98;
You can also find this information in various reference books or in your online help files.
The following modified version of the KeyMouse_OnKey function shows how to explicitly detect
a few of these keystrokes:
procedure KeyMouse_OnKeyDown2(var Msg: TWMKeyDown);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
case Msg.CharCode of
vk_Left: S := ‘Left arrow pressed. ‘;
vk_Right: S := ‘Right arrow pressed. ‘;
vk_F1: S := ‘F1 key pressed. ‘;
vk_F12: S := ‘F12 key pressed. ‘;
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 140, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
Color_BtnFace
When examining this procedure, you might note the VK_CLEAR and VK_F12 constants. Captur-
ing both of these keystrokes could be a bit complicated for DOS programmers working with
certain compilers, but in Windows this kind of information is readily available.
For now, that’s all I’m going to say about the wm_KeyDown message. I won’t comment directly
on the wm_KeyUp message because it is simply the converse of wm_KeyDown. This subject is really
very straightforward. If you want to know more about the wm_KeyUp message, review the
KeyMouse_OnKeyUp routine, or spend some time with the wm_KeyUp entry in the WIN32.HLP
file that ships with Delphi.
NOTE
There is some chance that you did not install the Windows API help file when you ran
the Delphi setup program. The file is about 10 MB in size, so some people opt to leave
it on the CD-ROM. If you want, you can load it from the CD-ROM or you can copy
it over to your hard drive now.
yourself stuck in the midst of some lengthy, hair-pulling, patience-trying debugging session,
you might want to consider the possibility that you have simply forgotten to pass a message on
to the default window procedure for further processing. This seems to be less of an issue in
Windows 95 than it was in Windows 3.1, but you should still keep the thought in mind.
The code for handling wm_SysKeyDown messages is shown here:
procedure KeyMouse_OnSysKeyDown(var Msg: TWMSysKeyDown);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TWMSysKeyDown is again just another way of referencing TWMKey. As a result, the code shown here
is very similar to the code in the wm_KeyDown handler, so it needs no further comment. You
should, however, view the wm_SysKeyDown entries in the online help. I don’t repeat them here
because there is little point in my writing a book that does nothing more than rehash the online
help.
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 80, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
The first thing to notice is what a good job the message cracker does of passing on relevant
information to the KeyMouse_OnMouseMove function. In particular, note that the TWMMouseMove
record ends up resolving to a more primary structure, which in this case is called TWMMouse:
TWMMouse = record
Msg: Cardinal;
Keys: Longint;
case Integer of
0: (
XPos: Smallint;
YPos: Smallint);
1: (
Pos: TSmallPoint;
Result: Longint);
end;
If you look in a reference book, you see that when a window procedure gets a wm_MouseMove
message, information is packed into the hwnd, wParam, and lParam parameters. The latter of these
parameters carries information about the column the mouse is on in its low-order word, and it
carries information about the row it’s on in its high-order word. Thanks to the message crack-
ers, this obscure information is translated into the x and y coordinates visible in the record passed
to the KeyMouse_OnMouseMove function.
The KeyMouse_OnMouseMove function makes the x, y, and Keys parameters visible onscreen by
first translating them into a string and then displaying them through the TextOut function.
This is the same system you saw previously when studying the response to a keypress.
Sometimes it seems to me that the WndProc function is weathering a storm of messages that
come in at a rate that boggles the mind. You can get a feeling for how many wm_MouseMove
messages are sent to the program by studying the KeyMouse_OnMouseMove function in action.
Remember that every time a new set of mouse coordinates is shown onscreen, another message
has been processed by the WndProc. It’s as if the window procedure is walking through a bliz-
zard of messages that descends on it in great flurries of activity.
Besides the x and y coordinates of the mouse, the other piece of information sent with every
wm_MouseMove message is contained in the Msg.Keys argument. This parameter can have one of
the following values:
Constant Significance
MK_CONTROL The Ctrl key is down.
MK_LBUTTON The left mouse button is down.
MK_RBUTTON The right mouse button is down.
This code changes the color of the line output to the screen, depending on whether the Ctrl
key is pressed.
If you review the KeyMouse_OnMouseMove function, you will see that it changes colors depend-
ing on which mouse button is pressed or depending on whether the Ctrl or Shift key is pressed.
It does this just by testing the bits in the Msg.Keys field.
KeyMouse_OnMouseMove shows off the powers inherent in the Windows messaging system. The
line associated with the wm_MouseMove message smoothly changes colors as the mouse buttons
and Ctrl and Shift keys are pressed. Watching it in action helps you get a feeling for the agility
and flexibility of a message-based operating system. In practice, it turns out to be a much more
powerful and flexible system than it first appeared to me when I read descriptions of how it
worked.
NOTE
The SetTextColor and RGB API calls are used over and over again in Windows pro-
grams. As a result, if you don’t already understand them, you might want to take a few
moments to get a feeling for how they work.
The RGB macro returns a 4-byte value that designates a color. The color to be returned
is defined by the three parameters passed to the macro, each of which can have a value
of from 0 to 255.
If the third parameter is set to 255 and the others are set to 0, the resulting color is
dark blue. If the first parameter is set to 255, and the others to 0, the resulting color is
red. The middle parameter controls the amount of green in the color returned by the
RGB macro.
In the fourth example of the previous function, both the blue and the red are turned
on all the way, resulting in a deep purple. If you wanted to produce a gray color, you
could set all three values to 127. Setting them all to 0 produces a deep black, and
setting them all to 255 produces white. If you turn on green and red simultaneously,
you produce a bright yellow. With just a few minutes of experimentation, you should
begin to get a feeling for all the interesting combinations you can produce with this
powerful macro.
The SetTextColor function is easier to understand than the RGB macro because it does
nothing more than its name implies. That is, it sets the text color output with the
device context passed to it. In other words, it copies the result of the RGB function into
the device context.
S := Format(‘WM_LBUTTONUP ==> x = %d y = %d F = %d ‘,
[Msg.XPos, Msg.YPos, Msg.Keys]);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 120, PChar(S), Length(S));
ReleaseDC(hWindow, DC);
end;
All in all, nothing could be much simpler than this little fellow. Its purpose, of course, is sim-
ply to let the user know where the mouse was when the user released the left mouse button.
This is accomplished through the tried-and-true method of snagging hold of the device con-
text and using it to print information to the screen.
Handling button presses has one gray area, however, because it is necessary to distinguish be-
tween ordinary button presses and button presses that are really double-clicks. Fortunately,
Windows has a system that makes it relatively easy to distinguish between these two condi-
tions, as long as you know how to get started.
Here comes a gotcha that can trip up many unsuspecting Windows programmers. If you want
to process double-clicks on the left or right mouse button, you must begin by setting the win-
dow class style to CS_DBLCLKS. This is done in the Register method:
WindowClass.Style := cs_hRedraw or cs_vRedraw or cs_dblClks;
In this case, you can see that the style associated with the window class now contains three
flags. I covered the first two earlier in this book, and they are also covered in the online help.
The cs_dblClks style informs Windows that this class needs to know when the user double-
clicks the mouse in its client area. If you forget to do this, and if you don’t figure out what’s
wrong, you will begin pulling your hair out in about five minutes flat. I guarantee it.
Assuming you have the flags set up correctly, the message sent when the user double-clicks the
left mouse button is wm_LButtonDblClk:
procedure KeyMouse_OnLButtonDblClk(var Msg: TWMLButtonDblClk);
var
S: string;
DC: HDC;
begin
DC := GetDC(hWindow);
SetBkColor(DC, GetSysColor(Color_BtnFace));
TextOut(DC, XVal, YVal + 100, PChar(S), Length(S));
ReleaseDC(HWindow, DC);
end;
Take the time to study exactly how the KeyMouse_OnLButtonDblClk function works. When it is
called, the function prints out a string stating that a wm_LButtonDblClk message has been sent;
the function also informs the user about the current coordinates and other settings.
As I suggested earlier, Windows programs respond to changes in their environment just as a
well-driven car responds to highway signs, or to curves in the road. In other words, Windows
makes sure that a program is sent plenty of information about its environment. The job of the
programmer is to teach a program how to respond to these messages. You have seen that a
Windows program can respond to a blizzard of keyboard and mouse messages yet still stay on
the road.
Handling wm_Paint
It’s probably worth taking just a moment to discuss handling wm_Paint messages in the KeyMouse
program:
procedure KeyMouse_OnPaint(var Msg: TWMPaint);
const
Message : array[0..5] of string =
(
‘WM_CHAR’,
‘WM_KEY’,
‘WM_SYSKEY’,
‘WM_MOUSEMOVE’,
‘WM_MOUSEDOWN’,
‘WM_MOUSEUP’);
var
PaintStruct: TPaintStruct;
Rect: TRect;
PaintDC: HDC;
OldFont: HFont;
i: Integer;
begin
PaintDC := BeginPaint(hWindow, PaintStruct);
SetBkColor(PaintDC, GetSysColor(Color_BtnFace));
GetClientRect(hWindow, Rect);
Rect.top := 5;
DrawText(PaintDC, ‘MOUSE AND KEYBOARD DEMONSTRATION’, -1, Rect, DT_CENTER);
Rect.top := 25;
Rect.bottom := 45;
DrawText(PaintDC,
‘(Try experimenting with the mouse and keyboard)’,
-1, Rect, DT_CENTER);
SelectObject(PaintDC, OldFont);
for i := 0 to 5 do
TextOut(PaintDC, XVal, YVal + (20 * (i + 1)),
PChar(Message[i]), Length(Message[i]));
EndPaint(hWindow, PaintStruct);
end;
Unlike the code shown in the various other message handlers, anything printed by this routine
stays onscreen even if the program is temporarily pushed into the background and covered by
other applications. This helps to illustrate the significance of wm_Paint messages. If you wanted
this program to always keep all its information current, you would have to output all its data in
response to wm_Paint messages.
This function starts by using DrawText to print some general instructions to the user at the top
of the screen. Notice that these calls to DrawText use the Rect function to constrain the output
inside two small rectangular areas at the top of the screen. At the same time, the function uses
the DT_CENTER flag to say that the text should be centered within these rectangles.
The code also changes the font that is used to output the top two instructional lines. To do
this, it first selects a system font into the current device context:
OldFont := SelectObject(PaintDC, GetStockObject(OEM_FIXED_FONT));
When you select a font into a device context, the font that was previously selected is spit out
the back end of the SelectObject function. It’s as if the device context has room for only one
font, and indeed, it probably does contain a record that has only one field for tracking fonts.
The key point here is that you don’t want to waste system resources by just throwing the old
font away. Instead, you need to preserve it in a temporary variable, which in this case is called
OldFont. When you are finished using the new font, you need to restore the old font:
SelectObject(PaintDC, OldFont);
In some cases you would also need to destroy the font that you originally selected into the device
context. In this case, however, that is not necessary because you selected one of the system fonts,
which are created and maintained by Windows. In particular, you selected the OEM_FIXED_FONT,
which is just a standard font that Windows keeps around for anyone or anything that needs it.
The little loop at the bottom of the program is used to write the system messages down the left
side of the program. If you play with the KeyMouse program, you will notice that these labels,
which read wm_Char, wm_Key, and so forth, never disappear as long as the program is onscreen.
They are painted in response to a wm_Paint message, so they are permanently fixed to the main
screen of the program. In a sense, this whole program is designed to emphasize the fact that
wm_Paint messages are sent by the system whenever the screen needs to be updated, whereas
wm_Char, wm_Key, and all the other messages shown in this program are sent only when the user
explicitly triggers them.
In this program I use SetBkColor rather than SetBkMode, which I used in the last chapter. It’s
important to understand why I made this change. SetBkColor causes TextOut to overwrite the
text that underlies it, whereas SetBkMode leaves the old text exposed, unless it is directly over-
written by the characters themselves. In other words, SetBkColor causes a letter to wipe out a
rectangular area, whereas SetBkMode just wipes out the area directly beneath the black part of a
letter. Clearly this has something to do with the bit modes Windows uses when blitting the
text to the screen.
Because the output from this program writes multiple lines of text at the same coordinates, it
is necessary to ensure that the text that is being overwritten is entirely blanked out. SetBkColor
does the job. I also tend to append a few blank spaces onto the end of the output strings in case
the new text is a little shorter than the old text. The blank spaces wipe out the occasional letters
that might stick out farther in the old text than in the new. You should notice that built-in
Delphi controls such as TLabel or TEdit take care of this kind of thing for you automatically.
Once again, you can better understand how and when to use Delphi VCL controls if you un-
derstand exactly what they do for you and exactly when they need to expend extra clock cycles
performing a task that may or may not be necessary for you.
Summary
In this chapter, you have dug beneath the surface of a Windows program to see how keyboard
and mouse messages are handled.
In particular, you have taken a look at GetDC, ReleaseDC, RGB, and the SetTextColor functions.
You have also been introduced to a slew of Windows constants, such as VK_CLEAR ,
wm_LButtonDblClk, and MK_CONTROL. Obviously, most people are never going to be able to memo-
rize all these identifiers, so you must learn how to use the reference books and online help ser-
vices available to you. Don’t try to memorize the constants; instead, memorize the places where
you can find them.
Of course, some messages are so important that you should always have them in mind. Mes-
sages that fit in this category include wm_Paint, wm_KeyDown, wm_Char, wm_MouseMove, and
wm_lButtonDown. These easy-to-comprehend messages prove their usefulness again and again
when you are programming Windows applications.
To help you understand the main messages shown in this program, you should also read Chapter
24, “Objects, Encapsulation, and Properties,” which is the VCL counterpart to this chapter.
Perhaps I should have placed these two chapters back to back, but I instead opted to put chap-
ters directly involving the Windows API in one place, and chapters directly involving the Win-
dows VCL in a second place. It would not be inappropriate, however, for you to go directly
from this chapter to Chapter 24 to see how to write a VCL program that does essentially the
same thing as KeyMouse.
A key point to remember is that wm_KeyDown messages occur whenever a key is pressed. How-
ever, when this message is sent to a window procedure, Windows has not yet translated the
keystroke. As a result, it’s easy to become confused about exactly which key has been pressed.
wm_Char messages, however, are an ideal place to find out about which alphanumeric key the
user has pressed.
The BeginPaint and EndPaint functions are used only when responding to wm_Paint messages.
Because wm_Paint messages are sent to a window whenever it needs to be updated, anything
painted to the screen (with a DC retrieved by BeginPaint) is always visible when the window
is visible. The GetDC function, however, can be called at virtually any time during the life of an
application. As a result, it gives you considerably more flexibility than the BeginPaint function
does. However, if you call GetDC, the screen won’t be updated automatically after it was cov-
ered up by some other object (such as a window or dialog box). You can use BeginPaint only
in response to wm_Paint messages, and you should not use GetDC in response to a wm_Paint message.
It doesn’t take a genius to see that the Windows message system works. It gives programmers
the kind of control over the computer’s hardware that DOS programmers often longed in vain
to achieve. So, take the time to linger over the riches revealed in this chapter.
This chapter concentrates on events and DLLs. In particular, you will learn how to:
■ Load and unload a DLL at runtime.
■ Create Delphi DLLs that contain Delphi dialogs. You can call these DLLs from
standard Delphi programs, from Delphi Windows API programs, and from other
environments such as Borland C++ or Paradox.
■ Use the Windows API to create old-style dialogs in tiny DLLs that can be called from
Delphi programs, C programs, or any tool that supports DLLs.
■ Share long strings between executables and DLLs with the ShareMem unit.
Like the last chapter, this one has lots of information in it, much of it presented on a large but
tightly woven tapestry. Windows is a complicated operating system, and many of its key fea-
tures will be laid bare. The chapter starts with a basic overview of DLLs, and quickly moves on
to more advanced material. Concentrate on the main threads of the discussion, and you’ll end
up with a considerable amount of useful information by the end of this chapter.
DLL Basics
DLL stands for dynamic link library. DLLs are simple binary files containing sets of routines
that can be called by any application or even by another DLL. They are called dynamic link
libraries because they are linked into a program at runtime as they are needed.
DLLs are valuable because they enable you to write a single set of routines that can be called by
a number of different programs. You can create smaller executables by avoiding the wasteful
habit of linking the exact same code into multiple programs. Furthermore, DLLs can be loaded
or unloaded from memory at runtime, allowing you to build applications that are many mega-
bytes in size but which may never occupy more than a few hundred kilobytes of memory at
any one time. The trick, of course, is to design DLLs that contain modules specific to a par-
ticular part of your program. That way you can load one DLL in memory when the user is
working with one set of tools, then unload it and load another when the user wants to move to
another part of your program.
You also can produce multiple DLLs that have the same interface, but that work slightly dif-
ferently. For example, if you create a DLL that contains functions for displaying text to the
user, you could build multiple copies of this DLL, each copy containing language-specific in-
formation. The first DLL might contain Spanish phrases, the second French phrases, the third
Chinese, and so on. You could ship different versions of this DLL with different versions of
your product. That way the core of your program could make the same calls to what appears
to be the same DLL, but will in fact be a language-specific DLL. The calls will have the same
names in each DLL, and the DLL itself will always have the same name, but the results of a call
will depend on which particular DLL is shipped with that version of the product.
DLLs are one of the most important constructs inside Windows. In fact, Windows itself is in
large part nothing but a collection of DLLs. If you look inside the WINDOWS\SYSTEM
directory, you find files such as GDI32.DLL, KRNEL32.DLL, and USER32.EXE. The logic
that runs Windows is stored in these and other files, all of which are DLLs.
NOTE
Most DLLs have the letters DLL as an extension. You can, however, give a DLL file
any extension you want by using the DOS Rename command. For example, files with
a DRV extension are also almost always DLLs. These are driver files that run things
like your printer or your video display.
In Windows 3.1, the core of the Windows API was stored in files called GDI.EXE,
KERN386.EXE, and USER.EXE. These were DLLs, and in fact you will still find
copies of these binary files shipping with Windows 95. They are provided to supply
the strong 16-bit compatibility available in Windows 95.
To get a feeling for what role DLLs play in your environment, open up WINDOWS.PAS and
look at the declarations it contains. WINDOWS.PAS, of course, is the unit that serves as an
interface between Delphi and the Windows API. Here is a small excerpt:
function MessageBeep; external user32 name ‘MessageBeep’;
function MessageBoxA; external user32 name ‘MessageBoxA’;
function MessageBoxW; external user32 name ‘MessageBoxW’;
function MessageBox; external user32 name ‘MessageBoxA’;
function ModifyMenuA; external user32 name ‘ModifyMenuA’;
function ModifyMenuW; external user32 name ‘ModifyMenuW’;
function ModifyMenu; external user32 name ‘ModifyMenuA’;
function MoveWindow; external user32 name ‘MoveWindow’;
This business of calling MessageBoxA and MessageBoxW is simply a way to handle the Unicode
versions of these functions. MessageBoxA is the standard ASCII version that you call on most
occasions, and MessageBoxW is the Wide, or Unicode, version of the call.
The code excerpt from WINDOWS.PAS states that the MessageBox function is stored in
USER32.DLL, and that it can be looked up in the module by searching for the name or string
‘MessageBoxA’. If you scan through the rest of WINDOWS.PAS, you can see that there are
more than a thousand functions, most of which are in the major DLLs such as GDI32.DLL,
USER32.DLL, and KRNEL32.DLL.
NOTE
Under Windows 3.1, KRNL386 and KRNL286 are both referred to as 'KERNEL' inside
of the old WINPROCS unit:
function FindResource; external ‘KERNEL’ index 60;
function LoadResource; external ‘KERNEL’ index 61;
function LockResource; external ‘KERNEL’ index 62;
function FreeResource; external ‘KERNEL’ index 63;
Needless to say, Windows decides that 'KERNEL' refers to one or the other of these
DLLs, depending on the particulars of your current system and setup. In Windows 95
and Windows NT, there is no support for 286 computers, so there was no need to
have two versions of KERNEL32.DLL.
Another DLL that Delphi uses all the time is CMPLIB32.DCL. Once again, don’t let the ex-
tension fool you. CMPLIB32.DCL is a DLL with its extension renamed. To see the source for
this DLL, go to the Options | Tools | Library menu option and choose Save Library Source Code.
Then recompile CMPLIB32.DCL, as explained in Chapter 1. When you are done, you will be
able to find the source for your copy of CMPLIB32.DCL. Here, for instance, are the opening
lines of my DPR file for CMPLIB32:
library cmplib32;
uses
ShareMem,
…
As you already know, CMPLIB32.DCL is the file that you recompile when you add compo-
nents, OCXs, or other tools to the Delphi environment. In other words, Delphi itself is in part
just a series of DLLs. Your ability to extend Delphi’s IDE is a simple result of the fact that you
can recompile a key DLL that is in fact part of Delphi itself. In particular, you can rebuild
CMPLIB32.DCL. (Another seemingly mysterious and complex concept that ends up being
quite simple!)
One last thing you should know about DLLs is that they can be explored using the
TDUMP.EXE utility that ships with Delphi 1.0, C++, BP7, and other Borland products. To
use this file, go to the DOS prompt and type:
tdump filename
where > kernel.TXT pipes the output into a text file. When you are done, KERNEL.TXT will
list all the functions available in KERNEL, even if they are not documented elsewhere:
If you are interested in this information, I have included the output from USER.EXE and
KRNL386.EXE in the files USER.TXT and KERNEL.TXT on the disk accompanying this
book. If you are very interested in this topic and want to explore it in depth, you should refer
to Andrew Schulman’s Undocumented Windows and Unauthorized Windows 95.
A Simple DLL
Also on the CD is a simple DLL example stored in a directory called SimpDLL. If you’ve never
created a DLL before, you can use this example to examine the most basic facts about DLLs.
Because it’s so simple, I’ll discuss it only briefly here and then move on to the meat of this
chapter, which involves several much more complex examples.
The SimpDLL directory contains one DLL called DLLTEST.DLL and one very simple ex-
ecutable called RUNDYNLK.DPR, which stands for “Run Dynamic Link Library.” The code
for the DLL and the executable appear below in Listings 13.1 and 13.2. If you get an error
message about the uses clause for the DllTest library, you can safely ignore it.
Listing 13.1. The DllTest module is a “simplest possible case” DLL that shows the
basics of putting together a dynamic link library.
library Dlltest;
continues
exports
TestTen name ‘TestTen’;
begin
end.
Listing 13.2. The main module for the executable that calls the DllTest library.
unit Testcall;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
Before running this program, make sure that the DLLTEST.DPR module is compiled. Com-
piling that module will produce a DLL called DLLTEST.DLL, which must exist or this ex-
ample will not run.
When you run the main program a simple dialog containing a button and an edit control ap-
pears, as shown in Figure 13.1. If you press the button, the number 10 appears in the edit control.
This number is retrieved from the DLL’s sole function, which does nothing more than return
the number 10.
FIGURE 13.1.
The simple interface for
RUNDYNLK.EXE.
exports
TestTen name ‘TestTen’;
Notice that the first line of the function called TestTen contains the word “export.” This word
indicates that the function will be exported from this DLL. In other words, this function can
be called not only by routines residing inside the DLL, but it can also be called from outside
the DLL.
Besides the export directive, also notice the mandatory exports clause. This clause lists all the
functions that will be exported from the DLL, along with the name under which each will be
exported. While you may have several exported functions listed in various units attached to
your DLL, the exports clause should appear only in the DPR file, to list all the exported func-
tions in one place. The purpose of the exports directive is to provide the name or index under
which the function will be exported.
NOTE
Don’t confuse the exports directive used to list all the functions exported from a DLL
with the export directive used to designate one particular function as being marked for
export.
This DLL exports its sole function by name. That is, it uses the name directive, and follows that
directive with the name of the function that needs to be exported. In Windows 3.1, it was faster
to look up functions in DLLs by index, but in Win32 you can use the name directive. Delphi
imports all its functions by name.
Here is how the exports clause would look if you exported functions by name rather than by
index:
exports
TestTen index 1;
You now know the basics of creating any DLL. And it is easy to create an Object Pascal DLL.
You import the TestTen function into the executable with the following line of code:
function TestTen: Integer; external ‘DLLTEST’ name ‘TestTen’;
This declaration states the header for the function; that is, it gives it a signature or type. The
external directive states that the function is implemented outside of the current module, and,
in particular, it is implemented in a DLL called DLLTEST.DLL. The declaration also states
that the function is exported from the DLL under the name ‘TestTen’.
After importing the function as shown above, you call it as you would any other Object Pascal
or Windows API function:
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text := IntToStr(TestTen);
end;
Here the TestTen function is called, and its result is translated into a string and shown in
Edit1.Text. You could also call the function this way:
X := TestTen;
Unlike the situation in Windows 3.1, you cannot automatically use a DLL to share memory
between multiple programs. Each program usually has a separate copy of any variables stored
in a DLL and of any allocations made in a DLL. As a rule, use memory-mapped files, or OLE
automation, to share memory or variables between applications.
The main program that calls a DLL can use the handles opened by a DLL function, and vice
versa. You can, therefore, pass the handle of your main window to a DLL if you want to make
the windows in that DLL child windows of the main window, or if you want them to exist as
modal windows whose parents reside in the main executable. We’ll look at examples of both
these cases later in this chapter.
The whole C/C++ business about LibMain, DllEntryPoint, and WEPs does not apply to Ob-
ject Pascal DLLs. Instead, you can use the initialization section of a DLL unit as the entry
point, and you can use the finalization section as the exit point:
unit fileio;
interface
implementation
var
F: Text;
initialization
AssignFile(F, ‘c:\Sam.txt’);
ReWrite(F);
WriteLn(F, ‘Init’);
Flush(F);
finalization
WriteLn(F, ‘Close’);
CloseFile(F);
end.
In the example shown here, the initialization code is executed as the DLL is loaded into
memory, and the finalization section is executed as the DLL is unloaded. A similar example
is supplied on the accompanying disk.
You cannot include the initialization and finalization sections in the DPR file for a li-
brary, but you can include them in a unit listed in the DPR file’s uses clause. This has the same
effect, as the unit’s initialization clause is executed before any code in the DPR file.
The begin..end clause at the bottom of the DPR file for a library is executed at load time, but
after the initialization section for any units included with the code. You can therefore initialize
variables in the begin..end clause, but it is not a great place to allocate memory, since there is
no good way to guarantee that the memory will be disposed.
If you use load-time dynamic linking (that is, if you don’t call LoadLibrary), the links to the
functions in a DLL are mapped at load time, but the code for each of the functions may or may
not stay in memory. The code will, of course, be loaded into memory when you actually call
the functions in question; the rest of the time, however, the code may be swapped to disk. In
WIN32 it doesn’t matter if you use the PRELOAD or LOADONCALL directives; they have no effect.
FIGURE 13.2.
A dialog built in Delphi for
retrieving a date from the
user.
The point, of course, is that deciding to write a straight Windows API application does not
force you to give up all the advantages of the Delphi RAD tools. In particular, you can create
a DLL using the visual tools, and then call that DLL from inside your API-based application.
This technique keeps the size of your application down, while simultaneously allowing you to
use certain RAD development tools. Your application’s memory usage will suffer somewhat
for a short period of time while the DLL is being accessed, but you will be free of the DLL’s
weight once it is no longer needed. Most of the time you will have a small, fast executable in
memory, and only temporarily will you also have the DLL in memory.
To make this work correctly, you need to call LoadLibrary and FreeLibrary. These two calls
allow you to launch your application without having to simultaneously load your application’s
DLLs into memory. If you need to access a DLL, you can call LoadLibrary to read the DLL
into memory and give you a handle to its functions, but the DLL must be in memory only
during the times that you need it.
Two sample API programs appear below. The first, called SMALL1.DPR, shows how to call a
DLL without using LoadLibrary. The second shows how to access a DLL through the good
graces of the LoadLibrary function. Before creating either of these programs, however, it’s nec-
essary to first take a moment to put together a Delphi VCL-based DLL.
The form of the DLL is illustrated previously in Figure 13.2. The project source file appears
below, in Listings 13.3 and 13.4.
Listing 13.3. The project source file for the DATEINFO DLL.
library dateinfo;
uses
ShareMem,
main in ‘main.pas’ {DateInformation};
exports
GetDate index 1 name ‘GetDate’;
begin
end.
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
continues
const
{ From SYSUTILS.INC }
SShortMonthNames = 65472;
SLongMonthNames = 65488;
SShortDayNames = 65504;
SLongDayNames = 65511;
type
TDateInformation = class(TForm)
Cal: TCalendar;
bbOk: TBitBtn;
bbCancel: TBitBtn;
Bevel1: TBevel;
cbMonth: TComboBox;
cbYear: TComboBox;
procedure FormCreate(Sender: TObject);
procedure cbMonthChange(Sender: TObject);
procedure cbYearChange(Sender: TObject);
public
function GetDate: string;
end;
var
DateInformation: TDateInformation;
implementation
{$R *.DFM}
Cal.Month := cbMonth.ItemIndex + 1;
end;
end.
This DLL enables the user to select a date from a calendar. There are two combo boxes on the
main form, one containing the 12 months of the year, and one holding a list of the years be-
tween 1900 and 2100 AD. The user can select a year and a month from the combo boxes, and
can select a date from the calendar component in the middle of the form.
Notice the reference to ShareMem in the first position of the uses clause of both the DLL and
the executable that calls it. This is necessary because the memory allocated for the long strings
used in both modules must be shared. In other words, you are going to allocate memory for a
string in the DLL, and then use the string in the executable. This will not work properly unless
you add ShareMem to your uses clause. This unit loads DELPHIMM.DLL into memory.
(DELPHIMM.DLL ships with Delphi in the DELPHI32\BIN directory, and contains the
memory manager.)
Ensuring that DELPHIMM.DLL is loaded in memory before your application runs can be
trying. As a result, you might prefer to use ShortStrings or PChars rather than long strings. If
so, you can remove the reference to ShareMem from your uses clause. The version of
DATEINFO.DLL that ships with this book uses the latter approach, and eschews ShareMem.
This is probably the wisest course to take when shipping any application that you produce with
Delphi. Here is how GetDate looks if you use ShortStrings:
procedure GetDate(Handle: THandle; var S: ShortString); export;
begin
Application.Handle := Handle;
DateInformation := TDateInformation.Create(Application);
if DateInformation.ShowModal <> mrCancel then
S := DateInformation.GetDate
else
S := ‘Unknown’;
DateInformation.Free;
end;
Notice that the variable S is declared as a ShortString, and not as a string. I will discuss this
procedure in a moment. The only point I’m making at this stage is that you have a choice between
using ShareMem and not using it. The choice involves a decision whether or not to use long
strings, and the function above shows the particular method that needs to be changed if you
don’t use ShareMem. This method needs to be changed because it is the one that will be ex-
ported from the library.
The project source code uses the reserved word Library to tell the compiler that this is a DLL
and not an executable. This word will be inserted automatically if you choose File | New | DLL
from the menu. Alternatively, you can create a normal project, save it, and then change the
word project to library:
project DateInfo; => library DateInfo;
The reserved word exports is used to list the names of the functions exported by the DLL. The
DLL’s sole function can be exported by name alone, or by name and index. Win32 handles
exporting functions equally well by either method. Here is the way the main module would
look if you did not use ShareMem and exported GetDate by name only:
library dateinfo;
uses
main in ‘main.pas’ {DateInformation};
exports
GetDate name ‘GetDate’;
begin
end.
The first lines in this method fill a combo box with a list of the names of the months of the
year:
for i := 0 to 11 do
cbMonth.Items.Add(LoadStr(SLongMonthNames + i));
SLongMonthNames is a constant declared in SYSUTILS.INC, which ships with the compiler. I’ve
copied this constant out of the include file and placed it at the top of the main module. These
numbers reference some strings held in SYSUTILS.RC:
SLongMonthNames + 1, “February”
SLongMonthNames + 2, “March”
SLongMonthNames + 3, “April”
SLongMonthNames + 4, “May”
SLongMonthNames + 5, “June”
SLongMonthNames + 6, “July”
SLongMonthNames + 7, “August”
SLongMonthNames + 8, “September”
SLongMonthNames + 9, “October”
SLongMonthNames + 10, “November”
SLongMonthNames + 11, “December”
You can access these strings by calling the LoadStr function, found in the SYSUTILS unit. It’s
worth your effort to spend some time examining SYSUTILS.PAS, SYSUTILS.RC, and
SYSUTILS.INC so that you become familiar with the various routines, constants, and resources
available in these files. If you are having trouble understanding the way RC and include files
work, turn back to Chapter 10, “Dialogs and Resources.”
The Calendar object has a property called UseCurrentDate that defaults to TRUE. When turned
on, this property sets the calendar automatically to the current date. To make the month combo
box reflect this date you can enter the following line of code:
cbMonth.ItemIndex := Cal.Month - 1;
The items in the combo box are numbered 0 through 11. The numbers used to represent the
months of the year in the TCalendar object are based on the number 1. That is, they are labeled
1 through 12, where 1 is January and 12 is December. To synchronize these two sets of num-
bers, I subtract one from the currently selected month of the calendar object. For instance, if
the current month is October, then Cal.Month will be equal to 10. Subtract one from that
number, and you get 9, which is the offset of the word October in the list held by the combo
box. To set a combo box to a particular string in its string list, just find out the number of that
string in the string list, and assign that number to the ItemIndex property of the combo box, as
shown above.
After filling the second combo box with the years from 1900 to 2100, you must initialize the
box to the current year:
cbYear.ItemIndex := cbYear.Items.IndexOf(IntToStr(Cal.Year));
This code fragment shows several of the traits of the TStringList object associated with a combo
box. As in the example above, the code selects a particular string in the combo box by finding
the string’s index in the Items list, and then setting the ItemIndex property to that number. To
retrieve the index of the string, the code translates the Year property of the TCalendar object
into a string, then calls the TStringList IndexOf function, which returns the index of that string.
This index is then assigned to the ItemIndex property. This approach is not the only way to
solve this problem, but it’s reasonably efficient.
Once you have filled out the combo boxes at the top of the DateInformation form, there are
only a few additional steps you need to take to make this form behave properly. In particular,
you need to keep track of when a new month is selected in the combo box:
procedure TDateInformation.cbMonthChange(Sender: TObject);
begin
Cal.Month := cbMonth.ItemIndex + 1;
end;
This code performs the opposite action from the code shown in the FormCreate method. It is
called whenever the user changes the selection in the list box. In response to this action, the
code tells the calendar to switch to the newly selected month chosen in the combo box. In this
case, of course, you need to add 1 to the currently selected combo box index in order to prop-
erly synchronize it with the months of the year tracked by the TCalendar object.
Another important method in this unit is the one which returns the currently selected date:
function TDateInformation.GetDate: string;
begin
Result := IntToStr(Cal.Month) + ‘/’ + IntToStr(Cal.Day) + ‘/’ +
IntToStr(Cal.Year);
end;
This code is trivial in the extreme, and is noteworthy only in that it shows a little something
about string concatenation in Delphi.
The object in this DLL will not be initialized automatically in the project source. In particular,
there is no code like this in the DPR file:
begin
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
As a result, I have created my own function for this purpose, and exported the function so it
can be called from outside the DLL. Note that this is the same function referenced in the
export statement shown previously, and the same one I quoted when discussing the ShareMem
versus ShortStrings issue:
procedure GetDate(Handle: THandle; var S: string); export;
begin
Application.Handle := Handle;
DateInformation := TDateInformation.Create(Application);
Note that this version of the code assumes that you are using ShareMem.
This code first creates an instance of the TDateInformation object. This is the moment when
memory is allocated, and is roughly equivalent to calling the Object Pascal functions GetMem or
New, or the WinAPI functions HeapAlloc or VirtualAlloc. In fact, ultimately this call resolves
into a call to VirtualAlloc. The next step is to show the form to the user. When the user has
selected a date and pressed OK, you can free up the form’s memory and return the selected
date.
Remember! You won’t be able to share the string between the two units unless you add ShareMem
to the uses clause of both the DLL and the executable, or unless you use ShortStrings. The
reference to ShareMem should be the first entry in the uses clause for the main module of both
files. This crucial fact is not documented clearly, so it is also unclear whether it will change in
future versions of the product!
Overall this is not a particularly complicated DLL. I’ve explained it in some depth, however,
because it provides an interesting example of how to use Delphi components. Make sure you
compile a version of this DLL, and place it in the same directory as the program that will use
its services.
Listing 13.5. The Small1 program allows the user to use code stored in a DLL.
program Small1;
uses
ShareMem, Windows;
const
AppName = ‘SmallOne’;
var
DateStr: string;
begin
Result := 0;
case AMessage of
wm_Destroy: begin
PostQuitMessage(0);
Exit;
end;
wm_LButtonDown: begin
GetDates(Window, DateStr);
InvalidateRect(Window, nil, True);
Exit;
end;
wm_Paint: begin
PaintDC := BeginPaint(Window, PaintStruct);
SetBkMode(PaintDC, TransParent);
TextOut(PaintDC, 10, 10, PChar(DateStr), Length(DateStr));
EndPaint(Window, PaintStruct);
end;
end;
begin
WindowClass.Style := cs_hRedraw or cs_vRedraw;
WindowClass.lpfnWndProc := @WindowProc;
WindowClass.cbClsExtra := 0;
WindowClass.cbWndExtra := 0;
WindowClass.hInstance := HInstance;
WindowClass.hIcon := LoadIcon(0, idi_Application);
WindowClass.hCursor := LoadCursor(0, idc_Arrow);
WindowClass.hbrBackground := GetStockObject(LtGray_Brush);
WindowClass.lpszMenuName := nil;
WindowClass.lpszClassName := AppName;
var
AMessage: TMsg;
Window: HWnd;
begin
DateStr := ‘Click here to start’;
if not WinRegister then Exit;
Window := WinCreate;
if Window = 0 then Exit;
ShowWindow(Window, CmdShow);
UpdateWindow(Window);
Halt(AMessage.wParam);
end.
This code pops up a regular window. If you click the window, the date dialog from the DLL
appears, and you can select a date. If you click the OK button in the dialog, the string is re-
turned to the executable and displayed in its main window.
The executable imports one routine from the DLL, called GetDates:
procedure GetDates(Handle: THandle; var S: string);
external ‘DATEINFO’ name ‘GetDate’;
If you look in the DLL itself, you will find that there is no routine in the DLL called GetDates.
The name of the exported function is GetDate. This works because the code above states that
the routine to search for is called GetDate. The point is that the “name” under which the func-
tion is exported is GetDate, and what it’s called inside the executable is irrelevant. Of course,
most of the time you should try to call the function by the same name in the DLL and in the
executable, but you don’t have to do this. Be careful, however, to make sure you give the func-
tion the same signature in the DLL and in the executable. That is, the parameters passed to the
function should be identical in both cases.
The external directive in the above code specifies the DLL in which the function resides. In
this case, the name of the DLL is DATEINFO, and so I explicitly name that string. It is, of course,
necessary to specify this name exactly.
Once you have included this declaration in your code, you are free to call the GetDates method.
The above declaration automatically loads DATEINFO.DLL into memory and assures that
you can call the GetDate method when needed:
wm_LButtonDown: begin
GetDates(Window, DateStr);
InvalidateRect(Window, nil, True);
Exit;
end;
If you need to export many functions from a DLL, you should put them all in a single unit and
list the unit in your uses clause. To do this, you should declare the function normally in the
interface, and specify the relevant DLL and name in the implementation:
unit DLLUnit;
interface
uses
Windows;
implementation
end.
The above code represents a complete unit that will compile correctly and can be added to an
application. Clearly this is overkill for importing a single function. However, if you have a
complex set of routines that you need to work with, this is the preferred method for handling
them. See WINDOWS.PAS or WINSOCK.PAS for classic examples of this type of unit.
var
Window: Hwnd;
DateStr: string;
...
begin
GetDates(Window, DateStr);
...
end;
This is simple enough in principle, but it turns out there are a few more concepts to grasp before
you have a good understanding of the role of this function in the program.
Take a moment to study the WindowProc routine:
function WindowProc(Window: HWnd; AMessage, WParam,
LParam: Longint): Longint; stdcall; export;
var
PaintDC: HDC;
PaintStruct: TPaintStruct;
begin
Result := 0;
case AMessage of
wm_Destroy: begin
PostQuitMessage(0);
Exit;
end;
wm_LButtonDown: begin
GetDates(Window, DateStr);
InvalidateRect(Window, nil, True);
Exit;
end;
wm_Paint: begin
PaintDC := BeginPaint(Window, PaintStruct);
SetBkMode(PaintDC, TransParent);
TextOut(PaintDC, 10, 10, PChar(DateStr), Length(DateStr));
EndPaint(Window, PaintStruct);
Exit;
end;
end;
Each message that comes into this routine passes through the filter of a single, potentially very
large, case statement. In this particular example, the three messages handled are wm_Destroy,
wm_LButtonDown, and wm_Paint. (As you can see, I am not using message crackers in this par-
ticular program. I will in the next version of the code, but I thought it would be interesting to
omit them on this occasion just to remind you of how a program looks that doesn’t use them.)
NOTE
Remember that wm_Destroy messages are sent to all windows after they are removed
from the screen. It’s your chance to do any cleanup of variables allocated by your
window. A window may have a series of child windows or other allocations. For
instance, a window might be associated with various data structures for which memory
is allocated. This is the chance to deallocate the memory associated with those struc-
tures. If the window receiving a wm_Destroy message is the main window for an
application, it should call PostQuitMessage. PostQuitMessage terminates an application,
using its sole parameter as the application’s exit code. In particular, PostQuitMessage
tells the program to break out of the message loop.
When you actually break it down, a typical API program like Small1 isn’t really that complex,
but it appears complex, and it takes a long time to code. It’s worth contemplating, however,
because Delphi RAD programs go way out of their way to hide this kind of code from the user.
One of the major goals of tools like Delphi is to make the basic mechanisms that lie at the
heart of Windows intuitively obvious to the developer. If you click the Events page in the Object
Inspector and start handling OnMouseDown messages, you don’t have to think much about the
process involved. It comes naturally. However, if you want to create components that handle
wm_LButtonDown messages, you have to put the old nose to the grindstone! In short, Delphi’s
ability to protect you from the innards of Windows is a benefit up to a point, but it can be-
come a liability because it can prevent you from ever understanding what is really going on.
Once you think about it, it’s obvious the OnMouseDown events in the Object Inspector are just
Delphi’s way of handling wm_LButtonDown and wm_RButtonDown messages. Furthermore,
OnKeyPress events correspond to wm_Char messages, OnKeyDown events correspond to wm_KeyDown
messages, OnPaint to wm_Paint messages, and so on.
Sometimes this analogy breaks down, at least in part. For instance, OnDestroy events are not
exactly analogous to wm_Destroy messages. They are similar, but there are some slight differ-
ences in the way they behave. For instance, a Windows HWnd is still valid when you get a
wm_Destroy message, but it is not valid when you handle an OnDestroy event in Delphi.
The beauty of Delphi, of course, is that if you need to handle wm_Destroy messages instead of
OnDestroy messages, the system is flexible enough to allow you to do so. (See Chapter 22, “Han-
dling Messages,” for more information on this type of maneuver.) However, if you want to
decide whether you need to handle OnDestroy, or wm_Destroy messages, you first need to un-
derstand the Windows API. That’s one of the primary reasons I’m taking the trouble to write
this chapter and the ones related to it. If you want to be really good at using Delphi, you have
to understand not only how to use Delphi, but also how Delphi actually works. Furthermore,
to create first class components you need to understand this stuff at some fairly deep-seated
level.
The call to GetDates is the key line that causes the date dialog in the DLL to be shown to the
user. In fact, if you compile both the DLL and the executable with TDW information, then load
the executable into the stand-alone debugger, you can step into the GetDates routine, and into
the code in DATEINFO.DLL. Alternatively, you can load the executable into the stand-alone
debugger, then right-click the main pane of the debugger. Now select the Modules option,
and use its dialog to load the DLL into the debugger.
NOTE
Remember that you need to select the Project | Options | Linker | Include TDW
Debug menu option before you can use the stand-alone debugger. The stand-alone
debugger does not ship with all versions of Delphi, but it can be bought as an “up sell”
from Borland or through third-party retail stores. Programmer’s Paradise (800-445-
7899) is one mail-order store that carries these kinds of tools.
After the call to GetDates, the next line of code forces a wm_Paint message to be sent to the
WndProc:
InvalidateRect takes three parameters. The first is the Window that contains the WndProc to
which the wm_Paint message should be sent. The second is a Trect variable designating the area
in that window that needs to be repainted. If you pass nil in this parameter, the entire window
will be repainted. The final parameter specifies whether or not to erase the window before re-
painting it.
After InvalidateRect is called, the window is repainted as soon as the wm_Paint message hits
the WndProc. wm_Paint messages are sent whenever there are no other messages in the queue for
that window. In other words, wm_Paint messages are low priority, and they don’t get through
until other messages have already been processed. Another message with a low priority is
wm_Timer.
Shows how to use a DLL without having the DLL loaded at program
launch. That is, shows how to use LoadLibrary and FreeLibary. }
uses
ShareMem, Windows, Messages, SysUtils;
const
AppName = ‘SmallTwo’;
type
TGlobalVars = record
XSpot: Integer;
YSpot: Integer;
DateStr: string[200];
end;
var
GlobalVars: TGlobalVars;
{ -- Miscelaneous Routines -- }
{ -- Message Functions -- }
case AMessage of
wm_Destroy: wmDestroy(Window, TWMDestroy(AMsg));
wm_LButtonDown: wmLButtonDown(Window, TWMLButtonDown(AMsg));
wm_Paint: wmPaint(Window, TWMPaint(AMsg));
else
Result := DefWindowProc(Window, AMessage, WParam, LParam);
Exit;
end;
Result := AMsg.Result
end;
continues
var
AMessage: TMsg;
Window: HWnd;
begin
GlobalVars.DateStr := ‘UnKnown’;
ShowWindow(Window, CmdShow);
UpdateWindow(Window);
Halt(AMessage.wParam);
end.
This program looks and acts almost exactly the same as the Small1 program seen earlier in this
chapter. The main difference, from the user’s point of view, is that the string recording the
date is always printed out at the location where the user clicked the window, rather than in the
upper left-hand corner of the window. This refinement occurs because of the way the program
responds to wm_LButtonDown events.
NOTE
Remember that the version of this program on the CD-ROM passes Shortstrings back
and forth rather than using the ShareMem unit. If you are unclear about which tech-
nique you want to use, then use the technique found on the CD-ROM.
I might also add that if there is a discrepancy between the book’s version of the code
and version on disk, you should always trust the code on the CD-ROM. That code
will be tested against the latest version of Delphi, and it has the highest certainty of
being correct.
Despite the obvious similarities between the Small1 and Small2 programs, they contain a number
of fairly significant differences. Most of these differences are due to Small2 using message crackers
and message handling routines.
The key function in the program is the GetDate routine:
procedure GetDate(Window: HWnd);
type
TDateProc = procedure(Handle: THandle; var S: string);
var
hLib: THandle;
DateProc: TDateProc;
S: string;
begin
hLib := LoadLibrary(‘DATEINFO.DLL’);
if hLib < 32 then begin
MessageBox(Window, ‘No DLL’, nil, mb_Ok);
Exit;
end;
DateProc := TDateProc(GetProcAddress(hLib, ‘GetDate’));
DateProc(Window, S);
GlobalVars.DateStr := S;
FreeLibrary(hLib);
end;
This code has a rough parallel to the external directive used when importing a routine from a
DLL using the technique shown earlier in this chapter:
procedure GetDates(Handle: THandle; var S: string);
external ‘DATEINFO’ name ‘GetDate’;
The difference, of course, is that the external directive loads the DLL when the executable is
launched, while the LoadLibrary loads the DLL only when your program calls for it.
NOTE
Some DLLs will already be loaded into memory when your program starts. For
instance, GDI32.DLL or USER32.DLL are core Windows modules that are almost
surely loaded in memory at any given moment. The only reason you would ever call
LoadLibrary on DLLs of this type would be because you explicitly needed a handle
that referenced them.
In particular, what LoadLibrary really does is map a DLL into your program’s address
space. Its primary purpose is to give you access to a DLL, not to load it into memory.
If, however, you create a DLL for use with only your program, and you load it by
using LoadLibrary, it is unlikely that the DLL would be in memory before you
explicitly referenced it. It is not a certainty, however, that a call to FreeLibrary will
force your DLL to be unloaded from memory.
You can call LoadLibrary on an executable. The most likely reason for doing this is
that you want access to resources in another executable.
Note also that all the Windows API function calls that you make are calls into the
standard DLLs such as USER32.DLL. Every API call that you make is a call into a
DLL. This means that you call DLLs all the time; what’s new in this chapter is that
you are also building the DLL into which you are making the call.
To find out whether LoadLibrary succeeded, check if its return value is larger than 32:
if hLib < 32 then begin
MessageBox(Window, ‘No DLL’, nil, mb_Ok);
Exit;
end;
If the result were less than 32, it would be set equal to an error value. That is, hlib would con-
tain not a handle to a DLL, but an error number. To get information on the error, call
GetLastError.
Assuming that the call to the function succeeded, you have the handle to a DLL in your hands.
This handle is very much like the hInstance value given you at startup, or like the hWnd to a
window in your application. In particular, you can use this handle to call GetProcAddress:
DateProc := TDateProc(GetProcAddress(hLib, ‘GetDate’));
GetProcAddress returns the address of an exported routine in a DLL. GetProcAddress takes two
parameters. The first is a handle to a DLL module, and the second is a reference to an exported
routine. The reference can be either a string or a numeric value, although in Win32 string values
are almost always used. If you want to reference the function by number, rather than by name,
place the index value in the low-order word, and set the high-order word to zero.
GetProcAddress returns a TFarProc, which resolves to a Pointer. In this case, the specific signa-
ture of the requested function is known, so the result of the function is typecast as a TDateProc:
type
TDateProc = procedure(Handle: THandle; var S: string);
Procedural types always look a bit strange to programmers who haven’t worked with them before.
In particular, I often want to see the name of a function in a declaration of this type. “Proce-
dure what? ” one wants to ask! Of course, there is no particular function associated with this
type, any more than there is a particular integer associated with the (hidden) declaration of the
integer type in Object Pascal. It’s just an abstract type, and it just happens, in this particular
case, to match directly with the declaration of a function in the DateInfo DLL. There is more
information on procedural types, function pointers, method pointers, and related matters in
Chapter 23, “Objects and Inheritance.”
Once you’ve retrieved the address of the specified function from the DLL, you are free to call
the function in a normal fashion:
DateProc(Window, S);
GlobalVars.DateStr := S;
FreeLibrary(hLib);
This call to DateProc looks just the same as a similar call in the Small1 program. In particular,
you pass a window handle in the first parameter. This window handle assures that the modal
dialog which you are going to create knows its parent. In particular, the date dialog in the DLL
is going to be shown in a modal state, so it’s best if it has a specific parent. That parent is passed
in the first parameter. The second parameter is a pointer to the string you want to retrieve. It’s
a var parameter, and var parameters are always passed by address. That is, they are passed as
pointers, even though you don’t have to treat them explicitly as such.
The retrieved string is stored in a global variable, and the library is freed, which means that
Windows can now move it out of memory if need be. As I said earlier, you should not assume
that the DLL will always be discarded the moment you call FreeLibrary. In some circumstances,
Windows simply notes the fact that you are no longer using the DLL. It might not, however,
unload the DLL, because it is easiest to leave it where it is for the time being. Once again,
LoadLibrary and FreeLibrary are intended as means for accessing a DLL. It’s just coincidence
that they can also help you manage memory.
Before closing this section I want to mention once again that Windows is built on top of a
series of DLLs. Nearly all the calls in the WINDOWS.PAS unit are stored in the DLLs named
KERNEL32.DLL, USER32.DLL, and GDI32.DLL. These DLLs are handled by Delphi pro-
grams using the techniques shown in Small1. There is no need to load and unload these DLLs,
since they are kept in memory continuously almost by definition. Windows itself resides in
these DLLs, and nothing can happen without first calling them. If they are not in memory,
however, the declarations referencing them in WINDOWS.PAS ensure that they will be loaded
when needed.
In general, don’t be afraid to break your code up into DLLs. They are the core technique hold-
ing the Windows technology together. Take a look in the Delphi32\Bin directory and you will
find many more DLLs. Within reason, feel free to build as many DLLs as you want. They present
a fast, efficient way to handle memory. DLLs are the operating system’s bread and butter, and
you should use them to help you construct robust, well-organized, programs.
case AMessage of
wm_Destroy: wmDestroy(Window, TWMDestroy(AMsg));
wm_LButtonDown: wmLButtonDown(Window, TWMLButtonDown(AMsg));
wm_Paint: wmPaint(Window, TWMPaint(AMsg));
else
WindowProc := DefWindowProc(Window, AMessage, WParam, LParam);
Exit;
end;
Result := AMsg.Result
end;
The goal of this code is break up the case statement in the WndProc so that it is easier for you to
handle messages.
In a standard Delphi program, you don’t have to handle messages in a case statement. Instead,
each message is delivered to you in a custom message handler, or handled internally by the
VCL. The WndProc shown above delivers similar functionality. In particular, notice that there
are separate functions for handling wmPaint, wmLButtonDown, and wm_Destroy messages.
One of the themes of the last few chapters has been to point out how the VCL processes many
messages passed to your program without your knowledge. For instance, every wm_Paint mes-
sage or wm_Size message that your RAD program receives is parsed automatically by the TForm
object. The interesting thing about the programs shown in these chapters on the API is that
you are explicitly handling many of the messages yourself. Of course there are many other
messages that are being passed on to DefWindowProc and being handled by the operating sys-
tem. There is a good deal of difference, however, between having the message handled by
Windows, and having it handled by the VCL.
FIGURE 13.3.
The Saturn form from
FRACTDLL.DLL.
FIGURE 13.4.
The Squares form from
FRACTDLL.DLL.
FIGURE 13.5.
The Ferns form from
FRACTDLL.DLL.
The source code for the program is shown in Listings 13.7 through 13.14. Listing 13.8 is the
most important of these listings, as it contains the code for the straight Windows API DLL.
The main form and all of the straight Windows API DLL forms are shown in Figure 13.6. In
particular, the code for the windows with captions that read “Child without VCL,” “Popup
without VCL,” and “About” all reside in a DLL. That is, they are not Delphi RAD windows
and dialogs, but straight API windows and dialogs.
FIGURE 13.6.
The child, pop-up, and
dialog windows are all
created by straight
Windows API code that
resides in a 15KB DLL.
uses
Winstuff in ‘Winstuff.pas’;
exports
About index 1,
HandleButton index 2,
ShowAbout index 3,
RegisterWinPopup index 4,
CreateWinPopup index 5,
RegisterChild index 6,
CreateChild index 7;
begin
end.
Listing 13.8. The Windows API DLL features two windows and one dialog.
unit Winstuff;
{
This unit shows four types of routines exported from a DLL:
About
ShowAbout: Launch and run a modal dialog
RegisterWinPopup
CreateWinPopup: Create and show a popup window
RegisterChild
CreateChild: Register, Create, and Show a child window
}
interface
uses
Windows, Messages;
implementation
{$R WINSTUFF.RES}
continues
const
idm_About = 100;
{ -- Popup Window -- }
const
PopupName = ‘PopStuff’;
begin
Result := 0;
case AMessage of
wm_LButtonDown: begin
HandleButton(Window);
Exit;
end;
wm_RButtonDown: begin
ShowAbout(Window);
Exit;
end;
wm_Paint: begin
PaintDC := BeginPaint(Window, PS);
TextOut(PaintDC, 10, 10, PChar(S), Length(S));
TextOut(PaintDC, 10, 50, PChar(S1), Length(S1));
EndPaint(Window, PS);
end;
end;
Result := DefWindowProc(Window, AMessage, WParam, LParam);
end;
CmdShow := Sw_ShowNormal;
ShowWindow(Window, CmdShow);
UpdateWindow(Window);
Result := True;
end;
{ The Register routine for the Popup Window. The main chore
is to fill out the complex TWndClass structure, and pass it
to RegisterWindow. After the window has been registered,
the call to the local CreateWinPopup routine actually creates
it and shows it on screen. Only call RegisterWindow once
for each window in your application.}
continues
Result := CreateWinPopUp;
end;
{ -- Child Window -- }
const
ChildName = ‘ChildWindow’;
begin
ChildProc := 0;
case AMessage of
wm_LButtonDown: begin
HandleButton(Window);
Exit;
end;
wm_RButtonDown: begin
AboutProc := MakeProcInstance(@About, HInstance);
DialogBox(HInstance, ‘AboutBox’, Window, AboutProc);
FreeProcInstance(AboutProc);
Exit;
end;
wm_Paint: begin
PaintDC := BeginPaint(Window, PS);
TextOut(PaintDC, 10, 10, PChar(S), Length(S));
TextOut(PaintDC, 10, 50, PChar(S1), Length(S1));
EndPaint(Window, PS);
Exit;
end;
end;
Result := ASimpleWindow;
end;
{ The Register routine for the Child Window. The main chore
is to fill out the complex TWndClass structure, and pass it
to RegisterWindow. After the window has been registered,
the call to the local CreateChild routine actually creates
it and shows it on screen. Only call RegisterWindow once
for each window in your application.}
function RegisterChild(ParentWindow: HWnd): HWnd;
var
WndClass: TWndClass;
begin
if HPrevInst = 0 then begin
WndClass.Style := cs_HRedraw or cs_VRedraw;
WndClass.lpfnWndProc := @ChildProc;
WndClass.cbClsExtra := 0;
WndClass.cbWndExtra := 0;
WndClass.hInstance := HInstance;
WndClass.hIcon := LoadIcon(0, idi_Application);
WndClass.hCursor := LoadCursor(0, idc_Arrow);
WndClass.hbrBackground := GetStockObject(White_Brush);
WndClass.lpszMenuName := ChildName;
WndClass.lpszClassName := ChildName;
Result := CreateChild(ParentWindow);
end;
end.
Listing 13.9. The project source for the second DLL exports three functions, one
for each of its forms.
library Fractdll;
uses
Pics in ‘PICS.PAS’ {SpacePict1},
Squares in ‘SQUARES.PAS’ {DrawSqr},
Fern in ‘FERN.PAS’ {Ferns};
exports
ShowPictures index 1,
ShowSquares index 2,
ShowFerns index 3;
begin
end.
Listing 13.10. The Pics form is written entirely using the VCL. Its sole function
pops up a window containing some TImage and TTabbedNotebook controls.
unit Pics;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
ExtCtrls, TabNotBk, ComCtrls;
type
TSpacePict1 = class(TForm)
TabbedNotebook1: TTabbedNotebook;
Image1: TImage;
Image2: TImage;
Image3: TImage;
end;
var
SpacePict1: TSpacePict1;
implementation
{$R *.DFM}
begin
Application.Handle := Handle;
SpacePict1 := TSpacePict1.Create(Application);
try
SpacePict1.ShowModal;
finally
SpacePict1.Free;
end;
end;
end.
Listing 13.11. The Squares form shows off the TCanvas object.
unit Squares;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
const
BoxCount = 25;
type
TDrawSqr = class(TForm)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
Colors: array [1..BoxCount] of TColor;
procedure DrawSquare(Scale: Double; Theta: Integer);
public
{ Public declarations }
end;
var
DrawSqr: TDrawSqr;
implementation
{$R *.DFM}
type
TSquarePoints = array [0..4] of TPoint;
continues
end.
Listing 13.12. The Fern unit shows how to draw a fractal shape in a VCL
window.
unit Fern;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TDAry = array[0..3] of Double;
TFerns = class(TForm)
Timer1: TTimer;
procedure FormResize(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
MaxX, MaxY: Integer;
MaxIterations, Count: INteger;
x, y: Double;
public
procedure DoPaint;
end;
const
a: TDAry = (0, 0.85, 0.2, -0.15);
b: TDAry = (0, 0.04, -0.26, 0.28);
c: TDAry = (0, -0.04, 0.23, 0.26);
d: TDAry = (0.16, 0.85, 0.22, 0.24);
e: TDAry = (0, 0, 0, 0);
f: TDAry = (0, 1.6, 1.6, 0.44);
var
continues
implementation
{$R *.DFM}
procedure TFerns.DoPaint;
var
k: Integer;
TempX, TempY: Double;
begin
k := Random(100);
if ((k > 0) and (k <= 85)) then k := 1;
if ((k > 85) and (k <= 92)) then k := 2;
if (k > 92) then k := 3;
TempX := a[k] * x + b[k] * y + e[k];
TempY := c[k] * x + d[k] * y + f[k];
x := TempX;
y := TempY;
if ((Count >= MaxIterations) or (Count <> 0)) then
Canvas.Pixels[Round(x * MaxY / 11 + MaxX / 2),
Round(y * - MaxY / 11 + MaxY)] := clGreen;
Count := Count + 1;
end;
for i := 0 to 200 do
DoPaint;
end;
end.
Listing 13.13. The main unit for the program that calls the DLLs listed above.
unit Testem;
{ This is an example program showing how to create and call DLLs in Delphi.
This program will test FRACTDLL and APIDLL. FRACTDLL has Delphi forms
stored in it, APIDLL, creates windows by directly calling
the Windows API.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons, DLLUnit;
type
TBigNumber = class(TForm)
CreateChild: TBitBtn;
AboutDLL: TBitBtn;
Pictures: TBitBtn;
Fern: TBitBtn;
Squares: TBitBtn;
Start: TBitBtn;
RegPopup: TBitBtn;
CreatePopup: TBitBtn;
procedure StartClick(Sender: TObject);
procedure PicturesClick(Sender: TObject);
procedure AboutDLLClick(Sender: TObject);
procedure CreateChildClick(Sender: TObject);
procedure RegPopupClick(Sender: TObject);
procedure CreatePopupClick(Sender: TObject);
private
DLLWnd: HWnd;
end;
continues
implementation
{$R *.DFM}
end.
Listing 13.14. The DLL containing the import statements for the calls in the DLLs.
unit Dllunit;
interface
uses
Windows;
implementation
end.
Before launching this program, make sure that both the APIDLL.DLL and the
FRACTDLL.DLL are compiled. When you launch the main program, a simple form contain-
ing eight buttons appears. Each button, as shown in Figure 13.7, calls one or more of the func-
tions that reside in the DLLs listed above.
FIGURE 13.7.
The main form for the
MainDemo program has
eight buttons on it.
The StartChild button on the main form will call into the DLL and create a child window that
appears only within the scope of its parent, which in this case is the main form of the execut-
able. This is an interesting example of how the handles for a window work across the boundary
of a DLL. The “Child Without VCL” window created inside the DLL acts as if it is very much
a part of the main executable. Most users would never guess that it resides inside a DLL.
If you want to create multiple instances of this “Child Without VCL” window, you can press
the CreateChild button on the main form for the application. This function calls a CreateWindow
procedure in the DLL. The difference between pressing the StartChild button and the
CreateChild button is that the former calls both RegisterClass and CreateWindow, while the
latter calls only CreateWindow. Notice that the StartChild button becomes disabled after you
press it. This is because you only need to register a window once. After that you can create
multiple instances of the child window by calling CreateWindow multiple times. I create the
child window in the exact same place each time, so you’ll need to move the first instance of the
window if you want to see the secondary instances.
The RegPopup button registers and creates a pop-up window inside the DLL. The CreatePopup
button calls CreateWindow on the pop-up window, in case you want to see more than one in-
stance of the window. There is no practical limit on the number of pop-up windows you can
create. A pop-up window appears as a separate, free-floating window that is not attached to the
main form. Notice that child windows stay within their owner window, while pop-up win-
dows float free.
I discuss the difference between pop-up and child windows in more depth a bit later, but the
key point is that you use the ws_ChildWindow flag when calling CreateWindow on a child win-
dow:
ASimpleWindow := CreateWindow(
ChildName, ‘Child without VCL’,
ws_ChildWindow or ws_Caption or ws_ClipSiblings or
ws_visible or ws_ThickFrame or ws_SysMenu,
1, 75, 200, 100, ParentWindow, 0, HInstance, nil);
Notice that the child window has a parent window handle specified in the fourth to last pa-
rameter, while the pop-up window has 0 in this spot. The child needs a parent, while the pop-
up does not.
NOTE
Unfortunately, this book is not long enough to cover all the styles you can pass to a
window. The subject is covered in many other C reference books, however. By this
time, you should be able to read any C reference book on the Windows API with little
trouble. The code in such books is almost identical to the Windows API programs you
have seen here. Be sure to look for a straight Windows API book like Teach Yourself
Windows 95 Programming in 21 Days, published by Sams. Don’t get a book that
concentrates on OWL or MFC.
At any rate, the ws_OverLappedWindow style is actually a compound style defined like
this in WINDOWS.PAS:
WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or
WS_THICKFRAME or WS_MINIMIZEBOX or WS_MAXIMIZEBOX);
Here are three other styles of WINDOWS.PAS that might come in handy in the
context of this discussion:
WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW;
WS_POPUPWINDOW = (WS_POPUP or WS_BORDER or WS_SYSMENU);
WS_CHILDWINDOW = (WS_CHILD);
The AboutDLL button on the main form of the MainDemo application will pop up a stand-
alone dialog. I will comment on this dialog in more depth down below, but the basic code for
creating it is the same as that shown in the Window2 program discussed in Chapter 10, “Dia-
logs and Resources.” Note that you can pop up this same dialog by clicking the child and pop-
up windows mentioned above.
The last three buttons on the MainDemo window call standard VCL forms that make only
limited use of the Windows API. I won’t discuss them in any great depth in this book, in large
part because this chapter has already become a bit long.
Probably the most interesting of these three forms is the Fern form, which calls some nice fractal
code originally given to me by Borland employee and expert programmer Lar Mader. In gen-
eral, both the Squares form and the Fern form are interesting because they contain a good deal
of math. If you have mathematical code that depends on fast execution, Object Pascal is a good
language to work in, because it is both very fast and very tight.
Below I take a somewhat closer look at WINSTUFF.PAS, which is the module that contains
the core Windows API code of interest to readers of this chapter. Most of the code in
WINSTUFF.PAS may not make sense to you unless you have read the previous four chapters
of this book.
The APIDLL
This APIDLL contains seven routines that are grouped here into four basic types:
// Simple MessageBox routine
HandleButton
The simplest code in the DLL belongs to the HandleButton routine, which enables you to pass
in a window handle to be used by the MessageBox function:
procedure HandleButton(Window: HWnd);
begin
MessageBox(Window, ‘Hello’, ‘Sam’, MB_OK);
end;
This routine is not meant to be particularly useful because you can pass in 0 as the first param-
eter to MessageBox if you don’t have a handle, and because you can call MessageBox directly
from a program just as easily as you can wrap it in a DLL. However, I provide the routine
because it serves to remind you that you often need to pass in a handle to a parent window
when you are creating child windows in a DLL. It’s easy to do this, and the Window handle
will serve as a link between your application and the DLL. This becomes a crucial point when
you want to show a modal dialog.
The ShowAbout function allocates memory for a dialog, shows it modally, and then destroys it:
procedure ShowAbout(Window: Hwnd);
begin
DialogBox(HInstance, ‘AboutBox’, Window, @About);
end;
If you wanted this same code to compile under both Windows 3.1 and Win32, you would
have to write this:
I won’t discuss the actual Windows API calls shown here, but you’ll remember that they were
discussed in depth in Chapter 10. Notice that this code calls MakeProcInstance and
FreeProcInstance, which provide compatibility with 16-bit code.
There are two other pairs of calls in the WINSTUFF unit. The first pair launches a pop-up win-
dow that has no parent and that roves freely across the Windows desktop:
RegisterWinPopup
CreateWinPopup
The second pair of calls launches a child window that lives inside its parent and cannot escape
its confines:
RegisterChild
CreateChild
Child windows never get the focus. Although it’s not the case in this particular example, they
are usually shown without a caption or a thick-framed border. As a result, they usually appear
in one place in a larger window, just like a button or an edit control. In particular, they usually
don’t have a caption. I don’t treat them that way in this example, since I want you to be able to
move them around on their parent. (The Music program, presented in the chapters on data-
bases, shows how to turn a standard Delphi form into a child window.)
I have two pairs of routines for each window because it is necessary to first register the window
before you create and launch it. The RegisterWinPopup and RegisterChild routines are both
used to register windows. These routines should be called only once for each window. The
CreateChild and CreateWinPopup routines create and launch windows, and they can be called
multiple times during a session.
The register routines fill out a TWndClass structure and then call RegisterWindow:
function RegisterWinPopup: Boolean;
var
WndClass: TWndClass;
begin
if HPrevInst = 0 then begin
WndClass.Style := 0;
WndClass.lpfnWndProc := @PopupWinProc;
WndClass.cbClsExtra := 0;
WndClass.cbWndExtra := 0;
WndClass.hInstance := HInstance;
WndClass.hIcon := LoadIcon(0, idi_Application);
WndClass.hCursor := LoadCursor(0, idc_Arrow);
WndClass.hbrBackground := GetStockObject(white_Brush);
WndClass.lpszMenuName := PopupName;
WndClass.lpszClassName := PopupName;
Result := CreateWinPopUp;
end;
Notice that the call to RegisterClass is checked to see if it returns zero. RegisterClass returns
an atom that uniquely identifies the class on success, or zero on failure.
The pop-up window in WINSTUFF.PAS uses the PopupWinProc routine as its window proce-
dure, whereas the child window uses ChildProc. You don’t have to do anything special to call
CreateWindow or RegisterClass in a DLL. The principles are the same as in a normal execut-
able such as the Window1 program you saw earlier.
In the CreateChild and CreateWinPopup methods, you find calls to the complex CreateWindow
function, which, as you know, takes a fairly baffling array of parameters:
function CreateChild(ParentWindow: Hwnd): Hwnd;
var
ASimpleWindow: HWnd;
begin
ASimpleWindow := CreateWindow(
ChildName, ‘Child without VCL’,
ws_ChildWindow or ws_Caption or ws_ClipSiblings or
ws_visible or ws_ThickFrame or ws_SysMenu,
1, 75, 200, 100, ParentWindow, 0, HInstance, nil);
Result := ASimpleWindow;
end;
Not to beat a dead horse, but please notice the fourth-to-last parameter in the CreateWindow
call from the CreateChild routine. It’s the handle of the parent window. Notice that this same
parameter is zeroed out in the CreateWinPopup method. The parent window parameter is an
important part of what distinguishes a child window from a pop-up window. Another key fac-
tor is the presence of the ws_ChildWindow style. You should almost always use the ws_ClipSiblings
style to avoid problems when painting the child windows.
NOTE
The previous paragraph is perhaps a bit misleading in that you can specify the parent
of a pop-up window. However, it is not absolutely necessary.
The CreateChild routine calls ShowWindow only if the call to CreateWindow succeeds. If
CreateWindow fails, the function automatically returns zero, thereby signifying a failure.
Once again, this section of the book strives to illustrate how to use the Windows API, and how
to create very small binary files that do the same thing as standard Delphi RAD programs. The
Windows API lies at the heart of the operating system most of us use day in and day out. It is
also the foundation on which Delphi is built. Understand the API, and everything else is pos-
sible. If you don’t understand the Windows API, you will keep running into brick walls.
var
DrawSqr: TDrawSqr;
implementation
{$R *.DFM}
procedure ShowSquares;
begin
DrawSqr := TDrawSqr.Create(Application);
DrawSqr.ShowModal;
DrawSqr.Free;
end;
CDS[0].X := -100;
CDS[0].Y := -100;
CDS[1].X := 100;
CDS[1].Y := -100;
CDS[2].X := 100;
CDS[2].Y := 100;
CDS[3].X := -100;
CDS[3].Y := 100;
CDS[4].X := -100;
CDS[4].Y := -100;
for i := 0 to 4 do begin
x1 := CDS[i].X;
y1 := CDS[i].Y;
xt := Round(Scale * (x1 * cos(Theta * PI / 180) + y1 * sin(Theta * PI/180)));
yt := Round(Scale * (y1 * cos(Theta * PI / 180) - x1 * sin(Theta * PI/180)));
if (i = 0) then
MoveToEx(PaintDC, xt, yt, nil)
else begin
if Scale = 1.0 then
OldPen := SelectObject(PaintDC, Pens[0])
else
OldPen := SelectObject(PaintDC, Pens[i]);
LineTo(PaintDC, xt, yt);
SelectObject(PaintDC, OldPen);
end;
end;
for I := 0 to 4 do
DeleteObject(Pens[i]);
end;
end.
CreatePen takes a constant in the first parameter that specifies the kind of pen you want to
create. The following excerpt from the online help shows the kinds of pens you can create:
PS_SOLID Pen is solid.
PS_DASH Pen is dashed. This style is valid only when the pen width is 1 or
less in device units.
PS_DOT Pen is dotted. This style is valid only when the pen width is 1 or
less in device units.
PS_DASHDOT Pen has alternating dashes and dots. This style is valid only when
the pen width is 1 or less in device units.
PS_DASHDOTDOT Pen has alternating dashes and double dots. This style is valid
only when the pen width is 1 or less in device units.
PS_NULL Pen is invisible.
PS_INSIDEFRAME Pen is solid. When this pen is used in any graphics device
interface (GDI) drawing function that takes a bounding rect-
angle, the dimensions of the figure are shrunk so that it fits
entirely in the bounding rectangle, taking into account the width
of the pen. This applies only to geometric pens.
You use pens to draw lines and curves. For instance, the outside line in a rectangle or ellipse is
painted in the current pen.
The second parameter passed to CreatePen designates the width, in pixels, of the pen you want
to create. The third parameter designates the color of the pen you want to create. You can use
the RGB function to return the 32-bit value designated in this parameter. The RGB function is
discussed in several other places in this book, but remember that the first number passed to the
function designates how much red will be in the color, the second parameter designates the
amount of green, and the third the amount of blue. The numbers range from 0 to 255.
Once you have a pen, you can select it into the current device context:
OldPen := SelectObject(PaintDC, Pens[0])
Notice that the old pen that resided in the device context needs to be saved in a temporary
variable. When you are finished with the pen, reselect the original pen into the device context:
SelectObject(PaintDC, OldPen);
The other important method to notice in this example is the MoveToEx function. It replaces the
MoveTo function commonly used in Windows 3.1. MoveToEx is portable between Windows 3.1
and Win32, but MoveTo works only in Windows 3.1. After calling MoveToEx, you can call the
LineTo function to draw a line from the point specified in MoveToEx to the point specified in
LineTo. MoveToEx will move from one point to another without drawing a line, while LineTo
moves the point from one place to another and draws a line in the process. Both MoveToEx and
LineTo are GDI functions that require a device context. The line drawn by these functions will
have the color and size of the current pen. In other words, if you want to change the size, type,
or color of the line drawn with LineTo, you should create a new pen and select it into the device
context.
Summary
In this chapter you have looked at some DLLs and seen how they can be handled by a Delphi
program. Most of these DLLs make heavy use of Windows API functions. In fact, you have
seen DLLs that are made entirely out of Windows API calls, as well as DLLs that are standard
Delphi RAD productions but that make heavy use of the Windows API.
The key points from this chapter are the techniques used for creating DLLs, and the techniques
for calling functions in a DLL. In particular, you saw two techniques for importing functions
from a DLL. The first technique loaded the DLL at the launch of the program, which is called
load time dynamic linking. The second technique allowed you to control more explicitly the
moment the DLL was loaded into memory by calling LoadLibrary, GetProcAddress, and
FreeLibrary.
If you call LoadLibrary and FreeLibrary, you are performing what is known as runtime dy-
namic linking. If you call into a DLL without using these calls, you are using load time dy-
namic linking.
When you call into the Windows API functions, you are always using load time dynamic link-
ing. In particular, when you import a function using external and then use the name or index
directive, you are always using load time dynamic linking, which means the DLL will be loaded
at the same time as your executable and won’t be freed until your executable is closed down.
This is the technique used most often in all Object Pascal programs, and indeed it is the stan-
dard in most Windows programs regardless of the language used to create them.
To use runtime dynamic linking, you have to explicitly call LoadLibrary, GetProcAddress, and
FreeLibrary. It’s more work to do things this way, but you can reap advantages in terms of
speed and memory usage.
Windows Controls
and Callbacks 14
This chapter presents a relatively complex program written entirely in the Windows API. The
program is called the FontsExp program because it experiments with fonts. FontsExp makes
heavy use of standard Windows controls to display all the fonts on the current system. One of
the main points of the program is that it shows how to use Windows controls without using
the VCL.
The examination of FontsExp is divided into two sections. The first section focuses on the various
Window controls introduced in the FontsExp program. In particular, it includes an examination
of the following controls:
■ Static controls, used primarily to display text that the user can’t edit.
■ List boxes, used to display lists of one-line strings that can be scrolled by the user.
■ Edit controls, which enable the user to edit a string.
■ Check boxes, which are like the tiny boxes on a ballot or school quiz—if an X is in the
box, the item is selected. Otherwise, it’s either gray or blank.
The second half of this chapter presents an explanation of how to communicate with controls
and how to use a callback function to retrieve the fonts available on the system. In particular,
those sections show:
■ How to use SendMessage and PostMessage
■ How to communicate with list boxes and check boxes
■ How to create user-defined messages
■ How to use the EnumFontFamilies callback
Controls form a key part of the Windows operating environment; as a result, this chapter explores
them in considerable depth. However, I obviously don’t want you to interpret this chapter as
advocating the use of Windows API code rather than Delphi RAD tools. The reasons for
including this chapter are:
■ The Delphi TListBox, TComboBox, TButton, TRadioButton, and other controls are,
of course, constructed out of calls to the Windows API. After reading this chapter, you
will have the basic knowledge you need to create your own TButton, TRadioButton,
and similar controls, or to modify or extend the existing controls.
■ Sometimes you may want to optimize your code by making direct Windows API calls.
You also might want to get at functionality that is not available in the standard Delphi
controls.
■ When you construct your own components, you usually need to understand the
Windows API. This chapter reveals a lot of key details about the inner workings of
Windows. You can use that knowledge when you create your own custom controls.
■ You can communicate with Delphi controls using the techniques shown here. You
don’t have to use properties to talk to Delphi components. The components that are
wrappers around standard Windows controls such as TEdit, TButton, and the like
will respond to direct calls from the Windows API.
Of course, an in-depth knowledge of Windows pays off in more ways than just those listed
here. However, these points should help to put the code you are about to see into context.
FIGURE 14.1.
The FontsExp program
enables you to iterate
through all the fonts on the
system.
The visual front end of the FontsExp program bears a slight resemblance to the view you see
when you pop up a Fonts common dialog (TFontDialog). From a programmer’s perspective,
however, FontsExp is a very different type of program. It takes you inside the world of Windows
fonts, letting you control how, where, and with what degree of detail fonts are presented to the
user.
You can build on the knowledge presented in the next few pages to expand the FontsExp program
into a world-class tool that many Windows users would love to have at their disposal. As always
in the world of programming, there are plenty of opportunities. Start with a look at the code
shown in Listings 14.1 through 14.4.
Listing 14.1. The FontsExp program demonstrates techniques for using fonts and
controls.
program FontsExp;
//////////////////////////////////////
// Program Name: FONTSEXP.DPR
// Copyright (c) 1996 by Charlie Calvert
// Description: FontsExp Windows program
//////////////////////////////////////
uses
Windows,
Messages,
FontStr in ‘FontStr.pas’,
SysUtils;
{$I FONTSEXP.INC}
{$R FONTEXP.RES}
const
AppName = ‘FontsExp’;
WM_STARTFONTS = (WM_USER + 0);
WM_NEWFONT = (WM_USER + 1);
type
PEnumStruct = ^TEnumStruct;
TEnumStruct = record
Count: Integer;
hWindow: HWnd;
end;
var
TextMetrics: TTextMetric;
TextFace: string;
hFontList, hNumFonts: HWnd;
hFontName, hAlphaEdit: HWnd;
ButtonWindows: array[0..2] of HWnd;
TheFont: HFont;
//////////////////////////////////////
// FontCallback
//////////////////////////////////////
function FontCallBack(lpnlf: PEnumLogFont;
lpntm: PNewTextMetric;
FontType: Integer;
Data: PEnumStruct): Integer; stdcall;
var
S: string;
begin
S := (lpnlf.elfLogFont.lfFaceName);
SendMessage(Data.hWindow, LB_ADDSTRING, 0, LongInt(S));
Inc(Data.Count, 1);
Result := 1;
end;
//////////////////////////////////////
// DescribeFontCallback
//////////////////////////////////////
function DescribeFontCallBack(lpnlf: PEnumLogFont;
lpntm: PNewTextMetric;
FontType: Integer;
Data: PEnumStruct): Integer; stdcall;
begin
SendMessage(Data.hWindow, WM_NEWFONT, 0, LPARAM(@lpnlf.elfLogFont));
Result := 1;
end;
//////////////////////////////////////
// HandleMetrics
//////////////////////////////////////
procedure HandleMetrics(TextMetrics: TTextMetric);
begin
if ((TextMetrics.tmPitchAndFamily and TMPF_TRUETYPE) = TMPF_TRUETYPE) then
SendMessage(ButtonWindows[0], BM_SETCHECK, 1, 0)
else
SendMessage(ButtonWindows[0], BM_SETCHECK, 0, 0);
continues
//////////////////////////////////////
// Handle WM_CREATE
//////////////////////////////////////
procedure FontsExp_OnCreate(Window: HWnd; var Msg: TWMCreate);
const
Titles: array[0..2] of string = ( ‘TrueType’ , ‘Heavy’, ‘Italic’) ;
var
i: Integer;
begin
SetLength(TextFace, 250);
for i := 0 to 2 do
ButtonWindows[i] := CreateWindow(‘button’, PChar(Titles[i]),
WS_CHILD or WS_VISIBLE or BS_CHECKBOX,
260 + (i * 125), 180, 100, 35, Window,
HMENU(-1), hInstance, nil);
TheFont := 0;
end;
//////////////////////////////////////
// Handle WM_DESTROY
//////////////////////////////////////
procedure wmDestroy(Window: HWnd; var Msg: TWMDestroy);
begin
if (TheFont <> 0) then
DeleteObject(TheFont);
PostQuitMessage(0);
end;
//////////////////////////////////////
// ShowTheFont
//////////////////////////////////////
procedure ShowTheFont(hWindow: HWnd);
var
Buffer: string;
EnumStruct: TEnumStruct;
Index: LResult;
DC: HDC;
Alpha: string;
SaveIt: hFont;
begin
SaveIt := 0;
SetLength(Buffer, 250);
Alpha := ‘1234567890abcdefghijklmnopqrstuvwxyz’ +
‘ABCDEFGHIJKLMNOPQRSTUVWXYZ’;
DC := GetDC(hWindow);
ReleaseDC(hWindow, DC);
//////////////////////////////////////
// Handle WM_COMMAND
//////////////////////////////////////
continues
CM_INFO: ShowFontString(hWindow);
end;
end;
SetFocus(hFontList);
SendMessage(hFontList, LB_SETCURSEL, 0, 0);
ShowTheFont(hWindow);
end;
case AMessage of
wm_Create: FontsExp_OnCreate(hWindow, TWMCreate(AMsg));
Result := AMsg.Result
end;
Result := hWindow;
end;
var
AMessage: TMsg;
hWindow: HWnd;
begin
if not WinRegister then begin
MessageBox(0, ‘Register failed’, nil, mb_Ok);
Exit;
end;
hWindow := WinCreate;
continues
Listing 14.2. The FontStr Module provides code for examining the features of a
font.
unit FontStr;
//////////////////////////////////////
// Program Name: FONTSTR.DPR
// Copyright (c) 1996 by Charlie Calvert
// Description: Create description of a font
//////////////////////////////////////
interface
uses
Windows, SysUtils;
var
TextMetrics: TTextmetric;
TheFaceName: string;
Result := S;
end;
GetType(szType);
GetFamily(szFamily);
GetCharSet(szCharSet);
S := Format(‘Font: %s ‘ + #13 +
‘Height: %d ‘ + #13 +
‘Ascent: %d ‘ + #13 +
‘Descent: %d ‘ + #13 +
‘AveCharW: %d ‘ + #13 + ‘MaxCharW: %d ‘ + #13 +
‘Weight: %d ‘ + #13 + ‘Italic: %d ‘ + #13 +
‘Underlined: %d ‘ + #13 + ‘%s ‘ + #13 +
‘%s ‘ + #13 + ‘%s ‘,
[TheFaceName,
continues
Result := S;
end;
end.
const
id_ListBox = $FF;
cm_Info = id_ListBox + 1;
const
id_ListBox = $FF;
cm_Info = id_ListBox + 1;
The FontsExp program creates a window and populates it with static controls, edit controls,
check boxes, and list boxes. These controls enable the user to display a series of fonts. As shown
in Figure 14.2, the FontInfo menu choice lets the user view information about the current font.
FIGURE 14.2.
The FontsExp program,
displaying the system font
in its list box and the Times
Roman font in an edit and
static control.
Now that you’ve seen the code, it’s time to rev up your engines and pay special attention to the
rest of this chapter. Most Windows programmers work with controls on a daily basis. Understand
them thoroughly, and you’ll be well on your way to creating powerful, robust programs that
users love.
Static Controls
Users never interact with static controls, like the TLabel control. Such controls are meant only
to display text. Static controls are the easiest controls to utilize. Here are the minimum steps
involved:
■ Call CreateWindow.
■ Fill in the first field of CreateWindow, called lpszClassname, with the string static.
■ Fill in the third field, called dwStyle, with the WS_CHILD and WS_VISIBLE styles and any
others you might need to add, such as SS_LEFT or SS_SIMPLE.
■ Specify the control’s dimensions.
■ Fill in the program’s hwndParent field with the HWnd from the main window.
■ Give the control a unique ID by filling in the hMenu field with a predefined constant.
(Or, on occasion, you might calculate the number in a loop or some other code
fragment. The point is that you should be able to predict what the ID will be, so that
you can reference it from other parts of your code.)
■ Either specify the caption at creation, or else use SetWindowText to fill the control with
a string.
As you’ll see, there are a few other factors to keep in mind, but these steps form the core
procedures used when creating static controls. In fact, you can use these steps as a template for
creating all window controls. In other words, all controls are brought kicking and screaming
into the world by calls to CreateWindow. Figure 14.3 shows the FontsExp program.
FIGURE 14.3.
A static control displays the
number of fonts and a
read-only edit control
displays the font name.
Following is the call that creates the static control in the FontsExp program:
hNumFonts := CreateWindow(‘static’, nil,
WS_CHILD or WS_VISIBLE or WS_BORDER,
10, 10, 200, 20, Window, HMENU(-1),
hInstance, nil);
As you can see, this code looks like the routines used to create a main window, except that
there’s no need to first register a class. The reason for this omission is simply that the static
class has been preregistered for you. That’s what controls are all about. They are preregistered
classes.
NOTE
The fact that a static control is just a preregistered window is such an important point
that I want to take a moment to emphasize it.
In this book, you have learned how to create a number of different windows that
behave in a particular way. You did this by filling out their TWndClass structure with
values of your own choosing, and then writing message response functions called from
a window procedure. In the process, you created several classes of potentially reusable
windows that behave in various, predefined ways.
This is exactly what has already been done for you with the static, listbox, combobox,
button, and scrollbar classes. Microsoft has already registered these classes and defined
how they will respond to certain messages. The same is true of the Windows 95 and
Windows NT specific classes such as toolbars, rich edits, progress bars, property sheets,
and up/down controls.
You have been calling CreateWindow regularly in the last few chapters. When creating child
window controls, however, you need to take special note of a few key points. The next few
paragraphs outline those points for you while showing you how to create a static control.
The dwStyle field of CreateWindow ORs together WS_CHILD, WS_VISIBLE, and WS_BORDER. The first
and last styles simply tell Windows that this is a child window with a border. The WS_VISIBLE
style is a nifty little flag that requests Windows to call the ShowWindow function. In past chapters,
most calls to CreateWindow have been followed by calls to ShowWindow and UpdateWindow:
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
If you use the WS_VISIBLE style, this isn’t necessary, because Windows does the dirty work for
you.
The next set of fields you need to be concerned with are those that define the size of the border
surrounding the static control. When CreateWindow is being called for a main window, these
fields are usually filled with CW_USEDEFAULT constants. With static controls, however, you should
explicitly fill in these fields with the actual dimensions you want associated with your control.
The next step is to fill out the hwndParent parameter to CreateWindow. Controls are child windows,
and children need parents. In this case, of course, the parent is the main window.
Another field you need to be aware of is the ninth, called hMenu. In this field, you normally
designate the number to be associated with a window. Because objects such as static controls
and child windows don’t have menus, this field can be available as a place to specify an ID for
a window.
The ID number you choose can prove important later when you set up a line of communication
between a control and its parent window. As a result, if you decide to specify a value in this
field, you should pick a unique ID number for each control. This is especially true for controls
that are embedded in dialogs. In those cases, the ID number is the only link between a dialog
and a control.
Because programmers develop a lot of these ID constants in their programs, they’ve fallen into
the habit of using constants to act as mnemonics:
const
id_ListBox = $FF;
cm_Info = id_ListBox + 1;
It is not strictly necessary to typecast these constants before passing them on to Windows, but
doing so is a nicety that helps specify exactly what your code means.
NOTE
The FontsExp program usually communicates with a control through its HWnd, which is
carefully saved after every call to CreateWindow. This technique is easy to use when
working with controls embedded in Windows. Within dialogs, however, it’s simpler to
use an ID and the GetDlgItem Windows API function.
To make static controls useful objects, all you need to do is place a bit of text in them with the
SetWindowText call:
SetWindowText(hNumFonts, PChar(S));
hNumFonts is the handle to a static or edit control, and S is the string that appears in the control’s
window.
Static controls are the simplest controls you’re likely to use in a Windows program. Of course,
nothing in the Windows API is ever totally devoid of syntactical verbosity. For instance, there
are a number of changes you can run on static texts. You can change their border and color or
use them to seat icons or bitmaps.
Following are some of the valid styles that can be ORed into the dwStyle field of a static control:
SS_BLACKRECT SS_BLACKFRAME
SS_GRAYRECT SS_GRAYFRAME
SS_WHITERECT SS_WHITEFRAME
SS_LEFT SS_RIGHT
SS_CENTER SS_ICON
Feel free to experiment with these styles. Take special note, however, of the simple examples
I’ve included in the FontsExp program. They can remain as an image of what lies at the heart
of every control: an ordinary window with something painted inside it.
NOTE
In this sample program, a great deal goes on in the WM_CREATE message function
handler. It’s the place in which all the controls used in the program are created. This,
of course, only makes sense. In fact, the idea of creating controls in response to a
WM_CREATE message really defines what the message is all about. A WM_CREATE message is
sent to a window specifically to say: “Hey, wake up there! It’s time for you to create all
your child windows and to do any other initialization you think necessary!”
List Boxes
Of particular interest in the context of the current chapter is the code for creating list box
controls:
hFontList := CreateWindow(‘listbox’, nil,
WS_CHILD or WS_VISIBLE or LBS_STANDARD,
9, 30, 201, 180, Window, HMENU(ID_LISTBOX),
hInstance, nil);
List boxes, of course, are rectangular windows that present the user with an array of items to
select. A list box and a static control are shown in Figure 14.4.
In the list box, the most important field is the third, in which you can define the style of the
class:
WS_CHILD or WS_VISIBLE or LBS_STANDARD
By this time, I’m sure it won’t come as a surprise to you to learn that the LBS_STANDARD flag is
part of another lengthy array of options. This is the Windows way of doing things. Good
FIGURE 14.4.
A static control with a
string in it. Immediately
beneath it is a list box filled
with font names.
The online help has already provided you with several copies of this list, so I’ll show you only
the ones programmers are likely to use on a regular basis:
LBS_MULTIPLESEL User can select more than one item at a time.
LBS_EXTENDEDSEL Enables the user to select multiple items by using the
Shift key and the mouse or special key combinations.
LBS_NOTIFY When this flag is turned off, the parent window doesn’t
know whether the user has clicked or double-clicked a
string.
LBS_STANDARD The standard list box with a border and with LBS_NOTIFY
turned on.
LBS_OWNERDRAW The style you need if you want to display bitmaps or
other nonstandard items in a list box.
LBS_USETABSTOPS Used for simulating multiple columns in a single list
box.
LBS_WANTKEYBOARDINPUT Enables the programmer to pick up on keyboard input
when the list box has the focus.
Of these items, the two most important are LBS_STANDARD and LBS_MULTIPLESEL. Multiple
selection list boxes work like the windows in the File Manager: they let you select multiple
items at one time. Of course, they don’t have all the capabilities of the windows in the File
Manager, but they do enable multiple selections.
Even without the Delphi RAD tools, creating a list box is a trivial task that you can accomplish
in just a few moments. Simply cut and paste the call from another program, or copy the
CreateWindow call from your main window. There’s really nothing to it. This is true whether
you are inside a RAD program, or inside straight Windows API code. Once you’ve got one
sample to work from, it’s easy to create all the other list boxes you ever need to create.
The subject of communicating with a list box can become fairly involved. For now, a few simple
examples should suffice. If you want to add a string to a list box, you can do so by sending it an
LB_ADDSTRING message:
MyString = ‘Sambo’;
SendMessage(hListBox, LB_ADDSTRING, 0, LPARAM(MyString));
The code shown here typecasts the last parameter to SendMessage as an LPARAM. LPARAM is a type
declared in WINDOWS.PAS that resolves into a LongInt:
type
WPARAM = UINT;
LPARAM = Longint;
LRESULT = Longint;
This material is being presented here mostly as a reference tool and brief introduction to list
boxes. To really get a deep insight into this material, you should turn to the Windows API
documentation, which you now have enough knowledge to read, whether the examples in those
docs are written using Pascal or C.
You should also be getting a sense that there is nothing you can’t do in Object Pascal. It’s all
there—all the techniques for accessing the Windows API are at your fingertips whenever you
need them.
Check Boxes
Following are the calls that create all three of the check boxes used in this program:
const
Titles: array[0..2] of string = ( ‘TrueType’ , ‘Heavy’, ‘Italic’) ;
var
i: Integer;
begin
...
for i := 0 to 2 do
ButtonWindows[i] := CreateWindow(‘button’, PChar(Titles[i]),
WS_CHILD or WS_VISIBLE or BS_CHECKBOX,
260 + (i * 125), 180, 100, 35, Window,
HMENU(-1), hInstance, nil);
...
end;
To create a check box, you should specify the “button” class name and use the BS_CHECKBOX
style. The preceding example also shows how to use constant arrays and FOR loops to create a
series of three controls. This is a technique you’ll probably use on many occasions if you work
regularly with the Windows API.
NOTE
Check boxes are little square bordered windows with a name attached to them, as shown
in Figure 14.4. If the bordered window has an X in it, that item is selected; otherwise,
it’s not selected. In most cases, users click a check box with a mouse to select or
deselect an option. FontsExp, however, sends messages to select or deselect a check
box, and the user never interacts with the box directly.
Figure 14.5 shows check boxes and the list box of the FontsExp program. If you use the arrow
keys to move the highlight bar up and down the list, the check boxes blink on and off like
Christmas lights. Well, okay, like black-and-white Christmas lights.
I’m sure you’ve already guessed that the BS_CHECKBOX style is only one of many different pos-
sible styles that can be associated with a button control. This versatile class is one that Win-
dows programmers rely on time and time again. Take a careful look at the following button
styles:
BS_CHECKBOX Creates a check box.
BS_DEFPUSHBUTTON Has a heavy black border and is chosen by default when the
user presses the Enter key in a dialog.
FIGURE 14.5.
A selected check box has a
check mark in it, whereas
unselected check boxes are
blank.
This list shows that buttons are very versatile tools. At first, you might not even make the con-
nections among standard OK buttons, check boxes, and radio buttons. However, all these tools
belong to the same class, and you should learn to think of them as being closely related.
Communicating with check boxes can be a fairly delicate matter. For now, I’ll just say that you
can use the BM_SETCHECK messages to perform the most important aspects of the job. The key
point to remember is that passing 1 in the WPARAM field of SendMessage sets the check, whereas
passing 0 clears the check mark:
SendMessage(ButtonWindows[0], BM_SETCHECK, 1, 0);
If you want to query a check box as to the state of its button, use BM_GETCHECK:
var
IsCheck: Integer;
begin
IsCheck := SendMessage(ButtonWindows[0], BM_GETCHECK, 0, 0);
...
Buttons with the BS_3STATE or BS_AUTO3STATE have a third grayed state selected by sending the
value 2 to them.
An in-depth discussion of this topic is presented later in the chapter. There you’ll see how
FontsExp uses messages to control and query check boxes.
Edit Controls
The FontsExp program also makes use of two edit controls, one of which is shown in Figure
14.6 and the other in Figure 14.3. Edit controls are used for getting input from a user through
the keyboard, as in a text editor. In fact, edit controls can be used to create small text editors
that can handle a few pages of text at a time.
You can also create read-only edit controls by giving them the ES_READONLY style. The ES_READONLY
style makes an edit control behave as if it were a souped-up static control. Compared to static
controls, you have better control over fonts and highlighting in an edit control even if it has
the ES_READONLY style.
The following code creates a read-only edit control:
hFontName := CreateWindow(‘edit’, nil, WS_CHILD or ES_LEFT or
WS_VISIBLE or WS_BORDER or ES_READONLY,
260, 10, 350, 70, Window, HMENU(-1),
hInstance, nil);
FIGURE 14.6.
An edit control, with its
scrollbar, displays the ever-
useful Wingdings font.
In this particular program, you might never type in either edit control; you might prefer to
simply scroll the text back and forth. However, it’s easy to see how a user might want to look
at a particular combination of letters, so I’ve designed the program to respond to that
contingency. Notice, however, that this is the only part of the program that requires any typing
skills.
The call to initialize the second edit control uses six different styles ORd together to form the
third parameter:
hAlphaEdit := CreateWindow(‘edit’, nil, WS_CHILD or
WS_VISIBLE or WS_BORDER or WS_HSCROLL or
ES_LEFT or ES_AUTOHSCROLL or ES_MULTILINE,
260, 90, 350, 70, Window, HMENU(-1),
hInstance, nil);
NOTE
You can add the WS_BORDER style if you want the control to be outlined in black.
Scrollbars are handled automatically by Windows. You can add this feature to your
edit controls simply by using the WS_HSCROLL style.
Don’t confuse this style with either the ES_AUTOHSCROLL or ES_AUTOVSCROLL styles. These
two styles let the user scroll text into view if it is hidden behind the right or bottom
edge of a control. They seem to imply that this latter process involves using scrollbars,
but that is not actually the case.
I’m sure the astute reader has already prepared him or herself for another long list of identifiers.
So, without further delay, here are the edit control styles:
ES_AUTOHSCROLL Automatically scrolls horizontally.
ES_AUTOVSCROLL Automatically scrolls vertically.
ES_CENTER Centers the text.
ES_LEFT Aligns text on the left margin.
ES_LOWERCASE Converts text to lowercase.
ES_MULTILINE Uses multiple lines.
ES_NOHIDESEL Forces Windows to keep the text highlighted, even if you set
the focus to another control. (If you highlight a text fragment,
Windows normally will not preserve the selection after the
control loses focus.)
ES_OEMCONVERT Helps preserve characters outside the range of normal letters
and numbers.
ES_PASSWORD Helps prevent other users from seeing what is being typed into
an edit control.
ES_READONLY Uses the edit control only for viewing text.
ES_RIGHT Aligns text on the right margin.
ES_UPPERCASE Sets all input to uppercase.
ES_WANTRETURN Treats carriage returns normally, rather than sending them on
to the default button.
This list is invaluable. Countless Windows programmers have spent hours trapping WM_KEYDOWN
messages to handle carriage returns or to create edit controls appropriate for handling pass-
words. Don’t become one of these poor, overworked programmers. Instead, take a careful look
at these styles, so you don’t have to reinvent the proverbial wheel.
Check the online help for additional styles. You should get used to popping up the Win32
help on a moment’s notice to see what’s available.
NOTE
Don’t bother memorizing all the styles shown in this chapter. Instead, just absorb the
fact that every control has a set of styles that can be used to fine-tune, or even radically
change, its appearance and behavior. Another helpful point is that the styles associated
with each of the controls begin with a particular set of letters. For instance, edit-control
styles all begin with ES. To help you understand how this works, here is a list of letters
associated with each of the major control types:
Button BS
Static SS
Communicating with edit controls is a fairly simple process. To insert text into an edit control,
call SetWindowText, just as you do with a static control:
SetWindowText(hAlphaEdit, PChar(S));
With GetWindowText, the first parameter is the HWnd of the edit control; the second is a buffer to
hold the string displayed in the control; the third is the maximum number of bytes the string
can hold. Because you are passing in a string, you need to allocate memory for it, as Windows
won’t know how to do this. At the time of this writing, Length returns the amount of memory
allocated for a string, not its length. You can use the following code to return the true length of
a long string:
Len := StrLen(PChar(S));
In the later sections of this chapter, you’ll see that the SendMessage function provides an
alternative to this technique. For instance, the following call passes the string Alpha to an edit
control:
SendMessage(hAlphaEdit, WM_SETTEXT, 0, LPARAM(Alpha));
Besides WM_SETTEXT, edit controls also respond to a series of messages that enable you to di-
rectly manipulate their text. Most of these messages involve relatively advanced Windows pro-
gramming issues, but you should at least be aware of their existence. Here is a small sample:
EM_CANUNDO Checks whether an operation can be undone.
EM_GETMODIFY Checks whether contents have changed.
begin
...
SendMessage(MyEdit.Handle, WM_SETTEXT, 0, LPARAM(Alpha));
...
end;
When you are reading this chapter, try to pay particular attention to the material on the debugger.
It’s almost impossible for most people to do any serious programming without having an in-
depth knowledge of the debugger.
NOTE
From a purely conceptual point of view, the second half of this chapter covers fairly complex
material. The theme for the day is communication—specifically the way Windows
communicates with messages and callbacks. In an attempt to make this material as
comprehensible as possible, I have arranged it around the life history of a single event. In par-
ticular, the agenda is as follows:
■ Learn about a user-defined message called WM_STARTFONTS.
■ Trace WM_STARTFONTS from the moment of its conception until it sets off two calls to
EnumFontFamilies, thereby causing messages to be sent that fill up the list box and
other visual elements of the FontsExp program.
Each step on the agenda is explained methodically, starting from point one and working to the
end. The narrative flow is provided by the life history of WM_STARTFONTS. In other words, the
narrative starts by describing the declaration of WM_STARTFONTS, then follows through by
describing how the message is sent and processed. My theory is that if you watch a message
from the moment it’s defined, until it’s finally delivered and processed, you’ll get a complete
overview of the subject—without any major gaps or omissions.
To understand how PostMessage works, it’s best to start out by defining messages from a new,
and hopefully elucidating, perspective. WM_STARTFONTS is a user-defined message sent to tell the
main window that it’s time to fill up the list box and other controls with the names and
descriptions of all the fonts on the system. There are two differences between user-defined
messages and other messages:
■ User-defined messages are declared by the programmer, whereas normal Windows
messages are declared in WINDOWS.PAS, or in other units that come with the
system such as WINSOCK.PAS.
■ User-defined messages normally have only local scope. This means that they can be
defined for one class only.
More specifically, user-defined messages are calculated in terms of a constant called WM_USER,
which is defined in WINDOWS.PAS:
{ NOTE: All Message Numbers below $0400 are RESERVED.
Private Window Messages Start Here: }
WM_USER = $400;
As you can see, the comment in WINDOWS.H specifies that messages below 1024 (0X400)
are reserved for internal use by Windows.
The WM_STARTFONTS message is defined in FONTSEXP.DPR:
WM_STARTFONTS = (WM_USER + 0);
The preceding line of code simply states that WM_STARTFONTS should be assigned a number that
will enable Windows to recognize it as a message of local import. By definition, it is intended
specifically for the FontsExp class. To define additional messages, simply add 1 to the value of
WM_STARTFONTS, and so on:
It is sometimes possible to have two messages that have the same value. This is not necessarily
a problem. All messages are designed to be sent to a particular window. The window in ques-
tion is designated by the HWnd in the first parameter to PostMessage or SendMessage:
var
MainWindow, hCheckBox; // Handles to windows
IsCheck: Integer;
...
// Code to initialize hMainWindow and hCheckbox.
...
PostMessage(hMainWindow, WM_STARTFONTS, 0, 0);
IsCheck := SendMessage(hCheckBox, BM_GETCHECK, 0, 0);
...
Suppose (just hypothetically) that in this example, WM_STARTFONTS and BM_GETCHECK had the same
value. This call states explicitly that WM_STARTFONTS is being sent to the program’s main window.
If WM_STARTFONTS were accidentally sent to a check box, Windows would cheerfully ignore all
the fonts on your system and return the state of the check box. The point here is that all will be
on the up and up as long as you send the message to the correct window! The name of a message,
however, is meaningless. What’s important is its underlying integer-based value.
There are times when you need to create a message that is sent across the desktop to more than
one application. To do so, call RegisterWindowMessage. This call returns a unique message,
defined at runtime, to be used by your applications.
NOTE
Use WM_USER as an offset for messages that are going to be sent to only one class.
Don’t try to send these messages between applications. Use the RegisterWindowMessage
function to assign numbers to messages that will be sent between applications. The
numbers returned from RegisterWindowMessage are in the following range: $C000
through $FFFF. Refer to the online help for more information on this process.
I should add, however, that in Delphi 2.0 using OLE Automation is probably the best
way to set up interprocess communication. At any rate, it should be considered
carefully whenever you are contemplating methods for getting two applications to
communicate. For the record, mutexes and memory mapped files can also be used
when two processes need to share memory or communicate with one another.
The basic purpose of both SendMessage and PostMessage is to tell a particular window to per-
form a task. The SendMessage function does this by explicitly calling the window procedure
associated with the HWnd in its parameter list. SendMessage doesn’t return from that window
procedure until the window has processed the message in question.
The PostMessage function, however, doesn’t explicitly call a window procedure. Instead, it posts
a message to the application’s message queue and immediately returns.
The old 16-bit Windows applications had a message queue that would, by default, handle up
to eight messages at a time. WIN32 applications can automatically resize the queue; that is, it
will grow or shrink as needed.
Messages are retrieved from the queue by the GetMessage function. If you glance at FontsExp’s
WinMain function, you can see that after GetMessage is called, a message is passed on to the window
procedure with the DispatchMessage function.
What it all boils down to is that SendMessage delivers a message directly to a window proce-
dure, whereas PostMessage just plops a message into a queue. In other words, SendMessage is
the express route, and PostMessage is the slower, more laid-back way to deliver the mail. (See
Figure 14.7.) SendMessage lets you deliver a message immediately, while PostMessage lets you
immediately continue processing the code in the current routine.
FIGURE 14.7
SendMessage delivers a
message directly to a
window procedure, whereas
PostMessage takes a
more roundabout route.
NOTE
Use SendMessage when time is of the essence; that is, when you want to be sure a
message is processed before the SendMessage call ends. Don’t use SendMessage if you
can possibly put off processing the message. Remember that SendMessage won’t return
until its message has been processed. Don’t forget that PostMessage returns
immediately, enabling your program to continue on the next line of code, usually well
before the message is processed.
In this particular case, FontsExp uses PostMessage for two reasons. The first reason is that there
is no immediate need to process the message; the second reason is that the creation of the window
should end before the WM_STARTFONTS message is processed.
How can FontsExp be sure that the creation of the window will be completed before the message
is processed? To understand the reasoning, simply trace through the steps involved in starting
a Windows application.
Start the debugger and place a breakpoint at the first call to CreateWindow. Next, start stepping
through the program from the beginning, after WinMain is called. The first step is the call to
RegisterWindow. Then, there are the calls to CreateWindow, ShowWindow, UpdateWindow, and
GetMessage.
The WM_STARTFONTS message is posted at the end of a WM_CREATE message handler function.
WM_CREATE messages are sent during the call to CreateWindow. This means that a window will
finish creation, be shown, and be updated before GetMessage ever has a chance to pull the
WM_STARTFONTS message out of the application queue.
NOTE
Because this book assumes a certain amount of knowledge on the programmer’s part,
I’m not going to take the time to describe exactly how to use your debugger. If you are
not yet familiar with your debugger, you should take the time to learn about it by
reading Delphi’s documentation.
For most programmers, there is nothing more important than a thorough knowledge
of their debugger. You should take the time to get to know that tool, which in Delphi’s
case is particularly easy because the debugger is built right into the IDE. Specifically, be
sure you know how to set breakpoints, how to watch a variable, and how to step
through your code one line at a time. Believe me, it will be well worth it. If everything
goes wrong, it will take about 30 minutes to learn all these things. On a good day, you
can learn them in five minutes. Use the docs!
Here’s a step-by-step view of the process that occurs when you first start the FontsExp program:
1. Register the window.
2. Call CreateWindow. This is the stage at which a window handles WM_CREATE
messages. This is also the stage when the WM_STARTFONTS message is posted.
3. Show and update the window.
4. Start the message loop. It’s during this stage that the WM_STARTFONTS message is
actually passed on to WndProc. The message loop has to be running before any
messages can be pulled out of the queue!
That’s the end of my introduction to Windows messages. I’ve discussed this topic in-depth
because the whole idea of sending and posting messages is a crucial part of the life of a Win-
dows application. Furthermore, you won’t really understand what’s being said in the rest of
this chapter unless you first understand, at least in general terms, how SendMessage and
PostMessage works.
SetFocus(hFontList);
SendMessage(hFontList, LB_SETCURSEL, 0, 0);
ShowTheFont(hWindow);
end;
The goal of this code is to enumerate through the system fonts. It asks the system: “What fonts
do you have available?” The system answers back, “Arial, Times New Roman, Courier” (and
so on). This process becomes somewhat complex, primarily because it is Windows (not the
FontsExp program) that knows which fonts are available. Therefore, the FontsExp program
has to find a way to ask Windows which fonts are available on the system.
The solution is to set up an address for Windows to mail the information to and then to sim-
ply wait at that address while Windows iterates through all the available fonts. Each time a
font is found, Windows sends a letter to the proper address in FontsExp. It’s just like ordering
something from the Land’s End catalog. You send something in the mail and wait for the goodies
to arrive. Anybody can do it. (See Figure 14.8.)
FIGURE 14.8.
FontsExp asks Windows
what fonts are available,
and Windows sends
information back.
The first step is to set up the mailing address, which in this case happens to be a function called
FontCallBack. Right now, don’t worry about how this function works; just concentrate on the
fact that it exists. It’s a little post office box with its own address.
To set up the post office, you call EnumFontFamilies and pass it the address of FontCallBack:
if not EnumFontFamilies(DC, nil, @FontCallBack,
LongInt(@EnumStruct)) then
What you are doing here is talking to Windows, telling it that you want to set up a post office
called FontCallBack. You say: “Here, Windows, is the address of the place where I want you to
send the mail. It’s in the third parameter.”
NOTE
Calling EnumFontFamilies involves a similar process to the one you went through when
you were learning how to set up a DialogBox procedure. All the issues are the same. In
particular, the FontCallBack function is part of FontsExp, but it’s going to be called by
Windows. In the dialog procedure, DialogBox is like EnumFontFamilies, and the dialog
procedure is like the FontCallBack function.
Don’t forget that in 16-bit Windows, you would have to call MakeProcInstance before
calling EnumFontFamilies, just as you had to call it before calling DialogBox.
If, by chance, the last few sentences don’t make a great deal of sense to you, you should
turn back to the discussion of the DialogBox procedure, and reread that section of the
book. The point is that Windows and your program need to agree on the mailing
address to which the list of fonts will be delivered, just as Windows and your program
had to agree on where the address of the About dialog was located. (Dialogs were
discussed in Chapter 10, “Dialogs and Resources.”)
Now that you understand the general concept involved, I want to go back over the call to
EnumFontFamilies in more detail. The key lines look like this:
DC := GetDC(hWindow);
EnumStruct.Count := 0;
EnumStruct.hWindow := hFontList;
if not EnumFontFamilies(DC, nil, @FontCallBack, LongInt(@EnumStruct)) then
Exit;
ReleaseDC(hWindow, DC);
The center of focus is the EnumFontFamilies call, which takes a device context as its first parameter.
The HDC is needed because fonts are part of the GDI. The second parameter is the name of
a font family, which in this case is set to nil. The third parameter is the address of the callback
function, which is the post office box to which information will be sent. The final parameter is
the address of some data that you can pass to the FontCallBack function. In many cases, you
can set this last parameter to 0. However, I have filled it in here, primarily so you can see how
to use it.
NOTE
Feel free to pass in the address of any structure or type in this last parameter to the
EnumFontFamilies function. In this specific case, I’m passing in a record that contains two fields:
the first is an integer and the second is an HWnd. (See the definition of TEnumStruct at the top
of FONTSEXP.DPR in Listing 14.1.) The integer is used to keep track of how many fonts are
available on the system, and the HWnd belongs to the list box where the fonts will be displayed.
I want to stress that I’ve customized the TEnumStruct record for my own purposes. If you
prefer, you can pass a structure containing four strings and a float, the address of another
procedure, or just plain old nil. Any valid address is fine.
When you are through calling EnumFontFamilies, you should also remember to free the device
context used during the call:
ReleaseDC(hwnd, DC);
That’s it for the description of setting up a callback. The next step is to see how to implement
the function that’s going to receive the mail.
The parameters passed to FontCallBack tell almost everything any programmer could ever want
to know about a font:
■ The first parameter is the LogFont of the font.
■ The second parameter is the TEXTMETRIC structure associated with the font.
■ The third parameter can be used to determine if the font is a TrueType font, or if it is
meant to be displayed on a screen, a printer, neither, or both.
■ The last parameter is the user-defined data passed in when EnumFontFamilies was
called.
Between them, the first two parameters hold structures containing some 30 different fields that
detail the characteristics of the font. (Use the stand-alone debugger to examine these fields, or
look up their types in the online help.)
Here’s what the FontCallBack function looks like:
function FontCallBack(lpnlf: PEnumLogFont;
lpntm: PNewTextMetric;
FontType: Integer;
Data: PEnumStruct): Integer; stdcall;
var
S: string;
begin
S := (lpnlf.elfLogFont.lfFaceName);
SendMessage(Data.hWindow, LB_ADDSTRING, 0, LPARAM(S));
Inc(Data.Count, 1);
Result := 1;
end;
As you can see, 90 percent of the data passed to FontCallBack is simply ignored by this function.
In this case, the only real area of interest is the lfFaceName field of the LOGFONT structure. This
data is promptly sent off to the list box, where it is displayed for the user’s perusal. The process
of sending the message to a list box is described in more depth in the next section.
Immediately after the first face name is sent off, the next piece of mail comes flying in (by airmail,
as it were), courtesy of Windows itself. Once again, FontCallBack bundles up the font name
and sends it off to the list box to be displayed. This whole process is continued until there are
no more fonts left to iterate or the callback returns zero.
Well, there you have it. You’ve just seen a rough outline of how to enumerate the fonts on the
system through a callback. There were five steps involved:
1. Write a callback procedure.
2. Get a device context.
3. Call EnumFontFamilies and wait for Windows to send the font information to
your callback.
4. Process the information sent to your callback.
5. Release the DC.
These outlined steps are all well and good in and of themselves. However, nothing described
so far does anything to fill the user in on what has taken place. In order to give the call a visual
element, FontsExp must first communicate directly with visual controls such as list boxes, check
boxes, and edit and static controls.
This function actually sends the names of the enumerated fonts to the list box so the user can
peruse them at his or her leisure.
You already know that SendMessage delivers a notice directly to a particular WndProc, which in
this case happens to be the window procedure of the list box. Remember that the HWnd of the
list box was passed in via the custom user-defined date passed to EnumFontFamilies.
What’s key here, of course, is the LB_ADDSTRING message. LB_ADDSTRING is one of nearly 40
predefined messages that can be sent to a list box. It tells Windows that the list box in question
needs to have a new string added to it. When you use LB_ADDSTRING, the third and fourth
parameters of SendMessage or PostMessage should be filled in as follows:
■ The third parameter (WPARAM) should be set to 0.
■ The fourth parameter is set to the address of the string that’s going to be added to the
list box. Because the fourth parameter of the SendMessage function is an LPARAM, it’s
necessary to typecast the string before sending it.
Following is a list of some of the more commonly used messages sent to a list box:
LB_ADDSTRING Adds a string
LB_DELETESTRING Deletes a string
LB_DIR Adds a list of filenames
LB_GETCOUNT Gets the number of items
LB_GETCURSEL Gets the index of a selected item
LB_GETSEL Gets the selection state of an item
LB_GETSELCOUNT Gets the number of selected items
LB_GETTEXT Gets a string
LB_GETTEXTLEN Gets the length of a string
LB_RESETCONTENT Removes all items
LB_SELECTSTRING Selects a matching string in a list box
LB_SELITEMRANGE Selects consecutive items in a list box
LB_SETCOLUMNWIDTH Sets the width of columns in a list box
LB_SETCURSEL Selects an indexed string in a list box
LB_SETSEL Selects a string in a multiple-selection list box
LB_SETTOPINDEX Ensures that an item is visible
LB_SETTABSTOPS Sets tab stops
After the FontCallBack function has been sent the name of all the fonts on the system, the
program needs to fill the static control above the list box. To see it do this, switch your focus
back to the StartFont function:
These lines use the Count field, which was incremented every time FontCallBack was sent a
new piece of mail. (See Figure 14.9.)
FIGURE 14.9.
The list box and its static
text after they have been
filled with information
about the fonts on the
system.
The next step in the program is to set the focus on the list box, and then highlight the first item
in the box. Use the debugger to step through these lines, which, by the way, are still part of the
response to WM_STARTFONT:
SetFocus(hFontList);
SendMessage(hFontList, LB_SETCURSEL, 0, 0);
This call to SetFocus makes it easy for the user to scroll through the fonts in the list box. A list
box can respond when a user manipulates the arrow keys. However, the list box must first have
the focus before these keystrokes will have the desired effect. To select a list box, call SetFocus
with the HWnd of the control as the sole parameter. This has the same effect as actually clicking
the control with the mouse.
Selecting an item in a list box is a straightforward process. All you need to do is send the list
box an LB_SETCURSEL message and the index of the item to highlight. The message itself in-
structs the list box to highlight a particular item. The index of the item to highlight is specified
in WPARAM. Because the index is zero-based, a value of 0 in WPARAM instructs the list box to high-
light the first item in the list.
FIGURE 14.10.
These controls are handled
by the ShowTheFont
function.
NOTE
The ShowTheFont function is called twice. It is called once at the beginning of the
program in response to WM_STARTFONTS message. Thereafter, it is called every time the
user selects a different font in the list box. In other words, the list box gets filled only
one time—at program creation. ShowTheFont, however, gets called every time the user
asks that another font be displayed.
Following is the code for ShowTheFont. Take the time to step through the code with your debugger
while you follow the discussion.
procedure ShowTheFont(hWindow: HWnd);
var
Buffer: string;
EnumStruct: TEnumStruct;
Index: LResult;
DC: HDC;
Alpha: string;
SaveIt: hFont;
begin
SaveIt := 0;
SetLength(Buffer, 250);
Alpha := ‘1234567890abcdefghijklmnopqrstuvwxyz’ +
‘ABCDEFGHIJKLMNOPQRSTUVWXYZ’;
DC := GetDC(hWindow);
ReleaseDC(hWindow, DC);
The function starts by getting the name of the currently selected font from the list box. To do
this, it must first find the index of the selected item and then ask for the string associated with
that item number.
NOTE
When retrieving an item from a list box, you usually first find out which item the user
has selected by sending an LB_GETCURSEL message. Then, use the LB_GETTEXT message to
retrieve the string:
Index := SendMessage(hFontList, LB_GETCURSEL, 0, 0);
SendMessage(hFontList, LB_GETTEXT, WPARAM(Index), LPARAM(Buffer));
The LB_GETCURSEL message should have both the WPARAM and the LPARAM of the
SendMessage function set to 0. The LB_GETTEXT message, on the other hand, expects the
index of the string to be in the WPARAM argument, and a buffer to hold the string in
LPARAM. The buffer in LPARAM must be long enough to hold the string and the NULL
terminator. If you need to get the length of a particular string, send an LB_GETTEXTLEN
message.
Of course, the first time these messages are sent, the selected item will be the first item
in the list box. However, this function gets called over and over again throughout the
life of a program as shown in the next section. On later occasions, the user will most
likely select a different item than the one set by default at the beginning of the
program.
The next step in the ShowTheFont function is a second call to EnumFontFamilies. This time, the
name of a font is passed in the second parameter. The mailing address, or callback, is the
DescribeFontCallback:
begin
SendMessage(Data.hWindow, WM_NEWFONT, 0, LPARAM(@lpnlf.elfLogFont));
Result := 1;
end;
During the previous call to EnumFontFamilies, the callback procedure was called once for every
font on the system. This time, however, DescribeFontCallBack is called only once. The purpose
of the first set of calls is to get the name of all the fonts on the system. The purpose of the
second call is to get detailed information about a specific font, specified in the second parameter
of EnumFontFamilies.
NOTE
Obviously, it would have been possible to call EnumFontFamilies only once in this
program. That is, I could have iterated through the fonts once and maintained a linked
list or dynamic array containing all the information about each font on the system. I
opted not to take this route for the following reasons:
■ To give you working examples demonstrating two different ways to call
EnumFontFamilies.
Back in the WndProc, the response to the WM_NEWFONT message is simply to use the packaged
TLogFont to create a new font for the main window:
The code first deletes any existing font. Then, it typecasts lParam to get hold of the TLogFont
and calls CreateFontIndirect to create a new font. The font itself is stored in a global variable.
After the SendMessage function is processed, the code returns to the ShowTheFont function.
TIP
Set debugger breakpoints on both the DescribeFontCallBack and the WM_NEWFONT case
statements, and then step through the program several times until you understand the
relationship between the two code fragments. This is quintessential Windows
programming—take the time to understand how it works.
Notice that the BM_SETCHECK message always takes 0 as its last parameter. You should set WPARAM
to 1 to set the check, and 0 to erase the check. Once again, no one expects you to remember
how to use the BM_SETCHECK message. All that’s important is that you know it exists and that
you know how to look up the information. If remembering all these bits of information comes
to you easily, that’s fine. But don’t waste time struggling to remember all these details. Just
know how to look them up when you need them.
The last few calls in ShowTheFont send the information to the two edit controls with the
WM_SETTEXT message, which wants a string buffer sent in the last parameter. After sending the
text to the control, set the font with the WM_SETFONT message. You’ll probably want to take a
close look at those two SendMessage calls, because they perform an important task that you
might want to utilize in your own programs. Anyway, the FontsExp program wouldn’t be nearly
as useful, or as much fun, if the text on-screen weren’t written in the current font.
NOTE
CM_INFO: begin
S := GetFontString(S, TextMetrics, TextFace);
MessageBox(hWindow, PChar(S), ‘Info’, MB_OK);
end;
end;
end;
The code checks to see whether the ItemID field is set to ID_LISTBOX and whether the message
being sent signifies that the selection in the list box has changed. If both statements are true,
ShowTheFont is called. As mentioned, ShowTheFont knows how to set the controls in the window
to exhibit an example and a description of the current font.
It’s quite typical of Windows to make it so very easy to find out whether a selection has changed
in a list box. Information about things the user has done almost always comes to you free of
charge, courtesy of the people at Microsoft. However, it’s also typical of Windows to make it
fairly difficult for you to send information back to the user. Windows is such an ornate and
powerful tool that creating an effective interface can sometimes be a little bit tricky, which is
where the RAD tools come in.
Finally, you should notice that this program calls the G e t F o n t S t r i n g function in
FONTSTR.PAS. This function displays additional information about the current font.
tmBreakChar: AnsiChar;
tmItalic: Byte;
tmUnderlined: Byte;
tmStruckOut: Byte;
tmPitchAndFamily: Byte;
tmCharSet: Byte;
end;
TTextMetric = TTextMetricA;
SimpFont wrestles with the TTextMetric structure in an isolated module called FontStr. FontStr
returns most of the TTextMetric fields in a single string. This module is designed so it can be
easily linked to another program, in case you have use for it. (Always strive for reuse. Reuse is
arguably even better than optimization.)
The core of FontStr is the GetFontString function. It contains the following straightforward,
but lengthy, call to Format:
TheFaceName := FaceName; // Facename had its length set to 250
Len := StrLen(PChar(FaceName)); // We have to shorten the string
SetLength(TheFaceName, Len); // To use it with Format!!!
GetType(szType);
GetFamily(szFamily);
GetCharSet(szCharSet);
S := Format(‘Font: %s ‘ + #13 +
‘Height: %d ‘ + #13 +
‘Ascent: %d ‘ + #13 +
‘Descent: %d ‘ + #13 +
‘AveCharW: %d ‘ + #13 + ‘MaxCharW: %d ‘ + #13 +
‘Weight: %d ‘ + #13 + ‘Italic: %d ‘ + #13 +
‘Underlined: %d ‘ + #13 + ‘%s ‘ + #13 +
‘%s ‘ + #13 + ‘%s ‘,
[TheFaceName,
TextMetrics.tmHeight,
TextMetrics.tmAscent,
TextMetrics.tmDescent,
TextMetrics.tmAveCharWidth,
TextMetrics.tmMaxCharWidth,
TextMetrics.tmWeight,
TextMetrics.tmItalic,
TextMetrics.tmUnderlined,
szType, szFamily, szCharSet]);
Result := S;
As you can see from Figure 14.11, most of the information from the TEXTMETRIC structure is
transferred into a single string. This string is returned to the WM_COMMAND handler function in
the program’s main module and displayed through a call to the MessageBox function:
S := GetFontString(S, TextMetrics, TextFace);
MessageBox(hWindow, PChar(S), ‘Info’, MB_OK);
GetFontString is the only function in the FontStr module that is made global so it can be accessed
from other modules.
FIGURE 14.11.
The SimpFont program
pops up a message box
relating important
information about the
currently selected font.
The parts of the FontStr module described so far are fairly straightforward. The tricky sections
involve deciphering the contents of the tmPitchAndFamily and the tmCharSet fields:
■ The four low-order bits of tmPitchAndFamily describe the type of font, which can be
one of the following values:
TMPF_FIXED_PITCH A fixed-pitch font
TMPF_VECTOR A vector font
TMPF_TRUETYPE A TrueType font
TMPF_DEVICE A device font
Many times, a font combines one or more of these values.
■ The four high-order bits can be ANDed together with the hexadecimal value $F0 and
then used to determine the current font family. Common font families include
Roman, Swiss, Script, and Modern.
■ The tmCharSet field is set to one of the following values:
ANSI_CHARSET 0
DEFAULT_CHARSET 1
SYMBOL_CHARSET 2
SHIFTJIS_CHARSET 128
OEM_CHARSET 255
Extracting this information from a TEXTMETRIC structure is the job of the following functions,
all found in the FontStr module:
function GetType(var S: string): string;
function GetFamily(var S: string): string;
function GeTCharSet(var S: string): string;
The GetType function uses BitWise operations to find the current font type. The GetFamily
function uses the hex value F0 to determine the font family, and the GetCharSet functions retrieves
the current CharSet. If you want to understand these functions in-depth, you should crank up
the debugger and start stepping through them one line at a time.
NOTE
Although SimpFont doesn’t use it, I should probably also mention the GetDeviceCaps
routine. It enables you to ask Windows what capabilities are associated with a
particular device, such as a video screen or printer. You can use this function to
discover whether the device can, for instance, print very large fonts, clip fonts, or
stroked fonts.
You should keep both the GetDeviceCaps and GetTextMetrics functions in mind,
because no serious work with fonts can be conducted without them.
Summary
In this chapter, you’ve had a chance to become acquainted with Windows controls. You have
seen that these tools can be used to form an interactive interface between your program and a
user.
This chapter covers the creation of four controls:
■ Static controls
■ List boxes
■ Check boxes
■ Edit controls
This is an incomplete, but representative, selection of Windows controls. The information
presented here should be enough to allow you to start working with any type of control, even
the new Windows 95 controls.
When creating window controls, the most important step is to fill in the first field of
CreateWindow, called lpszClassName, with the class name of a control. For instance, if you want
to create an edit control, copy the word edit into lpszClassName. Other controls are created by
copying in one of the following words: static, scrollbar, list box, or button.
This chapter also presents an in-depth discussion of the various styles used to define the behav-
ior of a control. For instance, you have seen that the ES_CENTER style centers text in an edit
control and the BS_CHECKBOX style converts an ordinary button into a check box.
All in all, this chapter covers a lot of ground. You’ve learned about communicating with static
controls, list boxes, check boxes, and edit controls. You’ve had another look at fonts and learned
how to put them at your absolute beck and call with the EnumFontFamilies and GetTextMetrics
calls.
By now you should be getting the sense that you have the ability to completely control your
presentation of textual materials. You know all about TLogFonts, TTextMetrics, and the
EnumFontFamilies callbacks. This knowledge should help you begin to understand how Windows
is put together.
Once again, the point of this chapter is not to suggest an alternative to the RAD tools that ship
with Delphi. This information is meant to help you understand the RAD tools, to help you
extend them so they do exactly what you want, and to help you create new components of
your own. However, there is one virtue to the code in this chapter that does not cling to the
VCL. The FontsExp program is 33 KB in size. Its much smaller than a similar program built
in the VCL.
IV
IN THIS PART
Databases
The fourth part of this book is about databases. After the theoretical issues
explored in Parts II and III, these chapters help to ground the book in more
concrete examples of how to perform specific tasks. This is similar to a how-
to section of the book. It explores new features of Delphi, such as data
modules, data dictionaries, lookups, filters, cached updates, and the new
grids and multi-record objects. On a more theoretical note, the database
material also explores what it means to create a relational database and how
Delphi’s tools can be used to design and construct relational databases. The
last chapter in this section, written by Andy Fears, takes a highly
professional, in-depth look at ReportSmith.
■ Fields 513
■ Bookmarks 527
In this chapter, you learn some of the basics about accessing database tables. In the examples
given here, you will be explicitly working with local Paradox tables, but nearly everything
explained here applies equally to dBase files or to files located on a server.
If you are already familiar with the Delphi 1.0 database tools, you still might want to read this
chapter in order to learn about the TDataModule, the Object Repository, the Database Explorer,
and a new method for filtering on non-keyed fields. There is also a new and much easier
technique for accessing fields by name.
Looking a little more deeply at the content of this chapter, you can expect to find information
on the following:
■ The TTable object, which provides the fastest and simplest access to tables.
■ The TQuery object, which is the gateway to the flexible and powerful world of SQL.
■ The TDataSet object, an ancestor of TTable and TQuery, which provides the core
functionality for accessing tables and the records that lie within them.
■ The TField object, which gives you access to the fields in a table or dataset. This
object has powerful descendants such as TStringField and TIntegerField, all of which
can be created automatically by a visual tool called the Fields Editor.
■ The TDataSource object, which serves as an intermediary between data-aware controls
and the TTable and TQuery objects.
■ The TDBGrid object, which provides a simple and easy way to display the contents of
tables to the user. TDBGrid objects support editing, deletion, and insertion. They also
have support for dropdown lists in lookup fields and the ability to assign colors to a
column.
■ The TDBEdit component, which enables you to display a single field from a single
record and to edit or insert the contents of that field.
Here is a second way to categorize the objects listed:
■ Nonvisual: TTable, TQuery, TDataSet, TField
■ Visual: TDBGrid, TDBEdit
■ Link (also nonvisual): TDataSource
This latter view of the major database components breaks them down into two major categories.
The nonvisual components enable you to open, close, edit, and otherwise manipulate tables,
records, and fields. The visual components display the tables to the user and enable the user to
edit them. The powerful TDataSource object forms a link between the visual and nonvisual
database controls.
This chapter also discusses the TDataModule object, which enables you to place nonvisual
components, and particularly nonvisual database components, on a special window designed
for this purpose. This window can be stored in an object repository and can be used to specify
business rules that can be reused by a wide range of projects and programmers.
The overriding purpose of this chapter is to give you a good overview of the basic facts about
using a Delphi database component called TDataSet. TDataSet is the driving force behind both
the TTable and TQuery objects.
Specific information about other database issues will be presented in subsequent chapters. For
instance, the TQuery object will be treated in depth in the next chapter, and a more detailed
explanation of TDBGrid, TField, TStringField, and TIntegerField is found in Chapter 17,
“Fields and Database Tools.”
Database Basics
To create a simple database application, start by placing a TTable, a TDataSource, and a
TDBGrid component on a form, as shown in Figure 15.1.
FIGURE 15.1.
TTable, TDataSource, and
TDBGrid arranged on a
form.
Wire these three controls together by completing the following simple steps:
1. Connect the DataSource property of the TDBGrid to DataSource1.
2. Connect the DataSet property of the TDataSource control to Table1.
After completion of these steps, the three components are hooked together and can communicate
with one another.
Connecting the TTable object to a table that resides on disk is a three-step process:
1. Set the DatabaseName property either to a valid alias or to the subdirectory where your
data resides. In this case, you can set the DatabaseName property to the DBDEMOS
alias, which is created by default during Delphi’s installation. Alternatively, you could
type c:\delphi32\demos\data into the DatabaseName Property Editor, where you
might need to change some aspects of this path to fit the needs of your system.
2. Set the TableName property to the name of the table you want to view; for instance,
you might choose the CUSTOMER.DB table. The Property Editor drops down a list
of available tables, so there is no need for you to type anything.
3. Set the Active property, found at the very top of the Object Inspector, to True. (See
Figure 15.2 for the Object Inspector.)
FIGURE 15.2.
The Object Inspector after
connecting to a table called
Customer, using an alias
called DBDEMOS.
If you have completed all these steps properly, you should now be looking at the data from the
table you chose, as shown in Figure 15.3. To take this process one step further, you can com-
pile and run the program, and then begin browsing and editing your data.
FIGURE 15.3.
Simple form displaying
the contents of
CUSTOMER.DB.
If you want to simplify the task of browsing through the data in your application, you can go
back into design mode and add the TDBNavigator control to the program described earlier.
To hook this control into the loop, all you need to do is set its DataSource property to
DataSource1. Now you can run the program and begin iterating through the records with the
navigator, as shown in Figure 15.4. In Figure 15.4, most of the functionality of the dbNavigator
has been turned off by manipulating the VisibleButtons property. For instance, a navigator
can automatically enable you to edit, insert, delete, post, cancel, and refresh. All of those capa-
bilities have been disabled and hidden in the form shown here.
FIGURE 15.4.
A simple database program
with a TDBNavigator
control.
The DataModule
In the previous example, you placed a TTable and TDataSource component on the same form
with your visual components. When you ran the program, the icons representing these
components disappeared. However, they are visible at design time and have a tendency to clutter
up the form. In order to eliminate this clutter, the 32-bit version of Delphi has a new component
called a TDataModule, which can be used to store nonvisual controls such as TTable and
TDataSource.
To get started working with TDataModules, first start a new application. Now choose File |
New and select the DataModule component from the New page of the New Items dialog, as
shown in Figure 15.5.
FIGURE 15.5.
Selecting the TDataModule
component from the New
Items dialog.
NOTE
-TComponent
-TControl
-TWinControl
-TScrollingWinControl
-TForm
The hierarchy for a TDataModule, on the other hand, looks like this:
-TComponent
-TDataModule
Clearly, TForms and TDataModules are two very different beasts, despite some
apparent similarities between them.
If you have the source to the VCL, the TDataModule object is found in FORMS.PAS.
After adding a TDataModule object to your application, click Form1, open the File menu,
and choose the Use Unit menu option. In the Use Unit dialog, select Unit2 and press OK.
This is a simple way of automatically inserting a line into the uses clause of Unit1. In particular,
the following changes are made to your code:
implementation
uses Unit2; // This is the new line added by Use Unit dialog.
{$R *.DFM}
end.
Arrange Form1 and DataModule2 on the screen so you can view them both at the same time.
Drop a TTable and TDataSource component on the datamodule, as shown in Figure 15.6.
FIGURE 15.6.
Form1 and DataModule2
arranged on the screen so
that you can easily view
them both at the same time.
Wire the TDataSource to the TTable object, and set the DatabaseName of the TTable object to
DBDEMOS and the TableName to BioLife. Set the Active property of the TTable object to
True.
Right-click the TTable object and bring up the Fields Editor. Right-click the Fields Editor,
and bring up the AddFields dialog. Make sure all the fields in the dialog are selected, which is
the default behavior for the tool. Click the OK button.
The Fields Editor now contains a list of all the fields in the BioLife table. To add these files to
your form, simply click one or more fields, hold the left mouse button down, and drag them
onto the form. For instance, select the Graphic field, making sure that it is the only one
highlighted. Now drag it onto the form. When you release the left mouse button, this graphics
field will automatically display itself in a TDBImage component. Drag over several other fields,
and create a form that looks something like the image in Figure 15.7.
FIGURE 15.7.
Some of the fields of the
BioLife table arranged in
visual controls on a TForm
object.
Start a new project. Choose File | New. This time, instead of choosing the DataModule com-
ponent from the New page, select the DataModules page and choose the DataInfo component
that you just finished creating. When it appears on the screen, you will see that it contains a
TTable and TDataSource component. The components are wired together, and the TTable
object is set to the BioLife table with its Active property set to True.
To access this table from Form1, you must first employ the Use Unit menu option from the
File menu to add Unit2 to the uses clause in Unit1. Go to the DataControls page of the
Component Palette, and drop down a TDBGrid object on Form1. In the Object Inspector,
dropdown the DataSource property of the TDBGrid object, and you will see the TDataSource
object from the DataInfo module listed. Select this item, and the grid will automatically fill up
with data.
If you drop down a TDBEdit control instead of a TDBGrid control, then you proceed the
same way, except that you will need to fill in not only the DataSource property in the Object
Inspector, but also the DataField property. There is no need to type information into the
DataField property because it will automatically contain a list of the available fields in the BioLife
table.
NOTE
The whole point of adding Unit2 to the uses clause of Unit1 is so that you can access
the components in Unit2 from Form1. For instance, if you did not have Unit2 listed
in Unit1’s uses clause, then you would find the DataSource property of the TDBGrid
object empty when you dropped it down to search for a TDataSource component.
Of course, you don’t have to do things manually. Instead, you can use the Fields
Editor to drag and drop objects from the datamodule onto Form1, as described earlier.
What I’ve done here is show you two different techniques for hooking up Form1 and
the datamodule. In practice, you will probably sometimes use the drag-and-drop
technique and sometimes manually hook the visual tools to the datasource.
The true significance of the object repository is only hinted at by this example. The importance
of this tool is made more obvious if you have six or seven tables dropped onto a datamodule.
You might then define several relationships between the tables and add other related code. For
instance, you might have some one-to-many relationships established, possibly a many-to-many
relationship established, and you might have several filters, lookups, and several calculated fields
defined.
Altogether, a datamodule of this type might encapsulate several sets of business rules defining
exactly how tables should be accessed and how they relate to each other. The ability to save all
this work in the Repository, and to then automatically reuse it in multiple projects, is extremely
valuable. I am, however, getting ahead of myself. Discussions of filters, lookups, calculated fields,
and other database issues will occur in various places over the next few chapters.
FIGURE 15.8.
Exploring the BioLife table
in the Database Explorer.
Click the little plus symbol in front of the BIOLIFE.DB node, and you will see a list of properties
for the BioLife table. The properties listed are Fields, Indices, Validity Checks, Referential
Constraints, Security Specs, and Family Members. You can expand each of these nodes to view
their properties. For instance, if you select the Fields node, you will see a list of all the fields in
the table. As you select each individual field, you will see a description of its primary
characteristics.
The Database Explorer is a fairly complex tool with a number of powerful traits, many of which
will undoubtedly be expanded in future versions of the product. In particular, you should note
that it contains a DataDictionary, and that it enables you to define a new alias or to modify
existing aliases. Chapter 17, “Fields and Database Tools,” examines the Explorer in more depth.
At this stage, I want to show you only one key trait of the Database Explorer. Arrange your
screen so you can view both the Explorer and Form1 at the same time. Select the BioLife table
in the Explorer with the mouse, and then drag and drop it onto Form1. If you want, you can
experiment further by expanding the BioLife node in the Explorer and dragging and dropping
individual fields of the table onto Form1, just as you did when using the Fields Editor.
If you start a new application, and then drag and drop the BioLife table onto Form1 from the
Explorer, you will find that the TTable and TDataSource objects are placed on Form1. If you
want to move them off the form, you can add a TDataModule object to the project, and then
select both the TTable and TDataSource objects and choose Edit | Cut from the menu. Now
select the TDataModule object and choose Edit | Paste. Make sure the uses clause for Form1
contains a reference to the unit that contains the TDataModule, and then hook up the grid to
the TDataSource object in the TDataModule. This sounds like a fairly complicated process
when written out, but you can perform this task in just a few seconds using Delphi’s visual
tools.
NOTE
Dragging a table from the Explorer onto a form is one of the fastest ways to get access
to a table. When I need to get data quickly and easily, this is the technique I prefer to
use. The viability of this method, will, of course, depend a little on the power of the
machine you are currently using. If it takes a long time for the Database Explorer to
launch itself into memory, then this technique will be less appealing.
You can also drag a table from the Explorer directly onto a TDataModule. You may
get a message that its not being legal to add controls to a TDataModule, but the
operation should still succeed.
Once again, the last few paragraphs have done nothing more than introduce you to the Database
Explorer. This is a complex tool that will prove useful to you in many different ways, some of
which might not even have been apparent to its creator. For now, the key point to grasp about
it is that it gives you an overview of all the data in a database and enables you to drag and drop
fields and tables onto your forms.
TTable and TQuery inherit most of their functionality from TDataSet. As a result, the TDataSet
class is one of the most important database objects. To get started working with it, you need to
concentrate on the following hierarchy:
TDataSet
|
TDBDataSet
|
/ \
TTable TQuery
TDataSet contains the abstractions needed to directly manipulate a table. TDBDataSet knows
how to handle passwords and other tasks directly associated with linking to a specific table.
TTable knows how to handle indices and the specific chores associated with linking two tables
in a one-to-many relationship. As you will see in the next chapter, TQuery has specific knowledge
of how to process SQL statements.
The methods of the TDataSet object enable you to open and navigate through a table. Of course,
you will never directly instantiate an object of type TDataSet. Instead, you will usually be
working with TTable, TQuery, or some other descendant of TDataSet. The exact way this
system works, and the precise significance of TDataSet, will become clear as you read through
this chapter.
On the most fundamental level, a dataset is nothing more than a set of records, as depicted in
Figure 15.9.
FIGURE 15.9.
A dataset consists of series of
records, each containing X
number of fields and a
pointer to the current
record.
On many occasions, a dataset has a direct, one-to-one correspondence with a physical table
that exists on disk. However, at other times, you may perform a query or other action that
returns a dataset that contains either a subset of one table, or else a join between multiple tables.
The text that follows, however, sometimes uses the terms dataset and table interchangeably if
it helps to simplify the explanation of a particular concept.
You will normally instantiate an object of type TTable or TQuery in order to access the
functionality of TDataSet. Because of this relationship, the code in the next few sections will
always assume the existence of an instance of class TTable called Table1. Remember, however,
that the functions under discussion are part of TDataSet, unless the text specifically states
otherwise.
It’s now time for you to begin a direct exploration of TDataSet. As you become familiar with
its capabilities, you will begin to understand exactly how Delphi accesses the raw data saved to
disk as a database. The key point to remember is that nearly every time a Delphi programmer
opens a table, he or she will be using a class such as TTable or TQuery, which are merely thin
wrappers around TDataSet.
Or, if you prefer, you can set the Active property equal to True:
Table1.Active := True;
There is no difference between the effects produced by these two statements. Open, however,
ends up setting Active to True, so it may be ever so slightly more efficient to use the Active
property directly.
Just as there are two ways to open a table, there are two ways to close a table. The simplest way
is to call Close:
Table1.Close;
Once again, there is no substantial difference between these two calls. You should note, however,
that Close and Open are procedures, but Active is a property.
You should also know about the TDatabase object, which exists primarily to give you a means
of staying connected to a database even if you are continually opening and closing a series of
tables. If you use TDatabase, you can be connected to Oracle, InterBase, or other servers without
ever opening any tables. You can then begin opening and closing tables over and over without
having to incur the overhead of connecting to the database each time you call Open.
The TDatabase object also enables you to start server-based applications without specifying a
password, and it gives you access to transactions. For further information on TDataBase, see
the TRANSACTS program, which is discussed at the end of Chapter 19, “Working with the
Local InterBase Server.”
procedure Close;
You also learned about the TDatabase object, which can be used to optimize code that is
continually connecting and disconnecting from tables that belong to one database.
Navigational Routines
After opening a dataset, the next step is to learn how to move about inside it. The following
rich set of methods and properties from TDataSet provides all the tools you need to access any
particular record inside a dataset:
procedure First;
procedure Last;
procedure Next;
procedure Prior;
property BOF: Boolean read FBOF;
property EOF: Boolean read FEOF;
procedure MoveBy(Distance: Integer);
Experienced programmers will find these procedures very easy to use. Here is a quick overview
of their functionality:
■ Calling Table1.First moves you to the first record in a table.
■ Table1.Last moves you to the last record.
■ Table1.Next moves you one record forward, unless you are at the end of a table.
■ Table1.Prior moves you one record back, unless you are at the beginning of the table.
■ You can check the BOF or EOF properties in order to see if you are at the beginning or
the end of a table.
■ The MoveBy procedure moves x number of records forward or backward in a table.
There is no functional difference between calling Table1.Next and calling
Table1.MoveBy(1). Furthermore, calling Table1.Prior has the same effect as calling
Table1.MoveBy(-1). In fact, Next and Prior are one-line procedures that call MoveBy,
exactly as shown here.
Most of these properties and methods are demonstrated in the example program found on the
CD accompanying this book as NAVY.DPR. You can open this example directly, or construct
it piece-by-piece by following the description that follows.
To get started using these navigational routines, you should:
1. Place a TTable, TDataSource, and TDBGrid on a form.
2. Hook the grid to the datasource and the datasource to the table.
3. Set the DatabaseName property of the table to the DBDEMOS alias, or type in the path to
the demos subdirectory (..\delphi 2.0\demos\data).
4. Set the TableName property to the CUSTOMER table.
If you are having trouble completing these steps refer to NAVY.DPR, which comes on the
CD that accompanies this book.
If you run a program that contains a TDBGrid control, you will find that you can iterate through
the records in a dataset by manipulating the scrollbars on the edges of the grid. You can gain
the same functionality by using the TDBNavigator component. However, there are times when
you want to move through a table programmatically, without the use of the built-in visual tools.
The next few paragraphs explain how this process works.
Place two buttons on a form and label them Next and Prior, as shown in Figure 15.10.
FIGURE 15.10.
The Prior and Next
buttons in NAVY.DPR
enable you to maneuver
through a database.
Double-click once on the Next button to create an OnClick method, and fill it in like this:
procedure TForm1.bNextClick(Sender: TObject);
begin
Table1.Next;
end;
Perform the same action with the Prior button, so that the function associated with it looks
like this:
procedure TForm1.bPriorClick(Sender: TObject);
begin
Table1.Prior;
end;
Now run the program and click the two buttons. You will find that they easily let you iterate
through the records in a dataset.
Now drop down two more buttons and label them First and Last, as shown in Figure 15.11.
FIGURE 15.11.
The Navy program with all
four buttons inserted.
Proceed to do the same thing for the calls to Table1.First and Table1.Last as you did with
Next and Prior:
Nothing could be more straightforward than these navigational functions. First takes you to
the beginning of a dataset, Last takes you to the end, and the Next and Prior functions move
you one record forward or backward.
The following code shows a very common method for using Prior to get to the beginning of a
file:
while not Table1.BOF do begin
DoSomething;
Table1.Prior;.
end;
In the code shown here, the hypothetical function DoSomething is called on the current record
and then on every other record between the current location and the beginning of the dataset.
The loop continues until a call to Table1.Prior fails to move you back any further in the table.
At that point BOF returns True, and the program breaks out of the loop.
To optimize the code, set DataSource1.Enabled to False before beginning the loop, and then
reset it to True after the loop is finished. These two lines of code allow you to iterate through
the table without having to update the visual tools at the same time.
Everything said previously about BOF also applies to EOF. In other words, the code that follows
provides a simple means of iterating over all the records in a dataset:
Table1.First;
while not Table1.EOF do begin
DoSomething;
Table1.Next;
end;
The classic error in cases like this is to enter into a while or repeat loop but to forget to call
Table1.Next:
Table1.First;
repeat
DoSomething;
until Table1.EOF;
If you accidentally wrote code like this, your machine would appear to lock up. You could break
out of the loop only by hitting Ctrl+Alt+Del and asking Windows to kill the current process.
Also, this code could cause problems if you opened an empty table. Because the code uses a
repeat loop, DoSomething would still be called once, even though there was nothing to process.
As a result, it’s better to use while loops rather than repeat loops in situations like this.
EOF returns True in the following three cases:
■ If you open an empty dataset
■ If you call Table1.Last
■ If a call to Table1.Next fails
The last navigational routine that I want to cover is called MoveBy. MoveBy enables you to move
X number of records forward or backward in a dataset. If you want to move two records forward,
you would write:
MoveBy(2);
And if you wanted to move two records backward, you would write:
MoveBy(-2)
When using this function, you should always remember that when you are working on a net-
work, datasets are fluid entities, and the record that was five records back a moment ago may
now be back four records, or six records, or who knows how many records. In other words,
when you are on a network, someone on another machine may delete or add records to your
database at any time. If that happens, MoveBy might not work exactly as you expect. One solution
to this “fluidity” problem is to use the Bookmark functions mentioned later in this chapter.
NOTE
Prior and Next are simple one-line functions that call MoveBy. If you have the source,
look up TDataSet.Next in DB.PAS. The TDataSet object is a beautifully written
wrapper around the core functionality in the BDE.
After reading the last two sections, you should have a good feeling for how to move around in
a dataset. The navigational commands you have been learning about are very easy to use, but
it is essential that you understand them because they are likely to be part of your day-to-day
Delphi programming experience.
Fields
On most occasions when you want to programmatically access the individual fields of a record,
you can use one of the following properties or methods, all of which belong to TDataSet:
property Fields[Index: Integer];
function FieldByName(const FieldName: string): TField;
property FieldCount;
Table1[‘MyField’];
The Fields object also has a number of useful descendant classes with names such as TStringField
and TIntegerField. These child objects are discussed in Chapter 17, “Fields and Database Tools.”
The FieldCount property returns an integer that specifies the number of fields in the current
record structure. If you wanted a programmatic way to read the names of these fields, you could
use the Fields property:
var
S: String;
begin
S := Table1.Fields[0].FieldName;
end;
If you were working with a record with a first field called CustNo, the code shown would put
the string “CustNo” in the variable S. If you wanted to access the name of the second field in the
example, you could write:
S := Table1.Fields[1].FieldName;
In short, the index passed to Fields is zero-based, and it specifies the number of the field you
want to access, where the first field is number zero, the second is referenced by the number
one, and so on.
If you want to find out the current contents of a particular field from a particular record, you
can use the Fields or FieldByName property, or you could access the entire table as an array of
fields. To find the value of the first field of a record, index into the first element of the Fields
array:
S := Table1.Fields[0].AsString;
Assuming that the first field in a record contains a customer number, the code shown would
return a string such as '1021', '1031', or '2058'. If you wanted to access this variable as an integer
value, you could use AsInteger in place of AsString. Similar properties of Fields include
AsBoolean, AsFloat, and AsDate.
If you want, you can use the FieldByName function instead of the Fields property:
S := Table1.FieldByName(‘CustNo’).AsString;
As used in the examples shown, both FieldByName and Fields return the same data. The two
different syntaxes are used solely to provide programmers with a flexible and convenient set of
tools for programmatically accessing the contents of a dataset. When in doubt, use FieldByName
because it won’t be affected if you change the order of the fields in your table.
You can also treat TTable as a variant array, which will let you access the fields of a table with
the following syntax:
S := Table1[‘CustNo’];
FIGURE 15.12.
The Fielder program shows
how to use the Fields
property.
Double-click the Fields button and create a method that looks like this:
procedure TForm1.bFieldsClick(Sender: TObject);
var
i: Integer;
begin
ListBox1.Clear;
for i := 0 to Table1.FieldCount - 1 do
ListBox1.Items.Add(Table1.Fields[i].FieldName);
end;
This method starts by clearing the current contents of the first list box, and then it iterates
through each of the fields, adding their names one by one to the list box. Notice that the for
loop shown here counts from 0 to FieldCount - 1. If you don’t remember to subtract one from
FieldCount, you will get a “List Index Out of Bounds” error, because you will be attempting to
read the name of a field that does not exist.
If you enter the code correctly, you will fill the list box with the names of all the fields in the
current record structure. Delphi provides other means to get at the same information, but this
is a simple, programmatic way for you to access these names at runtime.
In the FIELDER example, you can associate the following code with the second button you
placed on the program’s form:
procedure TForm1.bCurRecordClick(Sender: TObject);
var
i: Integer;
begin
ListBox2.Clear;
for i := 0 to Table1.FieldCount - 1 do
ListBox2.Items.Add(Table1.Fields[i].AsString);
end;
This code adds the contents of each of the fields to the second list box. Notice that once again,
it is necessary to iterate from zero to FieldCount - 1. The key point here is that the indices to
Fields are zero-based.
NOTE
Much of the functionality of TField can be achieved with visual tools. In particular,
you can manipulate fields with the Fields Editor, which you can access by clicking once
with the right mouse button on the top of a TTable or TQuery object. This subject is
explored in more depth in Chapter 17, “Fields and Database Tools.” However, good
programmers know how to use both the methods of TDataSet and the Fields Editor.
Furthermore, the Fields Editor can be used to best advantage if you understand how to
enhance its functionality with some of the code you are learning about in this chapter.
In this section you have learned how to access the fields of a record. In the next section you will
see how to use this knowledge when you want to append, insert, or edit records in a dataset.
Changing Data
The following methods enable you to change the data associated with a table:
procedure Append;
procedure Insert;
procedure Cancel;
procedure Delete;
procedure Edit;
procedure Post;
All these routines are part of TDataSet, and they are inherited and used frequently by TTable
and TQuery.
Whenever you want to change the data associated with a record, you must first put the dataset
you are using into edit mode. As you will see, most of the visual controls do this automatically.
However, if you want to change a table programmatically, then you need to use the functions
listed above.
Here is a typical sequence you might use to change a field of a given record:
Table1.Edit;
Table1[‘CustNo’] := ‘1234’;
Table1.Post;
The first line shown places the database in edit mode. The next line assigns the string '1234' to
the field labeled 'CustNo'. Finally, the data is written to disk when you call Post.
The very act of moving on to the next record automatically posts your data to disk. For in-
stance, the following code has the same effect as the code shown previously, plus it moves you
on to the next record:
Table1.Edit;
Table1[‘CustNo’] := ‘1234’;
Table1.Next;
Calls to First, Next, Prior, and Last all perform Posts, as long as you are in edit mode. If you
are working with server data and transactions, the rules explained here do not apply. However,
transactions are a separate matter with their own special rules, as explained in the next section.
Even if you are not working with transactions, you can still undo your work at any time, as
long as you have not yet either directly or indirectly called Post. For instance, if you have put
a table into edit mode, and have changed the data in one or more fields, you can always change
the record back to its original state by calling Cancel. For instance, you can edit every field of a
record, and then call the following line to return to the state you were in before you began editing:
Table1.Cancel;
There is also a Cancel button on the TDBNavigator control. You can use this button to cancel
an operation without having to write any code.
There are two methods, called Append and Insert, which you can use whenever you want to
add another record to a dataset. It obviously makes more sense to use Append on datasets that
are not indexed, but Delphi won’t throw an exception if you use it on an indexed dataset. In
fact, it is always safe to use either Append or Insert whenever you are working with a valid dataset.
On your disc you will find a simple program called INSERTS, which shows how to use the
Insert and Delete commands. To create the program by hand, first use a TTable, TDataSource,
and TDBGrid to open up the COUNTRY table from the demos subdirectory. Then place
two buttons on the program’s form and call them Insert and Delete. When you are done, you
should have a program like the one shown in Figure 15.13.
FIGURE 15.13.
The INSERTS program
knows how to insert and
delete a record from the
COUNTRY table.
NOTE
To spice up the Inserts program, you can drop a panel on the top of the form and then
add the buttons to the panel. Set the panel’s Align property to alTop, and set the
TDBGrid’s Align property to alClient. If you run the program, you can then maximize
and resize the form without damaging the relationship between the various visual
components.
The procedure shown here first puts the table into insert mode, which means that a new record
filled with blank fields is automatically inserted into the current position of the dataset.
After inserting the record, the next job is to assign strings to one or more of its fields. There are,
of course, several different ways to enter this data. Using the current program, you could simply
type the information into the current record in the grid. Or, if you wanted, you could place
standard edit controls on the form and then set each field equal to the value the user has typed
into the edit control:
Table1[‘Name’] := Edit1.Text;
If you place a table in edit mode, or if its TDataSource object has AutoEdit set to True, you can
use data-aware controls to insert information directly into a record.
The intent of this chapter, however, is to show you how to enter data programmatically.
Therefore you are presented with an example in which information is hardwired into the code
segment of the program:
Table1[‘Name’] := ‘Erehwon’;
NOTE
One of the interesting (or perhaps “frustrating” would be a more appropriate word)
byproducts of this technique is that pressing the Insert button twice in a row
automatically triggers a “Key Violation” exception. To remedy this situation, you must
either delete the current record or manually change the Name and Capital fields of the
newly created record.
Note also that you can pass either a string or a number into a field using this
technique. The array of Fields property is of type Variant, and so you can, to a
considerable degree, ignore type issues in this situation.
Looking at the code shown previously, you will see that the mere act of inserting a record and
of filling out its fields is not enough to change the physical data that resides on disk. If you
want the information to be written to disk, then you must call Post.
After calling Insert, if you change your mind and decide to abandon the current record, you
should call Cancel. As long as you do this before you call Post, everything you have entered is
discarded, and the dataset is restored to the condition it was in before you called Insert.
One last related property that you should keep in mind is called CanModify. A table might be
set to ReadOnly, in which case CanModify would return False. Otherwise CanModify returns True
and you can enter edit or insert mode at will. CanModify is itself a read-only property. If you
want to set a dataset to read-only, you should use the ReadOnly property, not CanModify.
In this section, you have learned how to use the Insert, Delete, Edit, Post, Cancel and Append
commands. Most of the actions associated with these commands are fairly intuitive, though it
can take a little thought to see how they interact with the Fields property.
FIGURE 15.14.
The SEARCH program
enables you to enter a
customer number and then
search for it by pressing a
button.
The functionality for the SEARCH program is encapsulated in a single method that is attached
to the Search button. This function retrieves the string entered in the edit control, searches the
CustNo column until it finds the value, and finally switches the focus to the record it found.
In its simplest form, here’s how the code attached to the Search button looks:
The first call in this procedure sets Table1 into search mode. Delphi needs to be told to switch
into search mode simply because you use the Fields property in a special way when Delphi is
in search mode. Specifically, you can index into the Fields property, and assign it to the value
you want to find.
In the example shown, the CustNo field is the first column in the database, so you set the Fields
index to zero. To actually carry out the search, simply call Table1.GotoKey. GotoKey is a Bool-
ean function, so you could write code that looks like this:
If not Table1.GotoKey then DatabaseError(‘’);
In this case, you set the Fields index to 3, since City is the fourth field in the Customer database.
Remember: this search will fail unless you first assign the correct value to the IndexName property.
Furthermore, you should note that IndexName is a property of TTable and would therefore not
automatically be included in any direct descendant of TDataSet or TDBDataSet that you might
create yourself.
When you are searching for a value in a database, there is always a strong possibility that the
search might fail. You can raise an exception in such a case and handle the error by writing the
code like this. Delphi will not automatically throw an exception in such a case, so if you want
to handle the error, you might write code that looks like this:
procedure TSearchDemo.SearchClick(Sender: TObject);
begin
Table1.SetKey;
try
Table1.Fields[0].AsString := Edit1.Text;
If not Table.GoToKey then raise DatabaseError.create(‘);
Table1.GotoKey;
except
on EDataBaseError do
MessageDlg(‘Value not found’, mtError, [mbOk], 0);
end;
end;
In the code shown, either an illegal assignment to the Fields property or a failure to find the
value on which you are searching would automatically force the code to pop up an error message
stating “Value not found.” For more information on exceptions, see Chapter 6, “Exceptions.”
The FindKey and FindNearest routines perform the same function GotoKey or GotoNearest, but
they are much easier to use. Here, for instance, is the technique for using FindKey:
Table1.FindKey([Edit1.Text]);
Run the program. When you type into the edit control, you will automatically begin
incrementally searching through the table. For instance, if you type in C, you will go to the
record for Canada, if you type in Cu, you will go to the record for Cuba.
The incremental search example is available on disc as the IncSrch program. It’s perhaps worth
pointing out that this program is interesting in part because it shows how you can use the built-
in features of Delphi to easily implement additional features that were never planned by the
developers. For instance, there is no Incremental Search component in Delphi. However, if
you need to build one, the tools come readily to hand.
FIGURE 15.15.
The RANGER program
shows how to limit the
number of records from a
table that are visible at any
one time.
The SetRangeStart and SetRangeEnd procedures enable you to declare the first and last mem-
bers in the range of records you want to see. To get started using the procedures, first double-
click the ApplyRange button, and then create a procedure that looks like this:
procedure TForm1.bApplyRangeClick(Sender: TObject);
begin
Table1.SetRangeStart;
Table1.Fields[0].AsString := Edit1.Text;
Table1.SetRangeEnd;
Table1.Fields[0].AsString := Edit2.Text;
Table1.ApplyRange;
end;
The bApplyRangeClick procedure first calls SetRangeStart, which puts the table into range mode
and blanks out the records seen in the TDBGrid control. Once in range mode, the program
next expects you to specify the beginning range, which in this case you grab from the first edit
control. Setting the end range for the program involves following a very similar pattern. First
you call SetRangeEnd, and then you snag an appropriate value from the second Edit control.
Note that you can use the Fields property to specify the actual range you want to use:
Table1.Fields[0].AsString := Edit2.Text;
This use of the Fields property is obviously a special case, since the syntax shown here is usually
used to set the value of a field, not to define a range. This special case comes into effect only
after you have put Table1 into range mode by calling SetRangeStart.
The final step in the procedure just shown is the call ApplyRange. This is the routine that actually
puts your request into effect. When the call to ApplyRange ends, the table is no longer in range
mode, and the Fields property returns to its normal functionality.
If you want to undo the results of a call to ApplyRange, then you can call the CancelRange func-
tion:
procedure TForm1.UndoClick(Sender: TObject);
begin
Table1.CancelRange;
end;
A typical run of the program might involve the user typing in the number 4000 in the first edit
control and the number 5000 in the next edit control. After entering the data, a click on the
ApplyRange button would then put the request into effect.
In this section, you have learned how to filter the functions from a table or dataset so that you
view only a particular range of records. The steps involved are threefold:
1. Call SetRangeStart and specify the beginning value in the range of records you want
to see.
2. Call SetRangeEnd and specify the ending value in the range of records you want to see.
3. Call ApplyRange in order to view the results of your request.
Delphi also provides a shorthand method calling the SetRangeStart, SetRangeEnd, and ApplyRange
methods:
procedure TForm1.ezApplyRangeClick(Sender: TObject);
begin
Table1.SetRange([Edit1.Text], [Edit2.Text]);
end;
This method performs exactly the same chore as the bApplyRangeClick method shown above.
Place a TCheckBox object on the panel and set its Caption to Filtered. Associate the following
method with the OnClick event for the checkbox:
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
Table1.Filtered := CheckBox1.Checked;
end;
This code ensures that the table will be filtered whenever the checkbox is checked.
Use the Fields Editor for the Table1 object to create field objects for all the fields in the database.
Drag the Continent field of the Fields Editor onto the form, as shown in Figure 15.16.
FIGURE 15.16.
The main form for the
Filter program includes a
grid, a panel, a checkbox, a
TDBEdit control, and a
button.
Turn to the Events page for the TTable object, and associate the following code with the
OnFilterRecord event:
procedure TForm1.Table1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
Accept := Table1[‘Continent’] = dbEdit1.Text;
end;
This code states that the OnFilterRecord event will accept all records where the Continent field
of the Country table equals the value in the dbEdit control. The continent field will have either
the value North America or South America in it. If you run the program, then view a record
that lists a country in South America, then the dbEdit control will say South America. If you
then click the checkbox to turn the filter on, you will see only the records from South America.
In short, the filter will automatically accept only those records whose Continent field matches
the value of the current record.
It’s important to note that the Accept field of the OnFilterRecord event is a Boolean value.
This means that you can set up any kind of a Boolean statement in order to set the value of this
field. For instance, in addition to the = operator, you could also use the following operators:
<>, > or <.
The FindNext, FindFirst, FindPrior, and FindLast functions are extremely easy to use. For
instance, if you wanted to find the next record in the database that satisfied the requirements
specified in the OnFilterRecord event, then you could write the following code to be fired in
response to a click on a button:
procedure TForm1.bFindNextClick(Sender: TObject);
begin
Table1.FindNext;
end;
The other functions work exactly the same way. This is a Boolean function that will return
False if the search fails. Remember that these methods work even if the Filtered property of
the TTable object is set to False.
NOTE
Bookmarks
It is often useful to mark a particular location in a table so that you can quickly return to it
when desired. Delphi provides this functionality through three methods that use the metaphor
of a bookmark. When you use these functions, it is as if you have left a bookmark in the dataset,
and you can therefore turn back to it quickly whenever you want:
procedure FreeBookmark(Bookmark: TBookmark);
function GetBookmark: TBookmark;
procedure GotoBookmark(Bookmark: TBookmark);
As you can see, the GetBookmark call returns a variable of type TBookmark. A TBookmark contains
enough information to enable Delphi to find the location to which it refers. Therefore, you
can simply pass this bookmark to the GotoBookmark function, and you will immediately be
returned to the location with which the bookmark is associated.
It’s important to note that a call to GetBookmark allocates memory for the bookmark, and so
you must remember to call FreeBookmark before you exit your program, and before every attempt
to reuse a bookmark. For instance, here is a typical set of calls for freeing, setting, and moving
to a bookmark:
procedure TForm1.MarkClick(Sender: TObject);
begin
if Bookmark = nil then
Bookmark := Table1.GetBookmark;
end;
The code shown here is excerpted from a program called BOOKMARK.DPR, which comes
with this book. In the declaration for TForm1, a variable called BookMark is declared in the private
section. Every time the MarkClick procedure is called, the first step is to be sure the BookMark is
freed. It is never a mistake to call FreeBookmark, as the procedure checks to make sure BookMark
is not set to nil. After de-allocating any existing copies of the Bookmark, a new one is allocated.
You can then call GotoBookmark and repeat the cycle.
Bookmarks are powerful features that can be of great benefit under certain circumstances. The
developers of Delphi, for instance, used bookmarks frequently in order to develop the database
components. They often have several different bookmarks open at the same time.
NOTE
Most of the features surfaced in TDataSet are built into the BDE. For instance, filters,
searching for keys, and bookmarks are easily available to anyone who uses the BDE.
What the developers of Delphi have done is surface these features so that they can be
easily accessed using object-oriented programming techniques.
TTable
TTable adds several frequently used properties to TDataSet:
property DetailFields
property Exclusive
property MasterFields
property MasterSource
property ReadOnly
property TableName
Of the properties shown here, the most common ones are probably TableName and ReadOnly.
You can use the TableName property to specify the table you want to open, and you can set the
ReadOnly property to True or False depending on whether you want to allow the user to change
the data in a dataset. Neither of these properties can be used when a table is active.
The Exclusive property lets you open up a table in a mode that guarantees that no other user
will be able to access it at the same time. You will not be able to set Exclusive to True if another
user is currently accessing the table.
The MasterSource property is used to specify a TDataSource from which the current table needs
to obtain information. For instance, if you linked two tables in a master/detail relationship,
the detail table can track the events occurring in the first table by specifying the first table’s
datasource in this property. This technique is demonstrated in the following section on linked
cursors.
The LINKS program on your disc demonstrates how to create a program that uses linked cur-
sors. To create the program on your own, place two tables, two datasources, and two grids on
a form. Wire the first set of controls to the CUSTOMER table and the second set to the
ORDERS table. If you run the program at this stage, you should be able to scroll through all
the records in either table, as shown in Figure 15.17.
FIGURE 15.17.
The LINKS program shows
how to define a relationship
between two tables.
The next step is to link the ORDERS table to the CUSTOMER table so that you view only
those orders associated with the current customer record. To do this, you must take three steps,
each of which requires some explanation:
1. Set the MasterSource property of Table2 to DataSource1.
2. Set the MasterField property in Table2 to CustNo
3. Set the IndexName property of Table2 to ByCustNo.
If you now run the program, you will see that both tables are linked together, and that every
time you move to a new record in the CUSTOMER table, you can see only those records in
the ORDERS table that belong to that particular customer.
The MasterSource property in Table2 specifies the DataSource from which Table2 can draw
information. Specifically, it allows the ORDERS table to know which record currently has the
focus in the CUSTOMERS table.
The question then becomes this: what other information does Table2 need in order to properly
filter the contents of the ORDERS table? The answer to this question is twofold:
1. It needs the name of the field that links the two tables.
2. It needs the index of the field in the ORDERS table that is going to be linked to the
CUSTOMER table.
In order to correctly supply the information described here, you must first ensure that both the
CUSTOMER table and the ORDERS table have the correct indices. Specifically, you must
ensure that there are indices on both the CustNo field and the CustNo field in the ORDERS
table. If the index in question is a primary index, there is no need to specifically name that
index, and therefore you can leave the IndexName field blank in both tables. However, if ei-
ther of the tables is linked to the other through a secondary index, you must explicitly desig-
nate that index in the IndexName field of the table that has a secondary index.
In the example shown here, the CUSTOMER table has a primary index on the CustNo field,
so there is no need to specify the index name. However, the ORDERS table does not have a
primary index on the CustNo field, and so you must explicitly declare it in the IndexName
property by typing in or selecting the word CustNo.
NOTE
To simplify the process described above, the developers put in a dialog that appears
when you click the MasterFields property. This dialog simplifies the process described
here and helps to automate the task of setting up a link between two tables.
In particular, to use the MasterFields dialog, start a new project and drag the Customer
and Orders tables off the Explorer onto the main form. Arrange the grids with the
Customer grid on top and the Orders grid beneath it. Set the DataSource property of
the Orders TTable object to the TDataSource object associated with the Customer
table.
Pop up the MasterFields dialog and make sure the Available Indexes is set to Primary
Index. Click the CustNo field in both Detail Fields and MasterFields list boxes. Click
Add. The two fields will appear in the Joined Fields list box. At this stage, you are all
done, and so you can click the OK button.
Some indices can contain multiple fields, so you must explicitly state the name of the field you
want to use to link the two tables. In this case, you should enter the name CustNo in the
MasterFields property of Table2. If you wanted to link two tables on more than one field, you
should list all the fields, placing a pipe symbol between each one:
Table1.MasterFields := ‘CustNo | SaleData | ShipDate’;
In this particular case, however, the statement shown here makes no sense, since the SaleData
and ShipDate fields are neither indexed nor duplicated in the CUSTOMER table. Therefore,
you should only enter the field called CustNo in the MasterFields property. You can specify
this syntax directly in a property editor, or else write code that performs the same chore.
It’s important to note that this section covered only one of several ways you can create linked
cursors using Delphi. Chapter 16, “SQL and the TQuery Object,” describes a second method
that will appeal to people who are familiar with SQL. The Database Expert provides a third
means of achieving this end. As you have seen, the Database Expert is an easy-to-use visual
tool. The Query Builder is yet a fourth way of creating a one-to-many relationship between
two tables. Like the Database Expert, the Query Builder is a visual tool that can save you much
time. However, it’s best not to rely entirely on the visual tools, since there are times when you
might feel limited by their functionality.
TDataSource Basics
Class TDataSource is used as a conduit between TTable or TQuery and the data-aware con-
trols such as TDBGrid, TDBEdit, and TDBComboBox. Under most circumstances, the only
thing you will do with a TDataSource object is to set its DataSet property to an appropriate
TTable or TQuery object. Then, on the other end, you will also want to set a data-aware control’s
DataSource property to the TDataSource object you are currently using.
NOTE
Visual tools such as TDBEdit or TDBGrid all have a DataSource property that
connects to a TDataSource object. When reading this chapter, you need to distinguish
between a visual control’s DataSource property and the TDataSource object to which it
is attached. In other words, the word DataSource can refer to either a property or a
class, depending on the context. I tend to refer to the class as a TDataSource and the
property as a DataSource, but you should watch the context in which these words are
used.
A TDataSource also has an Enabled property, and this can be useful whenever you want to
temporarily disconnect a table or query from its visual controls. This functionality might be
desirable if you need to programmatically iterate through all the records in a table. For instance,
if a TTable is connected to a data-aware control, each time you call TTable.Next, the visual
control needs to be updated. If you are quickly going through two or three thousand records,
then it can take a considerable time to perform the updates to the visual controls. In cases like
this, the best thing to do is set the TDataSource object’s Enabled field to False, which will
allow you to iterate through the records without having to worry about screen updates. This
single change can improve the speed of some routines by several thousand percent.
The AutoEdit property of TDataSource enables you to decide whether or not the data-aware
controls attached to it will automatically enter edit mode when you start typing inside them.
Many users prefer to keep AutoEdit set to True, but if you want to give a user more precise
control over when the database can be edited, this is the property you need. In short, if you set
AutoEdit to False, you have essentially made the table read-only.
OnDataChange occurs whenever you move on to a new record. In other words, if you call Next,
Previous, Insert, or any other call that is likely to lead to a change in the data associated with
the current record, an OnDataChange event will get fired. If someone begins editing the data in
a data-aware control, an OnResync event occurs.
A TDataSource OnStateChange event occurs whenever the current state of the dataset changes.
A dataset always knows what state it’s in. If you call Edit, Append, or Insert, the table knows
that it is now in edit mode. Similarly, after you Post a record, the database knows that it is no
longer editing data, and it switches back into browse mode. If you want more control, the next
section in this chapter explains that a dataset also sends out messages just before and just after
you change states.
The dataset has five different possible states, each of which are captured in the following
enumerated type:
TDataSetState = (dsInactive, dsBrowse, dsEdit,
dsInsert, dsSetKey, dsCalcFields);
During the course of a normal session, the database will frequently move back and forth between
browse, edit, insert, or the other modes. If you want to track these changes, you can respond to
them by writing code that looks like this:
procedure TForm1.DataSource1StateChange(Sender: TObject);
var
S: String;
begin
case Table1.State of
dsInactive: S := ‘Inactive’;
dsBrowse: S := ‘Browse’;
dsEdit: S := ‘Edit’;
dsInsert: S := ‘Insert’;
dsSetKey: S := ‘SetKey’;
dsCalcFields: S := ‘CalcFields’;
end;
StatusBar1.Panels[0].Text := S;
end;
An OnUpdateData event occurs whenever the data in the current record is about to be updated.
For instance, an OnUpdateEvent will occur between the time Post is called and the time
information is actually posted.
NOTE
The above code assumes that you have dropped one of the Windows 95 TStatusBar
objects on the form. To create one or more Panels in the StatusBar, use the Panels
property of the TStatusBar object. In the Statusbar Panels Editor dialog, click New to
create a new panel.
The events belonging to TDataSource can be extremely useful. To help illustrate them, you will
find a program on your disc called DSEVENTS that responds to all three TDataSource events.
This program shows an easy way to set up a “poor man’s” data-aware edit control that
automatically shows and posts data to and from a database at the appropriate time.
This example works with the COUNTRY database, and it has a TTable, TDataSource, five
edits, six labels, eight buttons, and a panel on it. The actual layout for the program is shown in
Figure 15.18. Note that the sixth label appears on the panel located at the bottom of the main
form.
FIGURE 15.18.
The DSEVENTS program
shows how to track the
current state of a table.
DSEVENTS has one small conceit that you need to understand if you want to learn how the
program works. Because there are five separate edit controls on the main form, you need to
have some way to refer to them quickly and easily. One simple method is to declare an array of
edit controls:
Edits: array[1..5] of TEdit;
To fill out the array, you can respond to the forms OnCreate event:
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
for i := 1 to 5 do
Edits[i] := TEdit(FindComponent(‘Edit’ + IntToStr(i)));
end;
The code shown here assumes that the first edit control you want to use is called Edit1, and the
second is called Edit2, and so on.
Given the existence of this array of controls, it is very simple to use the OnDataChange event to
keep them in sync with the contents of the current record in a dataset:
procedure TForm1.DataSource1DataChange(Sender: TObject;
Field: TField);
var
i: Integer;
begin
for i := 1 to 5 do
Edits[i].Text := Table1.Fields[i - 1].AsString;
end;
This code iterates through each of the fields of the current record and puts its contents in the
appropriate edit control. Whenever Table1.Next is called, or whenever any of the other
navigational methods are called, the procedure shown previously is called. This assures that the
edit controls always contain the data from the current record.
Whenever Post gets called, you will want to perform the opposite action. That is, you will want
to get the information from the edit controls and place it inside the current record. To perform
that action, simply respond to TDataSource.OnUpdateData events, which are generated
automatically whenever Post is called:
procedure TForm1.DataSource1UpdateData(Sender: TObject);
var
i: Integer;
begin
for i := 1 to 5 do
Table1.Fields[i - 1].AsString := Edits[i].Text;
end;
The DSEVENTS program automatically switches into edit mode whenever you type anything
in one of the edit controls. It does this by responding to OnKeyDown events:
begin
if DataSource1.State <> dsEdit then
Table1.Edit;
end;
This code shows how you can use the State variable of a TDataSource object to find out the
current mode of the dataset.
Most of these properties are self-explanatory. The BeforePost event, for instance, is functionally
similar to the TDataSource.OnUpdateData event that is explained and demonstrated previously.
In other words, the DSEVENTS program would work the same if you responded to
DataSource1.OnUpdateData or to Table1.BeforePost. Of course, in one case you would not need
to have a TDataSource on the form, while the other requires it.
Summary
In this chapter, you have learned how to use the TDataSet, TField, TDBDataSet, TTable, and
TDataSource classes. Through the simple techniques of exposition, example, and repetition,
all the major ideas associated with objects of these types should now be clear to you.
The key points to remember are as follows:
■ TDataSet encapsulates the basic functions you will perform on a table.
■ TField is a property of TDataSet that allows you to access the contents or name of
each field in a record.
■ TDBDataSet gives you the ability to associate a dataset with a given table.
■ TTable encapsulates all the functionality of a dataset, but it also gives you access to
table-specific chores such as setting indices or creating linked cursors.
■ TDataSource forms a link between TTable or TQuery and any of the data-aware
components such as TDBEdit or TDBGrid. TDataSource also contains three useful
events that keep you informed about the current state of the database.
In the next chapter, you will learn about the TQuery object and SQL. SQL is especially useful
when you want to access the advanced capabilities associated with servers and server data.
This chapter is about queries. It’s a subject that lies at the heart of client/server programming,
so this is one of the most important chapters in the book.
The material will be broken down into the following main sections:
■ Using the TQuery object
■ Using SQL with local and remote servers to select, update, delete, and insert records
■ Using SQL statements to create joins, linked cursors, and programs that search for
individual records
The acronym SQL stands for Structured Query Language and is usually pronounced as
“Sequel” or by saying each letter (“Ess Que El”). Whatever way you choose to pronounce it,
SQL is a powerful database language that is easily accessible from within Delphi but is distinct
from Delphi’s native language. Delphi can use SQL statements to view tables, to perform joins
between tables, to create one-to-many relationships, or to perform almost any action that your
underlying database tools can perform.
Delphi ships with two local SQL engines, one built into the BDE and the other built into
InterBase. As a result, you can perform SQL queries even if you’re working on a stand-alone
machine and don’t have access to a server.
Delphi provides support for pass-through SQL, which means that you can compose SQL state-
ments and then have them sent directly (with one or two exceptions) to an Oracle, Sybase,
InterBase, or other server. Pass-through SQL is a powerful feature for two different reasons:
1. Most servers can process SQL statements very quickly, which means that you can use
SQL on remote data to get an extremely fast response to your requests.
2. You can compose SQL statements that ask a server to perform specialized tasks
unavailable through Delphi’s native language.
In the last chapter, you learned a lot about how Delphi works internally and about how to
utilize its native capabilities. Now it’s time to see how Delphi interacts with the database tools
that exist either on your current machine or on a network.
TQuery Basics
You can create a Delphi SQL statement by using a TQuery component in the following
manner:
1. Drop down a TQuery, TDataSource, and TdbGrid object on a form and wire them
together.
2. Assign an alias to the DatabaseName property of the TQuery object. For instance, use
the DBDEMOS alias.
3. Use the SQL property to enter a SQL statement such as Select * from Country.
4. Set the Active property to True. If you completed each step correctly, and if the BDE
is set up correctly, the grid should now contain the records from the Country table.
If you’re working with local data, you can substitute a fully qualified subdirectory path for an
alias. When using the latter method, it’s best if you don’t include the actual name of a table,
but only the subdirectory in which one or more tables exist. In my opinion, however, it is al-
most always better to work with an alias rather than specify the path directly in the DatabaseName
property.
There are two general points that you need to understand before you proceed further:
■ This chapter isn’t intended to be a SQL primer, but rather a description of the
TQuery object and the basic tasks you can perform with it. Even if you don’t know
anything about SQL, this chapter will still be helpful to you, and you’ll end up
learning a number of basic facts about how to compose a SQL statement. However,
for a detailed analysis of the language, you should turn to one of the many books and
public documents available on this subject. You also can refer to the handy reference
in the online help for the WISQL utility. Additional information is available in the
form of a LOCALSQL.HLP file that ships with Delphi. (Open help, press Alt+F+O,
and then choose LOCALSQL.HLP. You need to be in a help file for this work, not in
the Index or Content section. Also, you may have to browse to the ..\delphi 2.0\help
subdirectory to find this file.)
■ Because Delphi uses pass-through SQL, you’ll find that you can access the unique
features of the servers to which you connect. For instance, the Borland Database
Engine, which ships with Delphi, provides a special, if somewhat limited, local SQL
engine. To learn about the specific capabilities of that engine, you should read not
only this chapter, but also the LOCALSQL.HLP file that is included with Delphi.
Overall, you’ll find that the TQuery object is one of the most useful and flexible features avail-
able inside the Delphi environment. With it, you can tap into the power inherent in some of
today’s premier servers such as Interbase, Oracle, or Sybase.
times. The most efficient, most powerful, and simplest means of doing this is through param-
eterized queries, which are explained in the next section. However, this chapter first examines
the basic features of the SQL property and then covers more advanced topics, such as param-
eterized queries.
The SQL property is of type TStrings, which means that it is a series of strings kept in a list. The
list acts very much as if it were an array, but it’s actually a special class with its own unique
capabilities. If you want to find out everything you can about the SQL property, you should
study the class TStrings or TStringList. However, the next few paragraphs review its most
commonly used features.
When using TQuery programmatically, you should first close the current query and clear out
any strings that might already be residing in the SQL property:
Query1.Close;
Query1.SQL.Clear;
It’s always safe to call Close. If the query is already closed, the call will not cause an error.
The next step is to add the new strings that you want to execute:
Query1.SQL.Add(‘Select * from Country’);
Query1.SQL.Add(‘where Name = ‘’Argentina’’’);
You can use the Add property to append from one to x number of strings to an SQL query,
where x is limited only by the amount of memory on your machine.
NOTE
For those not familiar with Object Pascal strings, I should point out that this code
specifies that there should be single quotes around the word Argentina. Argentina is a
string within a string, so it is necessary to add double single quotes each time you want
to enter single quotes inside a string.
You can, if you want, set the Argentina off with double quotes:
Query1.SQL.Add(‘where Name = “Argentina”’);
This is probably the preferred method, but you should also be aware of how to handle
single quotes in an Object Pascal string.
To ask Delphi to process the statement and return a cursor containing the results of your query,
you can issue the following statement:
Query1.Open;
Whenever you want to change the SQL statement, you can simply alter the strings you pass to
the Add property:
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add(‘Select * from Country’);
Query1.Open;
The sample program called EASYSQL demonstrates this process. EASYSQL is shown in Fig-
ure 16.1.
FIGURE 16.1.
The EASYSQL program
shows how to issue multiple
queries from a single
TQuery object.
The EASYSQL program uses a feature of local SQL that lets you use case-insensitive wild cards.
For instance, the following SQL statement returns a dataset containing all the records in which
the Name field begins with the letter C:
Select * from Country where Name like ‘C%’
The following syntax enables you to see all the countries that have the letter C embedded some-
where in their name:
Select * from Country where Name like ‘%C%’;
Here’s a statement that finds all the countries whose name ends in the letters ia:
Select * from Country where Name like ‘%ia’;
If you want to compose a series of statements like the preceding one, you can expedite matters
by using either parameterized queries or the Format function. Both of these techniques will be
explained in this chapter.
One of the most powerful features of the SQL property is its capability to read files directly from
disk. This feature is also demonstrated in the EASYSQL program.
Here’s how it works. In the EASYSQL subdirectory, there are several files with the extension
SQL. These files contain SQL statements such as the ones shown previously. The EASYSQL
program has a Load button that enables you to select one of these text files and then run the
SQL statement stored in that file. Be sure that your DatabaseName property for your TQuery
object is assigned an alias before you try this code. In particular, I am working with the
DBDEMOS alias in all these examples.
The Load button has the following response method for its OnClick event:
procedure Form1.LoadClick(Sender: TObject);
begin
if OpenDialog1.Execute then begin
Query1.Close;
Query1.SQL.LoadFromFile(OpenDialog1.FileName);
Query1.Open;
end;
end;
The LoadClick method first loads the OpenDialog component and enables the user to select a
file with an SQL extension. The code checks to see whether the user has selected a file. If a file
has been selected, the current query is closed, and the selected file is loaded from disk and dis-
played to the user.
OpenDialog1 has its Filter property set to the following value:
OpenDialog1.Filter := ‘SQL(*.SQL)|*.SQL’
As a result, it lists only files that have an SQL extension, as shown in Figure 16.2.
FIGURE 16.2.
The Open dialog from the
EASYSQL program enables
you to select a prepared
SQL statement from an
ASCII file stored on disk.
The LoadFromFile procedure enables you to load an entire text file at runtime by issuing a single
command. The trick, then, is to store SQL statements in text files and load them at runtime.
Because the SQL property can contain an essentially unlimited number of strings, there is no
practical limit to the size of the SQL statement that you could load in this fashion. You can use
this technique to quickly execute a series of very complex SQL statements.
In this section, you have seen two methods of changing the SQL property at runtime. The first
technique enables you to add strings to the SQL property, run a query, change the strings, and
run the query again. The second technique enables you to load one or more statements from a
file. The LoadFromFile technique is obviously quite elegant. The first technique can be very
powerful at times, but it can be a bit awkward if all you want to do is change one word in a
SQL statement. In the next section, you’ll learn about how you can eliminate this awkward-
ness by using parameterized queries.
To turn this statement into a parameterized query, just replace the right side of the like clause
with a variable called NameStr:
select * from County where Name like :NameStr
In this SQL statement, NameStr is no longer a predefined constant, but instead can change at
either design time or runtime. The SQL parser knows that it is dealing with a parameter in-
stead of a constant because a colon is prepended to the word NameStr. That colon tells Delphi
that it should substitute the NameStr variable with a value that will be supplied at some future
point.
It’s important to note that the word NameStr was chosen entirely at random. You can use any
valid variable name in this case, just as you can choose a wide range of identifiers when you
declare a string variable in one of your programs.
There are two ways to supply variables to a parameterized SQL statement. One method is to
use the Params property to supply the value at runtime. The second is to use the DataSource
property to supply information from another dataset at either runtime or design time. Here
are the key properties used to accomplish these goals:
property Params[Index: Word];
function ParamByName(const Value: string);
procedure Prepare
When you substitute bind variables in a parameterized query by using the Params property,
you usually take four steps:
1. Make sure the table is closed.
2. Ready the Query object by issuing the Prepare command (optional, but highly
recommended).
3. Assign the correct values to the Params property.
4. Open the query.
Here’s a sample code fragment showing how this might be done in practice:
Query1.Close;
Query1.Prepare;
Query1.Params[0].AsString := ‘Argentina’;
Query1.Open;
If you’re not familiar with parameterized queries, the preceding code might appear a bit mys-
terious. To understand it thoroughly, you’ll need to do a careful line-by-line analysis. The sim-
plest way to begin is with the third line because it is the Params property that lies at the heart of
this process.
Params is an indexed property that uses a syntax similar to the Fields property from TDataSet.
For instance, you can access the first bind variable in a SQL statement by referring to element
0 in the Params array:
Params[0].AsString := ‘Argentina’;
with the Params statements shown previously, the end result is the following SQL statement:
select * from Country where Name like ‘Argentina’
What’s happened here is that the variable :NameStr has been assigned the value Argentina by
the Params property, thereby enabling you to complete a simple SQL statement.
If you have more than one parameter in a statement, you can access them by changing the index
of the Params property:
Params[1].AsString := ‘SomeValue’;
So far, you’ve seen that a parameterized query uses bind variables, which always begin with a
colon, to designate the places where parameters will be passed. With this concept in mind, you
can move on to the other lines in the previous code fragment.
Before you use the Params variable, you should first call Prepare. A call to Prepare causes Delphi
to parse your SQL statement and ready the Params property so that it’s prepared to accept the
appropriate number of variables. If you try to assign a value to the Params variable without first
calling Prepare, your code will still work, but the routine may not be as highly optimized. There
is also an UnPrepare statement that you should use if you are very concerned about taking up
database resources.
After you’ve called Prepare and assigned the correct values to the Params variable, you should
call Open to complete the binding of the variables and produce the dataset that you hope to
find. In this particular case, given the input shown previously, the dataset includes the con-
tents of the record where the name field is set to Argentina.
In the examples subdirectory, you’ll find a program called EASYSQL2 that demonstrates how
to use parameterized queries. The EASYSQL2 program performs a function very similar to the
one shown earlier in the first EASYSQL program. However, this new version shows how pa-
rameterized queries can be used to increase the flexibility of a SQL statement.
To create the program, place TQuery, TDataSource, TdbGrid, and TTabSet components on
a form. Hook up the data controls and set the query’s DatabaseName property to the demos
subdirectory in which the COUNTRY table is found. Fill in the tabset so that it lists the al-
phabet from A to Z, as shown in Figure 16.3.
FIGURE 16.3.
The EASYSQL2 program
shows how to use
parameterized queries.
Enter the following string in the SQL property for the query component:
select * from Country where Name like :NameStr
Now all that’s left to create is a response method for the OnChange property of the tabset:
procedure TForm1.TabSet1Change(Sender: TObject;
NewTab: Integer;
var AllowChange: Boolean);
var
S: String;
begin
S := TabSet1.Tabs.Strings[NewTab] + ‘%’;
Query1.Close;
Query1.Prepare;
Query1.Params[0].AsString := S;
Query1.Open;
end;
The code shown here follows the four simple steps outlined previously. This is what the code
does:
1. Closes the query.
2. Prepares the Params property.
3. Assigns a string to the Params property.
4. Executes the resultant SQL statement by calling Query1.Open.
The actual string assigned to the Params property consists of one of the letters of the alphabet
plus the % symbol. A typical query produced by this method might look like this:
Select * from Country where Name like ‘C%’
The end result, then, is that the EASYSQL2 program lets you view the contents of the table in
alphabetical sequence. Press the tab labeled A, and you see only those records in the database
for which the first letter of the Name field begins with an A. Press the B tab, and you see only
those items with a first letter of B.
The important point, of course, is that you were able to produce the previous program by writing
only five lines of code:
S := TabSet1.Tabs.Strings[NewTab] + ‘%’;
Query1.Close;
Query1.Prepare;
Query1.Params[0].AsString := S;
Query1.Open;
This combination of SQL and Delphi’s native language provides maximum power and flex-
ibility when you want to produce your own applications.
NOTE
In the last chapter, I showed you how to write this same type of program using the
TTable object rather than the TQuery object. The question then becomes, which one
is better?
Well, there is no definitive answer to this question. If you come from the client/server
world and have been writing SQL statements for years, you will almost certainly prefer
the TQuery object. If you come from the Turbo Pascal or C++ world, you will
probably prefer the TTable component because you are likely to find SQL awkward to
write, at least at first.
Ultimately, I can only call them as I see them. I have to confess that after considerable
experience trying both methods, I tend to use the TTable object unless I’m forced into
a corner and have to use the TQuery object. In some cases, I use a combination of the
TTable and TStoredProc object and add in a TQuery object only when absolutely
necessary.
Just so I don’t mislead anyone, I should emphasize that a stated preference for the
TTable object over the TQuery object is likely to be greeted with boos and hisses from
an experienced client/server-oriented crowd. I honestly don’t know whether this
preference is just machismo, or perhaps a failure to appreciate the true power of the
TTable object, or whether it is grounded in the bitter fruit of experience. Certainly it
would be a mistake to read this note as a ringing endorsement of the TTable object
over the TQuery object. I am merely mentioning the two opposing viewpoints on this
issue and expressing a very tentative personal opinion on this matter.
Finally, I should add that no one can hope to do any serious contemporary database
development without understanding something about SQL. SQL is a vital part of the
client/server world, and if you don’t understand how to use the TQuery object, your
viability as a professional client/server programmer would be seriously, perhaps
hopelessly, impaired.
Further examples of parameterized queries are found on the CD that accompanies this book as
PARAMS2 and PARAMS3. The PARAMS2 program is particularly interesting because it shows
how to work with two parameterized variables at once.
To create the PARAMS2 program, drop a query, datasource, and dbgrid on a form, and place
two list boxes and TdbImage above the grid, as shown in Figure 16.4. Use TLabel objects to put
the word Size above the first list box, and the word Weight above the second list box. Set the
DataSource property of the TdbImage control to DataSource1, and type the word BMP in the editor
for its DataField property.
FIGURE 16.4.
The form for the
PARAMS2 program, as it
appears at design time.
The SQL statement used in the PARAMS2 program looks like this:
select * from Animals
where
Animals.”Size” > :Size and
Animals.”Weight” > :Weight
To satisfy the two parameters specified in this SQL statement, you should create the following
method:
procedure TForm1.ListBox1Click(Sender: TObject);
begin
Query1.Close;
Query1.Prepare;
Query1.Params[0].AsInteger :=
strtoInt(ListBox1.Items.Strings[ListBox1.ItemIndex]);
Query1.Params[1].AsInteger :=
strtoInt(ListBox2.Items.Strings[ListBox2.ItemIndex]);
Query1.Open;
end;
The OnClick events for both list boxes should be set to the previous routine.
When you run the PARAMS2 program, both list boxes are automatically filled with numbers
that range from 0 to 42. By selecting a value from the first list box, you specify the size of the
animal you want to find. By selecting one from the second list box, you select its weight. Using
both values together, you are able to resolve both parameterized variables, thereby effectively
selecting a range of animals to view. For instance, select 4 in the Size list box, and then iterate
through the choices 2, 4, 6, 8, and 10 in the Weight list box.
As a final touch, the PARAMS2 program displays a picture of the animals in question in the
TdbImage control. The blob field of the table that contains the picture is called BMP. The
TdbImage control asks only that you set its DataSource property to a valid TDataSource object
and its DataField property to the name of the blob field you want to display. In this case, the
DataSource is DataSource1, and the blob field is called BMP. (See Listing 16.1.)
Listing 16.1. The PARAMS2 program shows how to work with a parameterized
query that has two fields.
unit Main;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
DB, StdCtrls, dbTables,
DBGrids, DBCtrls, Grids;
type
TForm1 = class(TForm)
Query1: TQuery;
DataSource1: TDataSource;
ListBox1: TListBox;
DBGrid1: TDBGrid;
ListBox2: TListBox;
Label1: TLabel;
Label2: TLabel;
DBImage1: TDBImage;
procedure ListBox1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
SysUtils;
{$R *.DFM}
end.
There are definitely times when you might want to parameterize the last word in this state-
ment so that it could vary over the life of a program:
Select * from :ACountry
Unfortunately, most SQL servers won’t support this syntax, so you’re forced to find another
solution.
At times like these, the Format function can come to the rescue. The Format function works a
lot like the printf function found in the C language. A detailed explanation of this function is
available in Chapter 2, “Delphi Types Under Win32;” for now, all you really need to know
about it is that it enables you to substitute variables of almost any type for certain words in a
string. More specifically, you can compose a string that looks like this:
S := ‘Select * from %s’;
In this string, the syntax %s performs the same role that the :FileName syntax does in a param-
eterized query. The one difference, of course, is that you should only use %s when you’re work-
ing with a string. If you’re working with an integer, use %d. Once again, you should refer to
Chapter 3 or the online documentation if you want more details on how this function works.
The second parameter passed to Format is an array of values separated by commas. To start an
array, use an open bracket. Then include a series of values and mark the end of the array with
a closed bracket:
Format(S, [value1, value2]);
Given the preceding code, if Args were a string set to the value Country, after the Format func-
tion was executed you would end up with the following string in the variable S:
‘Select * from Country’
Needless to say, this was exactly what you hoped to achieve, and the Format function enables
you to reach your goal without any of the restrictions placed on parameterized variables.
Of course, this example was fairly simplistic, but if you wanted, you could create a string that
looks like this:
S = ‘Select * from %s where %s = %d’;
This string contains three variables that can be changed at runtime, and it should give you some
hints as to the kind of flexibility you can achieve using this system. For instance, you could
write code that looks like this:
function GetQuery(S1, S2: string; Value: Integer): string;
begin
Result := Format(‘Select * from %s where %s = %d’, [S1, S2, Value]);
end;
After substitutions are made, this sets the Caption of Form1 to the following string:
select * from Customer where CustNo = 42.
To see this entire process in action, refer to the PARAMS1 program in the CHAP16 subdirectory
on the CD-ROM that comes with this book. This program, shown in Listing 16.2, lets you
pick from a list of tables and display the contents of each table in a data grid.
Listing 16.2. The PARAMS1 program shows how to use the Format function with
an SQL query.
unit Main;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
DB, DBGrids, StdCtrls,
DBTables, Grids;
const
BaseStr = ‘Select * from %s’;
type
TForm1 = class(TForm)
Query1: TQuery;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
ListBox1: TListBox;
procedure ListBox1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
continues
implementation
uses
StrBox,
SysUtils;
{$R *.DFM}
end.
To create the PARAMS1 program, place a query on the form, and set its DatabaseName prop-
erty to DBDEMOS. To create the list of tables, place a TListBox object on the form and create
the following FormCreate method:
procedure TForm1.FormCreate(Sender: TObject);
begin
Session.GetTableNames(Query1.DatabaseName, ‘’,
False, False, ListBox1.Items);
end;
The call to Delphi’s built-in GetTableNames routine returns a complete list of valid table names
from the database specified in the first parameter. The second parameter is a string that can
contain a file mask, if you so desire. For instance, you can enter c*.* to get a list of all tables
beginning with the letter C. The fourth parameter is a Boolean value that specifies whether
you want to work with system tables, and the final parameter is a value of type TStrings that
holds the output from the function.
To enable the user to view the contents of the tables listed in the FormCreate method, you should
add a TDataSource and TdbGrid to the form, and then wire them up.
At the very top of the unit that holds your main form, you should declare a string constant:
const
BaseStr = ‘Select * from %s’;
The first line of the code shown here assigns a string the value from the currently selected item
from a list box.
The next line of code checks to make sure that the query is closed:
Query1.Close
The next line then clears out any strings currently sitting in the SQL property:
Query1.SQL.Clear;
The next line in the program adds a new SQL statement to the query. To do this, it calls on the
Format function, the BaseStr constant, and the string selected from the list box. The result is a
new SQL statement that requests a dataset containing the contents of a table. For example, the
string might look like this:
select * from Orders
The PARAMS1 program, shown in Listing 16.2, demonstrates how to use the Format func-
tion in lieu of parameterized queries. The Format function is useful because it is more flexible
than parameterized queries.
Later in this chapter, you’ll revisit this program again to see how you can automatically list all
the tables in a particular database or subdirectory.
All you have to do is ensure that the dataset is connected to a datasource, and then you’re free
to make the link.
In the following explanation, assume that you want to create a link between the ORDERS
table and the CUSTOMERS table, so that whenever you view a particular customer record,
only the orders associated with that customer will be visible.
Consider the following parameterized query:
Select * from Orders where CustNo = :CustNo
In this statement, :CustNo is a bind variable that needs to be supplied a value from some source.
Delphi enables you to use the TQuery.DataSource field to point at another dataset, which can
supply that information to you automatically. In other words, instead of being forced to use
the Params property to manually supply a variable, the appropriate variable can simply be plucked
from another table. Furthermore, Delphi always first tries to satisfy a parameterized query by
using the DataSource property. Only if that fails does it expect to get the variable from the Params
property.
Take a moment to consider exactly what happens in these situations. As you saw in the last
chapter, the CustNo field forms a link between the ORDERS table and the CUSTOMER table.
Therefore, if both tables are visible on a form, the appropriate CustNo value is always available
in the current record of the Customer table. All you need to do is point the query object in the
appropriate direction.
To obtain the bind value, just set the DataSource for the Query object to the TDataSource ob-
ject that’s associated with the CUSTOMER table. That’s all there is to it! Just enter a short
SQL statement, then link up the DataSource property, and Bingo! You’ve established a one-to-
many relationship like the linked cursors example from the last chapter!
On the CD that accompanies this book, you’ll find an example called QLINKS that demon-
strates how this technique works. To create the QLINKS program, place two TQuery, two
TDataSource, and two TdbGrids on a form, as shown in Figure 16.5.
FIGURE 16.5.
The QLINKS program
shows how to create a one-
to-many relationship using
the TQuery object.
In the SQL property for the first TQuery component, enter the following:
select * from Customer
To complete the program, all you have to do is wire up the controls by attaching dbGrid1 to
DataSource1 and DataSource1 to Query1. Perform the same action for the second set of con-
trols, and then set the Query2.DataSource property to DataSource1. This last step is the main
action that forms the link between the two tables. If you now run the program, you’ll see that
the two tables work together in the desired manner.
If you want to create a link between two tables using multiple fields, you can simply specify the
relevant fields in your query:
select * from Orders
where CustNo = :CustNo and
CustCountry = :CustCountry
The important point to understand is that this one-to-many example works simply because
Delphi supports parameterized variables. There is no other hand-waving going on in the back-
ground. All that’s happening is that you’re using a basic SQL statement to view the members
of the ORDERS table that happen to have a particular customer number. The customer num-
ber in question was passed to you through the DataSource property and the bind variable you
created.
The examples you’ve seen so far in this chapter should give you some feeling for the extreme
power and flexibility inherent in the TQuery object. If you’re looking for a lever powerful enough
to move the roadblocks in your client/server programming world, TQuery is likely to be the
tool you require.
NOTE
The RequestLive field of the TQuery object can play a very important role in SQL
programming. By default, any query you make with the TQuery object will return a
read-only dataset. However, you can attempt to get a live query by setting the TQuery
RequestLive property to True. At the time of this writing, it is not clear exactly when
you will be able to get live datasets back from a query. As a rule, if your query involves
only one table, then you can set RequestLive to True. If your query involves multiple
tables, then setting RequestLive to True might not produce the desired result. You can
check the CanModify property to see if your request has succeeded.
In general, I use the TTable object rather than the TQuery object when I want to edit
the results of a join between one or more tables. The SQL language is designed to
prevent indiscriminate updating of a table. In particular, if you want to update a table
with a SQL query, then you should use the SQL Update command. That’s the way
SQL is supposed to work. It’s a very conservative language. If you want to let the user
edit tables at will, then you should use the TTable object.
Whatever limitations the RequestLive property may have are not unique to Delphi. If
you want to edit tables quickly, with a high-performance system, then use the TTable
object. Of course, you can try to use the TQuery object first, and see how these
requests are handled with your particular server.
In the next section, you’ll learn more about the TQuery object when you see how to join two
tables together so that you can view them both in a single dataset.
In a situation like this, it’s sometimes simplest to join the data from the ORDERS table and
the ITEMS table, so that the resulting dataset contains information from both tables:
Suzie Oct 1, 1994 Shrimp $12.95
Suzie Oct 1, 1994 Artichoke $6.25
etc...
The act of merging these two tables is called a join, and it is one of the fundamental operations
you can perform on a set of two or more tables.
Given the ORDERS and ITEMS tables from the demos subdirectory, you can join them in
such a way that the CustNo, OrderNo, and SaleDate fields from the ORDERS table are merged
with the StockNo, Price, and Qty fields from the ITEMS table to form a new dataset contain-
ing all six fields. A grid containing the resulting dataset is shown in Figure 16.6.
FIGURE 16.6.
The QJOIN program joins
the ORDERS and ITEMS
table producing a dataset
with fields from each table.
There’s a substantial difference between linking cursors and joining tables. However, they both
have two things in common:
■ They both involve two or more tables.
■ Each table is linked to the other by one or more shared fields.
The act of joining the ORDERS and ITEMS tables can be accomplished by a single SQL state-
ment that looks like this:
select
O.”OrderNo”, O.”CustNo”,
O.”SaleDate”, O.”ShipDate”,
I.”PartNo “, I.”Qty”, I.”Discount “
from
Orders O, Items I
where
O.OrderNo = I.OrderNo
■ The where clause is vitally important, because it specifies which field will link the two
tables. Some servers are capable of returning valid datasets even if you don’t include a
where clause in your join, but the resulting set of records will almost surely not be
what you want. To get the results you’re looking for, be sure to include a where clause.
When you’ve created the SQL statement that you want to use, there is nothing at all difficult
about performing a join. The QJOIN example that ships with Delphi demonstrates exactly
how to proceed. All you need do is drop a TQuery, TDataSource, and TdbGrid onto a form
and then wire them up in the standard way. When you’re hooked up, you can paste the query
statement in the SQL property of the query, fill in the DatabaseName property, and then set Ac-
tive to True. Now, compile and run the program and take a moment to scroll through the new
dataset you’ve created from the raw materials in the ORDERS and ITEMS tables.
NOTE
When you are composing SQL statements in the SQL field of the TQuery object, you
may find that the space you are working in is a little cramped. To open up your
horizons, click on the Code Editor button in the String List Editor dialog box. Your
code will then be transferred from the String List Editor to Delphi’s main editor. The
main editor gives you more room to work, and provides syntax highlighting for your
SQL statements.
There is not much point to showing you the actual source code for the QJOIN program be-
cause all the magic occurs in the SQL statement quoted previously.
FIGURE 16.7.
The QJOIN2 program
shows three tables linked
together in a logical and
coherent fashion.
To create this program, drop down a TTable, a TQuery, two datasources, and two data grids.
Hook up the TTable, the first datasource, and the first grid to the CUSTOMER table. Wire
up the remaining controls, and specify DataSource1 in the Query1.DataSource property. Now
add the following SQL statement in the Query1.SQL property:
select
O.CustNo, O.OrderNo, O.SaleDate,
L.PartNo, L.Discount, L.Qty
from
Orders O, Items L
where
O.CustNo = :CustNo and
O.OrderNo = L.OrderNo
The statement pictured here is very much like the one you saw in the last section, except that
the where clause has been expanded to include a bind variable:
where
O.CustNo = :CustNo and
O.OrderNo = L.OrderNo
This clause now specifies two different relationships: one between the CUSTOMER table and
the ORDERS table, and the second between the ORDERS table and the ITEMS table. More
specifically, the value for the CustNo variable will be supplied by the current record of the CUS-
TOMER table through the link on the Query1.DataSource property. The link between the
ORDERS table and ITEMS table will be the OrderNo field.
Conceptually, the QJOIN2 program forces you to wrestle with some fairly complex ideas. This
complexity is inherent in the task being performed. Delphi, however, enables you to encapsu-
late these complex ideas in a few very simple mechanical steps. In short, once you understand
the goal you want to achieve, Delphi enables you to perform even complex data operations
with just a few minutes of work.
This statement deletes any record from the COUNTRY database that has Argentina in the
Name field.
It doesn’t take long to see that this is a case in which you might want to use a parameterized
query. For instance, it would be nice to be able to vary the name of the country you want to
delete:
delete from Country where Name = :CountryName
In this case, CountryName is a variable that can be changed at runtime by writing code that looks
like this:
Query2.Prepare;
Query2.Params[0].AsString := ‘Argentina’;
Query2.ExecSQL;
Query1.Refresh;
The code shown here first calls Prepare to inform Delphi that it should parse the SQL state-
ment you gave it and ready the Params property. The next step is to insert a value into the Params
property, and then to execute the newly prepared SQL statement. Note that you execute the
statement not by calling Open but by calling ExecSQL. Call ExecSQL when you don’t need to
return a dataset! Finally, you display the results of your actions to the user by asking the first
query to refresh itself.
The Insert2 program from the examples subdirectory demonstrates this technique. That pro-
gram uses three different TQuery objects. The first TQuery object works with a TDataSource
and TdbGrid object to display the COUNTRY database on screen. In Figure 16.8, you can
see that the program has two buttons: one for deleting records, and the other for inserting records.
The second TQuery object in the INSERT2 program is used to insert a record into the
COUNTRY table, as explained next. The third TQuery object is used for deleting records. It
has the following statement in its SQL property:
delete from Country where Name = :Name;
FIGURE 16.8.
The INSERT2 program uses
three TQuery components and
one TDataSource component.
The code associated with the Delete button looks like this:
Query3.Prepare;
Query3.Params[0].AsString := Query1.Fields[0].AsString;
Query3.ExecSQL;
Query1.Refresh;
Query3 snags the name of the record to delete from the currently selected record in the first
query. This enables the user to scroll through the list of records using the TdbGrid tool, and
then delete whatever record is current. After the deletion, Query1.Refresh is called. A call to
Refresh forces the Query to obtain the most recent data from the disk, thereby allowing the
program to reflect the deletion at almost the same moment it is made. (Note that a real-world
program meant to be used with a typical set of users would query the user before performing a
deletion of this sort.)
Here is a typical SQL statement for inserting data into a table:
insert into
Country
(Name, Capital, Continent, Area, Population)
values
(‘Argentina’, ‘Buenos Ares’,
‘South America’, 2777815, 32300003)
This is a convenient system, but it has the disadvantage of forcing you to hard-code values into
the statement. To avoid this problem, the Query2 object has the following code in its SQL prop-
erty:
insert
into Country (Name, Capital, Continent, Area, Population)
values (:Name, :Capital, :Continent, :Area, :Population)
Note that in this code, all the actual values intended for insertion are specified by bind variables.
These bind variables are convenient because they enable you to write code that looks like this:
procedure TForm1.InsertClick(Sender: TObject);
begin
Query2.Prepare;
Query2.Params[0].AsString := ‘Erewhen’;
Query2.Params[1].AsString := ‘None’;
Query2.Params[2].AsString := ‘Imagination’;
Query2.Params[3].AsFloat : = 0.0;
Query2.Params[4].AsFloat : = 1.0;
Query2.ExecSQL;
Query1.Refresh;
end;
In the code shown here, you can use edit controls to dynamically specify the values that you
want to insert at runtime. Notice that once again, the program calls ExecSQL rather than Open.
This is because there’s no need to return a cursor from an SQL insert statement. The proce-
dure ends with a call to Refresh, which assures that Query1 goes out to the disk and gets the
most recent data.
NOTE
You might want to compare this version of the Insert program with the Insert1
application that uses the TTable object. There are advantages to both techniques, but
you should remember that keeping code as simple as possible is one way to construct
applications that are robust and easy to maintain.
In this section, you’ve learned about the differences between ExecSQL and Open. The major point
to remember is that select statements return a cursor and therefore require a call to Open.
delete, insert, and update don’t return a cursor, and should therefore be accompanied by calls
to ExecSQL. All of this is demonstrated on the CD in the INSERT2 program. The call to Refresh
ensures that the data displayed to the user reflects the changes made by the delete statement.
The UniDirectional property is used to optimize your access to a table. If you set UniDirectional
to True, you can iterate through a table more quickly, but you’ll only be able to move in a for-
ward direction.
The StmtHandle property is related to the Handle property from TDataSet; it’s included solely
so you can make your own calls directly to the Borland Database Engine. Under normal cir-
cumstances, there would be no need for you to use this property, because Delphi’s compo-
nents can handle the needs of most programmers. However, if you’re familiar with the Borland
Database Engine, and if you know that it has some particular capability that isn’t encapsulated
in the VCL, you can use TQuery.StmtHandle or TQuery.Handle to make calls directly to the engine.
The following short code fragment shows two calls being made directly to the BDE:
var
Name: array[0..100] of Char;
Records: Integer;
begin
dbiGetNetUserName(Name);
dbiGetRecordCount(Query1.Handle, Records);
end;
The DBIPROCS unit contains a list of all the possible calls made to the Borland Database
Engine. This file may appear on your system as either DBIPROCS.PAS or DBIPROCS.INT.
Summary
In this chapter, you have learned the main features of the TQuery component. You have seen
that you can use this component to create SQL statements that enable you to manipulate tables
in a wide variety of useful ways.
One of the keys to understanding TQuery’s SQL property is the ability to manipulate it at
runtime. In this chapter, you saw three different methods of manipulating this property. The
first, and conceptually simplest, is to merely use the Query1.SQL.Add function whenever you
need to change a query at runtime. Parameterized queries are less wasteful than using the Add
property, but there are some limits on what you can do with parameterized queries. To get
beyond these limits, you can use the Format function, which enables you to create almost any
kind of SQL statement you could want at runtime.
Regardless of how you treat the SQL property, there is no doubt that it is one of the power cen-
ters in the Delphi environment. Programmers who want to write powerful SQL applications
need to know almost everything they can about the SQL property.
In the next chapter, you will learn how to use the Fields Editor, the Database Explorer, the
DataDictionary, QBE, and the Query Builder to automate some of the database tasks you have
been performing in the last two chapters.
I should perhaps add that almost all the code in this chapter will compile without change in
either the 16- or 32-bit version of Delphi. This ability to write one source file that compiles
fine under 16- or 32-bit Windows is one of Delphi’s great strengths.
This chapter covers a set of visual tools you can use to simplify database development. The
major areas of concentration are as follows:
■ Relational databases
■ The Fields Editor
■ TField descendant objects
■ Calculated fields
■ The TdbGrid Component
■ Lookup fields
■ The Database Explorer
■ The Database Desktop
■ Query by example (QBE)
■ MultiRecord objects—TdbCtrlGrid
The general theme that ties these subjects together is how you can use Delphi’s visual and pro-
grammatic tools to create relational databases. Delphi has become a very sophisticated data-
base tool, and it takes time to get a feeling for the breadth of the tools available to client/server
developers. One of the goals of this chapter is to give you some sense of the key components
used when designing database applications.
One of the most frequently mentioned tools in this chapter is the Fields Editor. By using the
Fields Editor, you can create objects that you can use to influence the manner and types of data
that appear in visual controls such as TdbEdit and TdbGrid. For instance, you can use the ob-
jects made in the Field Editor to format data so that it appears as currency or as a floating-
point number with a defined precision. These same changes can be accomplished through the
Data Dictionary in the Database Explorer, or through the Database Desktop. These latter tools,
however, have a global impact on the field’s potential values, whereas the changes made in the
Object Inspector affect only the current application.
The new Columns property of the TdbGrid control can be used to change the appearance of a
grid so that its columns are arranged in a new order or are hidden. You can also use the Columns
property to change the color of columns in a grid, or to insert dropdown combo boxes into a
grid.
The lessons you learn in this chapter will demonstrate a key part of the techniques used by
most programmers when they present database tables to their users. Much of the material in-
volves manipulating visual tools, but the basic subject matter is fairly technical and assumes a
basic understanding of the Delphi environment and language.
If you are an experienced database programmer, don’t be fooled by the relatively basic over-
view of relational databases found in the first few pages of this chapter. There is a lot of impor-
tant subject matter to be found here, and much of it is unique to Delphi in general and to
Delphi 2.0 in particular.
Relational Databases
In order to make sure everyone is following the discussion in the next few chapters, I’m going
to spend a few pages giving a quick-and-dirty introduction to relational databases. This discus-
sion will also include a brief overview of the Database Explorer and the Database Desktop.
There are many different kinds of possible databases, but in today’s world, on the PC, there are
only two kinds that have any significant market share:
1. Flat-file databases
2. Relational databases
Flat-file databases consist of a single file. The classic example would be an address book that
contains a single table with six fields in it: Name, Address, City, State, Zip, and Phone. If that
is your entire database, then what you have is a flat-file database. In a flat-file database, the
words table and database are synonymous.
In general, relational databases consist of a series of tables related to each other by one or more
fields in each table. In Chapter 15, “Using TTable and TDataSet,” and Chapter 16, “SQL and
the TQuery Object,” you saw how to use the TTable and TQuery objects to relate the Cus-
tomer and Orders table together in a one-to-many relationship. As you recall, the two tables
were joined on the CustNo field. The relationship established between these two tables on the
CustNo field is very much at the heart of all relational databases.
What are the differences between the address book example and the Customer and Orders
example? Well, here are three key differences:
1. The address book database consists of one single table. That’s the whole database.
There is nothing more to say about it.
2. The Customer and Orders example consists of two tables. Relational databases always
contain multiple tables. As you will see, there are in fact more than just these two
tables in the DBDEMOS database, but for now just concentrate on the Customer and
Orders tables.
3. The Customer and Orders tables are related to one another by the CustNo field.
The key differences, then, are that relational databases consist of multiple tables, at least some
of which are related together by one or more fields. Flat-file databases, on the other hand, con-
sist of only one single table, which is not related to any other table.
What advantages do relational databases have over flat-file databases? Here are two key
advantages:
■ Relational databases save disk space. The Customer table holds information about
customers, including their address, phone, and contact information. The Orders table
holds information about orders, including their date, cost, and payment method. If
you were forced to keep all this information in a single table, then each order would
also have to list the customer information, which would mean that some customers’
addresses would be repeated dozens of times in the database. That’s wasteful of space.
It’s better to use a relational database because each customer’s address would be
entered only once. You could also have two flat-file databases, one holding the
customer information and the other holding the orders information. The problem
with this second scenario is that flat-file databases provide no means of relating the
two tables so that you can easily see which orders belong to which customer.
■ Relational databases provide flexibility. If you combined the Customer and Orders
table into one single table, that might be good on some occasions, but it would mean
that you would have no immediately intuitive way to view the orders information
separately from the customer information, or the customer information separately
from the orders information. (You could make certain fields invisible, but how could
you intuitively know which fields belonged to which group?) A relational database
offers these possibilities:
1. You can view the Customer table alone if you want and view the Orders table
alone, if you want.
2. You can place the two tables in a one-to-many relationship, so that you can see
them side by side, but only see the Orders relating to the currently highlighted
customer.
3. Yet another alternative would be to perform a join between the two tables, so
that you see them as one combined table, much like the combined table you
would be forced to use if you wanted to “join” the customer and orders table in
a single flat-file database. However, you can decide which fields from both tables
will be part of the join, leaving out any you don’t want to view.
The one disadvantage that relational databases have when compared to flat-file databases is that
they are more complicated to use. This is not just a minor sticking point. Neophytes are liter-
ally completely baffled by relational databases. They don’t have a clue as to what to do with
them. Even if you have a relative degree of expertise, anyone can still become overwhelmed by
a relational database consisting of three dozen tables related to one another in some hundred
different ways.
NOTE
One common feature of relational databases is that most records will have a unique
number associated with them, and these numbers will be used as the indexes that relate
one table to another. For instance, if you have a list of Customers, there will be a
unique CustNo field in each record. Furthermore, if you have a list of Orders, then
there will be a unique OrderNo associated with the orders table. The Orders table will
also have a CustNo field that will relate it to the Customer table. These CustNo,
OrderNo, AuthorNo, BookNo, and similar fields might also be used in flat-file
databases, but they play a key and unique role in relational databases because they are
the indexes used to relate different tables.
NOTE
If you are new to databases, you will undoubtedly be frustrated to discover that
different databases have varying rules for setting up indexes, keys, and so on. In this
book, I tend to use Paradox tables as the default, but I also spend considerable time
describing InterBase tables. If you use some other database, such as dBASE, Oracle, or
Sybase, then you should be sure to read up on the basic rules for using those tools.
If you are not sure what database to use, then I would tentatively suggest using Paradox
to get started. It has a robust set of rules for enforcing data integrity, a rich set of types,
and some nice features such as autoincrement fields. It works fine on a network, as
long as everyone can attach their PCs to one centralized server and you aren’t expecting
a large number of simultaneous users.
If you are expecting 20 or more simultaneous users, then I would bite the bullet
financially and switch to InterBase or to another standard SQL server such as Oracle,
Sybase, or MS SQL Server. These tools will:
■ Let you talk to the server over TCP/IP, or some other network protocol
■ Put much of the computational burden on the server rather than the client
machine
■ Prove to be much more robust
■ Allow you to use stored procedures, triggers, views, and other advanced server-
side technologies
Remember that when I make suggestions about databases or about anything else, I am
usually not so much trying to establish a definitive standard as I am trying to give
reasonable advice to those readers who are not sure which way to turn.
Here is a list of the keys and indexes on the Customers, Orders, Items, and Parts tables:
fields make up which parts of the Primary and Secondary Indexes. In this case, you could prob-
ably guess, but it would still be nice to get a more definitive answer.
If you open up the Database Explorer, expand the DBDEMOS node, the Tables node, the
Customer node, and finally the Indexes node, you get (naturally enough) a list of the indexes
on the Customer table! This is a great feature, and you should use it whenever possible. Figure
17.1 shows the expanded nodes of the Indexes for the Customer table.
FIGURE 17.1.
The Indexes of the
Customer table viewed in
the Database Explorer.
While you have the Explorer open, you should also expand the Fields node, as shown in Figure
17.2. This gives a quick list of all the fields and their types. Notice that you can drag and drop
individual fields onto a form.
A third way to get a look at the structure of a table is through the Database Desktop (DBD).
You can open this program from the Tools menu in Delphi. Use the File menu in the DBD to
set the Working Directory to the DBDEMOS Alias. Open up the Customer table and choose
the Table | Info Structure menu choice. Drop down the Table Properties combo box and look
up the Secondary Indexes, as shown in Figure 17.3. The Primary Index is designated by the
asterisks after the keyed fields in the Key Roster. In this case, only the CustNo field is starred,
since it is the sole keyed field.
NOTE
Over time, the Database Desktop will probably be replaced entirely by the Explorer.
However, there are still some things that the DBD does better than the Explorer, so
both products are shipped with Delphi.
FIGURE 17.2.
The Fields view of the
Customer table from the
Database Explorer.
FIGURE 17.3.
The Database Desktop
struts its venerable features
by displaying the indexes on
the Customer table.
Notice the Save As button on the Info Structure dialog. You can use this to save a table that
contains the structure of the Customer table. You can then print this out on a printer using
Report Smith or TQuickReports. Be sure to use a fixed-size font, not a proportional font:
Field Name Type Size Key
CustNo N *
Company A 30
Addr1 A 30
Addr2 A 30
City A 15
State A 20
Zip A 10
Country A 20
Phone A 15
FAX A 15
TaxRate N
Contact A 20
LastInvoiceDate @
In the example shown here, I have printed out only the first four fields of the table because of
space considerations. (The fields are Field Name, Type, Size, and Key.) If I then recursively
print out the structure of the table used to house the structure of the Customer table, I get the
following report:
Field Name Type Size Key
Field Name A 25
Type A 1
Size S
Key A 1
_Invariant Field ID S
_Required Value A 1
_Min Value A 255
_Max Value A 255
_Default Value A 255
_Picture Value A 176
_Table Lookup A 255
_Table Lookup Type A 1
This is the same information found in the Data Dictionary, and it should prove sufficient under
most circumstances.
FIGURE 17.4.
The datamodule for the
Relate project.
Drop four TdbGrid objects on the main form for the project. Use the File | Use Unit menu
option to link Form1 to DataModule1. Wire the grids to the datasources on the datamodule,
making sure that each grid has its DataSource property assigned to a unique object. For in-
stance, link the first grid to the Customer table, the second to the Orders table, and so on.
Using the names visible in Figure 17.4, click the TblOrders component and set its MasterSource
property equal to dsCust, that is, set its MasterSource equal to the TDataSource object that is
linked to the TTable object that hosts the Customer table. Set the TblItems MasterSource
property equal to dsOrders and the TblParts MasterSource equal to dsItems.
Click the TblOrders MasterFields property and link up the Orders and Items tables on the
CustNo field, as described in Chapter 15, “Using TTable and TDataSet.” In the same way,
hook up the TblItems to TblOrders on the OrderNo field, and TblParts to TblItems on the
PartNo field. If you set all the tables to active and then run the program, the result should look
like what you see in Figure 17.5.
FIGURE 17.5.
The Relate program
at runtime.
Spend a little time mucking about with this program. Notice, for instance, that if you change
the selected item in the Customer table, then the contents of the grids showing the Orders,
Items, and Parts table will change. In particular, notice that the CustNo in all the items in the
Orders table is always equal to the CustNo in the currently selected item in the Customer table.
The same thing can be said about the OrderNo field in the Orders and Items tables, and the
PartNo field in the Items and Parts tables.
One point you should be sure to come away with is that, in general, selecting one item at any
level but the lowest in the hierarchy will force many records to change in the lower levels of the
hierarchy. That is why these are called one-to-manyrelationships. One record in the Orders
table points to many records in the Items and Parts tables.
NOTE
In this particular example, you might notice that the Parts table is always arranged in a
one-to-one relationship with the Items table. However, if you reverse the order of these
table and make the Parts table the Master, then the arrangement will look more like a
proper one-to-many relationship. However, it is not wrong to make either table the
master. The point is simply to arrange the tables so that you get the information from
that you want to obtain.
This discussion of the Relate program has given you a look at some of the important features
in the Database Explorer and Database Desktop. It has also given you a quick run-down on
some of the key ideas behind the construction of relational databases. The point here is that
Delphi has lots of built-in tools that help you construct relational databases. There is more
that I want to say about these topics, however, even in this rather sketchy overview of a com-
plicated subject. In particular, I have not yet talked about joins.
Though not horrendously complicated, the syntax shown here is still ugly enough to give some
people pause.
The basic principles involved in this kind of statement are simple enough to describe. All that’s
happening is that the Customer, Orders, Items, and Parts tables are being joined together into
one large table of the type you would have to create if you were trying to track all this informa-
tion in a single flat-file database. The one proviso, of course, is that not all the fields from the
four tables are being used, in fact, the only ones mentioned are:
d.Company, d1.AmountPaid, d2.Qty,
d3.Description, d3.Cost, d3.ListPrice
Here the d, d1, d2, and d3 are described in the following From clause:
“Customer.db” d, “Orders.db” d1,
“Items.db” d2, “Parts.db” d3
The Order By clause, of course, simply defines the sort order to be used on the table created by
this join.
You can create a program that performs this join by dropping a TQuery, TDataSource, and
TdbGrid on a form. Wire the objects together, wire the TQuery to the DBDEMOS database,
and set its SQL property to the query shown above. An example program called RelJoin demon-
strates this process. The output from the program is shown in Figure 17.6.
FIGURE 17.6.
The RelJoin program
demonstrates a join
between four tables.
If you are not familiar with this kind of join, you might want to bring up the Relate and RelJoin
tables side by side and compare them. Look, for instance, at the Action Club entries in the
RelJoin program and trace them through so that you see how they correspond to the entries in
the Relate program. Both programs describe an identical set of relationships—they just show
the outcome in a different manner.
Notice that the AmountPaid column in the RelJoin program has the same number repeated
twice in the Action Club section, as shown in Figure 17.6. In particular, the numbers $1,004.80
and $20,108 both appear twice. This is because there are two different Items associated with
these orders, as you can tell from glancing at the Parts table in the Relate program.
NOTE
Unless you are already familiar with this material, be sure to run the RelJoin and Relate
programs and switch back and forth between them until you understand why RelJoin
program works as it does. I find it easy to understand the Relate program at a glance,
but the RelJoin program is a bit more subtle.
FIGURE 17.7.
Four tables used in a single
QBE example.
FIGURE 17.8.
The complete QBE query
for joining the Customer,
Orders, Items, and Parts
tables.
To test your work, click the lightning bolt icon once. You should get a table that looks just like
the one in the RelJoin program.
To translate the QBE statement into SQL, first close the result table so you can view the query
shown in Figure 17.8. Click once on the SQL icon to perform the translation. You can save
this SQL to disk, or just block-copy it and deposit it in the SQL property of a TQuery object.
On paper, this process takes a few minutes to explain. However, once you understand the QBE
tool, you can use it to relate multiple tables in just a very few seconds. For most people, QBE
is probably the simplest and fastest way to compose all your SQL Select statements. Don’t
neglect learning to use this tool. It’s a simple, easy-to-use tool that can save you hours of time.
NOTE
The only peculiarity of the QBE tool is that by default it saves its output in a text-
based language called QBE, rather than in SQL. However, once you press the SQL
button, it converts the QBE code to SQL, thereby rendering the exact same results
produced by standard SQL query builders. Once again, the great advantage of the
QBE tool over other SQL tools is that it ships with the DBD product that accompa-
nies nearly all versions of Delphi. If you have access to a more powerful SQL builder,
then you might want to use it instead of the QBE tool. However, QBE works fine in
most circumstances, even when running against SQL data in an InterBase table.
That’s it for the discussion of the basic principles of relational databases. You’ve seen how to
build master detail relationships, and how to construct joins. More importantly, you’ve seen
how Delphi encapsulates these key aspects of relational database design. The rest of this chap-
ter digs in further and discusses other tools Delphi uses to help you create client/server or stan-
dard database applications.
Drop down the Object Selector at the top of the Object Inspector. Notice that you currently
have two components in use: TForm and TQuery.
Right-click the TQuery object and select the Fields Editor menu choice to bring up the Fields
Editor. Right-click the Fields Editor and select Add Fields from the menu to pop up the Add
Fields dialog, as shown in Figure 17.9.
FIGURE 17.9.
The Add Fields dialog box
from the Fields Editor.
By default, all of the fields in the dialog box are selected. Click the OK button to select all five
fields, and then close the Fields Editor.
Open the Object Selector a second time; notice that there are now five new objects on your
form, as shown in Figure 17.10.
FIGURE 17.10.
The Object Selector lists the
objects created in the Fields
Editor. You also can find
this list in the TForm1
class definition.
These objects help you control your presentation of the Animals table to the user.
Here’s a complete list of the objects you just created:
Query1NAME: TStringField;
Query1SIZE: TSmallintField;
Query1WEIGHT: TSmallintField;
Query1AREA: TStringField;
Query1BMP: TBlobField;
I cut and pasted this list from the TForm1 class definition found in the Editor Window. The
origins of the names shown here should be fairly obvious. The Query1 part comes from the
default name for the TQuery object, and the second half of the name comes from the fields in
the Animals Table. If I had renamed the Query1 object to Animal, I would have produced
names that looked like this:
AnimalNAME
AnimalSIZE
AnimalWEIGHT
This kind of convention can be very useful if you are working with several tables and want to
know at a glance which table and field are being referenced by a particular variable.
NOTE
The names of the fields in the example shown here are capitalized only because the
table in question is a dBASE table. dBASE tables automatically capitalize all letters in
field names. If I had chosen to work with some other type of table, the capitalization of
the letters in the field name would have followed the rules defined by the current
database software.
Each of the objects created in the Fields Editor is a descendant of TField. The exact type of
descendant depends on the type of data in a particular field. For instance, the Query1WEIGHT
field is of type TSmallIntField, whereas the Query1NAME field is of type TStringField. These
are the two field types you will see most often. Other common types include TDateField and
TCurrencyField , neither of which are used in this particular table. Remember that these types
were selected to correspond with the field types in the table itself.
TStringField , TSmallIntField , and the other objects shown here are all descendants of TField
and share its traits. If you want, you can treat these objects exactly as you did the TField objects
that you learned about in Chapter 20, “Using TTable and TDataSet.” For instance, you can
write this:
S := TIntegerField.AsString;
and this:
S := TIntegerField.Name.
However, these descendants of TField are very smart objects and have several traits that go
beyond the functionality of their common ancestor.
To start getting a feel for what you can do with TField descendants, you should open up the
Browser, turn off the option to view Private and Protected fields, and then scroll through the
Public and Published properties and methods.
The most important property you will see is called Value. You can access it like this:
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
S: string;
begin
i := Query1SIZE.Value;
S := Query1NAME.Value;
Inc(i);
S := ‘Foo’;
Query1SIZE.Value := i;
Query1NAME.Value := S;
end;
The code shown here first assigns values to the variables i and S. The next two lines change
these values, and the last two lines reassign the new values to the objects. It usually wouldn’t
make much sense to write code exactly like this in a program, but it serves to illustrate the syn-
tax used by TField descendants.
The Value property always conforms to the type of the field you have instantiated. For instance,
TStringFields are strings, whereas TCurrencyFields always return floating-point double val-
ues. However, if you show a TCurrencyField in a data-aware control, it will return a string that
looks like this: “$5.00”. The dollar sign and the rounding to decimal places are simply part and
parcel of what a TCurrencyField is all about.
The preceding example might make you think that these variables are declared as Variants.
The point here is that TCurrencyField.Value is declared as a Double. If you tried to assign a
string to it, you would get a type mismatch. Likewise, TIntegerField.Value is declared as an
LongInt, and so on. TSmallIntField and TWordField are both descendants of TIntegerField
and inherit the Value declaration as a LongInt. However, they have other internal code that
affects the Value field, just as TCurrencyField rings some changes on its Value field to make it
look like a monetary value. If you have the source, look up DBTABLES.PAS and DB.PAS to
find the details of these constructions. At any rate, the point here is that the preceding code is
an example of polymorphism; it is not an example of relaxed type-checking. The Value field
has a specific type—it’s just that it undergoes polymorphic changes.
If you want the names of each field in the current dataset, you should reference the FieldName
property through one of the following two methods:
S := Query1.Fields[0].FieldName;
S := Query1NAME.FieldName;
If you want the name of an object associated with a field, you should use the Name property:
S := Query1.Fields[0].Name;
S := Query1NAME.Name;
When using the ANIMALS table, the first two examples shown above yield the string “Name”,
while the second two lines yield “Query1NAME”.
Special properties are associated with most of the major field types. For instance, TIntegerFields
have DisplayFormat and DisplayEdit properties, as well as MinValue and MaxValue properties.
TStringFields, on the other hand, have none of these properties, but they do have an EditMask
property, which works just like the TEditMask component found on the Additional Page of
the Component Palette. All these properties are used to control the way data is displayed to the
user, or the way that input from the user should be handled.
NOTE
I don’t want to get ahead of myself, but properties such as MinValue and MaxValue are
also used in the Data Dictionary, as will be explained later in this chapter. Changes
made in the Data Dictionary will affect these values as seen in the Object Inspector,
but changes in the Object Inspector will not affect the Data Dictionary. Don’t worry if
this doesn’t make the slightest bit of sense yet, as I will get to the Data Dictionary in
just a little while.
You should be aware of one more thing about the Fields Editor. You can use this tool not only
to build objects that encapsulate existing fields, but also to build objects that represent new
fields. For instance, suppose you wanted to create a sixth field, MASS, which contains the product
of the SIZE and WEIGHT fields, in the Animals table.
To create the MASS field, open the Field Editor again, right-click it, and select the New Field
menu choice. In the top part of the New Field dialog, enter the word MASS. Now set its type to
Integer, and leave its field type as calculated, as shown in Figure 17.11.
FIGURE 17.11.
Creating the MASS field
in the Field Editor.
If you close the Field Editor and add a TDataSource and TdbGrid to your project, you will see
that the Animals table now appears to have six fields, the last of which is called MASS.
Of course, it’s one thing to create a field, and another to fill it in at runtime with an appropri-
ate value. The act of placing a value in the new field you have created involves calculated fields,
which are addressed in the next section.
Calculated Fields
Calculated fields are one of the most valuable fruits of the Fields Editor. You can use these
fields for several different purposes, but two stand out:
■ If you need to perform calculations on two or more of the fields in a dataset and want
to show the results of the calculations in a third field, you can use calculated fields. A
scenario describing this type of situation was set up in the last section and will be
explained further in this section.
■ If you are viewing one dataset and want to perform calculations or display data that
involve lookups in at least one additional dataset, you can use the Field Editor and
Calculate Fields to show the results of these calculations in a new field of the first
dataset. There is also a second, much better method for doing lookups. I will talk
about that method later in this chapter. As a rule, you should do calculations in
calculated fields, and lookups in lookup fields, though calculated fields are powerful
enough to fill a number of different roles in your programs.
The MASS program illustrates one example of the first of the two uses for calculated fields
described previously. You got this program started in the last section when you created the
field called MASS and displayed it in a grid.
To continue working with the MASS program, highlight the Query1 object and set the Ob-
ject Inspector to the events page. Now create an OnCalcFields event that looks like this:
procedure TForm1.Query1CalcFields(DataSet: TDataset);
begin
Query1MASS.Value := Query1SIZE.Value * Query1WEIGHT.Value;
end;
The code shown here assigns the value of the Query1MASS object to the product of the
Query1SIZE and Query1WEIGHT fields. This kind of multiplication is legal to do because
all of the fields are of the same type.
OnCalcFieldmethods get called each time a record is first displayed to the user. As a result, all
of the MASS fields displayed in the grid are properly filled in, as shown in Figure 17.12.
FIGURE 17.12.
The MASS field contains
the product of the
WEIGHT and SIZE fields.
A TdbImage control
contains a bitmap from the
BMP field of the table.
To get the image shown in Figure 17.12, I opened the Column property in the TdbGrid object
and selected Add All Fields. I then deleted the Area and BMP fields and closed the Column prop-
erty editor. I will talk more about the grid object later in this chapter.
If you choose to never instantiate a particular field in the field editor, the current dataset you
are working with no longer contains that field. It can’t be accessed programmatically or visu-
ally at runtime. Usually, this is exactly the effect you want to achieve, and so this trait will gen-
erally be perceived as a strong benefit. However, there are times when it might not serve your
purposes, and in those cases you should either create an object for all the fields in a table or stay
away from the Field Editor altogether.
TdbGrid at Runtime
dbGrids can be completely reconfigured at runtime. You can hide and show columns, change
the order of columns, the color of columns, and change the width of columns.
The MOVEGRID (see Figure 17.13) program shows how to take a TdbGrid through its paces
at runtime. The program is fairly straightforward except for two brief passages. The first pas-
sage involves creating check box controls on the fly, whereas the second shows how to change
the order of items in a list box on the fly.
FIGURE 17.13.
The main MOVEGRID
program enables you to
change the appearance
of a grid at runtime.
When the user wants to decide which fields are visible, MOVEGRID pops up a second form
and displays the names of all the fields from the ORDERS table in a series of check boxes. The
user can then select the fields that he or she wants to make visible. The selected check boxes
designate fields that are visible, whereas the nonselected ones represent invisible fields. The
program also enables you to set the order and width of fields, as well as to hide and show the
titles at the top of the grid. (See Listings 17.1 and 17.2.)
Listing 17.1. The code for the MOVEGRID program is in the CHAP17 directory on
this book’s CD-ROM.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
Grids, DBGrids, DB,
DBTables, StdCtrls, Buttons;
type
TForm1 = class(TForm)
Query1: TQuery;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
Name: TButton;
ListBox1: TListBox;
Button3: TButton;
Visible: TButton;
FieldName: TButton;
Help: TButton;
Query1OrderNo: TFloatField;
Query1CustNo: TFloatField;
Query1SaleDate: TDateTimeField;
Query1ShipDate: TDateTimeField;
Query1EmpNo: TIntegerField;
Query1ShipToContact: TStringField;
Query1ShipToAddr1: TStringField;
Query1ShipToAddr2: TStringField;
Query1ShipToCity: TStringField;
Query1ShipToState: TStringField;
Query1ShipToZip: TStringField;
Query1ShipToCountry: TStringField;
Query1ShipToPhone: TStringField;
Query1ShipVIA: TStringField;
Query1PO: TStringField;
Query1Terms: TStringField;
Query1PaymentMethod: TStringField;
Query1ItemsTotal: TCurrencyField;
Query1TaxRate: TFloatField;
Query1Freight: TCurrencyField;
Query1AmountPaid: TCurrencyField;
procedure NameClick(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CustNoIndexClick(Sender: TObject);
procedure FieldNameClick(Sender: TObject);
procedure VisibleClick(Sender: TObject);
procedure ListBox1DblClick(Sender: TObject);
procedure BitBtn1Click(Sender: TObject);
procedure DBGrid1DblClick(Sender: TObject);
private
ShowTitles: Boolean;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
VisForm;
{$R *.DFM}
continues
end.
Listing 17.2. The VisiForm unit, displays a set of check boxes, where the user
designates which fields are to be made visible.
unit Visform;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
DBTables, StdCtrls;
const
RadSize = 25;
type
TVisiForm = class(TForm)
private
R: array[0..25] of TCheckBox;
procedure CreateRad(Index: Integer; Name: String; Visible: Boolean);
public
{ Public declarations }
procedure ShowMe(Query1: TQuery);
end;
var
VisiForm: TVisiForm;
implementation
{$R *.DFM}
end.
In the next few paragraphs, you will find descriptions of the key parts of the MOVEGRID
program. Understanding its constituent parts will help you to take control over the grids you
display in your programs.
Most of the code in the MOVEGRID program is fairly simple. However, the program per-
forms a number of separate tasks. To grasp the program, it’s necessary to divide and conquer,
that is, take the tasks performed by the program one at a time. Find out how each one works,
and then move on to the next one. If you proceed in this fashion, you will find the program
easy to understand.
You can use the Options field of a TdbGrid to change its appearance. The Options property
has the following possible values:
dgEditing Set to True by default, it enables the user to edit a grid. You can
also set the grid’s ReadOnly property to True or False.
dgTitles Designates whether titles can be seen.
dgIndicator Determines whether to show the small icons on the left of the
grid.
dgColumnResize Designates whether or not the user can resize columns.
dgColLines Determines whether or not to show the lines between columns.
dgRowLines Designates whether or not to show the lines between rows.
dgTabs Enables the user to tab and shift+tab between columns.
Here is the declaration for the enumerated type where these values are declared:
TDBGridOption = (dgEditing, dgAlwaysShowEditor, dgTitles,
dgIndicator, dgColumnResize, dgColLines,
dgRowLines, dgTabs);
For instance, you can set the options at runtime by writing code that looks like this:
DBGrid1.Options := [dgTitles, dgIndicator];
If you want to toggle an option on and off, you can do so with a logical operator. For instance,
the following code will add a set of titles to DBGrid1:
DBGrid1.Options := DBGrid1.Options + [dgTitles];
Given the presence of a Boolean variable called ShowTitles, the following code enables you to
toggle an option back and forth at the press of a single button:
procedure TForm1.Button3Click(Sender: TObject);
begin
if ShowTitles then
DBGrid1.Options := DBGrid1.Options + [dgTitles]
else
DBGrid1.Options := DBGrid1.Options - [dgTitles];
ShowTitles := not ShowTitles;
end;
To move the location of a column at runtime, you can simply change its index, which is a zero-
based number:
Query1.FieldByName(‘CustNo’).Index := 1;
Query1CustNo.Index := 2;
By default, the CustNo field in the Customer table is at the top of the record, in the zero (first)
position. The code in the first example above moves it to the second position, whereas the code
that reads Query1CustNo.Index := 2; moves it to the third position. Remember, the Index field
is zero-based, so moving a field to Index 1 moves it to the second field in a record. The first
field is at Index 0.
When you change the index of a field, you do not need to worry about the indexes of the other
fields in a record. They will be changed automatically at runtime.
The MOVEGRID program contains a list box that displays the current order of the fields in a
dataset. The code shown following responds to a double-click in a list box by querying the user
if he or she wants to change the position of the currently selected item in the list box. The user
can then enter the number to which he wants to move the currently selected field. For instance,
if the user clicked the CustNo string in the list box and entered the number 2, CustNo would
move from the first to the third position in the list box. The code shown here also moves the
index of the real field from 0 to 2, thereby changing the actual position of the field in the grid.
These changes are being made only to the visual representation of a dataset. The actual data on
a disk is not changed by this procedure.
The following code is activated whenever the user double-clicks the list box:
procedure TForm1.ListBox1DblClick(Sender: TObject);
var
Input, Temp, CurPos: string;
StartPos: Integer;
begin
Input := ‘’;
Temp := ListBox1.Items.Strings[ListBox1.ItemIndex];
StartPos := ListBox1.ItemIndex;
CurPos := ‘Current Position: ‘ + IntToStr(StartPos);
InputQuery(CurPos, ‘New Position’, Input);
ListBox1.Items.Delete(ListBox1.ItemIndex);
ListBox1.Items.Insert(StrToInt(Input), Temp);
Query1.Fields[StartPos].Index := StrToInt(Input);
end;
The last line in the preceding example is what actually changes the index of the column the
user wants to move. The two lines directly before it delete the currently selected string in the
list box and then insert it into a new position.
If you want to change the width of a column at runtime, just change the DisplayWidth prop-
erty of the appropriate TField object:
Query1.FieldByName(‘CustNo’).DisplayWidth := 12;
Query1CustNo.DisplayWidth := 12;
The value 12 refers to the number of characters that can be displayed in the control.
If you want to hide a field at runtime, you can set its Visible property to False:
Query1.FieldByName(‘CustNo’).Visible := False;
Query1CustNo.Visible := False;
Both lines of code perform identical tasks. To show the fields again, simply set Visible to True:
Query1.FieldByName(‘CustNo’).Visible := True;
Query1CustNo.Visible := True;
In order to allow the user to decide which fields are visible, MOVEGRID pops up a second
form with a series of check boxes on it. (See VISFORM.PAS.) The program actually creates
each of these check boxes at runtime. In other words, it doesn’t just pop up a form with the
correct number of check boxes on it, but instead iterates through the Query1 object, finds out
how many check boxes are needed, and then creates them dynamically at runtime.
To perform these tasks, MOVEGRID passes a copy of the Query1 object through to the form
that displays the check boxes:
procedure TForm1.VisibleClick(Sender: TObject);
begin
VisiForm.ShowMe(Query1);
end;
The ShowMe method of the VisiForm first calls a routine called CreateRad, which creates the
check boxes, displays the form, and finally sets the state of the check boxes:
procedure TVisiForm.ShowMe(Query1: TQuery);
var
i: Integer;
begin
for i := 0 to Query1.FieldCount - 1 do
CreateRad(i, Query1.Fields[i].Name, Query1.Fields[i].Visible);
Height := i * (RadSize + 5);
ShowModal;
for i := 0 to Query1.FieldCount - 1 do
Query1.Fields[i].Visible := R[i].Checked;
end;
The ShowMe method iterates through the Query1 object and assigns one check box to each field.
It also asks TQuery for the names of the fields, and whether each field is currently hidden or
visible. Here is the code that creates a check box on the fly:
procedure TVisiForm.CreateRad(Index: Integer;
Name: String;
Visible: Boolean);
begin
R[Index] := TCheckBox.Create(Self);
R[Index].Parent := VisiForm;
R[Index].Caption := Name;
R[Index].Left := 10;
R[Index].Top := Index * RadSize;
R[Index].Width := 200;
R[Index].Checked := Visible;
end;
Most of the code in this example is performing relatively mundane tasks, such as assigning names
and locations to the check boxes. These are the two key lines:
R[Index] := TCheckBox.Create(Self);
R[Index].Parent := VisiForm;
The first line actually creates the check box and gives it an owner. The second line assigns a
parent to the check box.
NOTE
The difference between a parent and an owner can be confusing at times. A form is
always the owner of the components that reside inside it. As such, it is responsible for
allocating and deallocating memory for these components. A form might also be the
parent of a particular component, which means that the component will be displayed
directly on the form. However, one component might also find that another compo-
nent is its parent, even though both components are owned by the form. For instance,
if you place a TPanel on a form and then two TButtons on the TPanel, all three
components will be owned by the form; however, the buttons will have the panel as a
parent, whereas the TPanel will have the form as a parent. Ownership has to do with
memory allocation. Parenthood usually describes what surface a component will be
displayed on. If you get confused about this while in the midst of a lengthy program-
ming session, you can look it up in the online help by searching on the topic, Parent.
The grids supplied with the first version of Delphi are reasonably flexible objects that perform
most of the tasks required of them. If you feel you need some additional functionality, check
with third-party tool makers such as TurboPower software. A number of third-party grids with
extended capabilities are available on the market, and some of them are well worth the pur-
chase price.
New features of the TdbGrid object not found in Delphi 1.0 include the ability to add combo
boxes to the grid, and to color the columns of your grids. Both of these processes are discussed
elsewhere in this chapter, particular in the next section on Lookups.
Lookup Fields
You can use Lookup fields to look up a value in one table that you want to use in a second
table. For instance, suppose you had two tables, one of which contained a list of books, and the
other contained a list of authors. It would be nice if you could automatically view a list of the
existing authors whenever you needed to add a new book to the Book table. That way you
could enter the book’s name, then just look up the author in a dropdown list, and presto, you
would be done. The Book table would then automatically contain a reference to the appropri-
ate author in the Author table. That is, the author number from the Author table would auto-
matically be inserted in the Book table.
Needless to say, Delphi gives good support for using Lookup fields. You can now perform
automatic lookups inside grids, list boxes and combo boxes. The Lookup program on disk shows
how to proceed. The listings for this application are shown in Listings 17.3 through 17.4. Two
views of the program are shown in Figures 17.14 and 17.15.
FIGURE 17.14.
The AuthorLookup field in
the grid for the Books table
is a lookup into the Authors
table.
FIGURE 17.15.
This form features a list
box and combo box that let
you performs lookups from
the Book table into the
Author table.
NOTE
Delphi 1.0 had a dbLookupCombo control and a dbLookupList control that had
certain limited capabilities. Both of these controls are still present in Delphi 2.0, but
they have been moved off the Data Controls page onto the Win 3.1 page. They are
being kept around solely for compatibility with 16-bit programs. The
dbLookupComboBox control and the dbLookupListBox control now replace the old
16-bit controls, and they outperform them on several fronts. In particular, the
dbLookupComboBox and dbLookupListBox will automatically be filled up with the
data from the lookup table. Don’t confuse the old control with the new ones! This is
an area where Delphi has become much more powerful in version 2.0 than it was
version 1.0.
Listing 17.3. The main work for the program is done in the Object Inspector for
the TData object.
unit DMod;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, DB,
DBTables, Menus;
type
TData = class(TDataModule)
tblAuthor: TTable;
tblAuthorAuthNo: TIntegerField;
tblAuthorFirst: TStringField;
tblAuthorLast: TStringField;
tblAuthorDates: TStringField;
tblAuthorLastFirst: TStringField;
dsAuthor: TDataSource;
dsBook: TDataSource;
tblBookBookNo: TIntegerField;
tblBookAuthNo: TIntegerField;
tblBookTitle: TStringField;
tblBookAuthorLook: TStringField;
tblAuthorBirthPlace: TStringField;
tblBook: TTable;
procedure tblAuthorCalcFields(DataSet: TDataSet);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Data: TData;
implementation
{$R *.DFM}
end.
Listing 17.4. Form1 gives you a look at both the Author table and the Book
table. A dropdown in dbGrid2 lets you view the lookup field.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, DB,
Grids, DBGrids, DBTables,
DBCtrls, Menus;
type
TForm1 = class(TForm)
DBGrid1: TDBGrid;
DBGrid2: TDBGrid;
MainMenu1: TMainMenu;
File1: TMenuItem;
ViewEntryForm1: TMenuItem;
OneToMany1: TMenuItem;
Exit1: TMenuItem;
procedure Form21Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
DMod, fieldvew;
{$R *.DFM}
end.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, ExtCtrls,
DBCtrls, StdCtrls, Mask;
type
TForm2 = class(TForm)
Panel1: TPanel;
DBEdit3: TDBEdit;
DBLookupComboBox1: TDBLookupComboBox;
DBLookupListBox1: TDBLookupListBox;
DBNavigator1: TDBNavigator;
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
uses dmod;
{$R *.DFM}
end.
The Lookup program enables you to easily fill in the key fields of the Book table by looking
them up in the Author table. To understand why this capability is important, notice that the
only way to tell which author is associated with which book is by placing the appropriate au-
thor number in the AuthNo field of the Book table. This is convenient from the point of view
of the programmer who wants to construct a well-made relational database. In particular, it
allows for saving space through the construction of one-to-many relationships. However, the
user isn’t going to want to have to remember that Herman Melville is associated with the num-
ber 2, Jack Kerouac with the number x, and so on. The point of a lookup field is that it lets you
look up a list of authors in the author table, then automatically assigns the chosen author num-
ber to the AuthNo field in the book table.
This program uses two tables called, not surprisingly, AUTHOR.DB and BOOK.DB. Both
of these tables are found on the CD that accompanies this book. Tables 17.1 and 17.2 show
the schema for the tables.
NOTE
Notice the use of the AutoIncrement fields in the table definitions shown in Tables
17.1 and 17.2. These fields will automatically be filled in when the user adds a new
record at runtime. For instance, when you add the first record to the Books table, it
will automatically be given a BookNo of 1. The second record will automatically be
given a BookNo of 2, and so on. AutoIncrement fields are read-only, and frequently
there is no need to show them to the user at runtime.
There is little actual work required to construct this program. In particular, look over the source
code shown earlier, and you will see that the only significant line of code in the whole program
is the one for the OnCalcFields event. Other than that, it’s just a matter of manipulating the
visual tools.
To get started, create a new application and add a data module to it. Drag both the Authors
and Books tables out of the Explorer and onto a form. Cut and paste them from there onto the
data module. Bring up the Fields Editor for both tables and create objects for all of their fields.
Give the tables and their datasources appropriate names, as shown in Figure 17.16.
FIGURE 17.16.
The datamodule for the
Lookup program.
Inside the Author table, create a Calculated Field called LastFirst. To create the calculated field,
first right-click the TTable object, then right-click the Field’s editor and select New from the
menu. After creating the calculated field, assign the following method to the OnCalcFields event:
procedure TData.tblAuthorCalcFields(DataSet: TDataSet);
begin
tblAuthorLastFirst.Value := tblAuthorLast.Value + ‘, ‘ +
tblAuthorFirst.Value;
end;
This field will be the one that is looked up in the second table. The issue here is that just look-
ing up the last name of an author is not sufficient—you need to look up both first and last
names in order to be sure you are finding a unique author. It would be wasteful of disk space
to permanently add a field to the table that combined the first and last names, but you can
create a temporary copy of that field with a calculated field.
Bring up the Fields Editor for the Book table. Right-click it and create a new field called
AuthorLookup. Set its Type to string and its Field Type to Lookup. The KeyField should be
set to AuthNo, the Dataset to tblAuthor, the Lookup Key to AuthNo, and the Result field to
LastFirst. Figure 17.17 shows how the New Field dialog should look when you are done. Notice
that you can also fill in this same information in the Object Inspector if you first select the
tblBookAuthorLookup object. (In other words, you could create a new object and then close
the Fields Editor without specifying any of its properties. Later, you could select the object and
designate its type, its lookup fields, and so on.)
FIGURE 17.17.
Filling in the New Field
dialog.
Go back to Form1 and make sure the two dbGrids are arranged one above the other and are
hooked up properly to the tables on the datamodule. Run the application.
The AuthorLookup field in the dbGrid object associated with the Book table is now a dropdown
combo box. If you click it once, then dropdown its list, you can then perform a lookup into
the LastFirst field of the Author table. This lookup will automatically fill in the AuthNo field
of the book table. You can use this lookup to insert a new author into a new record, or to change
the author of an existing record.
NOTE
The capability to have a dropdown list in a grid object comes for free in Delphi 2.0.
Go back in design mode, and open up the Columns property of a grid object. Add all
the fields to the Columns list box. You can now select one of the fields, such as Title,
and choose the PickList button in order to create a set of default values available for the
field. The user can access these values at runtime by clicking the field and dropping
down the combo box, per the lookup example discussed above.
Besides the dbGrid object, there are two other controls in Delphi that understand lookup fields.
Both of these controls are shown on Form2. The first is a TdbLookupComboBox, which is the
default control you will get if you drag and drop the AuthorLookup field from the Fields Edi-
tor onto a form. If you perform the drag-and-drop operation, then the control will be hooked
up automatically. If you want to hook it up manually, just connect its DataSource to the dsBook
object and its DataField to the AuthorLookup field. There is also a TdbLookupListBox, which
works exactly the same way as the TdbLookupComboBox.
NOTE
When you are working with the Lookup program, you should note that Form1 is a bit artifi-
cial. It’s meant to help you scan through all the available data so you can grok the significance
of the lookup process. Form2 is somewhat closer to the type of display you would want to present
to the user in a real program.
When working with Form2, notice how easy it is to simply type in a new book name, then
click an author in the list box, and click the Post button on the TdbNavigator. The process is
very simple from the user’s point of view. In particular, a new BookNo is being assigned auto-
matically by the AutoIncrement field, and the new AuthNo is being filled in automatically by
the lookup process.
If you want to change the author associated with a particular record, then you just click a new
item in the list box. The author number will be changed automatically for you by the lookup.
It’s all very simple and intuitive when viewed from the user’s perspective.
MultiRecord Objects
Another object that deserves mention is the dbCtrlGrid, shown in Figure 17.8. You can use
this object to view multiple records from a single table at one time without using the TdbGrid
component. In other words, you can dropdown TdbEdit controls onto a TdbCtrlGrid, and
these edit controls will automatically be duplicated in a series of rows, where the first set of
controls shows the first record, the second set the second record, and so on. You only have to
dropdown one set of controls—the extra sets are duplicated for you automatically by the
dbCtrlGrid, as shown in Figure 17.18.
FIGURE 17.18.
The TdbCtrlGrid object
on the form the CtrlGrid
application.
To get started with this object, drag and drop the Country table off the Explorer. Delete the
dbGrid object, and add the TdbCtrlGrid object off the Data Controls page of the Component
Palette. Use the Fields Editor to drag and drop all the fields from Country table onto the top
section of the dbCtrlGrid. Arrange them as shown in Figure 17.18. Notice that dbCtrlGrids
have RowCount and ColCount properties that enable you to define the number of rows and col-
umns in the object. In this case, I have set the RowCount to 7.
The dbCtrlGrid component doesn’t bring any new functionality to Delphi. It’s useful, how-
ever, as it eliminates the need to have the user slide the scrollbar back and forth on a TdbGrid
object. In other words, the TdbGrid object sometimes forces you to use the scrollbar in order
to view all the fields of a record. That can be inconvenient, but the ability to view multiple
records at once is so valuable that users are willing to put up with the minor annoyance of the
scrollbar. The point of the dbCtrlGrid object is that it lets you view multiple records at one
time, while eliminating the need to scroll back and forth when viewing the data. It’s hardly
earth-shattering in its importance, but it can be very useful under some circumstances. It’s a
way to make the presentation of your data potentially more viable to the user.
Summary
In this chapter you learned some fairly sophisticated methods for displaying the data from
multiple tables. In particular, you saw how Delphi handles the key features of a relational da-
tabase.
The tools discussed in this chapter include
■ The Fields Editor
■ The Database Explorer
■ The Database Desktop
■ The Query by Example tool in the DBD
The components discussed in this chapter include
■ TField descendant objects
■ The TdbCtrlGrid, TdbLookupComboBox, TdbLookupListBox objects
■ The TdbGrid component
The properties discussed in this chapter include
■ Calculated fields
■ Lookup fields
Good database programmers will find that there is a considerable amount of hidden power in
the TField object and in the Fields Editor, as well as the other tools and components men-
tioned in this chapter. These database tools represent a very strong aspect of Delphi, which is
further expanded on by the work of third-party developers such as TurboPower software,
Woll2Woll, and Shoreline.
Real-World
Databases 18
In this chapter, you will get a look at a simple address book program called ADDRESS2. This
program is designed to represent the simplest possible database program that is still usable in a
real-world situation.
In particular, you will look at the following:
■ Sorting data
■ Filtering data
■ Searching for data
■ Printing data
■ Dynamically moving a table in and out of a read-only state
■ Forcing the user to select a field’s value from a list of valid responses
■ Allowing the user to choose the colors of a form at runtime
■ Saving information to an .INI file
After finishing this chapter, you will have learned something about the kinds of problems ex-
perienced when writing even a very basic database program. The final product, though not
quite up to professional standards, provides solutions to many of the major problems faced by
programmers who want to create tools that can be used by the typical user.
You will find that the final program is relatively long when compared to most of the programs
you have seen so far in this book. The reason is that this program is meant not just to show you
how to perform a particular action, but to show you how to accomplish some of the basic ef-
fects expected in real-world settings and provide a minimum degree of robustness.
programmers will find themselves in the same boat. The 16-bit world will apparently not die
too quickly.
After making this list and contemplating it for a moment, you might ask the following ques-
tions:
■ What about complex addresses that can’t be written on one line?
■ Is one phone number enough? What about times when you need a home phone and a
work phone?
■ Speaking of work, what about specifying the name of the company that employs
someone on the list?
■ What about faxes?
■ This is the 1990s, so what about an e-mail address?
■ What about generic information that doesn’t fit into any of these categories?
This list of questions emerges only after a period of gestation. In a real-world situation, you
might come up with a list of questions like this only after you talk with potential users of your
program, after viewing similar programs that are on the market, and after experimenting with
a prototype of the proposed program. Whichever way you come up with questions, the key
point is that you spend the time to really think about the kind of data you need.
NOTE
Many books will tell you to complete your plan before you begin programming. The
only thing wrong with that theory is that I have never seen it work out as expected in
practice.
Nearly all the real-world programs that I have seen, both my own and others, whether
produced by individuals or huge companies, always seem to go through initial phases
that are later abandoned in favor of more sophisticated designs. This is part of what
Delphi and RAD programs in general are all about. They make it possible for you to
create a draft of your program and then rewrite it.
Think hard about what you want to do, then get a prototype up in fairly short order,
critique it, then rethink your design. It’s rarely necessary to totally abandon the first
draft, but you are almost certainly going to have to rewrite. This is why it’s not a good
idea to concentrate on details at first. Get things up and running, then if they look
okay, go back and optimize.
The point is that it’s an iterative process. You keep rewriting, over and over, the same
way authors keep rewriting the chapters in their books. RAD programming tools help
make this kind of cycle possible.
If you have to write everything in code from scratch, then you have to try to get it right
the first time. That’s almost impossible to do, which is why so many programming
projects have failed. The issue was that people were using the wrong tools. The
interesting thing about Delphi is that the same tool that lets you prototype quickly is
also the tool that lets you optimize down to the last clock cycle.
After considering the preceding questions, you might come up with a revised list of fields for
your program:
First Name
Last Name
Company
Address1
Address2
City
State
Zip
Home Phone
Work Phone
Fax
EMail1
EMail2
Comment
This list might actually stand up to the needs of a real-world user. Certainly it doesn’t cover all
possible situations, but it does represent a reasonable compromise between the desire to make
the program easy to use and the desire to handle every possible variety of potential user de-
mand.
At this stage, you might start thinking about some of the basic functionality you want to asso-
ciate with the program. For instance, you might decide that a user of the program should be
able to search, sort, filter, and print the data. After stating these needs, you’ll find that there is
going to be a need to break the data up into various categories so that it can be filtered. The
question, of course, is how these categories can be defined.
After considering the matter for some time, you might decide that two more fields should be
added to the list. The first field can be called Category, and it holds a name that describes the
type of record currently being viewed. For instance, some entries in an address book might
consist of family members, whereas other entries might reference friends, associates from work,
companies where you shop, or other types of data. A second field will be called Marked, and it
designates whether a particular field is marked for some special processing.
Here is the revised list, with one additional field called Category, which is used to help the user
filter the data he or she might be viewing:
First Name
Last Name
Company
Address1
Address2
City
State
Zip
Home Phone
Work Phone
Fax
EMail1
EMail2
Comment
Category
Marked
After carefully considering the fields that might be used in the ADDRESS2 program, the next
step is to decide how large and what type the fields should be. Table 18.1 shows proposed types
and sizes.
Table 18.1. The length, type, and size of fields used by the ADDRESS2 program.
Name Type Size
FName Character 40
LName Character 40
Company Character 40
Address1 Character 40
Address2 Character 40
City Character 40
State Character 5
Zip Character 15
HPhone Character 15
continues
WPhone Character 15
Fax Character 15
EMail1 Character 45
EMail2 Character 45
Comment Memo 20
Category Character 15
Marked Logical
As you can see, I prefer to give myself plenty of room in all the fields I declare. In particular,
notice that I have opted for wide EMail fields to hold long Internet addresses, and I have de-
cided to make the Comment field a memo field so that it can contain long entries, if necessary.
The names of some of the fields have also been altered so that they don’t contain any spaces.
This might prove useful if the data is ever ported to another database.
Now that you have decided upon the basic structure of the table, the next thing is to work out
some of the major design issues. In particular, the following considerations are important:
■ The program should run off local tables, because this is the kind of tool likely to be
used on individual PCs rather than on a network. It’s a toss-up as to whether to use
Paradox or dBASE tables, but I’ll opt to use Paradox tables because they provide a bit
more flexibility.
■ The user should be able to sort the table on the FName, LName, and Company fields.
■ It should be possible to search on the FName, LName, and Company fields.
■ The user should be able to set up filters based on the Category field.
■ It should be absolutely clear whether or not the table is editable, and the user should
be able to move in and out of read-only mode easily.
■ It should be possible to print the contents of the table based on the filters set up by
the Category field.
■ It’s very difficult to choose a set of colors that will satisfy all tastes, so the user should
be able to set the colors of the main features in the program.
A brief consideration of the design decisions makes it clear that the table should have a primary
index on the first three fields and secondary indexes on the FName, LName, Company, and
Category fields. The primary index can be used in place of a secondary index on the FName
field, but the intent of the program’s code will be clearer if a secondary index is used for this
purpose. In other words, the code will be easier to read if it explicitly sets the IndexName to
something called FNameIndex rather than simply defaulting to the primary index. Table 18.2
shows the final structure of the table. The three stars in the fourth column of the table show
the fields that are part of the primary index.
Now that there is a clear picture of the type of table that needs to be created, you can open up
the Database Desktop and create the table, its primary index, and its four secondary indexes.
When you are done, the structure of the table should look like that in Figure 18.1. You can
save the table under the name ADDRESS.DB.
Here is another way of looking at the indexes for this table:
Primary Index
FName
LName
Company
Category Index
Category
Company Index
Company
FName
LName
LName Index
LName
FNameCompany
{the sort order should be Company, LName then FName. The last name is more
significant than the first name.}
FIGURE 18.1.
Designing the main table
for the ADDRESS2
program.
FIGURE 18.2.
The main form for the
ADDRESS2 program.
The top of the program contains a menu with the following format:
Caption = ‘File’
Caption = ‘Print’
Caption = ‘-’
Caption = ‘Exit’
Caption = ‘Options’
Caption = ‘Delete’
Caption = ‘Search’
Caption = ‘Filter’
Caption = ‘Set Category’
Caption = ‘Sorts’
Caption = ‘First Name’
Caption = ‘Last Name’
Caption = ‘Company’
Caption = ‘Colors’
Caption = ‘Form’
Caption = ‘Edits’
Caption = ‘Edit Text’
Caption = ‘Labels’
Caption = ‘Panels’
Caption = ‘Marked’
Caption = ‘Mark All’
Caption = ‘Clear All Marks’
Caption = ‘Print Marked to File’
Caption = ‘Help’
Caption = ‘About’
Each line represents the caption for one entry in the program’s main menu. The indented
portions are the contents of the dropdown menus that appear when you select one of the menu
items visible in Figure 18.2.
After creating the program’s interface, dropdown a TTable and TDataSource, wire them up to
ADDRESS.DB, and hook up the fields to the appropriate data-aware control. To make this
work correctly, you should create an alias, called Address, that points at the location of
ADDRESS.DB.
The only tricky part of this process involves the Category field, which is connected to the
TdbComboBox. You make the basic connection to the TdbComboBox by setting its DataSource
field to DataSource1 and its DataField to Category.
If you run the program, you will find that the TdbComboBox for the Category field does not
contain any entries. The purpose of this control is to enable the user to select categories from
a prepared list, rather than forcing the user to make up categories on the fly. The list is needed
to prevent users from accidentally creating a whole series of different names for the same gen-
eral purpose. For instance, if you want to be able to set a filter for the program, which presents
you with a list of friends, you should create a category called Friend and assign it to all the
members of the list that fit that description. If you always choose this category from a dropdown
list, it will presumably always be spelled the same. However, if you rely on users to type this
word, you might get a series of related entries that look like this:
Friend
Friends
Frends
Acquaintances
Buddies
Buds
Homies
HomeBoys
Amigos
Chums
Cronies
Companions
This mishmash of spellings and synonyms won’t do you any good when you want to search for
the group of records that fits into the category called Friend.
The simplest way to make the TdbComboBox perform its required function is to simply pop
open the Property Editor for the Items property and type in a list of categories such as the fol-
lowing:
Home
Work
Family
Local Business
Friend
Now when you run the program and drop down the Category combo box, you will find that
it contains the preceding list.
The only problem with typing names directly into the Items property for the TdbComboBox
is that it is impossible to change this list at runtime. To do away with this difficulty, the pro-
gram stores the list in a separate table, called CATS.DB. This table has a single character field
that is 20 characters wide. After creating the table in the Database Desktop, you can enter the
following five strings into five separate records:
Home
Work
Family
Local Business
Friend
The program loads these records into the TdbComboBox during the OnCreate method for the
main form:
Table2.Open;
Table2.First;
while not Table2.EOF do begin
cbCategory.Items.Add(Table2.Fields[0].AsString);
Table2.Next;
end;
Table2.Close;
This code makes use of a second TTable object dropped onto the form. This object has its
DatabaseName set to the address alias and its TableName property set to the CATS table.
NOTE
In the last chapter, you saw the TdbLookupComboBox, which is smart enough to load
itself with values automatically. I’m not using that here because I still use the AD-
DRESS2 program on some 16-bit machines. That means I need to be careful not to
include features that work only in Delphi 2.0.
To enable the user to change the contents of the CATS table, you can create a form like that
shown in Figure 18.3. This form needs only minimal functionality because it’s best to discour-
age the user from changing the list except when absolutely necessary. Note that you need to
add the CATS unit to the uses clause in order for this code to run.
FIGURE 18.3.
The Category form enables
the user to alter the contents
of CATS.DB.
There is no need for the Category dialog, and the memory associated with it, to be created and
allocated at program startup. As a result, you should open the Options | Project menu, select
the Forms page, and move the Category dialog into the Available Forms column. In response
to a selection of the Set Category menu item from the main form of the ADDRESS2 program,
you can write the following code:
procedure TfrAddress.Category1Click(Sender: TObject);
begin
CatsDlg := TCatsDlg.Create(Self);
CatsDlg.ShowModal;
CatsDlg.Free;
end;
This code creates the Category dialog, shows it to the user, and finally deallocates its memory
after the user is done. This approach is taken because it assures that the Category dialog and
CATS.DB are only in memory when absolutely necessary.
The skeletal structure of the ADDRESS2 program is starting to come together. However, there
is one remaining task that must be completed before the core of the program is complete. A
number of basic commands are issued by the program, and these can be defined in a single
enumerated type:
TCommandType = (btClose, btInsert, btPrior,
btEdit, btNext, btCancel,
btPrint, btFirst, btLast);
This type enables you to associate each of the program’s commands with the Tag field of the
appropriate button or menu item, and then to associate all of these buttons or menu items
with a single method that looks like this:
procedure TfrAddress.CommandClick(Sender: TObject);
begin
case TCommandType((Sender as TComponent).Tag) of
btClose: Close;
btInsert: tblAddress.Insert;
btPrior: tblAddress.Prior;
btEdit: HandleEditMode;
btNext: tblAddress.Next;
btCancel: tblAddress.Cancel;
btPrint: PrintData;
btFirst: tblAddress.First;
btLast: tblAddress.Last;
end;
end;
All of the code in this program will compile at this stage except for the references to
HandleEditMode and PrintData. For now, you can simply create dummy HandleEditMode and
PrintData private methods and leave their contents blank.
There is no reason why you can’t have a different method associated with each of the buttons
and menu items in the program. However, it’s neater and simpler to handle things this way,
and the code you create is much easier to read. The key point here is to be sure that the Tag
property of the appropriate control gets the correct value and that all of the controls listed here
have the OnClick method manually set to the CommandClick method.
Table 18.3 gives a brief summary of the commands passed to the CommandClick method:
At this stage, you are ready to run the ADDRESS2 program. You can now insert new data,
iterate through the records you create, cancel accidental changes, and shut down the program
from the menu. This is the bare functionality needed to run the program.
NOTE
The program as it exists now is what I mean by creating a “rough draft” of a program.
It gets the raw functionality of the program up and running with minimum fuss, and it
lets you take a look at it to see if it passes muster.
If you were working for a third-party client, or for a demanding boss, this would be the
time to call the person or persons in question and have them critique your work.
“Is this what you are looking for?”, you might ask. “Are there any fields you think need
to be there that aren’t yet visible? Do you feel that the project is headed in the right
direction?”
Nine times out of ten, they will come back to you with a slew of suggestions, most of
which have never occurred to you. If there are irreconcilable differences of opinion
about the project, this is the time to find out. If there are some good ideas you never
considered, now is the time to add them.
This is also your chance to let everyone know that after this point, it may become
impossible to make major design changes. Let them know that you are about to start
doing the kind of detail work that is very hard to undo. If people need a day or two to
think about your proposed design, give it to them. It’s better to make changes now, at
the start, than after you have everything polished and spit-shined. By presenting them
with a prototype, you give them a sense of participating in the project, which at least
potentially puts them on your side when you turn in the finished project.
Listing 18.1. The source code for the ADDRESS2 program has several different
modules.
unit Main;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Controls, Printers,
Forms, ExtCtrls, StdCtrls,
DB, DBTables, DBCtrls,
Mask, Menus, Dialogs,
DBLookup, Report, Search, Buttons;
const
mfClearMarks = 100;
mfMarkAll = 101;
ReadOnlyStr = ‘ [Read Only Mode]’;
EditModeStr = ‘ [Edit Mode]’;
type
TColorType = (ccForm, ccEdit, ccEditText, ccLabel, ccPanel);
TChangeType = (tcColor, tcFontColor);
TCommandType = (btClose, btInsert, btPrior,
btEdit, btNext, btCancel,
btPrint, btFirst, btLast);
TfrAddress = class(TForm)
tblAddress: TTable;
Panel1: TPanel;
ELName: TDBEdit;
EFName: TDBEdit;
Panel2: TPanel;
BNext: TButton;
BPrevious: TButton;
EAddress1: TDBEdit;
EAddress2: TDBEdit;
ECity: TDBEdit;
EState: TDBEdit;
EZip: TDBEdit;
ECompany: TDBEdit;
Insert: TButton;
EEdit: TButton; {Shouldn’t this just be called ‘Edit’}
DataSource1: TDataSource;
MainMenu1: TMainMenu;
File1: TMenuItem;
Print1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
Options1: TMenuItem;
Search1: TMenuItem;
Help1: TMenuItem;
About1: TMenuItem;
First: TButton;
Last: TButton;
Sorts1: TMenuItem;
FirstName1: TMenuItem;
LastName1: TMenuItem;
Company1: TMenuItem;
EHomePhone: TDBEdit;
EWorkPhone: TDBEdit;
EFax: TDBEdit;
EEMail1: TDBEdit;
Delete1: TMenuItem;
Label3: TLabel;
Address1: TLabel;
Address2: TLabel;
City: TLabel;
State: TLabel;
Zip: TLabel;
Company: TLabel;
HPhone: TLabel;
WPhone: TLabel;
Fax: TLabel;
continues
procedure PrintData;
procedure SetCategoryList;
procedure SetEdits(TypeChange: TChangeType; NewValue: TColor);
procedure SetIni(CType: TColorType; C: TColor);
procedure SetLabels(C: TColor);
procedure SetPanels(C: TColor);
procedure SetReadOnly(NewState: Boolean);
procedure WriteRecord(var F: System.Text);
end;
var
frAddress: TfrAddress;
implementation
uses
About, Cats, Filter,
IniFiles, StrBox, SysUtils,
UtilBox, ClipBrd;
{$R *.DFM}
{$R BITS.RES}
continues
CheckForIni;
for i := 0 to 4 do
case TColorType(i) of
ccForm: Color := GetIni(TColorType(i));
ccEdit: SetEdits(tcColor, GetIni(TColorType(i)));
ccEditText: SetEdits(tcFontColor, GetIni(TColorType(i)));
ccLabel: SetLabels(GetIni(TColorType(i)));
ccPanel: SetPanels(GetIni(TColorType(i)));
end;
tblAddress.Open;
DataSource1.AutoEdit := False;
SetCategoryList;
end;
procedure TfrAddress.CheckForIni;
var
F: System.Text;
Path: string;
begin
Path := GetPath + ‘address.ini’;
if not FileExists(Path) then begin
System.Assign(F, Path);
System.ReWrite(F);
System.WriteLn(F, ‘[Colors]’);
System.WriteLn(F, ‘Form=7045184’);
System.WriteLn(F, ‘Edits=8421376’);
System.WriteLn(F, ‘EditText=0’);
System.WriteLn(F, ‘Labels=0’);
System.WriteLn(F, ‘Panels=12639424’);
System.Close(F);
end;
end;
procedure TFrAddress.DoSort;
begin
case FSortType of
stFirst: tblAddress.IndexName := ‘FNameIndex’;
stLast: tblAddress.IndexName := ‘LNameIndex’;
stCompany: tblAddress.IndexName := ‘CompanyIndex’;
end;
end;
continues
procedure TfrAddress.HandleEditMode;
begin
Insert.Enabled := not DataSource1.AutoEdit;
Cancel.Enabled := not DataSource1.AutoEdit;
Delete1.Enabled := not DataSource1.AutoEdit;
if not DataSource1.AutoEdit then begin
SetReadOnly(True);
EEdit.Caption := ‘ReadOnly’;
Caption := FCaptionStr + EditModeStr;
end else begin
if tblAddress.State <> dsBrowse then tblAddress.Post;
SetReadOnly(False);
EEdit.Caption := ‘Edit’;
Caption := FCaptionStr + ReadOnlyStr;
end;
end;
procedure TfrAddress.PrintData;
begin
Cursor := crHourGlass;
Report1.ReportDir := GetPath;
Report1.Run;
Cursor := crDefault;
end;
procedure TfrAddress.SetCategoryList;
begin
Table2.Open;
while not Table2.EOF do begin
cbCategory.Items.Add(Table2.Fields[0].AsString);
Table2.Next;
end;
Table2.Close;
end;
continues
{ ––––––––––––––––––––––––––––––––––––––––––
Toggle the Boolean value of the marked field
––––––––––––––––––––––––––––––––––––––––––}
procedure TfrAddress.SpeedButton1Click(Sender: TObject);
begin
tblAddress.Edit;
tblAddress.FieldByName(‘Marked’).AsBoolean :=
not tblAddress.FieldByName(‘Marked’).AsBoolean;
tblAddress.Post;
end;
WriteLn(F, FieldByName(‘Zip’).AsString);
WriteLn(F);
WriteLn(F, ‘*************’);
end;
end;
continues
end.
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
StdCtrls, Buttons, ExtCtrls;
type
TAboutBox = class(TForm)
Panel1: TPanel;
OKButton: TBitBtn;
ProgramIcon: TImage;
ProductName: TLabel;
Version: TLabel;
Copyright: TLabel;
Comments: TLabel;
private
{ Private declarations }
public
{ Public declarations }
end;
var
AboutBox: TAboutBox;
implementation
{$R *.DFM}
end.
Listing 18.3. The Cats dialog enables the user to work with the category table.
unit Cats;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
Buttons, StdCtrls, Grids,
DBGrids, DB, DBTables,
ExtCtrls;
type
TCatsDlg = class(TForm)
OKBtn: TBitBtn;
CancelBtn: TBitBtn;
HelpBtn: TBitBtn;
Bevel1: TBevel;
tblCats: TTable;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
end;
var
CatsDlg: TCatsDlg;
implementation
{$R *.DFM}
end.
Listing 18.4. The Filter dialog enables the user to set a range on the visible
records.
unit Filter;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons;
type
TFilterDlg = class(TForm)
ListBox1: TListBox;
BitBtn1: TBitBtn;
BitBtn2: TBitBtn;
private
{ Private declarations }
public
{ Public declarations }
function GetFilter(SList: TStrings): string;
end;
var
FilterDlg: TFilterDlg;
implementation
{$R *.DFM}
end.
Listing 18.5. The Search dialog lets the user decide which fields to search on.
unit Search;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
Buttons, StdCtrls, ExtCtrls;
type
TSortType = (stFirst, stLast, stCompany);
TSearchDlg = class(TForm)
OKBtn: TBitBtn;
CancelBtn: TBitBtn;
HelpBtn: TBitBtn;
Bevel1: TBevel;
Edit1: TEdit;
GroupBox1: TGroupBox;
FirstName: TRadioButton;
LastName: TRadioButton;
Company: TRadioButton;
Label1: TLabel;
procedure FormActivate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
function GetSearchStr(var ST: TSortType; var S: string): Boolean;
end;
var
SearchDlg: TSearchDlg;
implementation
{$R *.DFM}
S := Edit1.Text;
if FirstName.Checked then
ST := stFirst
else if LastName.Checked then
ST := stLast
else
ST := stCompany;
Result := True;
end;
end.
It’s important to note that a lot of the code ends up being considerably less formidable than it
might appear at first glance. For instance, the code for the About dialog or the Category dialog
is primarily created visually with very little effort on your part. Even the Search dialog, which
is perhaps the most complicated of the utility dialogs, really consists of nothing more than a
dozen fairly straightforward lines of code. The code for the main form is a bit more meaty, but
most of it will become fairly transparent if you study the logic one step at a time.
The HandleEditMode routine is called from the CommandClick method described in the last sec-
tion:
procedure TfrAddress.HandleEditMode;
begin
Insert.Enabled := not DataSource1.AutoEdit;
Cancel.Enabled := not DataSource1.AutoEdit;
Delete1.Enabled := not DataSource1.AutoEdit;
if not DataSource1.AutoEdit then begin
SetReadOnly(True);
EEdit.Caption := ‘ReadOnly’;
Caption := FCaptionStr + EditModeStr;
end else begin
if tblAddress.State <> dsBrowse then tblAddress.Post;
SetReadOnly(False);
EEdit.Caption := ‘Edit’;
Caption := FCaptionStr + ReadOnlyStr;
end;
end;
The primary purpose of this code is to ensure that the proper components are enabled or dis-
abled, depending on the current state of the program. After altering the appearance of the pro-
gram, the code calls SetReadOnly:
The center around which both of these routines revolve is the DataSource1.AutoEdit property.
When this property is set to False, all the data-aware controls on the form are disabled and the
user won’t be able to type in them, as shown in Figure 18.4. When the property is set to True,
the data becomes live and the user can edit or insert records, as shown in Figure 18.5.
FIGURE 18.4.
ADDRESS2 as it appears
in read-only mode.
The purpose of the AutoEdit property is to determine whether or not a keystroke from the user
can put a table directly into edit mode. When AutoEdit is set to False, the user can’t type infor-
mation into a data-aware control. When AutoEdit is set to True, the user can switch the table
into edit mode simply by typing a letter in a control. It’s important to note that even when
AutoEdit is set to False, you can set a table into edit mode by calling tblAddress.Edit or
tblAddress.Insert . As a result, the technique shown here won’t work unless you gray out the
controls that give the user the power to set the table into edit mode. (See Figure 18.5.)
The code in the HandleEditMode method is concerned entirely with interface issues. It enables
or disables the Insert, Cancel, and Delete controls, depending on whether the table is about to
go in or out of read-only mode. The code also ensures that the caption for the Edit button
provides the user with a clue about the button’s current function. In other words, the
button doesn’t report on the state of the program, but on the functionality associated with the
button.
The HandleEditMode method is written so that the program is always moved into the opposite
of its current state. At startup time the table should be set to read-only mode (AutoEdit = False),
and the appropriate controls should be disabled. Some of the code in the FormCreate method
ensures that this is, in fact, the state of affairs at startup. Thereafter, every time you press the
Edit button, the program will switch from its current state to the opposite state, from read-
only mode to edit mode, and then back again.
FIGURE 18.5.
The ADDRESS2 program
as it appears in edit mode.
NOTE
In addition to the TDataSource AutoEdit property, there is a second way to take a table
in and out of read-only mode. This second method is really more powerful than the
first, because it makes the table itself completely resistant to change. However, this
second method is a bit more costly in terms of time and system resources. The trick,
naturally enough, is to change the ReadOnly property of a TTable component. Here is
how the SetReadOnly procedure would look if you employed this second technique:
procedure TfrAddress.SetReadOnly(NewState: Boolean);
var
Bookmark: TBookmark;
begin
Bookmark := tblAddress.GetBookMark;
tblAddress.Close;
tblAddress.ReadOnly := NewState;
tblAddress.Open;
tblAddress.GotoBookMark(Bookmark);
tblAddress.FreeBookmark(Bookmark);
end;
It turns out that you cannot set a table in or out of read-only mode while it is open.
Therefore, you have to close the table every time you change the ReadOnly property.
Unfortunately, every time you close and open a table, you are moved back to the first
record. As a result, it is necessary to set a bookmark identifying your current location in
the table, close the table, and then move the table in or out of read-only mode. When
you are done, you can open the table and jet back to the bookmark. This sounds like
quite a bit of activity, but in fact it can usually be accomplished without the user being
aware that anything untoward has occurred. The entire functionality described in this
paragraph is encapsulated in the second version of the SetReadOnly method, shown
previously.
If you choose this second method, you would have to set the tblAddress.ReadOnly
property to True at program startup and rewrite the HandleEditMode method so that it
looks like this:
procedure TfrAddress.HandleEditMode;
begin
Insert.Enabled := tblAddress.ReadOnly;
Cancel.Enabled := tblAddress.ReadOnly;
Delete1.Enabled := tblAddress.ReadOnly;
if tblAddress.ReadOnly then begin
SetReadOnly(False);
EEdit.Caption := ‘ReadOnly’
end else begin
if tblAddress.State <> dsBrowse then tblAddress.Post;
SetReadOnly(True);
EEdit.Caption := ‘Edit’;
end;
end;
With the ADDRESS2 program, it is clear that the first technique for moving a
program in and out of read-only mode is best. In other words, it’s much faster and
much easier to switch DataSource1 in and out of AutoEdit mode than it is to switch
tblAddress in and out of read-only mode. However, I have shown you the second
technique because there may be a time when you have to quickly open and close a
table, and it is useful to have a routine to use in such cases.
On the whole, the act of moving ADDRESS2 in and out of read-only mode is fairly trivial.
The key point to grasp is the power of the TDataSource AutoEdit method. If you understand
how it works, you can provide this same functionality in all your programs.
Sorting Data
At various times, you might want the records stored in the program to be sorted by first name,
last name, or company. These three possible options are encapsulated in the program’s menu,
as depicted in Figure 18.6, and also in an enumerated type declared in SEARCH.PAS:
TSortType = (stFirst, stLast, stCompany);
FIGURE 18.6.
The Sorts menu has three
different options.
Once again, it is the Tag field from the Sorts dropdown menu that makes it possible to detect
which option the user wants to select:
procedure TfrAddress.SortMenu(Sender: TObject);
begin
FSortType := TSortType((Sender as TComponent).Tag);
DoSort;
DoSearch(‘A’);
end;
After sorting, there may be a group of blank records at the beginning of the table. For instance,
if you choose to sort by the Company field, it is likely that many of the records in the AD-
DRESS table will not have anything in the company field. As a result, there could be several
hundred, or even several thousand, records at the beginning of the table that are of no interest
to someone who wants to view only companies. The solution, of course, is to search for the
first record that has a non-blank value in the company field. You can do this by searching for
the record that has a company field that is nearest to matching the string “A”. The actual details
of searching for a record are covered in the next section.
That’s all there is to sorting the records in the ADDRESS2 program. Clearly, this is not a dif-
ficult subject. The key points to grasp are that you must create secondary indexes for all the
fields you want to sort on, and then performing the sort becomes as simple as swapping
indexes.
NOTE
It’s important to note that some databases don’t force you to search only on actively
keyed fields. Some SQL servers, for instance, don’t have this limitation. But local
Paradox and dBASE tables in the 16-bit version of the BDE are restricted in this
manner, so you must use the techniques described here when searching for fields in
these databases. Remember that these limitations do not apply to the 32-bit version of
Delphi, where you can search with the OnFilterRecord process.
To find out what string and field interests the user, the ADDRESS2 program pops up the dia-
log shown in Figure 18.7. This dialog contains three radio buttons and an edit control, along
with a few buttons.
FIGURE 18.7.
The Search Dialog from the
ADDRESS2 program.
This code retrieves the relevant string and search field, calls the DoSort method, and finally
calls a method that performs the actual search. Here is the code for the GetSearchStr method:
function TSearchDlg.GetSearchStr(var ST: TSortType; var S: string):
Boolean;
begin
Result := False;
S := Edit1.Text;
if FirstName.Checked then
ST := stFirst
else if LastName.Checked then
ST := stLast
else
ST := stCompany;
Result := True;
end;
This code first pops up the Search dialog in modal mode. If the user selects the OK button, the
selected string is retrieved and the selected radio button is translated into a variable of type
TSortType. In other words, the user must select a string to search on and also choose a radio
button in order to indicate the field he or she believes contains the string.
If the GetSearchStr method returns True, the Search1Click method calls DoSort, which sets
the current index to the field that interests the user. Because the program now knows the string
that needs to be found and because the index has been set to the appropriate field, it is now safe
to perform a search:
procedure TfrAddress.DoSearch(S: string);
begin
tblAddress.SetKey;
case FSortType of
stFirst: tblAddress.FieldByName(‘FName’).AsString := S;
stLast:tblAddress.FieldByName(‘LName’).AsString := S;
stCompany:tblAddress.FieldByName(‘Company’).AsString := S;
end;
tblAddress.GotoNearest;
end;
The DoSearch method sets the table into SetKey mode and then tells the table to look for a
particular string in the field that is currently indexed. Finally, tblAddress.GotoNearest is called,
and the table is brought as close to the sought-after record as possible.
This sounds a bit complex, but it has some hidden virtues. Because the table has already been
indexed on the appropriate field, you will find that you can perform partial searches that take
you close to the record you seek, even if you can’t remember its exact spelling. For instance, if
you know someone’s name begins with Gab, but can’t recall if the rest is Gabfest, Gabfald, or
Gabheld, you can search on the string Gab and iterate forward from that point in search of the
record in question. This is the technique used by the SortMenu method when it calls DoSearch
with “” as its parameter.
Once again, it turns out that the actual code for searching for data in a Paradox table is fairly
straightforward. A curve is thrown at you here in that you are forced to set the table to a par-
ticular index before you can search for a value, but even this seeming inconvenience can turn
out to have some fairly important hidden benefits. However, you should remember that many
SQL servers enable you to search on fields that are not currently part of the active index.
Filtering Data
It turns out that setting up a filter and performing a search are very similar tasks. The first step
is to find out the string the user wants to use as a filter. To do this, you can call the Filter dia-
log, passing it the contents of the cbCategory combo box as a parameter:
function TFilterDlg.GetFilter(SList: TStrings): string;
begin
ListBox1.Items := SList;
This code accepts the list from the combo box as a parameter, displays it inside a list box, and
shows the Filter form as a modal dialog. If the user presses OK, the GetFilter function returns
the selected string. If Cancel is pressed, an empty string is returned.
After retrieving the string, the ADDRESS2 program sets up the CategoryIndex and then per-
forms a normal filter operation:
procedure TfrAddress.Filter1Click(Sender: TObject);
var
S: string;
begin
if Filter1.Caption = ‘Filter’ then begin
S := FilterDlg.GetFilter(cbCategory.Items);
if S = ‘’ then Exit;
Filter1.Caption := ‘Cancel Filter’;
tblAddress.IndexName := ‘CategoryIndex’;
tblAddress.SetRangeStart;
tblAddress.FieldByName(‘Category’).AsString := S;
tblAddress.SetRangeEnd;
tblAddress.FieldByName(‘Category’).AsString := S;
tblAddress.ApplyRange;
end else begin
Filter1.Caption := ‘Filter’;
tblAddress.CancelRange;
end;
end;
This is a simple process that lets you narrow the number of records displayed at any one time.
The key point to remember is that this whole process works only because the user enters data
in the Category field by selecting strings from a dropdown combo box. Without the
TdbComboBox, the number of options in the Category field would likely become
unmanageable.
Note that the code also lets you cancel the filter operation after it has been implemented. In
doing so, the program toggles the Caption for Filter1 between Filter and Cancel Filter. This
way, the currently available menu item is always appropriate.
Printing Records
Chapter 21, “ReportSmith,” is set aside in this book to discuss ReportSmith. However, I will
touch on this subject briefly, just to give you a few clues about how to proceed.
To start creating a report, bring up ReportSmith using Tools | ReportSmith and select the type
of report you want to make, which is probably a label-based report. Go to the Tables page in
the Report Query dialog, choose Add Table, and select ADDRESS.DB.
Next, go to the Report Variables page and create a new variable called Filter. Set its type to
String, its Title to Filter List, and the prompt to “What filter do you want?”. Set the Entry to
Type-in, as shown in Figure 18.8. When you are done, choose Add.
NOTE
You do not have to choose Type-in as the Entry method. In fact, the ADDRESS2
program is ideally suited for using the “Choose from a Table” method. After you select
this method, a workspace will appear in the bottom-right corner of the Report
Variables page that enables you to choose a table and field that contain a list of
available entries. In this case, you can choose the CATS.DB table and the Category
field. Now when the user wants to run the report, he or she will be prompted with a
list of valid categories and can choose the appropriate one, without the likelihood of an
error being introduced.
FIGURE 18.8.
Creating report variables
in ReportSmith.
Turn to the Selections page, click on the yellow number “1” in the center of the page, and
choose “Select SQL selection criteria” from the dropdown list. Select the Category field from
the DataFields list box on the left, and choose “x=y” from the Comparison Operators in the
middle list box. Go back to the left-hand list box, and change the combo box at the top so it
reads “Report Variables” rather than “Data Fields”. Choose Filter, and then set this variable in
quotes:
‘ADDRESSxDB’.’CATEGORY’ = ‘<<Filter>>’
When you are done, the dialog should look like that in Figure 18.9. Now click the OK button
at the bottom of the dialog.
FIGURE 18.9.
Creating SQL selection
criteria in ReportSmith.
The final step in this process is to create derived fields in the Derived Fields page of the Report
Query dialog. The first derived field should combine the FName and LName fields, so you
might want to call this field FirstLast. After typing in the name, select Add, and the Edit De-
rived Fields dialog box will appear. Select FName from the left column:
‘ADDRESSxDB’.’FName’
End by adding the LName field. The string you create should look like this:
‘ADDRESSxDB’.’FName’ + ‘ ‘ + ‘ADDRESSxDB’.’LName’
This statement combines the FName and LName fields so that they produce a single string
out of a first and last name:
Kurt Weill
You should then create a second derived field called CityStateZip, which combines the City,
State, and Zip fields:
‘ADDRESSxDB’.’CITY’ + ‘, ‘ + ‘ADDRESSxDB’.’STATE’ + ‘ ‘ +
‘ADDRESSxDB’.’ZIP’
You have now created the logic behind a report, and so you should choose Done from the bottom
of the Report Query dialog. ReportSmith will then pop up a dialog to fill in the report variable
you created. In other words, it’s time for you to fill in the Filter portion of the following state-
ment:
‘ADDRESSxDB’.’CATEGORY’ = ‘<<Filter>>’
You can type the word Family, Work, or whatever value you feel will return a reasonably sized
dataset.
The Insert Field dialog now appears, and you can enter the fields and derived fields that you
have created. The combo box at the top of the dialog enables you to switch back and forth
between data fields and derived fields; you should do so when you think it’s appropriate. For
instance, the first field you select will probably be the derived field called FirstLast, whereas the
second will probably be the data field called Address1. When you are done, the report you create
should look something like the image shown in Figure 18.10.
FIGURE 18.10.
A “live data” label report
produced by ReportSmith.
Even the report shown here is probably not totally complete; you might want to rearrange the
location of the fields inside each label and change the size and location of each individual label.
You will find that you can move individual fields around by simply dragging them with the
mouse. The actual decisions you make will be based on your personal tastes and on the type of
labels you have in your office or home. When you are performing these tasks, you will prob-
ably find it easiest to do so by choosing the File | Page Setup menu option from the ReportSmith
main menu.
If you want to change the fonts used in your report, select one of the fields in a label, then
right-click on the page, and choose Character from the popup menu. Other options, such as
adjusting borders and field height, and inserting pictures, are available when you right-click a
report.
When you are completely finished preparing a report, choose File | Save to save the report you
have created under the name Address, in the same directory where the ADDRESS2 program
resides.
As I stated previously, this brief tutorial on ReportSmith is not meant to be anything more
than a very loosely structured primer. ReportSmith is very easy to use, and most of the actions
you will perform are intuitive variations on the simple steps already outlined. However, you
will probably also want to study the ReportSmith manuals, the online help, and perhaps a third-
party book dedicated to this subject.
NOTE
ReportSmith has a fairly sophisticated query builder in the Selections page of the
Report Query Dialog. On several occasions, I have actually abandoned Delphi’s query
builder and opened up ReportSmith to construct a SQL statement. When I’m done, I
just block-copy the SQL code from ReportSmith into the SQL property of my current
Delphi TQuery object. This is a major kludge, but under some circumstances you
might want to consider it as an option.
As easy as it is to use ReportSmith, it is even easier to link this tool into a standard Delphi
program.
Drop a TReport object on a form and set the ReportName to the name of the report you want
to use. You can resolve the path to the report at runtime, by calling the GetPath method, which
will be described later in this chapter.
The actual code for printing the report is encapsulated in the PrintData method. As you recall,
you blocked out the code from this routine when you were setting up the CommandClick method.
Here is the code for PrintData:
procedure TfrAddress.PrintData;
begin
Cursor := crHourGlass;
Report1.ReportDir := GetPath;
Report1.Run;
Cursor := crDefault;
end;
This routine finds the path in which ADDRESS.RPT resides, and then it loads ReportSmith
by calling Report1.Run . As long as you have the RS_RUN subdirectory on your path, the
ReportSmith runtime will load automatically after a call to Run, and you can then enter the
name of the category you want to set up as a filter. Because ReportSmith can take a few mo-
ments to load, you should probably set the program’s cursor to crHourGlass before calling Run.
It’s important to understand that there are two versions of ReportSmith. The main version of
the program resides in the RPTSMITH subdirectory. This is the version you should use when
you are designing reports. The runtime version of ReportSmith resides in the RS_RUN
subdirectory; it is the one you can distribute with your programs. Specifically, it is RS_RUN.EXE
that gets loaded when you are running a Delphi program.
After RS_RUN is loaded, you have two options. If you have set the AutoUnload property to
True, ReportSmith will be taken out of memory automatically after the print job finishes.
However, because it takes a while for RS_RUN to load, you might want to leave it running
while you complete several print jobs. To do this, just set AutoUnload to False. You can then
perform your print jobs and close RS_RUN by calling CloseApplication and passing it a Boolean
parameter specifying whether the program should prompt the user to save reports, and so on.
Because CloseApplication uses DDE commands, it is possible that the routine might fail. As
a result, you should test to see if CloseApplication succeeds, and repeat the command if it fails:
function CloseReportSmith: Boolean;
begin
Result := Report1.CloseApplication(False);
end;
It is possible to set up macros inside ReportSmith. The RunMacro method of TReport can be
used to execute a ReportSmith macro. RunMacro takes a single parameter, which is the name of
the report you want to run.
You can pass parameters to ReportSmith by using the InitialValues field. The InitialValues
fields lets you automatically pass a string to ReportSmith, rather than having the user type in
a value in a dialog. In short, you can pass the category you want to filter through the InitialValues
field, and then the user will not need to enter the value. This can be particularly useful if you
want to set up error-free input methods.
NOTE
As mentioned, ReportSmith can associate a table with a report variable, thereby letting
the user select a value from the table, rather than typing it in. In my experience, this
technique works fine and may be preferable to using the InitialValues field.
You should also note that it is possible to change a report variable at runtime by using the
SetVariable and SetVariableLines methods. After using these methods, you should call
ReCalcReport to make sure that ReportSmith assimilates the changes you have made. SetVariable
and SetVariableLines use DDE, so you should test to make sure the calls succeed.
Another property of TReport that you might want to use is the Preview property, which lets a
user determine whether the correct report has been generated. You can use the MaxRecords and
StartPage properties to control the number of records that ReportSmith prints.
ReportSmith is a good tool that has the enormous advantage of being easy to use. In fact,
ReportSmith is so easy to use that it turns the task of generating reports into something very
similar to play. As a result, you can quickly generate elegant-looking reports and then get back
to the more interesting task of writing code.
Setting Colors
The Colors menu, shown in Figure 18.11, lets you set the colors for most of the major objects
in the program. The goal is not to give the user complete control over every last detail in the
program, but to let him or her customize the most important features.
FIGURE 18.11.
The options under the
Colors menu enable you to
change the appearance of
the ADDRESS2 program.
The ColorClick method uses the time-honored method of declaring an enumerated type and
then sets up the Tag property from a menu item to specify the selection of a particular option.
Here is the enumerated type in question:
TColorType = (ccForm, ccEdit, ccEditText, ccLabel, ccPanel);
The routine begins by enabling the user to select a color from the Colors dialog and then as-
signs that color to the appropriate controls:
procedure TfrAddress.ColorClick(Sender: TObject);
begin
if not ColorDialog1.Execute then Exit;
case TColorType((Sender as TMenuItem).Tag) of
ccForm: frAddress.Color := ColorDialog1.Color;
ccEdit: SetEdits(tcColor, ColorDialog1.Color);
ccEditText: SetEdits(tcFontColor, ColorDialog1.Color);
ccLabel: SetLabels(ColorDialog1.Color);
ccPanel: SetPanels(ColorDialog1.Color);
end;
SetIni(TColorType((Sender as TMenuItem).Tag), ColorDialog1.Color);
end;
If the user wants to change the form’s color, the code to do so is simple enough:
ccForm: frAddress.Color := ColorDialog1.Color;
However, it is a more complicated process to change the color of all the data-aware controls.
To accomplish this goal, the ColorClick method calls the SetEdits routine:
This code iterates though all the components belonging to the main form of the program and
checks to see if any of them are TdbEdits, TdbComboBoxes, or TdbMemos. When it finds a
hit, the code casts the control as a TdbEdit and sets its color to the new value selected by the
user:
tcColor: TdbEdit(Components[i]).Color := NewValue;
This code will very quickly change all the data-aware controls on the form to a new color.
The SetEdits routine works for both setting the color of the data-aware controls themselves,
and for setting the fonts displayed in these controls. To tell which property the user wants to
change at any particular moment, the SetEdits routine uses the following enumerated type:
TChangeType = (tcColor, tcFontColor);
The code for setting labels and panels works exactly the same way as the code for the data-
aware controls. The only difference is that you don’t need to worry about looking for multiple
types of components:
procedure TfrAddress.SetLabels(C: TColor);
var
i: Integer;
begin
for i := 0 to ComponentCount - 1 do
if (Components[i] is TLabel) then
TLabel(Components[i]).Font.Color := C;
end;
When working with the colors of the program, the final step is to make sure that the program
“remembers” the user’s selections. To do this, ADDRESS2 makes use of an INI file that looks
something like this:
[Colors]
Form=8421440
Edits=8421376
EditText=0
Labels=0
Panels=12639424
This INI file has only one section, which contains five different items, one for each of the col-
ors specified by the user. The colors themselves are stored as integers, each representing one of
the possible values captured in the TColor enumerated type.
NOTE
In this chapter, I assume that all readers are familiar with INI files. The most common
INI files are WIN.INI and SYSTEM.INI. However, most major programs have their
own INI files, such as the DELPHI.INI file that is stored in the WINDOWS
subdirectory. These files are used to store settings for a program; you can read more
about them in any good book on Windows basics. In Windows 95, INI files are still
supported, but the Registry provides an appealing alternative. This program, however,
provides support for both 16- and 32-bit programs, so it does not use the Registry.
Many powerful new programs, such as Delphi 2.0 itself, use a combination of the
Registry and an INI file. (See the DELPHI32.INI file in the Windows subdirectory.)
This method takes advantage of the TIniFile type, which is found in a source file, called
INIFILES.PAS, that ships with Delphi.
You create a TIniFile object by passing in the name of the INI file you want to work with to
the object’s Create method:
Ini := TIniFile.Create(GetPath + ‘address.ini’);
The GetPath method performs some hand-waving to return the subdirectory from which the
ADDRESS2 program was launched. In particular, it uses Delphi’s built in ExtractFilePath
method:
function TfrAddress.GetPath: string;
begin
Result := ExtractFilePath(ParamStr(0));
Before discussing the key methods of TIniFile, I should mention that after you are through
with the object, you must deallocate the memory you created for it:
Ini.Free;
There are a number of methods you might want to call between the time you create a TIniFile
object and the time you free it. However, you cannot call these methods successfully without
first allocating memory for the object, and you must free that memory when you are through
with the object.
Windows supplies a set of functions called G e t P r i v a t e P r o f i l e S t r i n g ,
WritePrivateProfileString, GetPrivateProfileInt, and WritePrivateProfileInt that let you
talk with INI files. These routines work well enough, and they are fairly easy to use, but they
expect you to pass in PChars in their numerous parameters. TIniFile simplifies this process by
enabling you to pass in Pascal strings rather than PChars. For instance, the WriteInteger method
replaces WritePrivateProfileInt:
procedure WriteInteger(const Section, Ident: string;
Value: LongInt);
This function expects the name of a section in the first parameter, the name of an item in the
second parameter, and the value you want to write in the third parameter. For instance, the
INI file has an entry in it labeled Form. if you wanted to change the Form entry to black, you
would enter the following code:
WriteInteger(‘Colors’, ‘Form’, 0);
This will change the Form entry in the INI file to zero:
[Colors]
Form=0
With this knowledge in hand, it’s easy enough to go back to the SetIni function and see ex-
actly how it works. The key, of course, is simply that the second parameter passed to WriteInteger
is designated by using an enumerated type in a case statement:
case CType of
ccForm: S := ‘Form’;
ccEdit: S := ‘Edits’;
ccEditText: S := ‘EditText’;
ccLabel: S := ‘Labels’;
ccPanel: S := ‘Panels’;
end;
Ini.WriteInteger(‘Colors’, S, C);
It’s not enough, however, simply to write the values to an INI file. You must also read these
values whenever the program starts. Here’s how you can read an integer value from an INI file:
function TfrAddress.GetIni(CType: TColorType): TColor;
var
Ini: TIniFile;
Path, S: string;
begin
Ini := TIniFile.Create(GetPath + ‘address.Ini’);
case CType of
ccForm: S := ‘Form’;
ccEdit: S := ‘Edits’;
ccEditText: S := ‘EditText’;
ccLabel: S := ‘Labels’;
ccPanel: S := ‘Panels’;
end;
Result := Ini.ReadInteger(‘Colors’, S, clBlue);
Ini.Free;
end;
This code is almost the exact reverse of the SetIni method, except that ReadInteger takes a
default value in its last parameter. This default value will be returned if the program is unable
to locate the INI file, or if it can’t find the item requested in the INI file.
The code in the FormCreate method that calls the GetIni method looks like this:
for i := 0 to 4 do
case TColorType(i) of
ccForm: Color := GetIni(TColorType(i));
ccEdit: SetEdits(tcColor, GetIni(TColorType(i)));
ccEditText: SetEdits(tcFontColor, GetIni(TColorType(i)));
ccLabel: SetLabels(GetIni(TColorType(i)));
ccPanel: SetPanels(GetIni(TColorType(i)));
end;
Though somewhat complex, this code should be fairly easy to understand at this stage because
you have already seen all the various routines called in this loop. It’s just a question of bringing
them all together so the program can read the INI file and set the appropriate controls to the
appropriate colors.
There is always the possibility that the INI file in question does not exist or cannot be found.
In such a case, the ADDRESS2 program is smart enough to create a new INI file containing
default values:
procedure TfrAddress.CheckForIni;
var
F: System.Text;
Path: string;
begin
Path := GetPath + ‘address.ini’;
if not FileExists(Path) then begin
System.Assign(F, Path);
System.ReWrite(F);
System.WriteLn(F, ‘[Colors]’);
System.WriteLn(F, ‘Form=7045184’);
System.WriteLn(F, ‘Edits=8421376’);
System.WriteLn(F, ‘EditText=0’);
System.WriteLn(F, ‘Labels=0’);
System.WriteLn(F, ‘Panels=12639424’);
System.Close(F);
end;
end;
The CheckForIni routine is called from the FormCreate method. It begins by using the built-in
FileExists method to check for the existence of the INI file. If the file can’t be found, a new
one is created, using standard text file operations.
In this section, you have learned how to change the colors of components at runtime and how
to save the value of these colors in an INI file. The key fact you learned is that you can use the
Components array to iterate through the components owned by a form, and you can check to
see the type of each component you iterate past. When you find a component of a particular
type, you can then perform a typecast and change its color.
Marking Files
I have also added the ability to mark files to the ADDRESS2 application. In the current ver-
sion, the only thing you can do with a marked file is write it to disk in a text file. Still, it’s useful
to see how to work with a logical field in a table, and to see an interesting technique for dis-
playing these marked fields to the user.
The Marked field in this table is declared to be of type Boolean. (Remember that one of the
fields of ADDRESS.DB is actually called Marked, so in the last sentence I’m not referring to
an attribute of a field, but to its name.) Here is a method for showing the user whether the
Boolean Marked field is set to True or False:
procedure TfrAddress.DataSource1DataChange(Sender: TObject; Field: TField);
begin
if tblAddress.FieldByName(‘Marked’).AsBoolean then begin
BookOpen := LoadBitmap(HInstance, ‘BookOpen’);
SpeedButton1.Glyph.Handle := BookOpen
end else begin
BookShut := LoadBitmap(HInstance, ‘BookShut’);
SpeedButton1.Glyph.Handle := BookShut;
end;
end;
If the field is marked, then a bitmap called BookOpen is loaded from one of the program’s two
resource files. This bitmap is then assigned to the Glyph field of a speedbutton. If the Marked
field is set to False, then a second bitmap is loaded, and shown in the speedbutton. The bitmaps
give a visual signal to the user as to whether or not the record is marked.
As hinted in the last paragraph, the ADDRESS2 program has two resource files. The first is
the standard resource file, which holds the program’s icon. The second is a custom resource
build from the following RC file:
bookopen BITMAP “BOOKOPEN.BMP”
bookshut BITMAP “BOOKSHUT.BMP”
This file is called BITS.RC, and it compiles to BITS.RES, and is linked into the executable by
the last of these lines quoted from MAIN.PAS:
uses
About, Cats, Filter,
IniFiles, StrBox, SysUtils,
UtilBox;
{$R *.DFM}
{$R BITS.RES}
Whenever the user toggles the SpeedButton where the Bookshut and BookOpen bitmaps are
displayed, then the logical Marked field in the database is toggled:
procedure TfrAddress.SpeedButton1Click(Sender: TObject);
begin
tblAddress.Edit;
tblAddress.FieldByName(‘Marked’).AsBoolean :=
not tblAddress.FieldByName(‘Marked’).AsBoolean;
tblAddress.Post;
end;
Note that this code never checks the value of the Marked field—it just sets it to the opposite of
its current state.
There are several other routines in the ADDRESS2 program that relate to this logical field.
However, none of them are very complicated to understand once you understand the logic
from the SpeedButton1Click and the DataSource1DataChange methods.
This method is called when the user chooses Copy from the Edit menu. The code first checks
to see if the currently selected control is one of those that contains data from the Address table;
that is, it checks to see if it is a TDBEdit, TDBMemo, or TDBComboBox. If it is, the control
is typecast so that its properties and methods can be accessed.
The TDBEdit and TDBComboBox controls have CopyToClipBoard, PasteFromClipBoard,
and CutToClipBoard commands. Each of these commands simply copies, cuts, or pastes data
from the live database control to or from the Clipboard. The TDBComboBox does not have
quite so rich a set of built-in functions, so it is handled as a special case. In particular, note that
I have to use the built-in ClipBoard object, which can easily be a part of every Delphi project.
To access a fully allocated instance of this object, simply include the ClipBrd unit in your Uses
clause.
You can see that working with the clipboard is a trivial operation in Delphi. The Paste1Click
and Cut1Click methods from the ADDRESS2 program demonstrate a specific technique you
can use when pasting and cutting from or to the clipboard.
Summary
You have had a chance to look at all the major portions of the ADDRESS2 program. The only
items not mentioned in this chapter were the construction of the About dialog and the use of
the Object Repository to expedite the creation of some of the forms. These techniques have
been examined in previous chapters, so you shouldn’t find them challenging.
I went into such detail about the ADDRESS2 program because it contains many of the fea-
tures that need to be included in real-world programs. As stated earlier, the ADDRESS2 pro-
gram isn’t quite up to the standards expected from a professional program, but it does answer
some questions about how you can take the raw database tools described in the last few chap-
ters and use them to create a useful program.
■ Transactions 663
Delphi ships with the Local InterBase Server, which is sometimes simply called LIBS. This
tool provides all the capabilities of the full InterBase server, but it runs on a local machine. In
other words, you do not need to be connected to a network in order to run the Local InterBase
Server.
The goal of this chapter is to provide a useful introduction to LIBS and also a brief overview of
transactions. In particular, you will see how to do the following:
■ Connect to local InterBase tables
■ Connect without having to specify a password
■ Create databases
■ Work with TDatabase objects
■ Create tables
■ Commit and roll back transactions in both local and InterBase tables
■ Maintain the data you have created
■ Work with cached updates
■ Work with many-to-many relationships
Almost everything that is said about the local InterBase in this chapter applies equally to the
full server version of InterBase, so this chapter will also be of interest to people who use InterBase
on a network.
Most readers of this book will probably find this an interesting chapter, including those who
have access to the full-blown version of InterBase. In particular, this chapter shows how you
can use a local system to create a database that is fully compatible with the network version of
InterBase.
Some readers come from the world of “big iron,” where the only kinds of databases that exist
are servers such as Oracle, Sybase, InterBase, or DB2. Other readers come from the world of
PCs, where tools such as dBASE, Paradox, Access, or FoxPro are considered to be the standard
database tools. It is almost impossible to overemphasize the huge gap that exists between these
two worlds.
Readers who are familiar with “big iron” and large network-based servers are likely to find the
Local InterBase Server very familiar. Readers who come from the world of PCs are likely to
find InterBase very strange indeed, especially at first.
InterBase is meant to handle huge numbers of records, which are stored on servers. It does not
come equipped with many of the amenities of a tool such as dBASE or Paradox. In fact, InterBase
supplies users with only the most minimal interface and instead expects you to create programs
with another front end such as Delphi.
The key piece of information you are getting here is the location of GDS32.DLL.
NOTE
Readers who want to connect to the full server version of InterBase will find that the
procedure I have just outlined works fine, except that you must have a network
protocol such as TCP/IP loaded first. This book does not include a description of the
wizardry needed to successfully set up a network protocol, though the task is usually
handled automatically by either Windows 95 or Windows NT. I assume that you will
turn to another source in order to learn how to ping your server and how to test the
connection with a few simple FTP sessions. When you have gotten that far, you can
open this book and follow the directions that I give here.
Almost everything I have said about databases in this book applies equally to local
databases and servers. The key point is that you have to set up the network connection
first, then turn to this book to learn how Delphi talks to databases. Of course, I am
fully aware of how difficult and frustrating it can be to set up a network connection,
and how little clear and easy-to-understand prose there is on the subject. Nevertheless,
it is simply too large a topic for me to tackle in this book. My goal here is to write
about Delphi, and you will have to turn elsewhere to learn about setting up network
connections. I should add, however, that setting up an InterBase connection is usually
a fairly straightforward process when compared to setting up other servers.
Name the new alias TESTGDB, or give it whatever name you prefer. The ServerName property for
this alias should be set to:
c:\program files\borland\intrbase\examples\employee.gdb
You can adjust the drive letter and path for the way you have set up the files on your machine.
The user name should be set to SYSDBA, and the Password you will use is masterkey. All the
other settings can have their default values, as shown in Figure 19.1. When you have every-
thing set up correctly, choose Database | Apply from the menu.
FIGURE 19.1.
A sample InterBase alias,
as it appears in the
Database Explorer.
After you have set up and saved your alias, you can connect to the TESTGDB alias exactly as
you would with any other set of data. From inside the Explorer, just click on the plus symbol
before the TESTGDB node. A dialog will pop up prompting you for a password. Make sure
the USER NAME is set to SYSDBA, and then enter masterkey as the password. Everything
else will then be the same as when working with a Paradox table, except that you will find many
new features such as stored procedures and triggers. Most of these new features will be described
in this chapter and the next.
To connect to the database from inside Delphi proper, first drop a table onto a form, and set
its DatabaseName property to TESTGDB. When you try to drop down the list of TableNames,
you will be prompted for a password. You should enter masterkey at this point, all in lower-
case. After taking these steps, you can set the Active property for Table1 to True. If this call
succeeds, everything is set up correctly, and you can begin using the local InterBase to create
Delphi database programs. If you can’t set Active to True, you should go over the steps out-
lined previously and see whether you can correct the problem.
NOTE
I usually use SYSDBA and masterkey as the user name and password combination for the
InterBase databases in this book. However, I sometimes work with USER1 and
USER1 instead, simply because it is easier to type in USER1 than it is to type in
masterkey.
In the last two sections, you have learned the basic facts about using LIBS. The next step is to
learn how to create your own databases and tables.
Creating Databases
Unlike local Paradox or dBASE files, InterBase tables are not stored in separate files located
within a directory. Instead, InterBase tables are stored in one large file called a database. This
means that you need to first go out and create a database, and then you can create a series of
tables inside this larger database.
The simplest way to create a database is with the WISQL program that ships with the Local
InterBase Server. WISQL stands for Windows Interactive Standard Query Language, or sim-
ply the Interactive SQL tool. WISQL is fundamentally a tool for entering SQL statements,
with a few other simple features thrown in for good measure.
NOTE
In addition to WISQL, the other important tool that ships with InterBase is the
InterBase Server Manager, IBMGR32.EXE. It enables you to test connections to
servers and perform simple maintenance tasks such as backing up and restoring
databases and setting passwords. You can use IBMGR32 to back up your data so that
you can recover if disaster strikes. What little information you need for this easy-to-use
tool is available in the InterBase documentation. The IBMGR32 will not be men-
tioned further in this book, but you should take the few minutes required to browse
through its online help and take it through its paces.
After starting up WISQL, choose Create Database from the File menu. A dialog like the one
shown in Figure 19.2 appears. Set the Location Info to Local Engine. Because you are working
with local InterBase, in fact, there is no need for an active network protocol.
FIGURE 19.2.
The dialog used for
creating databases inside
WISQL.
In the Database field, enter the name of the table you want to create. If the table is to be lo-
cated inside a directory, include that directory in the database name. For practice, you should
create a database called INFO.GDB that is located in a subdirectory called DATA. If it does
not already exist on your system, you should first go to DOS and create the DATA subdirectory.
When you are all set up, enter the following in the Database field:
E:\DATA\INFO.GDB
You can replace E: with the letter for the appropriate drive on your system. The extension .GDB
is traditional, though not mandatory. However, I suggest always using this extension so that
you can recognize your databases instantly when you see them. It can be a tragedy if you acci-
dentally delete a database.
The user name can be set to anything you want, although the traditional entry is SYSDBA,
and the traditional password is masterkey. When you first start out with InterBase, it is probably
best to stick with this user name and password combination. Even if you assign new passwords
to your database, the SYSDBA/masterkey combination will still work unless you explicitly re-
move it using the IBMGR.
After you have entered a user name and password, you can create the database by clicking the
OK button. If all goes well, you are then placed back inside WISQL proper. At this stage, you
can either quit WISQL or add a table to your database. If something went wrong, an error
message will appear. Click the Details button to try to track down the problem.
Assuming all went well, the following SQL statement can be run inside WISQL if you want to
create a very simple table with two fields:
CREATE TABLE TEST1 (FIRST VARCHAR(20), LAST INTEGER);
Enter this line in the SQL Statement field at the top of WISQL, and then press the Run but-
ton. If all goes smoothly, your statement will be echoed in the ISQL output window without
being accompanied by an error dialog. The lack of an error dialog signals that the table has
been created successfully. The preceding code creates a table with two fields. The first is a char-
acter field containing 20 characters, and the second is an integer field.
NOTE
The table-creation code shown here is used to describe or create a table in terms that
WISQL understands. Indeed, throughout most of this chapter and the next, I work
with WISQL rather than with the DBD. In describing how to perform these actions in
WISQL, I do not mean to imply that you can’t use the Database Desktop to create or
alter InterBase tables. In fact, the 32-bit version of DBD provides pretty good support
for InterBase tables. Still, I have found WISQL to be considerably more powerful than
I suspected when I first started using it.
After creating a database and table, you should select Commit Work from the File menu. This
command causes WISQL to actually carry out the commands you have issued. At this stage
you should choose File | Disconnect from Database.
In this section, you have learned the basic steps required to use InterBase to create a database
and table. The steps involved are not particularly complicated, although they can take a bit of
getting used to if you are new to the world of server tools.
FIGURE 19.3.
Connecting to the
INFO.GDB database
using the WISQL tool.
Go to View | Metadata Information, and set “View Information On” to Database, as shown in
Figure 19.4. After you choose the OK button, the information displayed in the ISQL output
window should look something like this:
SHOW DB
Database: c:\data\info.gdb
Owner: SYSDBA
PAGE_SIZE 1024
Number of DB pages allocated = 210
Sweep interval = 20000
FIGURE 19.4.
Preparing to view
information on the
INFO.GDB database.
To see the tables available in a database, select View | Metadata Information from the menu,
and set “View Information On” to Table. Select the Run button, and view the information in
the ISQL output window, which should look like this:
SHOW TABLES
TEST1
Browsing through the Metadata Information menu choices, you can see that InterBase sup-
ports triggers, stored procedures, views, and a host of other advanced server features.
The Extract menu choice enables you to find out more detailed information about the data-
base and its tables. For instance, if you choose Extract | SQL Metadata for Database, you get
output similar to the following:
/* Extract Database e:\data\info.gdb */
CREATE DATABASE “e:\data\info.gdb” PAGE_SIZE 1024
;
If you choose Extract | SQL Metadata for Table, you get the following output:
/* Extract Table TEST1 */
You should note that WISQL often asks whether you want to save the output from a com-
mand to a text file, and the File menu gives you some further options for saving information to
files. You can take advantage of these options when necessary, but 90 percent of the time, I
pass them by with barely a nod.
NOTE
The WISQL program accepts most SQL statements. For instance, you can perform
Insert, Select , Update, and Delete statements from inside WISQL. Just enter the
statement you want to perform in the SQL Statement area, and then select the Run
button.
You should also be aware that WISQL bypasses the BDE altogether, so you can use it
to test your connections to InterBase even if you are not sure that you have the BDE
set up correctly. For instance, if you are having trouble connecting to InterBase and
you’re not sure where the problem lies, start by trying to connect with WISQL. If that
works but you can’t connect from inside Delphi, it might be that the problem lies not
with your InterBase setup, but with the way you have deployed the BDE.
WISQL also comes equipped with a handy online reference to SQL. If you have
questions about how to format an Alter, Drop, Insert, Create Index, or other SQL
statement, you can look it up in the help for WISQL.
After reading the last three sections, you should have a fair understanding of how WISQL works
and how you can use it to manage a database. The information provided in this chapter is nothing
more than an introduction to a complex and very sophisticated topic. However, you now know
enough to begin using the local InterBase. This is no insignificant accomplishment. Tools such
as InterBase, Oracle, and Sybase lie at the heart of the client/server activity that is currently so
volatile and lucrative. If you become proficient at talking to servers such as InterBase, you might
find yourself at an important turning point in your career.
Transactions
The TRANSACT program, found on the UnleashedCD that accompanies this book, gives a
brief introduction to Transactions. To use Transactions, you must have a TDatabase compo-
nent on your Form, and you must use a real server such as Sybase, Informix, InterBase, or the
local InterBase. It won’t work with Paradox or dBASE files. (In just a moment I’ll show you a
second technique, called Cached Updates, that works with local tables and InterBase tables.)
To begin, drop down a TDatabase component. Set the AliasName property of the TDataBase
object to a valid alias such as IBLOCAL. Create your own string, such as IBNAME, to fill in
the DatabaseName property of the TDatabase object. In other words, when you are using a
TDatabase component, you make up the DatabaseName, rather than picking it from a list of
available aliases.
Drop down a TQuery object and hook it up to the EMPLOYEE.GDB file that ships with Delphi.
In particular, set the DatabaseName property of the TQuery object to IBNAME, not to IBLOCAL.
In other words, set the DatabaseName property to the string you made up when filling in the
DatabaseName property of the TDatabase component. You will find that DBNAME, or what-
ever string you chose, has been added to the list of aliases you find when you use the
Query1.DatabaseName Property Editor.
Then set the Active property to True and set RequestLive to True.
Once you’ve connected to the database, you can add a TDataSource and a Tdbgrid so you can
view the data. Also add four buttons, and give them the following captions:
Begin Transaction
Rollback
Commit
Refresh
The code associated with these buttons should look like this:
procedure TForm1.BeginTransactionClick(Sender: TObject);
begin
Database1.StartTransaction;
end;
Run the program, click Begin Transaction, and edit three or four records. If you click RollBack
and then Refresh, you will find that all of your work is undone, as if none of the editing
occurred. If you edit three or four records and then click Commit, you will find that your work
is preserved.To learn more about this subject, you should read the very important online help
entry called “Transaction Isolation Levels.”
NOTE
I don’t call Refresh directly because the table I am using is not uniquely indexed. In
lieu of this call, I close the table and then reopen it. You could use bookmarks to
preserve your location in the table during this operation.
Note that when you run the TRANSACT program included on the disk, you don’t have to
specify a password. This occurs because the LoginPrompt property of the TDatabase object is set
to False, and the Params property contains the following string:
password=masterkey
Cached Updates
One of the fanciest new features in the BDE is cached updates. Cached updates are like the
transactions just described, only considerably fancier. Like transactions, cached updates enable
you to edit a series of records without committing any of your work, and indeed, without caus-
ing any network activity at all. When you are ready to commit your work, cached updates enable
you to do so on a record-by-record basis, where any records that violate system integrity can be
repaired or rolled back on a case-by-case basis.
NOTE
One of the interesting things about cached updates is that they work with both local
and server data. To illustrate this point, the example in this section will work with the
local DBDEMOS alias. You could, however, do the same things with an InterBase
table.
Some users have reported remarkable increases in performance on some operations
when they use cached updates.
Cached updates are relatively complex mechanisms that enable you to keep track of the status
of each record on a field-by-field basis. In particular, when cached updates are turned on, you
can query your records one at a time and ask them if they have been updated. Furthermore, if
they have been updated, you can ask the current value of each field in the updated record, and
you can also retrieve the old, or original, value of the field.
You can do three things with the records in a dataset once the CachedUpdates property for the
dataset has been set to True:
1. You can call ApplyUpdates on the dataset, which means that you will try to commit all
the other records updated since CachedUpdates was set to True, or since the last
attempt to update the records. This is analogous to committing a transaction.
2. You can call CancelUpdates, which means that all the updates made so far will be
canceled. This is analogous to rolling back a transaction.
3. You can call RevertRecord, which will roll back the current record, but not any of the
other records in the dataset.
There is an excellent example program in the Delphi DEMOS subdirectory that shows how to
use cached updates. In this particular case, the example program is so good that it’s hard to
imagine improving on it. So, instead of trying to go it one better, I will create a sample pro-
gram that takes the basic elements of cached updates and presents them in the simplest pos-
sible terms.
The CacheUp program, shown in Figure 19.5, has one form. On the form is a copy of the
Orders table. The Orders table, as you recall, is related to both the Customer table and the
Items table. As a result, it is difficult to change either the OrderNo or CustNo fields without
violating system integrity in one way or another. When working with this program, you should
change these fields to values like 1 or 2, which will almost surely be invalid values. You can
then watch what happens when you try to commit the records you have changed.
The code for the CacheUp program is shown in Listing 19.1. Go ahead and get this program
up and running, and then come back for a discussion of how it works. When implementing
the code shown here, the key point to remember is that none of it will work unless the
CachedUpdates property of Table1 is set to True .
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, DB,
Grids, DBGrids, DBTables,
StdCtrls, Buttons, TypInfo, ExtCtrls;
type
TForm1 = class(TForm)
Table1: TTable;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
bApply: TBitBtn;
bRevert: TBitBtn;
BitBtn1: TBitBtn;
Panel1: TPanel;
Edit1: TEdit;
Edit2: TEdit;
Label1: TLabel;
Label2: TLabel;
ListBox1: TListBox;
BitBtn2: TBitBtn;
Label3: TLabel;
procedure bApplyClick(Sender: TObject);
procedure bRevertClick(Sender: TObject);
procedure Table1UpdateError(DataSet: TDataSet; E: EDatabaseError;
UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
procedure BitBtn1Click(Sender: TObject);
procedure DataSource1DataChange(Sender: TObject; Field: TField);
procedure BitBtn2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
The first thing to notice about the CacheUp program is that it tracks which records have been
modified. For instance, change the OrderNo field of the first two records to the values 1 and 2.
If you now select one of these records, you will see that the small panel in the upper right-hand
corner of the screen gets set to usModified. This means that the update status for this field has
been set to modified.
Here is the TUpdateStatus type:
TUpdateStatus = (usUnmodified, usModified, usInserted, usDeleted);
Edit1.Text := Table1.Fields[0].OldValue;
Edit2.Text := Table1.Fields[0].NewValue;
end else begin
Edit1.Text := ‘Unmodified’;
Edit2.Text := ‘Unmodified’;
end;
end;
The relevant line in this case is the first one. In particular, notice that it reports on the value of
Table1.UpdateStatus . This value will change to reflect the update status of the currently se-
lected record. In order to show this value to the user, the code calls a routine in the TypInfo
unit that gets the name of an enumerated value. To use this routine, pass in the type that you
want to examine, as well as the ordinal value of the element in that type whose name you want
to see. For instance, in this case you are working with a variable of type TUpdateStatus, so you
should pass that type in the first parameter. You should then pass the ordinal value of your
current record in the second parameter. The routine will return the value of the type in string
form.
At the same time that the CacheUp program reports that a record has been modified, it also
reports on the old and new value of the OrderNo field for that record. In particular, if you
changed the first record’s OrderNo field to 1, it will report that the old value for the field was
1003, and the new value is 1. (This assumes that you have the original data as it shipped with
Delphi. Remember that if you end up ruining one of these tables performing these kinds of
experiments, you can always copy the table over again from the CD!)
In the code that reports on the old and new value of the OrderNo field, you should examine
these lines in particular:
Edit1.Text := Table1.Fields[0].OldValue;
Edit2.Text := Table1.Fields[0].NewValue;
As you can see, this information is easy enough to come by—you just have to know where to
look.
If you have entered the values 1 and 2 into the OrderNo fields for the first two records, you are
going to encounter errors when you try to commit the data. In particular, if you try to apply
the data, the built-in referential integrity will complain that there are detail records associated
with the OrderNo fields numbered 1003 and 1004. As a result, it is not possible to commit the
records. The code then rolls back the erroneous records to their original state.
NOTE
Here is the code that reports on the errors in the OrderNo field and rolls the data back to its
original state:
procedure TForm1.Table1UpdateError(DataSet: TDataSet; E: EDatabaseError;
UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
var
S: string;
begin
S := GetEnumName(TypeInfo(TUpdateKind), Ord(UpdateKind)) + ‘: ‘ + E.Message;
ListBox1.Items.Add(DataSet.Fields[0].OldValue + ‘: ‘ + S);
UpdateAction := uaSkip;
end;
This particular routine is an event handler for the OnUpdateError event for the Table1 object.
To create the routine, click once on the Table1 object, select its Events page in the Object
Inspector, and then double-click on the OnUpdateError entry.
The Table1UpdateError method will get called only if an error occurs in attempting to update
records. It will get called at the time the error is detected, and before Delphi tries to commit
the next record.
Table1UpdateError gets passed four parameters. The most important is the last, which is a var
parameter. You can set this parameter to one of the following values:
TUpdateAction = (uaAbort, uaSkip, uaRetry, uaApplied);
If you set the UpdateAction variable to uaAbort, then the entire attempt to commit the up-
dated data will be aborted. None of your changes will have taken place, and you will return to
edit mode as if you had never attempted to commit the data. The changes you have made so
far will not be undone, but neither will they be committed. You are aborting the attempt to
commit the data, but you are not rolling it back to its previous state.
If you choose uaSkip, then the data will be committed, but the record that is currently in error
will be rolled back to its previous value. This will not affect any of the other records in the
dataset—it’s just the current record that is being reverted.
If you set UpdateAction to uaRetry, that means you have attempted to update the information
in the current record, and that you want to retry committing it. The record you should update
is the current record in the dataset passed as the first parameter to Table1UpdateError.
In the Table1UpdateError method, I always choose uaSkip as the value to assign to UpdateAction.
Of course, you could pop up a dialog and show the user the old value and the new value of the
current record. The user would then have a chance to retry committing the data. Once again,
you retrieve the data containing the current “problem child” record from the dataset passed in
the first parameter of Table1UpdateError. I show an example of accessing this data when I re-
trieve the old value of the OrderNo field for the record:
ListBox1.Items.Add(DataSet.Fields[0].OldValue + ‘: ‘ + S);
Needless to say, the OldValue field is declared as a Variant in the source code to DB.PAS, which
is where TDataSet’s declaration is located.
Two other values are passed to the TableUpdateError method. The first is an exception report-
ing on the current error, and the second is a variable of type TUpdateKind:
TUpdateKind = (ukModify, ukInsert, ukDelete);
The variable of type TUpdateKind just tells you how the current record was changed. Was it
updated, inserted, or deleted? The exception information is passed to you primarily so that
you can get at the message associated with the current error:
E.Message;
If you handle the procedure by setting UpdateAction to a particular value, say uaSkip, then
Delphi will not pop up a dialog reporting the error to the user. Instead, it assumes that you are
handling the error explicitly, and it leaves it up to you to report the error or not, as you see fit.
In this case, I just dump the error into the program’s list box, along with some other informa-
tion.
That is all I’m going to say about cached updates. At this point, you should go back and run
the dbCache program that ships with Delphi. It covers all the same ground covered in the last
few pages, but it does so in a slightly different form. In particular, it shows how to pop up a
dialog so that you can handle each OnUpdateError event in an intelligent and sensible manner.
In general, cached updates give you a great deal of power you can tap into when updating the
data in a dataset. If necessary, go back and play with the CacheUp program until it starts to
make sense to you. This subject isn’t prohibitively difficult, but it does take a few moments
thought to absorb the basic principles involved.
Many-To-Many Relationships
Many-to-many relationships are necessities in most relational database projects. Suppose, for
instance, that you have a set of software routines that you want to store in a database. Some of
the routines you can use in DOS, some in Windows 3.1, some in Windows NT, and some in
Windows 95. Some routines, however, apply to two or more of the operating systems.
To track this information, you might try adding an OS field to your Routines table, where OS
stands for Operating System. It all sounds simple enough. However, there is one problem. The
issue, of course, is that some routines will work with more than one OS. For instance, you may
have a routine that works in Windows NT and Windows 95, but not in Windows 3.1 or DOS.
This means that the fairly simple one-to-many relationship you tried to establish with the OS
fields really needs to be converted into a many-to-many relationship.
This section describes how to create many-to-many relationships. This is an annoyingly com-
plex subject, but one that you can master if you take a little time to think things through. My
basic goal is to break this process down into a series of steps that you can follow whenever you
have to create one of these many-to-many relationships. It might be going too far to say that
these steps make the process simple. They do make it manageable, however.
In Listing 19.2, you will find the database definition for a simple set of InterBase tables. You
can run this definition through WISQL, using the File | Run ISQL Script menu option. Alter-
natively, you can create a new database and pass the key statements shown here through one at
a time.
Beneath the data definition for the database, you will find the code to a program called
Man2Man. This code is found in Listings 19.3 and 19.4, and it shows how to handle a many-
to-many relationship in a Delphi program.
Listing 19.2. The schema for a simple database that can capture a many-to-
many relationship.
/* Extract Database c:\src\unleash2\data\man2man.gdb */
CREATE DATABASE “c:\src\unleash2\data\man2man.gdb” PAGE_SIZE 1024;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, Grids,
DBGrids, DB, DBTables,
StdCtrls, Buttons;
type
TForm1 = class(TForm)
Table1: TTable;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
bAttribs: TBitBtn;
continues
var
Form1: TForm1;
implementation
uses relate;
{$R *.DFM}
end.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Buttons, Grids, DBGrids,
DB, DBTables;
type
TRelater = class(TForm)
tblAttrib: TTable;
dsAttrib: TDataSource;
DBGrid1: TDBGrid;
bbInsert: TBitBtn;
bbDelete: TBitBtn;
BitBtn3: TBitBtn;
qInsert: TQuery;
DBGrid2: TDBGrid;
qViewAttrib: TQuery;
dsViewAttrib: TDataSource;
qDelete: TQuery;
procedure bbInsertClick(Sender: TObject);
procedure FormShow(Sender: TObject);
var
Relater: TRelater;
implementation
{$R *.DFM}
procedure TRelater.ViewAttribs;
begin
qViewAttrib.Close;
qViewAttrib.Params[0].AsInteger := FCustNo;
qViewAttrib.Open;
end;
end.
The Man2Man program enables you to pop up a dialog that contains two lists of attributes.
The left-hand list shows all the possible attributes that can be associated with a record in the
main table for this program. The second list shows the currently selected attributes for the current
record in the main table. Buttons are supplied so that you can add items from the left-hand
column to the column on the right. The dialog in question is shown in Figure 19.5.
FIGURE 19.5.
The Relater dialog relates
the Customer table to the
Attributes table.
The basic idea behind a many-to-many relationship is that you need to have an intermediate
table between the main table and the list of attributes that you assign to it. For instance, if you
have the Routines and the OS tables described, you need a middle table that relates the Rou-
tine ID from the Routines table to the OS ID from the OS table.
In the database just shown, the Middle table serves as the intermediary between the Customers
table and the Attribs table. Here’s how it works.
The Customer table has a series of records in it like this:
select * from Customers
CUSTNO NAME
=========== ===================================
1 SAM
2 MIKE
3 FREDDY FREELOADER
4 SUNNY SUZY
5 LOU
6 TYPHOID MARY
7 SANDRA
8 MICHELLE
9 NICK
10 NANCY
The Attribs table also has a set of attributes that can be assigned to these Customers:
select * from Attribs
ATTRIBNO ATTRIB
=========== ==================================
1 Nice
2 Naughty
3 Generous
4 Guilty
5 Onerous
6 Criminal
7 Hostile
8 Beautiful
9 Bodacious
10 Endearing
Suppose that Sunny Suzy was both Nice and Bodacious. To connect her to these two attributes,
you could add two fields to the Middle table:
CustNo AttribNo
4 1
4 9
Now when you open up the Middle table, you will find two entries in it. The first entry has a
CustNo of 4, which stands for Sunny Suzy, and an AttribNo of 1, which stands for Nice. Like-
wise, the second line translates into Sunny Suzy, Bodacious.
Now that you understand the principle behind creating a many-to-many relationship, the next
step is to create a dialog that can capture this relationship in terms that the user can under-
stand. The Man2Man program has a main form that contains a grid showing the fields of the
Customer table. There is a button on the main form called Attribute. If you click this button,
a dialog like the one shown in Figure 19.5 appears.
On the left-hand side of the Relater dialog is a list of possible attributes that can be assigned to
a Customer. On the right-hand side of the dialog is a list of the attributes that have in fact been
assigned to the current customer. In between the two lists are two buttons. If you push the
button with the arrows pointing to the right, then the word selected on the left will be added
to the list on the right. That is, the attribute will be assigned to the currently selected customer.
(The Customer list, remember, is back on the main form.) The button with the arrows point-
ing left will delete the current selected attribute in the right-hand list. This, in effect, removes
that attribute from the current Customer’s list of traits.
At this stage, you are ready to prepare a list of things that you must do in order to complete the
many-to-many relationship:
1. Create a way to insert a new item into the Middle table.
2. Assuming you know the CustNo of the current selected record, you need a way to
view the attributes associated with the current customer.
3. Find a way to delete an item from the Middle table.
There are other tasks associated with creating the Relater dialog. For instance, you must put
up the table showing the list of possible attributes, and you must add buttons and grids to the
dialog. However, I am assuming that all these tasks are too trivial to be worth describing. The
key tasks are the ones just listed. Keep your mind focused on them, and the rest will be easy.
To begin, drop down a table, datasource, and grid, and set up the list of possible attributes as
shown in the left-hand side grid in Figure 19.5. Name the TTable object containing this dataset
tblAttrib.
Now drop down a button that will move things from the left-hand grid to the right-hand grid.
Put two arrows on it as shown in Figure 19.5. Drop down a TQuery object, call it qInsert, and
place the following line of code in its SQL property:
insert into middle (CustNo, AttribNo)
values (:CustNo, :AttribNo);
Here is the code you can create in order to fill in the two bind variables called :CustNo and
:AttribNo. This code should be associated with the button that points to the right:
The FCustNo variable is assigned a value when the dialog is launched. It’s the CustNo of the
currently selected Customer, and it is passed in when the dialog is first called by the main form.
The AttribNo value is retrieved from the currently selected record in the grid on the left. In
order to actually insert the data into the database, you call ExecSQL.
The ViewAttribs routine shows the attributes associated with the current Customer. That is, it
is the one responsible for filling in the grid on the right-hand side of the Relater dialog. The
ViewAttribs routine is very simple:
procedure TRelater.ViewAttribs;
begin
qViewAttrib.Close;
qViewAttrib.Params[0].AsInteger := FCustNo;
qViewAttrib.Open;
end;
This code does nothing more than resolve a single bind variable and then open the qViewAttrib
query object. The SQL property of the qViewAttrib query object should look like this:
SELECT DISTINCT A.ATTRIB, A.ATTRIBNO
FROM MIDDLE M, ATTRIBS A
WHERE
(M.CUSTNO = :CustNo)
AND (A.ATTRIBNO = M.ATTRIBNO)
ORDER BY A.ATTRIB
This code selects the Attribute and AttribNo from the Attribs table in all the cases where the
AttribNo in the Attribs table is also found in a record from the Middle table that has the CustNo
of the currently selected Customer. (Phew!) The resulting set of data is shown in the grid on
the right-hand side of the dialog shown in Figure 19.5.
To help make this process intelligible, consider the case I outlined, where Sunny Suzy was both
Nice and Bodacious:
CustNo AttribNo
4 1
4 9
This SQL code searches through the Middle table and finds all the cases where Sunny Suzy is
mentioned in it. In other words, it finds the two records shown. The code then performs a
lookup in the Attribs table, finding the words that are associated with the two AttribNos shown
in the preceding code. Whenever it finds a match, it displays it in the right-hand grid. When
there are only two records in the Middle table, this does not seem like much of a trick, but the
SQL shown here seems a bit smarter if there are thousands of records in the Middle table, only
two of which relate to Sunny Suzy.
At this stage, you are two-thirds of the way through completing the many-to-many relation-
ship. You have found out how to insert records and how to show the list of currently selected
items associated with a particular customer. The only step left is to come up with a technique
for deleting records.
The SQL to perform a delete from the Middle table looks like this:
delete from Middle where
CustNo = :CustNo and
AttribNo = :AttribNo;
Here is the code, associated with the leftward-pointing button, that fills in the bind variables
in the SQL delete code shown in the preceding paragraph:
procedure TRelater.bbDeleteClick(Sender: TObject);begin
qDelete.Params[0].AsInteger := FCustNo;
qDelete.Params[1].AsInteger := qViewAttrib[‘AttribNo’];
qDelete.ExecSQL;
ViewAttribs;
end;
The CustNo bind variable is resolved easily enough because you had the appropriate CustNo passed
in from the main form when the dialog was first created. The qViewAttrib query holds a list of
the currently selected attributes and their AttribNo. Therefore, you can simply ask the
qViewAttrib TQuery object for the AttribNo of the currently selected record in order to find
out what item the user wants to delete. Now both bind variables are satisfied, and you can
perform the deletion by calling ExecSQL!
All in all, a many-to-many relationship is not so bad as long as you are methodical about the
process. Remember that there are three key steps involved:
■ First you have to add to the Middle table.
■ Then you have to show the records associated with the current Customer.
■ And finally, you have to delete from the middle table.
Tackle these tasks one at a time, and the process will not prove to be terribly difficult.
Summary
This chapter gave you a basic introduction to the local InterBase and to several related sub-
jects.
I should stress that InterBase is a very complex and powerful product, and what you have seen
in this chapter should serve as little more than a brief introduction that will whet your appe-
tite. In the next chapter, you will look at stored procedures, triggers, InterBase calls, and a few
other tricks that should help you grasp the extent of the power in both the local and server-
based versions of InterBase.
Delphi protects you from the details of how a server handles basic database chores. However,
Delphi also enables you to tap into the power associated with a particular server. This was one
of the most delicate balances that the developers had to consider when they created Delphi:
How can you make a database tool as generic as possible, without cutting off a programmer’s
access to the special capabilities of a particular server? The same type of question drove the
developers’ successful quest to make Delphi’s language as simple and elegant as possible with-
out cutting off access to the full power of the Windows API.
Advanced InterBase
Techniques 20
In this chapter you get a look at the Music program, which shows a good deal about working
with relational databases in general, and about working with InterBase in particular. This chapter
is also meant to sum up much of what has been said so far about databases.
This chapter features overviews of
■ Relational database design
■ Referential integrity
■ Stored procedures and the TStoredProc components
■ Triggers
■ Generators
■ Domains
■ Table alteration
By the time you finish this chapter, you should have a pretty good feel for how to tap into the
power of InterBase. This chapter is not meant to appeal only to InterBase developers, however.
It also contains many general comments about working with relational databases.
Everything in this chapter also applies to working with Paradox tables. While Paradox might
not support stored procedures, it provides parallel means of providing all the same features which
the Music program accesses through stored procedures.
The Music program uses many of the new features of Delphi32. For instance, there are ex-
amples of lookups, filters, and searching a database with FindFirst. Many other standard da-
tabase techniques are also used in this program, such as searching a database with FindNearest
and working with ranges. There is also an example of how to search records in the detail table
of a master-detail relationship.
In general, this chapter sums up and places in perspective everything said in Chapters 15 through
19. It describes the basic techniques for creating a professional-quality Delphi database.
NOTE
Already there are potential problems emerging. Artist is a good word for the creator of
a song or a work of fiction, but perhaps not a good word for the author of a book on
computers! I could choose a more abstract word, such as Creator, but it’s a bit awkward
to refer to Vivaldi or Miles Davis as Creators. So I stay with the word Artist, despite its
imperfections. Databases provide a great deal of power, but they have a rather rigid
structure that is not always as flexible as one might wish.
The danger with this kind of analysis is that you may consider the word Creator as a
good choice for this field, or perhaps you have another choice that you think is more
appealing. My emphasis, however, is not so much on the particulars of this situation,
but on the necessity of thinking about the names you give to the fields in a table. You
should make these decisions only after serious thought, and you should consider
discussing the matter with others so you can get a wide range of opinions.
The program has two main tables, related in a one-to-many relationship. The master table is
called Artist, and the detail table is called Album. The Album table holds either albums or books,
so perhaps it is poorly named. However, the only other words that work are abstract terms like
Creations, or Items, both of which perhaps have their own problems.
Besides the two main tables, there are several lookup tables used to store the various lists of
possible categories to which the albums and books can belong. For instance, a record can be of
type Jazz, Rock, Folk, Blues, and so on, and a book can be of type Fiction, Computer, Mystery,
Science Fiction , Reference, and so on. These words are stored in the lookup tables.
There is no need to create a complex database, so I will stop with the short list presented above.
Even with this simple structure, however, there are still enough tables to provide some food for
thought. In particular, how will these tables be related, and how can you put constraints on
them so that it’s difficult for the user to accidentally break a dependency? For instance, if there
are six albums associated with an artist, a user should not be able to delete the artist without
first deleting or reassigning the albums. And how about generating the IDs for each artist and
each album? This is not Paradox, so there is no auto-increment field. This means that genera-
tors have to be created, and some means of accessing the generators needs to be employed. (If
you are new to the concept of generators, don’t worry, as you will hear more about them later
in this chapter.)
Clearly there are enough questions to keep someone busy for an hour or two. To resolve these
issues, a specific database schema needs to be generated.
The definition for this table assumes the presence of a domain called CODE_DOM. You can create
a domain in WISQL with the following code:
CREATE DOMAIN CODE_DOM AS INTEGER;
Make sure you understand what is happening here. The GroupCode field in the Album table ref-
erences the Group, or Artist, associated with this particular album. For instance, if Bob Dylan’s
code was 57, and the name of the current album was Blonde on Blonde , the GroupCode field in
the album table would be set to 57. This would tie the album Blonde on Blonde to the artist Bob
Dylan.
Creating a domain called CODE_DOM allows you to easily assign the same type to the Code field in
the Artist table and the GroupCode field in the Album table. It’s not earth-shattering in impor-
tance, but it can be helpful.
NOTE
Notice that the Code field is declared as Not Null. This means that the user cannot
leave this field blank. This rule is implemented by the server and will be enforced
regardless of which front end you use to access the data.
Deciding which fields should be given the value Not Null is one of the more difficult
chores in creating a database. This is one of those situations where I almost never call it
right every time in design-mode. Instead, I am forced to go back and massage my data
after having created a first draft of the data definition.
To change a table using WISQL, you have to call an SQL command called ALTER
TABLE:
ALTER TABLE MYTABLE
ADD NAME VARCHAR(25),
DROP NAMES;
This code adds a field called NAME to a table and drops a field called NAMES. You don’t
have to add and drop fields at the same time; for instance, you can write:
ALTER TABLE MYTABLE
ADD NAME VARCHAR(25)
or:
ALTER TABLE MYTABLE
DROP NAMES
Because you often have to go back and alter the structure of an existing table, make
sure you run many tests on your program before entrusting a large amount of data to
your tables.
After the Code field, the next five fields in the Artist table are pretty straightforward:
LAST VARCHAR(30),
FIRST VARCHAR(30),
TYPE VARCHAR(15),
BORN DATE,
DIED DATE,
BIRTHPLACE VARCHAR(35),
COMMENT BLOB SUB_TYPE TEXT SEGMENT SIZE 80,
The code for creating a BLOB field is a bit tricky-looking, but basically you can just repeat this
code any time you need to create a text blob in InterBase.
The final line in the definition for the Artist table defines the primary key:
PRIMARY KEY (CODE));
This states that the primary key is the Code field. It’s important that Code be a keyed field, since
it will be referenced by a foreign key in the Album table.
Here, once again, in slightly different form, is the definition for the Album table:
CREATE TABLE ALBUM (CODE CODE_DOM NOT NULL,
ALBUM VARCHAR(25) NOT NULL,
TYPES SMALLINT,
LOUDNESS SMALLINT,
MEDIUM SMALLINT,
RATING SMALLINT,
Once again, the Code field is the primary key. This field contains a unique number for each
new Album record that is entered by the user. There is a CHAR field designating the name of
the album or book, and also the GroupCode field that relates each record to the Artist table.
Notice that the GroupCode field is a foreign key referencing the Code field of the Artist table. A
foreign key provides referential integrity. The foreign key asserts that:
■ Every GroupCode entry must have a corresponding Code field in the Artist table.
■ You can’t delete an Artist record if there is a corresponding record in the Album table
with a GroupCode the same as the Code field of the record you are trying to delete.
These two rules go a long way toward describing what foreign keys are all about. They also
help to explain some key aspects of what referential integrity is all about. In particular, note
that these rules are enforced by the server, and they will be implemented regardless of which
front end attempts to alter the table.
NOTE
Referential integrity is not unique to InterBase. In fact, Paradox supplies good tools for
supporting referential integrity. It’s built right into the Database Desktop, and every
Paradox table that you create can have referential integrity if you want it. In most cases,
you do indeed want it!
To see referential integrity in action, run the Music program that comes with this book and try
to delete one of the Artist records that has an Album associated with it. For instance, try to
delete Bob Dylan, Miles Davis, or Philip Glass. Your efforts will be stymied because there are
albums associated with all of these artists.
Go into the Database Desktop, enter a new album, and try to give it a GroupCode that does not
have a corresponding entry in the Code field of the Artist table. The Database Desktop won’t
let you do it. (Note that there are other fields that have foreign keys in this table, so you have
to give valid values all the way round, or you won’t be able to enter a record. You can, however,
leave the other fields blank if you wish.)
The key point here is that referential integrity is being enforced automatically in Delphi, and
automatically in the Database Desktop. In fact, the rules are being enforced on the server side,
and so no matter how you try to get at the data, you have to obey the rules. It’s not just some
client-side code in Delphi; the rule is built into the database itself, which is what you want.
This concept is so important that I will repeat it once more: These rules are enforced automati-
cally no matter what front end the user attempts to use on the table!
The Types, Loudness, Medium, and Rating fields are all integers. Types, Loudness, and Medium are
all foreign keys that reference one of three small tables called, logically enough, TYPES,
LOUDNESS, and MEDIUM :
The structure of these tables ought to be intuitively obvious. The Types table, for instance, is
designed to hold the following records:
select * from types
TYPES CODE
=============== ===========
JAZZ 1
ROCK 2
CLASSICAL 3
NEW AGE 4
FOLK 5
BLUES 6
COMPUTER 1000
FICTION 1001
SCIFI 1002
MYSTERY 1003
REFERENCE 1004
What you have here are six types for albums and five types for books. I separate the two “types
of types” by a large range, so that you can add a virtually unlimited number of additional types
of either kind. (If you want to work with more than 999 different types of music, you have a
problem! I could, of course, have made the split at 10,000 or 100,000 instead of at 1,000, but
it’s unlikely you would want to have more than 999 distinct types of music in this database.)
The key point to grasp here is that you cannot add a number to the Types field of the Album
table unless it has a corresponding entry in the Types table. The foreign key on the TYPES field
is placed there explicitly to enforce this rule. Furthermore, you can’t delete an entry from the
Types table if it has a corresponding element in the TYPES field of the Album table. You could,
however, change the content of one of the strings in the Types table, and thereby either en-
hance, or totally trash, your data.
Astute readers probably noticed that I designed the relationship between the TYPES field of the
Album table and the Types table itself so that it is easy to perform lookups on the Types table
when necessary. You will hear more about this topic later in the chapter, or you can refer to the
discussion of lookup fields in Chapter 17, “Fields and Database Tools.”
If you want to create a new index in WISQL, you can do so with the SQL CREATE INDEX com-
mand, as shown in the preceding code excerpt. The command takes the name of the index, the
name of the table on which the index will be enforced, and finally, in parentheses, the names
of the fields in the index. For more information on this and other commands, see the InterBase
Workgroup Server Language Reference.
The first two indices listed above allow you to perform sorts on the Album table or the Artist
table so that the data is easy to read. For instance, it’s only natural that you would want to sort
the Artist table on the artist’s first and last names.
The ARTIST_TYPE_NDX is provided so you can filter out either the books or the albums, depend-
ing on your current needs. This index is not strictly needed anymore because of the new
OnFilterRecord event, but force of habit makes me feel that filtering on an indexed field is
faster than filtering on a non-indexed field. Also, I will have another use for the OnFilterRecord
event for the Artist table.
The last three indices are provided in case you ever need them. These are such small tables that
the indices probably won’t be of much use, but there isn’t any harm in creating them.
Autoincrement fields are filled in automatically at runtime. Generators, however, merely gen-
erate random numbers in sequence, where the first number generated might be one, the sec-
ond two, and so on. You can tell a generator to start generating numbers at a particular starting
value, where the first number might be X, the next X + 1, and so on.
Here is how to create a generator in WISQL and set it to a particular value:
CREATE GENERATOR MUSIC_GEN;
SET GENERATOR MUSIC_GEN TO 300;
As a result of this code, the first generated number would be 300, the next, 301, and so on.
Here is how to write atrigger that will automatically put this value into the CODE field of the
Artist table whenever an Insert occurs:
This code appears on the server side. It’s not Delphi code. You enter it exactly as in WISQL
and the procedure runs on the server end; it is not processed by Delphi. There is never any
need to call this procedure explicitly. The whole point of triggers is that they run automatically
when certain events occur. This one is designed to run right before an Insert occurs. In other
words, the way to call this procedure is to perform an Insert!
This code states that you want to create a trigger called SetMusicGen to be run on the Artist
table. The generator is called before an insert operation:
BEFORE INSERT AS
The NEW statement says that you are going to define the new value for a particular field that is
about to be inserted into a table. In this case you are referencing the new value for the CODE
field of the Artist table.
GEN_ID is a function built into InterBase that produces an integer value. It takes a generator as
its first parameter and a step value as its second parameter. The step value will increase or de-
crease the value produced by the generator. For instance, the preceding code increments the
value by 1.
You can get a generator to fill in a field automatically with the trigger shown above. Unfortu-
nately, Delphi does not provide particularly good support for triggers, in part because each
server generates a different kind of trigger. The developers of Delphi didn’t want to run around
finding out how to handle triggers for 30 different kinds of servers, and neither did the devel-
opers of the BDE. There are some third-party solutions to this problem, however, including a
good one that works with InterBase.
In the example under discussion, Delphi’s poor support for triggers is irrelevant because the
table is not sorted on the CODE field. If it were, this trigger might cause Delphi to lose track of
the current record after the insert operation. Delphi would not know that the Code value was
inserted, because it would not know that the trigger fired. As a result, the current record might
be lost because it had been sorted on a value of which Delphi was not aware. In other words,
the index would cause the record to be moved to a particular place in the dataset, but Delphi
would not know how to follow it. As far as Delphi is concerned, the CODE field would still be
blank! (There would not, however, be a referential integrity exception raised, since the InterBase
server would be aware that the field was inserted!)
You can now see that in this case there is no problem using a trigger because the table is sorted
on the LAST and FIRST fields, not on the CODE field.
NOTE
For a detailed understanding of how to create triggers, turn to the Language Reference
for the InterBase server.
I show you this trigger because it works fine when used with Delphi. Delphi does not
need to know that the Set_Company_Upper trigger occurred. The trigger can take place
in the background, without impacting Delphi’s inner workings. However, if you use a
trigger that affects a field that Delphi sorts on, you can have trouble. Delphi will not
know that the new value for the field has been inserted, and thus will not know to
search on that value to find the newly inserted record. Suppose, for instance, that you
are sorting records on a field called CustNo, which can have values between 0 and 64
KB. If you insert a new record, and you use a trigger to assign a new CustNo value,
Delphi will never know the number assigned by Interbase. The server will then
automatically sort the records on the new number, but Delphi will be left in the lurch,
not knowing that the record you were editing has been moved to a different part of the
table based on the value of its CustNo field. As far as Delphi is concerned, the record
you were editing just disappeared when you posted it. It won’t know where to look
for it!
If you find yourself in a situation where you can’t use a trigger, there is no great need for alarm.
The absence of trigger support is not a big concern under most circumstances. Instead of using
a trigger, you can use a stored procedure to retrieve the next number from a generator.
A stored procedureis simply a routine that is stored on the server side rather than being listed in
your Object Pascal source code. Like the language for writing triggers, there is a unique lan-
guage for writing stored procedures that has nothing to do with Object Pascal or SQL. In fact,
you need to keep in mind that there is no particular relationship between Delphi and InterBase.
They are made by two different teams, using two different languages, with two different goals
in mind. The stored procedure language was made up long before anyone ever thought of cre-
ating Delphi, and in general, the two languages have absolutely nothing to do with one an-
other.
Stored procedures are not difficult to create. Here, for instance, is a stored procedure that re-
turns the next number generated by the Music_Gen generator:
CREATE PROCEDURE GETMUSICGEN
RETURNS (NUM INTEGER)
AS
BEGIN
NUM = GEN_ID(MUSIC_GEN, 1);
END
The first line tells WISQL that you are going to create a procedure called GetMusicGen. The
next line states that it is going to return a value called Num, which is an integer. The AS state-
ment tells InterBase that you are now ready to define the body of the procedure. The proce-
dure itself appears between a BEGIN..END pair, and consists of a call to the GEN_ID function,
which returns the next number from the MUSIC_GEN generator. When it retrieves the number,
it asks InterBase to increment its value by one.
NOTE
Stored procedures are handled on the Delphi end with either a TStoredProc compo-
nent, or by returning an answer set by way of a SQL statement. In general, if the stored
procedure returns several rows of data, you access it by way of a SQL statement in a
TQuery component. If the stored procedure returns only a single item of data, you can
call it with a TStoredProc component. Examples of calling stored procedures from
Delphi appear in the next section.
There are, as I said earlier, two generators, one trigger, and one stored procedure in the data
definition for the MUSIC database. The stored procedure generates a unique number for the
Code field in the Album table. As with the Artist table, I could have used a trigger instead of a
stored procedure. However, I wanted to provide an example of using a stored procedure in this
type of situation.
As you can see, this is a simple stored procedure that does nothing more than return a single
value.
NOTE
If GetAlbumGen returned several rows of data rather than a single value, you would call
it by using a TQuery object rather than a TStoredProc object. The SQL statement to use
in such a case would be Select * from GetAlbumGen.
After selecting the procedure to use with the TStoredProc, you can pop up the Params field to
see the parameters passed to or returned by the function. In this case there is only one param-
eter, which is returned as the result of the function.
Whenever the user wants to insert a record into the Album table, the following procedure is
called:
procedure TMusicAlbum.sbInsertClick(Sender: TObject);
var
S: string;
begin
S := ‘’;
if not InputQuery(‘Insert New Album Dialog’, ‘Enter album name’, S) then Exit;
Form1.tblAlbum.Insert;
GetAlbum.ExecProc;
Form1.tblAlbum.FieldByName(‘Code’).AsInteger := GetAlbum.Params[0].AsInteger;
Form1.tblAlbum.FieldByName(‘Album’).AsString := S;
Form1.tblAlbum.FieldByName(‘Types’).AsString := ‘’;
Form1.tblAlbum.FieldByName(‘Loudness’).AsString := ‘’;
Form1.tblAlbum.FieldByName(‘Medium’).AsString := ‘’;
Form1.tblAlbum.Post;
lcbType.SetFocus;
end;
The key lines of this procedure are the ones involving the stored procedure:
GetAlbum.ExecProc;
Form1.tblAlbum.FieldByName(‘Code’).AsInteger := GetAlbum.Params[0].AsInteger;
This code first executes the stored procedure, then snags its return value from the Params field
of the TStoredProc. The Params field for stored procedures works the same way as the Params
field for TQuery objects.
NOTE
Notice that you are going to get back a dataset, not just three variables, when you call
this function. In other words, you will get back a series of records, each containing the
SLAST, SFIRST ,and STITLE fields. The results of the query are placed in the dataset via
the syntax that reads:
INTO : SLAST, SFIRST, STITLE;
The SUSPEND statement temporarily delays the execution of the procedure while a Fetch
statement is carried out. Imagine the moment after the first row has been calculated,
and while it is being returned via the calling procedure. The loop is temporarily put on
hold while this is going on; that is, it is SUSPENDed. After the fetch is complete, the next
row is calculated, and so on. You would use SUSPEND statements in procedures that
return rows of values, not in a simple procedure that returns only one value.
The point here is that you can store a particular query in a stored procedure, then call
it from a TQuery or TStoredProc component. This approach can eliminate considerable
complexity in programs that need to perform complex operations such as creating
many-to-many relationships. This process allows you to keep a SQL statement on the
server side rather than in the SQL property of a TQuery component.
As you can see, Delphi makes it easy for you to use stored procedures in your programs. In this
particular case, you could have used a stored procedure rather than a trigger. However, it is not
a great crisis if you have to use a stored procedure rather than a trigger. The one advantage
triggers have over stored procedures is that they are called automatically, thereby helping you
to ensure data integrity.
/* Domain definitions */
CREATE DOMAIN CODE_DOM AS INTEGER;
COMMIT WORK;
SET AUTODDL OFF;
SET TERM ^ ;
/* Stored procedures */
CREATE PROCEDURE GETALBUMGEN AS BEGIN EXIT; END ^
BEGIN
NUM = GEN_ID(ALBUM_GEN, 1);
END
^
SET TERM ; ^
COMMIT WORK ;
SET AUTODDL ON;
SET TERM ^ ;
If you want to create this database from scratch, you can run this entire statement through
WISQL from the File menu. Otherwise, you could create the database and pass the statements
through one at a time. A third use for the code is simply to give you one place to look when
you need a reference for MUSIC.GDB. To create the output shown above, select Extract |
SQL Data From Database from the WISQL menu.
on the future of this issue. If PC-based developers can use Distributed OLE to place rules eas-
ily on remote servers, the whole way databases are constructed is likely to change. In other words,
if I could use Delphi to enforce a bunch of rules, then encapsulate those rules in an object that
resides on the same machine as the InterBase server, I would do it. I would put all my database
logic out there, and just provide a few entry points for my front-end program to call. This is
not yet a reality, however, and so I would suggest not getting too heavily involved in placing
complex logic out on a server where you can’t debug it or maintain it easily. One man’s
opinion!
FIGURE 20.1.
The Index page from
MUSIC.DPR enables you
to view artists and their
related productions.
NOTE
I use the following code to prevent the user from using the Insert key to insert a record
into either of the grids on the Index page:
procedure TIndexForm.DBArtistGridKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if (Key = vk_Insert) or (Key = vk_Delete) then
Key := 0;
end;
This code takes Insert keystrokes and Delete keystrokes and disarms them. Users never
know the method is being called, but find that they can’t use the Insert or Delete keys.
The code is called in response to OnKeyDown events.
The second page in the Music program allows you to see one record from the Artist table, as
shown in Figure 20.2. The general theory is that users can browse or search through the data
using the Index page shown in Figure 20.1. If they want to look at a particular record, they can
switch to the Artist, Album, or Book pages. Figures 20.3 and 20.4 show the latter two of these
three pages.
FIGURE 20.2.
The Artist page from
the Music program.
FIGURE 20.3.
The Album page from the
Music program.
FIGURE 20.4.
The Book page from the
Music program.
Traditionally, relational databases have been shown to users all on one screen. The typical tech-
nique used is like that demonstrated in the Relate program in Chapter 17, “Fields and Data-
base Tools.” In general, the idea is to put the master table at the top of a screen, and to put the
detail tables beneath it, finding room for all the fields as best one can.
I prefer to use pages, where each page represents a different table, because it cuts down on the
use of cluttered, hard-to-comprehend screens. However, paged relational databases can be con-
fusing to users who don’t understand the structure of the database. There are plenty of examples
of database programs that throw several tables onto one cluttered screen and let the user have
at it. In this chapter you will get a look at an alternative structure for presenting relational da-
tabases to users.
The key point to grasp is that you need to convert the standard Delphi pop-up form to a child
form that has Form1 as its parent. Here is how to proceed:
unit Ritchey1;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs;
type
TRitchey = class(TForm)
private
{ Private declarations }
public
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
end;
var
Ritchey: TRitchey;
implementation
{$R *.DFM}
procedure TRitchey.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
end.
This is the work of Pat Ritchey, a long-time Object Pascal developer who clearly understands
how Windows, as well as Delphi, is put together.
When using this technique, you want the parent form to explicitly create the child forms. To
do this, do not auto-create the forms, but choose Options | Project | Forms, and move the forms
from the Auto-Create list box into the Available Forms list box. Then you can create the forms
as needed inside Form1, with code that looks like this:
MusicAlbum := TMusicAlbum.Create(Self);
I will describe this process in more depth below, in the section that examines the FormCreate
event for the main module.
To make sure the form adheres to the dimensions of its parent, you can respond to OnResize
events:
procedure TForm1.FormResize(Sender: TObject);
var
i: Integer;
begin
for i := 0 to MaxForms do
MoveWindow(ChildForms[i].Handle, 0, 0, Panel1.ClientWidth,
Panel1.ClientHeight, True);
end;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
DB, DBTables, Grids,
DBGrids, ExtCtrls, Tabs,
Menus;
const
MaxForms = 3;
type
TChildType = (cfIndex, cfArtist, cfAlbum, cfBook);
TForm1 = class(TForm)
TabSet1: TTabSet;
Panel1: TPanel;
MainMenu1: TMainMenu;
File1: TMenuItem;
Exit1: TMenuItem;
Options1: TMenuItem;
Search1: TMenuItem;
N1: TMenuItem;
Index1: TMenuItem;
continues
FArtistCode: Integer;
FChildType: TChildType;
ChildForms: array[0..MaxForms] of TForm;
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
Index1, Mus2Alb, Artist1,
Bookfrm1, Globals, Search1;
{$R *.DFM}
continues
ChildForms[NewTab].BringToFront;
end;
continues
end.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
Grids, DBGrids, DB,
ExtCtrls, Buttons, DBTables;
type
TIndexForm = class(TForm)
Panel1: TPanel;
Panel2: TPanel;
Header1: THeader;
DBArtistGrid: TDBGrid;
DBGrid2: TDBGrid;
sbSearch: TSpeedButton;
Query1: TQuery;
procedure sbSearchClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Header1Sizing(Sender: TObject; ASection, AWidth: Integer);
procedure DBArtistGridKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
procedure DBGrid2KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
end;
var
IndexForm: TIndexForm;
implementation
uses
Main, NewArt, Globals;
{$R *.DFM}
procedure TIndexForm.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
end.
interface
uses
SysUtils, WinProcs, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, WinTypes,
Grids, DBGrids, StdCtrls,
Mask, DBCtrls, DB,
ExtCtrls, Buttons;
type
TArtistForm = class(TForm)
Panel1: TPanel;
Panel2: TPanel;
dbeLast: TDBEdit;
dbeFirst: TDBEdit;
sbInsert: TSpeedButton;
Label1: TLabel;
Label2: TLabel;
dbeType: TDBEdit;
DBEdit2: TDBEdit;
dbeDied: TDBEdit;
dbeBirthPlace: TDBEdit;
dbeComment: TDBMemo;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
Label7: TLabel;
sbDelete: TSpeedButton;
Shape3: TShape;
Shape4: TShape;
DBNavigator1: TDBNavigator;
procedure sbInsertClick(Sender: TObject);
procedure sbDeleteClick(Sender: TObject);
private
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
public
{ Public declarations }
end;
var
ArtistForm: TArtistForm;
implementation
uses
Main, NewArt, Index1,
Globals;
{$R *.DFM}
procedure TArtistForm.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
end.
interface
uses
SysUtils, WinProcs, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, WinTypes,
Grids, DBGrids, DB,
continues
type
TMusicAlbum = class(TForm)
DataSource2: TDataSource;
Panel1: TPanel;
Label1: TLabel;
Label2: TLabel;
Label4: TLabel;
dbAlbum: TDBEdit;
Label5: TLabel;
Panel2: TPanel;
DBText1: TDBText;
Panel3: TPanel;
DBNavigator1: TDBNavigator;
sbDelete: TSpeedButton;
Image1: TImage;
OpenDialog1: TOpenDialog;
Bevel1: TBevel;
Label3: TLabel;
Spin1: TSpinButton;
Bevel2: TBevel;
Image2: TImage;
lcbType: TDBLookupComboBox;
lcbLoud: TDBLookupComboBox;
lcbMedium: TDBLookupComboBox;
GetAlbum: TStoredProc;
sbInsert: TSpeedButton;
procedure Spin1DownClick(Sender: TObject);
procedure Spin1UpClick(Sender: TObject);
procedure ComboChange(Sender: TObject);
procedure sbInsertClick(Sender: TObject);
procedure sbDeleteClick(Sender: TObject);
procedure DataSource2StateChange(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
procedure LoadCombos(T: TTable; C: TComboBox);
procedure SetCombo(T: TTable; C: TComboBox);
procedure SetField(T: TTable; C: TComboBox);
procedure SetLabelColor(Color: TColor);
public
{ Public declarations }
end;
var
MusicAlbum: TMusicAlbum;
implementation
uses
Main, StrBox, Globals;
{$R *.DFM}
procedure TMusicAlbum.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
continues
Form1.tblAlbum.FieldByName(‘Medium’).AsString := ‘’;
Form1.tblAlbum.Post;
lcbType.SetFocus;
end;
end.
interface
uses
SysUtils, WinProcs, WinTypes,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
DB, StdCtrls, Mask,
DBCtrls, DBTables, ExtCtrls,
Buttons, Spin;
type
TBookForm = class(TForm)
Panel1: TPanel;
Panel2: TPanel;
DBEdit1: TDBEdit;
DBNavigator1: TDBNavigator;
sbInsert: TSpeedButton;
sbDelete: TSpeedButton;
continues
var
BookForm: TBookForm;
implementation
uses
StrBox, Main, Globals;
{$R *.DFM}
procedure TBookForm.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
Params.Y := 0;
end;
continues
end.
Listing 20.6. The New Artist form from the Music program.
unit Newart;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Mask, DBCtrls,
DB, Buttons, DBTables;
type
TNewArtist = class(TForm)
dbLast: TDBEdit;
dbFirst: TDBEdit;
Label1: TLabel;
Label2: TLabel;
bbCancel: TBitBtn;
bbOk: TBitBtn;
DataSource1: TDataSource;
Label3: TLabel;
DBComboBox1: TDBComboBox;
MusicGen: TStoredProc;
procedure bbOkClick(Sender: TObject);
procedure bbCancelClick(Sender: TObject);
procedure FormShow(Sender: TObject);
end;
var
NewArtist: TNewArtist;
implementation
uses
Main;
{$R *.DFM}
end.
Listing 20.7. The Globals form tracks some global variables and contains some
general-purpose utilities.
unit globals;
interface
const
BookStr = ‘book’;
MusicStr = ‘music’;
type
TIndexMode = (imPrimary, imMusic, imBook);
var
IndexMode: TIndexMode;
uses
StrBox;
end.
Listing 20.8. The Search unit lets you search on the Album table.
unit Search1;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Buttons, ExtCtrls, Grids,
DBGrids, DB, DBTables;
type
TAlbumSearch = class(TForm)
Table1: TTable;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
Panel1: TPanel;
Edit1: TEdit;
bbOk: TBitBtn;
bbCancel: TBitBtn;
procedure Edit1Change(Sender: TObject);
public
function AlbumSearchModal: Integer;
end;
var
AlbumSearch: TAlbumSearch;
implementation
{$R *.DFM}
end.
You have already had several different explanations of how the Music program is structured
and how it works, so I won’t spend any time discussing the program itself. I would recom-
mend, however, that you spend some time working with it so that you understand how to interact
with it; practice inserting records, deleting records, and searching for records.
While working with this program you should remember that it is an example from a book, and
not a polished professional program. You will probably be able to find some ways to improve
this program, and it should not be difficult to crash it if you set your mind to it. As always, I
have had to make a choice between keeping the program easy to understand, and loading it up
with code that does nothing more than check for user errors.
What the Music program does well is show you how to work with a relational database in a
Delphi program. There are five different interrelated tables in this program, all working to-
gether to produce a particular result. The thing to come away with when studying this pro-
gram is how you can make the tables in a relational database work together seamlessly toward
a particular end. This chapter is about how to construct a powerful relational database, and the
Music program is the main exhibit.
Because this program uses InterBase, you could use it in a rigorous multiuser environment
without fear that it would collapse under the load. For instance, there is no reason why this
program, as is, could not handle two or three hundred simultaneous users.
NOTE
Most of the DataAccess controls for the Music program are placed on the main form. I
could also have placed them on a datamodule, but on a paged program of this sort no
visual tools are ever placed directly on the main form, so its surface is free. Given the
circumstances, why bother with a datamodule?
FIGURE 20.5.
The alias for MUSIC.GDB
as it appears in the
Database Explorer.
The DatabaseName property of the TDatabase object is set to the string MusicData, which is the
alias attached to by all the other TTable, TStoredProc, and TQuery objects in the program. Re-
member: Only the TDatabase object attaches directly to the Music alias. This enables you to
point the entire program at a second database by changing only one variable: the AliasName
property. This feature can be handy if you need to experiment with the program, but don’t
want to be working with your primary data while you are doing the testing.
The Params property for the TDatabase object contains the following information:
USER NAME=SYSDBA
PASSWORD=masterkey
The LoginPrompt property is then set to False, which makes it possible to launch the program
without having to enter a password. This is pretty much a necessity during development, and
it’s a useful trait in a program like this that will probably have little fear of hostile attacks on its
data.
The first four lines of the routine create the forms. The next four lines assign them to an array
of TForm objects. You can then use this array to iterate through all the main forms for the pro-
gram, as shown in the last two lines of the routine, and in the OnResize response method shown
above.
The following type is used to track the child forms:
TChildType = (cfIndex, cfArtist, cfAlbum, cfBook);
As you can see, cfIndex, with an ordinal value of 0, would give you immediate access to the
Index form if you wrote code that looks like this:
ChildForms[cfIndex].Width := X;
The cfArtist, cfAlbum, and cfBook elements can also be used to access their appropriate re-
lated forms in the ChildForms array.
The importance of referencing the ChildForms array by number increases when you see that
the TTabSet control has four tabs on it, with element zero equal to Index, element one to
Artist, and so on. This means that the tabs clicked by the user also map into the enumerated
type. To see this principle in action, see the FormResize method referenced above, and the
TabSet1Change method discussed at the beginning of the next section.
begin
tblTypes.Filtered := False;
tblMedium.Filtered := False;
FChildType := TChildType(NewTab);
if (FChildType = cfBook) or (FChildType = cfAlbum) then begin
tblTypes.Filtered := True;
tblMedium.Filtered := True;
end;
ChildForms[NewTab].BringToFront;
end;
The key line here is the last, which is the one that brings the appropriate form to the top of the
stack of visible forms.
The first seven lines of code in the TabSet1Change method are used to set up the Album and
Books forms so that their dropdown combo boxes provide the right list of choices to the user.
In particular, when you flip to the Album page, and drop down the Types combo box, you see
a list of choices associated with albums, such as Jazz, Folk, and so on. If you then flip to the
Book page, you see a list of options associated with books.
You saw earlier that the Types table is divided into two sections. Entries with an ID less than
1,000 are associated with music, and those with an ID of 1,000 or greater are associated with
books. The OnFilterRecord event is used to filter this list so that the user sees the appropriate
half of the list when viewing the appropriate page.
TabSet1Change starts filtering data only if the user chooses the Album or Book pages:
if (FChildType = cfBook) or (FChildType = cfAlbum) then begin
tblTypes.Filtered := True;
tblMedium.Filtered := True;
end;
A quick glance shows that this code filters the records in the Types table so that the user can see
only those that are appropriate to the currently selected page.
To actually show the types to the user, I employ a TdbLookupComboBox. In particular, note that
the Types field can be assigned values by simply doing a lookup in the Types table. I define a
lookup field for this purpose in the Album table, calling the field luTypes. There are also luMedium
and luLoudness lookup fields. These are standard lookups of the kind described in Chapter 17,
“Fields and Database Tools.” As I mentioned earlier, the Types, Loudness, and Medium tables
were designed to make it easy to do lookups in them from the Album table. The fruits of this
forethought are the easy-to-use TdbLookupComboBoxes on the Album and Book pages. If you want
to assign a type to an album, you just drop down the combo box and pick a Type: Jazz, Rock,
Classical, and so on.
If you want to add more types, simply put more entries in the Types table. Make sure that musical
entries have an ID under 1,000 and that book entries have an ID of 1,000 or greater.
FIGURE 20.6.
The Search dialog
lets you search on the
Album table.
Here is the code, attached to the OnChange event of the edit control, that lets the user search
through the Album table:
procedure TAlbumSearch.Edit1Change(Sender: TObject);
begin
Table1.FindNearest([Edit1.Text]);
end;
Notice that this is a standard edit control, and not a TdbEdit control.
The dialog has one other method, used to report the results of the search back to the calling
routine:
function TAlbumSearch.AlbumSearchModal: Integer;
begin
if ShowModal = mrCancel then
Result := -1
else
Result := Table1[‘GroupCode’];
end;
If the user clicks the Cancel button, the routine returns -1, which means that no further process-
ing is necessary. If the user clicks the OK button, the GroupCode associated with the current
record is returned. The GroupCode, of course, contains the Code from the record in the Artist
table that you now want to locate.
Here is the routine on the main form that calls the SearchAlbum dialog:
procedure TForm1.SearchAlbums1Click(Sender: TObject);
var
OutCome: Integer;
begin
AlbumSearch := TAlbumSearch.Create(Self);
OutCome := AlbumSearch.AlbumSearchModal;
AlbumSearch.Free;
if OutCome = -1 then
Exit
else begin
FArtistCode := OutCome;
tblArtist.FindFirst;
end;
end;
The first three lines of the routine allocate memory for the dialog, call the AlbumSearchModal
method, and free the memory associated with the dialog. Note that you should choose Project |
Options | Forms and remove the SearchAlbum dialog from the Auto-Create list box in order to
make this process work correctly.
After the dialog has been shown to the user, the integer OutCome variable holds either a -1 or the
Code of the record that you want to find in the Artist table. If the value is -1, the user chooses
Cancel, and the function immediately exits. If there is a valid code in the OutCome variable, it is
assigned to a global variable called FArtistCode, and a FindFirst operation takes place on the
Artist table.
FindFirst operations on database tables cause the OnFilterRecord event associated with the
table to be called until a particular record is found. Here is the OnFilterRecord event for the
Artist table:
Clearly this is a simple process. You should note, however, that code of this kind is highly
optimized and will allow you to perform very fast searches.
NOTE
It’s important to understand why this search is carried out in this manner rather than
using a call to FindNearest or FindKey. These latter two calls are dependent on particu-
lar indices being in place. In this case, you don’t want to switch indices, but want
instead to perform a search in the background without doing anything that might
change the way the data looks to the user. The FindFirst or FindNext procedures fit
the bill in this case and allow you to perform the search in a rapid, unobtrusive
manner.
NOTE
This is another situation where the relentless inflexibility of databases can be a bit
frustrating. For instance, what can you do about musicians, such as Arnold
Schoenberg, Bob Dylan, and Jimmy Buffet, who also write books? The simplest
solution, and the one I have opted for, is to simply ignore this situation. Another
possible option would be to create a Type called BothAuthorAndMusician, but that has
its own problems since Jimmy Buffet would suddenly not be listed in a filter set to
catch musicians. A more thorough solution would be to turn the Types field into part
of a many-to-many relationship. But then you are faced with difficulties whenever you
want to perform a Range operation on this field, as well as the innate complexity of
executing the many-to-many relationship. In general, I find that ignoring the excep-
tions to a rule really is the best way of handling certain annoying situations!
Because the Types field is expected to contain a certain kind of data, you must rigorously con-
trol the way data is put into this field. I am not going to discuss the input routines used in this
program for inserting records, but you might want to glance at them and notice that I never
leave it up to the user to just type characters into this field. Instead, I force the user to make a
choice between the word “Book” or the word “Music.” In situations like this, you should con-
sider having the Types field declared as Not Null.
If the user chooses the menu choice that filters the Artist table so that only records of albums
or books are shown, the following procedure is called:
procedure TForm1.ShowOnlyMusic1Click(Sender: TObject);
begin
IndexMode := TIndexMode((Sender as TMenuItem).Tag);
case IndexMode of
imPrimary: tblArtist.IndexName := ‘ARTIST_LASTFIRST_NDX’;
imMusic: begin
tblArtist.IndexName := ‘ARTIST_TYPE_NDX’;
tblArtist.SetRange([‘Music’], [‘Music’]);
end;
imBook: begin
tblArtist.IndexName := ‘ARTIST_TYPE_NDX’;
tblArtist.SetRange([‘Book’], [‘Book’]);
end;
end;
end;
The IndexMode is calculated by the Tag field of the menu item selected to call the routine. The
possible menu choices are:
Show All
Show Only Music
Show Only Books
If the user selects Show All, the default sort method for the table is selected:
imPrimary: tblArtist.IndexName := ‘ARTIST_LASTFIRST_NDX’;
If the user elects to see only albums, the following code is executed:
imMusic: begin
tblArtist.IndexName := ‘ARTIST_TYPE_NDX’;
tblArtist.SetRange([‘Music’], [‘Music’]);
end;
This code selects an index that sorts on the Types, Last, and First fields. The range is then set
so that only those records with a Type field of Music will be shown. Notice that in this situa-
tion, it’s OK to re-sort the data in the middle of the operation, since the user expects the data
to change in appearance after choosing this menu option. The user would not, however, ex-
pect the data to be resorted in the middle of a search operation.
Summary
That’s all I’m going to say about the Music program. There are a few lines of code in the pro-
gram that were never mentioned, but most of the program has been reviewed in this chapter.
Once again, this program contains a lot of the basic code that anyone uses when constructing
a relational database with Delphi. The particular example shown is not robust enough to use
in a professional setting, but it gives you a good feeling for how to proceed if you want to con-
struct such an application.
In particular, you got a good look at the techniques used to create a robust database with ref-
erential integrity. You also saw how to use generators, triggers, and stored procedures, and how
to perform filters and lookups on relational data. In general, this chapter is the one that sums
up the core information necessary to produce a professional database program. If you under-
stand all this material, you are not yet necessarily an expert, but you are ready to start building
relational databases in a professional setting.
ReportSmith
21
by Andy Fears
Now that you have finished the front end, it’s time to write the reports. Which report writing
tool do you use? The obvious choice for Delphi programmers is ReportSmith because it is
bundled with Delphi. This chapter details the interface between Delphi and ReportSmith.
Unfortunately, in only one chapter I cannot go into too much detail on the ReportSmith side,
but I can provide firm foundation for further ReportSmith development.
You are going to build two reports, both with the exact same layout and functionality. “Why
build the same report twice,” I hear you ask, “are you crazy?” There is reason behind this mad-
ness. The first report is built in the traditional sense, using ReportSmith. It generates its own
SQL statement and uses report variables to interface with Delphi. The second report takes
advantage of the new Delphi32 features and interfaces with Delphi using the TQuery compo-
nent.
The last section examines a third example of a Delphi form. This form is used to dynamically
create a ReportSmith report using Delphi.
Example Report 1
This chapter demonstrates how to build two reports that are linked to a front-end Delphi ap-
plication. In the examples that follow, assume that you are working on a project for a video
rental store. You will use the demo tables that come with ReportSmith. These are dBASE da-
tabases that reside in the rptsmith\demos directory. The first report you will build is a Cus-
tomer Invoice report that lists each customer and its corresponding invoices. The manager of
the video rental store has asked for four specific criteria for the report she wants you to build:
■ It must report either all customers or one specific customer.
■ It must show the total amount owed by each customer.
■ It must calculate a discounted total for each invoice according to a specified discount
percentage.
■ It must be able to highlight invoices that are over a certain dollar amount.
Building Reports
Having heard the requests from the rental manager, you can now begin. Start up ReportSmith
and select Columnar Report as the report type. You are now presented with the Report Query
dialog box, with the Tables page showing. (See Figure 21.1.)
NOTE
The Report Query dialog box has several pages, including Tables, Selections, and
Sorting. This dialog box is used to build the SQL statement for the report. (For
example, adding a table adds a table to the SQL; adding a sort order adds an Order By
statement to the SQL; adding Database Grouping adds a Group By statement.) Under-
standing how each page of the Report Query dialog box manipulates the SQL state-
ment will greatly help your report building and will further your understanding of the
SQL language, which is key to building reports.
FIGURE 21.1.
Adding the CUSTOMER
and INVOICES tables to
the report.
Select Add Table and choose the CUSTOMER.DBF table. Next choose Add Table and select
the INVOICES.DBF table. Using the Table Columns button, select the following columns to
be included in the report from each table:
FIGURE 21.2.
ReportSmith displays a
warning if the SQL query
contains tables that are not
joined.
The Create New Table Link dialog box, shown in Figure 21.3, is used to link two tables to-
gether. The top section shows the tables included in the report, and the bottom half of the
dialog box shows which columns are available within these tables.
Select the CSTMR_ID column from the Customer table and the CSTMR_ID column from
the Invoices table. Link these columns using the relational operator (=).
NOTE
ReportSmith tries to guess the join for you by selecting columns that have the same
name from each table. It is common practice among database designers to name
columns that are used to join tables with the same name.
You will notice that on each side of the Create New Table Link dialog box is an Include Un-
matched Records checkbox. When checked, it enables records from the table to be returned
even if such records do not satisfy the join condition.
FIGURE 21.3.
Linking tables using the
Create New Table Link
dialog box.
For example, customer “Fred Jones and Company” exists in the Customer table but does not
have any invoices in the Invoices table. Because Fred has no matching invoice records, the pre-
ceding query skips Fred, and he does not appear in the report.
If you want to see cases like Fred (customers without invoices), check the Include Unmatched
Records checkbox. Now customers with no invoices are also returned. The invoice informa-
tion for these customers is automatically set to null by the SQL query. This option is known as
an outer join and is specific for each server. Each server has its own level of support for outer
joins; therefore, the checkbox is dynamic and appears only if an outer join is available.
NOTE
Use outer joins only where necessary, because a query with an outer join is not as
efficient as a query with a straight join.
Now that you have completed the initial SQL statement for the report, choose the Done but-
ton. ReportSmith runs the SQL query and returns the data to the report. One of the first things
you will notice is that you are presented with live data, not just a representation of the data.
Within the ReportSmith design environment you always work with live data so that it is easy
to see what the final report will look like to the user.
Initially all data is presented on one line (you will change this later). First, select the first col-
umn in the report, then hold down the Shift key and select the last column. All columns are
now selected. Choose the Best Fit button from the toolbar. This compresses the column width
for each column, so that now all columns fit on one page, as shown in Figure 21.4.
FIGURE 21.4.
The initial layout for the
Customer Invoices report.
Now you are ready to lay out the report. Select the two CSTMR_ID columns by holding down
the Ctrl key and clicking each column, then remove them by pressing the Del key. These col-
umns are used internally to join the two tables, but they are not needed in the report layout.
NOTE
Deleting columns from the report layout does not remove them from the SQL query.
If a column is deleted by mistake it can be added to the layout by using the Insert |
Field menu item to display the Insert Field dialog box, shown in Figure 21.5.
FIGURE 21.5.
The Insert Field dialog
box can be used to add
fields to the report
layout.
As you can now see from the initial layout, the customer information is being repeated because
some customers have more than one invoice. You need to tell ReportSmith to group informa-
tion from the same customer together. In other words, you want to see each customer once,
followed by that customer’s invoices. To do this you must create a Report Group.
Report Groups are created as a logical way to group the data in your report. To create a Report
Group, choose the Tools | Report Groupings menu item. This displays the Define Groups dialog
box shown in Figure 21.6.
FIGURE 21.6.
Create Report Grouping
using the Define Groups
dialog box.
The Define Groups dialog box shows what groups are currently created. In this report the only
group present is the Entire_Report_Group. The Entire_Report_Group encapsulates all the data
within the report and cannot be deleted. Select the CSTMR_NAME field and the choose New
Group. A new group is created and named CSTMR_NAME_Group.
Choose Group Properties, which displays the Group Properties dialog box. The Group Prop-
erties dialog box enables you to specify which fields to suppress and how the grouping is per-
formed.
Use the Group By combo box to control how the grouping is performed. There are two op-
tions:
■ SameValue: This keeps values that are the same together. In this example, you want
invoices for the same customer to be kept together, so select this option.
■ Every: This enables a hard-coded number of records to be grouped together. For
example, you can create a group that groups every 10 records together, regardless of
their values.
Select the CSTMR_NAME field and choose Suppress. An asterisk appears next to
CSTMR_NAME; this means that the customer’s name is no longer repeated for each invoice
but appears only for each new customer, as shown in Figure 21.7.
FIGURE 21.7.
Adding properties to the
CSTMR_NAME_Group.
Choose the OK button to close both of these dialog boxes and return to the layout editor. The
report layout is changed in accordance with the newly created CSTMR_NAME_Group.
ReportSmith has separated the detail section into separate sections, each one belonging to an
individual customer. One important point to remember is that Report Groups and the report
order go hand in hand. Report grouping is performed by ReportSmith on the client machine.
It expects the rows to be returned in the order specified by the Report Groups. To ensure that
this is the case, you need to add an Order By statement to the SQL. Choose Tools | Sorting to
display the Sorting page of the Report Query dialog box, shown in Figure 21.8. Select the
CSTMR_NAME field and drag and drop this into the Sort list. Select Done to return to the
report layout. You are now assured that CSTMR_NAME_Group will be correct, because you
are also ordering on the CSTMR_NAME column.
FIGURE 21.8.
Sorting by
CSTMR_NAME.
Report Groups can be nested. If the preceding report included customers, invoices, and then
invoice items, it would be possible to create a second Report Group to group invoices by in-
voice ID. This group would be nested under the main customer name group. Again, the report
order must match. In this example, it would be nested first on CSTMR_NAME and then on
INVOICE_ID. You can try this by adding the table DETAIL to the report.
Report grouping can also be created by selecting the required column and using one of the
toolbar buttons shown in Figure 21.9. When Report Groups are created in this way, the fol-
lowing actions are performed by ReportSmith:
1. The Report Group is created and named accordingly.
2. The Suppress attribute is automatically added to the group properties for the selected
column.
3. The selected column is automatically included in the report’s sort order.
4. A header or footer is created for the Report Group.
FIGURE 21.9.
Report Groupings can be
created quickly using the
toolbar buttons.
NOTE
Returning to the report layout, you now have the Report Group as shown Figure 21.10. It
would be nice to have each customer name appear above the list of invoices. To do this you
need to create a new report section. In this case you will create a group header for the
CSTMR_NAME_Group. Select the Insert | Header/Footer menu item. From the Header/
Footer dialog box, choose CSTMR_Group and check the header checkbox to create a group
header. The section CSTMR_NAME_Header now appears within the report above each de-
tail section for the customer.
NOTE
Boundaries such as page headers, footers, and detail sections are always displayed at
specific locations in a report. Feel free to resize, move horizontally, and manipulate
boundaries with the Borders and Format Section dialog boxes, but do not move them
from their specific vertical location using the mouse. Problems occur when boundaries
are moved out of the normal expected sequence. For example, do not move a group
header to be displayed after its detail section!
FIGURE 21.10.
Creating the
CSTMR_NAME_Group
has split the detail section
into sections, one for each
Customer.
You can now drag and drop the CSTMR_NAME field from the detail section into the
CSTMR_NAME_Header section. To do this, ensure that you are in Form Mode and not
Column Mode. This is indicated by the buttons on the toolbar (see Figure 21.11).
FIGURE 21.11.
Use the toolbar to toggle
between Form Mode and
Column Mode.
Form Mode
Column Mode
Use Column Mode when you are working on the detail section of a columnar report. It lets
you move columns horizontally across the page. Column labels move along with the column.
In Column Mode, ReportSmith automatically left-justifies columns with their headings, keeping
them evenly aligned so you don’t have to adjust alignment. When you move a column,
ReportSmith displays a heavy line indicating where the column would drop if you released the
mouse.
Form Mode enables you to move selected objects freely around the report page, horizontally
and vertically. This is useful when you’re positioning fields and labels on form reports or plac-
ing values into headers and footers. Form Mode does not reorder column positions. If you delete
a column in Form Mode, columns do not reposition to fill in the blank space.
After the CSTMR_NAME field has been moved, return to Column Mode and delete the
CSTMR_NAME column by deleting the column heading.
The section CSTMR_NAME_Group can have properties set using the Format | Sections menu
item. This displays the Format Section dialog box, shown in Figure 21.12. Select the Keep
Section Together checkbox for the detail section to prevent this section (the invoices for a cus-
tomer) from being split over a page break.
FIGURE 21.12.
The Format Section dialog
box can be used to control
how sections are displayed.
Now that the basic report layout is complete, it is time to add the rental manager’s requests to
this report.
dialog box as shown in Figure 21.13. Create a new variable, name it Customer_name, set its type
to String, its title to Customer, and the prompt to “Please enter the Customer (Use % for All
Customers.” After you have done this, choose Add to create the report variable.
Report variables are key to allowing your created report to interface with the user of the report.
By using report variables, your report can become dynamic, responding specifically to a user’s
needs. When a report variable is created, ReportSmith automatically creates an individual dia-
log box that is used specifically to assign a value for that report variable. The dialog box is pre-
sented to the user of the report each time a report variable value is needed. You can use the
various options from the Report Variables page of the Report Query dialog box to change the
way the variable is entered. In this example, choosing the value from a table would be more
appropriate. You are going to use a Delphi screen to set the values of the report variables you
create, so there is no need to worry about the presentation of the Report Variable dialog box,
because the user never needs to see this.
FIGURE 21.13.
A report variable is created
to allow a specific customer
to be entered.
Choose Done and enter a valid customer for the value of the report variable, for example, Another
Video Store. What happened? Nothing. You have created a report variable but have not yet
used it. Select the Tools | Selections menu item to display the Selections page of the Report
Query dialog box.
The Selections page of the Report Query dialog box is used to manipulate the where clause of
the report’s SQL statement. Selection criteria can be added in two ways:
■ English-like manner. Using this option enables you to add selection criteria in
English. You do not need to have knowledge of SQL to use this. Using this method
you are assured that ReportSmith will generate correct SQL.
■ Using SQL language. This option displays the SQL Selection Criteria dialog box (see
Figure 21.14). From here you can build the SQL where clause either by typing or by
dragging and dropping fields, report variables, and operators from the list boxes. If
you decide to use this method, you have taken on the responsibility of writing SQL
that is syntactically correct. Within this dialog box you can enter anything for the
selection criteria. This gives you great flexibility, allowing any SQL statement that the
SQL server supports to be written. For example, a subquery could be written. Remem-
ber that ReportSmith returns a database error if SQL criteria is entered incorrectly.
FIGURE 21.14.
The SQL Section Criteria
dialog box can be used to
build an SQL where
condition.
NOTE
The SQL Selection Criteria dialog box is dynamic according to the server. This means
that only the specific SQL functions that your server supports are shown to you. To
advance your report writing prowess, you should become familiar with the specific
functions that your server offers.
For this example, add selection criteria in an English-like manner. Choose Tools | Selections,
choose the 1 button, then select Add selection criteria (see Figure 21.15). A default selection
string is produced. Modify this by clicking each underlined item to be equal to this:
data field CUSTOMERxDBF.CSTMR_NAME is patterned like report variable customer_name.
When you examine the SQL statement, you will notice that the SQL now has an extra line (see
Figure 21.16). This is the where clause that you added via the Selections page of the Report
Query dialog box.
FIGURE 21.15.
Adding selection criteria.
FIGURE 21.16.
Adding selection criteria
simply adds aWHERE clause
to the SQL statement.
You have now completed Request 1. This report reports on only the specified customer. If % is
entered for the Customer_name report variable, the report returns all customers.
FIGURE 21.17.
Toolbar buttons are a
quick way to create
summaries.
NOTE
Using the toolbar buttons is a quick way to create summaries. Such summaries are
automatically created for each group in the report and are inserted into the group
footer. You can also create summaries by using the Tools | Summary Fields dialog box.
type to Number. This is performed the same way as the report variable Customer_name, which
was created earlier. This request requires an extra field for each invoice that shows the discounted
total owed. This must be calculated and cannot be retrieved directly from the database.
ReportSmith calls such fields derived fields. Choose Tools | Derived fields to display the De-
rived Fields page of the Report Query dialog box, enter the name of the derived field as
discount_amt , and select the Add button (see Figure 21.18).
FIGURE 21.18.
Creating a derived field.
The Edit Derived Fields dialog box is now displayed. This dialog box is exactly the same as the
SQL Selection Criteria dialog box in Figure 21.14. In this case you need to calculate the dis-
count for the data field total. With the help of the three list boxes, enter the following calcula-
tion for the derived field:
‘INVOICESxDBF’.’TOTAL’ - ( ( ‘INVOICESxDBF’.’TOTAL’ / 100 ) *
➥<<discount>> )
When calculation for the derived field has been entered, choose OK and Done. The derived
field is added in the report layout as the rightmost column of the detail section for each in-
voice. A derived field created in this way simply adds to the SELECT section of the SQL state-
ment (see Figure 21.19). Such a derived field can be manipulated in the same way as a normal
data field. Create a summary for this derived field and align this summary with the other sum-
maries in the CSTMR_GROUP_footer.
FIGURE 21.19.
A derived field defined via
SQL simply adds to the
SQL SELECT statement.
NOTE
As shown in Figure 21.19, a derived field can be defined via SQL as in this case, or
defined via a ReportBasic macro. A derived field defined via a ReportBasic macro has
its value calculated using a ReportBasic script. ReportBasic is a macro scripting
language that uses the same syntax as Basic. Whenever possible, always use derived
fields defined by SQL. It is always more efficient because the value for the derived field
is calculated by the server. A ReportBasic macro is not run by ReportSmith until the
answer set is returned by the server. ReportBasic can be used to perform complex
operations that SQL cannot do, such as if..then..else logic. ReportBasic is another
subject in itself, and there is no time here to examine this subject in detail; however,
please refer to the ReportSmith manuals for more information.
FIGURE 21.20.
The Macro Commands
dialog box is used to
maintain ReportBasic
macros.
Enter Highlight as the macro name and choose New. The Edit Macro dialog box is displayed
and now you can create the ReportBasic code for the macro. The code for this is shown in
Listing 21.1. You should enter this code directly or choose Load to insert the example named
highlite.mac from the CD.
‘
‘Uses the RGB function in order to set the variables Red and
‘Green equal to the required values for these colors.
‘
‘
‘ Get the value of the total field and store this in the local variable invtotal
‘
invtotal = val(Field( “TOTAL”))
‘
‘ Get the value of the report variable and store this in the local variable
‘ ➥breakpoint
‘
‘
‘ Set the local variable color to Red if the Total for the invoice is over the
‘ ➥breakpoint
‘
‘ Apply the color to the field.
‘ This will be applied to whichever field is linked to the macro
‘
FieldFont “”,0,-1,Color,-1
End Sub
This macro first uses the RGB function to set the variables Red and Green equal to the required
values for these colors. The function Field is called to return the value of the field total into the
variable invtotal. The function GetRepVar is used to return the value of the report variable
highlight into the variable breakpoint.
The two variables invtotal and breakpoint are then compared. If the variable invtotal is greater
than or equal to the variable breakpoint, the variable color is set equal to the variable Red.
Otherwise, color is set equal to the variable Green. The FieldFont function is called to set the
field’s color to the color as stored in the variable color. Although on the surface this macro
might look complex to write in terms of syntax, the Edit Macro dialog box enables functions
to be dragged and dropped into the code. For example, if a report variable is dropped into the
macro from the first list box, the code GetRepVar(“highlight”) is automatically written.
Notice that the FieldFont function does not have the field explicitly stated. Choose Done to
return to ReportSmith. What happens? Nothing! ReportSmith is unsure when to run the new
macro. You must link this to a ReportSmith event. Choose the Tools | Macro menu item to
again display the Macro Commands dialog box. Select the highlight macro and choose Links.
Change the Object combo box to read DataField and the Event combo box to read display.
Now select the field total and choose Link. The macro highlight is now linked to the display
event of the data field total. The Macro Links dialog box should look like Figure 21.21 when
you are finished.
Choose OK. The macro now runs, and the total field color changes. This is why no field was
specified when the FieldFont function in the macro was called. This function acts on which-
ever field the macro is linked to. This means that this macro could be linked to many fields.
Try linking this to the CSTMR_NAME field and see what happens.
Now that the report is complete, save it and close ReportSmith. You can go to Delphi and
build a front-end screen to control this report. Why do this? You could give the report to the
users as is, and the users could use the RunTime viewer that comes with ReportSmith to open
the report. The report would then prompt them for the three report variables: Customer_name,
discount, and highlight. Each of these would prompt the user for a value in a separate dialog
box over which you have little control. Using Delphi to create a front-end screen for the report
means that you have far more control over validating what the user enters, as well as a much
improved interface.
FIGURE 21.21.
Linking thehighlight
macro to thedisplay
event of the data field total.
TReport
The TReport component encapsulates all the operations needed to control ReportSmith from
Delphi. Start up Delphi and drop down a TReport component. First, step through the proper-
ties you need to set to control the previously built report:
■ AutoUnload .
Leave this set to False. AutoUnload automatically unloads ReportSmith
RunTime once the report has finished. This is usually used when a report requires
printing only. ReportSmith is loaded while the report is printed; when the report is
finished, ReportSmith RunTime is closed.
■ Start Page and End Page. These control which pages are printed. Leave these as
default.
■ InitialValues . InitialValues is used to populate the values for report variables the
report uses when it first opens. You use this property to set the value for each report
variable: Customer_name, discount, and Highlight. Once set, when the report opens
the normal report variable dialog boxes do not appear because the report variables
already have values. InitialValues is a TString, just like the Items property of a
TListBox. This property is set by the code for the Start button (see Figure 21.22).
■ MaxRecords. MaxRecords can be used to control how many records the report should
return. For those who are familiar with ReportSmith, this is the same as running a
report in draft mode. This can be useful during development to limit the number of
records a report returns in order to speed the development process along.
■ Preview. Preview is a Boolean property. Set this to True. This decides whether the
report should be shown in the RunTime viewer or printed immediately. Setting
Preview to False automatically prints the report.
■ ReportDir and ReportName. These properties control which report the TReport compo-
nent relates to. Set this to the Directory and ReportName of the report created in
Example 1.
Now that you have set some initial properties of TReport, you can build the rest of the form. I
am assuming that through reading this book you are already quite adept at building simple
forms. Drop down three edit boxes, three labels (one for each report variable), and two but-
tons. Set the caption for the first button to Start and the caption for the second to Recalc. When
you have completed this, the form should look like Figure 21.22.
FIGURE 21.22.
Form to control the
Example 1 report.
The three edit boxes are used to get the value for each report variable. The code is shown in
Listing 21.2. The Start button needs to set the value of the report variables and then open the
report.
continues
This code first sets the initialvalues property for each report variable and then opens the report
using the run method. This sets the value for each report variable when the report is opened.
By setting initialvalues you are preventing the report from prompting for the values of the
report variables. The initialvalues property can set the values of many variables, one on each
line. The following syntax must be used for initial values:
@Reportvariablename = <reportvariablevalue>
The Recalc button is used to reset the report variables and recalculate the report using the new
report variable values while the report is still open. The code is shown in Listing 21.3.
The Recalc button sets the value of each report variable, but this cannot be done using the
initialvalues property because the report is already open. Instead it uses the setvariable
method to place a new value into each report variable. The recalcreport method is then used.
This causes the SQL statement for the report to be rerun so that the new report variable values
are used.
The form is now complete. Run the form, enter three values for each report variable, and choose
Start. The report opens and the three values you entered are used within the report. Enter three
more values and choose Recalc. The report reruns the SQL and uses the three new values you
have entered. You have now completed the first report.
Before moving on, let’s examine some other methods of TReport that are useful when building
a report interface:
■ CloseReport . This method can be used to close down a report. This closes the current
report that is open but does not close down ReportSmith RunTime.
RunMacro is not limited to running a ReportBasic macro—it can be used to call other
specific ReportBasic commands. The following example uses the RunMacro method to
call the ReportBasic procedure msgbox to display a ReportSmith message box:
report1.RunMacro(‘msgbox “Hello from ReportBasic:”’);
The first report is now complete and has a working front-end Delphi interface.
(You can either cut and paste the SQL from the Sample 1 report or use the query builder.)
SELECT
CUSTOMER.”CSTMR_ID” ,
CUSTOMER.”CSTMR_NAME” ,
INVOICES.”INVOICE_ID” ,
INVOICES.”DATE” ,
INVOICES.”SUBTOTAL” ,
INVOICES.”FREIGHT”,
INVOICES.”TOTAL”
FROM
“CUSTOMER.DBF” CUSTOMER ,
“INVOICES.DBF” INVOICES
WHERE
( CUSTOMER.CSTMR_ID = INVOICES.CSTMR_ID )
ORDER BY
CUSTOMER.CSTMR_NAME
The initial query the report will use is complete. You must now modify the query to incorpo-
rate the rental manager’s requests.
This creates a bind parameter for the query. The bind parameter is used to control which cus-
tomers are returned by the query. Edit the params property of the query, set the parameter type
to String, and enter a value of %. Now that the Customer_name parameter has been created, you
must give the user control over it. To ensure that the SQL is correct, open and close the query
by toggling the active property in the Object Inspector.
Drop down a TComboBox and set the text to All Customers . This combo box enables the
user to select the required customer. Drop down a TLabel for the combo box and set the caption
to Customer. Drop down a TTable and set the database name to rsdemo, the table name to
CUSTOMER.DBF, and the index name to the CSTMR_NAME field. Setting this index
ensures that the customers are ordered by name. This table is used to populate the dropdown
list for the combo box. Enter the code shown in Listing 21.4 for the FormCreate event of the
form.
Listing 21.4. Code to populate the Items property of the combo box.
procedure TForm1.FormCreate(Sender: TObject);
begin
table1.open;
table1.first;
ComboBox1.items.clear;
ComboBox1.items.Add(‘All Customers’);
while not Table1.eof do
begin
ComboBox1.items.add(Table1.FieldbyName(‘CSTMR_NAME’).asstring);
Table1.next;
end;
end;
When the form runs, this code populates the combo box with a list of all customers plus the
first option, which is to choose All Customers.
Next you need to link the combo box into the query. To do this, add the code shown in List-
ing 21.5 to the OnChange event of the combo box. Now, when the user selects a customer from
the dropdown list, the OnChange event fires and the query returns only the records for the se-
lected customer.
You have now completed Request 1. When the user selects a customer from the combo box,
the query reflects that selection. Skipping Request 2 for now, let’s move on to Request 3.
This line calculates the discounted total using a default discount amount of 10 percent. Notice
that you are using a column alias Discount, which means the column will be named Discount.
Next add the code shown in Listing 21.6 to the OnChange event of the combo box.
NOTE
Using a column alias has two advantages. First, the column has a meaningful name.
Second, you can change the calculation in the SQL without having any impact on the
column name, because the column will be called Discount regardless of the calculation.
Listing 21.6. Code to amend the query according to the selected discount.
procedure TForm1.ComboBox2Change(Sender: TObject);
begin
with Query1 do
begin
sql.delete(8);
sql.insert(8,(‘ INVOICES.”TOTAL” - ( ( INVOICES.”TOTAL” / 100 ) * ‘ +
➥ComboBox2.text + ‘) DISCOUNT,’));
end;
end;
This code assumes that the SQL select line that calculates the discount is the ninth line in your
SQL statement (remember that Delphi is zero-based, hence in code you reference this as line
8). Of course, in a real-world example you would not hard-code the line number, but I hope
you will allow me a little leeway here. When the user selects a discount amount from the combo
box, the OnChange event fires and the discount selected is reflected in the SQL for the query.
Request 3 is now complete.
“CUSTOMER.DBF” CUSTOMER ,
“INVOICES.DBF” INVOICES
WHERE
( CUSTOMER.CSTMR_ID = INVOICES.CSTMR_ID )
AND (CUSTOMER.CSTMR_NAME LIKE :CUSTOMER_NAME)
ORDER BY
CUSTOMER.CSTMR_NAME
Again you must add the code and components to enable the user to manipulate this line of
SQL. Drop down a further TCombobox and TLabel. Set the caption to HIGHLIGHT and the items
for the combo box to go from 50 to 300 in increments of 50. Set the text to 100. Add the code
shown in Listing 21.7 to the OnChange event of the combo box.
Listing 21.7. Code to amend the query according to the selected highlight
amount.
procedure TForm1.ComboBox3Change(Sender: TObject);
begin
with Query1 do
begin
sql.delete(9);
sql.insert(9,(ComboBox3.text +’ HIGHLIGHT’));
end;
end;
Now when the user selects a value from the Highlight combo box the OnChange event fires and
the SQL is changed to include the selected highlight amount. Notice that here again you are
using a column alias Highlight, which automatically names the field. The Delphi portion of
Request 4 is now complete.
FIGURE 21.23.
ReportSmith 3.0 can use
Delphi TTable and
TQuery to build a report.
NOTE
For the server connect to work correctly, the Delphi application must have the TReport
component present. Without this, the server connection will fail.
Follow the same steps as with the Sample 1 report to change the layout. Create the Report
Group CSTMR_NAME_Group using the Define Groupings dialog box. Add a header and
footer to this group using the Header/Footer dialog box. Move the CSTMR_NAME field into
the CSTMR_NAME_Group header. Create summaries for the TOTAL, SUBTOTAL,
FREIGHT, and DISCOUNT fields. Add these summaries to the CSTMR_NAME_Group
footer. Select the Highlight field and click Del to remove it. This field is used by the macro
highlight, but you do not need to display it in the report layout.
NOTE
You will notice that if you try to use the toolbar buttons to create the Report Groups, a
warning is displayed stating that this operation is not allowed for the connection type.
This is because the toolbar button is trying to amend the sort order for the SQL. You
must create the report grouping by choosing the menu item Tools | Report Groups.
Now that the report layout is complete, you can finish the ReportSmith section of Request 4.
to read
breakpoint = val(field (“HIGHLIGHT”))
FIGURE 21.24.
Completed Delphi Form for
the Example 2 report.
NOTE
You will notice that you also changed the case when returning the field highlight.
ReportBasic is case-sensitive when it is returning values from fields and report vari-
ables. When you created the report variable, it was named highlight in lowercase;
therefore, you referenced it as such in the function GetRepVar. When you created the
field HIGHLIGHT, you named it using an alias in uppercase; therefore, it must be refer-
enced as such using the Field function.
As with Example 1, link this macro to the display event for the data field TOTAL. The invoice
total now changes color according to the field HIGHLIGHT. Request 4 is now complete.
Save the report and close down ReportSmith. Return to Delphi and set the ReportDir and
ReportName to the report you created. Drop down two buttons and set their captions to Start
and Recalc, respectively. The Start button is again used to start up ReportSmith RunTime and
open the first report. Add the following code to the OnClick event of the Start button:
procedure TForm1.Button1Click(Sender: TObject);
begin
report1.run;
end;
Remember that the Start button does not need to set the initial values because you are control-
ling the report from the Delphi query. It is also not necessary to open the query. ReportSmith
opens the query itself. Next, add the code for the OnClick event of the Recalc button:
procedure TForm1.Button2Click(Sender: TObject);
begin
report1.recalcreport;
end;
Again, you do not need to use any methods other than the recalcreport. All control is per-
formed through the query, which you are changing according to the user’s selections.
The form is now complete. Run the form. Choose a customer, a discount amount, and a high-
light breakpoint. Choose Run. ReportSmith RunTime starts and opens the report. The report
is using the SQL from the Delphi query that was set according to the options you selected.
Next choose different values and select the Recalc button. The report reruns the SQL, which
now uses the new values you selected.
Example Report 3
Sample Report 3 actually creates a ReportSmith report dynamically using Delphi. This example
automatically creates a report that is based on the Customer table. Two named connections
must be set up within ReportSmith for this example to work. To add these, start up ReportSmith
and choose the File | Connections menu item to display the Connection dialog box. In this
dialog box, choose New, enter Delphi as the name, select Delphi for the type, and choose Save.
You have now created a named connection that you will use to connect to Delphi from
ReportSmith. Create a second connection, name it Video, choose DBASE (IDAPI) as the type,
and enter RptSmith\video as the path. This named connection is used to connect to the dBASE
tables in the RptSmith\video directory.
NOTE
If you are having problems creating these named connections, copy the named connec-
tion file rptsmith.con from the CD to your Windows directory.
To look at this example, an additional component must first be installed. This new compo-
nent is called a TReportSmith. Install this component by starting up Delphi and choosing the
Component | Install menu item to display the Install Components dialog box. From this dia-
log box, choose Add and select the ReportSmith.dcu file from the CD, then choose OK to
rebuild the library. TReportSmith should now appear on the Samples page of the component
palette.
This new component is a TReportSmith, which is essentially the same as a TReport except that
it has an additional property called RunTime, as shown in Figure 21.25.
FIGURE 21.25.
The TReportSmith
component has an
additional property called
RunTime.
Open up the project dynamicreport from the CD. This project uses a TReportSmith. Examine
the TReportSmith component using the Object Inspector. This shows the additional property
RunTime. This is a Boolean property. Setting this to True forces TReportSmith to always load
ReportSmith RunTime; setting this to False forces TReportSmith to load the full-blown
ReportSmith designer. In this project, RunTime is set to False. This is because this project dy-
namically creates reports and therefore you must access some of the functionality of ReportSmith
that is not available in ReportSmith RunTime.
As shown in Figure 21.26, this project has two options: Standalone and Delphi Dependent.
These two options enable two types of report to be created. When created, the Standalone re-
port has its own SQL statement and therefore can be opened later independently of Delphi.
(This is the same type of report as the Sample 1 report). The Delphi Dependent report does
not have its own SQL query but uses a Delphi TQuery as its datasource. (This is the same as the
Example 2 report.) The code that generates the reports is behind SpeedButton1 (see Listing 21.8).
This is split into two blocks. The first is used to generate the Standalone report and the second
the Delphi Dependent report.
FIGURE 21.26.
The dynamicreport project
creates a customer report
on-the-fly.
Let’s examine the first block of code, shown in Listing 21.9. This code generates the Standalone
report.
This code uses the TReportSmith method RunMacro to pass a series of ReportBasic commands
to ReportSmith via DDE. Multiple ReportBasic commands can be passed by separating each
one using ||. The first ReportBasic command sent is
dim ds as dataset
This declares ds to be of type dataset. A dataset is what ReportSmith calls a control object. A
dataset has various properties and methods that can be manipulated to define a new report or
modify an existing one. The next line establishes a connection between the dataset and the
datasource. This line therefore connects the dataset to the named connection video:
ds.connect 0,”video”,” “,” “,” “
Once the connection is established, the method setusersql is called. This adds the SQL state-
ment to the dataset:
ds.setusersql “select * from ‘+ chr(39)+’C:\PROGRAM
FILES\BORLAND\RPTSMITH\DEMOS\CUSTOMER.DBF’+chr(39)+’ CUSTOMERxDBF”
Now that you have established a connection and set the SQL for the dataset, a new report can
be created by calling the CreateReport method for the dataset:
ds.CreateReport
This line creates a new ReportSmith report using the dataset as its definition. Once created,
the new report is saved to the filename specified in edit1.text using the ReportBasic command
savereport .
The second block of code, used for the Delphi Dependent report, uses the same techniques.
See Listing 21.10.
Again a dataset is used to create the report, but the connect method is different. In this ex-
ample you need to connect the dataset to Delphi and not to the dBASE tables directly. There-
fore, you connect to the named connection called delphi, not the named connection video:
ds.connect 0,”delphi”,” “,” “,” “
The other change needed is setting the SQL for the dataset. Instead of calling the setusersql
method, you call another dataset method, AddTable. This adds the Delphi TQuery, Query1, to
the dataset:
ds.AddTable “Query1”,” “
Again the report can then be created and saved using the CreateReport and SaveReport
methods.
NOTE
The more observant reader will notice there is a TReport component present on the
form. This component was added solely to enable the join from ReportSmith to
Delphi to work correctly for the Delphi Dependent report. The TReportSmith compo-
nent does not perform this functionality.
This has been a simple example. The TReportSmith component was introduced only to enable
the ReportSmith designer to be loaded instead of the RunTime. This example always creates a
report that uses the Customer table, but in showing this I was hoping to pass on further insight
into ReportBasic and also show some of the power that lies within ReportSmith.
Summary
I hope that through these examples I have demonstrated how easy it is to control ReportSmith
reports from within Delphi. You are now able to control Standalone reports using report vari-
ables and also build reports that use Delphi as their query source. Using these two techniques,
you should be able to easily integrate your reports within your Delphi application. Through
the last example I hope you now understand some ReportBasic techniques and have a further
appreciation for some of the power of ReportSmith.
V
IN THIS PART
Objects
The fifth section of the book plunges into the heart of object-oriented
programming. There are chapters on inheritance, encapsulation, and poly-
morphism, as well as a description of how to create both visual and
non-visual components. These pages include some of the most important
material in the book, all of which should be well understood by any seri-
ous Delphi programmer.
Handling Messages
22
Event-oriented code is one of the central tenets of Windows programming. Some rapid-
application development environments attempt to hide users from this feature altogether, as if
it were something so complicated that most programmers couldn’t understand it. The truth is
that event-oriented programming is not, in itself, particularly complex. However, some fea-
tures of the way it is implemented in Windows can be confusing under certain circumstances.
Delphi gives you full access to the event-oriented substructure that provides Windows with a
high degree of power and flexibility. At the same time, it simplifies and clarifies the way a
programmer handles those events. The end result is a system that gives you complete access to
the power of Windows, while simultaneously protecting you from unnecessary complexity.
This chapter covers the following topics:
■ Event-oriented programming basics
■ Responding to mouse events or key events
■ Accessing the information passed in events
■ The basics of sets, which are used frequently in Delphi event handlers
■ Circumventing Delphi’s message handling tools and directly capturing messages
■ Creating wm_Command handlers and finding the IDs of the components used in a
program
Some of the ideas in this chapter were introduced in chapters 9–14, which are on the Win-
dows API. However, this chapter looks at these ideas from the perspective of a Delphi RAD
programmer, rather than the perspective of a Windows API programmer. To be really good at
programming Windows, you need to be able to look at these subjects from both perspectives.
That is, you need to know the VCL inside out, and you need to know the Windows API inside
out. This chapter concentrates on the VCL side of that equation. Put both perspectives to-
gether and you can go on to create your own VCL components, or you can design elegant,
well-structured Windows programs.
Before starting, let me reiterate that Delphi hides much of the complexity of Windows pro-
gramming. However, the developers did not want to prevent programmers from accessing any
portion of the Windows API. By the time you finish reading this chapter, you should be able
to see that Delphi gives you access to the full range of power provided by an event-oriented
system.
Delphi Events
Delphi makes it easy to handle keyboard and mouse events. Suppose, for instance, that you
wanted to capture left mouse clicks in the main form of your program. Here’s how to get started.
Create a new project and name it EVENTS1.DPR. Go to the area in the code just underneath
the word implementation and write the following uses statement, shown here with some sur-
rounding text so you can see where to write it:
implementation
uses
Dialogs;
{$R *.DFM}
In the Object Inspector for the main form, choose the Events Page and double-click the area to
the right of the OnClick property. Create the following procedure:
procedure TForm1.FormClick(Sender: TObject);
begin
MessageDlg(‘Hello’, mtInformation, [mbOk], 0);
end;
This code tells Windows that a dialog box should appear every time the user clicks the left
mouse button in the form. The dialog box is shown in Figure 22.1.
FIGURE 22.1.
The dialog box displayed by
the EVENTS1 program
when you click the left
mouse button inside the
main form.
The previous code presents one of the simplest possible cases of responding to an event in a
Delphi program. It is so simple, in fact, that many programmers write this kind of code with-
out ever understanding that they are writing event-oriented code. In this case, Delphi program-
mers get the event secondhand, because VCL massages the event before passing it on to the
main form. Nonetheless, this is real event-oriented programming, albeit in a very simplified
manifestation.
As you saw back in the section on the Windows API, the operating environment notifies you
not only of the event, but of several related bits of information. For instance, when a mouse
down event is generated, a program is informed about where the event occurred and which
button generated the event. If you want to get access to this kind of relatively detailed informa-
tion, you should turn to the Events Page for the form and create an OnMouseDown handler:
procedure TForm1.FormMouseDown(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState;
X, Y: Integer);
begin
if ssRight in Shift then
Canvas.TextOut(X, Y, ‘* Button’);
end;
This method writes text to the form every time the user clicks the right mouse button. To test
this method, run the program and click the right mouse button in the form in several different
locations. You’ll see that each spot you click is marked, as shown in Figure 22.2.
FIGURE 22.2.
When you click the mouse
button in the form of the
EVENTS1 program, the
location of the event is
recorded.
The Canvas.TextOut procedure prints text to the screen at the location specified by the vari-
ables X and Y. Both of these variables are supplied to you by Delphi, which received them in
turn from the operating system. The X variable tells you the column in which the mouse was
clicked, and the Y variable tells you the row.
As you can see, Delphi makes it very simple for you to respond to events. Furthermore, not
only mouse events are easy to respond to. You can respond to key presses in a similar manner.
For instance, if you create a method for the OnKeyDown property on the Events Page for the
form, you can show the user which key was pressed on the keyboard whenever the EVENTS1
program has the focus:
procedure TForm1.FormKeyDown(Sender: TObject;
var Key: Word;
Shift: TShiftState);
begin
MessageDlg(Chr(Key), mtInformation, [mbOk], 0);
end;
In the preceding code, the Chr function is used to translate a numerical value into an ASCII
value. In other words, Delphi and the operating system both pass you not an actual letter like
A, B, or C, but the number associated with the key you pressed. On PCs, the letter A is asso-
ciated with the number 65. The Chr function translates the number 65 into the letter A. It
wouldn’t be appropriate for Delphi to perform this translation for you automatically because
some keys, such as the F1 or Enter key, have no letter associated with them. Later in this chap-
ter, you learn how to use OnKeyDown events to respond sensibly to keypresses on special keys
such as F1, Shift, or Caps Lock.
Besides OnKeyDown events, Delphi also lets you respond to keyboard activity via the OnKeyPress
event:
procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
MessageDlg(Key, mtInformation, [mbOk], 0);
end;
You can see that this event is very similar to an OnKeyDown event. The difference is that the Key
variable passed to OnKeyPress events is already translated into a Char. However, OnKeyPress
events work only for the alphanumeric keys and are not called when special keys are pressed. In
short, the OnKeyPress event is the same as a wm_Char event.
The code for the EVENTS1 program is shown in Listing 22.1. Get the program up and run-
ning and take whatever time is necessary to be sure it all makes sense to you. There is no point
in trying to be a Windows programmer if you don’t understand events.
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls;
type
TEventsOne = class(TForm)
procedure FormClick(Sender: TObject);
procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
{ Private declarations }
public
{ Public declarations }
end;
var
EventsOne: TEventsOne;
implementation
uses
Dialogs;
{$R *.DFM}
continues
end.
After this introduction to event-oriented programming, it’s time to step back and see some of
the theory behind the code. After explaining something of how the system works, this chapter
goes on to give examples of how to take full advantage of Windows event-oriented code base.
NOTE
Some astute readers may notice that the code in this program, and in other programs
in this book, still includes WINTYPES.PAS and WINPROCS.PAS in the Uses clause.
These units are part of the 16-bit version of Delphi but not the 32-bit version. How-
ever, code that references them still compiles in the 32-bit version due to the Unit
Alias option, which you can reach by choosing Project | Options | Directories/
Conditionals. The default line automatically entered by Delphi into the Unit Alias
directive reads as follows:
WinTypes=Windows;WinProcs=Windows
The directive means that the compiler should internally change the identifiers WinTypes
or WinProcs into the identifier Windows during compilation.
Going through all the programs in this book to make sure that all the references to
WinTypes and WinProcs were changed to Windows obviously would not have been
much work for me. I decided against doing so on the grounds that you may one day
have some reason for wanting to compile this code under 16-bit Windows. Why
deliberately make the code incompatible with 16-bit Windows doing so presents no
advantage? Making this change would be especially strange in a time when the vast
majority of professional programmers are forced to straddle a murky line between
16- and 32-bit Windows.
Only one or two programs in this book were deliberately designed to run in both
16-bit Windows and 32-bit Windows. Many of the programs were designed to take
explicit advantage of 32-bit features and therefore will not compile under 16-bit
Windows. However, the developers of Delphi went to great lengths to ensure that a
tremendous amount of compatibility exists between the 16-bit and 32-bit versions of
their compiler. This compatibility is one of the strengths of Delphi, and I saw no
reason to obscure that compatibility deliberately to make this book appear artificially
fancy or up to date. For instance, you will find very little difference between the way
Delphi handles messages in the 16- and 32-bit versions. Therefore, all or most of the
code in this chapter will compile under both versions of Delphi. This is a strength, not
a weakness, and it is completely in accord with the carefully crafted plans of the Delphi
developers.
If you are curious, the reason the units changed names in this version is that 32-bit
Windows no longer enforces 64KB limitations on the internal tables that Delphi
builds when it compiles a unit. Sans a 64KB limitation, the developers of Delphi could
combine the WinTypes and WinProcs units into one large unit called WINDOWS.PAS.
Understanding Events
Event-oriented programming isn’t unique to Windows, nor is it a chore that can be handled
only by an operating system. For instance, any DOS program could be based around a simple
loop that keeps running the entire time the program is in memory. Here is a hypothetical ex-
ample of how such code might look:
repeat
CheckForMouseEvent(Events);
CheckForKeyPress(Events)
HandleEvents(Events);
until Events.Done := True;
This code represents a typical event-oriented loop. A simple repeat..until statement checks
for keyboard and mouse events and then calls HandleEvents to give the program a chance to
respond to the events that are generated by the user or the operating system.
The variable called Events might be a record with a fairly simple structure:
TEvent = record
X, Y: Integer;
MouseButton: TButton;
Key: Word;
Done: Boolean;
end;
X and Y give the current location of the cursor, and Key contains the value of the top event in
the key buffer. The TButton type might have a declaration that looks like this:
TButton = (lButton, rButton);
These structures permit you to track where the mouse is, what states its buttons are in, and
what keys the user has pressed. Admittedly, this is a very simple type of event structure, but the
principles involved mirror what is going on inside Windows, or inside other event-oriented
systems such as Turbo Vision. If the program being written was an editor, the HandleEvent for
the program might look like this:
procedure HandleEvent(Events: TEvent);
begin
case Events.Key of
‘A..z’: Write(Events.X, Events.Y, Events.Key);
EnterKey: Write(CarriageReturn);
EscapeKey: Events.Done := True;
end;
end;
Given the preceding code, the program would go to location X,Y and write the letter most
recently pressed by the user. If the Enter key was pressed, a carriage return would be written to
the screen. A press on the Esc key would cause the program to terminate. All other key presses
would be ignored.
Code like this can be very powerful, particularly if you’re writing a program that requires ani-
mation. For instance, if you need to move a series of bitmaps across the screen, you want to
move the bitmap a few pixels and then check to see whether the user has pressed a button or hit
a keystroke. If an event has occurred, you want to handle it. If nothing occurred, you want to
continue moving the bitmap.
I hope the short code samples shown here give you some feeling for the way event-oriented
systems work. The only piece that’s missing is an understanding of why Windows is event-
oriented.
Microsoft made Windows event-oriented in part because multiple programs run under the
environment at the same time. In multitasking systems, the operating system needs to know
whether the user has clicked in a program, or whether the click was in the desktop window. If
the mouse click occurred in a window that was partially hidden behind another window, it is
up to the operating system to recognize the event and bring that window to the foreground.
Clearly, it wouldn’t be appropriate for the window itself to have to be in charge of that task.
To ask that much would place an impossible burden on the programmer who created the win-
dow. As a result, it’s best for the operating system to handle all the keystrokes and mouse clicks,
and to then pass them on to the various programs in the form of events. Any other system
would force every programmer to handle all the events that occurred when his or her program
had the focus, and to manipulate the entire operating system in response to certain mouse events
or keystrokes, such as Alt+Tab.
In short, Windows programmers almost never directly monitor the hardware or hardware in-
terrupts. Instead, the operating system handles that task. It passes all external events on to in-
dividual programs in the form of messages. In a typical event-oriented system, the operating
system continually polls the hardware in a loop and then sends each event off to its programs
in the form of some kind of event structure or event variables. This is the same kind of activity
you saw in the brief code snippets shown earlier.
You have seen that Windows handles mouse and keyboard events and passes them on to the
appropriate window. The message that is generated in these cases gets sent to the default win-
dow procedure, which, as you know, is called DefWindowProc. DefWindowProc is analogous to
the HandleEvent procedure shown earlier.
The important point to understand is that Windows messages contain the information that
drives the entire operating environment. Almost everything that happens inside Windows is a
message, and if you really want to tap into the power of Delphi, you need to understand how
these messages work.
One of the tricky parts of Windows event-oriented programming is extracting information from
the wParam and lParam variables passed to the window procedure. In most cases, Delphi frees
you from the necessity of performing this task. For instance, if you create an event for the
OnMouseDown property, Delphi directly tells you the X value and Y value where the event oc-
curred. As a programmer, you don’t have to struggle to get the event and its associated values.
As you will see in the next section, everything about the event is shown to you in a simple and
straightforward manner.
This is by far the most convenient way for you to handle events. Delphi can also give direct
access to the values sent to you by the operating system. That is, you can handle wParams and
lParams directly, if you want. After you have studied the EVENTS2 program and learned more
about sets, I’ll show you exactly how to get at that raw data.
Take a moment to consider the Shift parameter shown in the FormMouseDown header. Shift is
declared to be of type TShiftState:
TShiftState = set of (ssShift, ssAlt, ssCtrl,
ssRight, ssLeft, ssMiddle,
ssDouble);
TShiftState is a set, and so far I have not spent much time on Delphi sets. As a result, that
topic is covered in some detail throughout the rest of this section of the chapter.
To find out whether a particular element is a member of the set passed to you by Delphi, you
can perform simple tests using the in operator:
if ssRight in Shift then Label1.Caption := ‘ssShift’;
This code asks whether the element ssRight is in the set passed to you via the Shift variable.
If it is, the code assigns the word ssShift to the Caption of a label.
Here is how you can declare a set at runtime:
Shift := [ssright, ssShift, ssAlt, ssCtrl];
Given this set, the in operator from the if statement shown previously returns True. The fol-
lowing set causes the operator to return False:
Shift := [ssAlt, ssCtrl];
Besides in, there are three other operators you can use with sets:
+ Union
- Difference
* Intersection
All three of these operators return a set, whereas the in operator returns a Boolean value. The
SETSEXP program, shown in Listing 22.2, shows how to work with all four operators.
Listing 22.2. The SETSEXP program shows how to use operators to track the
members of sets such as TShiftState.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, Buttons;
type
TOptType = (otUnion, otIntersection, otDifference);
TUnion = class(TForm)
Panel1: TPanel;
ControlKey: TCheckBox;
ShiftKey: TCheckBox;
LeftButton: TCheckBox;
RightButton: TCheckBox;
Union: TBitBtn;
Intersection: TBitBtn;
Difference: TBitBtn;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Panel2: TPanel;
Label4: TLabel;
procedure PanelMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure SetBtnClick(Sender: TObject);
private
{ Private declarations }
procedure CheckState(Shift: TShiftState);
public
{ Public declarations }
end;
var
Union: TUnion;
implementation
{$R *.DFM}
end.
The SETSEXP program shows how you can read and manipulate sets. In particular, you work
with sets of type TShiftState. When you are finished studying the program, you will have all
the knowledge you need to work with Delphi sets.
The main form for the SETSEXP program consists of four checkboxes, two panels, four labels,
and three bitbtns, as shown in Figure 22.3. The four checkboxes are placed on top of the first
panel, and the fourth label is placed on the top of the second panel.
FIGURE 22.3.
The SETSEXP program’s
main form enables you to
manipulate variables of
type TShiftState.
SETSEXP tells you whether the Shift or Ctrl keys are pressed when the mouse is clicked, and
it tells you whether the user pressed the right or left mouse button. The code also shows how
to use the Intersection, Union, and Difference operators.
The key method in the SETSEXP program looks at a variable of type TShiftState and displays
its contents to the user via the program’s radio buttons:
procedure TUnion.CheckState(Shift: TShiftState);
begin
ShiftKey.Checked := ssShift in Shift;
ControlKey.Checked := ssCtrl in Shift;
LeftButton.Checked := ssLeft in Shift;
RightButton.Checked := ssRight in Shift;
end;
This code takes advantage of the fact that the in operator returns a Boolean variable, and the
Checked property of a radio button is also declared to be of type Boolean. As a result, you can
test to see whether a particular element is part of the Shift set. If it is, you can easily set the
Checked state of a radio button to record the result. For example, in the preceding code, if ssShift
is part of the current set, the ShiftKey radio button is checked.
Two different routines pass variables of type TShiftState to the CheckState method. The first
routine is called whenever the user clicks in the panel at the bottom of the program or the label
that rests on the panel:
procedure TUnion.PanelMouseDown(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState;
X, Y: Integer);
begin
CheckState(Shift);
end;
This code passes the Shift variable on to CheckState, which displays the contents of the vari-
able to the user. For instance, if the Shift key is being held down and the right mouse button
is pressed, PanelMouseDown is called. PanelMouseDown then passes the Shift variable to CheckState,
and CheckState causes the ShiftKey and RightButton controls to be checked. The other two
radio buttons are left unchecked.
There are three bitbtns on the right side of the main form. They are labeled Union, Intersec-
tion, and Difference. Clicking any of these buttons demonstrates one of the non-Boolean set
operators. All three buttons have their OnClick event set to the following procedure:
procedure TUnion.SetBtnClick(Sender: TObject);
const
Operators: array[0..2] of string = (‘+’, ‘*’, ‘-’);
var
FinalSet: TShiftState;
LeftShift: TShiftState;
LeftCtrl: TShiftState;
begin
LeftShift := [ssLeft, ssShift];
LeftCtrl := [ssLeft, ssCtrl];
case TOptType((Sender as TBitBtn).Tag) of
otUnion: FinalSet := LeftShift + LeftCtrl;
otIntersection: FinalSet := LeftShift * LeftCtrl;
otDifference: FinalSet := LeftShift - LeftCtrl;
end;
CheckState(FinalSet);
Label2.Caption := Operators[(Sender as TBitBtn).Tag];
end;
The SetBtnClick method declares three variables of type TShiftState. Two of these variables
are used to declare sets that are used by the rest of the SetBtnClick method:
LeftShift := [ssLeft, ssShift];
LeftCtrl := [ssLeft, ssCtrl];
The first line assigns the LeftShift variable to a set that contains the values ssLeft and ssShift.
The next line assigns the LeftCtrl variable to a set that contains ssLeft and ssCtrl. The rest of
this method enables the user to see the union, intersection, and difference of these two sets.
The case statement in the middle of the SetBtnClick method detects which of the three bitbtns
the user clicked. This is the old time-honored technique featuring the use of an enumerated
type and the assignment of zero-based ordinal values to the Tag field of each button.
If the user clicks the Union button, the FinalSet variable is set to the union of the LeftShift
and LeftCtrl variables:
FinalSet := LeftShift + LeftCtrl;
The difference of the sets is calculated if the user clicks the Difference button:
FinalSet := LeftShift - LeftCtrl;
After the case statement ensures the selection of the proper operator, the FinalSet value is passed
to CheckState and its contents are displayed to the user. For instance, if the user clicks the Union
button, the LeftButton, ShiftKey, and ControlKey radio buttons are all checked. The Inter-
section button causes the LeftKey to be checked, and the Difference button causes the ShiftKey
to be checked. Here is another way of looking at the work accomplished by these operators:
[ssLeft, ssShift] + [ssLeft, ssCtrl] = [ssLeft, ssShift, ssCtrl];
[ssLeft, ssShift] * [ssLeft, ssCtrl] = [ssLeft]
[ssLeft, ssShift] - [ssLeft, ssCtrl] = [ssShift]
To help the user understand exactly what is happening, the current set operation is displayed
at the top of the form. For instance, if the user clicks the Union button, the following expres-
sion is shown to the user:
LeftShift + LeftCtrl
Throughout a run of the program, the words LeftShift and LeftCtrl are displayed to the user
in a pair of TLabels. A third label displays +, -, or *, depending on the current state of the
program:
Label2.Caption := Operators[(Sender as TBitBtn).Tag];
In this code, Operators is an array of three strings that contain the operators that return a set:
const
Operators: array[0..2] of string = (‘+’, ‘*’, ‘-’);
The SETSEXP program gives you enough information that you should be able to work with
the sets that are passed to Delphi event handlers. The code shown here defines the way sets are
usually handled in all Delphi programs. However, you can actually directly manipulate the raw
data that represents a Delphi set. Techniques for performing these manipulations are shown in
the GetShift method, which is part of the program examined in the next section of this
chapter.
Listing 22.3. The EVENTS2 program provides a detailed look at how to track
events.
unit Main;
{ Events Two }
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
StdCtrls, vKeys, Messages,
SysUtils, ExtCtrls;
const
Space = 20;
type
TEvents = class(TForm)
Panel1: TPanel;
Label1: TLabel;
LMouseMove: TLabel;
Label3: TLabel;
LMouseDown: TLabel;
Label5: TLabel;
LKeyDown: TLabel;
Label7: TLabel;
LKeyUp: TLabel;
Label9: TLabel;
LMouseUp: TLabel;
Label11: TLabel;
LWidth: TLabel;
Label13: TLabel;
LHeight: TLabel;
LSpecialMouse: TLabel;
Label16: TLabel;
procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
procedure FormKeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
procedure FormMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure FormResize(Sender: TObject);
procedure FormPaint(Sender: TObject);
private
procedure MyMouseMove(var M: TWMMouse); message wm_MouseMove;
public
{ Public declarations }
end;
continues
implementation
{$R *.DFM}
end;
end.
Listing 22.4 shows the VKeys unit used by the EVENTS2 program.
interface
uses
Classes;
type
T3StrAry = array[0..2] of String;
T5StrAry = array[0..4] of String;
T8StrAry = array[0..7] of String;
const
AMessage: T5StrAry = (‘WM_CHAR’, ‘WM_KEY’, ‘WM_MOUSEMOVE’,
‘WM_MOUSEDOWN’, ‘WM_MOUSEUP’);
Buttons: T3StrAry = (‘mbLeft’, ‘mbRight’, ‘mbCenter’);
Shifty: T8StrAry = (‘ssShift’, ‘ssAlt’, ‘ssCtrl’, ‘ssLeft’,
‘ssRight’, ‘ssMiddle’, ‘ssDouble’, ssUnknown);
continues
EVENTS2 shows how to extract the full content of a message sent to you by Delphi. The main
form for the program (shown in Figure 22.4) provides information on a wide range of mouse
and keyboard generated events.
FIGURE 22.4.
The EVENTS2 program
tracks key Windows events
as they occur.
To use the program, simply compile and run. Click the mouse in random locations and strike
any of the keys on the keyboard. Just rattle away; you won’t do any harm unless you press
Ctrl+Alt+Del. Every time you move the mouse, click the mouse, or strike a key, the exact na-
ture of the event that occurred is shown in the main form of the program. For instance, if you
move the mouse, its current location is shown on the form. If you strike the F1 key while the
Ctrl key is pressed, those keys’ values are displayed on the form.
The FormMouseMove event handler in the EVENTS2 window tracks the current location of the
mouse and the state of its buttons. It does this by responding to OnMouseMove events:
procedure TEvents.FormMouseMove(Sender: TObject;
Shift: TShiftState;
X, Y: Integer);
begin
LMouseMove.Caption := ‘X: ‘ + IntToStr(X) + ‘ Y: ‘ +
IntToStr(Y) + ‘ ‘ + GetShift(Shift);
end;
The method for tracking the X and Y values is fairly intuitive. X stands for the current column,
and Y stands for the current row, with columns and rows measured in pixels. Before these val-
ues can be shown to the user, they need to be translated into strings by the IntToStr function.
Nothing could be simpler than the techniques used to record the current location of the mouse.
The technique for recording the current shift state, however, is a bit more complex. As you saw
earlier, TShiftState is a predeclared structure that looks like this:
TShiftState = set of (ssShift, ssAlt, ssCtrl,
ssRight, ssLeft, ssMiddle,
ssDouble);
The elements of this set track all the possible states of the Shift, Alt, and Ctrl keys, as well as the
mouse buttons.
Delphi records the state of a set in the individual bits of a variable, in which bit 0 corresponds
to the first element in the set, bit 1 corresponds to the second element in the set, bit 2 corre-
sponds to the third element in the set, and so on. This fact enables you to use bitwise operators
to track the state of the shift button:
function GetShift(State: TShiftState): String;
var
B, i: Byte;
begin
Result := ‘‘;
for i := 0 to 7 do begin
B := Byte(1) shl i;
if (B and Byte(State)) > 0 then Result :=
Result + ‘ ‘ + Shifty[i];
end;
end;
More specifically, the code checks to see whether the 0 bit is set, and if it is, ‘ssShift’ is added
to the string returned by the function. If the first bit in the Shift variable is set to 1, the string
‘ssAlt’ is added to the string returned by the function, and so on. As you move through the
bits in the State variable, you get a picture of the current state of the mouse and keyboard. For
instance, if the Shift and Ctrl keys are pressed, as well as the right mouse button, the string
returned by the function looks like this:
ssShift ssCtrl ssRight
Bitwise operators can be tricky at times, but anyone can call the GetShift function. (See the
BINARY.PAS unit, included on the CD in the UNITS subdirectory, for additional routines
that help simplify the process of working on the bit level.)
When you get a wm_KeyDown message, you need to have some way of translating that message
into a meaningful value. To help with this chore, Windows declares a set of virtual key con-
stants that all start with vk. For example, if you press the F1 key, the Key variable passed to an
OnKeyDown event is set to vk_F1, in which the letters vk stand for virtual key. The virtual key
codes are found in the WINDOWS unit and also in the online help if you search under “Vir-
tual Key Codes.”
You can test to see which virtual key has been pressed by writing code that looks like this:
if Key = vk_Cancel then DoSomething;
This code simply tests to see whether a particular key has been pressed. If it has, the code calls
the DoSomething procedure.
To help you understand virtual keys, the GetKey method from the VKEYS unit returns a string
stating exactly what key has been pressed:
function GetKey(K: Word): String;
begin
case K of
vk_LButton: Result := ‘vk_LButton’;
vk_RButton : Result := ‘vk_RButton’;
vk_Cancel : Result := ‘vk_Cancel’;
vk_MButton : Result := ‘vk_MButton’;
vk_Back : Result := ‘vk_Back’;
vk_Tab : Result := ‘vk_Tab’;
vk_Clear : Result := ‘vk_Clear’;
This procedure is really just a giant case statement that checks to see whether the Key variable
is equal to any of the virtual keys. If it is not, the code assumes that it must be one of the stan-
dard keys between A and Z. (See the else clause in the code to see how these standard keys are
handled.)
As explained in the last paragraph, the virtual key codes do not cover normal letters such as A,
B, and C. In other words, there is no value vk_A or vk_B. To test for these letters, just use the
standard ASCII values. In other words, test whether Key is equal to 65, or whether Chr(Key) is
equal to A. The point here is that these letters already have key codes. That is, the key codes for
these letters are the literal values A, B, C, and so on. Because these are perfectly serviceable val-
ues, there is no need to create virtual key codes for the standard letters of the alphabet, or for
numbers.
You probably won’t have much use for the GetKey routine in a standard Delphi program.
However, it is useful when you are trying to understand virtual keys and the OnKeyDown event.
As a result, I have included it in this program.
The declaration shown here tells Delphi that you want to respond directly when the operating
system informs your program that the mouse has moved. In other words, you don’t want the
Delphi VCL to trap the message first and then pass it on to you in an OnMouseMove event. In-
stead, you just want the message sent straight to you by the operating system, as if you were
working with one of the Windows API programs shown earlier in the book. In short, you’re
telling the VCL: “Yes, I know you can make this task very simple and can automate nearly the
entire process by using visual tools. That’s nice of you, but right now I want to get the real
event itself. I have some reason of my own for wanting to get very close to the metal. As a re-
sult, I’m going to grab the message before you ever get a chance to look at it!”
If you want to intercept messages directly, you should use the message keyword, as shown ear-
lier in the MyMouseMove declaration. The proper way to use this syntax is to first write the word
message and then follow it by the constant representing the message you want to trap. When
you use the message directive, you are declaring a form of dynamic method, which means Delphi
uses the dynamic method table to dispatch your message. In particular, Delphi maintains a list
of dynamic methods used in your application, and it locates a particular method’s address by
using the offset implied in the wm_MouseMove constant.
Here’s the code for the MyMouseMove procedure:
procedure TEvents.MyMouseMove(var M: TWMMouse);
begin
Inherited;
LSpecialMouse.Caption := ‘X: ‘ + IntToStr(M.XPos) +
‘ Y: ‘ + IntToStr(M.YPos);
end;
You can see that the code begins by calling the inherited wm_MouseMove handler. If you didn’t
make this call, the program would still run, but the OnMouseMove event would never be sent to
the FormMouseMove procedure. It isn’t an error if you don’t pass the message back to Delphi.
You can either keep the message for yourself or pass it on, as you prefer.
Normally, when you call an inherited method, you need to specify which function you mean
to call. For instance, if you are in a Paint method, you write the following:
procedure Form1.Paint;
begin
Inherited Paint;
Canvas1.TextOut(1, 1, ‘hi’);
end;
When you are inside message handlers, however, there is no need to specify the inherited method
you want to call, and you can therefore just write Inherited.
If you omit the call to Inherited from the MySpecialMouse procedure, the FormMouseMove method
in the EVENTS2 program is no longer called. In other words, you are directly trapping
wm_MouseMove messages and not passing them on to the VCL. As a result, the VCL does not
know that the event occurred, and FormMouseMove is not called.
The explanation in the last paragraph might not be easy to grasp unless you actually experi-
ment with the EVENTS2 program. You should run the program once with the default version
of the MyMouseMove method, and once with the call to Inherited commented out:
procedure TEvents.MyMouseMove(var M: TWMMouse);
begin
{ Inherited; }
LSpecialMouse.Caption := ‘X: ‘ + IntToStr(M.XPos) +
‘ Y: ‘ + IntToStr(M.YPos);
end;
Notice that when you run the program this way, the OnMouseMove message at the top of the
form is left blank.
If you look at the header for the MyMouseMove function, you can see that it is passed a parameter
of type TWMMouse. As you recall, the TWMMouse record, found in MESSAGES.PAS, looks like
this:
TWMMouse = record
Msg: Cardinal;
Keys: Longint;
case Integer of
0: (
XPos: Smallint;
YPos: Smallint);
1: (
Pos: TSmallPoint;
Result: Longint);
end;
If you break out both of the options shown in this variant record, you can further simplify this
record by writing
TWMMouse = record
Msg: Cardinal;
Keys: LongInt;
XPos: SmallInt;
YPos: SmallInt;
end;
or
TWMMouse = record
Msg: Cardinal;
Keys: LongInt;
Pos: TSmallPoint;
Result: Longint;
end;
For most users, one of these two views will be the most useful way to picture the record.
The same information is present in a TWMMouse record that you would find if you responded to
an OnMouseMove or OnMouseDown event. If appropriate, you can find out the row and column
where the mouse is located, what key is pressed, and what state the Shift, Alt, and Ctrl keys are
in. To pursue this matter further, you should look up wm_MouseMove and wm_MouseDown mes-
sages in the online help, or refer to the earlier chapters on the Windows API, such as Chapter
12, “The Mouse and the Keyboard.”
TWMMouse plays the same role in a Delphi program that message crackers from WINDOWSX.H
play in a C++ program. In other words, they automatically break out the values passed in lParam
or wParam. However, if you want, you can pass a variable of type TMessage as the parameter sent
to the wm_MouseMove message handler:
procedure MyMouseMove(var M: TMessage);
message wm_MouseMove;
Because TMessage and TWMMouse are both the same size, Delphi doesn’t care which one you use
when trapping wm_MouseMove events. It’s up to you to decide how you want to crack the wParam
and lParam parameters.
NOTE
Delphi is such a flexible tool that it lets you write Windows API code that can be
totally specific to either the 16- or 32-bit versions of Windows. In other words, there is
nothing in Delphi to prevent you from writing 32-bit code that takes advantage of
threads. However, you cannot take that code to the 16-bit version of Windows and
expect it to work. The rule to follow is very simple: If you write VCL code, it compiles
under either the 16- or 32-bit compiler. However, if you start calling the Windows
API directly, you take the chance that the call you make will change or not be sup-
ported under WIN32. Of course, most of the Windows API works in either 16- or 32-
bit mode, but some calls can cause trouble, and you need to be aware of them if you
are going to try to write portable code.
In this section, you have learned something about directly handling Windows messages. When
you write code that captures messages directly, you are in a sense reverting back to the more
complicated model of programming found in Borland Pascal 7.0. However, there are times
when it is very helpful to get close to the machine, and Delphi lets you get there if that is what
you need to do.
Handling wm_Command
In standard Windows programming as it was conducted before the appearance of visual tools,
one of the most important messages was wm_Command. This message was sent to a program every
time the user selected a menu item, a button, or clicked almost any other control that is part of
the current program. Furthermore, each of the buttons, menu items, and other controls in a
program had a special ID, which was assigned by the programmer. This ID was passed to
wm_Command handlers in the wParam variable.
Delphi handles wm_Command messages in such a way that you almost never have to think about
them. For instance, you can get clicks on a button or menu by using the delegation model.
Standard Delphi controls still have IDs, but Delphi assigns these numbers automatically, and
there is no obvious way for you to learn the value of these IDs.
Despite Delphi’s capability to simplify this aspect of Windows programming, there are still
times when you want to get down to the bare bones and start handling wm_Command messages
yourself. In particular, you will want to find a way to discover the ID associated with a particu-
lar command, and you will want to trap that ID inside a wm_Command handler.
The MENUDEF program gives a general overview of this topic. The program enables you to
discover the ID used by a series of menu items and then enables you to trap these IDs when
they are sent to a wm_Command handler in the form of a TMessage.wParam variable.
Figure 22.5 shows the form for the MENUDEF program. Here are the menu items that you
can’t see in Figure 22.5:
Caption = ‘File’
Caption = ‘Open’
Caption = ‘Close’
Caption = ‘Exit’
Caption = ‘Edit’
Caption = ‘Cut’
Caption = ‘Copy’
Caption = ‘Paste’
FIGURE 22.5.
The MENUDEF program
uses a TMemo, a TButton,
and a TMainMenu control.
The code for the MENUDEF program is in Listing 22.5. You can see that it features two stan-
dard Delphi event handlers, as well as a wm_Command handler.
Listing 22.5. The MENUDEF program shows how to retrieve the ID of a Delphi
menu item.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
Menus, StdCtrls;
type
TForm1 = class(TForm)
MainMenu1: TMainMenu;
File1: TMenuItem;
continues
var
Form1: TForm1;
implementation
{$R *.DFM}
Memo1.Lines.Clear;
for i := 0 to ComponentCount - 1 do
if Components[i] is TMenuItem then begin
Command := TMenuItem(Components[i]).Command;
Name := TMenuItem(Components[i]).Caption;
Memo1.Lines.Add(Name + ‘ = ‘ + IntToStr(Command));
end;
end;
end.
The code begins by clearing the current contents of the memo control. Then it iterates through
all the components on the form and finds any of them that are of type TMenuItem. The next
step is to get the ID and the caption of the menu items. To get the ID, you need only reference
the Command property of the TMenuItem component. The caption can be retrieved the same way,
and then you can add this information to the list box.
The use of the is operator in the previous code demonstrates Delphi’s capability to work with
Run Time Type Information (RTTI). RTTI enables you to test the type of a particular vari-
able and respond accordingly. For instance, in this case, the program simply asks whether a
particular component is of type TMenuItem; if this Boolean question returns True, the pro-
gram examines the component in more depth. The capability to use the is and as operators to
detect the type of an object at runtime is one of the sexier features of Delphi’s language.
The remaining code in the program captures the IDs of the menu items in an array and then
responds to wm_Command messages generated by clicks in one of the program’s menu items. As
explained earlier, the code displays a message box stating that the menus are not yet functional.
In the caption of the menu box, you can read the ID of the control that was clicked.
The code that captures the menu items in an array occurs in a FormCreate method:
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
for i := 0 to ComponentCount - 1 do
if Components[i] is TMenuItem then begin
MenuItemAry[TotalMenuItems] :=
TMenuItem(Components[i]).Command;
Inc(TotalMenuItems);
end;
end;
This code is almost identical to the MenuIDClick method, except that the IDs of the TMenuItems
are stored in an array, rather than being shown in a TMemo. The declaration for the array looks
like this:
MenuItemAry: array [0..100] of Word;
The declaration for the wm_Command handler should be familiar to you by this time:
procedure WMCommand(var Message:TMessage);
message wm_Command;
Here, the message directive tells the compiler that this is a dynamic method and the offset in
the dynamic method table is established by the wm_Command constant.
The WMCommand method compares the values sent in Message.wParam to the values in the
MenuItemAry . If it finds a match, it displays the message box previously described.
This code calls the Windows API MessageBox function rather than MessageDlg.
One final point about working with the IDs of a control: If you build the interface of your
program and then don’t change its menus or any other aspect of its interface, the IDs Delphi
associates with each control will (at least theoretically) remain the same throughout the devel-
opment of your application. This might give some people the idea of using the MenuIDClick
method shown earlier as a temporary response to a click on one of your controls and to then
store the output to a file. Thereafter, you would hope to know the ID associated with each of
your controls and could handle them in a wm_Command routine. This technique is theoretically
possible, but I wouldn’t recommend it as part of the framework for any serious application. In
short, if you have to know the ID of a particular control, I would determine it in the FormCreate
method, thereby ensuring that the ID is correct for that build of your program.
Summary
In this section, you learned how to handle events using the standard Delphi delegation model.
However, you saw that you can also circumvent this system and handle events directly by
employing the message keyword. This is an important subject, and you shouldn’t really move
on until you have a good idea of what’s going on in the EVENTS2 program.
This chapter also explained that you can start handling wParam and lParam variables directly.
Furthermore, Delphi gives you a way to parse the information passed with events, so that you
can place a custom record as the parameter to a message handler. For instance, if Windows
conceals a set of X and Y coordinates inside the high and low words of wParam, Delphi enables
you to define a custom record that automatically breaks up wParam into two separate variables
called X and Y. This is the same functionality that is found in the message crackers provided in
the WINDOWSX.H module that ships with Borland C++. The TWMMouse record discussed earlier
is one of these records; TMessage is another. If you want, you can create your own records that
parse the information associated with standard Delphi events or with events that you create
yourself.
Objects and
Inheritance 23
Objects,
Encapsulation, 24
and Properties
■ Properties 844
In this chapter, you continue the overview of object-oriented programming begun in the last
chapter. In particular, the focus is on encapsulation and properties. The next chapter focuses
on polymorphism.
This chapter covers the following topics:
■ An overview of encapsulation, including the need to hide data and certain parts of the
implementation
■ An in-depth look at the private, protected, public, and published scoping directives
■ Property creation
■ The five basic types of properties
■ Read-only properties
■ Default value declarations for properties
This chapter features some of the syntactical jewels in the Delphi treasure chest. In particular,
Delphi offers a complete array of scoping directives. These tools enable you to fine-tune access
to your objects in a way that helps promote their reuse. Properties are also cutting-edge tools,
and their implementation in Delphi yields some surprising fruits, such as arrays that are in-
dexed on strings.
Encapsulation
The word encapsulationand the word objectare closely linked in my mind. Encapsulation is
one of the primary and most fundamental aspects of OOP. It is useful because it helps to enlist
related methods and data under a single aegis, and to hide the implementation details that don’t
need to be exposed or that might change in future versions of an object.
The ability to encapsulate methods and data inside an object is important because it helps you
design clean, well-written programs. To use a classic example, suppose that you were to create
an object called TAirplane, which would represent (naturally enough) an airplane. This object
might have fields such as Altitude, Speed, and NumPassengers; it might have methods such as
TakeOff, Land , Climb , and Descend . From a design point of view, everything is simpler if you
can encapsulate all these fields and methods in a single object, rather than leaving them spread
out as individual variables and routines:
TAirplane = class(TObject)
Altitude: Integer;
Speed: Integer;
NumPassengers: Integer;
procedure TakeOff;
procedure Land;
procedure Climb;
procedure Descend;
end;
There is a sleek elegance in this simple object declaration. Its purpose and the means for imple-
menting its functionality are readily apparent.
Consider the class declaration from the current version of the OBJECT2 program:
THierarchy = class(TMyObject)
HMethod: THMethod;
procedure ShowHierarchy; override;
procedure SetHierarchyMethod(Method: THMethod); dynamic;
end;
THierarchy encapsulates the ShowHierarchy method. If you want to call ShowHierarchy, you
need to first instantiate an object of type THierarchy, then use that object as a qualifier when
you call ShowHierarchy:
var
H: THierarchy;
begin
H := THierarchy.Create;
H.ShowHierarchy;
H.Free;
end.
This kind of encapsulation is useful primarily because it makes you treat everything about the
THierarchy object as a single unit. There are some limitations to this technique, however. In
particular, when you have declared an object of type THierarchy, you have full access to all its
methods and fields. It’s true that you have to jump through one hoop before you can access
THierarchy ’s innards, but there is still a sense that a program’s data is not very well protected.
To eliminate this problem, Delphi defines four directives meant to aid in the process of
encapsulation:
private: Use this directive to declare a section in a class that can only be accessed from
inside the current unit. For instance, if the HMethod variable is declared in a private
section, it can’t be accessed directly by code that is outside its unit. Private code and
data can, however, be accessed by any object, method, procedure, or function that
resides within the same unit.
protected: Code declared in a protected section can only be accessed by descendant
objects. In other words, if HMethod is declared in a protected section, variables of type
THierarchy do not have access to it. You have to declare a descendant of THierarchy
before you can access this variable directly. Once again, this rule applies only when
you are outside the unit in which the object was declared. The point here is that
protected fields and methods are available only to other component developers, not to
standard consumers of the object.
public: Code declared in a public section of an object is available to anyone who uses
an object of that particular type. It’s the standard interface of the object.
published: Properties that are declared in the published section are public variables
that appear in the Object Inspector. Furthermore, you can discover the type of
published properties at runtime; simple published properties can be streamed to disk
automatically. (See the ENUMINFO program in the PROGRAMS directory for
additional information on determining types or type of information at runtime.)
Because published properties are for use in the Object Inspector, Delphi does not
allow you to declare a property as published unless the object descends from
TComponent or one of TComponent’s children. TComponent is the base object from which
all objects that appear on the Component Palette must descend. Without inheriting
TComponent ’s functionality, no object would normally be able to show its properties in
the Object Inspector.
NOTE
A directive called Automated is also used with the TAutomation object. Because the
subject is separate, I cover it in Chapter 29, “OLE Automation Basics.”
Given these scoping directives, the following might be a sensible way to declare THierarchy:
THierarchy = class(TMyObject)
private
FHierarchyMethod: THMethod;
public
procedure ShowHierarchy; override;
procedure SetHierarchyMethod(Method: THMethod); dynamic;
end;
■ A set of interface routines that enable programmers to talk to the concealed methods
and data that form the heart of an object.
Data and implementation sections are hidden so that the developer of the object can feel free
to change those sections at a later date. If you expose a piece of data or a method to the world
and find a bug that forces you to change the type of that data, or the declaration for that method,
you are breaking the code of people who rely on that data or that method. Therefore, it’s best
to keep all your key methods and data hidden from consumers of your object. Give them ac-
cess to those methods and procedures only through properties and public methods. Keep the
guts of your object private, so that you can rewrite it, debug it, or rearrange it at any time.
One way to approach the subject of hiding data and methods is to think of objects as essen-
tially modest beings. An object doesn’t want to show the world how it performs some task, and
especially doesn’t directly show the world the data or data structures it uses to store information.
The actual data used in an object is a private matter that should never be exposed in public.
The methods that manipulate that data should also be hidden from view because they are the
personal business of that object. Of course, an object does not want to be completely hidden
from view, so it supplies a set of interface routines that talk to the world, but these interface
routines jealously guard the secret of how an object’s functionality is actually implemented.
A well-made object is like a beautiful woman who conceals her charms from a prying world.
Conversely, a poorly made object should also hide, much like an elderly man who doesn’t want
the world to see how his youth and virility has faded. These analogies are intentionally a bit
whimsical, but they help to illustrate the extremely powerful taboo associated with directly
exposing data and certain parts of the implementation.
Of course, the point of hiding data and implementations is not to conceal how an object works,
but to make it possible to completely rewrite the core of an object without changing the way it
interacts with the world. In short, the previous analogies collapse when it comes to a functional
analysis of data hiding, but they serve well to express the spirit of the enterprise.
NOTE
If you have the source code to the VCL, you will find that at least half of most key
objects in the Delphi code base are declared as private. A few complex objects contain
several hundred lines of private declarations, and only 20 or 30 methods and properties
that serve as an interface for that object. Objects near the top of a hierarchy don’t
always follow this model because they usually consist primarily of interface routines.
It’s the core objects that form the heart of a hierarchy that are currently under
discussion.
The internal methods of Delphi objects tend to reference their data through properties,
rather than directly addressing the internal data storage. Not only consumers, but
the objects themselves, should have the discipline to enforce data-hiding in their
implementation. Strictly speaking, this is the correct and wisest approach, but you will
find that I sometimes allow methods of an object to directly reference the data declared
in that same object. I am moving away from this practice gradually, however, because
it has cost me during the maintenance phase of a project.
Whatever weaknesses I allow myself, however, I believe firmly that it is always wrong
to declare a variable outside of a private section. Hiding data and methods is one of the
most important aspects of OOP, and programmers who ignore this practice are simply
asking to deliver products that are buggy and late.
No hard-and-fast rules exist in the area of object design. However, the presence of an easy-to-
use interface is a good sign. If you are working with a component, you should be able to drop
it onto a form and hook it up in just a few seconds. If you are working with an object that is
not encapsulated in a component, you should be able to initialize and use it by writing just a
few lines of code. If hooking up takes more than a few minutes, you often have a reason to be
suspicious of the entire enterprise.
The private methods of your objects should also be cleanly and logically designed. However, it
is not a disaster if your private methods are difficult to understand as long as your public meth-
ods are easy to use. The bottom line is that the private methods should be designed to accom-
plish a task, and the public methods should be designed to be easy to use. When you look at
the design this way, you can see a vast difference between the public and private methods in an
object.
As you read through the next few chapters, you might consider thinking about the importance
of creating a simple interface to your objects. Having a simple interface is particularly impor-
tant for the top-level objects in your hierarchy.
A Concrete Example
Neither TMyObject nor THierarchy give us much scope for exploring encapsulation. As a result,
it’s time to introduce a new class called TWidget, which is a descendant of THierarchy. In the
chapter on polymorphism, TWidget will become the ancestor of a series of different kinds of
widgets that can be stored in a warehouse. In other words, in a computer factory TWidget might
be the ancestor of several classes of objects such as TSiliconChip, TAddOnBoard, and TPowerSupply.
Descendants of these objects might be called T486Chip, T386Chip, TVideoBoard, and so on. Object
hierarchies always move from the general to the specific:
TWidget
TSiliconChip
TPentiumChip
(I don’t use the published directive since it is only allowed in TComponent descendants.) Notice
also that the class declarations have been broken off into their own unit. This is done for two
reasons:
■ The program needs to be able to enforce the declaration of private data, which can
only be accomplished if the classes involved are declared in their own unit.
■ Class THierarchy is nearing the point of usefulness, so it’s time for it to be broken off
into a separate module that can be used by multiple programs and can exist as a single
definable entity.
uses
Classdef in ‘CLASSDEF.PAS’;
var
W: TWidget;
begin
W := TWidget.Create;
W.HierarchyMethod := hmScreen;
W.ShowHierarchy;
WriteLn(W.Quantity);
W.Stock;
WriteLn(W.Quantity);
W.Sell(2);
WriteLn(W.Quantity);
W.Col := 20;
W.Row := 12;
W.Paint;
W.Free;
ReadLn;
end.
Listing 24.2. The core ofthe OBJECT3 program is in the CLASSDEF unit.
unit Classdef;
interface
uses
SysUtils, Windows;
type
THMethod = (hmScreen, hmDisk);
TMyObject = class(TObject)
public
procedure ShowHierarchy; virtual;
end;
THierarchy = class(TMyObject)
private
FHierarchyMethod: THMethod;
public
procedure ShowHierarchy; override;
property HierarchyMethod: THMethod
read FHierarchyMethod write FHierarchyMethod;
end;
TWidget = class(THierarchy)
private
FCol: Integer;
FRow: Integer;
FQuantity: LongInt;
FBoxSize: LongInt;
FMaxQuantity: LongInt;
FDescription: string;
function GetQuantity: string;
procedure SetQuantity(S: string);
protected
function GetName: string; virtual;
public
constructor Create; virtual;
procedure Sell(Amount: LongInt); virtual;
procedure Stock; virtual;
procedure Paint; virtual;
property Quantity: string read GetQuantity write SetQuantity;
property Col: Integer read FCol write FCol;
property Row: Integer read FRow write FRow;
end;
var
HOut: THandle;
implementation
uses
StrBox;
{ -- TMyObject -- }
procedure TMyObject.ShowHierarchy;
var
AClass: TClass;
begin
WriteLn(ClassName);
AClass := ClassParent;
while AClass <> nil do begin
WriteLn(AClass.ClassName);
AClass := AClass.ClassParent;
end;
end;
continues
procedure THierarchy.ShowHierarchy;
var
F: Text;
AClass: TClass;
begin
if FHierarchyMethod = hmScreen then
inherited ShowHierarchy
else begin
Assign(F, ‘inherit.txt’);
ReWrite(F);
WriteLn(F, ClassName);
AClass := ClassParent;
while AClass <> nil do begin
WriteLn(F, AClass.ClassName);
AClass := AClass.ClassParent;
end;
Close(F);
end;
end;
{ -- TWidget -- }
constructor TWidget.Create;
begin
inherited Create;
FBoxSize := 5;
end;
procedure TWidget.Stock;
begin
FQuantity := FQuantity + FBoxSize;
end;
procedure TWidget.Paint;
begin
GotoXY(FCol, FRow);
Write(‘* ‘ + GetName);
end;
begin
HOut := GetStdHandle(STD_OUTPUT_HANDLE);
end.
The functionality associated with this program is still severely limited. An object of type TWidget
is created, and its hierarchy is shown. The program next simulates the act of stocking up on a
predefined number of these widgets. Finally, a bare representation of a widget is displayed on
the screen:
W := TWidget.Create;
W.HierarchyMethod := hmScreen;
W.ShowHierarchy;
WriteLn(W.Quantity);
W.Stock;
WriteLn(W.Quantity);
W.Col := 20;
W.Row := 12;
W.Paint;
W.Free;
FIGURE 24.1.
The simple text output
from the OBJECT3
program.
From a user’s point of view, this is pretty tame stuff. However, the declaration for class TWidget
shows programmers a good deal about how Delphi implements encapsulation:
TWidget = class(THierarchy)
private
FCol: Integer;
FRow: Integer;
FQuantity: LongInt;
FBoxSize: LongInt;
FMaxQuantity: LongInt;
FDescription: string;
function GetQuantity: string;
procedure SetQuantity(S: string);
protected
function GetName: string; virtual;
public
constructor Create; virtual;
procedure Sell(Amount: LongInt); virtual;
procedure Stock; virtual;
procedure Paint; virtual;
property Quantity: string read GetQuantity write SetQuantity;
property Col: Integer read FCol write FCol;
property Row: Integer read FRow write FRow;
end;
The private section of TWidget contains several fields of data and two methods:
private
FCol: Integer;
FRow: Integer;
FQuantity: LongInt;
FBoxSize: LongInt;
FMaxQuantity: LongInt;
FDescription: string;
function GetQuantity: string;
procedure SetQuantity(S: string);
All of the private data in the program have variable names that begin with the letter F. As stated
before, this is a convention and not a syntactical necessity. These variables are called internal
storage.
Internal storage should always be declared private, and as such cannot be accessed from out-
side of this unit. To reiterate, all of the data in any object should always be declared private.
Other objects should never access any of this data directly, but should manipulate it through a
predefined interface that appears in the protected, published, or public sections. If you want,
you can think of the F in these names as standing for “Forbidden,” as in “it is forbidden to
directly access this data!”
NOTE
As I mentioned before, some of the programs in this book may place data in a public
section. You won’t see that happen often, but it may show up once or twice in small,
simple, example programs. While it’s one thing to write an example program that has
some public data, it’s another thing to have public data in a production executable on
which your own or a company’s fortunes might depend. That’s just not done.
Don’t drive down an urban highway at eighty-five miles an hour. Don’t drink and
drive. Don’t think that you are somehow miraculously going to be immune to crack’s
addictive qualities. Don’t step between a mother grizzly bear and her cubs. Don’t ask
who’s buried in Grant’s tomb during the middle of a job interview. Don’t swim with
the sharks if you are bleeding. Don’t declare public data in a production quality
program!
The GetQuantity and SetQuantity functions are also declared private, and you will see that
they are accessed through a property. Most objects have many more private methods, but TWidget
is relatively bare in this department. The lack of private methods occurs because TWidget is
such a simple object that there isn’t much need to perform complex manipulations of its data.
The protected section is simple, and contains a single virtual method. This portion of the
object can be accessed by descendants of TWidget, but not by an instance of the class. For
example, if you make the declaration
var
W: TWidget;
This functionality is available only from outside the unit in which the object is declared. That
is, the rule enforcing the inaccessibility of GetName is valid only if you are outside the unit in
which the object is declared. From inside the unit, the code shown above will compile.
The methods in the public section of the object make it possible to manipulate the widgets
that you declare:
public
constructor Create; virtual;
procedure Sell(Amount: LongInt); virtual;
procedure Stock; virtual;
procedure Paint; virtual;
Here you can see a method for selling widgets, a method for stocking up on new widgets, and
a method for painting pictures of widgets to the screen. All of these are common activities and
need to be declared public.
The create method for the object sets the value of FBoxSize:
constructor TWidget.Create;
begin
inherited Create;
FBoxSize := 5;
end;
FBoxSize is meant to represent the number of items found in a box of widgets. Some items
aren’t sold individually, but only in boxes; this item describes how many widgets fit in one
box.
Both Sell and Stock are simple methods:
procedure TWidget.Sell(Amount: LongInt);
begin
if Amount := 0 then
FQuantity := FQuantity - FBoxSize
else
FQuantity := FQuantity - Amount
end;
procedure TWidget.Stock;
begin
FQuantity := FQuantity + FBoxSize;
end;
Because the calculations that occur here are trivial, there is no need to hide their implementa-
tions in private methods. However, if this were a more sophisticated program, and either Sell
or Stock needed to call methods with names such as CalculateBoxSize or CalculateQuantity,
those methods would be hidden behind a property because they might change in the future,
and the user does not need to know of their existence.
You have now had an overview of all the code in the OBJECT3 program, except for its prop-
erties, which will be discussed in the next section. The discussion so far has concentrated on
the Delphi object scoping directives. You have learned about the private, protected, public,
and published sections of a program, and have seen why each is necessary.
Properties
Properties provide several advantages:
■ Properties enable you to hide data.
■ If you write a component and place it in the Component Palette, its published
properties appear in the Object Inspector.
■ Some properties can be made available at design time, while variables are only avail-
able at runtime.
■ Properties can have side effects such as not only setting the value of the FWidth
variable, but also physically changing the width of the object that appears on the
screen.
■ Property access methods can be declared virtual, which gives them more flexibility
than simple variables.
If the TWidget object were a descendant of TComponent, all or some of these properties could be
put in the published section and, therefore, could be seen from inside the Object Inspector if
the object were compiled into Delphi’s library.
There is no rule that says which properties should be declared in the published or public sec-
tions. In fact, properties often appear in public sections, although there is little reason for them
to be in private or protected sections. In this case, all the properties appear in the public sec-
tion because TWidget is not a descendant of TComponent.
The two properties shown here are simple tools that do nothing more than hide data and lay
the groundwork for their use inside the Object Inspector:
property Col: Integer read FCol write FCol;
The declaration starts with the keyword property, which performs the same type of syntactical
chore as procedure or function. Every property must be declared as having a certain type, which
in this case is Integer.
Most properties can be both read and written. The read directive for the Col property states
that the value to be displayed is FCol and the value to write is FCol. In short, writing
var
i: Integer;
begin
MyWidget.Col := 2;
i := MyWidget.Col;
end;
sets FCol to the value 2, and sets i to the value of FCol (again, 2).
The reasons for doing this are two-fold:
■ To hide data so that it is protected.
■ To create a syntax that allows properties to be shown in the Object Inspector. Of
course, you won’t see these values in the Object Inspector until you metamorphose
the object into a component, which is a subject that will be covered in Chapter 27,
“Creating Nonvisual Components.”
The Col and Row properties provide what is called direct access; they map directly to the internal
storage field. The runtime performance of accessing data through a direct-access property is
exactly the same as accessing the private field directly.
NOTE
The FCol and FRow properties are used to designate a location on the screen. To move
to a location on a text-mode screen, Pascal programmers have traditionally called the
GotoXY procedure. This procedure is not, however, automatically implemented in
Delphi. Instead, I am forced to make up my own version of this routine:
procedure GotoXY(X, Y: SmallInt);
var
C: TCoord;
begin
C.X := X;
C.Y := Y;
SetConsoleCursorPosition(HOut, C);
end;
SetConsoleCursorPosition is a routine provided by Windows for those cases when you
need to move the cursor in a console application. It is declared in WINDOWS.PAS.
The TCoord type is also declared in WINDOWS.PAS:
PCoord = ^TCoord;
TCoord = packed record
X: SHORT;
Y: SHORT;
end;
A SHORT is declared as a SmallInt in WINDOWS.PAS. Remember that when you see a
Delphi variable declared in all capital letters, it is likely that it is being included to
provide compatibility with the variables declared in the official copy of WINDOWS.H
(and its related files) provided by Microsoft.
The HOut variable of type THandle used in the first parameter of
SetConsoleCursorPosition is a handle to standard out . You can retrieve it with the
following code, which appears at the bottom of the ClassDef unit:
HOut := GetStdHandle(STD_OUTPUT_HANDLE);
The Col and Row examples represent the simplest possible case for a property declaration. The
Quantity property presents a few variations on these themes:
Rather than reading a variable directly, Quantity returns the result of a private function:
function TWidget.GetQuantity: string;
begin
Result := ‘Quantity: ‘ + IntToStr(FQuantity);
end;
SetQuantity , on the other hand, enables you to change the value of the FQuantity variable:
procedure TWidget.SetQuantity(S: string);
begin
FQuantity := StrToInt(S);
end;
GetQuantity and SetQuantity are examples of access methods. Just as the internal storage for
direct access variables begins by convention with the letter F, access methods usually begin with
either “Set” or “Get.”
Take a moment to consider what is happening here. To use the Quantity property, you need
to use the following syntax:
var
S: string;
begin
S := W.Quantity;
W.Quantity := ‘25’;
end;
In the preceding code, S is set to a string that might look like ‘Quantity: 10’ or ‘Quantity:
25’. Note that when you are writing to the FQuantity variable, you don’t write:
W.Quantity(‘25’);
Instead, you use the simple, explicit syntax of a direct assignment. Delphi automatically trans-
lates the assignment into a function call that takes a parameter. C++ buffs will recognize this as
a limited form of operator overloading.
If there were no properties, the previous code would look like this:
var
S: string;
begin
S := W.GetQuantity;
W.SetQuantity(‘25’);
end;
Instead of remembering one property name, this second technique requires you to remember
two, and instead of the simple assignment syntax, you must remember to pass a parameter.
Although it is not the main purpose of properties, it should now be obvious that one of their
benefits is that they provide a clean, easy-to-use syntax.
NOTE
The Quantity property differs from the Sell and Stock methods because it changes
FQuantity directly, rather than adding to or subtracting from it.
In the last few pages, you had a good look at the OBJECT3 program. There are several addi-
tional traits of properties that should be explored, however, before moving on to the colorful
warehouse simulation found in the next chapter.
More on Properties
Delphi provides support for five different types of properties:
■ Simple properties are declared to be integers, characters, or strings.
■ Enumerated properties are declared to be of some enumerated type. When shown in
the Object Inspector, you can view them with a drop-down list.
■ Set properties are declared to be of type Set. BorderIcons from TForm is an example of
this type of property. You can only choose one enumerated value at a time, but you
can combine several values in a property of type Set.
■ Object properties are declared to be of some object type, such as the Items property
from the TListBox component, which is declared to be of type TStrings.
■ Array properties are like standard arrays, but you can index on any type, even a string.
The classic example of this kind of property is the Strings property in a TStringList.
The PROPTEST program (referenced in Listings 24.3 and 24.4) gives an example of each of
the five types of properties. It also gives the TStringList object a fairly decent workout. The
program itself is only minimally useful outside the range of a purely academic setting such as
this book.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
ListBox1: TListBox;
ListBox2: TListBox;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
MyObj1;
{$R *.DFM}
end.
interface
uses
Classes, Forms, Controls,
StdCtrls, Graphics, SysUtils;
continues
TCouple = class(TObject)
Husband: string;
Wife: string;
end;
TMyProps = class(TCustomControl)
private
FSimple: Integer;
FEnumType: TEnumType;
FSetProp: TSetProp;
FObjectProp: TStringList;
FDefault1: Char;
function GetArray(Index: integer): string;
function GetStrArray(S: string): string;
procedure SetStrArray(Index: string; S: string);
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property ArrayProp[i: integer]: string read GetArray;
property StrArrayProp[i: string]: string read GetStrArray write SetStrArray;
published
property SimpleProp: Integer read FSimple write FSimple;
property EnumProp: TEnumType read FEnumType write FEnumType;
property SetProp: TSetProp read FSetProp write FSetProp;
property ObjectProp: TStringList read FObjectProp write FObjectProp;
property Default1: Char read FDefault1 write FDefault1 default ‘1’;
end;
implementation
uses
StrBox;
constructor TMyProps.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 100;
Height := 100;
Left := (TForm(AOwner).ClientWidth div 2) - (Width div 2);
Top := (TForm(AOwner).ClientHeight div 2) - (Height div 2);
FObjectProp := TStringList.Create;
Default1 := ‘1’;
end;
destructor TMyProps.Destroy;
var
i: Integer;
begin
for i := 0 to FObjectProp.Count - 1 do
FObjectProp.Objects[i].Free;
FObjectProp.Free;
inherited Destroy;
end;
procedure TMyProps.Paint;
begin
Canvas.Brush.Color := clBlue;
inherited Paint;
Canvas.Rectangle(0, 0, Width, Height);
Canvas.TextOut(1, 1, ‘FSimple: ‘ + IntToStr(FSimple));
Canvas.TextOut(1, Canvas.TextHeight(‘Blaise’), GetArray(0));
Canvas.TextOut(1, Canvas.TextHeight(‘Blaise’) * 2, FObjectProp.Strings[1]);
end;
end.
The structure of the PROPTEST program is simple. There is a main form with a button on it.
If you click the button, you instantiate an object of type TMyObject. TMyObject has five proper-
ties, one for each of the major types of properties. These properties have self-explanatory names:
property SimpleProp;
property EnumProp;
property SetProp;
property ObjectProp;
property ArrayProp;
Before exploring these properties, I should mention that TMyObject is descended from the na-
tive Delphi object called TCustomControl. TCustomControl is intelligent enough to both display
itself on the screen and store itself on the Component Palette. It has several key methods and
properties already associated with it, including a Paint method and Width and Height fields.
(This instance of TMyObject has nothing to do with the TMyObject from which THierarchy is
descended. It’s just a coincidence that the two objects have the same name.)
Because TCustomControl is so intelligent, it is easy to use its Paint method to write values to the
screen:
procedure TMyProps.Paint;
begin
Canvas.Brush.Color := clBlue;
inherited Paint;
Canvas.Rectangle(0, 0, Width, Height);
Canvas.TextOut(1, 1, ‘FSimple: ‘ + IntToStr(FSimple));
Canvas.TextOut(1, Canvas.TextHeight(‘Blaise’), GetArray(0));
Canvas.TextOut(1, Canvas.TextHeight(‘Blaise’) * 2,
FObjectProp.Strings[1]);
end;
Note that you do not need to explicitly call the Paint method. Windows calls it for you when-
ever the object needs to paint or repaint itself. This means that you can hide the window be-
hind others, and it will repaint itself automatically when it is brought to the fore. Inheriting
functionality that you need from other objects is a big part of what OOP is all about. You will
get a chance to look more closely at TCustomControl and similar objects in Chapter 26, “Cre-
ating Components.”
The first three properties of TMyObject are extremely easy to understand:
property SimpleProp: Integer read FSimple write FSimple;
property EnumProp: TEnumType read FEnumType write FEnumType;
property SetProp: TSetProp read FSetProp write FSetProp;
These are direct access properties that simply read to and write from a variable. You can use
them with the following syntax:
M.SimpleProp := 25;
M.EnumProp := teEnum;
M.SetProp := [teEnum, TeSet];
NOTE
I once asked one of the developers whether properties such as these didn’t waste
computer clock cycles. Looking somewhat miffed, he said, “Obviously, we map those
calls directly to the variables!”
Chastened, and somewhat the wiser, I nodded sagely as if this were the answer I
expected. Then, I ventured, “So they don’t cost us any clock cycles?”
“Not at runtime, they don’t!” he said, and concentrated once again on his debugger,
which hovered over some obscure line in CLASSES.PAS.
The syntax for using the ObjectProp property is similar to the examples shown previously, but
it is a bit harder to fully comprehend the relationship between an object and a property:
property ObjectProp: TStringList
read FObjectProp write FObjectProp;
ObjectProp is of type TStringList, which is a descendant of the TStrings type used in the
TListBox.Items property or the TMemo.Lines property. I use TStringList instead of TStrings
because TStrings is essentially an abstract type meant for use only in limited circumstances.
For general purposes, you should always use a TStringList instead of a TStrings object. (In
fact, neither TListBox nor TMemo actually uses variables of type TStrings. They actually use
descendants of TStrings, just as I do here.)
NOTE
A TStringList has two possible functions. You can use it to store a simple list of
strings, and you can also associate an object with each of those strings. To perform the
latter task, call AddObject, passing a string in the first parameter and a TObject descen-
dant in the second parameter. You can then retrieve the object by passing in the string
you used in the call to AddObject.
TStringLists do not destroy the objects that you store in them. It is up to you to
deallocate the memory of any object you store on a TStringList.
If you want a simple list object that doesn’t have all this specialized functionality, use a
linked list or the versatile TList object that ships with Delphi.
After making the declaration for ObjectProp shown earlier, you can now use it as if it were a
simple TStringList variable. This can sometimes be a bit inconvenient, however. For instance,
the following syntax retrieves an object that is associated with a string:
S := ‘StringConstant’;
MyObject := FObjectProp.Objects[FObjectProp.IndexOf(S)]
Furthermore, you must be sure to allocate memory for the FObjectProp at the beginning of
TMyProps’s existence, and you must dispose of that memory in the TMyProps destructor:
...
end;
destructor TMyProps.Destroy;
begin
for i := 0 to FObjectProp.Count - 1 do
FObjectProp.Objects[i].Free;
FObjectProp.Free;
inherited Destroy;
end;
This is the classic cycle so well known to BP7 programmers, but less frequently encountered in
standard Delphi programming. The key point is that TMyProps.Destroy is called automatically
whenever the form is freed.
Finally, you must also allocate memory for each object you place in a TStringList. That is, you
must create not only a TStringList, but you must also create each object you pass to the
TStringList.AddObject routine. When you are done with those objects, you must destroy them:
for i := 0 to FObjectProp.Count - 1 do
FObjectProp.Objects[i].Free;
There is nothing you can do about the necessity of allocating and deallocating memory for an
object of type TStringList. You can, however, use array properties to simplify the act of ac-
cessing it, and to simplify the act of allocating memory for each object you store in it.
TESTPROP shows how this can be done. Specifically, it entertains the concept that you are
creating a list for a party to which only married couples are being invited. Each couple’s last
name is stored as a string in a TStringList, and their first names are stored in an object that is
stored in the TStringList in association with the last name. In other words, TESTPROP calls
AddObject with the last name in the first parameter and an object containing their first names
in the second parameter. This sounds complicated at first, but array properties can make the
task trivial from the user’s point of view.
In the TESTPROP program, I store a simple object with two fields inside the TStringList:
TCouple = class(TObject)
Husband: string;
Wife: string;
end;
Note that this object looks a lot like a simple record. In fact, I would have used a record here,
except that TStringLists expect TObject descendants, not simple records. (Actually, you can
sometimes get away with storing nonobjects in TStringLists, but I’m not going to cover that
topic in this book.)
As described earlier, it would be inconvenient to ask consumers of TMyObject to allocate memory
for a TCouple object each time they needed to be used. Instead, TESTPROP asks the user to
pass in first and last names in this simple string format:
‘HusbandName, WifeName’
TESTPROP also asks them to pass in the last name as a separate variable. To simplify this
process, I use a string array property:
property StrArrayProp[i: string]: string read GetStrArray write SetStrArray;
Notice that this array uses a string as an index, rather than a number!
Given the StrArrayProp declaration, the user can write the following code:
M.StrArrayProp[‘Jones’] := ‘Sam, Mary’;
This is a simple, intuitive line of code, even if it is a bit unconventional. The question, of course,
is how can Delphi parse this information?
If you look at the declaration for StrArrayProp, you can see that it has two access methods called
GetStrArray and SetStrArray . SetStrArray and its associated functions look like this:
Note the declaration for SetStrArray. It takes two parameters. The first one is an index of type
string, and the second is the value to be stored in the array. So, ‘Jones’ is passed in as an
index, and ‘Sam, Mary’ is the value to be added to the array.
SetStrArray begins by allocating memory for an object of type TCouple. It then parses the
husband’s and wife’s names from the string by calling two token-based functions from the StrBox
unit that ships with this book. Finally, a call to AddObject is executed. When the program is
finished, you must be sure to deallocate the memory for the TCouple objects in the Destroy
method:
destructor TMyProps.Destroy;
var
i: Integer;
begin
for i := 0 to FObjectProp.Count - 1 do
FObjectProp.Objects[i].Free;
FObjectProp.Free;
inherited Destroy;
end;
The twin of SetStrArray is GetStrArray . This function retrieves a couple’s name from the
TStringList whenever the user passes in a last name. The syntax for retrieving information
from the StrArray property looks like this:
S := M.StrArrayProp[‘Jones’];
In this case, S is assigned the value ‘Sam, Mary’. Once again, note the remarkable fact that
Delphi enables us to use a string as an index in a property array.
The implementation for GetStrArray is fairly simple:
function TMyProps.GetStrArray(S: string): string;
var
Couple: TCouple;
begin
Couple := TCouple(FObjectProp.Objects[FObjectProp.IndexOf(S)]);
Result := Couple.Husband + ‘, ‘ + Couple.Wife;
end;
The code first retrieves the object from the TStringList, then performs some simple hand waving
to re-create the original string passed in by the user. Obviously, it would be easy to add addi-
tional methods that retrieved only a wife’s name, or only a husband’s name.
I’m showing you this syntax not because I’m convinced that you need to use TStringLists and
property arrays in exactly the manner showed here, but because I want to demonstrate how
properties can be used to conceal an implementation and hide data from the user. The last two
properties declared in this program show how to use important property types, and they also
demonstrate how properties can be used to reduce relatively complex operations to a simple
syntax that looks like this:
M.StrArrayProp[‘Doe’] := ‘John, Johanna’;
S := M.StrArrayProp[‘Doe’];
Consumers of this object don’t need to know that I am storing the information in a TStringList,
and they won’t need to know if I change the method of storing this information at some later
date. As long as the interface for TMyObject remains the same — that is, as long as I don’t change
the declaration for StrArrayProp — I am free to change the implementation at any time.
There is one other array property used in this program that should be mentioned briefly:
property ArrayProp[i: integer]: string read GetArray;
ArrayProp uses the traditional integer as an index. However, note that this array still has a spe-
cial trait not associated with normal arrays: It is Read Only! Because no write method is de-
clared for this property, it cannot be written to; it can be used only to query the TStringList
that it ends up addressing:
function TMyProps.GetArray(Index: integer): string;
begin
Result := FObjectProp.Strings[Index]
end;
Creating a simple interface for an object may not seem important at first, but in day-to-day
programming a simple, clean syntax is invaluable. For instance, the TESTPROP program calls
ArrayProp in the following manner:
for i := 0 to M.ObjectProp.Count - 1 do
ListBox2.Items.Add(M.ArrayProp[i]);
In this case, it’s very helpful that the call to GetArray is so simple. It would not be fun if you
had to complicate matters further by writing this line:
ListBox2.Items.Add(M.FObjectProp.Strings[0]);
NOTE
Astute readers might be noticing that Delphi is flexible enough to enable you to
improve even its own syntax. For instance, if you wanted to, you could create a list box
descendant that enables you to write this syntax:
ListBox2.AddStr(S);
instead of
ListBox2.Items.Add(S);
In the chapter on creating components you will see that you can even replace the
TListBox object on the Component Palette with one of your own making! The
techniques you are learning in these chapters on the VCL will prove to be the key to
enhancing Delphi so that it becomes a custom-made tool that fits your specific needs.
If you bury yourself in the Delphi source code, eventually you might notice the default direc-
tive, which can be used with properties:
property Default1: Char read FDefault1 write FDefault1 default ‘1’;
Looking at this syntax, one would tend to think that this code automatically sets FDefault1 to
the value ‘1’. However, this is not its purpose. Rather, it tells Delphi whether this value needs
to be streamed when a form file is being written to disk. If you make TMyProp into a compo-
nent, drop it onto a form, and save that form to disk, Delphi explicitly saves that value if it is
not equal to 1, but skips it if it is equal to 1.
An obvious benefit of the default directive is that it saves room in DFM files. Many objects
have as many as 25, or even 50, properties associated with them. Writing them all to disk would
be an expensive task. As it happens, most properties used in a form have default values that are
never changed. The default directive merely specifies that default value, and Delphi thus knows
whether to write the value to disk. If the property in the Object Inspector is equal to the de-
fault, Delphi just passes over the property when it’s time to write to disk. When reading the
values back in, if the property is not explicitly mentioned in the DFM file, the property retains
the value you assigned to it in the component’s constructor.
NOTE
The property is never assigned the default value by Delphi. You must ensure that you
assign the default values to the properties as you indicated in the class declaration. This
must be done in the constructor. A mismatch between the declared default and the
actual initial value established by the constructor will result in lost data when streaming
the component in and out.
Similarly, if you change the initial value of an inherited published property in your
constructor, you should also reassert/redeclare (partial declaration) that property in
your descendant class declaration to change the declared default value to match the
actual initial value.
The default directive does nothing more than give Delphi a way of determining
whether it needs to write a value to disk. It never assigns a value to any property. You
have to do that yourself in your constructor.
Of course, there are times when you want to assign a property a default value at the moment
that the object it belongs to is created. These are the times when you wish the default directive
did what its name implies. However, it does not now, and never will, perform this action. To
gain this functionality you must use the constructor, as shown in the TESTPROP application:
constructor TMyProps.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 100;
Height := 100;
FDefault1 := 1;
...
Here, the Width and Height properties are set by default to 100. (As explained in the second
paragraph of the preceding note, you need to be careful that you check to see whether a
published property is declared as default.)
The TESTPROP program is obviously not meant to perform any useful function, but instead
explores the world of properties from a syntactical point of view. Even the example of storing
a couple’s names in a TStringList is implemented primarily for the sake of exploring the syn-
tax involved. It was just a fortuitous coincidence that it ended up yielding a fairly efficient, if
idiosyncratic, solution to a real-life problem.
After reading this section, it should be clear that array properties represent one of the most
powerful and flexible aspects of Delphi programming. I don’t think it’s stretching things to say
that array properties provide the same kind of breakthrough flexibility that some people feel
operator overloading brings to C++. However, operator overloading can be confusing because
one never knows just what an operator in C++ does. Because it can be overloaded, a ‘+’,
‘-’, ‘/’, or other operator in a C++ program might mean different things in different circum-
stances. Delphi array properties, on the other hand, always provide a clean, easy-to-read syntax
that hides object data and implementations, without misleading users of the code.
Summary
That wraps up this introduction to properties and encapsulation. The key points you have
explored are the private, protected, public, and published directives, as well as the art of cre-
ating useful properties. I have also attempted to browbeat you with the importance of hiding
data and methods. Remember that robust, easily maintainable objects never directly expose
their data! Instead, they present the user with a simple, custom-designed interface that should
usually be easy to use.
In the next chapter, you will learn about polymorphism, which is the crown jewel of object-
oriented theory.
Polymorphism
25
■ OBJECT4 875
In this chapter you will learn about polymorphism. This feature is simultaneously one of the
more esoteric and one of the most important concepts in object-oriented programming. Many
people who write object-oriented code never use polymorphism. They may reap a number of
the important benefits of OOP theory, but they miss out on a key tool that yields robust, flex-
ible architectures.
A program called OBJECT4 forms the core of this chapter. OBJECT4 uses simple graphic
objects to depict a warehouse in which several different kinds of widgets are stored. There are
seven panels in this warehouse, each containing from 4 to 12 palettes full of widgets. You are
able to stock each palette with new widgets and de-populate the palettes by selling off stock.
It’s also possible to reach a special view that shows the state of the widgets on an individual
palette, and another view that uses graphs to show the state of the total stock.
The point of this chapter is to build up the object hierarchy begun in OBJECT1 to the point
where it can be used for the relatively practical task just described. One of the main themes of
the chapter is that OOP, which seems highly theoretical and intangible at first glance, actually
turns out to be a natural tool for tracking and depicting the status of real objects, such as the
inventory in a warehouse.
You don’t need to understand polymorphism or much about objects to program in Delphi.
However, if you want to be an expert Delphi programmer and want to create components, this
is material you should master.
The interesting thing about this technique is that, after the change, the parent then acts in
different ways, depending on the traits of the child that is currently assigned to it. One object,
the parent, therefore acts in many different ways.
NOTE
The words “assign” and “equal” are ones that I find extremely tricky. If I am speaking
off the cuff and use the word “assign” in three consecutive sentences, I will trip over
myself for sure! Because this seemingly simple subject is so tricky, I’m going to take a
moment to lay out some rules that you can reference while you’re reading this chapter.
Consider the following code fragment:
Parent := Child;
In this simple statement, the child is assigned to its parent. The parent is not assigned
to its child. You could easily argue that this definition could be reversed, but I do not
take that approach in this chapter.
When referencing the preceding code fragment, I also say that the parent is set equal to
its child. The child is not set equal to its parent. Once again, you can argue that this
definition could be reversed, or that the action is reflexive. Throughout this chapter,
however, I consistently state that the preceding code sets a parent equal to its child, and
not a child equal to its parent.
Because these seemingly simple English phrases are so confusing, at least to me, I try to
illustrate exactly what I mean as often as possible. That is, I may write
“You can set a parent equal to its child:
Parent := Child;”
Here I say the statement in English, insert a colon, and then follow the English phrase
with code that provides an example of the meaning of my statement. I find the English
phrases ambiguous and confusing, but the code is starkly clear. Don’t waste too much
time parsing the English; concentrate on the code!
The classic example of polymorphism is a series of objects all of which do the following:
1. Descend from one base class.
2. Respond to a virtual command called Draw.
3. Respond to the command in different ways.
For instance, you might have four objects called TRectangle, TEllipse, TCircle, and TSquare.
Suppose that each of these objects is a descendant of a base class called TShape, and that TShape
has a virtual method called Draw. (This is a hypothetical TShape object and is not necessarily the
one that appears on Delphi’s Component Palette.) All TShape’s children also have Draw meth-
ods, but one draws a circle, one a square, the next a rectangle, and the last an ellipse. You could
then assign any of these objects to a variable of type TShape, and that TShape variable would act
differently after each assignment. That is, the object of type TShape would draw a square if set
equal to a TSquare object:
Shape := Square
Shape.Draw; // Draws a square.
and so on.
One object, called Shape, therefore acts in many different ways. That’s polymorphism.
From a conceptual point of view, this description does much to explain what polymorphism is
all about. However, there is one key aspect that still needs to be explained.
According to the rules of OOP, you can pass all these objects to a single function that takes an
object of type TShape as a parameter. That single function can call the Draw method of each of
these objects, and each one will behave differently:
procedure DrawIt(Shape: TShape);
begin
Shape.Draw; // TShape draws different shapes depending on “assignment”
end;
var
Rectangle: TRectangle;
Square: TSquare;
Ellipse: TEllipse;
begin
… // Initialize variables;
DrawIt(Rectangle); // Draws a rectangle
DrawIt(Square); // Draws a square
DrawIt(Ellipse); // Draws an ellipse
… // Free Variables
end.
When you pass an object of type TRectangle to a function that takes a TShape as a parameter,
you are accessing the TRectangle object through an object of type TShape. Or, if you look at the
act of passing a parameter from a slightly different angle, you are actually assigning a variable
of type TRectangle to a variable of type TShape:
var
Shape: TShape;
Rectangle: TRectangle;
begin
…
Shape := Rectangle;
…
end;
This assignment is the actual hub around which polymorphism revolves. Because this assign-
ment is legal, you can use an object of a single type, yet have it behave in many different ways:
Once again, that’s polymorphism.
To fully understand the last few paragraphs, you have to grasp that children of an object are
assignment-compatible with their parents. In other words, given the declarations you saw in
the OBJECT1 program, the following is legal:
var
Parent: TObject;
Child: TMyObject;
begin
Parent := Child;
end;
You can’t set a child equal to a parent because the child is larger than its parent—that is, it has
more methods or fields—and therefore all its fields and methods will not be filled out. All other
things being equal, you can build a two-story building out of the pieces meant for a three-story
building; but you can’t build a three-story building out of the pieces meant for a two-story
building!
Consider the following hierarchy:
type
TParent = class(TObject)
procedure Draw; virtual;
end;
TChild = class(TParent)
procedure Draw; override;
procedure ShowHierarchy; virtual;
end;
The issue here is that setting child equal to a parent is not safe:
Child := Parent; // Don’t do this!
In this hypothetical world, the call might compile, but it would fail at runtime because Parent
has no ShowHierarchy method; therefore, it could not provide a valid address for the function
at the time of the assignment operation. I will return to this subject in the next section of this
chapter.
If you set a parent equal to child, all the features of the parent will be filled out properly:
Parent := Child;
That is, all the functions of TParent are part of TChild, so you can assign one to the other with-
out fear of something going wrong. The methods that are not part of TParent are ignored.
NOTE
When you are thinking about this stuff, you need to be sure you are reading statements
about assigning parents to children correctly. Even if I manage to straighten my
grammar out, there is still nothing in the English language that makes it totally clear
which item in an assignment statement is on the left and which is on the right. I could
use the terms lvalue and rvalue in this case, except that they don’t quite fit the case
either. However, if you take this as an analogy, you can consider a child to be an rvalue
and a parent to be an lvalue. You can set a parent equal to a child:
Parent := Child;
You literally can’t do this. It’s a type mismatch. The compiler replies “Incompatible
types.” In this sense, Child becomes like an rvalue in this one case. Even though it’s
ultimately possible to assign values to Child, you can’t assign a Parent to it. In this one
case, it might as well be an rvalue!
StartTable StartTable
Draw ------------------------------ Draw
EndTable ShowHierarchy
EndTable
This shows a parent being set equal to a child. As you can see, the address of the Draw method
for the parent is assigned to the address for the Draw method for the child. There is no
ShowHierarchy method in the parent, so it is ignored.
Here’s what happens if you try to set the child equal to the parent:
Simplified VMT for Child Simplified VMT for Parent
StartTable StartTable
Draw ------------------------------ Draw
ShowHierarchy -------------------- ????
EndTable EndTable
As you can clearly see, there is no method pointer in the parent table that can be assigned to
the ShowHierarchy method of the child table. Therefore it is left blank, which means a call to
the ShowHierarchy method of the child almost certainly fails. Therefore it is illegal to assign a
parent to a child. You can, however, assign a child to a parent, and it’s this assignment that lies
at the heart of polymorphism! For the sake of clarity, let me spell it out. Here is the legal as-
signment:
Parent := Child;
The issue, of course, is that by passing Parent to FreeAllClasses, you are, in effect, asking Delphi
to make the following assignment:
Child := Parent;
Delphi is kind enough to tell you not to do this. It’s bad practice.
Every object in the VCL can use polymorphism to some degree, because they all inherit meth-
ods from TObject. In particular, they all inherit a virtual Destroy method, as shown in this excerpt
from SYSTEM.PAS:
TObject = class
constructor Create;
procedure Free;
…
destructor Destroy; virtual;
end;
The VCL frequently iterates through a series of objects, calling Destroy on each in turn. Some
use the Destroy method inherited from TObject; others override TObject.Destroy, and thereby
implement the “highest form” of polymorphic behavior.
NOTE
Notice that the Free method for TObject is not declared as virtual. That’s because there
is never any need to override this method. Free just checks to see if Self is equal to nil,
and if it’s not, Free calls Self.Destroy. You can’t improve on the method, and you
shouldn’t try. So it’s not virtual. You may, however, need to destroy some of the
objects which you create inside one of your own objects. You will need, therefore, to
override Destroy, and so it is declared as virtual.
To help illustrate this point, you can use this simple example to step through in the debugger.
In particular, you could create an example in which you implement the Draw method of a TChild
and TParent object differently:
procedure TParent.Draw;
begin
ShowMessage(‘Parent Draw’);
end;
procedure TChild.Draw;
begin
ShowMessage(‘Child Draw’);
end;
You could then instantiate objects of both types and pass their parameters to a CallDraw method:
procedure CallDraw(O: TParent);
begin
O.Draw;
end;
The CallDraw method works with a variable of type TParent. It has only one kind of object in
it. It works only with variables of type TParent. And yet if you pass both a parent object and a
child object into it, one instance will call the TChild’s Draw method and the other will call the
TParent’s Draw method. This is polymorphism in action. The concept here is so important that
I have placed this example on disk, and show it below in Listing 25.1.
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
bPolymorph: TButton;
procedure bPolymorphClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
type
TParent = class(TObject)
procedure Draw; virtual;
end;
TChild = class(TParent)
procedure Draw; override;
procedure ShowHierarchy; virtual;
end;
procedure TParent.Draw;
begin
ShowMessage(‘Parent Draw’);
end;
procedure TChild.Draw;
begin
ShowMessage(‘Child Draw’);
end;
procedure TChild.ShowHierarchy;
begin
ShowMessage(‘Child Show Hierarchy’);
end;
Parent: TParent;
begin
Child := TChild.Create;
Parent := TParent.Create;
CallDraw(Parent);
CallDraw(Child);
Child.Free;
Parent.Free;
end;
If you have any questions about what’s going on in this code, run this program. Step through
it with the debugger, if necessary, until it starts to make sense to you. The key point is that one
variable, called O, which is of type TParent, behaves in two different ways. It’s polymorphic.
One variable acting in two different ways!
Parent := Child;
Clearly this is an array of objects of type TField. Suppose, however, that you execute the fol-
lowing method in a simple program containing a TTable object that’s pointed at the Biolife
table:
procedure TForm1.bViewFieldTypesClick(Sender: TObject);
var
i : Integer;
begin
for i := 0 to Table1.FieldCount - 1 do
ListBox1.Items.Add(Table1.Fields[i].ClassName);
end;
According to the declaration of the Fields object, the type of each of the members of the Fields
array should be TField. However, here is what actually gets printed in the program’s list box:
TFloatField
TStringField
TStringField
TStringField
TStringField
TFloatField
TFloatField
TMemoField
TGraphicField
Clearly this isn’t really an array of TField objects at all. So what’s going on, anyway?
Well, by now the answer should be clear. Somewhere internally, Delphi is assigning
TStringField, TFloatField, TMemoField, and TGraphicField objects to the members of the TField
array. Why is this legal? Because it’s always legal to set a parent object equal to a child object!
In essence, this is what happens:
var
Field: TField;
StringField: TStringField;
begin
Field := StringField; // This is legal!
end;
Given this hierarchy, it’s always legal to assign a TStringField or TFloatField object to a vari-
able of type TField:
var
Fields: array [1..3] of TField;
StringField: TStringField;
FloatField: TFloatField;
GraphicField: TGraphicField;
begin
Fields[1] := StringField; // legal!
Fields[2] := FloatField; // legal!
Fields[3] := GraphicField; // legal!
end;
This is such an important point that I’m going to include the source code for a program, called
POLYFLDS.DPR, which demonstrates the issue discussed in this section. Listing 25.2 con-
tains the source for this program.
Listing 25.2. The PolyFlds program shows how Delphi makes practical use of
polymorphism.
unit Main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
DB, Grids, DBGrids,
DBTables;
type
TForm1 = class(TForm)
Table1: TTable;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
bViewFieldTypes: TButton;
ListBox1: TListBox;
procedure bViewFieldTypesClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
This example program makes it clear how important polymorphism is to Delphi. The Fields
array of TTable is one of the most commonly used elements in all of Delphi. And what lies at
the heart of the whole thing? Polymorphism!
Another important point to grasp here is that there is very little about Delphi that is mysteri-
ous. If you work with Visual Basic or PowerBuilder, there are lots of elements of the code or
environment where the only explanation for why they work a particular way is “because.” How
come? “Because!” With Delphi, it’s never like that. There is an explanation for everything in
Delphi, and there is no part of the VCL or the environment that you cannot build yourself
using Delphi.
Delphi is built in Delphi. There are no mysteries here. If you know the language well enough,
even the most esoteric parts of the VCL will make sense to you. How can the Fields array be
so powerful? How does it do those things? Well, now you know the answer: It’s polymorphic!
the parent can’t call methods of the child that are not also visible in the parent’s class
declaration. In short, a variable of type TParent can never call the ShowHierarchy
method even if the TParent object has been assigned to a variable of type TChild.
The point is that you can take a whole slew of hierarchically arranged objects, assign each of
them to their parent, call a virtual method belonging to the parent, and watch them all behave
in different ways. Whee! Polymorphism!
For some readers, I’m sure this is old hat. Other readers might be new to the subject, but have
grasped it completely from the descriptions given already. However, most readers probably still
have some questions lingering in the back of their minds.
If you want, just reread the sections above as often as necessary. I know from personal experi-
ence that as many as three out of four object-oriented programmers don’t really understand
polymorphism. The subject, however, is not that complicated. Concentrate on these two
sentences:
Polymorphism allows you to set one variable of type
TParent equal to a series of child objects. When
you call certain virtual methods of that parent it will behave in different ways, depending on the
traits of the currently selected child.
It’s a mouthful, but it’s not impossible to comprehend.
I will now move on to some concrete examples that should shed more light on the issue. In
particular, I am going to develop the OBJECT4 program, which builds on the code presented
in the last two chapters, but finally brings it to some sort of useful fulfillment. The OBJECT4
program is quite long, so I will present it in its own section.
OBJECT4
As described at the beginning of this chapter, OBJECT4 is a warehouse simulation that fea-
tures a series of panels arranged in a large room. Each panel has from 4 to 12 palettes on it, and
each palette contains a certain number of widgets. Users can stock additional widgets on the
palettes, or they can sell widgets. If a large number of widgets are sold, the program is smart
enough to iterate through the palettes and empty them one by one.
NOTE
The OBJECT4 program is stocked with hypothetical widgets named after colors: TYellow, TBlue,
TGreen, and TViolet. A real-world application might have TChairs , TTables , TBureaus, and so
on, instead of colors. Or, a bit more to the point, a real-world program might have TStringField,
TFloatField , and TGraphicField . The issue is not the names or traits of the individual widgets,
but the fact that a series of different TWidget descendants needs to be created.
You will find that TYellow, TBlue, and the other colors share many traits. This is a somewhat
artificial aspect of this program; in a real-world program, each of these objects would be more
varied. For instance, a TChair would have an FLegs data store, a TBed would have an FFrame
data store, and so on.
NOTE
When studying OBJECT4, you will find that the TWidget object has changed slightly
from its appearance in OBJECT3. These types of minor structural changes should be
expected in OOP. Developers don’t start with a base class and build up a hierarchy
without ever deciding that changes need to be made to the structures they are creating.
Frameworks and hierarchies evolve over time; they do not burst into the world fully
formed.
If you are building a certain kind of tool, after the first release, it is very dangerous to
go back and start changing the way base classes work. Nevertheless, during develop-
ment, changes need to be made to the base classes in your hierarchy. This is one of the
major “gotchas” of object-oriented programming, and I would be remiss if I didn’t lay
it out in clear terms. Right now, there are no good remedies for this problem, but the
lesson it teaches is clear: Don’t release objects to unsuspecting programmers until you are
sure you have the proper design! If you are looking for some relief, my experience has
shown that most major companies don’t think beta testers fit the definition of “unsus-
pecting.”
This is probably also an interesting time to consider the object model used in COM.
The Component Object Model does not directly support inheritance. Instead, they
say you should rewrite the interface to an object entirely if you want to create a
descendant; that is, if you want to add features to it.
The OBJECT4 program features five forms. The first depicts the floor of the warehouse. (See
Figure 25.1.) The second depicts a report on the state of the widgets found on a particular
palette. (See Figure 25.2.) The third enables you to specify how many objects of each type you
want to sell. (See Figure 25.3.) The fourth depicts the status of the entire warehouse, using
graphs to display the number and type of each object found in the warehouse. (See Figure 25.4.)
The last form shows the hierarchy of a TWidget. (See Figure 25.5.)
FIGURE 25.1.
The Menagerie form shows
the floor of the warehouse.
FIGURE 25.2.
The Report View form
shows the status of the
objects on an individual
palette.
FIGURE 25.3.
The Sell dialog enables the
user to specify how many
widgets of each type are
being sold during a
transaction.
FIGURE 25.4.
The Status form shows how
many of each type of widget
are currently available in
the warehouse.
The OBJECT4 program (shown in Listings 25.3 through 25.9) is fairly long on code, but once
again you will find that many of the forms are relatively trivial, and that the core of the pro-
gram can be found in a few, relatively simple lines of code.
FIGURE 25.5.
The Hierarchy dialog
displays the hierarchy of a
TWidget.
Listing 25.3. The project file for the OBJECT4 program is listed here; the rest of
the program is found on the CD.
program Object4;
uses
Forms,
Main in ‘MAIN.PAS’ {Menagerie},
Classdef in ‘CLASSDEF.PAS’,
Reports in ‘REPORTS.PAS’ {Report},
Status in ‘STATUS.PAS’ {StatusForm},
Selldlgs in ‘SELLDLGS.PAS’ {SellDlg};
{$R *.RES}
begin
Application.CreateForm(TMenagerie, Menagerie);
Application.CreateForm(TReport, Report);
Application.CreateForm(TStatusForm, StatusForm);
Application.CreateForm(TSellDlg, SellDlg);
Application.Run;
end.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ClassDef, ExtCtrls,
Menus, Buttons, SellDlgs;
type
TMenagerie = class(TForm)
PopupMenu1: TPopupMenu;
Blue1: TMenuItem;
Yellow1: TMenuItem;
Timer1: TTimer;
Green1: TMenuItem;
Violet1: TMenuItem;
MainMenu1: TMainMenu;
Sell1: TMenuItem;
List: TMenuItem;
Panel1: TPanel;
Panel2: TPanel;
Panel3: TPanel;
Panel4: TPanel;
Panel5: TPanel;
Panel6: TPanel;
sp41: TSpeedButton;
Sp42: TSpeedButton;
Sp43: TSpeedButton;
Sp44: TSpeedButton;
Sp45: TSpeedButton;
Sp46: TSpeedButton;
Sp47: TSpeedButton;
Sp48: TSpeedButton;
Sp49: TSpeedButton;
Sp410: TSpeedButton;
SpeedButton12: TSpeedButton;
SpeedButton13: TSpeedButton;
SpeedButton14: TSpeedButton;
SpeedButton15: TSpeedButton;
SpeedButton16: TSpeedButton;
SpeedButton17: TSpeedButton;
SpeedButton18: TSpeedButton;
SpeedButton19: TSpeedButton;
SpeedButton20: TSpeedButton;
SpeedButton21: TSpeedButton;
SpeedButton22: TSpeedButton;
continues
var
Menagerie: TMenagerie;
implementation
{$R *.DFM}
uses
Status;
begin
FCurSp := TSpeedButton(Sender);
P := Point(FCurSp.Left, FCurSp.Top);
WinProcs.ClientToScreen(FCurSp.Parent.Handle, P);
PopUpMenu1.Popup(P.X, P.Y);
end;
procedure TMenagerie.Timer1Timer;
var
i: Integer;
A: TWidget;
begin
for i := 0 to ComponentCount - 1 do
if Components[i] is TWidget then begin
A := TWidget(Components[i]);
if A.Quantity <= 0 then begin
A.Twin.Enabled := True;
A.Twin.Visible := True;
A.Free;
Exit;
end;
end;
end;
continues
end.
Listing 25.5. The CLASSDEF unit, which is possibly the most important module in
the OBJECT4 program.
unit Classdef;
interface
uses
Classes, Controls, Graphics,
Dialogs, ExtCtrls, Buttons;
const
idBlue = 100;
idYellow = 101;
idGreen = 102;
idViolet = 103;
type
THMethod = (hmScreen, hmDisk);
TMyObject = class(TImage)
public
procedure ShowHierarchy; virtual;
end;
THierarchy = class(TMyObject)
private
FHierarchyMethod: THMethod;
procedure SetHierarchy(H: THMethod);
public
procedure ShowHierarchy; override;
published
property HierarchyMethod: THMethod read FHierarchyMethod write SetHierarchy;
end;
TWidget = class(THierarchy)
private
FQuantity: LongInt;
FBoxSize: LongInt;
FMaxQuantity: LongInt;
FDescription: string;
FTwin: TSpeedButton;
protected
procedure MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
public
constructor Create(AOwner: TComponent); override;
procedure Sell(Amount: LongInt); virtual;
procedure Stock; virtual;
function GetName: string; virtual;
published
property Description: string read FDescription;
property Quantity: LongInt read FQuantity write FQuantity;
property Twin: TSpeedButton read FTwin write FTwin;
property BoxSize: LongInt read FBoxSize write FBoxSize;
property OnMouseDown;
end;
TBlue = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
continues
TGreen = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
TViolet = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
implementation
uses
HierDlg,
Reports,
StrBox;
var
FActive: Boolean;
{ -- TMyObject -- }
procedure TMyObject.ShowHierarchy;
const
CR = #13#10;
var
AClass: TClass;
S: string;
begin
S := ClassName;
AClass := ClassParent;
while AClass <> nil do begin
S := S + CR + AClass.ClassName;
AClass := AClass.ClassParent;
end;
MessageDlg(S, mtInformation, [mbOk], 0);
end;
{ -- THierarchy -- }
procedure THierarchy.ShowHierarchy;
var
F: System.Text;
AClass: TClass;
begin
if FHierarchyMethod = hmscreen then
inherited ShowHierarchy
else begin
System.Assign(F, ‘inherit.txt’);
ReWrite(F);
WriteLn(F, ClassName);
AClass := ClassParent;
while AClass <> nil do begin
WriteLn(F, AClass.ClassName);
AClass := AClass.ClassParent;
end;
Close(F);
end;
end;
{ -- TWidget -- }
procedure TWidget.Stock;
begin
FQuantity := FQuantity + FBoxSize;
end;
{ -- TBlue -- }
continues
procedure TBlue.ShowHierarchy;
begin
if FHierarchyMethod = hmDisk then
inherited ShowHierarchy
else
HierarchyDlg.Run(Self, clBlue);
end;
{ -- TYellow -- }
procedure TYellow.ShowHierarchy;
begin
if FHierarchyMethod = hmDisk then
inherited ShowHierarchy
else
HierarchyDlg.Run(Self, clYellow);
end;
{ -- TGreen -- }
procedure TGreen.ShowHierarchy;
begin
if FHierarchyMethod = hmDisk then
inherited ShowHierarchy
else
HierarchyDlg.Run(Self, clGreen);
end;
{ -- TViolet -- }
procedure TViolet.ShowHierarchy;
begin
if FHierarchyMethod = hmDisk then
inherited ShowHierarchy
else
HierarchyDlg.Run(Self, clPurple);
end;
end.
Listing 25.6. The REPORTS unit, which describes the state of an individual widget.
unit Reports;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, ClassDef, Buttons;
type
TReport = class(TForm)
Panel1: TPanel;
LName: TLabel;
Panel2: TPanel;
LPanel: TLabel;
LPalette: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
LDescription: TLabel;
Label5: TLabel;
LQuantity: TLabel;
BitBtn1: TBitBtn;
StockItems: TBitBtn;
BitBtn2: TBitBtn;
procedure BitBtn1Click(Sender: TObject);
continues
var
Report: TReport;
implementation
{$R *.DFM}
procedure TReport.ShowData;
begin
LName.Caption := FWidget.GetName;
LPalette.Caption := IntToStr(FWidget.Twin.Tag);
LPanel.Caption := TPanel(FWidget.Parent).Name;
LDescription.Caption := FWidget.Description;
LQuantity.Caption := IntToStr(FWidget.Quantity);
end;
end.
Listing 25.7. The SELLDLGS unit, which enables the user to report the amount of
a particular sale.
unit Selldlgs;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
Buttons, StdCtrls, ExtCtrls,
Mask, SysUtils;
type
TDataRec = record
BlueTotal: LongInt;
YellowTotal: LongInt;
GreenTotal: LongInt;
VioletTotal: LongInt;
end;
TSellDlg = class(TForm)
OKBtn: TBitBtn;
CancelBtn: TBitBtn;
HelpBtn: TBitBtn;
Bevel1: TBevel;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
MaskEdit1: TMaskEdit;
MaskEdit2: TMaskEdit;
MaskEdit3: TMaskEdit;
MaskEdit4: TMaskEdit;
procedure FormActivate(Sender: TObject);
public
function GetData(var DataRec: TDataRec): Boolean;
end;
var
SellDlg: TSellDlg;
implementation
uses
StrBox;
{$R *.DFM}
continues
end.
Listing 25.8. The STATUS dialog, which displays graphs depicting the current
supplies of each widget.
unit Status;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Gauges, ExtCtrls,
Buttons, ClassDef;
const
MaxBlue = 10600;
MaxYellow = 120;
MaxGreen = 6000;
MaxViolet = 6000;
type
TStatusForm = class(TForm)
Panel1: TPanel;
gBlue: TGauge;
gYellow: TGauge;
gGreen: TGauge;
gViolet: TGauge;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
BitBtn1: TBitBtn;
LBlue: TLabel;
LYellow: TLabel;
LGreen: TLabel;
LViolet: TLabel;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
FBlueTotal: LongInt;
FYellowTotal: LongInt;
FGreenTotal: LongInt;
FVioletTotal: LongInt;
procedure ZeroTotals;
procedure TotalWidgets(Widget: TWidget);
public
{ Public declarations }
procedure RunAll(Components: TList);
procedure CalcTotals(Components: TList);
end;
var
StatusForm: TStatusForm;
implementation
{$R *.DFM}
procedure TStatusForm.ZeroTotals;
begin
FYellowTotal := 0;
FBlueTotal := 0;
FGreenTotal := 0;
FVioletTotal := 0;
end;
continues
end.
Listing 25.9. The Hierarchy dialog, which shows the hierarchy of TWidget.
unit Hierdlg;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Buttons, ClassDef;
type
THierarchyDlg = class(TForm)
ListBox1: TListBox;
BitBtn1: TBitBtn;
private
{ Private declarations }
public
{ Public declarations }
var
HierarchyDlg: THierarchyDlg;
implementation
{$R *.DFM}
end.
After starting the program, you can click any of the palettes shown in the warehouse. A menu
appears asking you to specify the type of widget you want to place on the palette. You can choose
to display a palette of Yellow, Blue, Green, or Violet widgets. After placing a few sets of wid-
gets in the warehouse, you can click the List menu item to see the status of the entire ware-
house. You can also click any individual palette that contains widgets to see a report on the
type and number of widgets stored there. From inside the TReport form, you can add addi-
tional boxes of widgets to a palette. The final option in the program is the Sell menu item,
which brings up a form called Sell Dialog that allows you to sell widgets. If you sell more than
a full palette of widgets, a palette is emptied out and automatically deleted. If you sell less than
a palette of widgets, the correct number of widgets is subtracted from one of the palettes. The
program does no error checking in case you sell more widgets than there are in stock. Adding
this feature, as the academicians would say, is left as an exercise for the reader!
NOTE
A key design issue in this program involves the ability to add new types at runtime.
That is, the program supports TGreen, TBlue, TViolet, and TYellow widgets. Why can’t
you add a TRed widget at runtime? It might seem as if OBJECT4 is limited because
you cannot dynamically add TPurple or TRed widgets at runtime, but must instead go
back to the source to add this feature. Such a complaint stems from taking the program
too literally.
For instance, you would not expect to be able to create new types of TField descen-
dants dynamically at runtime. You could create many instances of a TStringField
object, but you would not expect to be able to create a new type of TField descendant
such as TAviField.
In a real-world program, you might have a TColoredWidget object and would dynami-
cally add or delete colors at runtime by changing the widget’s Color property. Here,
I’m using colors as a metaphor for any kind of widget, where each widget is meant to
contain a unique set of traits representative of a larger class. For instance, a program-
mer might have objects such as TChair, TBed, TTable, and so on. The program could
then enable the user, at runtime, to add or delete various types of chairs such as
Chippendale chairs, art deco chairs, and so on.
Major classes such as beds and chairs need to be differentiated as distinct objects,
because beds will have FFrame and FMattress fields, while chairs will not have these
fields, but FLegs, FSeat, and FBack fields instead. In short, this type of program does
not allow the creation of a new class of objects at runtime, but it will enable you to add
new types of a particular class. Deciding which types of objects need to be created at
runtime is a major design issue worthy of serious consideration.
Before I begin talking about the code in this program, I should point out that I am not going
to go through it in a line-by-line analysis. The program is too long for that kind of approach.
At times, you may have to step through the code before you understand exactly how certain
features are implemented, but that kind of activity can have unexpected benefits. In general,
I’ll assume that you know how to use the debugger to figure out the code in this program, so
I will concentrate on discussing polymorphism.
The most important single fragment of code in the OBJECT4 program contains the declara-
tions for TYellow, TBlue, and the other widgets:
TBlue = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
TYellow = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
TGreen = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
TViolet = class(TWidget)
public
constructor Create(AOwner: TComponent); override;
procedure ShowHierarchy; override;
end;
Notice that these declarations are fairly simple. You are merely redefining some fundamental
traits of each object.
NOTE
If you have the Delphi source code, you might want to compare these declarations to
the declarations for TStringField, TFloatField, and so on. These declarations are
found in DBTABLES.PAS. Here, for instance, is the declaration for TWordField:
TWordField = class(TIntegerField)
public
constructor Create(AOwner: TComponent); override;
end;
If you compare several of these Create methods, you see that certain fields of each object are
assigned values that distinguish it from its peers.
The ShowHierarchy method is also straightforward.
procedure TBlue.ShowHierarchy;
begin
if FHierarchyMethod = hmDisk then
inherited ShowHierarchy
else
HierarchyDlg.Run(Self, clBlue);
end;
The Hierarchy dialog (shown in Figure 25.5) is a simple form containing a list box and a but-
ton. The list box displays the members of the hierarchy being shown. The key point here,
however, is that each object differs slightly from the last, because the type of variable repre-
sented by the word Self passes a different color to HierarchyDlg.Run.
If you look at the form for TMenagerie, you see that the palettes laid out on the panels are really
instances of TSpeedButton. If the user clicks on one of these palettes and asks to stock it with
blue widgets, the following code is executed:
procedure TMenagerie.WidgetClick(Sender: TObject);
var
WidgetType: Integer;
Widget: TWidget;
begin
WidgetType := (Sender as TMenuItem).Tag;
case WidgetType of
idBlue: Widget := TBlue.Create(Self);
idYellow: Widget := TYellow.Create(Self);
idGreen: Widget := TGreen.Create(Self);
idViolet: Widget := TViolet.Create(Self);
end;
FCurSp.Enabled := False;
FCurSp.Visible := False;
Widget.Parent := FCurSp.Parent;
Widget.Left := FCurSp.Left;
Widget.Top := FCurSp.Top;
Widget.Twin := FCurSp;
Widget.Show;
end;
FCurSp is a pointer to the instance of TSpeedButton that the user selected. The assignment of
the correct speedbutton to FCurSp occurs in a method called sp41Click:
procedure TMenagerie.sp41Click(Sender: TObject);
var
P: TPoint;
begin
FCurSp := TSpeedButton(Sender);
P := Point(FCurSp.Left, FCurSp.Top);
WinProcs.ClientToScreen(FCurSp.Parent.Handle, P);
PopUpMenu1.Popup(P.X, P.Y);
end;
WidgetClick uses polymorphism. Specifically, it sets a parent class equal to a child class:
Widget := TBlue.Create(Self);
The point is that the parent object, TWidget, has Parent, Top, Left, Twin, and Show fields; so you
can safely make these assignments regardless of whether the actual widget in question is of type
TYellow, TBlue , TGreen, or TViolet. This is polymorphism in action.
NOTE
After reading the last few paragraphs, the core functionality of the OBJECT4 program should
be clear to you. If it’s not, try stepping through the program with the debugger. From now on,
I will concentrate solely on explaining how the program implements various forms of poly-
morphism.
This is a method of TWidget, so all the TWidget descendants inherit it. As you can see, it calls
Report.Run and passes itself as the parameter. Of course, each instance that is passed in is actu-
ally a reference to a variable of type TBlue, TYellow, TGreen, or TViolet. Once again, it is poly-
morphism that makes it possible to pass in objects of different types and assign them to a single
variable of their ancestor’s type:
procedure TReport.Run(Widget: TWidget);
begin
FWidget := Widget;
ShowData;
ShowModal;
end;
Here, the variable called Widget is set equal to one of TWidget’s descendants. The assignment
occurs when a variable is passed as a parameter to the Run method. This is not what is tradition-
ally thought of as an assignment, but in the end a variable of type TWidget is set equal to an
object of type TBlue, TYellow, TGreen, or TViolet.
The ShowData method makes it clear that this technique is working:
procedure TReport.ShowData;
begin
LName.Caption := FWidget.GetName;
LPalette.Caption := IntToStr(FWidget.Twin.Tag);
LPanel.Caption := TPanel(FWidget.Parent).Name;
LDescription.Caption := FWidget.Description;
LQuantity.Caption := IntToStr(FWidget.Quantity);
end;
In this example, calling FWidget.GetName resolves one way for a class of type TViolet, and an-
other way for a class of type TBlue. In the first case, it retrieves the string ‘Violet’, and in the
second case, it retrieves the string ‘ B l u e ’ . The same principle applies to calls to
FWidget.Description , except that this time a property is called rather than a method. Each of
these calls is an example of polymorphism in action.
I said earlier that the highest form of polymorphism occurs when there are virtual methods
involved. If you click the Hierarchy button in the Report dialog, a call to FWidget.ShowHierarchy
is executed:
procedure TReport.BitBtn2Click(Sender: TObject);
begin
FWidget.ShowHierarchy;
ModalResult := mrNone;
end;
If you are having trouble understanding all this, you should fire up Delphi and step through
the code with the debugger. You can’t just read about this kind of stuff; you have to make it
happen yourself.
NOTE
This is definitely an excellent time to try to create your own objects and see whether
you can get some polymorphism working. Create virtual methods that are resolved
differently for different objects in your program. Get this subject down straight, and
you can call yourself an expert in object-oriented programming!
As I said earlier, there are many examples of polymorphism in the OBJECT4 program. If you
choose to explore it further, you will find examples scattered throughout every unit, and usu-
ally signaled by the declaration of a variable of type TWidget. These variables will, of course, be
set equal to a child object. Every time this assignment occurs, polymorphism is taking place.
But, as I said earlier, the purest — or at least the most interesting — forms of polymorphism
occur when virtual methods are also added into the mix.
FIGURE 25.6.
The property editor for the
EditMask property.
The purpose of this string is to ensure that the user can enter only numeric values. In particu-
lar, these numeric values represent the number of widgets that the user wants to sell.
To understand the EditMask property, you need to study the entry associated with it in the
online help. Here, you see all the characters that can be used to define a mask, along with a
description of the effects associated with each character. Because this list is so readily available
to you, I will not waste time or resources by reprinting it here. Instead, I will show how to use
the information in the online help to achieve a specific purpose.
In the online help for the EditMask property, you will find that a pound sign (#) enables you to
enter only numbers, spaces, and plus and minus characters. Notice, however, that there is also
a 0 character, which requires that you enter a number in each location marked with that char-
acter. The question, then, is which special character should you use: # or 0?
In this particular case, the 0 character is not what you want, because it forces the user to enter
a six-digit number, one for each of the 0s you place in the mask:
000000;0;_
The preceding mask would enable you to enter the number 123456, but it would reject 123.
Because you want to give the user the freedom to enter a number of any size up to 999,999, you
should use the # character rather than the 0 character. One way to sum this matter up is as
follows: The 0 character means that the user has to fill in that place with a number, while the
# character allows the user to fill in that space with either a number or a blank space.
The zero that appears after the semicolon in the previous string specifies that the mask entered
is not saved as part of the data. If I had placed a 1 here, the mask would have been saved. In this
particular case, there are no mask characters to either save or discard, so it doesn’t matter what
I placed in this location. The phone mask (shown in Figure 25.6) contains two parentheses
that would be affected by this value.
Finally, the very end of the previous mask includes an underscore. This value is used to specify
what will be used to designate the space character.
Notice that each field of the mask is separated from the last by a semicolon. Each mask consists
of three fields, which were described earlier.
I am not completely enamored of this system, nor of the TEditMask component itself. Perhaps
as I get better at using it, I will find that it has hidden strengths not yet evident to me. Only
time will tell. But for now, I would say that this portion of your program is one you might
want to entrust to a third party such as TurboPower Software. Or, perhaps you would like to
create your own TMaskEdit component and place it on the market.
If you really want to feel at home with this baby, look at the method addresses of an object of
type TChild, assign it to an object of type TParent:
Parent := Child;
you see that it has the same address for the Foo method as the child object did. That is why the
parent starts to act as if it were a child object! When stated so boldly, the whole idea of poly-
morphism suddenly becomes transparent. Of course it works this way! How else could it work?
The form for the HEXADDRESS program, shown in Figure 25.7, contains three edit con-
trols, one list box, two labels, and one button. The edit controls are used only for displaying
data, and the user never needs to enter anything in them. I use them instead of labels just be-
cause I like the way they set the information off on the screen.
FIGURE 25.7.
The HEXADDRESS
program shows that if you
set a parent object equal to
a child object, the two
objects’ methods will have
the same addresses.
The complete code for the HEXADDRESS program is shown in Listing 25.10.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
PressMe: TButton;
Edit1: TEdit;
Edit2: TEdit;
ListBox1: TListBox;
Edit3: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
procedure PressMeClick(Sender: TObject);
end;
TParent = class(TEdit)
published
procedure Foo; virtual;
end;
TChild = class(TParent)
published
procedure Foo; override;
end;
var
Form1: TForm1;
implementation
uses
StrBox;
{$R *.DFM}
procedure TParent.Foo;
begin
Form1.ListBox1.Items.Add(‘ParentFoo called’);
end;
procedure TChild.Foo;
begin
Form1.ListBox1.Items.Add(‘ChildFoo called’);
end;
end.
The HEXADDRESS program calls a routine found in the STRBOX unit that ships with this book.
The Address2Str method converts a pointer address into a string:
function Address2Str(Addr: Pointer): string;
begin
Result := Format(‘%p’, [Addr]);
end;
The HEXADDRESS program reduces polymorphism to a principle so obvious that one feels
foolish repeating it. Specifically, if you assign one object to another, of course their methods
are going to have the same addresses! This is true even if one object is the parent of the other.
The only tricky or unexpected part is the fact that Delphi enables you to set a parent equal to
one of its children:
Parent := Child;
Summary
This chapter has tackled the complex subject of polymorphism. You saw that polymorphism is
built on the fact that OOP languages enable you to assign a variable of type child to a variable
declared to be of type parent. When you then call a method of the parent type, it will go to the
address of the child object’s methods. As a result, an object of type TParent, when assigned to
four different objects of type TChild, might react in four different ways. One object, many
different faces: polymorphism.
Creating Components
26
This chapter and the next cover building components. Components are one of the most im-
portant developments in contemporary programming, and there is not an environment on the
market that makes them easier to use or to create than Delphi.
You build three types of components in this chapter:
■ Descendants that change default settings in existing components.
■ Descendants that add features to existing components.
■ Tools built on top of abstract component base classes such as TWinControl,
TCustomControl , TComponent, and TGraphicControl . These are the classes you descend
from if you want to build your own components from the bottom up.
This chapter presents material on building visual components, and the next chapter explores
building nonvisual components. Nonvisual components descend directly from TComponent and
appear on the form only at design-time.
More specifically, the components built in this chapter fall into two categories:
■ The first group is a set of TEdit, TLabel, and TPanel descendants that show how to
change default colors, captions, and fonts. This section of the chapter also covers
building components that consist of several different child components; that is, it
shows how to group components together to form new components. The specific
example included with this book shows a panel that comes with two radio buttons.
■ The second tool is a clock component that can be dropped on a form, and stopped
and started at will.
In the next chapter, you will see a nonvisual control that knows how to iterate through
subdirectories. You can use it to build programs that search for files, delete all files with a cer-
tain extension, and so on.
Besides components, this chapter also briefly covers two related topics:
Property editorsare used to edit the properties of components. The classic examples are
the common dialogs that pop up when you edit the Color or Font properties that
belong to most visible components. The drop-down lists and string editing capabilities
found in the Object Inspector are also property editors.
Component editorsare associated not with a single property, but with an entire
component. An example is the Fields Editor used with TTable and TQuery compo-
nents.
The property editors and component editors are related to a broader topic called the Tools
API. The Tools API consists of a series of interfaces to the Delphi IDE that allow you to build
experts, interfaces to version control systems, and similar utilities. The API for property
editors, component editors, and the Tools API are defined in files with names that end in INTF,
which stands for Interface. For instance, the TPropertyEditor class is found in DSGNINTF.PAS.
The Tools API will be covered in a later chapter.
Components, component editors, and property editors are perhaps the most important topics
in Delphi programming. In many ways, this chapter and the next are the keystones around
which this entire book has been designed. Most of what I have said before was calculated to
make these two chapters readily comprehensible.
Component Theory
Delphi components have three outstanding strengths:
■ They are native components, built in Delphi’s language. This means that you can
write, debug, and test your components from inside standard Delphi programs. In
short, it’s about ten times easier to write a Delphi component as it is to write an OCX
or VBX.
■ They are fully object-oriented, which means you can easily change or enhance existing
components by creating descendant objects.
■ They are small, fast, and light, and can be linked directly into your executables.
Few people would claim that VBXs weren’t groundbreaking, that OCX’s aren’t going to be
very important, or that OLE2 is not an enormously promising architecture. However, Delphi
components are relatively easy to create and come in a light, easy-to-use package. You can cre-
ate Delphi components that do nearly anything, from serial communications, to database links,
to multimedia. This gives Delphi a big advantage over other visual tools that force you to move
to C++, or some other tool, if you want to build components.
NOTE
Most publicly available components cost in the range of $50 to $150. Many of these
tools encapsulate functionality that might cost tens of thousands of dollars to produce
in-house. For instance, a good communication library might take a year to build.
However, if a company can sell it in volume, they can afford to charge $100 or $200
for the same product. That’s a real bargain. And most of these tools are easy to use.
Building components is a great way for relatively small third-party companies to make
money, and buying components is a great way to save time on big projects. These are
ground breaking tools that are changing everything about the way programs are
constructed.
Delphi components are flexible tools easily built by anyone who knows OOP and the Delphi
language. In this package you have explanations detailing all the prerequisite knowledge
component builders need, from a description of Delphi itself, through a description of its lan-
guage, and on to an overview of its implementation of OOP. From this foundation, it will be
easy for you to begin building your own components.
Listing 26.1. The code for a simple component descending from TEdit.
unit Unleash1;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TSmallEdit = class(TEdit)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
published
{ Published declarations }
end;
procedure Register;
implementation
inherited Create(AOwner);
Color := clBlue;
Font.Color := clYellow;
Font.Name := ‘Times New Roman’;
Font.Size := 12;
Font.Style := [fsBold];
end;
procedure Register;
begin
RegisterComponents(‘Edits’, [TSmallEdit]);
end;
end.
Listing 26.2. The main form for the TestEds1 program serves as a test bed for the
UNLEASH1 unit.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
UnLeash1;
{$R *.DFM}
continues
end.
It’s simple to create this unit, test it, and compile it as a component that’s merged in with the
rest of the tools on the Component Palette. To get started, choose File | New and select Com-
ponent from the first page of the Object Repository. The dialog shown in Figure 26.1 appears.
FIGURE 26.1.
The Component Expert
dialog.
The Component Expert is a simple code generator of the type that any reader of this book who
has made it this far should be able to write in an hour or two. All it does is ask you for the name
of the component you want to create, and to then select its parent from a drop-down list. After
you have defined the type of tool you want to create, you can select the page in the Compo-
nent Palette where you want it to reside. You should fill in the blanks with the following infor-
mation:
Class Name: TSmallEdit
Ancestor: TEdit
Palette Page: Unleash
For your efforts, the Component Expert churns out the code in Listing 26.3, in which every-
thing is boilerplate except for the first line of the class declaration, the global var declaration,
and the parameters passed to the RegisterComponents method.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TSmallEdit = class(TEdit)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents(‘Unleash’, [TSmallEdit]);
end;
end.
The Component Expert starts by giving you a uses clause designed to cover most of the bases
you are likely to touch in a standard component:
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
The next step is to give you a basic class declaration, in which the name and parent are filled in
with the choices you specified in the Component Expert dialog. All this business about the
scoping directives is just for your convenience, and you can delete any portion of it that you
don’t think you’ll need.
type
TSmallEdit = class(TEdit)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
Before you can place a component on the Component Palette, you must first register it with
the system:
procedure Register;
begin
RegisterComponents(‘Unleash’, [TSmallEdit]);
end;
Registering a class makes it known to the Delphi Component Palette when the unit is com-
piled into the Delphi component library. The Register procedure has no impact on programs
compiled with this unit. Unless your program calls the Register procedure (which it should
never do), the code for the Register procedure will never even appear in your executable.
After using the Component Expert, you should save the project. Proceed as you normally would
by creating a directory for the project and saving MAIN.PAS and TESTEDS1.DPR inside it.
The new unit that you created, however, should not be saved into the same directory, but should
be placed in the directory where you store files such as STRBOX or MATHBOX. This code is
now going to come into play as part of your system, and as such you want a single path that
leads to all related files of this type. If you have all of your components in different subdirectories
you will end up with a source path that is long and unwieldy. Furthermore, it’s best not to
open up the Project Manager and make this class a part of your project. Instead, merely add it
to the uses clause of unit MAIN. If you add the class to your project, the path to it becomes hard-
coded into your DPR file, which may cause problems later on.
The goal of this project is to give a component of type TEdit a new set of default behaviors, so
that it starts out with certain colors and certain fonts. To do this, you need to override the
Create method and change the fonts inside of it. To declare the method, write the following in
your class declaration:
TSmallEdit = class(TEdit)
public
constructor Create(AOwner: TComponent); override;
end;
Notice that in the declaration above I have removed the private, published, and protected
directives created by the Component Expert. This is neither here nor there, and I do it just to
keep the amount of code you need to look at as small as possible.
The Create method for TSmallEdit is declared as public. If you think about the process of cre-
ating a component dynamically, you will see in a moment that the Create method has to be
public. This is one method that must be exposed.
Create is passed a single parameter of type TComponent, which is a base class that encapsulates
the minimum functionality needed to be an owner of another component. In particular, what-
ever form you place a component on usually will be the owner of that component. Finally, use
the override directive to specify that this is a virtual method that you want to redefine.
The implementation of the Create method is simple:
constructor TSmallEdit.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Color := clBlue;
Font.Color := clYellow;
Font.Name := ‘Times New Roman’;
Font.Size := 12;
Font.Style := [fsBold];
end;
The code first calls Create, passing in the variable AOwner. As stated above, the owner of a com-
ponent will often, though not always, be the form on which the component is to be displayed.
In other words, the user will drop the component onto a form, and that form will become the
owner of the component. In such a case, AOwner is a variable that points at the form. The VCL
uses it to initialize the Owner property, which is one of the fields of all components.
The next step is to define the color and font that you want to use, with Font.Style defined as
follows:
TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeOut);
TFontStyles = set of TFontStyle;
If you want to add the underline and bold style to the text in the edit control, write the follow-
ing:
Font.Style := [fsBold, fsUnderline];
in which the plus symbol (+) is used as a set operator for unions, as explained in Chapter 22,
“Handling Messages.”
At this stage, the code is ready to go on the Component Palette. However, most of the time
when you write components, you should test them first to see whether they work. The issue
here is that, even on a fast machine, it takes 20 or 30 seconds to recompile CMPLIB32.DCL
and add your component to the Component Palette. (Most of that time is taken up by Win-
dows unloading the old CMPLIB32.DCL and then loading the new CMPLIB32.DCL into
memory.) Perhaps I’m a bit impatient, but that’s a little too long a wait for me if all I need to
tweak is a few aspects of my code. As a result, I test things out first in a small program, and then
add the component to the IDE.
To test the new class, drop a button on the program’s main form, and create an OnClick han-
dler:
procedure TForm1.Button1Click(Sender: TObject);
var
MyEdit: TSmallEdit;
begin
MyEdit := TSmallEdit.Create(Self);
MyEdit.Parent := Self;
MyEdit.Show;
end;
This code creates the component and shows it on the main form. Self, of course, is the way
that TForm1 refers to itself from inside one of its own methods. The owner of the new compo-
nent is Form1, which will be responsible for disposing of the component when finished with it.
This happens automatically. You never need to worry about disposing of a visible component
shown on a form.
The parent of the component is also Form1. The Parent variable is used by Windows when it is
trying to decide how to display the form on the screen. If you place a panel on a form and drop
a button on the panel, the owner of that button is the form, but the parent is the panel. Own-
ership determines when and how the component is deallocated, and parental relationships
determine where and how the component is displayed. Ownership is fundamentally a Delphi
issue, whereas parental relationships are primarily a concern of Windows.
NOTE
The next paragraph in the book explains how to recompile CMPLIB32.DCL. Remem-
ber that you should always keep a backup copy of CMPLIB32.DCL on your hard
drive. CMPLIB32.DCL is stored in the ..\DELPHI 2.0\BIN directory. Beneath this
directory I have created another directory called BAK where I keep a backup copy of
CMPLIB32.DCL. If worse comes to worse, you can always copy the version of
CMPLIB32.DCL on your install CD back into the BIN subdirectory. There is also a
backup copy of CMPLIB32.DCL made when you try to recompile it. You can
sometimes just rename this file, which is called CMPLIB32.~DC, to
CMPLIB32.DCL.
My personal experience is that everyone, sooner or later, makes a tasty Mulligan’s Stew
out of their copy of CMPLIB32.DCL. The worst case scenario is that you are on the
road when this happens, and that you don’t have access to your install disk. Don’t let
this happen to you! Keep a backup copy of CMPLIB32.DCL on your hard drive at all
times!
After running the program and testing the component, the next step is to put it up on the
Component Palette. To do this, select Components | Install, and choose the Add button.
Browse through the directories until you find the UNLEASH1 unit, select OK, and then close the
Install Components dialog by selecting OK. At this point, a project called CMPLIB32.DPR is
compiled. This project creates a huge DLL called CMPLIB32.DCL, which contains all the
components in the Component Palette, all the component and property editors associated with
those components, the form designer part of the IDE, the experts, and other support modules.
After CMPLIB32.DCL finishes recompiling, you can start a new project, turn to the newly
created Unleash page, and drop your new component onto a form. It will have a blue back-
ground, default to Times New Roman, and have its font style set to bold and its font color to
yellow. Notice that all the properties of TEdit have been inherited by TSmallEdit. That’s OOP
in action.
The CMPLIB32.DPR file used during compilation of CMPLIB32.DCL can be saved to disk
if you choose Tools | Project | Library | Options | Save Library Source Code. After choosing
this option and recompiling CMPLIB32.DCL, you can go to the \DELPHI\BIN directory
and view your copy of CMPLIB32.DPR.
Listing 26.4. The second version of the UNLEASH unit contains a panel that comes
equipped with two radio buttons.
unit Unleash;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TSmallEdit = class(TEdit)
public
constructor Create(AOwner: TComponent); override;
end;
TBigEdit = class(TSmallEdit)
public
constructor Create(AOwner: TComponent); override;
end;
TSmallLabel = class(TLabel)
public
constructor Create(AOwner: TComponent); override;
end;
TBigLabel = class(TSmallLabel)
public
continues
TEmptyPanel = class(TPanel)
public
constructor Create(AOwner: TComponent); override;
end;
TRadio2Panel = class(TEmptyPanel)
private
FRadio1: TRadiobutton;
FRadio2: TRadioButton;
public
constructor Create(AOwner: TComponent); override;
property Radio1: TRadioButton read FRadio1;
property Radio2: TRadioButton read FRadio2;
end;
procedure Register;
implementation
inherited Create(AOwner);
Caption := ‘ ‘;
end;
FRadio1 := TRadioButton.Create(Self);
FRadio1.Parent := Self;
FRadio1.Caption := ‘Radio1’;
FRadio1.Left := 20;
FRadio1.Top := 10;
FRadio1.Show;
FRadio2 := TRadioButton.Create(Self);
FRadio2.Parent := Self;
FRadio2.Caption := ‘Radio2’;
FRadio2.Left := 20;
FRadio2.Top := 32;
FRadio2.Show;
end;
procedure Register;
begin
RegisterComponents(‘Unleash’, [TSmallEdit, TBigEdit,
TSmallLabel, TBigLabel,
TEmptyPanel, TRadio2Panel]);
end;
initialization
RegisterClasses([TRadioButton]);
end.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
continues
var
Form1: TForm1;
implementation
uses
UnLeash;
{$R *.DFM}
end.
When you’ve created a component that does something you like, it’s easy to create children of
it. Class TBigEdit descends from TSmallEdit:
TBigEdit = class(TSmallEdit)
It inherits its font nearly unchanged from TSmallEdit, except that it sets Font.Size to 24, a nice
hefty figure that helps the control live up to its name:
constructor TBigEdit.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Font.Size := 24;
end;
This elegant syntax is a good example of how OOP can save you time and trouble while still
allowing you to write clear code.
The label controls shown in this code work in exactly the same way the edit controls do, except
that they descend from TLabel rather than from TEdit. The TEmptyPanel component rectifies
one of the petty issues that sometimes annoys me: Every time you put down a panel, it gets a
caption. Most of the time, the first thing you do is delete the caption so you can place other
controls on it without creating a mess!
Once again, you can change TPanel by overriding its constructor. This time, all you need to do
is set the Caption property to an empty string:
constructor TEmptyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Caption := ‘ ‘;
end;
The last new component in this version of the UNLEASH unit enables you to drop down a panel
that comes equipped with two radio buttons. This makes a single control out of a set of com-
ponents that are often combined. You could create other controls that contained three, four,
or more radio buttons. Or, you could even create a panel that would populate itself with a
specific number of radio buttons.
The declaration for this new radio button is fairly simple:
TRadio2Panel = class(TEmptyPanel)
private
FRadio1: TRadiobutton;
FRadio2: TRadioButton;
public
constructor Create(AOwner: TComponent); override;
property Radio1: TRadioButton read FRadio1;
property Radio2: TRadioButton read FRadio2;
end;
The actual radio buttons themselves are declared as private data, and access to them is given
by the Radio1 and Radio2 properties.
You don’t need write access to these radio button properties. Modifying a property of these
radio buttons doesn’t require write access to the radio button property. The following state-
ment
RP.Radio1.Caption := ‘hello’
performs one read of RP.Radio1 and one write to the Caption property of that radio button.
You don’t want write access to the properties, either, because that would allow the user to as-
sign them garbage (or nil). The Create method for the Radio2Panel begins by setting the width
and height of the panel:
constructor TRadio2Panel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 175;
Height := 60;
FRadio1 := TRadioButton.Create(Self);
FRadio1.Parent := Self;
FRadio1.Caption := ‘Radio1’;
FRadio1.Left := 20;
FRadio1.Top := 10;
FRadio1.Show;
FRadio2 := TRadioButton.Create(Self);
FRadio2.Parent := Self;
FRadio2.Caption := ‘Radio2’;
FRadio2.Left := 20;
FRadio2.Top := 32;
FRadio2.Show;
end;
The next step is to create the first radio button. Notice that the code passes Self as the owner,
and sets the parent to the panel itself. The rest of the code in the Create method is too trivial
to merit comment.
At the very end of the unit, the TRadioButton class is registered:
RegisterClasses([TRadioButton]);
This event would normally occur when you drop a component on a form. However, in this
case a TRadioButton is not necessarily ever dropped explicitly on a form. As a result, the safe
thing to do is register the component.
When it comes time to test out the TRadio2Panel object, you can write the following code in
the test-bed program to take it through its paces:
R := TRadio2Panel.Create(Self);
R.Parent := Self;
R.Left := 50;
R.Top := 100;
R.Radio1.Caption := ‘Delphi’;
R.Radio2.Caption := ‘Unleashed’;
R.Radio1.Checked := True;
R.Show;
Here, R is declared to be of type TRadio2Panel. Note that each of the radio buttons that belong
to the panel act exactly as you would expect a normal radio button to act, only you have to
qualify them differently before you access them.
NOTE
If you want, you can surface Radio1 and Radio2 as published properties of
TRadio2Panel . However, when you first do so, they will have no property editors
available because Delphi has no built-in property editors for TRadioButtons. To build
your own, you can refer to the DSGNINTF.PAS unit that ships with Delphi, as well
as the upcoming discussion of the Clock component and the Tools API.
Before closing this section I’d like to add some additional notes about how Delphi handles
streaming chores. I owe most of my severely limited understanding of this advanced material
to Danny Thorpe. Any holes or errors in this brief discussion of streaming are entirely mine,
not Danny’s.
The good news is that most of the time, you do not have to concern yourself with streaming at
all. Delphi handles most streaming chores automatically. In particular, it will automatically
stream published properties that are simple types. There are only limited circumstances under
which you must explicitly stream the fields of your object.
If a property type is a TComponent or descendant, the streaming system assumes it must create
an instance of that type when reading it in. If a property type is TPersistent but not TComponent,
the streaming system assumes it is supposed to use the existing instance available through the
property and read values into that instance’s properties.
The Object Inspector knows to expand the properties of TPersistent but not TComponent de-
scendants. This is not done for TComponent descendants because TComponents are likely to have
a lot more properties, which would make navigating the Object Inspector difficult.
You will learn more about streaming objects in Chapter 30, “COM Object Basics.”
■ TGraphicControl is for components that don’t need to receive input focus, don’t need
to contain other components, and don’t need a handle. These controls draw them-
selves directly on their parent’s surface, thereby saving Windows resources. Not having
NOTE
I should add one caveat to the rules about TGraphicControl explained previously. In
one limited sense, the user can interact with TGraphicControls. For instance, they do
receive mouse messages, and you can set the mouse cursor when the mouse flies over
them. They just can’t receive keyboard input focus. If an object can’t receive focus, it
usually seems inert to the user.
If you are having trouble deciding whether you want to descend from TWinControl or
TCustomControl , you should always go with TCustomControl . It has a real Paint method, and
some other functionality that is useful when creating a component of your own. If you want to
wrap an existing Windows control inside a VCL object, you should start with TWinControl.
Most Delphi components that follow this path begin by creating intermediate custom objects,
so that TEdit’s hierarchy looks like this:
TWinControl
TCustomEdit
TEdit
Of course, Delphi wraps all the major Windows controls for you, so you won’t need to per-
form this operation unless you are working with a specialized third-party control of some sort.
Following are the declarations for TGraphicControl and TCustomControl, as they appear in
CONTROLS.PAS or CONTROLS.INT:
TGraphicControl = class(TControl)
private
FCanvas: TCanvas;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
protected
procedure Paint; virtual;
property Canvas: TCanvas read FCanvas;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
TCustomControl = class(TWinControl)
private
FCanvas: TCanvas;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
protected
procedure Paint; virtual;
procedure PaintWindow(DC: HDC); override;
property Canvas: TCanvas read FCanvas;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
You can see that these are fairly simple objects. If you went back one step further in the hierar-
chy to TControl or TWinControl, you would see huge objects. For instance, the declaration for
class TWinControl is nearly 200 lines long (not the implementation, mind you, just the type
declaration).
I’m showing you this source code because component builders should work directly with the
source, rather than using the on-line help or the DOCs. For simple jobs, it’s easy to create your
own components without the source. However, if you have a big project, you have to get the
source code if it did not already ship with your product. The INT files that ship with all ver-
sions of Delphi are very helpful, but there is no replacement for the actual source. The source
is available with the client/server version of Delphi, and also as an up-sell from Borland.
FIGURE 26.2.
The CLOCK unit as it
appears on its own test bed,
before being placed on the
Component Palette.
When constructing class TClock, the first thing that needs to be decided is whether the clock is
going to descend from TWinControl or TGraphicControl. If you’ve built a clock in Windows
before, you know that one of the best ways to drive it is with a Windows timer. Timers require
the presence of a Handle in order to be stopped and started; furthermore, they send their wm_Timer
messages to the window that owns them. Because a TGraphicControl descendant isn’t a real
window, it will not automatically get the messages. As a result, TGraphicControl is not an ideal
choice for this type of object.
Of course, the objections to using TGraphicControl raised in the last paragraph aren’t insur-
mountable. If you really want to, you can still make it work. However, there is no point in
expending effort that isn’t strictly necessary, so I have opted for the simplest design possible
and descended the class from TCustomControl. I chose TCustomControl rather than TWinControl
because I needed a Paint method in which I could draw the clock.
NOTE
The decision to use Windows API calls to create a timer, rather than using a TTimer
object, was driven by experience, not by a desire to save memory. I found that I had
some buggy behavior when I placed four or five of these clocks on a single form. If I
got rid of the TTimer object and made the calls directly, all went smoothly. Once again,
when I ran into trouble, I didn’t fight upstream, but instead followed the course of
least resistance. I’ll admit the exact issues involved here aren’t clear to me, but I leave
this note as a signpost to others who might pass this way. Hopefully you will have
better luck (or skill) if you try the same ground where I trod.
You will see that the code, shown in Listing 26.6, also contains a special property editor, as
well as a simple component editor. As you will see, neither of these tools are inherently diffi-
cult to build.
Listing 26.6. The code for the Clock component should be kept in the UNITS
subdirectory where you store StrBox and other utility units.
unit Clock;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, StdCtrls,
DsgnIntf, Dialogs;
type
TClock = class(TCustomControl)
private
FTimer: Integer;
FRunning: Boolean;
procedure SetRunning(Run: Boolean);
protected
procedure Paint; override;
procedure WMTimer(var Message: TMessage); message WM_Timer;
procedure WMDestroy(var Message: TMessage); message wm_Destroy;
public
constructor Create(AOwner: TComponent); override;
published
property Running: Boolean read FRunning write SetRunning;
end;
TClockEditor = class(TComponentEditor)
procedure Edit; override;
end;
TColorClock = class(TClock)
private
FColor: TColor;
procedure SetColor(Color: TColor);
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
published
property Color: TColor read FColor write SetColor;
end;
TColorNameProperty = class(TColorProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure Edit; override;
end;
procedure Register;
implementation
uses
StrBox;
procedure Register;
begin
continues
{ -- TClock -- }
procedure TClock.Paint;
begin
Canvas.Ellipse(0, 0, 100, 100);
end;
{ -- TColorClock -- }
procedure TColorClock.Paint;
begin
Canvas.Brush.Color := FColor;
inherited Paint;
end;
procedure TClockEditor.Edit;
begin
MessageDlg(‘Clock copyright (c) 1995 Charlie Calvert’,
mtInformation, [mbOK],0);
end;
function TColorNameProperty.GetAttributes;
begin
Result := [paMultiSelect, paValueList, paDialog];
end;
procedure TColorNameProperty.Edit;
var
S: String;
begin
S := ‘’;
InputQuery(‘New Color’, ‘Enter Color’, S);
SetValue(S);
end;
end.
Listing 26.7. The test bed for the Clock component is stored in the CLOCK3
subdirectory.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
continues
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
ColorDialog1: TColorDialog;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
MyClock: TColorClock;
implementation
{$R *.DFM}
end.
To run this program, press the button that creates the clock and makes it visible on the form.
The next logical step is to start the clock running; then, if you’d like, you can also change its
color. You get an Access Violation if you click on the latter two buttons before pushing the
first. The problem is that it is an error to call a method or property of the TClock object before
the object itself has been created. To prevent this from happening, you could enable and dis-
able the second two buttons.
The code for the clock components uses inheritance, virtual methods, and properties. TClock
has two pieces of private data:
FTimer: Integer;
FRunning: Boolean;
One is an identifier for the timer, and the other is a Boolean value that specifies whether the
clock is running.
Windows timers are managed by two Windows API calls. When you want to start a timer, use
SetTimer; when you want to stop a timer, use KillTimer . SetTimer takes four parameters:
1000 specifies that the timer is called once every 1000 milliseconds, or once a second. SetTimer
returns zero if the call fails. This is a real possibility, so programs that include error checking
should inspect this value and put up a MessageBox if the call fails.
KillTimer takes two parameters, the first being the handle of your window, and the second
being the unique identifier associated with that timer:
KillTimer(Handle, FTimer);
When you are not using the callback function, timer events are sent to your window by way of
messages:
procedure WMTimer(var Message: TMessage);
message wm_Timer;
This is a classic dynamic method, of the kind covered in Chapter 22, “Handling Messages.”
The response to this event is a simple procedure that calls TextOut, and gets the time from a
function in the StrBox unit called GetTimeString:
procedure TClock.WMTimer(var Message: TMessage);
begin
Canvas.TextOut(10, 40, GetTimeString);
end;
The calls to SetTimer and KillTimer are managed primarily through a property called Running:
property Running: Boolean read FRunning write SetRunning;
If the user sets the Running property to True, this procedure is executed and a call is made to
the SetTimer function. If the user sets Running to False, KillTimer is called, and the clock im-
mediately stops functioning.
The final issue involving the timer concerns the case in which the user closes a form while the
clock is still running. In such a case, you must be sure to call KillTimer before the application
exits. If you don’t make the call, the timer keeps running even after the application closes. This
wastes system resources, and also uses up one of the dozen or so timers that are available to the
system at any one time.
The logical place to call KillTimer is the Destroy method for the TClock object. Unfortunately,
the window associated with the clock has already been destroyed by the time this call is made,
so there is no valid handle to use when you call KillTimer. As a result, you need to respond to
wm_Destroy messages in order to be sure the timer is killed before the TClock window is closed:
Before leaving this description of the TClock object, I should briefly mention the Paint method:
procedure TClock.Paint;
begin
Canvas.Ellipse(0, 0, 100, 100);
end;
This procedure is called whenever the circle defining the circumference of the clock needs to
be repainted. You never have to check for this circumstance, and you never have to call Paint
directly. Windows keeps an eye on the TClock window, and if it needs to be painted, it sends
you a wm_Paint message. Logic buried deep in the VCL converts the wm_Paint message into a
call to Paint, the same way TClock translates wm_Timer messages into calls to TCanvas.TextOut.
NOTE
When writing components, sometimes it’s easiest if you can get right down to the
Windows API level, or as near to it as you would like to get. The following code
reviews the techniques used to hook directly into the Window procedure:
procedure TMyObject.WndProc(var M: Tmessage);
begin
case M.Msg of
wm_Timer: DoSomething;
wm_Paint: PaintSomething;
else
M.Result := DefWindowProc(Handle, M.Msg, M.wParam, M.lParam);
end;
WndProc is typically declared in the private section as:
procedure WndProc(var Msg: Tmessage);
See the Delphi VCL source for the TTimer object for another example of how to use
the WndProc function.
The TColorClock component is a descendant of TClock that adds color to the control. I made
TColorClock a separate object, rather than just adding color to TClock , for two different reasons
(both of which are related to design):
1. You might want to create a descendant of TClock that doesn’t have color, or that
implements color differently than TColorClock does. By creating two objects, one
called TClock and the other called TColorClock, I enable programmers to have the
greatest amount of freedom when creating descendants. This principle has only
minimal weight in a simple object such as TClock, but it can become extremely
important when you are developing large and complex hierarchies. In short, be careful
of building too much functionality into one object!
2. TClock and TColorClock also provide another example of inheritance, and vividly
demonstrate how this technology can be utilized to your advantage.
TColorClock declares a private data store called FColor that is of type TColor. Users can set the
FColor variable by manipulating the Color property:
property Color: TColor read FColor write SetColor;
SetColor is a simple procedure that sets the value of FColor, and calls the Windows API call
InvalidateRect :
Just to be sure this makes sense, it’s worth taking a paragraph or two to provide a refresher
course on InvalidateRect. Here is the declaration for the routine:
procedure InvalidateRect(Wnd: HWnd; Rect: PRect; Erase: Bool);
InvalidateRect forces the window specified in the first parameter to completely redraw itself
if the third parameter is set to True. If the third parameter is set to False, only the portions of
the window that you specifically repaint are changed. The middle parameter is a pointer to the
TRect structure that can be used to define the area that you want to redraw. Compare this func-
tion with the native Delphi function called Invalidate.
Calls to InvalidateRect naturally force calls to the TColorClick.Paint method:
procedure TColorClock.Paint;
begin
Canvas.Brush.Color := FColor;
inherited Paint;
end;
Paint sets the brush associated with the window’s device context to the color specified by the
user; then, it calls the Paint method defined in TClock.
To add this object to the Component Palette, you must first register it:
procedure Register;
begin
RegisterComponents(‘Unleash’, [TClock, TColorClock]);
...
end;
Here, I specify that the TClock and TColorClock objects should be placed in a group called Unleash.
NOTE
That’s all I’m going to say about TClock and TColorClock. Overall, these are fairly simple
components, interesting primarily because they show you how to go about constructing your
own controls from scratch. This kind of exercise lies very much at the heart of Delphi’s
architecture, and I expect many readers will be spending most of their time engaged almost
exclusively in the business of building components.
NOTE
A DCR file is a Windows resource file with the extension changed from RES to DCR.
The resource file contains a bitmap resource with the same name as your component.
For instance, the bitmap resource in a DCR file for a TCOLOR component would
have a resource ID of TColor. This resource should be a 56×28 pixel (or smaller)
bitmap that can be edited in the Image Editor. All you need to do is place this DCR
file in the same directory as your component, and the images defined therein will show
up on the Component Palette. Use the Image Editor to explore the DCR files that
ship with Delphi. They are stored in the \DELPHI\LIB directory.
Here is a description of how to associate your own bitmaps with a particular component.
1. Open the Image Editor and choose New.
2. In the New Project dialog, choose Component Resource (DCR) and click OK.
3. A dialog called UNTITLED.DCR appears. Choose the New button.
4. A dialog called New Resource appears. Choose Bitmap and click OK.
5. A dialog called New Bitmap Attributes appears. Set Colors to 16 colors, because this
technology is available on nearly all Windows systems. Set Size in Pixels to 56×28,
with width being the larger number.
6. Draw a black line down the center of the bitmap, and paint the button-up bitmap on
the right side of the line and the button-down bitmap on the left side of the line. To
understand the difference between button-down and button-up bitmaps, just go to
the Component Palette and press a component. You see that it has one image for
depicting the unselected stage, and another for the selected stage.
7. Save the file as CLOCK.DCR. Rename the bitmap you have created to TCLOCK.
If you don’t like the Image Editor, you can create a 56×28 bitmap in PBRUSH.EXE or some
other bitmap editor, and then create an RC file that looks like this:
TCLOCK BITMAP clock.bmp
Save the file as CLOCK.RC. Run the Borland Resource Compiler from the command line:
brc -r clock.rc
The resulting file will be called CLOCK.RES. Rename that file CLOCK.DCR. An example of
the latter method is used for the TColorClock component, and stored in the UNITS directory
along with STRBOX.PAS and the other utility files.
Experts:
■ Enables you to write your own Experts
■ EXPINTF.PAS
■ VIRTINTF.PAS
■ TOOLINTF.PAS (for enumerating the component pages and components installed,
adding modules to the project, and so on)
Version Control:
■ Enables you to write your own Version Control system, or to link in a third-party
system
■ VCSINTF.PAS
■ VIRTINTF.PAS
■ TOOLINTF.PAS (for opening and closing files in the editor)
Component Editors:
■ Create dialogs associated with a control at design time (for instance, the DataSet
Designer is a component editor)
■ DSGNINTF.PAS
Property Editors:
■ Create editors for use in the Object Inspector
■ DSGNINTF.PAS
Editor Interfaces:
■ EDITINTF.PAS
■ FILEINTF.PAS
The letters INTF are an abbreviation for the word interface. This term was chosen because the
Tools API is an interface between your own code and the Delphi developers’ code.
Needless to say, most people will never use the Tools API. However, it will be important to a
small minority of developers, and its existence means that everyone will be able to buy or down-
load tools that extend the functionality of the IDE.
Property Editors
The Tools API for creating property editors is perhaps the most commonly used interface into
the heart of the IDE. When you first use Delphi and start becoming familiar with the Object
Inspector, you are bound to think that it is a static element that never changes. However, it
should come as no surprise that you can change the functionality of the Object Inspector by
adding new property editors to it.
As mentioned earlier, property editors control what takes place on the right side of the Proper-
ties page of the Object Inspector. In particular, when you click on the Color property of a
TEdit, you can select a new color from a dropdown list, from a common dialog, or by typing in
a new value. In all three cases, you are using a property editor.
If you want to create a new property editor, you should create a descendant of TPropertyEditor,
a class declared in DSGNINTF.PAS. Here is the declaration for the property editor associated
with the TColorClock component:
TColorNameProperty = class(TColorProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure Edit; override;
end;
The DSGNINTF unit is unusual in that it is very carefully documented by the developer who
created it. For instance, here are excerpts from that unit describing the two methods I call in
my descendant of TPropertyEditor:
Edit
Called when the ‘...’ button is pressed or the
property is double-clicked. This can, for example,
bring up a dialog to allow the editing of the component
in some more meaningful fashion than by text
(e.g. the Font property).
GetAttributes
Returns the information for use in the Object
Inspector to be able to show the appropriate tools.
GetAttributes return a set of type TPropertyAttributes.
I won’t quote further, for fear of sounding like I’m plagiarizing. The point, however, is that
these entries were written by the developers, and they extensively document this important
interface to the core code inside the heart of the IDE. Here are declarations for Edit and
GetAttributes , as well as the other key functions in TPropertyEditor :
TPropertyEditor = class
public
destructor Destroy; override;
function AllEqual: Boolean; virtual;
procedure Edit; virtual;
function GetAttributes: TPropertyAttributes; virtual;
function GetComponent(Index: Integer): TComponent;
function GetEditLimit: Integer; virtual;
function GetName: string; virtual;
procedure GetProperties(Proc: TGetPropEditProc); virtual;
function GetPropType: PTypeInfo;
function GetValue: string; virtual;
procedure GetValues(Proc: TGetStrProc); virtual;
procedure Initialize; virtual;
procedure SetValue(const Value: string); virtual;
property Designer: TFormDesigner read FDesigner;
property PrivateDirectory: string read GetPrivateDirectory;
Once again, all these methods are carefully documented inside DSGNINTF.PAS. You should
study that file carefully if you want to learn more about creating complex property editors.
The Edit method of TPropertyEditor is the one you want to override to change the way a property
editor actually edits data:
procedure TColorNameProperty.Edit;
var
S: String;
begin
S := ‘’;
InputQuery(‘New Color’, ‘Enter Color’, S);
SetValue(S);
end;
In this case, I am creating a substitute for the TColorDialog that pops up when you click on the
ellipsis icon in Object Inspector. I am, of course, replacing the fancy Windows common dia-
log with a simpler one that asks the user to enter a string such as “clBlue” or “clGreen”. The
point, however, is that you are learning how to create your own property editors. In a more
complex example, you might open a form that allowed the user to make extensive changes to
a property. SetValue, called at the end of this procedure, is another method of TPropertyEditor.
The GetAttributes method is a way of defining what types of property editors you want to
have associated with TColorDialog:
function TColorNameProperty.GetAttributes;
begin
Result := [paMultiSelect, paValueList, paDialog];
end;
A property editor that has the paMultiSelect flag remains active even if the user has selected
more than one component of that type. For instance, you can select 10 edit controls and change
all their Fonts in one step. Delphi enables you to do that because TEdits have their paMultiSelect
flag set.
The paValueList flag dictates that the property editor drops down a list of values from an enu-
merated or set type when the user presses the arrow button at the far right of the editor. This
functionality is built into Delphi, and you need only set the flag to have it be supported by
your property editor.
Finally, paDialog states that the property editor pops up a dialog. Because the Edit function
shown earlier uses an InputQuery, I have decided that this flag should be set. Ultimately, the
paDialog flag does little more than assure that the ellipsis button appears at the right of the
property editor.
NOTE
When you choose both paDialog and paValuelist in a single component, the property
editor button always winds up being a combo dropdown list button. In other words,
the dialog button is obscured, even though the functionality is still present. See, for
instance, the Color property of a TForm or TEdit.
You must register property editors with the system before compiling them into
CMPLIB32.DCL:
procedure Register;
begin
...
RegisterPropertyEditor(TypeInfo(TColor),
TClock, ‘Color’,
TColorNameProperty);
end;
When you have seen how to build property editors, it is easy to understand component edi-
tors. These tools are descendants of TComponentEditor, just as property editors are descendants
of TPropertyEditor:
TClockEditor = class(TComponentEditor)
procedure Edit; override;
end;
The TColorDialog has a simple editor that pops up a dialog specifying a copyright:
procedure TClockEditor.Edit;
begin
MessageDlg(‘Clock copyright (c) 1995 Charlie Calvert’,
mtInformation, [mbOK],0);
end;
This, of course, is the simplest possible component editor, but it gets you started working with
these useful tools.
The Register method for TClockEditor looks like this:
procedure Register;
begin
...
RegisterComponentEditor(TClock, TClockEditor);
...
end;
Clearly, the first parameter specifies the class with which the editor is associated, and the sec-
ond parameter specifies the class of the editor.
In this section, you have had an introduction to property editors and component editors. These
examples are important primarily because they help focus your attention on DSGNINTF.PAS,
which is one of several files that ship with Delphi that define the Tools API. If you want to
extend the Delphi IDE, you should get to know all the files that end in INTF.
Summary
In this chapter, you have learned about building components. Specifically, you learned how to
create components that:
■ Change an ancestor’s default settings. For instance, you created TEdit descendants
with new default colors and fonts.
■ Add functionality not available in an ancestor object. For instance, the TColorClock
object does things that TClock does not.
■ Are built up from scratch so that they can add new functionality to the IDE. For
instance, the TClock component brings something entirely new to Delphi program-
ming that does not exist in any other component that ships with the product.
You also learned about tools and files associated with components, such as the DCR and KWF
files. Finally, you learned something about the Tools API and specifically about the art of making
property editors and component editors.
Creating Nonvisual
Components 27
NOTE
Delphi has some built-in tools for creating stacks and lists. For instance, there are the
TList and TStringList objects. I avoid these tools here because they either did not exist
or I was not aware of their presence when I first created this program. However, the
code I provide here does have the advantage of working in either DOS or Windows.
That is, the core units will compile under either Delphi or Turbo Pascal.
VI
IN THIS PART
Object Linking
and Embedding 28
This chapter explains a few basic facts about using OLE and DDE in a Delphi program. Inher-
ently, the subject matter is extremely technical, but Delphi simplifies these interfaces, making
them easy to use in most programming projects.
Of particular interest to most programmers is the greatly improved Delphi 2.0 TOleContainer
component. This object now stands head and shoulders over the TOleContainer object that
appeared in the first version of Delphi. With the appearance of this upgraded component,
Delphi’s support for object linking and embedding is now fast, flexible, and reliable.
Here are a few of the specific topics covered in this chapter:
■ Creating OLE clients
■ Inserting or pasting OLE objects at runtime
■ OLE in-place activation
■ Reading and writing OLE objects from files
■ Working with OLE dialogs
■ Accessing OLE verbs from your application
■ DDE clients
■ DDE servers
■ Compiling from the command line with DCC32.EXE
■ Configuring the command-line compiler with DCC32.CFG
■ Modifying resource (RES) files
■ Creating resource files with BRCC32.EXE
In the opening sections of this chapter, I assume that some readers know very little about ei-
ther OLE or DDE. In particular, I include a few pages to introduce these topics to readers who
are new to this material. My goal is to bring you from a standstill to the point where you can
take these concepts for a mild walk around the track.
You should note, however, that this chapter serves as a warm-up exercise for the fairly interest-
ing examination of OLE that stretches out over the next few chapters. The material found in
this chapter on the TOLEContainer is quite important, but it is really only a prelude to the up-
coming discussion of OLE automation, COM objects, and the Games SDK, all of which are
covered in the next few chapters.
The Basics
OLE got its now anachronistic name from the acronym for object linking and embedding.
However, current OLE technology extends far beyond the bounds of object linking and em-
bedding, and Microsoft has officially stated that the letters of the word “OLE” no longer stand
for anything. OLE is called OLE for historical reasons, but the party line is that OLE is now
just a catchy word associated at least as closely with bullfights as it is with the specific technol-
ogy that gave it birth.
In other words, OLE is so large a technology that it no longer refers explicitly to object linking
and embedding. Instead, it refers to a host of technologies that include object linking and
embedding, OCXs, structured storage, automation, and drag-and-drop technology. All these
technologies are based on a software technology called the Component Object Model , or COM.
It is the common thread that runs through all OLE technology, and to me it is also the defin-
ing element in that technology.
What is OLE technology? It is the set of objects built on top of the COM object model that
allows for inter-process communication. All OLE technology is built on top of COM, and most
of it has something to do with getting multiple applications, processes, binary files, machines,
or operating systems to talk to one another. Whether it is a DLL talking to an executable, one
executable talking to another executable, one object talking to another object, or one machine
talking to another machine, OLE technology usually involves communication across some form
of boundary. All this communication—indeed, nearly every line of OLE code—is built on top
of COM. I discuss COM in some depth over the next few chapters, and you will find that it is
the major theme of the OLE material found in this book.
Both OLE and DDE enable you to break out of the bounds of your current program and in-
teract with other applications or other parts of the system. That is, they enable you to commu-
nicate across the boundaries that exist between executables. OLE, however, is a much more
advanced technology, and it is of interest to a broader audience than its little brother DDE. In
fact, DDE is historically just a precursor to the more grown-up technology found in OLE. DDE
came first, but Microsoft all but abandoned it in favor of OLE.
NOTE
Most people access DDE features through a library called DDEML. DDEML stands
for the Dynamic Data Exchange Management Library. It is built on top of the com-
plex message-based interface called Dynamic Data Exchange, or DDE. Microsoft built
DDEML to add increased power and flexibility to the original DDE message-based
system. If you want to work with raw DDE code, you should use DDEML rather than
the lowest levels of the DDE API.
Certain programs, such as Install utilities, still make heavy use of DDE. In fact, there are many
DDE-aware applications on the market at this time. As a result, an understanding of this tech-
nology can still be very useful and will remain useful for the foreseeable future. One of the
great lessons of the last 10 years has been the extraordinary persistence of legacy systems.
Dynamic Data Exchange gets its name because it lets two applications exchange data dynami-
cally at runtime. That is, a link can be created between two programs so that changes to one
application are reflected in the second application. For instance, if you change a number in a
spreadsheet, a second application can be updated automatically so that it reflects the change.
OLE also enables you to implement this same feature, but with much more control and reli-
ability. That is, a link can be established between your program and a spreadsheet so that your
program is informed if changes are made to the data in the spreadsheet.
What then, are the differences between OLE and DDE? Basically, OLE takes the concepts
inherent in DDE and makes much more of them. OLE is to DDE as a chicken is to an egg—
it’s more powerful, more sophisticated, more reliable, and has more diverse capabilities. It is
also more difficult to understand and more demanding of system resources. However, you should
always use OLE rather than DDE if you have choice. If resources are an issue, consider using
OLE automation rather than object linking and embedding.
NOTE
OLE’s demands on system resources are so high that it was originally tempting to
think that it would never prove useful to a large number of users. However, more or
less the same thing was said about GUIs, and now we are all using them—in fact, they
are actually quite snappy if you have a Pentium. As the years have gone by, the
demands of OLE have begun to seem less and less strenuous, and now a large number
of systems run OLE-based programs with little apparent effort.
As I explain later, one way to make OLE useful on slower systems is not to use huge
applications as servers. For instance, it might be counter-productive to use Word or
Excel as an OLE server. On at least some machines, OLE servers should be small
modules that provide one particular type of functionality; they should not be massive
applications that combine hundreds of features into one executable.
application. This means that one application can request services from a second application,
without the user having to be concerned about the details.
OCX technology enables links between your application and free-floating objects that exist on
your system. Or, to put the matter somewhat more generally, it’s a technique for creating com-
ponents that look and feel like Delphi components, but which can be used across multiple lan-
guages, applications, and perhaps, someday, even across platforms.
If you have an OCX toolbar object on your system, it can be embedded in your application in
such a way that it appears to be a native part of your code. You could also do the same thing
using Delphi objects. The difference between the two technologies is that Delphi objects can
usually be used only in Delphi applications, whereas OCXs can be used in a wide range of tools.
OLE technology in general, and OCXs in particular, may point the way toward a new tech-
nique for building applications. For example, you can create several different toolbar OCXs,
all of which have the same interface. Because they have a common interface, you could swap
these different toolbars in and out of an application at will. As a result, this aspect of OLE might
be utilized in the future to enable users to construct an application out of a series of interchange-
able parts.
OCXs are easy to use. You can just plop them down on a form exactly as you would a VCL
object. A page of OCXs ships with most versions of Delphi, and you can easily learn about
them by experimentation and reference to their online help.
Building OCXs, on the other hand, is almost as difficult as using them is easy. You might,
however, check the CD to see if there are any tools mentioned there that might help you con-
vert VCL objects into OCXs. In particular, at the time of this writing, a company called Api-
ary based in Little Rock, Arkansas, was in the process of constructing such a tool. Technology
of this kind is almost certain to be built into future versions of Delphi.
With the advent of Windows 95 and with the surge of Windows NT systems, it’s finally time
for object linking and embedding to start playing a role in the day-to-day life of at least some
computer users. The introduction of cleaner 32-bit code is one possible cause of the improve-
ment in the performance of this technology. However, the primary reason the technology now
seems more feasible is likely to be simply the result of many people using bigger, more power-
ful machines. What seemed unbearably slow on a 4MB 386 now suddenly seems plausible on
a 16MB Pentium. This turn of events probably does not come as a surprise to the original
developers of this technology.
I hope this brief introduction to OLE and DDE brings you to the point that you can under-
stand why this subject is of interest. Over the next few pages, you get a brief look at the
TOleContainer object, which is well designed, easy to use, surprisingly fast, and plenty of fun.
Using this object will get you used to working with well-constructed Delphi OLE objects and
will hopefully prepare you for the more technical OLE-based chapters that lie ahead.
An OLE Example
To run an OLE example, you need to have a reliable OLE server on your system. A number of
simple OLE 2-aware applications ship with Windows 95. Examples include Paintbrush and
WordPad. You can use these two applications when testing the TOleContainer object. These
OLE 2 applications even support in-place activation.
Some of my favorite servers are the applets that ship with Microsoft Word, Excel, and
Powerpoint, or similar programs such as Visio. You can also use programs such as Paintbrush
to good effect. These applications are small enough to load within a reasonable period of time,
and they can reside in memory without claiming so many resources as to bring your system to
a halt. If you have one of the Microsoft applets or another OLE server on your system, I sug-
gest using them as servers rather than a big application such as Word, Paradox, or Excel. You
can, however, effectively use big applications such as Word as an OLE client.
The code for the OleContainer example program is shown in Listing 28.1. This program shows
how to insert OLE objects into a program at runtime, how to access their features, and how to
save them to disk so that they can be loaded again at a later time. It also demonstrates in-place
activation, returning from in-place activation to the original Delphi application, viewing OLE
dialogs, and using OLE verbs.
Listing 28.1. The OleObj1 program shows the basic features of the
TOleContainer object.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
OleCtnrs, Menus, ExtCtrls;
type
TForm1 = class(TForm)
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
MainMenu1: TMainMenu;
File1: TMenuItem;
Open1: TMenuItem;
Save1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
Dialogs1: TMenuItem;
Property1: TMenuItem;
Icon1: TMenuItem;
Options1: TMenuItem;
PasteSpecial1: TMenuItem;
InsertObject1: TMenuItem;
N2: TMenuItem;
SourceDoc1: TMenuItem;
OleClassName1: TMenuItem;
LinkToFile1: TMenuItem;
Panel1: TPanel;
ListBox1: TListBox;
Panel2: TPanel;
OleContainer1: TOleContainer;
ReturnMode: TMenuItem;
procedure bOpenClick(Sender: TObject);
procedure bSaveClick(Sender: TObject);
procedure bGetPropertyDialogClick(Sender: TObject);
procedure bIconDialogClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Exit1Click(Sender: TObject);
procedure MenuClick(Sender: TObject);
procedure PasteSpecial1Click(Sender: TObject);
procedure InsertObject1Click(Sender: TObject);
procedure SourceDoc1Click(Sender: TObject);
procedure OleClassName1Click(Sender: TObject);
procedure LinkToFile1Click(Sender: TObject);
procedure ReturnModeClick(Sender: TObject);
private
procedure FillMenu;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
continues
OleContainer1.InsertObjectDialog;
FillMenu;
end;
end.
To create the preceding program, begin by dropping a TOleContainer component onto a form.
You can wait until runtime to access the properties of the component, or you can right-click
on it to bring up the InsertObject or PasteSpecial OLE dialogs. Both of these dialogs are part
of the operating system, and Delphi merely accesses them to give you, or your users, access to
their features. The InsertObject dialog is shown in Figure 28.1, and the PasteSpecial Dialog is
shown in Figure 28.2.
If you choose the InsertObject dialog, you can select an application from the list supplied to
you. If this dialog box is blank, no servers are available on your system. Otherwise, you can
select an object, click OK, and bingo, you have written an OLE application. Now all you need
to do is run the application and double-click the object whenever you want to activate it. Notice
that the InsertObject dialog also enables you to browse for a file. You can use this option with
WordPad files, BMP files, WAV files, or other file types supported by an OLE server.
FIGURE 28.1.
The InsertObject dialog in
action.
FIGURE 28.2.
The PasteSpecial dialog in
action.
To use the PasteSpecial dialog, you should first run an OLE-aware application such as Write
or WordPad. Enter some text in the application, copy it to the Clipboard, and return to Delphi.
Now select the PasteSpecial dialog using the right mouse button. The type of the item stored
in the Clipboard appears in the dialog, and you can first select it and then access it by clicking
the OK button.
All the functionality of the InsertObject and PasteSpecial dialogs is available to you at runtime.
However, I want to cover a few more general topics before discussing how to use the properties
and methods of a TOleContainer.
After you right-click the TOleContainer object and select a server, the application you choose
automatically starts up. If you select Paintbrush, for instance, you can draw a picture inside
that application, select Return to form1 from Paintbrush’s menu, and return to your Delphi
application, which now contains a copy of your drawing. If you run the Delphi application,
you find that you can begin modifying the painting at runtime just by double-clicking it.
Some servers, such as Paradox, Excel, and Word, are capable of providing in-place activation.
To take advantage of that feature, simply place a menu component onto your form, fill in one
or two items, drop down an OLEContainer, set the AllowInPlace property to True, and attach
to one of these applications using the InsertObject or PasteSpecial dialogs. Run the applica-
tion and double-click the OLEContainer. The menu in your application then takes on the
appearance of the application to which you are linked. Two examples of the OleObj1 program
using in-place activation are shown in Figures 28.3 and 28.4.
FIGURE 28.3.
The OleObj1 program uses
in-place activation to
perform a mind-meld with
the Paint program.
FIGURE 28.4.
The OleObj1 program and
Word for Windows achieve
digitalized holistic bliss
through in-place activation.
In Windows 95, several available servers are small enough to use in-place activation to good
purpose even on computers that have the minimal Win32 allotment of 16 MB of RAM. For
instance, you can have a good deal of fun embedding Paintbrush into a Delphi application.
(Remember the days when 256 KB of RAM seemed like a lot?)
Just a glance at these techniques tells you that there is nothing tricky about using either the
PasteSpecialDialog or InsertObjectDialog functions. A simple call to either of these routines
is all it takes to start using an OLE object at runtime. You can also open up an existing OLE
file type and insert it into the TOleContainer. To do so, just pop up a TOpenDialog, select a file,
and then pass it to the CreateLinkToFile method of TOleContainer. CreateLinkToFile takes
two parameters; the first specifies the filename you want to use, and the second specifies whether
you want the object to appear as an icon.
NOTE
You cannot just insert any file type into a TOleContainer. Instead, you must specify a
file that is associated by the system with an OLE server. To find out what applications
on your system are OLE servers, all you need do is open the InsertObject dialog. All
the registered servers on your system appear in that dialog. Most of these servers have
file types associated with them. For instance, BMP files are associated with Paint,
DOC files are associated with WordPad or Word, AVI files are associated with
MPLAYER.EXE, and so on. To find out more about file types and their associated
applications, open the Windows Explorer, choose View | Options, and turn to the File
Types page.
The FillMenu command shown in the preceding routines is local to the OleObj1 program. It
takes the verbs supported by the current OLE object and displays them in the Options menu
for the program. The code that displays the menu will not work correctly unless you have at
least three top-level menuitems.
NOTE
The verbs associated with an OLE object describe the actions that you can ask the
object to perform. The two most common and easiest-to-understand verbs are Play
and Edit. If you work with the MPlayer application, for instance, you might ask it to
Play a file. Conversely, if you are working with Paint, you might ask it to Edit a file.
The verbs associated with an OLE server are registered with the system and can be
viewed using the REGEDIT.EXE program. Go to the Run menu on the Taskbar and
launch RegEdit. Open up HKEY_CLASSES_ROOT and search for the mplayer entry. Open
up this entry as follows:
HKEY_CLASSES_ROOT/mplayer/protocol/StdFileEditing/verb
The verbs listed at this location are Edit, Play and Open. You can ask MPlayer to
perform these actions when it is embedded or linked in your Delphi application.
After you have inserted an object into your TOleContainer, you can right-click it to see a list of
the verbs supported by that object. As a rule, most objects support verbs such as Open, Play, or
Edit. If you want to get a list of these verbs so that you can display them to the user, access the
TOleContainer.ObjectVerbs array property.
The OleObj1 program does two different things with the verbs from the TOleContainer ob-
ject:
■ It inserts them into a listbox at the bottom of the program.
■ It inserts them into the Options pop-up menu of the TMainMenu for the program.
Here is the code that inserts the verbs into a listbox:
ListBox1.Items := OleContainer1.ObjectVerbs;
Once again, this code is extremely simple to use. It takes advantage of the fact that ObjectVerbs
and TListBox.Items are both instances of the TStrings object. Because of their mutual heri-
tage, you can simply copy the string collection back and forth between the two objects.
The process of inserting these strings into the menu and then accessing them from an event
handler is a little trickier:
procedure TForm1.FillMenu;
const
Stub: string = ‘MyItem’;
var
Temp: TMenuItem;
i: Integer;
begin
while MainMenu1.Items[2].Count > 0 do begin
Temp := MainMenu1.Items[2].Items[0];
MainMenu1.Items[2].Delete(0);
Temp.Free;
end;
Temp.Tag := i;
MainMenu1.Items[2].Insert(MainMenu1.Items[2].Count, Temp);
end;
end;
The preceding code first deletes any submenuitems that might be associated with the Options
menu. It then iterates through all the verbs associated with the currently selected OLE server
and uses the NewItem function from MENUS.PAS to create a menuitem for each one of them.
Each menuitem has its Tag field set to the ordinal number associated with a particular verb.
The code also associates each menuitem’s OnClick event with the MenuClick method of TForm1.
Here is the declaration for NewItem:
function NewItem(
const ACaption: string; // Caption for menuitem
AShortCut: TShortCut; // Shortcut keys
AChecked: Boolean; // Is it checked?
AEnabled: Boolean; // Is it enabled?
AOnClick: TNotifyEvent; // What event is called if users selects it.
hCtx: Word; // The help context
const AName: string // The name of the new objects
): TMenuItem; // Returns a newly created TMenuItem.
Most of this declaration is straightforward, with the possible exception of the AOnClick param-
eter. This parameter is a pointer to the method that will be called if the user clicks on the
menuitem. It is the same method that is automatically associated with a menuitem if you double-
click on it in design mode.
The MenuClick method passed in the AOnClick field of NewItem calls the DoVerb method of
TOleContainer . DoVerb takes a single integer as a parameter. Each verb used by an OLE server
has an ordinal number associated with it, where the first number is zero, and each successive
verb has the next consecutive ordinal number. For instance, a typical OLE server might have
the following numbers associated with it:
Play = 0
Edit = 1
Open = 2
Given these verbs, you could call DoVerb(0) to have the currently selected OLE server play the
currently selected file. The key point to remember in the preceding code is that the Tag field of
each newly created TMenuItem has the ordinal number associated with a particular command
stored in it.
If you inserted a bitmap file into a TOleContainer object using the InsertObject dialog, you can
save that object to disk using the preceding code. The filename you use is totally up to your
particular tastes, though in the example found on disk I chose to save all the sample OLE ob-
jects with the letters “OLE” as an extension.
To read one of these files back in, all you need to do is call the LoadFromFile method of
TOleContainer , as follows:
This code goes a little further than it needs to, in that it not only reads the file into the
TOleContainer , but it also shows the available verbs in a listbox to the user. It then calls the
FillMenu method, shown previously, to add these verbs to the menu. You do not have to add
code that shows the verbs to the user, but it is a nice touch for most applications. The ability to
access the object’s verbs through a right-click on the object is part of the default behavior of the
TOleContainer object.
I should also mention the SizeMode property. You can set this property to the following values:
smAutoSize , smCenter , smClip , smScale , and smStretch. Each of these values changes the way
the object appears on your form before it is activated.
As I said previously, the TOleContainer object has been vastly improved in this version of Delphi.
You will find that the object differs in many respects from the TOleContainer object found in
the first version of the product. This is one of the very few places where there are differences
between components that appear in both the first and second versions of Delphi. However,
the changes to TOleContainer are so overwhelmingly positive that complaining about this major
revitalization of a previously rather unimportant component would be hard.
This overview of Delphi and OLE has touched on the key methods and properties of
TOleContainer . I have not, however, discussed a number of TOleContainer methods and prop-
erties at all. In particular, if you are interested in the inner workings of OLE, you should spend
time viewing the OLECNTRS.PAS file, which ships with Delphi. It contains the source for
the TOleContainer object, as well as implementations of the IOleClientSite, IOleInplaceSite,
and IAdviseSink.
NOTE
One of the most important aspects of Delphi 2.0’s OLE implementation is that the
COM objects used to build it are on display in the Delphi RTL source. Most of these
objects are almost unbelievably small when considering the complexity of the subject
involved. Reading the key Delphi OLE source files such as OLE2.PAS,
OLECNTRS.PAS, OLEAUTO.PAS, and OLECTRLS.PAS offers one of the most
enlightening tutorials on OLE that I have seen anywhere. This body of objects and
routines represents the best and most readable implementation of OLE that I have seen
in any language, and also represents one of the most sophisticated and well-designed
pieces of Object Pascal code in existence.
In the following chapters, you see more technical explanations of OLE functionality such as
automation, structured storage, the Component Object Model, and the Games SDK. How-
ever, the material shown so far in this chapter should help to introduce you to Delphi 2.0’s
sophisticated implementation of OLE technology. The DDE code shown in the following
sections of this chapter is not nearly as important as the OLE code by which it is surrounded.
However, this material is still worth examining, particularly if you think you will be using DDE
in your own programs.
Using DDE
In the rest of this chapter, you see two paired projects: one called CLIENT1.DPR and the
second called SERVER1.DPR. The first application requests data; the second application
supplies data. The technical terms for these two types of applications are, respectively, client
applicationsand server applications.
NOTE
You should check the Unleashed CD for additional DDE examples that were discussed
in some depth in the original edition of this book. You also find introductory material
on DDE that I cut from this edition of the book. The text is stored in a Word 7.0 file
called DDECUT.DOC. I made the cut on the grounds that the material’s length alone
put too much emphasis on a technology, DDE, that does not play a very important
role in the strategic vision of Delphi 2.0.
NOTE
The SIMPSERV and SIMPCLI example programs discussed in this section are on the
CD in the SIMPDDE directory. Neither of them need to contain any code, so I won’t
provide the traditional listings in this section. In this case and in the programs shown
in the next section, I have broken a convention and placed two projects in a single
directory. I have done this because you want to be able to load the server automatically
from the client application. To do that, the server needs to be in the current directory
or on the path. The former of these two options was more convenient in this case. To
make this system work, I named the form for the SIMPSERV application MAINS and
the form for the SIMPCLI application MAINC. I follow the same convention in the
Client1 and Server1 example. The programs I am about to describe, however, are so
simple that I wouldn’t bother with the disk versions unless you are having trouble
creating the connection due to the general quirkiness of DDE technology.
To try this system out, drop a TDDEServerConv and a TDDEServerItem on a form and save the
application under the name SIMPSERV. To send information to a client application, you need
to do nothing more than assign a string to the TDDEServerItem’s Text property. For instance,
you might write this:
DDEServerItem1.Text := ‘25’;
If you want, you can simply fill in the Text field in the Object Inspector. If you follow this
latter path, you will complete the server application after writing zero lines of code!
Because there is no code for this program, I instead show you the text for the DFM file. Re-
member that you can open any Delphi form in the code editor by right-clicking on the form
and choosing View as Text from the pop-up menu that appears. In the SIMPSERV program,
the resulting file looks like this:
object SimpleServer: TSimpleServer
Left = 200
Top = 99
Width = 278
Height = 97
Caption = ‘SimpleServer’
Color = clLime
Font.Color = clWindowText
Font.Height = -13
Font.Name = ‘System’
Font.Style = []
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 16
object Edit1: TEdit
Left = 120
Top = 24
Width = 121
Height = 24
TabOrder = 0
Text = ‘Edit1’
OnChange = Edit1Change
end
object DdeServerItem1: TDdeServerItem
ServerConv = DdeServerConv1
Text = ‘25’
Lines.Strings = (
‘25’)
Left = 56
Top = 16
end
object DdeServerConv1: TDdeServerConv
Left = 16
Top = 16
end
end
NOTE
You don’t have to spend a lot of time studying the DFM text translations shown in
this section. I have included them because the Windows DDE technology can be a bit
quirky at times and I don’t want you wasting a lot of time wondering whether you
have filled in the values correctly. If you are having trouble connecting, you can turn to
these listings to be sure you have set everything up right; otherwise, I wouldn’t bother
with them unless you find them inherently interesting.
Any client that connects to the simple server shown previously can easily access the string you
have assigned to DDEServerItem1.Text. You can begin creating a client that connects to this
server by starting a new project and dropping a TDDEClientConv and TDDEClientItem onto a
form along with a button labeled Connect. You can then associate the button with the follow-
ing routine or else manipulate the proper fields in the Object Inspector:
procedure TForm1.ConnectClick(Sender: TObject);
begin
DDEClientConv1.SetLink(‘SimpServ’, ‘DdeServerConv1’);
DDEClientItem1.DDEConv := DDEClientConv1;
DDEClientItem1.DDEItem := ‘DdeServerItem1’;
Label1.Caption := DDEClientItem1.Text;
end;
If you are unclear how to use the DDEService and DDETopic properties in the Object Inspector,
you can read the DDECUT.DOC file found on disc for further information.
If you then place a label on the form for the client program, you can read values from the server
by making the following assignments in a method:
Label1.Caption := DDEClient1Item1.Text;
That’s all there is to it. Just follow those simple steps, either by writing code or by filling in the
appropriate sections in the Object Inspector.
To test these programs, first launch the SimpServ project using the Windows Explorer. Then
launch the SimpCli program either from the Windows Explorer or from inside Delphi. If you
click the Connect button in the SimpCli program, the number specified in the server appears
on the main form of the client. You can then use the edit control on the server to change this
number, and it will be updated in the client each time you click the Connect button.
To get started, create a new project and save it under the name SERVER1.DPR. Drop a
TDDEServerConv and three TDDEServerItems onto the main form of the project. The name
of the TDDEServerConv component should be set to BigServer, and the TDDEServerItem com-
ponents are called Item1, Item2, and Item3. The code for the program is shown in Listing 28.2.
Listing 28.2. Server1 assigns strings from its edit controls to its
DDEClientItem.Text field.
unit Mains;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
DdeMan, StdCtrls;
type
TServe1 = class(TForm)
BigServer: TDdeServerConv;
DDEItem1: TDdeServerItem;
DDEItem2: TDdeServerItem;
DDEItem3: TDdeServerItem;
Label1: TLabel;
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Memo1: TMemo;
procedure DDEItem1PokeData(Sender: TObject);
procedure BigServerExecuteMacro(Sender: TObject; Msg: TStrings);
procedure Edit1Change(Sender: TObject);
procedure FormCreate(Sender: TObject);
end;
var
Serve1: TServe1;
implementation
{$R *.DFM}
end.
I talk about the DDE code in Server1 later in this chapter. However, you should notice the call
to MoveWindow in the FormCreate method. MoveWindow is a Windows API call that moves a win-
dow from one place on the screen to another. You can simultaneously move the upper-left corner
of the window and change the window’s width and height. In this case, I don’t want to change
the dimensions of the window, so I simply pass in the existing Height and Width properties.
However, I specify 1,1 as the coordinates for the upper-left corner. This moves the window to
the top of the monitor so that you can have enough screen real estate to display both the Server1
and Client1 applications at the same time.
The Server1 application is meant to work in conjunction with the Client1 program. Both pro-
grams are saved in the CLI-SERV subdirectory. The code for the Client1 program is shown in
Listings 28.4 through 28.9.
Client1 has three labels that correspond to the edit controls in Server1. Whenever the user types
in any of Server1’s edit controls, the keystrokes are sent immediately to Client1 and displayed
in its labels.
Listing 28.3. The Client1 application has means for receiving information from
the server and for sending information back.
unit Mainc1;
interface
{
Every call to LoadResource needs to be followed
by a call to FreeResource. Also, LockResource is
paired with UnLockResource. I provide examples
below that demonstrate how this works.
}
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, DdeMan,
StdCtrls, Buttons, ExtCtrls,
Menus;
type
TForm1 = class(TForm)
MainMenu1: TMainMenu;
Options1: TMenuItem;
Clear1: TMenuItem;
Close1: TMenuItem;
Panel1: TPanel;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
Poke1: TBitBtn;
Poke2: TBitBtn;
Poke3: TBitBtn;
Conv1: TDdeClientConv;
Client1: TDdeClientItem;
Client2: TDdeClientItem;
Client3: TDdeClientItem;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Poke1Click(Sender: TObject);
procedure Poke2Click(Sender: TObject);
procedure Poke3Click(Sender: TObject);
procedure Clear1Click(Sender: TObject);
procedure Close1Click(Sender: TObject);
procedure Client1Change(Sender: TObject);
private
{ Private declarations }
Resource: THandle;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
{$R MILTON1.RES}
end.
Listing 28.4. Client1 has a second custom resource made up of a single long
string stored in text file called MILTON.TXT.
From Paradise Lost
by John Milton
From Book I
===========
Of man’s first disobedience, and the fruit
Of that forbidden tree, whose mortal taste
Brought death into the world, and all our woe,
With loss of Eden, till one greater Man
Restore us, and regain the blissful seat,
Sing Heav’nly Muse, that on the secret top
Of Oreb, or of Sinai, didst inspire
That shepherd who first taught the chosen seed
In the beginning how the heav’ns and earth
Rose out of Chaos; or if Sion hill
Delight thee more, and Siloa’s brook that flowed
Fast by the oracle of God, I thence
Invoke thy aid to my advent’rous song,
That with no middle flight intends to soar
Above th’ Aonian mount, while it pursues
Things unattempted yet in prose or rhyme.
From Book IX
============
To whom the Tempter guilefully replied:
“Indeed? Hath God then said that of the fruit
Of all these garden trees ye shall not eat,
Yet lords declared of all in earth or air?”
To whom thus Eve yet sinless: “Of the fruit
Of each tree in the garden we may eat,
But of the fruit of this fair tree amidst
The garden, God hath said, ‘Ye shall not eat
Thereof, nor shall ye touch it, lest ye die.’“
...
The Tempter all impassioned thus began:
“O sacred, wise, and wisdom giving Plant,
Mother of Science, now I feel thy power
Within me clear, not only to discern
Things in their causes, but to trace the ways
Of highest agents, deemed however wise.
Listing 28.5. Here is the RC file that holds the resource stored in MILTON.TXT.
/* From Paradise Lost */
Milton TEXT milton.txt
Listing 28.7. Here is a batch file for creating the Client1 and Server1 programs
from the command line.
@echo off
echo **********************************
echo You must create and properly edit
echo a DCC.CFG file before running this
echo macro. Search in this directory
echo for an example DCC.CFG file.
echo **********************************
brcc32 -r client1.rc
brcc32 -r milton1.rc
dcc32 /b server1.dpr
dcc32 /b client1.dpr
Listing 28.8. Here is a sample configuration file for the command-line compiler. It
is called DCC.CFG.
/Uc:\delphi32\lib
/Rc:\delphi32\lib
The Client1 program has two parts to it. The first part shows off DDE techniques such as
exchanging data, poking data, and sending macros. The second part shows how to use a RES
file with Delphi and how to compile from the command line. I treat these two parts in two
different sections, with the DDE code handled first.
The Server1 form sports three edit controls. The OnChange event for these controls looks like
this:
procedure TForm1.Edit1Change(Sender: TObject);
begin
Item1.Text := Edit1.Text;
Item2.Text := Edit2.Text;
Item3.Text := Edit3.Text;
end;
Whenever the user types anything in these edit controls, the DDEServItem components, called
Item1, Item2 , and Item3 , reflect these changes in their Text property.
At the other end, the Client1 program has three labels. Whenever any of the DDEClientItems
record an OnChange event, the following method gets called:
procedure TForm1.DdeClientItem1Change(Sender: TObject);
begin
Label1.Caption := DDEClientItem1.Text;
Label4.Caption := DDEClientItem2.Text;
Label6.Caption := DDEClientItem3.Text;
end;
This code transfers the information from the server into the labels on the form of the client
program. The changes take place letter by letter as you type them, so it appears that you are
typing in two places at once.
When you want to send data from a client back to a server, you can do so through two differ-
ent means: by poking and by executing a macro. The Client1 program has three different
methods that poke data into the Server1 application:
procedure TClientOne.Poke1Click(Sender: TObject);
begin
Conv1.PokeData(‘DDEItem1’, ‘Finny Fun’);
end;
The Poke1Click method is the classic and simplest means of sending data from a client to a
server. It calls a method of TDDEClientConv called PokeData. PokeData takes a string in the first
parameter. This string specifies the name of the server item to which you want to send infor-
mation. The second parameter is a PChar that holds the information you want to send. Delphi
uses a PChar here rather than a string because you might need to send information longer than
256 characters, and because PChars are the standard when you are passing information between
an application and Windows.
The Poke2Click method follows the same pattern as Poke1Click except that it first asks the user
for the string to send. Once the string is captured, it is translated into a PChar and sent to the
second edit control in the Server1 program.
Poke3Click again uses PokeData to send information to Server1, but this time the information
is the entire poem shown in Listing 28.5. The poem is first retrieved from the MILTON1.RES
file via a mechanism to be explained in the next section, and then it is sent to Server1 and dis-
played in a memo.
NOTE
You can poke information to a server via either the PokeData method or the
PokeDataLines method. PokeDataLines takes a TStringList or TStrings object as a
parameter. You can use PokeDataLines if you want to send information from a memo,
list box, or other TStrings-based component. Remember, however, that both PokeData
and PokeDataLines can handle large chunks of data. Furthermore, recall that if you
want to create the whole list on the fly, without the aid of a Delphi component, you
should use a TStringList, not an instance of the abstract TStrings class.
On the server end, all this poking of information is handled by a single method, which is at-
tached to the OnPokeData event of the DDEServerItem controls:
procedure TServe1.DDEItem1PokeData(Sender: TObject);
begin
case TDDEServerItem(Sender).Tag of
0: Edit1.Text := DDEItem1.Text;
1: Edit2.Text := DDEItem2.Text;
2: Memo1.Lines := DDEItem3.Lines;
end;
end;
Note that the long poem sent by Client1 is automatically hung on a TStrings object so that it
can be easily transferred to a memo or list box. I have set the Tag properties of the DDEServerItems
to 0, 1, and 2.
To send a macro from the Client1 program to Server1, just call the ExecuteMacro function:
procedure TClientOne.Clear1Click(Sender: TObject);
begin
Conv1.ExecuteMacro(‘Clear’, True);
end;
ExecuteMacro takes two parameters. The first is a string specifying the command you want to
send. The second is a Boolean flag stating whether DDE should wait for the result of this macro
to return before executing any more pokes or macros. It can be a mess if too many signals are
flying back and forth between a server and a client at the same time, so it is generally best to set
this flag to True.
NOTE
The commands used in the first parameter of the ExecuteMacro function are just strings
that you can make up to suit your own sense of aesthetics. In other words, these aren’t
predefined commands—they are just strings you create to identify the various com-
mands you might want to give to a server. If you create a server and then publish a list
of commands that it obeys, you can go far toward creating a very easy-to-implement
form of DDE Automation. Of course, if you are serious about this kind of functional-
ity, and you are not tied to any legacy code, you should use OLE automation.
The following method appears in Server1 and is meant to handle the macro once it is sent.
Note that this event occurs in response to the OnExecuteMarco event of the TDDEServerConv
component:
procedure TServe1.BigServerExecuteMacro(Sender: TObject;
Msg: TStrings);
var
i: Integer;
S: string;
begin
S := Msg.Strings[0];
if S = ‘Clear’ then begin
for i := 0 to ComponentCount - 1 do
if (Components[i] is TEdit) or (Components[i] is TMemo) then
TEdit(Components[i]).Clear;
PostMessage(Edit1.Handle, wm_KeyDown, 32, 0);
PostMessage(Edit1.Handle, wm_KeyDown, vk_Back, 0);
end else
Close;
end;
There are two commands that Client1 can send to Server1. The first command is called ‘Close’,
and Server1 responds to it by calling the VCL Close method. The second command comes in
the form of the string ‘Clear’, and Server1 responds to it by clearing the text of any compo-
nent of type TEdit or TMemo. I then notify the client that this event has occurred by creating a
fake keyboard event: I insert a space character (#32) into Edit1 and then immediately delete
the character. This latter move is a rather extreme kluge, but it does work. In fact, it works
even in a different type of program where there might be text in one of the controls that needs
to be preserved.
In this section, I have given a very brief overview of how to create DDE client and server appli-
cations. This discussion has made no attempt to be all-inclusive, and I have ignored material
on using multiple Clipboard formats to transfer data. Some Clipboard concepts are demon-
strated in the DDE demos that ship with Delphi. After seeing the brief examples in this chap-
ter, you should have little trouble deciphering the extra material in the Delphi demos.
To actually perform a compilation at the command line, you can enter a command that looks
like this:
dcc32 /b MYPROG.DPR
MYPROG is the name of your application, and /b says you want to compile all the units in the
project. Here are the commands for compiling Server1 and Client1:
dcc32 /b server1.dpr
dcc32 /b client1.dpr
Just passing in these lines doesn’t always work. Delphi also needs to know where the units are
that are listed in the program’s uses clause. You can pass this information into DCC32 with
special parameters like those shown, or you can place it in a separate text file labeled
DCC32.CFG. On my system, a typical DCC32.CFG file looks like this:
/Uc:\delphi32\lib
/Rc:\delphi32\lib
/U marks the beginning of the line where I show the path to the main Delphi units, and /R
shows the path to the main Delphi resources. If you have an additional location where you
save files such as STRBOX.PAS or MATHBOX.PAS, you can add those to the preceding code
using the same syntax used to build a DOS path:
/Uc:\delphi\lib;c:\delcode\units
/Rc:\delphi\lib;c:\delcode\resfiles
NOTE
There are other options available for use as parameters to DCC32 and also for use
inside DCC32.CFG files. If you want to pursue these valuable resources, just type
DCC32 at the command line with no parameters. You can place any parameters you
pass to DCC32 inside a CFG file and therefore have them implemented automatically.
On the DCC32 command line, you can also specify the directory in which you keep
the CFG file you want to use. As a result, you can specify different parameters in
various CFG files and then pass only the directory of the CFG file to DCC32 (using
the -T option).
Besides the options shown previously, other important parameters are as follows:
/v : Include debug info for the stand alone debugger
/gd : Create a detailed map file
/D : Define a conditional directive
/$R+ : Range checking on
You can use the $ syntax to define any of the compiler switches found in the Options | Project
| Compiler page.
This file compiles MILTON.TXT into an RES file. The type of resource being used here is
labeled more or less at random as TEXT, and it is given the word Milton as an identifier so you
can access it easily from inside Delphi.
The following code shows a simple DOS batch file you can run. It compiles the resources used
by the Client1 program, as well as both the Server1 and Client1 executables:
Brcc32 -r client1.rc
brcc32 -r milton1.rc
dcc32 /b server1.dpr
dcc32 /b client1.dpr
Note that CLIENT1.RC will not compile successfully unless you have an icon called
CLIENT1.ICO in the same directory. There is not really any need to build the CLIENT1.RES
file, but I threw it in here just to remind you that you can control all parts of Delphi programs
through code, if you so desire.
The CLIENT1.RES file is automatically linked into your program because of the $R *.RES
line found in CLIENT1.DPR. However, you have to take special steps to link MILTON1.RES
into your program. Specifically, you should add the following lines to MAINC.PAS:
{$R *.DFM}
{$R MILTON1.RES}
The line in question is the second of the two shown, where the first is merely meant as a clue
as to where in the file the reference to MILTON1.RES should occur.
In the Client1 program I make the call to LoadResource in the FormCreate method. Notice that
if you were trying to load a bitmap, you would call LoadBitmap rather than LoadResource.
Once you have loaded a resource into a program, you can retrieve the specific data you want by
calling LockResource:
Poem := PChar(LockResource(Resource));
In this case, the resource is just a long string, so it is safe to typecast it as a PChar and then assign
it to a variable called poem, which is declared to be of type PChar. Note, however, that nothing
dictates that the resource in question has to be a string. Because this is a custom resource, you
can load any kind of binary data this way, including a PCX file or an encrypted file.
I suppose the main theme of the last section is that Delphi can handle any of the resources that
you would normally associate with a C++ or BP7 program. However, Delphi tries to stream-
line this system by providing simple ways to handle most resources. For instance, remember
that the Image Editor enables you to open RES files and store icons and bitmaps inside them.
Summary
In this chapter, you saw some of the basic features of OLE and DDE programming. In par-
ticular, you learned that both techniques enable you to link one process to another so they can
interact dynamically at runtime. You have seen that DDE is really only a nascent part of a larger
project that is reaching fruition in OLE. DDE is, however, still useful, in part because it en-
ables you to support a wide range of legacy applications. You should not, however, decide on
DDE as your core technology until you have first had a good hard look at OLE automation.
Using DDE in any new application you are starting today would be very unusual. It is legacy
code, meant to support legacy applications.
If DDE looks to the past, OLE and COM look to the future. Microsoft has hinted that they
hold the seeds for future operating systems that will be based on an object-oriented architec-
ture. Specifically, the various parts of the operating system will offer a COM interface that is
similar to that offered by a Delphi object. Programmers will interact with these objects by gaining
access to an interface and then calling the object’s functions, as shown in Chapter 30, “COM
Object Basics,” and 31, “COM, Automation and Structured Storage.” Microsoft has claimed
that these new object-based operating systems will be more flexible and more configurable than
DOS, Windows 3.1, or Windows 95.
As you have seen, the goal of the Delphi OLE implementation is to find practical ways to en-
capsulate complex technologies inside an easy-to-use interface. The code in this chapter covers
powerful but easy-to-use Delphi objects that show the capabilities of OLE, but it teaches you
little about how OLE actually works. The next few chapters, however, dig continually deeper
into this technology, and end up laying a foundation on which you can build a deep under-
standing of OLE.
OLE Automation
Basics 29
OLE automation enables you to control objects that reside in other applications and in DLLs.
It works not only across application boundaries, but also across language boundaries. An OLE
automation object written in Delphi can be used in C++ or in Visual Basic, and Visual Basic or
C++ automation objects can be used in Delphi.
Many people feel that OLE automation and other key OLE technologies, such as OCXs, rep-
resent some of the most important developments in contemporary computing. This rapidly
growing technology is only just starting to unfold. For instance, Microsoft promises that in the
near future Distributed OLE will allow applications to control objects that reside on other
computers. This means that you can access applications and individual objects that reside not
only on your computer, not only on your network, but on any part of the Internet. It’s at least
conceivable to me that this change, or one similar to it, is likely to have the same overwhelming
impact on computing that occurred with the introduction of the first PCs. In short, it’s likely
to change everything.
In this chapter you use high-level Delphi objects to create automation servers and clients. The
actual act of creating automation servers and clients is not difficult. However, this technology
has a number of implications that make the subject more complicated than it at first appears.
As a result, this chapter also attempts to explain some of the key architectural guidelines needed
to make this technological work on a large scale.
In particular, the following subjects are covered:
■ Creating OLE objects with the CreateOleObject function
■ Using variants to encapsulate OLE objects
■ OLE servers, clients, in-process servers, and local servers
■ Variant arrays
■ Working with Word and Excel as automation servers
■ The Null and UnAssigned global variables
■ The TAutomation, TAutoDispatch, and TAutoObject Delphi objects
■ Registering and unregistering servers
The subject matter for this chapter is so large that it carries over into Chapter 30, which fea-
tures an explanation and example of an in-process server, as well as additional information on
local servers.
This is a relatively challenging chapter, but it contains some of the most important material in
the entire book. As a result, I have done my best to become the Hubble telescope of the tech-
nical writing world, struggling to make each issue as clear as possible, regardless of its distance
from the mind’s eye of the reader!
NOTE
You may have heard other names for automation clients, servers, in-process servers,
and local servers. I’ve chosen to use these particular names because they seem to me to
be the clearest of the terms I’ve heard. The one possible exception would be the use of
the term “out-of-process server” instead of local server. Once Distributed OLE is
released, however, there may be a need to distinguish between in-process servers, local
servers, and remote servers. The term “out-of-process server,” which seems clear now,
might become ambiguous at that point.
In general, I consider the “Creating OLE Servers” section of the Professional Features
manual for Visual Basic to be the closest thing we have to an OLE automation bible. I
don’t follow it slavishly, but I have read it carefully and try to follow its (usually well-
thought-out and well-presented) viewpoint whenever possible. Inside OLEby Kraig
Brockschmidt (Microsoft Press) also is valuable from a purely technical point of view,
but it’s hindered by one of the more peculiar and idiosyncratic writing styles I’ve ever
encountered. It’s an intelligent and spirited book, and a “must have” for serious OLE
programmers, but it’s not a good place to turn when you want plain talk about a
complicated subject, or when you want to find clearly stated standards.
The main reason for creating an in-process server is to share an object written in one language
with an application written in a second language. For instance, you can share Delphi objects
with C++ and Visual Basic applications using this method.
OLE automation objects are not necessarily better than standard Delphi objects. For instance,
if you did not need to move across language barriers, your DLL would be faster and easier to
implement if you used straight Object Pascal code that did not use OLE services.
This code inserts the words “Hello from Delphi” into an existing Word document. You can
find the code as OLEWord1 on the CD-ROM included with this book.
You must have Word up and running with a document open for this code to work. In a mo-
ment I show how to handle the situation where Word is not already up and running, but at
first it’s best to stick with the simplest possible case.
There are three key elements to the code shown above. The first is the OleAuto unit listed in
the uses clause. This unit has all the key code for handling automation inside Delphi applica-
tions. If you want to get good at this subject, you should know that unit like the back of your
hand.
The use of the variant V is one of the more important code fragments in Delphi 2.0 program-
ming. Variants were introduced into Delphi because Microsoft uses them extensively in
automation-based code. They are not a necessity for automation, but they make it much easier,
given the OLE implementation provided by Microsoft. In other words, variants are important
in Delphi not primarily as a technique for accessing strings or integers, but explicitly because
OLE objects can be assigned to them:
V := CreateOleObject(‘Word.Basic’);
The code shown above assigns the OLE object to variant V. The OLE object in question
resides inside Word for Windows. In particular, it is an OLE automation server that resides
inside Word. I discuss the assignment statement shown above in more depth in just one
moment.
The third key element in the code is the call to the Word Basic Insert method:
V.Insert(‘Hello from Delphi’);
This simple line of code is one of the most radical innovations in recent programming. Insert
is not an Object Pascal method or function, nor is it part of the Windows API. It’s part of
Word for Windows, and you are able to call it directly from inside of a Delphi application due
to the technology called OLE automation. In short, the closest you can get to the heart of OLE
automation is the moment when you call a method or access a property by using a line of code
like the insert statement shown above. This is what automation is all about, and the ability to
make these calls is what is important in this technology.
NOTE
There currently is no way for Delphi to perform syntax checking on a call to an OLE
method or property. Therefore, you could successfully compile code that looks like
this, even though it will not perform correctly at runtime:
var
V: Variant;
begin
V := CreateOleObject(‘Word.Basic’);
V.FormatDriveC;
end;
It happens that there is no FormatDriveC function in Word Basic. However, there is no
way for Delphi to discern this fact at compile time, and so you will not get an error
until you run the application. The whole issue of raising and handling errors in an
OLE automation server or client is approached later in this chapter and in the next
chapter.
As I explained earlier, this variant record defines the possible types that can be expressed by a
Variant.
NOTE
Remember that variant records and the Variant have nothing to do with one another,
and the similarity in their names is just a coincidence.
Just for sake of clarity, let me spell out a few important points here. Variant records
have nothing to do with the TVarData type. Instead, they are Object Pascal’s take on
the Unions you find in the C programming language. Here is an example of a com-
monly used variant record found in WINDOWS.PAS:
PRect = ^TRect;
TRect = record
case Integer of
0: (Left, Top, Right, Bottom: Integer);
1: (TopLeft, BottomRight: TPoint);
end;
This record can be interpreted as containing four integers or two TPoint records. The
choice is yours, depending on the needs of the moment.
The issue here is that the record can have varying interpretations, depending on your
whims. It is called, therefore, a variant record. This has absolutely nothing to do with
the Variant type or the TVarData record. If you are confused about variant records, look
them up in the online help under the subject “record types.”
You can find out what kind of Variant you have by either typecasting the Variant as a TVarData
and checking its VType field, or by calling the VarType function. In particular, if a Variant is
fronting for an OLE object, its VType field is set to VarDispatch.
The following code fragment creates a pop-up message box containing the word “Yes”:
V := CreateOleObject(‘Word.Basic’);
If VarType(V) = VarDispatch then
ShowMessage(‘Yes’)
else
ShowMessage(‘No’);
It would display the word No if you tried to compare the VType field from TVarData with varByte,
varString, and so on. In particular, what you are getting back is an oblique reference to an
object of type IDispatch. If you want to get the IDispatch object itself, which is not recom-
mended, here is how to proceed:
A := VarToInterface(V);
A.AddRef;
A.GetTypeInfoCount(i);
ShowMessage(IntToStr(i));
A.Release;
VarToInterface is implemented in the OleAuto unit. In particular, this function first checks to
see if the Variant does, in fact, contain a valid IDispatch interface. If it does, it returns the
interface by executing the following line of code:
Result := TVarData(V).VDispatch;
The code then goes on to call GetTypeInfoCount, a method of IDispatch, which usually returns
the value 1.
Another function closely related to VarToInterface is VarToAutoObject. This routine returns,
not an IDispatch interface, but the Delphi object used for manipulating IDispatch objects. In
short, it returns the Delphi wrapper that is built around IDispatch. This wrapper is called
TAutoObject, and you learn about it later in this chapter. When possible, you should use
TAutoObject rather than raw IDispatch objects, because TAutoObject is much easier to use and
much less likely to lead you down the dark path that leads to the Land of Embarrassing Pro-
gramming Errors.
It is possible for a Variant to be set to the value UnAssigned or to Null. For instance, the follow-
ing code is valid:
var
MyVariant: Variant;
begin
MyVariant := CreateOleObject(‘Word.Basic’);
// User defined code added here
MyVariant := UnAssigned;
end;
The code shown here first obtains an OLE object and then releases it. You might use code like
this to free an OLE object if you find that it is in an error state, or if you no longer need its
services.
UnAssigned is a variable of type Variant that is preset to varEmpty during the initialization for
the System unit:
TVarData(Unassigned).VType := varEmpty;
TVarData(Null).VType := varNull;
As you can see, Null also is a globally declared Variant that gets initialized to varNull before
any of the code in your program is executed. A null Variant is used primarily in database ap-
plications to signify that there is no data associated with a particular field.
A null Variant has been initialized but is set to Null. An unassigned Variant has been set to
varEmpty, which is one way of explicitly stating that it has not been initialized.
This matter is confusing enough to warrant a few explicit examples. Consider the following
code fragment:
procedure TForm1.Button1Click(Sender: TObject);
var
V: Variant;
begin
if varType(V) = varNull then
ShowMessage(‘Null’);
if varType(V) = varEmpty then
ShowMessage(‘Unassigned’);
end;
This example pops up a message box containing the word “Unassigned”. It’s guaranteed to do
that, even though V is a local variable. In other words, Variants are automatically set to varEmpty
when they first come in scope.
Consider the following example:
procedure TForm1.Button1Click(Sender: TObject);
var
V: Variant;
begin
V := Null;
if varType(V) = varNull then
ShowMessage(‘Null’);
V := Unassigned;
This code pops up the word Null and then the word Unassigned. Notice that an unassigned
Variant is actually set to varEmpty. There is no such thing as varUnassigned .
To add one final complication to this issue, you should remember that a Variant can also be
set to varUnknown. VarUnknown is distinct from either varEmpty or varNull.
This is a unique class ID that is inserted into the registry of all machines that contain a valid,
and properly installed, copy of Word for Windows. The only application that uses this ID is
Word for Windows. It belongs uniquely to that application.
Now go further up HKEY_CLASSES_ROOT and look for the CLSID branch. Open it up
and search for the CLSID shown above. When you find it, you can see two entries associated
with it: one is called LocalServer, or LocalServer32, and the other is called ProgID. The ProgID
is set to word.basic. The LocalServer entry looks something like this:
C:\WINWORD\WINWORD.EXE /Automation
If you take a look at this command, you can begin to grasp how Windows can translate the
ProgID passed to CreateOleObject into the name of an executable.
Having these kinds of entries in the registration database does not mean that the applications
in question are necessarily automation servers. For instance, there are many applications with
LocalServer and ProgID entries that are not automation servers. However, all automation serv-
ers do have these two entries. Note, further, that this is a reference to the automation server in
Word, and not a reference to Word as a generic application. It references an automation object
inside of Word, and not Word itself.
When you call CreateOleObject, a number of complex things happen, some of which are ex-
plained in detail in this book. But from the 25,000-foot perspective, what happens is that
Windows looks to see whether Word is loaded in memory. If Word is not loaded,
CreateOleObject launches it, passing the command line shown above. CreateOleObject , how-
ever, automatically opens an empty document when it launches Word. Windows then finds
an interface pointer to the OLE automation object requested, and returns that pointer in the
form of the result of the call to CreateOleObject. Once again, you are not being returned a
pointer to Word itself, but only a pointer to an object that resides in Word.
Once you have an interface (an IDispatch VMT) to the OLE automation object in Word, you
can start calling the functions made available by the OLE automation object. In particular, any
of the Word Basic functions found in the Word online Help are available to you. There are
some 200 of these functions, and they include commands for opening documents, saving docu-
ments, formatting documents, printing documents, and so on. This means that if you have a
Delphi 2.0 application and a copy of Word on your target systems, you need never implement
any code for handling documents. You can let Word carry the whole burden. After all, Word
is great at word processing, and now the entire application is in effect an extension of your
Delphi application. This is, to put it mildly, what programmers mean when they talk about
code reuse.
NOTE
There is some hope, but no certainty, that this system will spell the end of gargantuan
applications such as Word. In the future, applications could consist of a series of OLE
automation servers and OCXs, each linked together dynamically according to the
needs of the moment.
In particular, you might find that your application needs the services of an editing
kernel. Using OLE automation and type libraries, you could search the system for
available editors, choose the one you want, and use it. You might then need the
services of a printer object, and you could once again search for one and use it. There
might be 10 or 15 different printer objects on the market, all supporting the same low-
level automation API, but all implementing the API differently. Furthermore, with
Distributed OLE, there may be no need for these objects to reside on your current
system.
The whole idea of trying to copyright an interface to an application, a la Lotus/IBM,
will become even more absurd than it is at this time. Part of the definition of a spread-
sheet will be the standardized automation interface that it presents to other programs.
It is not, however, even remotely clear as to whether or not the lawyers who have been
arguing these cases have a clue as to the nature of the technology they are attempting to
regulate. Nor is it clear at this time whether some influential companies might try to
copyright their automation interfaces rather than presenting them as a standard to
which all entries in a particular market should adhere.
This code is part of an application called OleWord2. The application is available on the book’s
CD-ROM.
The interesting thing about this code is that it never causes Word to appear on the screen. Instead,
Word is launched temporarily into memory, but stays invisible. The moment the variable V
goes out of scope, or the moment it is set to varNothing, Word is shut down again. Besides the
sound of your hard disk drive churning, the only way to tell that the above code works is to
actually launch Word after a run of your application, then attempt to open SEMPERFI.DOC
and see if it has the proper contents.
The above code starts by calling CreateOleObject. This line launches Word into memory if it
is not already started. If Word is not started, CreateOleObject launches it, but passes the
/Automation parameter to Word. Word knows that when it’s started with this parameter it should
run quietly in the background, without ever becoming visible. It closes itself down as soon as
it no longer has an OLE automation client to service.
It’s no mere coincidence that Word runs in the background when called into memory by an
OLE automation client. In fact, it is part of the usual definition of a server that it should run
in the background unless specifically invoked by a user, rather than by an automation client. If
it is merely started by an automation client, it should remain hidden. This way the client can
use the services provided by Word without ever disturbing the user with the sordid technical
details of how an operation might be performed.
As you can see, FileSaveAs takes a total of 11 parameters. These parameters are called, respec-
tively, Name, Format, LockAnnot, Password, AddToMru, WritePassword, RecommendReadOnly,
EmbedFonts , NativePictureFormat , FormsData, and SaveAsAOCELetter . If FileSaveAs takes 11
parameters, how come our call to this function passes in only one parameter?
The issue here is that Word supports optional parameters. In the case shown in the last section,
I am passing only one parameter to the object. I could, if I wanted, pass two:
V.FileSaveAs(‘C:\SEMPERFI.TXT’, 3);
The code shown here takes advantage of the fact that FileSaveAs takes a Format specification
in the second parameter. The Format parameter can have one of the following values:
0 Word document
1 Document template
2 Text Only
3 Text Only with Line Breaks
4 MS-DOS Text (extended characters saved in IBM® PC character set)
5 MS-DOS Text with Line Breaks
6 Rich-Text Format
It is not necessary for you to define parameters in a particular order. For instance, you could
write:
V.FileSaveAs(‘C:\SEMPERFI.DOC’, Password := ‘Sam’);
This saves SEMPERFI.DOC in Word format, and assigns it a password called Sam. The next
time you try to open this document, you must supply a password.
NOTE
I’m sure you have noticed that the second parameter of the call to FileSaveAs includes
an assignment statement! This BASIC-like syntax is such anathema to veteran Object
Pascal programmers that I am tempted to simply mutter a few imprecations and then
move on as quickly as possible. Perhaps I should, however, take a moment to clarify
the situation. The issue here is that you need to state simultaneously the name of the
parameter you want to reference, as well as its value. The code shown above performs
this task. Other than these few basic hints, I don’t want to chance any more in-depth
explanations of this syntax. This book is on Delphi, not BASIC.
You should remember, however, that the parameters you pass to OLE automation
objects are not necessarily part of the Object Pascal language. All Delphi does is pass
the parameters onto Word, where your code can be ground up by the exceedingly fine
teeth of a BASIC interpreter. Delphi performs little syntax checking on these param-
eters. If Word does not like your syntax, it will raise an exception.
OLE automation is a powerful technology that is likely to have enormous impact on
the programming world. It does, however, expose the unwary to a host of foreign
syntaxes, much as the introduction of intercontinental travel brought the ways of
heathens into the refined courts of Europe!
This line of code is an example of using a named parameter. In particular, you are naming the
Password parameter. Password normally is the fourth parameter passed to this function, but
you can place it in the second position if you call it by name. The following line of code has the
same effect:
V.FileSaveAs(Password := ‘Sam’, Name := ‘C:\SEMPERFI.DOC’);
This code demonstrates that named parameters enable you not only to skip parameters, but
actually to reverse the order in which you call them.
Besides named parameters, Word also supports positional parameters.
V.FileSaveAs(‘C:\SEMPERFI.DOC’,,,’Sammy’);
In the code shown above, the password Sammy is added to SEMPERFI.DOC. Word knows that
you want Sammy to be a password because it resides in the fourth position. In this case the sec-
ond and third parameters use default values because they are left blank.
Note that I can add a carriage return to these lines from the Tao te Ching by inserting a line-
feed character (#13) into the text.
You can look up the use of carriage returns, and information on Word Basic methods shown
in this example, in the Word online Help. Here, for example, is an excerpt from the Word
Help describing what various characters mean when embedded in the text of a Word docu-
ment:
Chr$(9) Tab character
Chr$(11) Newline character (SHIFT+ENTER)
Chr$(13) Paragraph mark
Chr$(30) Nonbreaking hyphen
Chr$(31) Optional hyphen
Chr$(32) Space character
Chr$(34) Quotation mark
This code is specific to BASIC. In Object Pascal you would write, for example, #30 or Chr(30)
rather than Chr$(30).
One of the interesting implications of OLE automation is that it brings even more APIs knocking
at your door, requesting to be mastered. Many Delphi users, for instance, have copies of Word,
Visio, and Excel on their systems. All of these applications have powerful OLE-automation
capabilities. Once you have mastered a few thousand Windows and Delphi API commands,
you can further exercise your mind by learning additional commands in Word, Excel, and Visio.
This is progress, right?
If you think about this situation for a moment, you can see that there is clear need to establish
standards for certain commands. For instance, nearly all document-centered Windows appli-
cation respond to the Alt+F+O keystrokes by letting you open a document. It doesn’t take too
much foresight to see that there should probably be similar standards for performing the same
steps in an automation server! In fact, there are some standards currently being established,
and I show a few of them to you later in this chapter.
Variant Arrays
Delphi enables you to create variant arrays, which are the Delphi version of the safe arrays used
in OLE Automation. Variant arrays (and safe arrays) are costly in terms of memory and CPU
cycles, so you would not normally use them except in automation code, or in special cases where
they provide obvious benefits over standard arrays. For instance, the database code makes some
use of variant arrays.
The most important calls for manipulating variant arrays are VarArrayCreate and VarArrayOf.
In particular, these functions are both used to create variant arrays.
The declaration for VarArrayCreate looks like this:
function VarArrayCreate(const Bounds: array of Integer;
VarType: Integer): Variant;
The Bounds parameter defines the dimensions of the array. The VarType parameter defines the
type of variable stored in the array. A one-dimensional array of variants would be allocated like
this:
MyVariant := VarArrayCreate([0, 5], varVariant);
This array has six elements in it, where each element is a variant. You can assign a variant array
to one or more of the elements in this array. That way you can have arrays within arrays within
arrays, if you so desire.
If you know the type of the elements to be used in an array, you can set the VarType parameter
to that type. For instance, if you knew you were going to be working with integers, you could
write:
MyVariant := VarArrayCreate([0, 5], varInteger);
You cannot use varString in the second parameter; instead, use varOleStr. Remember that an
array of Variant takes up 16 bytes for each member of the array, whereas other types might
take up less space.
Arrays of Variant can be resized with the VarArrayRedim function:
procedure VarArrayRedim(var A: Variant; HighBound: Integer);
The variable to be resized is passed in the first parameter, and the number of elements to be
contained in the resized array is held in the second parameter.
A two-dimensional array would be declared like this:
MyVariant := VarArrayCreate([0, 5, 0, 5], varVariant);
This array has two dimensions, each with six elements. To access a member of this array you
would write code that looks like this:
procedure TForm1.GridClick(Sender: TObject);
var
MyVariant: Variant;
begin
MyVariant := VarArrayCreate([0, 5, 0, 5], varVariant);
MyVariant[0, 1] := 42;
Form1.Caption := MyVariant[0, 1];
end;
Notice that the array performs type conversions for you, as it is an array of variants and not, for
instance, an array of integer.
You can use the VarArrayOf routine to quickly construct a one-dimensional variant array:
function VarArrayOf(const Values: array of Variant): Variant;
The function internally calls VarArrayCreate, passing an array of Variant in the first parameter
and varVariant in the second parameter. Here is a typical call to VarArrayOf:
V := VarArrayOf([1, 2, 3, ‘Total’, 5]);
The following code fragment shows how to use the VarArrayOf function:
procedure TForm1.ShowInfo(V: Variant);
begin
Caption := V[3];
end;
begin
V := VarArrayOf([1, 2, 3, ‘Total’, 5]);
ShowInfo(V);
end;
This routine starts by getting the number of dimensions in the array. It then iterates through
each dimension, retrieving its high and low values. If you created an array with the following
call:
MyVariant := VarArrayCreate([0, 5, 1, 3], varVariant);
the ShowInfo function would produce the following output if passed MyVariant:
DimCount: 2
HighBound: 5
LowBound: 0
HighBound: 3
LowBound: 1
ShowInfo would raise an exception if you passed in a variant that would cause VarIsArray to
return False.
There is a certain amount of overhead in working with variant arrays. If you want to process
the arrays quickly, you can use two functions called VarArrayLock and VarArrayUnlock. The
first of these routines returns a pointer to the data stored in an array. In particular, VarArrayLock
takes a variant array and returns a standard Pascal array. For this to work, the array must be
explicitly declared with one of the standard types, such as Integer, Bool, string, Byte, or Float.
The type used in the variant array and the type used in the Pascal array must be identical.
Here is an example of using VarArrayLock and VarArrayUnlock:
const
HighVal = 12;
Notice that this code first locks down the array, then accesses it as a pointer to a standard array.
Finally, it releases the array when the operation is finished. You must remember to call
VarArrayUnlock when you are finished working with the data from the array:
Data := VarArrayLock(V);
for i := 0 to HighVal do
for j := 0 to HighVal do
Grid.Cells[i, j] := IntToStr(Data^[i, j]);
VarArrayUnLock(V);
Remember that the point of using VarArrayLock and VarArrayUnlock is that it speeds access to
the array. The actual code you write is more complex and verbose, but the performance is faster.
One of the most useful reasons for using a variant array is to transfer binary data to and from
a server. If you have a binary file, say a WAV file or AVI file, you can pass it back and forth
between your program and an OLE server using variant arrays. Such a situation would present
an ideal time for using VarArrayLock and VarArrayUnlock. You would, of course, use VarByte
as the second parameter to VarArrayCreate when you were creating the array. That is, you would
be working with an array of Byte, and accessing it directly by locking down the array before
moving data in and out of the structure. Such arrays are not subject to translation while being
marshaled across boundaries.
Listing 29.1 contains a single example program that encapsulates most of the ideas that you
have seen in this section on variant arrays. The program from which this code is excerpted is
called VarArray.
Listing 29.1. The VarArray program shows how to use variant arrays.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Menus, Grids;
type
TForm1 = class(TForm)
Grid: TStringGrid;
MainMenu1: TMainMenu;
Info1: TMenuItem;
OneDimension1: TMenuItem;
TwoDimension1: TMenuItem;
Show1: TMenuItem;
Normal1: TMenuItem;
LockedArray1: TMenuItem;
procedure bOneDimClick(Sender: TObject);
procedure bTwoDimClick(Sender: TObject);
procedure Normal1Click(Sender: TObject);
procedure LockedArray1Click(Sender: TObject);
private
procedure ShowInfo(V: Variant);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
continues
end.
This code creates a new worksheet in Excel, then fills a range of cells in the worksheet with
random values.
The following example starts a new worksheet in Excel. It then fills part of the sheet with ran-
dom data and adds a second sheet to the worksheet. Finally, it counts the number of sheets in
the worksheet and displays the result in the caption of the Delphi application.
procedure TForm1.Button1Click(Sender: TObject);
begin
V := CreateOleObject(‘Excel.Sheet’);
V.Range(‘A1:D8’).Formula := ‘=RAND()’;
V.Application.Sheets.Add;
Caption := ‘Num Sheets = ‘ + IntToStr(V.Application.Sheets.Count);
end;
Here is a second example that demonstrates a slight variation on the code in the Button1Click
method:
procedure TForm1.Button2Click(Sender: TObject);
begin
V := CreateOleObject(‘Excel.Application’); // Start a new copy of Excel
V.Visible := True;
V.WorkBooks.Add;
V.Sheets.Add;
Caption := ‘Num Sheets = ‘ + IntToStr(V.Sheets.Count);
end;
This code makes sure the application object is visible before adding to the workbook. In par-
ticular, the first two lines of the code start a new version of Excel and launch it without any
open spreadsheets. The call to WorkBooks.Add opens up a workbook with a number of spread-
sheets in it, and so on.
The Button1Click method shown starts a new worksheet in an already running copy of Excel,
if one is available; if Excel is not already running, this method starts it. The Button2Click method
always starts a new copy of Excel, even if one is already running.
To understand automation with Excel, you have to grasp a few of its OLE automation objects.
In particular, Excel has an Application object, a WorkBook object, a Range object, and a Sheets
object. For instance, in the Button1Click and Button2Click methods you are accessing, among
other things, both the Range and Sheets objects that reside inside Excel. You should read the
Excel online Help to get a better feeling for how these objects work.
Assuming you have created an Excel OLE object by using something similar to these code frag-
ments, you can access the individual cells of a spreadsheet like this:
procedure TForm1.bFancyAryClick(Sender: TObject);
var
i, j: Integer;
Ch: Char;
begin
for i := 1 to 10 do
for j := 1 to 10 do
SimpAry.Cells[j, i].Value := i * j;
for i := 1 to 10 do begin
Ch := Chr(64 + i);
SimpAry.Cells[11, i].Value := Format(‘=Sum(%s1:%s10)’, [Ch, Ch]);
end;
end;
Notice that the code shown here accesses the cells both as an array of Integer and an array of
String. In particular, it fills out a 10×10 grid with the multiplication table, and then adds for-
mulas into the 11th row, which lies directly beneath the times table. The formulas calculate
the sum of each column in the table.
The code shown in the FancyAryClick method is a classic example of how variants are inte-
grated into the heart of OLE automation. If Delphi did not support variants, writing code like
that in this section would be considerably more complicated. In particular, Cells.Value must
be treated as both an array of Integer and an array of String. Variants make these easy to ex-
press and easy to access.
Then run the application. It would return immediately, without the main form appearing. You
could then remove the /regserver line from the Parameters dialog. This step should not be
required in any of the shipping versions of Delphi 2.0.
As you might have surmised, this step was needed to register the object in the registration da-
tabase. If you want to remove the object, you can run your application with the parameter /
unregserver .
In the shipping version of Delphi, if you look in the code for the MyAuto unit, you should find
the following declaration:
AutoClassInfo: TAutoClassInfo = (
AutoClass: TMyAuto;
ProgID: ‘AUTOPROJ.MyAuto’;
ClassID: ‘{FE67CF61-2EDD-11CF-B536-0080C72EFD43}’;
Description: ‘Sam’;
Instancing: acMultiInstance);
The key elements here are the ProgID and the ClassID. When you ran the program with the
/regserver parameter, these keys were added to the registration database. ProgID is an
alpha-numeric alias for the ClassID. The alias is needed because it would be awkward for pro-
grammers to refer to the class by its lengthy, opaque-looking number. As a result, a relatively
easy-to-manage program ID is created. To confirm the existence of these keys, run
REGEDIT.EXE from its home in the Windows directory.
After starting RegEdit, expand HKEY_CLASSES_ROOT and search for the CLSID key.
Expand the CLSID node and search for your ClassID. In the example above, that number begins
with FE67F61..., though the number will almost certainly be different if you created the ap-
plication from scratch on your system. The whole point of creating a ClassID is that it should
prove to be unique if generated by an internal Microsoft algorithm called automatically for
you by Delphi.
NOTE
The idea that a number such as CLSID is said to be statistically unique is the source of
considerable humor in the computer world. However, a CLSID has a fairly large
number of significant digits, and Microsoft does, in fact, have the resources and
intelligence to implement an algorithm that can generate something at least potentially
worthy of the name “random number.” I am not, however, qualified to take a stand on
this matter. I simply state that this concept is key to the entire implementation of
OLE, and therefore I at least choose to assume that Microsoft’s algorithm is well-
designed. After distributed OLE is released, this issue is bound to receive a certain
amount of public attention, at which time various numerical experts will weigh in with
their opinions on the matter.
As explained a moment ago, underneath the CLSID for your program are two entries:
LocalServer32 and ProgID . The ProgID is the same as the similar entry in the AutoClassInfo
structure shown above. The LocalServer32 entry is the path and name of your executable.
If your program compiles, and if you have added the entry successfully in the registration da-
tabase, then you have successfully completed a Delphi automation server.
Needless to say, the steps for constructing an OLE server outlined in this section are relatively
easy to master. However, as you are about to see, some of the implications of this technology
can become rather complex.
If multiple clients can safely access your server at the same time, you should declare it to be of
type acMultiInstance. If you need to ensure that only one client at a time can access your server,
you should set its type to acSingle. If you want to declare an object that will be used only in-
ternally by your application, you should declare it as acInternal.
For instance, you could add a method that creates a pop-up dialog box:
TMyAuto = class(TAutoObject)
private
{ Private declarations }
automated
{ Automated declarations }
procedure ShowDialog;
end;
implementation
uses
Dialogs;
procedure TMyAuto.ShowDialog;
begin
ShowMessage(‘hi’);
end;
The ShowDialog procedure now is available to other applications, just as the Insert routine in
Word and the Add routine in Excel are available to other applications. The act of placing a
property or method in the automated section is all that’s needed to make it part of your object’s
public interface.
The following types are legal to use in the declarations for the methods or properties in the
Automated section:
Currency
Double
Integer
LongInt
Single
SmallInt
String
TDateTime
Variant
WordBool
The following types are illegal to use in the declarations for the methods or properties in the
Automated section:
Arrays
Boolean
Byte
Cardinal
Comp
Pointer
POleStr
Records
ShortInt
Word
There is no need for you to memorize these list of types, as the compiler will report an error if
you try to use an illegal type in an automation server.
NOTE
When you change the interface of an existing automation server, you should always
make sure the interface is backward-compatible. That is, don’t remove properties or
methods already included, as that will cause errors in existing clients. You should only
add to existing interfaces. If you modify an existing interface in ways that are not
backward-compatible, you should change the object’s name as well.
Here is a list of further rules, stolen from the Delphi online Help, that you must follow inside
the automated section of a TAutoObject declaration:
■ Property declarations can include only access specifiers (Read and Write). No other
specifiers (Index, Stored, Default, NoDefault) are allowed.
■ Access specifiers must list a method identifier. Field identifiers are not allowed.
■ Property-access methods must use register calling conventions.
■ Array properties are supported.
■ Property overrides (property declarations that don’t include the property type) are not
allowed.
■ Method declarations must use register-calling conventions. Methods can be virtual,
but not dynamic. Method overrides are allowed.
■ A property or method declaration can include an optional dispid directive, which
must be followed by an integer constant expression that gives the dispatch ID of the
property or method. If a dispid clause is not present, the compiler automatically picks
a number one larger than the largest dispatch ID used by any property or method in
the class and its ancestors. Specifying an already-used dispatch ID in a dispid clause
causes an error.
It’s now time to take a look at the code for the entire application, which is called AutoProj.
Listing 29.2 contains the code for the automation unit. Listing 29.3 contains the DPR file for
the automation server.
Listing 29.2. The code for the MyAuto unit shows how to create a simple
automation server.
unit MyAuto;
interface
uses
OleAuto;
type
TMyAuto = class(TAutoObject)
private
FMyProp: string;
implementation
uses
Dialogs;
constructor TMyAuto.Create;
begin
inherited Create;
end;
procedure TMyAuto.ShowDialog;
begin
if FMyProp = ‘’ then
FMyProp := ‘This object has a property called MyProp’;
ShowMessage(FMyProp);
end;
procedure RegisterMyAuto;
const
AutoClassInfo: TAutoClassInfo = (
AutoClass: TMyAuto;
ProgID: ‘AUTOPROJ.MyAuto’;
ClassID: ‘{FE67CF61-2EDD-11CF-B536-0080C72EFD43}’;
Description: ‘Sam’;
Instancing: acMultiInstance);
begin
Automation.RegisterClass(AutoClassInfo);
end;
initialization
RegisterMyAuto;
end.
uses
OleAuto,
Forms,
MAIN in ‘MAIN.pas’ {Form1},
myauto in ‘myauto.pas’;
{$R *.RES}
begin
if Automation.ServerRegistration then Exit;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
In these declarations, take special notice of the call to the TAutomation object’s ServerRegistration
method. This code updates the registry if you passed /regserver or /unregserver as a param-
eter to your application. Remember that you must pass /regserver to it at least once, so as to
enter the proper items in the registration database.
Now that you have a complete (and functional) automation server, you can easily test it by
creating a separate application that references the OleAuto unit and which contains a few addi-
tional lines of code:
uses
OleAuto;
When you run this second application and press the button that calls the Button1Click method,
the automation server is automatically launched, and the ShowDialog procedure called. An ex-
ample program, called TestDA, is available on the book’s CD-ROM.
NOTE
OLE automation controls can be publicized to the outside world through something
called a type library. Delphi does not support type libraries, and there are no tools for
building them in the package. Instead, you should just provide the interface to your
object—which will, of course, provide all the information found in a type library.
Furthermore, as explained above, the call to ShowDialog is not resolved at compile time.
By reading type information, it’s at least theoretically possible to resolve this call at
compile time, or at least to check the syntax of the call, but this version of Delphi
makes no forays in that direction.
In the last two sections you learned the basics about making an OLE automation server. The
rest of this chapter is dedicated to explaining some of the implications of this capability.
TMemoApp is the object with a ProgID of MemoEdit.Application. A copy of this object can be
obtained through a call to CreateOleObject. The TMemoDoc object has no ProgID, no ClassID,
and no AutoClassInfo record, and cannot be created through a call to CreateOleObject or to
any other OLE function. Instead, you must access this second object using a copy of the TMemoApp
object.
TMemoApp is called an externally creatable object. TMemoDoc is called a dependent, or nested, object.
From the point of view of the AutoDemo application, the automation objects in the MemoEdit
application look like this:
TMemoApp
TMemoDoc
This diagram does not imply that TMemoDoc is a descendant of TMemoApp , only that it is
contained by TMemoApp. Containment implies that you can create or access copies of TMemoDoc
using a copy of TMemoApp. This type of diagram is used widely in OLE automation documen-
tation; for instance, look up “Application Object” in the Excel online Help. There you will
find the following excerpt from the Excel “hierarchy” of objects:
Application
WorkBook
WorkSheet
Button
Once again, this is not an object hierarchy in the traditional sense, but only a way of diagram-
ming the relationship between objects, where the word “relationship” designates which object
contains another. An Application object can contain WorkBook, WorkSheet, and Button objects.
Application and WorkSheet objects are creatable objects, while button objects are dependent
objects. Note that from inside of Excel or MemoEdit, all of the objects belonging to those
applications are creatable. The idea of creatability versus dependence is only valid from the
OLE client’s point of view.
WorkBook is not a descendant of the Excel Application object, but you can obtain or reference
copies of it once you have a copy of the Application object. TMemoDoc is not a descendant of
TMemoApp; however, it can be obtained or referenced from TMemoApp. In short, TMemoApp con-
tains TMemoDoc.
In the MemoEdit application, the TMemoApp object handles high-level functions involving the
entire user interface, while the TMemoDoc object handles tasks specific to a single document. In
particular, TMemoApp gives you access to the following methods:
function NewMemo: Variant;
Creates a new editor window and returns the window’s automation object.
procedure TileWindows;
Tiles all open editor windows.
procedure CascadeWindows;
Cascades all open editor windows.
The TMemoDoc object, on the other hand, has the following interface:
procedure Clear;
Clears the contents of the memo.
procedure Save;
Saves the contents of the memo.
procedure Close;
Closes the memo.
As you can see, all the methods in TMemoDoc involve handling a single document. TMemoApp, on
the other hand, creates new instances of TMemoDoc, and enables you to manipulate the program’s
user interface.
All of the methods of TMemoApp manipulate a single instance of the TMainForm object, which is
the main object for the entire application:
procedure TMemoApp.CascadeWindows;
begin
MainForm.Cascade;
end;
Take a moment to consider the implications of this code. TMemoApp doesn’t do anything in and
of itself. Instead, it is just a wrapper around the TMainForm object.
In practice, this means you can design an entire application using standard Delphi objects. Then,
if you want to turn it into an OLE automation server, you can add additional TAutoObject
descendants that provide an interface, allowing you to manipulate one or more of the objects
inside your application.
You cannot simply add an automated section on to an existing TForm object because TForm is
not a descendant of TAutoObject. Instead, you must create separate objects, such as the TMemoApp
and TMemoDoc objects shown here. TMemoApp controls the TMainForm object, and TMemoDoc con-
trols the TEditForm object found in EDITFRM.PAS.
NOTE
It’s interesting to speculate whether the developers of Delphi ever considered adding
the functionality of TAutoObject to one of the root objects in the Delphi hierarchy.
This would be a powerful step from a syntactical point of view, but it would undoubt-
edly also entail a penalty in terms of performance.
Containment in Detail
The most interesting lines of code in the TMemoApp object are the ones that retrieve copies of
the TMemoDoc object. These are the methods that allow the container, TMemoApp, to return its
dependent object, which is called TMemoDoc. Here are the implementations of the three meth-
ods that give you access to a TMemoDoc instance:
function TMemoApp.GetMemo(Index: Integer): Variant;
begin
Result := TEditForm(MainForm.MDIChildren[Index]).OleObject;
end;
Notice that all of these methods return a Variant, not an instance of TMemoDoc. The issue here
is that a Variant can contain an OLE object, as you saw earlier when you learned how to call
CreateOleObject . In particular, the most common way to pass an OLE object across bound-
aries in an automation application is to first wrap it in a Variant.
To understand the call to the OleObject property, you must refer back to the declaration for
TAutoObject that appears in OLEAUTO.PAS:
TAutoObject = class(TObject)
private
FAutoDispatch: TAutoDispatch;
…
protected
procedure GetExceptionInfo(ExceptObject: TObject;
var ExcepInfo: TExcepInfo); virtual;
public
constructor Create; virtual;
destructor Destroy; override;
function AddRef: Integer;
function Release: Integer;
property AutoDispatch: TAutoDispatch read FAutoDispatch;
property OleObject: Variant read GetOleObject;
property RefCount: Integer read FRefCount;
end;
end;
All Delphi OLE objects are descendants of TAutoObject. TAutoObject exists so that you don’t
have to call messy IDispatch methods such as GetIDsOfNames and Invoke. The primary job of
TAutoObject is to hide that kind of complexity from Delphi programmers so that automation
presents a friendly easy-to-use face to the programmer.
NOTE
The IDispatch object is used by OLE to implement automation. When you call
CreateOleObject , what you get back is an IDispatch interface.
When looking at the TAutoObject declaration, take special notice of the two properties called
AutoDispatch and OleObject. TAutoObject and another object, called TAutoDispatch , reference
each other internally and work together to make automation easy for you to implement.
TAutoDispatch is Delphi’s implementation of the IDispatch interface.
NOTE
Never call the Free or Destroy methods of an automation object. OLE objects are
reference-counted, because they might be in use by more than one client. When you
are finished using an OLE object, call its Release method. Release decrements the
reference count. If the reference count reaches zero, the object automatically destroys
itself.
The IsInprocServer states whether your automation object is an InProcServer or a Local server.
The StartMode property returns one of the values smStandAlone, smAutomation, smRegServer,
or smUnregServer. The following table describes these four possible values:
Value Meaning
smStandAlone A user started the application.
smAutomation Windows started the application for the purpose of creating an
OLE object.
smRegServer The application was started solely to register one or more OLE
objects.
smUnregServer The application was started solely to unregister one or more OLE
objects.
The OnLastRelease is triggered when a server started by the system for automation purposes
no longer is needed. This occurs when all clients have released all OLE objects created by the
server. Delphi usually automatically shuts the server down once it is no longer needed, although
you can override this behavior if you wish.
OnLastRelease gets a Boolean var parameter called ShutDown, which is True by default. Setting
ShutDown to True overrides Delphi’s default behavior, and keeps the server in memory even though
it no longer is needed.
Summary
OLE Automation is a protocol by which one application can access an object that resides in-
side another application or DLL. Access to this object enables you to:
■ Control the actions of an application or DLL.
■ Access the features of an application or DLL.
An application that can be automated is called an automation server. An application that auto-
mates another is an automation client. An application can be simultaneously both a server and
a client.
Delphi supports easily building both automation clients and servers. That is, you can use an
application written with Delphi to automate another application, or you can set up your appli-
cation so that it can be automated by other applications.
In the next chapter you learn about in-process automation servers and more about local serv-
ers. The discussion then moves on to take an in-depth look at how automation servers work
behind the scenes.
In this chapter you learn how to create classes based on the Component Object Model (COM).
COM is the technology that underlies OLE, but is not itself part of OLE. It’s a more funda-
mental layer of the operating system, implementing objects that can be used across address spaces
and across languages. Someday it will work across operating systems, and across machines that
are connected to a network.
At the time of this writing the only major non-OLE architecture implemented using COM is
the Microsoft Games SDK. The games SDK is fun, and I discuss it in Chapter 32, “Games
SDK.” However, it is not enough of a force to create a compelling argument for COM as a
major technology. Why, then, should I bother to include this chapter in this book?
Here are three reasons to suppose that the COM architecture may be of importance to the
general programmer:
■ COM is the architecture upon which OLE is based. OLE automation, OCXs, and (to
a much lesser degree) object linking and embedding all are major forces, or potentially
major forces, in the computer industry. As developers we owe it to ourselves to
understand the technology upon which they are based.
■ COM represents a way to implement objects on the level of the operating system.
This means that objects need no longer be created as a layer that lies on top of the
linear code base, which forms that core of the operating system. Instead, objects can
be integrated into the operating system itself, and can act as natural extensions to that
operating system.
It’s likely that future operating systems will themselves be object-oriented; more
specifically, they could be based on COM. This means that COM technology would
not only be tightly integrated with the operating system, but would be a part of its
warp and woof.
(When you find yourself asking, no doubt repeatedly, “Why is COM so compli-
cated?”, the answer is that Microsoft has grandiose plans for this system and wants to
make it as flexible as possible. It’s arguable that that flexibility is coming at too high a
price in terms of complexity. However, an object-oriented operating system could well
prove to be a significant boon to all computer users.)
■ COM objects are designed to cross natural boundaries, such as those that exist
between programs, between languages, between machines, and between operating
systems. If you create a COM object in Delphi, you can instantiated it in a C++ or
BASIC program. Furthermore, a C++ programmer could create “descendants” of your
object; that is, they could extend the object by adding additional interfaces.
COM objects also can cross the boundaries between programs. That is, they can reside
in one program and be instantiated in another program. (I merely hint at this
technique in this chapter. You can, after all, use OLE automation to achieve the same
end.)
Finally, at the time of this writing Microsoft is planning to roll out extensions to the
COM model that will allow it to communicate across machine- and operating-system
boundaries. This means that you could instantiate an instance of an object that existed
on another machine, even if that machine was running on a different operating
system, such as UNIX or the Mac.
Having said all this, I need to stress that right now all we have is OLE and the Games SDK.
Whether everything that I have described above actually will become true is still a matter of
conjecture. We will have object-oriented operating systems based on COM technology on the
day when such a creature is actually released into the world and physically sold in stores or
mail-order houses, and not a moment before. Even networked OLE is not yet a reality, although
Visual Basic 4.0 offers some technology that mimics portions of its behavior. In other words,
nothing of this nature actually exists until it ships, and that casts a somewhat problematic pall
over this entire discussion.
There’s also the issue of whether you believe in Microsoft’s vision of the future, or whether you
are more inclined toward competing technologies (such as CORBA and SOM). But these are
religious issues (and, despite its pro-Delphi bias, this text refuses to give in to its weakest im-
pulses and become an overtly religious book. Our motto: “No language or OS bigots allowed!”)
If you work in both C++ and Delphi, it is arguable that it would be foolish to use anything but
COM for most of your serious work. After all, why create objects that are bound to Delphi or
bound to C++? Why not create objects on the operating-system level, and then use them in
both your C++ and Delphi projects? This is a completely realistic goal, fully realizable at the
time of this writing. Indeed, the MS Games SDK shows that it can be done, and that the re-
sulting code can be very fast. It’s the hard evidence. Furthermore, OLE promises to be one of
the major technologies of the next few years, and it is based on COM objects. If you want to
understand OLE, then, you have to understand COM objects.
This chapter shows you how to create COM objects that reside in DLLs. You can share these
COM objects between Delphi, C++, and other languages. This chapter also hints at how to
create COM objects that reside in executables; although, as I suggested above, this is a more
complex subject and one that is perhaps always going to play second fiddle to OLE automa-
tion.
COM object example. It’s just a simple Pascal object. However, it’s doing something that you
might think Delphi objects can’t do: it’s letting you call the methods of this object even though
it resides in a DLL. Furthermore, the techniques used to access this object form a sort of min-
iature introduction to the COM object model.
The point here is that COM objects also let you do this. However, you can also instantiate an
instance of a COM object that resides in another executable, which is something you can’t do
with a VCL object. (OK, “can’t” probably is too strong a word. But it would be difficult.)
There are three reasons why I’m showing you this example:
■ This is a useful general programming technique that can help you write small, flexible
programs. Specifically, you can place large object hierarchies in DLLs, then load these
DLLs dynamically by calling LoadLibrary and FreeLibrary. That way you can load
your main program without having to simultaneously load all its modules. Further-
more, objects that are kept in DLLs can be shared by multiple applications. The point
here is that even if there were no COM object model, and no need for a COM object
model, this technique still would be useful.
■ It’s simple to do this kind of thing using Delphi objects, while it’s relatively hard to do
it using COM objects. Therefore, one of the reasons for showing you this example is
to let you know that there is simple way to accomplish these ends. If all you want is to
put objects in DLLs and call them, there’s no need to use COM objects. Just use the
simple technique shown here. COM objects buy you cross-language capabilities that
extend beyond Borland C++ and Delphi.
■ The third reason for showing you this code is that it serves as the perfect lead-in for
using COM objects. Many of the principles shown here surface again when using
COM objects. However, these Object Pascal-based examples are fairly easy to under-
stand, while the COM material can be a bit confusing, especially the first time you see
it. In a sense, then, this is COM objects with training wheels. It introduces, in
simplified form, some concepts that reappear in more complex form later in this
chapter.
It will perhaps be simplest to start by showing you the DLL you must create. Once you have
that down, the next step is to see how to call the DLL from an executable. There’s nothing at
all unusual about the DLL. It’s just a standard DLL that contains a few unusual-looking code
constructions. The tricky code appears in the executable; although, once again, this is a simple
trick, and one that’s easy to utilize. Listings 30.1 and 30.2 show the code for the DNOOLE.DLL.
Listing 30.3, presented later in this section, shows the code for the executable.
uses
Forms,
myvclobj in ‘myvclobj.pas’;
{$R *.RES}
exports
CreateObject name ‘CreateObject’;
begin
end.
Listing 30.2. This module contains the object that is exported from the
DNOOLE.DLL.
unit myvclobj;
interface
const
ID_MyObject = 1;
type
TMyObject = class
function AddOne(i: Integer): Integer; virtual;
function AddTwo(i: Integer): Integer; virtual;
end;
implementation
end.
This DLL exports a simple object called TMyObject. You can pass a number to the methods of
this object and it increments that number by either one or two. In short, the methods don’t do
anything at all interesting. I include them simply so you have something to test, and because
they produce predictable return values that make it easy to tell whether or not your calls to the
DLL actually are succeeding.
It’s time now to look at the code. Of course, the DPR file for the DLL uses the word library
rather than program:
library dnoole;
Notice that both of these methods are declared as virtual. It is important that all the methods
in your object are declared this way.
When you want to export the object from the DLL, you call the CreateObject method:
function CreateObject(ID: Integer; var Obj): Boolean;
var
M: TMyObject;
begin
if ID = ID_MyObject then begin
M := TMyObject.Create;
Pointer(Obj) := M;
Result := True
end else begin
M := nil;
Result := False
end;
end;
This is the method that looks so much like the COM code you see later in this chapter. In
particular, you should compare this method with the QueryInterface method that is part of
every COM object, and also with the IClassFactory.CreateInstance method. But I’m getting
ahead of myself.
The CreateObject method expects to be passed both an ID and a typeless variable that is as-
signed to an instance of TMyObject. In this particular case, there’s no need for you to pass in an
ID. It would be useful only if you had multiple objects that you wanted to export, and you
wanted some simple method to designate which object it was that you wanted to retrieve. For
instance, you could assign the number 1 to your first object and the number 2 to your second
object. Then, if you passed in ID 1 you would get the first object back, and if you passed in ID
2 you would get the second object back. I’m implementing this method here because it might
be useful to you in your own code, and because it exactly parallels a technique used in COM
code.
The second parameter of CreateObject is a typeless parameter. If I were implementing this code
from scratch, with no guidelines, I would probably make this variable a pointer, or else assign
it type TObject. However, COM code uses these typeless types in similar situations, so I have
mirrored that style in this example.
The code checks first to see if it is being passed a valid ID. If the ID is not valid, the method
returns nil in the second parameter. If the ID is valid, the method returns an instance of
TMyObject:
Notice that I typecast the typeless parameter to be a pointer. I do this merely to please the
compiler, which uses its valuable type-checking skills to be sure that I’m writing code that at
least makes some kind of nominal sense.
As I said earlier, the DLL is simple and contains only a small amount of fairly easy-to-read
code. There are a few tricky lines in the CreateObject method, but the code in that method is
easy enough to understand once you grasp its purpose.
The next step, of course, is to call this DLL from an executable. The ENoOle example pro-
gram in Listing 30.3 demonstrates how to proceed.
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TMyObject = class
function AddOne(i: Integer): Integer; virtual; abstract;
function AddTwo(i: Integer): Integer; virtual; abstract;
end;
TForm1 = class(TForm)
AddOne: TButton;
AddTwo: TButton;
continues
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
This program enables you to call the two methods stored in the DLL. It posts the results of the
method in the caption of the ENoOle application, as shown in Figure 30.1.
FIGURE 30.1.
The ENoOle application
calls methods from an
object stored in a DLL,
then prints the results of
those calls in its caption.
NOTE
Yes, it’s a little strange to use an application’s caption to display data. However, I find
myself doing this kind of thing quite often when I’m experimenting with new ideas, so
I thought I might add some verisimilitude to this example by presenting it in this
fashion! It also has the benefit of helping me keep the code as simple as possible.
Under normal circumstances, you could not export TMyObject from a DLL because there’s no
way to get at the type information and variables declared in the interface of the DLL. DLLs
simply don’t know how to export that kind of information. As a result, Pascal objects declared
in DLLs typically have been out of reach of the application that wanted to call them. The point
of the ENoOle example is that it shows a trick demonstrating how to get at those DLLs. As far
as I know, this is not a documented trick, so there is no guarantee that it will work in future
versions of Delphi. However, I have reason to believe that this particular technique will not
disappear any time soon. I might also add that you can use this same technique to import a
C++ object into your program!
Here’s how it works. This program imports the sole exported function from the
DNOOLE.DLL:
function CreateObject(id: Integer; var Obj): Boolean;
external ‘DNOOLE’ name ‘CreateObject’;
This is the standard technique for importing a routine from a DLL, and there’s absolutely
nothing unusual here. You simply declare the method and its parameters, then tell the com-
piler that it is defined externally. To wrap up, you give the explicit name of the external mod-
ule in which it is defined, and then state the name under which it is exported.
Once again, there is nothing tricky about importing the CreateObject method into the execut-
able. What is tricky, however, is both the fact that TMyObject must be redeclared in the execut-
able and, more specifically, the way it is redeclared:
TMyObject = class
function AddOne(i: Integer): Integer; virtual; abstract;
function AddTwo(i: Integer): Integer; virtual; abstract;
end;
This may look like nonsense at first blush. After all, the keyword abstract is used to designate
a method that is not implemented. Furthermore, calling an abstract method terminates your
application. So what good does it do you to declare these abstract methods? What you need
is something you can call, not a method that’s explicitly declared in a manner that prevents
you from calling it!
NOTE
Abstract methods exist because there are times when you want to define base classes
that are not meant to be instantiated. For examples of this technique, see the TStrings
and TStream class, both of which are declared in CLASSES.PAS. Of course, there are
descendants of both TStrings and TStream that are used all the time in Delphi, but you
would not normally ever see an instance of either of these classes. If you tried to create
an instance of one of these classes you would succeed, but the act of calling certain
methods in the class would result in an exception being raised, and your application
summarily closing.
The point here is that a call to an abstract method is an error, and Delphi has to
provide some way of letting your know that things have gone wrong. Terminating your
application is certainly one effective means of letting your know that all is not well!
By now it should be clear that calls to abstract methods usually result in an error. So why are
the methods in TMyObject declared as abstract?
Well, the trick here is that redeclaring TMyObject as shown above is a valid way to access the
methods of the real TMyObject under certain circumstances. In particular, it works as long as
you can get at the virtual method table for a valid instance of the object! In other words, if you
can ask the DLL for a pointer to a fully initialized instance of TMyObject, you can use the
abstract method declaration to call its methods:
var
AObj: TMyObject;
begin
if CreateObject(1, AObj) then begin
Caption := IntToStr(AObj.AddTwo(5));
AObj.Free;
end;
end;
The CreateObject call shown here retrieves a pointer to the virtual method table for a fully
implemented instance of TMyObject. If you like, you can think of it as retrieving a pointer to
TMyObject. Given that pointer, and an abstract declaration of TMyObject, you can call the methods
of TMyObject in the normal manner.
As I just stated, an abstract method normally terminates your application when it executes.
The point here is that it’s not the compiler that forces your application to close when you call
an abstract method. Instead, an actual call to a method called Abstract is made. The purpose
of that call is to raise an exception that closes your application. In the previous example, how-
ever, the compiler jumps not to an instance of the abstract method, but instead jumps to the
valid methods exported from the DLL. In short, this technique tricks the compiler into calling
your method instead of the abstract routine defined by the creators of Delphi. The end result
is that you can call the methods of an object that resides in a DLL as long as all of those meth-
ods are declared as virtual.
NOTE
Although I have not tested the technique, I can see no reason why you could not have
static, nonvirtual routines in the objects that you export from a DLL. However, those
routines could not be called directly from outside the DLL. Furthermore, I would
place them after the declarations for your virtual method table. You could then access
these routines through virtual methods in the object you exported.
Once again, this entire example represents a useful trick that happens to mimic some of the
standard behaviors of certain COM objects. Use this technique when you need it, and study
its implementation so you can recycle this knowledge when you are working with the COM
examples shown below.
NOTE
I use the term binary above and in other places in this chapter because I want to refer
to both executables and DLLs with a single word. In most cases, where I use the word
in this chapter, you can think of binary as a macro that expands to DLLs and EXEs.
COM objects can reside in either DLLs or executables—or, eventually, on remote machines.
When they reside in DLLs they are known as in-process servers . When they reside in executables
they are known as local servers. At the time of this writing Microsoft has not yet released the
specification for creating distributed objectsthat reside on remote machines.
The first COM object example resides in a DLL called SimpObj. Later in this chapter you see
a client program, called MakeCom1, that instantiates an instance of this object and calls some
of its methods. The code for SIMPOBJ.DLL is shown in Listing 30.4. Listing 30.5 shows the
code for MYOBJ.PAS, which contains the COM object to be exported from the DLL. Listing
30.6 contains the registration file for SIMPOBJ.DLL. All the source files for this example are
found on the UnleashedCD-ROM in the SIMPDLL directory.
Listing 30.4. The code for SIMPOBJ.DLL, the DPR file controlling the COM object
that resides in a DLL.
library SimpObj;
uses
Ole2, OleAuto, SysUtils,
myobj in ‘myobj.pas’;
{$R *.RES}
try
MyClassFactory := IMyClassFactory.Create;
except
Result := E_OUTOFMEMORY;
Exit;
end;
hr := MyClassFactory.QueryInterface(riid, ppv);
Result := hr;
end;
exports
DllGetClassObject name ‘DllGetClassObject’,
DllCanUnloadNow name ‘DllCanUnloadNow’;
begin
end.
Listing 30.5. MYOBJ.PAS contains the COM object to be exported from the DLL.
unit myobj;
{
There are three things you need to keep track of:
1) How many objects have been created
2) How many references each object has
3) How many locks are on each object
If all three are zero, then DllCanUnloadNow will return true.
}
interface
uses
Ole2, Windows, SysUtils;
const
{ I’ve lost track of my block of GUID’s, so this one might be dup on
your system! Kind of unlikely, though... Best to use GENGUID.EXE. }
CLSID_MyObject: TGUID = (
D1:$C9B0B160;D2:$1308;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
continues
IMyObject = class(IUnknown)
private
FRefCount: LongInt;
FObjectDestroyed: TObjectDestroyed;
public
constructor Create(ObjectDestroyed: TObjectDestroyed);
function QueryInterface(const iid: TIID; var obj): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
function GetTwelve: Integer; virtual;
end;
IMyClassFactory = class(IClassFactory)
private
FRefCount: LongInt;
public
constructor Create;
function QueryInterface(const iid: TIID; var obj): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
function CreateInstance(unkOuter: IUnknown; const iid: TIID;
var obj): HResult; override;
function LockServer(fLock: BOOL): HResult; override;
end;
var
LockCount: Integer;
ObjCount: Integer;
{ F: TextFile; }
implementation
Inc(FRefCount);
Result := FRefCount;
end;
{ -- IMyClassFactory -- }
constructor IMyClassFactory.Create;
begin
inherited Create;
FRefCount := 0;
end;
continues
MyObject := IMyObject.Create(ObjectDestroyed);
hr := MyObject.QueryInterface(iid, Obj);
if Failed(hr) then
MyObject.Free
else
Inc(ObjCount);
Result := hr;
end;
initialization
{ AssignFile(F, ‘c:\info.txt’);
ReWrite(F);
WriteLn(F, ‘Initialization’); }
LockCount := 0;
ObjCount := 0;
finalization
{ CloseFile(F); }
end.
HKEY_CLASSES_ROOT\CLSID\{C9B0B160-1308-11cf-AB35-0000C07EBA2B} =
MyObject Test 1
HKEY_CLASSES_ROOT\CLSID\
{C9B0B160-1308-11cf-AB35-0000C07EBA2B}\ProgID = MyObject1.0
HKEY_CLASSES_ROOT\CLSID\{C9B0B160-1308-11cf-AB35-
0000C07EBA2B}\VersionIndependentProgID = MyObject
HKEY_CLASSES_ROOT\CLSID\{C9B0B160-1308-11cf-AB35-0000C07EBA2B}\InProcServer32 =
c:\src\unleash\chap30\simpdll\simpobj.dll
HKEY_CLASSES_ROOT\CLSID\{C9B0B160-1308-11cf-AB35-0000C07EBA2B}\NotInsertable
This DLL, of course, cannot be run on its own. Before you can test it you first must create a
client, or container, program called MakeOle1. As a writer I normally try to get readers up and
running with working example programs as quickly as possible. However, in this case, I’m going
to ask you to wait awhile before viewing the MakeOle1 program. The issue here is that there is
so much material to cover that I can’t imagine throwing more code at you before explaining at
least something about the code you have seen so far.
The last paragraph implies that there is considerable complexity in creating even the simplest
COM objects. On the whole, I would have to agree that this is a fairly complex subject. How-
ever, this material is not as complex as it’s often rumored to be in the computer press. Take
your time with it; study it carefully, and you’ll find that it yields up its secrets in a fairly short
period of time. There is some complexity here, perhaps more than there needs to be, but it is
not insurmountable.
As far as COM objects are concerned, the registry is a simple database that has one primary
task: it associates a numerical value with each of the COM objects available on the system.
This is not a complete definition of the registry, but it’s what you need to know about it for
now.
To work with the registry, open up REGEDIT.EXE. You will see something that looks like
the screen shot in Figure 30.2.
FIGURE 30.2.
The Registry Editor displays
this screen when it first
opens.
Open up the tree called HKEY_CLASSES_ROOT. Scroll down the list of entries until you
come to one called CLSID. Open up the CLSID tree and scroll down the entries, as shown in
Figure 30.3.
FIGURE 30.3.
The CLSID section of
HKEY_CLASSES_ROOT
in the RegEdit program.
The CLSIDs are on the left;
the associated program,
class, or file type is on the
right.
You might notice that, as you scroll down the list, various familiar names appear on the right
side of the RegEdit screen. For instance, the CLSID {0003000C-0000-0000-C000-
000000000046} is associated with the Package program. Scroll down even further in
HKEY_CLASSES_ROOT, and you come to an entry on the left side that reads Package. If
you open up the tree for the Package entry, as shown in Figure 30.4, you see a CLSID entry
identical to the one you found earlier.
FIGURE 30.4.
The Package entry in
REGEDIT.EXE has leaves
called CLSID, Protocol,
Server, and Verb.
The point here is that you can find the CLSID of the Package program by looking it up by
name in the registry. Conversely, if you have the CLSID of an object, you can find out its name.
Figure 30.4 suggests that there also are other important pieces of information stored in RegEdit
and associated with the Package program, but for now all you need to know is that it’s a place
to store CLSIDs.
If you want to insert an entry, or set of entries, in the registry, you can do so by preparing text
like the WIN32.REG file shown in Listing 30.6. Consider the following entry in that file:
HKEY_CLASSES_ROOT\MyObject\CLSID =
{C9B0B160-1308-11cf-AB35-0000C07EBA2B}
I have divided this into two lines because of space considerations. You, however, should type
this in as a single line. The code shown here tells the registry to create an entry called
MyObject\CLSID in the registry, and to associate a particular CLSID with that entry.
Besides associating a CLSID with a name, and a name with a CLSID, the registry also associ-
ates a CLSID with a path to an executable or DLL:
HKEY_CLASSES_ROOT\
CLSID\
{C9B0B160-1308-11cf-AB35-0000C07EBA2B}\InProcServer32 =
c:\src\unleash\chap30\simpdll\simpobj.dll
Given an entry like this in the registry database, it’s easy to see how you can give the COM
services a CLSID, and then have COM in turn load a DLL or executable that is associated
with the CLSID. It should also be obvious that you can easily create an error condition by
specifying the wrong path in your REG file. If you are having trouble with one of the programs
in this chapter, look carefully at the REG file and make sure that it points at the file you want
to launch. You should also make sure this information is listed correctly in the registry itself.
So what is a CLSID? Well, a CLSID is very much like an IID, and both CLSIDs and IIDs are
forms of GUIDs (pronounced Goo ID). In particular, a GUID is generic name for a globally
unique identifier. A CLSID is a GUID that identifies a class, and an IID is a GUID that iden-
tifies an interface. From a purely structural point of view, IIDs, GUIDs, and CLSIDs are iden-
tical. They differ only in what they represent; that is, in what they are meant to designate.
The basic fact to understand about them is that they are guaranteed to be statistically unique;
which means, I suppose, that it’s unlikely that any two of them will be identical. It’s probable
that in the future there will be thousands of COM objects on our machines. Furthermore, we’ll
have simultaneous access to thousands of other machines, which all have thousands of COM
objects on them. Given these kinds of numbers, it would not be possible to uniquely identify
objects by giving them names. (For instance, there seem to be many different programs called
EDIT.EXE, or EXPLORER.EXE. These are not unique names.) CLSIDs, and the closely re-
lated group of numbers called GUIDs, are meant to be statistically unique. Therefore, they
should help to meet the needs foreseen by the developers of the Component object model.
For now, you don’t need to know a great deal else about CLSIDs other than that they follow,
in Object Pascal terms, this record structure:
TGUID = record
D1: Longint;
D2: Word;
D3: Word;
D4: array[0..7] of Byte;
end;
When creating instances of these GUIDs for you to use, Microsoft’s code generates random
numbers based on the current date and time, and on the supposedly unique numeric proper-
ties of certain pieces of hardware, such as network cards. In particular, you can run a program
called GUIDGEN.EXE (Borland) or UUIDGEN (Microsoft) that generates these numbers
for you There are also Windows API calls you can make to generate these numbers on the fly.
For now, I am not going to spend any more time discussing the Registry. The key point to
remember is that the Registry is just a database. This database serves a wide range of purposes,
but one of the most important is that it associates CLSIDs with the names of programs and
with other associated bits of information, such as version numbers and the location at which
binaries are stored.
NOTE
Most of the fundamental routines used in COM are declared in OLE2.PAS. Here are
some routines that you can use to explore the Registry:
OleRegGetUserType
OleRegGetMiscStatus
OleRegEnumFormatEtc
OleRegEnumVerbs
NOTE
As a rule, you can consider any function that begins with the letters Co to be part of the
COM API. For instance, both CoGetClassObject and CoCreateInstance are part of the
Windows API, and are listed in books that discuss OLE 2.x. These are functions that
belong to the operating system, just as CreateWindow and MessageBox are Windows API
functions.
When writing the type of code shown in this chapter, you should try to stick to COM
functions because of memory and performance considerations. For instance, don’t call
OleInitiailize if CoInitialize will do. COM is a subset of OLE. It’s smaller and
more compact.
The COM API is fairly large, and the OLE API as a whole is huge. You should do
anything you can to obtain Microsoft SDKs, documents, and Help files that will
enable you to sort out some of these functions. Joining the Microsoft Developer
Network is one way to get this information. Buying third-party books is another way
to get information. You also should study OLE2.PAS, OLECTL.PAS,
OLEAUTO.PAS, and OLECTRLS.PAS. All of these files ship with the Delphi RTL,
and they provide hundreds of examples of how to write COM-based code.
The signature for DllGetClassObject is defined in the COM specification, but it is not imple-
mented there. Instead, it is up to you to implement the method. In other words, COM gives
you a particular function header, and then promises to call this function at the appropriate
time if you export it from a DLL. (This is similar to the process seen when you create a dialog
box or implement a callback. Of course, there are significant differences, but a parallel does
exit.)
Here is the declaration for DllGetClassObject:
function DllGetClassObject(
const rclsid: TCLSID; // Class you ultimately after
const riid: TGUID; // Usually IClassFactory
var ppv // Usually returns pointer to IClassFactory
): hResult; stdcall export; // S_OK on success
The first parameter passed to this function is the CLSID of the object you want to retrieve
from the DLL. However, DllGetClassObject does not return an instance of the object speci-
fied by the CLSID. It just checks to see if the class you want is supported by the current DLL
and by the IClassFactory object specified in the second parameter. Don’t be fooled into thinking
that DllGetClassObject returns the class object you are seeking to instantiate. That would be
the logical thing for a function with this name to do—but this is OLE code, and nothing in
OLE is quite so obvious as all that!
The second parameter is the ID of an intermediate class used to communicate with the object
you want to retrieve. This intermediate class is usually IClassFactory. I explain both the de-
tails of this communication and the reason for the existence of this intermediate class in just a
few moments.
The final parameter passed to DllGetClassObject holds the pointer to the object specified in
the second parameter. In particular, the third parameter usually returns an instance of
IClassFactory . This is what you are after when you call this function.
Note also that the object returns an hResult. This hResult is passed back to the calling appli-
cation so that you can see if the call succeeded. In other words, when you call CoGetClassObject,
it in turn calls DllGetClassObject, and CoGetClassObject returns the result of the call to
DllGetClassObject.
NOTE
The strange thing about this process is that you call CoGetClassObject, which is a
Windows API function that really only ends up calling another function you imple-
mented. This kind of indirection can be confusing at first. For instance, if your call to
CoGetClassObject fails, you’re likely to say, “Hey, that call can’t fail. I know I’ve called
it correctly. I’m sure of it!” And, indeed, you may be calling the function correctly.
However, even if it’s called correctly, it still can return an error if you have incorrectly
implemented DllGetClassObject or any of the routines that DllGetClassObject calls.
It is perhaps worth reiterating that you probably will never directly call DllGetClassObject.
Instead, this routine is called by OLE after you first call CoGetClassObject or CoCreateInstance.
Needless to say, when you call either of these functions, you pass them the CLSID you want to
retrieve, the ID of the interface you want to use to communicate with the object, and a pointer
variable that usually references an instance of IClassFactory.
CoGetClassObject usually returns an instance of IClassFactory, while CoCreateInstance re-
turns the object that you seek. For instance, in this case it would return IMyObject.
As I stated earlier, CoGetClassObject and CoCreateInstance are vaguely analogous to calls such
as DialogBox or EnumFontFamilies. DllGetClassObject is vaguely analogous to a dialog proce-
dure, a window procedure, or a TFNFontEnumProc . You call CoGetClassObject and
EnumFontFamilies directly, but you would never call DllGetClassObject or a dialog procedure
directly. Once again, this analogy is meant to help you understand the concepts involved; it’s
not meant to draw a direct parallel between blatantly disparate technologies.
NOTE
I’m aware that all this complexity can be a bit overwhelming. Remember, however,
that this book ships with working examples of COM technology. Once you have
implemented it correctly a first time, you can reuse the code simply by cutting and
pasting. The first time is hard, but once you have an example up and running, you can
go on to create your own COM-based programs with relatively little work.
The implementation for DllGetClassObject begins by checking to be sure that the object you
requested is supported by this DLL and by IMyClassFactory:
if not IsEqualIID(CLSID_MyObject, rclsid) then begin
result := E_FAIL;
Exit;
end;
The reason it performs this check is just to make sure that the whole process is not a waste of
time. DllGetClassObject is not going to return the object specified in the CLSID. However, it
knows what objects are supported by the DLL and by its implementation of IClassFactory. It
therefore checks the CLSID to see if the calling process is off on some kind of wild goose chase.
I could, if I wanted, ask IMyClassFactory directly if it supports this interface. After all, I imple-
mented IMyClassFactory, so I can figure out which CLSIDs it supports and which it doesn’t.
In this case, I know it supports only CLSID_MyObject, so I give the okay.
It should come as no surprise to learn that the COM function called IsEqualIID enables you to
compare two GUIDs to see if they are equivalent. You will use this function over and over
again in COM code to ensure that the objects being requested actually are supported by your
server.
NOTE
You can find a whole series of return codes specified in OLE2.PAS. Many of these
constants begin with either S or E. S stands for success and E stands for error.
The code fragment shown above returns E_FAIL if the two GUIDs being compared are
not equal. Two other, entirely self-explanatory, error codes include E_ACCESSDENIED and
E_ABORT. The two primary success codes are S_OK and S_FALSE, both of which also are
self-explanatory.
Often you don’t really care exactly what happened when a function executes, but are
concerned only with whether it succeeded or failed. Most OLE functions return
something called an HResult, and you can use the Succeeded and Failed functions to
determine whether or not an HResult is a success or failure code. For instance, if a
function returns S_FALSE, Succeeded returns True. If a functions returns E_ABORT,
Succeeded returns False. Here are some of the routines used to examine the return
values from COM and OLE functions:
function Succeeded(Res: HResult): Boolean;
function Failed(Res: HResult): Boolean;
function ResultCode(Res: HResult): Integer;
function ResultFacility(Res: HResult): Integer;
function ResultSeverity(Res: HResult): Integer;
function MakeResult(Severity, Facility, Code: Integer): HResult;
In most cases, DllGetClassObject is a tool for retrieving class factories from a DLL. That is,
DllGetClassObject instantiates an instance of IClassFactory and returns it to the calling pro-
cess. That’s what it does.
There is, in fact, no written rule saying that DllGetClassObject has to return an instance of
IClassFactory , but that is, in fact, what it usually does. One possible variation you could run
on this scenario would be to have several IClassFactorys stored in a single DLL or set of DLLs.
Then you could return a particular class factory in one circumstance, and another class factory
in a different circumstance. Once again, the point is that Microsoft designed this architecture
to be flexible. That flexibility comes with a price, however, and the price you pay is an increase
in complexity, and a resultant potential ambiguity.
NOTE
It might be worth noting that the developers of Delphi appear to have a formula they
follow when designing their product. In particular, they seek to find the best way to do
something, and to then make it as easy as possible for you to use that method. This
technique stands in contrast to architectures that seek to find the most flexible way to
do things. A third alternative is represented by a language like BASIC, which seeks to
find not the the best, not the most flexible, but the easiest way to do something. OLE
and C++ are flexible but difficult. BASIC is inflexible but easy to use. Delphi seeks to
find the middle ground. It’s developers look for the best way to do something, and
then make it as easy as possible without sacrificing preformance.
Turn, for a moment, to the DNoOle program in Listings 30.1 and 30.2. The DLL in that
program had a single function called CreateObject. That function was something of a minia-
ture class factory. You passed in to it an ID and a pointer; it did some hand waving, and then
set your pointer equal to a valid instance of the type of class identified by the ID you passed in.
The function was, in effect, a tiny factory that manufactured valid instances of classes.
IClassFactory performs the same task.
This code attempts to create an instance of IMyClassFactory. If the code fails, DllGetClassObject
is terminated and the function returns an error message stating that the system is out of memory.
It is unlikely that this call would ever fail. (You’re seeing very conservative code here.)
If the attempt to call IMyClassFactory succeeds, the IClassFactory method called QueryInterface
is called. This routine has an implementation almost identical to the CreateObject method
shown in the ENoOle and DNoOle examples:
hr := pObj.QueryInterface(riid, ppv);
Result := hr;
If the call fails, the class factory is freed and the function returns an error. If the call succeeds,
the function returns a success code and a valid pointer to IMyClassFactory.
To help you understand this, I want to show you the implementation of
IClassFactory.QueryInterface :
begin
if IsEqualIID(iid, IID_IClassFactory) or
IsEqualIID(iid, IID_IUnknown) then begin
Pointer(obj) := Self;
AddRef;
Result := S_OK;
end else begin
Pointer(obj) := nil;
Result := E_NOINTERFACE;
end;
end;
This code checks to see whether iid is equal to IID_IClassFactory or IID_IUnknown. If either
of these things are true, it returns a pointer to Self! From the OOP programmer’s point of
view, this is ridiculous. We had to have a pointer to IMyClassFactory or we wouldn’t have been
able to call QueryInterface. It’s as if the OOP programmer is being forced to jump through a
hoop in order to obtain something already in that programmer’s hands. The advantage of this
type of code, however, is that it makes sure you really are getting a pointer to the object you
requested. It’s performing type checking!
Once you have a pointer to an object, you call QueryInterface to retrieve a particular interface.
If the call to QueryInterface does not return the object you wish to retrieve, QueryInterface is
telling you that the current object does not support the desired interface. In this sense,
QueryInterface enables you to query an object and ask it what it can and cannot do. It’s a
means of building type checking into COM technology.
NOTE
Remember, when using COM you often don’t have access to the object’s source code,
and you don’t have a way for the compiler to perform careful type checking. As a
result, some other system has to be implemented that ensures that you really are
obtaining a pointer to the specific object you requested. This QueryInterface business
is a way to implement type checking, even if you are working across process or lan-
guage boundaries!
Because COM is so peculiar, it is perhaps worth going over the import of DllGetClassObject
one last time:
■ The first parameter is the object you want to ultimately retrieve. However,
DllGetClassObject doesn’t actually retrieve an object of this type. It just checks to
make sure it’s supported by the DLL. In some cases, it may even be a case of whether
the value specified in the second parameter supports the requested interface. That is,
the DLL may support the CLSID in the first parameter, but the GUID in the second
paramater may not be the proper means of accessing it!
■ The second parameter usually is set to IID_IClassFactory. IClassFactory is used to
retrieve the desired object. It is possible that the DLL supports the class specified in
the first parameter, but the instance of IClassFactory requested in the second param-
eter does not. However, for now, you don’t need to worry about that kind of situa-
tion.
■ The third parameter usually is used to return an instance of IClassFactory, or of
whatever interface is being used to communicate with the desired class.
This code increments a field of IMyObject called FRefCount. FRefCount is set to zero in the object’s
constructor.
This same variable is decremented whenever the object is released:
function IMyObject.Release: Longint;
begin
Dec(FRefCount);
if FRefCount = 0 then begin
FObjectDestroyed;
Free;
Result := 0;
end else
Result := FRefCount;
end;
As you can see, if this variable reaches zero, the entire object is automatically destroyed. This is
called reference counting. It guarantees that the object is not destroyed until all the processes
or objects that have attached to it have called Release.
NOTE
Delphi uses referencing counting internally when it works with long strings. If you
have two variables that point to the same string, Delphi doesn’t create two copies of
the string; it merely increments its reference count.
ObjectDestroyed is a simple, stand-alone routine that keeps track of the total number of ob-
jects in a DLL or executable. If that number reaches zero, the entire DLL or executable can be
unloaded from memory. You therefore track two reference counts: one that is internal to an
object, and one that is global to the whole DLL. Here is the very simple implementation of
ObjectDestroyed :
procedure ObjectDestroyed;
begin
Dec(ObjCount)
end;
Reference counting is not the main job of a direct descendant of IUnknown. Rather, its key job
is creating, and perhaps implementing, interfaces. From the point of view of Object Pascal, an
object is created when its constructor is called. From the point of view of COM, the creation
of an object is relatively unimportant. What is key is the moment when QueryInterface re-
turns a pointer to an interface.
The issue here is that COM must perform some type checking to ensure that you really are
getting the object that you requested. Because there is no way to do type checking in the tradi-
tional sense, the next best thing is to use this system of GUIDs and CLSIDs. You could, there-
fore, say that COM enforces a kind of type checking by insisting that you go through
QueryInterface if you want to retrieve a pointer to an object.Getting hold of what a Delphi
programmer would call an object is, therefore a multi-step process. First, the container appli-
cation calls into the DLL, usually asking for an instance of IClassFactory. The DLL then cre-
ates an instance of the requested object; that is, it calls the constructor for IClassFactory. The
DLL then calls IClassFactory’s QueryInterface method, asking for a valid, “type checked” in-
stance of IClassFactory. If that succeeds, it returns the object. From the OOP programmer’s
point of view, this call to QueryInterface is a spurious and absurd process; however, it’s the
way things are done in COM land!
When you retrieve an instance if IMyObject, the same process that occurred with IClassFactory
must be repeated. That is, first you call the constructor, then you call IMyObject.QueryInterface
to see if the specific requested interface is, in fact, supported:
function IMyObject.QueryInterface(const iid: TIID; var obj): HResult;
begin
if IsEqualIID(iid, IID_IUnknown) or
IsEqualIID(iid, CLSID_MyObject) then begin
Pointer(obj) := Self;
AddRef;
Result := S_OK;
end else begin
Pointer(obj) := nil;
Result := E_NOINTERFACE;
end;
end;
As you can see, IMyObject implements all three of these methods, so it can be said to imple-
ment the interface to IUnknown. Therefore, it returns a pointer to Self if the requested ID is
IUnknown.
The point here is that IMyObject might implement 30 methods in addition to the interface to
IUnknown. However, if it does in fact support IUnknown , it’s safe to return an instance of itself
when IUnknown is requested. The user may then end up calling only 3 of the 30 available meth-
ods, but the calls to these methods will in fact succeed, and will behave as you would expect the
methods of IUnknown to behave. This is what is meant when programmers say that IMyObject
supports the IUnknown interface!
Obviously, the specific example shown here leaves a little bit to be desired. The problem is that
this instance of IMyObject only supports the interface to IUnknown. It does not expland its reach
beyond the minimal capabilities of IUnknown, It is, in fact, a bland and rather unappatizing
example, but it has the virtue of being relatively easy to understand. When you are working
with OLE, things that are easy to understand are always virtuous!
Later in this chapter I show how you can add additional methods to IMyObject, and then ac-
cess them from an executable. This is the technique used in the Games SDK, and you are only
a few pieces of information away from understanding how it works. Before I can show it to
you, however, I must first talk about IClassFactory and freeing up DLLs, and then about ac-
cessing IMyObject from an executable. When you understand this additional material, you can
see how to go back and extend the functionality of IMyObject so that it implements a little some-
thing more than the IUnknown interface.
Implementing IClassFactory
After all this talk, it turns out that it is quite easy to implement IClassFactory:
IMyClassFactory = class(IClassFactory)
private
FRefCount: LongInt;
public
constructor Create;
function QueryInterface(const iid: TIID; var obj): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
function CreateInstance(unkOuter: IUnknown; const iid: TIID;
var obj): HResult; override;
function LockServer(fLock: BOOL): HResult; override;
end;
As you can see, all but two of these methods also are implemented in IUnknown, so there is no
need to discuss them further.
The key new method is CreateInstance, which is the method used to retrieve an instance of
IMyObject:
MyObject := IMyObject.Create(ObjectDestroyed);
hr := MyObject.QueryInterface(iid, Obj);
if Failed(hr) then
MyObject.Free
else
Inc(ObjCount);
Result := hr;
end;
NOTE
Remember that you must call CoInitialize before you can use COM. CoInitialize
usually takes nil in its sole parameter. When you are done using COM, you should
call CoUninitialize. Internally, these are reference counted objects, and only the first
call CoInitialize does anything useful.
For this system to work, you must keep track of the number of objects created in your DLL
and the number of locks placed on your server. Notice that both of these numbers are different
than the internal FRefCount variable manipulated in AddRef and Release.
Listing 30.7. The code for the MakeOle1 program shows how to call COM objects
residing in either DLLs or executables.
unit main;
choose Registry | Import Registry from REGEDIT.EXE and then select the
WIN32.REG file found in the directories where SimpExe and SimpObj
are stored. You can also register these programs by simply double
clicking on their REG files from inside the Windows Explorer.
Note that there is a DOS path included in these REG files. The REG
files themselves are just ASCII text. You may have to edit them
with Notepad so that the path points at the appropriate file. If you
are registering SIMPOBJ.DLL, then the path in the REG file should
point to the place on your system where SIMPOBJ.DLL is stored.
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Ole2, OleAuto, Menus,
ExtCtrls;
type
TForm1 = class(TForm)
MainMenu1: TMainMenu;
File1: TMenuItem;
Exit1: TMenuItem;
Exes1: TMenuItem;
Dlls1: TMenuItem;
ExeGetObject: TMenuItem;
ExeCreateInstance: TMenuItem;
DllGetObject: TMenuItem;
DLLCreateInstance: TMenuItem;
Panel1: TPanel;
N2: TMenuItem;
ClearText1: TMenuItem;
N1: TMenuItem;
FreeLibrary1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure ExeGetObjectClick(Sender: TObject);
procedure ExeCreateInstanceClick(Sender: TObject);
procedure DllGetObjectClick(Sender: TObject);
procedure DLLCreateInstanceClick(Sender: TObject);
procedure ClearText1Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
procedure FreeLibrary1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
procedure GetClass(CLSID: TGUID; ClassContext: DWord);
procedure CreateInstance(CLSID: TGUID; ClassContext: DWord);
procedure MessageOut(S: string);
end;
continues
var
Form1: TForm1;
implementation
{$R *.DFM}
uses
OleBox;
const
CLSID_MyDllObject: TGUID = (
D1:$C9B0B160;D2:$1308;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
CLSID_MyExeObject: TGUID = (
D1:$C9B0B161;D2:$1307;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
CLSID_Koala: TGUID = (
D1:$00021146;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
CLSID_WordBasic: TGUID = (
D1:$000209FE;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
CLSID_MyAuto: TGUID = (
D1:$C9B0B162;D2:$1307;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
OleCheck(P.Release);
end;
if Succeeded(hr) then
MessageOut(‘Object Exists’)
else begin
MessageOut(GetOleError(hr));
Exit;
end;
OleCheck(ClassFactory.Release);
OleCheck(P.Release);
end;
end.
The MakeOle1 program shown in Listing 30.7 enables you to call the DLL server created above.
This program has a file menu and two separate pop-up menus called DLLs and EXEs. The
DLLs pop-up menu is for launching objects that reside in-process. The EXEs pop-up menu is
for launching COM objects that reside in executables.
The code starts in the OnCreate method by making sure that COM is initialized. No OLE code
runs unless either COM or OLE is initialized:
procedure TForm1.FormCreate(Sender: TObject);
begin
OleCheck(CoInitialize(nil));
Left := 0;
Top := 0;
end;
The code shown here initializes only COM. If you want to initialize both COM and OLE, call
OleInitialize . Both CoInitialize and OleInitialize take a single parameter, which usually
is set to nil.
After initializing OLE, you can call either CoCreateInstance or CoGetGetClassObject in order
to retrieve your COM object from the DLL or executable. It’s easiest to call CoCreateInstance,
so that’s the way to proceed in most circumstances:
procedure TForm1.CreateInstance(CLSID: TGUID; ClassContext: DWord);
var
hr: hResult;
P: IUnknown;
begin
hr := CoCreateInstance(CLSID, nil, ClassContext, IID_IUnknown, P);
if Succeeded(hr) then
MessageOut(‘Object Exists’)
else
MessageOut(‘No Object’);
OleCheck(P.Release);
end;
The code shown here retrieves either an in-process server or a local server, depending on the
value you pass in the third parameter to CoCreateInstance. If you pass in CLSCTX_INPROC_SERVER,
you’re stating that you want to work with an in-process server. If you pass in
CLSCTX_LOCAL_SERVER , you’re stating that you want to work with a local server. Remember that
an in-process server is a DLL and a local server is an executable. There are, of course, excellent
reasons why the jolly boys and girls in Redmond didn’t call these variables CLSCTX_DLL_SERVER
and CLSCTX_EXE_SERVER! Excellent reasons I’m sure!
Here are the parameters, laid out one by one:
function CoCreateInstance(
const clsid: TCLSID; // The ID of the class you are after
unkOuter: IUnknown; // Controling unknown of agregate, usually nil
dwClsContext: Longint; // The context of the class.
const iid: TIID; // Interface to use when talking to the object
var pv // On success, returned object is placed here.
): HResult; stdcall; // Returns S_OK on success
hr: HResult;
ClassFactory: IClassFactory;
P: IUnKnown;
begin
hr := CoGetClassObject(CLSID, ClassContext, nil,
IID_IClassFactory, ClassFactory);
OleCheck(hr);
hr := ClassFactory.CreateInstance(nil, CLSID, P);
if Succeeded(hr) then
MessageOut(‘Object Exists’)
else begin
MessageOut(‘No object’);
Exit;
end;
OleCheck(ClassFactory.Release);
OleCheck(P.Release);
end;
The issue here is that you first call CoGetClassObject in order to retrieve an instance of
IClassFactory . You then use the ClassFactory from the DLL or executable to retrieve an in-
stance of your object:
function CoGetClassObject(
const clsid: TCLSID; // The class you want to retrieve
dwClsContext: Longint; // In processes or local server?
pvReserved: Pointer; // Reserved, must be nil
const iid: // Communication interface (IClassFactory)
TIID; var pv // Returned Communication Object.
): HResult; stdcall; // Returns S_OK on success
In this case, IMyObject also supports a virtual function called GetTwelve. The implementation
for GetTwelve is trivial:
function IMyObject.GetTwelve: Integer;
begin
Result := 12;
end;
To access this function from an executable you must declare an object that supports the GetTwelve
method:
IFooObject = class(IUnknown)
public
function GetTwelve: variant; virtual; stdcall; abstract;
end;
This object has its methods declared as virtual abstract, as described in the DNoOle example.
Notice that you don’t redeclare Release, AddRef, and QueryInterface, as they already are de-
clared for you in IUnknown. IFooObject is a descendant of IUnknown, so it inherits virtual
abstract declarations from its parents. (In the C++ world, virtual abstract methods are some-
times called pure virtual methods.)
When you want to call the method, you change the way you implement CoCreateInstance or
CoGetClassObject :
if Succeeded(hr) then
MessageOut(‘Object Exists’)
else begin
MessageOut(GetOleError(hr));
Exit;
end;
In this second example, notice that you can call other standard TObject VCL routines, such as
ClassName.
The technique shown here is close to the technique used in the Microsoft Games SDK. In other
words, it is a powerful technique that is used by many professional programmers.
Why would you use this technique rather than the technique used in the DNoOle example?
Well, DNoOle works with Delphi and Borland C++, but it doesn’t work with any other tools.
COM, however, works with Delphi, Borland C++, Visual C++, Visual Basic, Symantec C++,
and any other tool that supports COM.
The final point to make here is that you cannot use this technique to export an object from an
executable. At least, you can’t do it through the relatively simple techniques shown in this
chapter. The issue is that you need to expand the capability of IMyObject and IMyClassFactory
by adding support for marshaling. Marshaling is a subject that is beyond the scope of this book.
Notice that in this case you don’t have to write System.Text; this is a outside of any VCL ob-
ject. Next, add the following code at the bottom of the MYOBJ.PAS:
initialization
NumLocks := 0;
NumObjs := 0;
Assign(F, ‘c:\oletest.txt’);
ReWrite(F);
WriteLn(F, ‘Initialization’);
finalization
WriteLn(F, ‘Finalization’);
Close(F);
end.
This code takes care of opening and closing the file to which you want to write. It also keeps a
written record of the moment when your DLL is loaded and unloaded from memory.
NOTE
The code above uses the reserved word finalization, which has been added to Delphi
2.0. The initialization section of a unit will execute before any other part of the unit is
called, and the finalization section will execute just before the unit is shut down. This
means that the initialization section is usually called at program launch, and the
finalization section is called just before the program closes. If your unit is inside a DLL,
then the initialization section will be called just as the DLL is loaded into memory, and
the finalization section will be called when the DLL is freed. Besides the example
shown above, another classic use of the initialization and finalization section appears in
the OleAuto unit that ships with the Delphi RTL. That unit calls OleInitialize in the
initialization section and OleUninitialize in the finalization section.
Now that you have a file set up, you can write to it as needed. For instance, if you are con-
cerned that your AddRef and Release pairs are not being matched up evenly, you can add WriteLn
statements to all of them:
function IMyObject.AddRef: Longint;
begin
Inc(FRefCount);
WriteLn(F, ‘MyAddRef: ‘ + IntToStr(FRefCount));
Result := FRefCount;
end;
Inc(FRefCount);
WriteLn(F, ‘AddRef: ‘ + IntToStr(FRefCount));
Result := FRefCount;
end;
Given the presence of these statements, you can produce a text file that looks like this:
Initialization
AddRef: 1
MyAddRef: 1
Release: 0
MyRelease: 0
Finalization
This gives an orderly, easy-to-read report on the progress of your program. You can see that
the AddRefs and Releases are all properly paired, and you can see that the DLL is properly
unloaded when you’re through with it.
Here, for instance, is a report that illustrates what happens when something goes wrong:
Initialization
AddRef: 1
MyAddRef: 1
Release: 0
DllCanUnloadNow
Finalization
As you can see, there are two calls to AddRef but only one call to Release. The call to
DLLCanUnloadNow should have failed, but the finalization immediately after it correctly suggests
that it succeeded.
NOTE
If you use the technique described here on a local server, you may find that you get
additional, unexpected calls to QueryInterface, AddRef, and Release. These additional
entries in your text file are due to internal OLE calls to your object involving IMarshal
and other interfaces.
Listing 30.8. The source code for the main module of the SimpExe program.
unit main;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Ole2, ExeObj, OleAuto;
type
TForm1 = class(TForm)
Button1: TButton;
procedure FormCreate(Sender: TObject);
private
RegCo: LongInt;
continues
var
Form1: TForm1;
implementation
{$R *.DFM}
ExeObj.AHandle := Handle;
RegCo := 0;
try
MyClassFactory := IMyClassFactory.Create;
except
Close;
Exit;
end;
hr := CoRegisterClassObject(CLSID_MyExeObject, MyClassFactory,
CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, RegCO);
if Succeeded(hr) then
Caption := ‘Success’;
end;
end
Listing 30.9. The source code for the object implementation in the SimpExe
program.
unit exeobj;
interface
uses
Ole2, Windows, SysUtils;
const
CLSID_MyExeObject: TGUID = (
D1:$C9B0B161;D2:$1307;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
type
IExeObject = class(IUnknown)
private
FRefCount: LongInt;
public
constructor Create;
function QueryInterface(const iid: TIID; var obj): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
end;
IMyClassFactory = class(IClassFactory)
private
FRefCount: LongInt;
public
constructor Create;
function QueryInterface(const iid: TIID; var obj): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
function CreateInstance(unkOuter: IUnknown; const iid: TIID;
var obj): HResult; override;
function LockServer(fLock: BOOL): HResult; override;
end;
var
LockCount: Integer;
AHandle: hwnd;
implementation
constructor IExeObject.Create;
begin
inherited Create;
FRefCount := 0;
end;
continues
{ -- IMyClassFactory -- }
constructor IMyClassFactory.Create;
begin
inherited Create;
FRefCount := 0;
end;
{ Windows may call this function several times, besides the occasions
when you explicitly ask it to do so. It may call asking to see if
this interface supports IMarshall, IUnknown, etc. Remember, though,
AddRef will only get called if iid equals IID_IUnknown or
IID_IClassFactory }
var
hr: HResult;
ExeObject: IExeObject;
begin
Result := E_OutOfMemory;
if UnkOuter <> nil then begin
Result := CLASS_E_NOAGGREGATION;
Exit;
end;
hr := ExeObject.QueryInterface(iid, Obj);
if Failed(hr) then begin
ExeObject.Release;
Exit;
end;
Result := hr;
end;
{ LockServer:
This function can be used to tell the whole EXE that it
ought to stay in memory. The exe should not exit until
this LockCount is zero and the RefCount for all objects
is zero. Its up to the programmer to track this information;
there is nothing magic about LockServer! You use these tools
to decide when its okay to unload the EXE! When everything
is equal to zero, then you unload. I don’t check for these
contingencies in this example. }
initialization
CoInitialize(nil);
LockCount := 0;
finalization
CoUninitialize;
end.
HKEY_CLASSES_ROOT\CLSID\{C9B0B161-1307-11cf-AB35-0000C07EBA2B} =
MyExeObject Test 1
HKEY_CLASSES_ROOT\CLSID\{C9B0B161-1307-11cf-AB35-0000C07EBA2B}\ProgID =
MyExeObject1.0
HKEY_CLASSES_ROOT\CLSID\
{C9B0B161-1307-11cf-AB35-0000C07EBA2B}\VersionIndependentProgID = MyExeObject
HKEY_CLASSES_ROOT\CLSID\{C9B0B161-1307-11cf-AB35-0000C07EBA2B}\LocalServer32 =
c:\src\unleash\Chap30\simpexe\simpexe.exe
HKEY_CLASSES_ROOT\CLSID\{C9B0B161-1307-11cf-AB35-0000C07EBA2B}\NotInsertable
ExeObj.AHandle := Handle;
RegCo := 0;
try
MyClassFactory := IMyClassFactory.Create;
except
Close;
Exit;
end;
hr := CoRegisterClassObject(CLSID_MyExeObject, MyClassFactory,
CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, RegCO);
if Succeeded(hr) then
Caption := ‘Success’;
end;
The issue here is that the FormCreate method can’t directly return a pointer to IClassFactory,
as is the case with DllGetClassObject. DllGetClassObject succeeds because it does not need to
return the pointer across a process boundary. An executable does not reside inside another
applications address space, and so it can’t directly pass a pointer to it. Instead, it registers the
object with the system, and then the system can pass the object on to CoGetClassObject or
CoCreateInstance .
The fourth parameter specifies whether the object is multiple- or single-use. Objects that are
multiple-use can service requests for many containers at the same time. Objects that are single-
use can serve only one container at a time. If an object is single-use, a new instance of the ex-
ecutable is created for each new container that wants to access it.
Here are the possible result codes returned by a call to CoRegisterClassObject:
S_OK The object was registered successfully.
CO_E_OBJISREG The object class already was registered in the class object table.
E_OUTOFMEMORY The system is out of memory.
E_INVALIDARG One or more arguments are invalid.
E_UNEXPECTED An unexpected error occurred.
I personally find myself continually weighing the obvious significance of COM and OLE against
the seemingly inexplicable complexity of its implementation. Overall, the subjective weight of
this chapters adds up to a heavy recommendation of COM. However, I would be remiss as a
writer if I did not temper that enthusiasm with a few words of warning about the complexity
of the COM implementation, and the relative opaqueness of its documentation.
Summary
In this chapter you had a look at the Component Object Model (COM). This technique en-
ables you to write objects that can be placed in DLLs and shared across various language bound-
aries. It is also the ground in which all OLE technologies are rooted.
Once you understand how it’s implemented, this code can prove to be a relatively easy way for
you to create objects that can be used by multiple languages. One of the key factors to keep in
mind is that the Games SDK is implemented using techniques similar to those shown in this
chapter. The whole point of the Games SDK is that it’s fast—faster even than the graphics
code written in DOS. With its speed and flexibility, COM provides an excellent means for
you to create objects that can be used by multiple parts of your development team, or that can
be sold to many different types of programmers who use many different languages.
I do not explore this issue here, but COM is designed so that it can be accessed even by proce-
dural languages that do not support objects. This gives COM an extraordinary flexibility that
is almost unheard of in today’s programming world.
Finally, there is the fact that COM is the basis for OLE, and is the likely basis for the future
object-oriented operating systems that Microsoft should be rolling out in the next few years.
Quite simply, this means that all of our programming could become COM-based in the next
five to ten years. That gives this technology a tremendous significance that is hard to ignore
under any circumstances.
COM, Automation,
and Structured 31
Storage
In this chapter, you get a look at two more architectures rooted in the COM technology you
learned about in the last chapter. In particular, you will learn about structured storage and the
technical details of how to implement OLE automation.
You have already heard quite a bit about automation, but now you are going to take a step
behind the facade of the Delphi TAutoObject class and see how to implement the technology
out of the raw elements of the OLE API. I’m exploring this subject in depth because I believe
automation is one of the key building blocks of the monolithic OLE edifice. If you want to
understand OLE, automation is one of the technologies you need to master.
Structured storage is a new technique for writing objects or data to disk. This technique pro-
vides all the services you are used to seeing in standard file I/O. You can write files to disk, you
can create subdirectories, and you can read and write data to and from these files and directo-
ries through a wide variety of techniques. The difference between structured storage and stan-
dard file I/O is that each set of structured storage directories and files resides inside a single
larger file, much as a set of InterBase tables resides inside a single GDB file.
A structured storage file is called a compound file. The “directories” inside these compound
files are called storages, and the “files” inside these compound files are called streams. Other
types of objects can be stored in a compound file, but that subject will only be hinted at in this
chapter.
Structured storage sounds a bit esoteric when you first hear about it. However, it is a very com-
mon technology that could become ubiquitous in the near future. For instance, you will see in
this chapter that all the .DOC files you use in WordPad or Word for Windows are actually
structured storage files. When you open them up with the right tools, you will find that they
actually consist of a series of smaller files and directories embedded inside a single large .DOC
file.
Microsoft has hinted that structured storage will probably be the standard form of file I/O under
future versions of Windows. This means that if you begin using structured storage today, you
will be able to port your code more readily to the advanced object-oriented operating systems
that loom on the horizon.
complex example that enables the user to explore any structured storage file found on his or
her system.
The code for the MakeStg program is found in Listing 31.1. A picture of the simple form for
the program is shown in Figure 31.1.
FIGURE 31.1.
The MakeStg program has
two buttons. The first
creates a simple file, and
the second reads back its
contents.
Listing 31.1. The MakeStg program shows how to create and read a simple
structured storage file.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Ole2;
type
TForm1 = class(TForm)
bWriteStream: TButton;
bReadStream: TButton;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure bWriteStreamClick(Sender: TObject);
procedure bReadStreamClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
uses
OleAuto;
{$R *.DFM}
continues
hr := Storage.OpenStream(‘MyStream’, nil,
STGM_DIRECT or STGM_READ or STGM_SHARE_EXCLUSIVE,
0, Stream);
OleCheck(hr);
GetMem(S, 4);
Stream.Read(S, 4, @Size);
ShowMessage(S);
Stream.Release;
Storage.Release;
end;
end.
The first thing to notice about this program is that it calls OleInitialize in its OnCreate han-
dler and OleUninitialize in its OnClose handler:
Structured storage is part of OLE proper, so you must call OleInitialize rather than
CoInitialize . The parameter passed to OleInitialize is an instance of IMalloc that can usu-
ally be set to nil. (As civilized Object Pascal programmers, we will overlook the indiscretion of
naming a class after a now relatively obsolete C programming language function.)
The code to create a compound file looks like this:
procedure TForm1.bWriteStreamClick(Sender: TObject);
var
Hr: HResult;
Storage: IStorage;
Stream: IStream;
Size: LongInt;
S: string;
begin
Hr := StgCreateDocFile(‘C:\SAM.STG’,
STGM_DIRECT or STGM_CREATE or STGM_WRITE or STGM_SHARE_EXCLUSIVE,
0, Storage);
if Hr <> S_OK then
ShowMessage(‘Err’);
Hr := Storage.CreateStream(‘MyStream’, STGM_DIRECT or STGM_CREATE or
STGM_WRITE or STGM_SHARE_EXCLUSIVE, 0, 0, Stream);
OleCheck(HR);
S := ‘Sam’;
Stream.Write(PChar(S), 4, @Size);
if Size <> 4 then ShowMessage(‘Wrong size written’);
Stream.Release;
Storage.Release;
end;
The two key functions in this method are StgCreateDocFile and Istorage.CreateStream. The
first is used to retrieve an initialized IStorage object, and the second is used to retrieve an ini-
tialized IStream object. Both IStream and IStorage are OLE objects declared in OLE2.PAS.
Here is the declaration for StgCreateDocFile:
function StgCreateDocfile(
pwcsName: POleStr; // Name of file, nil if you want a temp file.
grfMode: Longint; // Access mode. Direct or Transacted?
reserved: Longint; // Must be zero
var stgOpen: IStorage // The IStorage object is returned here.
): HResult; stdcall; // S_OK on success
This is an OLE function, declared in OLE2.PAS, and so it takes a POleStr, in the first param-
eter, and not a long string. A POleStr is declared to be a PWideChar, which means it is a Unicode
string. This is not an issue in the simple code shown here, but if you want to pass a variable to
this function, you will probably end up having to wrestle with MultiByteToWideChar and
WideCh arToM ultiB yte . For help with these functions, see the following routine in
OLEAUTO.PAS:
procedure WideCharToShortString(P: PWideChar; var S: ShortString);
There is also some code in OLEBOX.PAS, which ships with this book. However, the routines
in SYSUTILS.PAS are the core source of help when you need to translate strings to WideChars
and back.
The access mode you use when working with compound files is relatively important. The key
point here is that you can open a file in direct mode or in transacted mode. Transacted mode
is like using transactions with a database: it enables you to roll back your work if you decide
you have made a mistake. You can, in fact, make huge changes to a compound file and then
undo them all if you are working in transacted mode.
When working with IStorage objects, you should always use STGM_DIRECT, which is the flag
designating direct mode. Here are three possible combinations of flags for use with root IStorage
objects:
STGM_READ OR STGM_SHARE_EXCLUSIVE OR STGM_READWRITE OR STGM_SHARE_DENY_WRITE
STGM_PRIORITY OR STGM_READ
STGM_READ OR STGM_SHARE_DENY_WRITE OR
STGM_READWRITE OR STGM_SHARE_EXCLUSIVE OR
STGM_READ OR STGM_PRIORITY OR STGM_SHARE_DENY_NONE
CreateStream creates a file inside a storage object. It’s as if the storage created by StgCreateDocFile
were a new hard drive, and the stream returned by IStorage.CreateStream is the first file on
the hard drive:
function CreateStream(
pwcsName: POleStr; // Unicode name of stream
grfMode: Longint; // Access mode
reserved1: Longint; // Must be zero
reserved2: Longint; // Must be zero
var stm: IStream // Stream is returned here
): HResult; virtual; stdcall; abstract; // S_OK on success
You can see that stream can be written to, read from, and copied. You can also seek to a par-
ticular point in the stream. If you used the STGM_TRANSACTED flag, then you can commit your
work or revert to a previous state.
There is no particular need to comment in depth on the act of reading the information back
from the file. The core of that code looks like this:
hr := StgOpenStorage(‘C:\SAM.STG’, nil,
STGM_DIRECT or STGM_READ or STGM_SHARE_EXCLUSIVE,
nil, 0, Storage);
OleCheck(hr);
hr := Storage.OpenStream(‘MyStream’, nil,
STGM_DIRECT or STGM_READ or STGM_SHARE_EXCLUSIVE,
0, Stream);
OleCheck(hr);
GetMem(S, 4);
Stream.Read(S, 4, @Size);
NOTE
Some of the code in the Storage program is derived from an article by John Lam that
appeared in PC Magazine.
Figure 31.2 shows the interface for the Storage program. In the TMemo control on the program’s
main window, you can see an open compound file containing various streams and substorages.
Notice also the SummaryInformation property. This is an instance of a standard summary
page found in most Microsoft products such as Word, Excel, or PowerPoint. It contains
information about the title, subject, and author of a document. The code for the Storage pro-
gram is found in Listings 31.2 and 31.3.
FIGURE 31.2.
The Storage program
iterates through the objects
in compound file.
Listing 31.2. The simple main module for the Storage program.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Ole2, OleStg, Buttons;
type
TForm1 = class(TForm)
bBrowse: TButton;
OpenDialog1: TOpenDialog;
ListBox1: TListBox;
bOk: TBitBtn;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure bBrowseClick(Sender: TObject);
procedure bOkClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
uses
StrBox, OleAuto;
{$R *.DFM}
continues
end.
Listing 31.3. The object that performs all the grunt work for the Storage
program.
unit olestg;
interface
uses
Windows, Classes, Ole2,
OleAuto, StrBox, Dialogs,
SysUtils;
type
TOpenStorage = class(TObject)
private
FStgStrings: TStringList;
procedure OpenStorage(FileName: string);
function ShowStorageElement(S: string; StatStg: TStatStg): Integer;
procedure EnumStorageElements(var Storage: IStorage);
procedure HandleProperty(Storage: IStorage);
procedure HandleSubStorage(var Storage: IStorage; StatStg: TStatStg);
destructor Destroy; override;
public
constructor Create(FileName: string); virtual;
property StgStrings: TStringList read FStgStrings;
end;
implementation
uses
OleBox;
destructor TOpenStorage.Destroy;
begin
FStgStrings.Free;
inherited Destroy;
end;
{----------------------------------------------------
Name: HandleProperty
Declaration:
Unit: Main
Description: Show Summary Info
----------------------------------------------------}
procedure TOpenStorage.HandleProperty(Storage: IStorage);
begin
{ Not Implemented }
end;
{----------------------------------------------------
Name: ShowStorageElement
Declaration: ShowStringType(S: string; StatStg: TStatStg);
Unit: Main
Description: Nonroot storage elements may have a first
character between #1 and #6 that has a special meaning.
We deal with that here.
----------------------------------------------------}
function TOpenStorage.ShowStorageElement(S: string; StatStg: TStatStg): Integer;
var
Temp: string;
begin
if S = ‘Unknown’ then begin
StgStrings.Add(‘End Storage (Unknown)’);
Result := -1;
Exit;
end;
continues
#5: begin
Temp := StripFromFront(Temp, 1);
Temp := ‘(Property) ‘ + Temp;
end;
end;
StgStrings.Add(S + ‘ ‘ + Temp);
Result := Ord(Temp[1]);
end;
{----------------------------------------------------
Name: EnumStorageElements
Declaration: EnumStorageElements(var Storage: IStorage);
Unit: Main
Description: Enumerate the elements inside a storage.
This is a recursive routine, but the
recursion occurs in the HandleSubStorage routine.
----------------------------------------------------}
repeat
hr := Enum.Next(1, StatStg, @Count);
OleCheck(hr);
case StatStg.dwType of
STGTY_STREAM: S := ‘Stream’;
STGTY_STORAGE: S := ‘Storage’;
STGTY_LOCKBYTES: S := ‘LockBytes’;
STGTY_PROPERTY: S := ‘Property’;
else
S := ‘Unknown’;
end;
{----------------------------------------------------
Name: OpenStorage
Declaration: OpenStorage(FileName: string);
Unit: Main
Description: Given a filename, try to open it as a storage
file.
----------------------------------------------------}
procedure TOpenStorage.OpenStorage(FileName: string);
var
hr: HResult;
S: PWideChar;
Size: Integer;
Storage: IStorage;
Failed: Boolean;
begin
Failed := False;
S := nil;
try
try
S := AnsiToUnicode(FileName, Size);
hr := StgIsStorageFile(S);
if hr <> NoError then
raise Exception.Create(‘Not a valid storage file.’);
StgStrings.Add(‘Storage ‘ + FileName);
hr := StgOpenStorage(S, nil,
Stgm_Direct or Stgm_ReadWrite or Stgm_Share_Exclusive,
nil, LongInt(nil), Storage);
if Ole2.Failed(hr) then
raise Exception.Create(‘Call to StgOpenStorage failed’);
except
Failed := True;
raise;
end; { try..except }
finally
VirtualFree(S, Size, Mem_Release);
end; { try..finally }
end.
Listing 31.4. The OleBox unit contains some simple routines that might be helpful
when you are working with OLE code.
unit olebox;
interface
uses
Ole2, SysUtils, Windows;
const
SOleError = 62211;
type
EOleError = class(Exception);
implementation
initialization
finalization
end.
The core of the main module for this program contains one simple routine:
procedure TForm1.bBrowseClick(Sender: TObject);
var
OpenStorage: TOpenStorage;
continues
This code pops up an open dialog. You can then browse around looking for compound files.
In most cases, you can just select a Word or .DOC file like the ones that ship on the CD.
Assuming that the user chooses a file, the next step is to create an instance of TOpenStorage.
The Create method for TOpenStorage first creates a TStringList object called FStgStrings and
then calls an internal method called OpenStorage:
procedure TOpenStorage.OpenStorage(FileName: string);
var
hr: HResult;
P: IUnknown;
S: PWideChar;
Size: Integer;
Storage: IStorage;
Failed: Boolean;
begin
Failed := False;
try
try
S := AnsiToUnicode(FileName, Size);
hr := StgIsStorageFile(S);
if hr <> NoError then
raise Exception.Create(‘Not a valid storage file.’);
StgStrings.Add(‘Storage ‘ + FileName);
hr := StgOpenStorage(S, nil,
Stgm_Direct or Stgm_ReadWrite or Stgm_Share_Exclusive,
nil, LongInt(nil), Storage);
if Ole2.Failed(hr) then
raise Exception.Create(‘Call to StgOpenStorage failed’);
except
Failed := True;
raise;
end; { try..except }
finally
VirtualFree(S, Size, Mem_Release);
end; { try..finally }
OpenStorage contains a try..except and try..finally block. The code begins by calling
AnsiToUnicode , which is a function found in the OleBox unit that ships with this book:
AnsiToUnicode calls VirtualAlloc to get memory for a PWideChar. This is necessary because
a call to GetMem did not work in this situation with the copy of Delphi I had at the time of this
writing. The code then calls the MultiByteToWideChar function, which converts a long string
into a Unicode string. This function leaves it up to the user to dispose of the memory allocated
by the call to VirtualAlloc. As a result, you might consider calling one of the functions in SysUtils
as shown rather than this routine.
StgIsStorageFile is one of the core OLE routines declared in OLE2.PAS. It takes the name of
the file in a parameter called pwcsName, and it returns one of the following values:
S_OK The file contains an IStorage.
S_FALSE The file does not contain an IStorage object.
STG_E_INVALIDFILENAME A bad name was passed in.
STG_E_FILENOTFOUND The pwcsName parameter could not be determined.
The function is called because the user might have chosen a file that was not a real structured
storage file. In that case, the code just raises an exception:
if hr <> NoError then
raise Exception.Create(‘Not a valid storage file.’);
Notice that I don’t bother creating an exception class for this particular error. This is not rec-
ommended programming practice, but it works well enough.
Assuming that the user chose a valid structured storage file, the next step is to call StgOpenStorage
so as to retrieve an instance of an IStorage object. If the call succeeds, then the following method
is called, with the IStorage object passed as a parameter:
procedure TOpenStorage.EnumStorageElements(var Storage: IStorage);
var
Enum: IEnumStatStg;
hr: hResult;
StatStg: TStatStg;
Count: LongInt;
S: string;
begin
if not Test(Storage.EnumElements(0, nil, 0, Enum)) then Exit;
repeat
hr := Enum.Next(1, StatStg, @Count);
OleCheck(hr);
case StatStg.dwType of
STGTY_STREAM: S := ‘Stream’;
STGTY_STORAGE: S := ‘Storage’;
STGTY_LOCKBYTES: S := ‘LockBytes’;
STGTY_PROPERTY: S := ‘Property’;
else
S := ‘Unknown’;
end;
This routine calls IStorage.EnumElements, which is a bit like entering the DIR command in a
subdirectory at the DOS prompt. Perhaps another, more apt analogy would be to the FindFirst
and FindNext commands shown in the TFileIterator component used by the DelAllDb pro-
gram. At any rate, the call first retrieves an instance of IEnumStatStorage, which is an enumera-
tor that retrieves each of the elements in the current storage:
var
Enum: IEnumStatStg;
begin
if not Test(Storage.EnumElements(0, nil, 0, Enum)) then Exit;
...
IEnumStatStg is a very simple OLE class that enables you to iterate over a series of elements:
IEnumStatStg = class(IUnknown)
public
function Next(celt: Longint; var elt;
pceltFetched: PLongint): HResult; virtual; stdcall; abstract;
function Skip(celt: Longint): HResult; virtual; stdcall; abstract;
function Reset: HResult; virtual; stdcall; abstract;
function Clone(var enm: IEnumStatStg): HResult; virtual; stdcall; abstract;
end;
NOTE
Remember that OLE classes such as IEnumStatStg are declared by Microsoft and
implemented originally in C++. It takes a while to get used to the idea that OLE
objects are language-independent!
mtime: TFileTime;
ctime: TFileTime;
atime: TFileTime;
grfMode: Longint;
grfLocksSupported: Longint;
clsid: TCLSID;
grfStateBits: Longint;
reserved: Longint;
end;
The fields of this record track the name, time, and type of one of the streams, substorages, or
properties stored in the compound file. In particular, here are the possible types stored in a
compound file:
case StatStg.dwType of
STGTY_STREAM: S := ‘Stream’;
STGTY_STORAGE: S := ‘Storage’;
STGTY_LOCKBYTES: S := ‘LockBytes’;
STGTY_PROPERTY: S := ‘Property’;
else
S := ‘Unknown’;
end;
Because it is possible that this code can stumble across a substorage, the code that iterates over
the elements in the stream recursively calls a method of TOpenStorage called HandleSubStorage,
which in turn recalls EnumStorageElements. I usually try to avoid recursion in my code, but
here the solution it offers is too simple to pass up.
That is all I’m going to say for now about the technical aspects of structured storage. This sys-
tem is widely used in OLE. If you embed an object, then it will be stored to disk using struc-
tured storage. Many OCXs also make heavy use of structured storage.
To me, the technology is also intriguing because it represents a means of storing many small
files inside one larger file. There are several occasions in programming where you need to save
a series of small files to disk. If you haven’t created such a program yourself, then you have
almost certainly used one. When you enter the program directory and type DIR, some one or
two hundred odd files may scroll by. It’s intriguing to think of the possibility of storing all
these small utility files inside one large compound file.
FIGURE 31.3.
The Auto1 program exports
an OLE automation object.
The object exported from the executable should be a descendant of IDispatch, which is de-
clared like this in OLE2.PAS:
IDispatch = class(IUnknown)
public
function GetTypeInfoCount(var ctinfo: Integer): HResult;
virtual; stdcall; abstract;
function GetTypeInfo(itinfo: Integer; lcid: TLCID;
var tinfo: ITypeInfo): HResult; virtual; stdcall; abstract;
function GetIDsOfNames(const iid: TIID; rgszNames: POleStrList;
cNames: Integer; lcid: TLCID; rgdispid: PDispIDList): HResult;
virtual; stdcall; abstract;
function Invoke(dispIDMember: TDispID; const iid: TIID; lcid: TLCID;
flags: Word; var dispParams: TDispParams; varResult: PVariant;
excepInfo: PExcepInfo; argErr: PInteger): HResult;
virtual; stdcall; abstract;
end;
You will need to implement an instance of this class. That is, you cannot leave this as an ab-
stract object, but must instead implement each of its methods.
In the COM object programs from the last chapter, you saw objects called IMyObject and
IExeObject. Those objects played the same role in the SimpDll and SimpExe that this object
plays in the Auto1 project. The difference, of course, is that this object knows how to transmit
information across process boundaries. That, as you recall, was a problem with the SimpExe
program!
In the Auto1 program, the instance of IDispatch that you will be working with is called
TMyAuto. The other key object in the program is called TMyClassFactory, and it is nearly
identical to the TMyClassFactory instances seen in the last chapter. The code in question is
shown in Listing 31.5, with Listing 31.6 featuring the same calls to CoRegisterClassObject
seen in the last chapter. Listing 31.7 contains the REG file you need to pass to REGEDIT.EXE.
interface
uses
Ole2, Windows, SysUtils,
Dialogs;
const
CLSID_MyAuto: TGUID = (
D1:$C9B0B162;D2:$1307;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
type
TMyAuto = class(IDispatch)
private
RefCount: Integer;
public
constructor Create;
function QueryInterface(const iid: TIID; var Obj): HResult;
override;
function AddRef: Longint; override;
function Release: Longint; override;
function GetTypeInfoCount(var ctinfo: Integer): HResult; override;
function GetTypeInfo(itinfo: Integer; lcid: TLCID;
var tinfo: ITypeInfo): HResult; override;
function GetIDsOfNames(const iid: TIID; rgszNames: POleStrList;
cNames: Integer; lcid: TLCID; rgdispid: PDispIDList): HResult; override;
function Invoke(dispIDMember: TDispID; const iid: TIID; lcid: TLCID;
flags: Word; var dispParams: TDispParams; varResult: PVariant;
excepInfo: PExcepInfo; argErr: PInteger): HResult; override;
end;
TMyClassFactory = class(IClassFactory)
private
FRefCount: Integer;
public
constructor Create;
function QueryInterface(const iid: TIID; var obj): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
function CreateInstance(unkOuter: IUnknown; const iid: TIID;
var obj): HResult; override;
function LockServer(fLock: BOOL): HResult; override;
end;
var
RegCo: LongInt;
implementation
uses
OleBox;
////////////////////////////
///////// TMyAuto //////////
////////////////////////////
constructor TMyAuto.Create;
begin
inherited Create;
RefCount := 0;
end;
continues
Result := S_OK;
end;
////////////////////////////
///// TMyClassFactory /////
////////////////////////////
constructor TMyClassFactory.Create;
begin
inherited Create;
FRefCount := 0;
end;
continues
Auto := TMyAuto.Create;
if Auto = nil then begin
Pointer(Obj) := nil;
Result := E_OutOfMemory;
Exit;
end;
hr := Auto.QueryInterface(iid, Obj);
if Failed(hr) then Auto.Release;
Result := hr;
end;
var
Inithr: hResult;
initialization
Inithr := OleInitialize(nil);
finalization
CoRevokeClassObject(RegCo);
if Inithr = S_OK then OleUnInitialize;
end.
Listing 31.6. The code for the main module of the Auto1 program.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Label1: TLabel;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
AutoMat, Ole2, OleBox;
{$R *.DFM}
try
MyClassFactory := TMyClassFactory.Create;
except
Close;
Exit;
end;
hr := CoRegisterClassObject(CLSID_MyAuto, MyClassFactory,
CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, RegCO);
if Succeeded(hr) then
Caption := ‘Success’
else
Caption := GetOleError(hr);
end;
end.
continues
Given the lengthy explanation of COM objects presented in the last chapter, I am going to
assume that most of the code in these units makes sense to you. If you don’t understand how
IClassFactory works, or how QueryInterface works, then you should return to the last chapter,
which explains these things in some detail.
There are, however, two key methods in the Automat unit that need to be explained. Both of
these methods are part of the IDispatch implementation called TMyAuto. The first call is
GetIDsOfNames , and the second is called Invoke.
Remember that the idea of an automation object is to enable the user to call functions or access
properties located inside an executable. In this case, the executable in question is called
AUTO1.EXE. To call a function or use a property, you need to call the InvokeMethod of
TMyAuto. However, you can’t pass the name of a function directly to TMyAuto; instead, you
need to first call GetIDsOfNames, which provides a set of IDs that serve as aliases for the names
and parameters by which you know the functions and properties supported by the object. Once
you know which ID is associated with which name, then you can call Invoke.
This process is a bit confusing at first blush, so I will take a moment to restate the matter in
slightly different terms. To get started, you pass GetIDsOfNames an array of strings. In response,
it passes back to you an array of IDs. The array of IDs define not only the individual functions
or properties supported by the object, but also the parameters passed to the function. You can
then pass these ideas to Invoke, and in turn Invoke will actually call the methods or properties
you specified when calling GetIDsOfNames.
GetIDsOfNames will translate a string name into a number that can be passed to Invoke:
function TMyAuto.GetIDsOfNames(const iid: TIID; rgszNames: POleStrList;
cNames: Integer; lcid: TLCID; rgdispid: PDispIDList): HResult;
var
S: string;
begin
if not IsEqualGUID(iid, GUID_NULL) then begin
Result := E_NOINTERFACE;
Exit;
end;
Result := DISP_E_UNKNOWNNAME;
S := UnicodeToAnsi(rgszNames[0]);
if cNames <> 1 then Exit;
if S = ‘Sam’ then
rgDispID^[0] := 1
Result := S_OK;
end;
In this case, the only element of interest is the first member of the array of strings passed in.
The code shown here uses a routine called UnicodeToAnsi from the OleBox unit to translate
the first member of the array to a string. It then sets the first element of the array of IDs to the
appropriate value associated with a particular string.
Here is a look at the parameters passed to GetIDsOfNames:
function TMyAuto.GetIDsOfNames(
const iid: TIID; // Reserved for future use, set to GUID_NULL
rgszNames: POleStrList; // Array of names to be mapped
cNames: Integer; // Number of names in array
lcid: TLCID; // Context used to interpret names
rgdispid: PDispIDList // Returned list of IDS
): HResult;
When considering this function, it’s important to remember that you will typically be calling
it from another executable. In particular, you will first call CoCreateInstance or CoGetClassName
as explained in the last chapter. Once you have a pointer to an instance of TMyAuto, then you
can call GetIDsOfNames or Invoke. This whole process will be explained in more depth later in
this section, but the general idea should be starting to make sense by this time.
Once you know the ID associated with the function or property that you want to access, then
you can pass that number to Invoke:
function TMyAuto.Invoke(dispIDMember: TDispID; const iid: TIID; lcid: TLCID;
flags: Word; var dispParams: TDispParams; varResult: PVariant;
excepInfo: PExcepInfo; argErr: PInteger): HResult;
begin
if not IsEqualGUID(iid, GUID_NULL) then begin
Result := E_NOINTERFACE;
Exit;
end;
Result := E_NOTIMPL;
Result := NOERROR;
Exit;
end;
end;
Invoke checks to see whether you want to call a function (DISPATCH_METHOD), or whether you
want to access a property (DISPATCH_PROPERTYGET). If the former, then a method is called, and
if the latter, then a property is accessed. In this particular implementation, the code is extremely
simple, with the method being nothing more than a call to ShowMessage, and the two proper-
ties simply returning integer values of either 7 or 11.
Here are the parameters passed to Invoke:
function TMyAuto.Invoke(
dispIDMember: TDispID; // ID obtained from GetIDsOfNames
const iid: TIID; // Reserved, must be GUID_NULL
lcid: TLCID; // Multi-lingual apps only: French, German, etc
flags: Word; // Is it a property, a method, a put or a get?
var dispParams: TDispParams;// Arguments to calls placed here
varResult: PVariant; // Return value placed here by Invoke
excepInfo: PExcepInfo; // Exception info placed here on error
argErr: Pinteger // If parameters are wrong error here
): HResult; // S_OK on success
The first parameter is the ID you got when you called GetIDsOfNames. The second should be
set to nil. The third parameter is for multi-lingual applications that need to be concerned about
which national language is being used. These are not computer languages, but national lan-
guages such as French, Spanish, German, and so on.
The flags parameter can be set to one of the following values:
DISPATCH_METHOD Method call in process
DISPATCH_PROPERTYGET Retrieving property value
DISPATCH_PROPERTYPUT Setting property value
DISPATCH_PROPERTYPUTREF Property is being changed by reference, not by value. In
short, is it a var parameter?
If there are any arguments to the call, they are placed in the dispParams parameter. If a result
is returned from the call, then it is placed in pvarResult; otherwise, this parameter is set to nil.
The last two parameters involve error-reporting if something goes wrong.
Here are the possible return values from Invoke. I won’t bother to explain them, as they are
well declared:
S_OK
DISP_E_BADPARAMCOUNT
DISP_E_BADVARTYPE
DISP_E_EXCEPTION
DISP_E_MEMBERNOTFOUND
DISP_E_NONAMEDARGS
DISP_E_OVERFLOW
DISP_E_PARAMNOTFOUND
DISP_E_TYPEMISMATCH
DISP_E_UNKNOWNINTERFACE
DISP_E_UNKNOWNLCID
DISP_E_PARAMNOTOPTIONAL
I’m not going to dig any further into this function. You should, however, have enough infor-
mation now to see the way the basic process operates. For additional information, see the rel-
evant Microsoft documentation. For instance, it is instructive to look this function up in the
MSDN, if you have a copy available.
FIGURE 31.4.
The RunAuto program is
similar to the MakeOle1
project seen in the last
chapter.
Listing 31.8. The code for the RunAuto program shows how to call an automa-
tion object without the aid of the VCL.
unit main;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Ole2, OleAuto, Menus,
ExtCtrls;
type
TForm1 = class(TForm)
MainMenu1: TMainMenu;
File1: TMenuItem;
Exit1: TMenuItem;
Exes1: TMenuItem;
ExeGetObject: TMenuItem;
continues
var
Form1: TForm1;
implementation
{$R *.DFM}
uses
OleBox;
const
CLSID_WordBasic: TGUID = (
D1:$000209FE;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
CLSID_MyAuto: TGUID = (
D1:$C9B0B162;D2:$1307;D3:$11cf;D4:($AB,$35,$00,$00,$C0,$7E,$BA,$2B));
if Succeeded(hr) then
MessageOut(‘Object Exists’)
else begin
MessageOut(GetOleError(hr));
Exit;
end;
OleCheck(ClassFactory.Release);
OleCheck(P.Release);
end;
continues
end.
The key method in this program is the one to TForm1.CreateInstance . The call to
TForm1.GetClass also invokes the TMyAuto object, but it does not do anything interesting with
the object once it has hold of it. As a result, it’s the CreateInstance method that forms the
center of the program’s intellectual life.
The call to CoCreateInstance should be self-explanatory after reading the last chapter:
hr := CoCreateInstance(CLSID, nil, ClassContext, IID_IUnknown, P);
This function retrieves a pointer, P, which is an instance of IUnknown. You can then ask IUnknown
if it supports IDispatch:
hr := P.QueryInterface(IID_IDispatch, P1);
If it does, then you know you have hold of a copy of TMyObject, and you can begin calling the
method and properties that it supports.
Here is the code to access a property called Sam that is supported by TMyObject:
S[0] := AnsiToUnicode(‘Sam’, Size);
hr := P1.GetIDsOfNames(GUID_NULL, @S, 1, 0, @Disp);
OleCheck(hr);
VirtualFree(S[0], Size, Mem_Release);
P1.Invoke(Disp[0], GUID_NULL, 0, Dispatch_PropertyGet, DispP, @V, nil, nil);
ShowMessage(V);
S is an array of WideChars, and I use the AnsiToUnicode function from the OleBox unit to fill its
first member. Remember that there are similar routines in the SysUtils unit that are a bit easier
to use. In particular, they don’t require that you call VirtualFree.
Once you have filled out the array, you can pass it to GetIDsOfNames, which returns an array of
IDs in its last parameter. In this particular case, there will only be one ID in the array, and you
can pass it to Invoke in that method’s first parameter. This is a property, so Dispatch_PropertyGet
is passed in the fourth parameter.
The address of the value returned by the function is passed in the sixth parameter. This vari-
able is called V, and it is declared to be of type variant. You will almost always pass variants in
this parameter when working with OLE automation.
Once V has been successfully retrieved, the only task left is to show the value to the user. This
is simple enough because the variant can convert the integer associated with property Sam into
a string:
ShowMessage(V);
Once you understand how this process works, it’s easy enough to see how to call a method,
rather than accessing a property:
S[0] := AnsiToUnicode(‘ChangeCaption’, Size);
hr := P1.GetIDsOfNames(GUID_NULL, @S, 1, 0, @Disp);
OleCheck(hr);
VirtualFree(S[0], Size, Mem_Release);
P1.Invoke(Disp[0], GUID_NULL, 0, Dispatch_Method, DispP, @V, nil, nil);
The method in question here is called ChangeCaption. The first step is to place this name into
an array of WideChars, and to pass it in to GetIDsOfNames. This function returns the appropri-
ate ID in the variable called Disp, which is in turn passed to Invoke.
In this case, there is no need to worry about a return value, as the method will simply be in-
voked and will have no side effects. As you recall, the method does little more than pop up a
message box, as shown in this code fragment from the TMyAuto object:
if flags and DISPATCH_METHOD <> 0 then begin
if dispIdMember = 3 then
ShowMessage(‘Groovy AutoTalk Jive’);
Result := NOERROR;
end;
Here you can see that the code first checks to see if it is indeed a method that’s being called,
that is, whether the flag is set to DISPATCH_METHOD. If it is a method and the ID associated with
that method is 3, the method is called. In this particular case the method is nothing more than
a call to ShowMessage, but there is no reason why a separate routine of considerable complexity
could not be invoked in its stead.
That’s all I’m going to say about the RunAuto program. Clearly this is a subject that could be
dwelled on in greater depth, but hopefully the information presented here will be enough to
get you started with this fascinating technology.
This code takes advantage of the simple routines in the OLEAUTO.PAS unit that ships with
Delphi. In many ways this technique is simpler and more intuitive to use the methods shown
in the RunAuto program. However practical it might be, this kind of code also protects you
from fundamental OLE routines that you ought to understand.
Summary
In this chapter, you got a look at structured storage and the technology behind OLE automa-
tion. In particular, you learned how to create compound files with these two calls:
StgCreateDocFile,
Storage.CreateStream,
You also learned how to open a compound file with these two calls:
StgOpenStream
Storage.OpenStream
The section on automation introduced you to the IDispatch interface, which is one of the most
important in OLE. The example code gave specific examples of how to implement GetIDsOfNames
and Invoke, and how to call these functions from inside an executable.
There is little doubt but that OLE deserves its reputation for complexity. However, you have
seen that it is not an unapproachable subject, and that it can be mastered if you have the time
and the will to set your mind to the task. No one can say for sure where the computer world is
headed, but there is at least a possibility that code of this type will come to dominate the com-
puter world over the next five years.
VII
IN THIS PART
Multimedia
and Games
The chapter on the Games SDK was written by multimedia expert Mike
Scott. One of Delphi’s earliest adopters, Mike has proven his
expertise in a number of settings, including construction of a popular Delphi
WinG add-on.Creating multimedia programs with visual tools is the
subject matter of this section. The first multimedia chapter probably
contains the easiest-to-understand material in this book. The second chapter
in this section, about advanced multimedia, takes an in-depth look at the
MCI interface to the Windows multimedia services.
Games SDK
32
by Mike Scott
■ DirectDraw 1134
1996 marks a turning point in the computer games industry. Microsoft has just introduced
the Windows 95 Game Software Development Kit (Game SDK) to great critical acclaim from
game developers and users alike. This is pivotal to the Windows game market. In this chapter,
you learn how you can program the graphical part of the Game SDK using Delphi 2.0.
NOTE
The Game SDK is available from Microsoft on CD-ROM. This must be installed on
your system before you can run any of the examples in this chapter. In particular, you
need the freely redistributable portion of the games SDK found on the CD for this
book. Once you have those DLLs, the Object Pascal files distributed with this book
will allow you to access them and use them in your own programs.
Historical Perspective
Microsoft Windows never really has been able to compete with DOS as a game platform. This
is a pity, because it has some great characteristics that game writers could use: device indepen-
dence, a large protected-mode memory space, and so on. But it’s the device independence that
also has proven to be the stumbling block. The developer has been shielded from the eccen-
tricities of the specific hardware by a layer of software device drivers. This has been great for
writing code that works on all kinds of devices, but something had to give—and that was speed.
Not only that, but Windows even stopped the programmers from getting their hands dirty by
preventing direct access to bit-map surfaces. This generally was a good thing, because the bit-
map format is defined by the driver and graphics card, and is therefore proprietary. True, often
the format is obvious—but it’s not guaranteed to not change. Of course, the graphics-card
hardware itself also was taboo. Windows applications have to be sociable and conform to strict
rules. There was no way you could be allowed to get at the screen directly, because other pro-
grams would be sharing it with you.
You had to make display requests through the Windows Graphical Device Interface (GDI)
API. This (along with its cousin, the user interface, which lives in USER.DLL) manages the
screen and arbitrates contentions by overlapping windows and clipping output to regions in-
side these windows. The GDI is reasonably rich, but it was born as part of an operating system
designed for business computing. This fact is betrayed by the glaring omission of one of the
most essential functions for game programming: a function that overlays images with trans-
parent areas. Almost all games require this feature somewhere.
The advent of Windows 3.0 helped slightly with the introduction of the device-independent
bitmap, or DIB for short. This has a defined format so you can access it with confidence. New
API functions were added to allow conversion from DIBs to device-dependent bitmaps (DDBs)
and vice versa. Because the DIB format is open, you could easily get at the pixel data yourself
and, for example, write your own high-performance routines for transparent overlays, if neces-
sary. The only problem was that the performance of the DIB conversion and display routines
was awful. So no matter how brilliant your 32-bit, hand-crafted assembler was, the bottleneck
was getting the final image to the screen.
In the last couple of years, we’ve seen the growth in popularity of the Windows graphics accel-
erator to the extent that it now has become ubiquitous. Even the most basic PC has some form
of acceleration these days. However, once again, DIBs could not take advantage of this. Of
course, the same is true of DOS, where games still are not able to use the silicon processing
power languishing on modern graphics cards. One reason why game writers never took to us-
ing this power is the proliferation of proprietary chip sets and standards. Windows is such a
huge market, and every player offers a driver for that environment, so your Windows graphics
code looks the same no matter whose card it’s running on. But there ain’t no such thing as a
generic game driver that lets the game developer get at the accelerator.
Over the past few years, Microsoft has gradually been waking up to this situation. It has openly
recognized the growing need for Windows to provide better support for game writers. The first
move toward this was the release of DIB.DRV, which was a device driver that used DIBs as its
native format. This gave you a DIB that you could paint on using the standard GDI calls, and
you could get at it directly with your own code. But it still wasn’t any good at getting the image
onto the screen quickly.
In the middle of 1993, Microsoft made its next stab at a solution in the form of WinG. This
was a development of DIB.DRV, but had optimized routines for copying the DIB onto the
graphics card. In most cases, the performance of this operation was nearly as good as the native
BitBlt code in the driver, which was optimized by the manufacturers for specific cards. A great
achievement, but you still had to compose your image on an off-screen DIB buffer and then
move all that data to the display. This was a real waste of time. What’s more, it couldn’t be
synchronized with the vertical retrace, making screen updates “tear” as the raster caught them
in mid-step.
Meanwhile, the DOS games had it all their own way. Their trump card is undoubtedly the
ability to page-flip between two or more display memory areas. This technique is simple, and
all modern (and even not-so-modern) home computers and consoles use it. Basically, you com-
pose the next frame in the memory buffer that’s not currently visible. When you’re finished,
you set some registers on the graphics card, telling the card to display this second buffer in-
stead. Then you do it all over again with the first buffer, and so on. You can easily synchronize
this process with the vertical retrace, so when the next raster scan starts, it’s displaying a whole
new, different image. There’s no moving around of memory blocks, so the technique is effi-
cient.
The other advantage, of course, is that you can’t see the frames being drawn. You see them
only when they’re complete. Of course, this flipping occurs quickly, with rates of 25 frames
per second now common. At such speeds, it’s too quick for your eye to see. The motion be-
comes apparently continuous, just as it is with television and movies.
But there was a crunch coming. Microsoft realized that their future was in Windows. DOS is
an ancient relic, and has become obsolete in virtually all areas except one. Yes, those darned
games! And with the massive drop in prices and the subsequent huge take-up of PCs as home
computers, the game market is important. In fact, it’s so important in the home sector that
unless Microsoft could offer a viable, or even better, alternative to DOS, the old operating system
would hang around forever just so people could play their games using it. It was out of this
necessity that the Microsoft Game SDK was born.
This is the graphical part of the Game SDK. It is by far the largest and most signifi-
cant part, answering the concerns raised a moment ago. It gives transparent access to
hardware acceleration, provides direct access to bitmaps in off-screen video memory
and supports the page-flipping capabilities of the hardware.
■ DirectSound
This provides a set of functions to help build multi-player games, either over a modem
or on a network.
■ DirectInput
This is a simple API function that installs the other DirectX components on a user’s
system, making it easier to write an installation program.
As you can see from the list, the Game SDK is a large and sophisticated development platform.
To cover all the DirectX components would require several chapters, maybe even a whole book.
Here I explore the most innovative and ground-breaking of these: the DirectDraw graphics
system.
As you work through the chapter, you build a DirectDraw page-flipping application from scratch.
Each stage in the tutorial illustrates a specific feature or function, and how to implement it in
Delphi 2.0. You’ll be able to use the code here as a basis for your own DirectX applications. I
cover:
You never create a COM object by calling a constructor, either. Instead, you call a function in
DirectDraw to create the main DirectDraw object. This object has further methods that you use
to create the other objects.
Apart from these two issues, you really can think of them as Delphi objects.
The DirectDraw COM objects are defined in a rather complicated C header file supplied with
the Game SDK. However, I have translated these into an import unit that you can use in Delphi.
This is file DDraw.pas on the companion CD. To access DirectDraw, you simply add DDraw to
your unit’s uses clause.
DirectDraw
DirectDraw can be a fairly tricky API to use. It seems simple enough at first glance; there are
only a few COM classes, and they don’t have that many methods. However, DirectDraw uses
records to specify all kinds of different parameters when creating its objects. At first sight, these
look really daunting. You can find them in the Game SDK Help file starting with the letters
DD, such as DDSurfaceDesc. Being a relatively low-level API, there are many options and set-
tings that allow for differences in hardware specifications and capabilities. Fortunately, in the
vast majority of cases, we can ignore most of these options. The biggest problem at the time of
this writing is the lack of information in the GDK documentation, which describes what com-
binations of options are allowed.
So to help you find your way through the minefield, this chapter goes through all the stages in
building a DirectDraw application in incremental steps. I present the code that’s added at each
stage and use that to explain an aspect of DirectDraw, as well as to give you a working example
on which to build your own programs.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DDraw ;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
DirectDraw : IDirectDraw ; // the main DirectDraw object
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
You can find this trivial example in the DDDemo1 directory on the companion CD. It doesn’t
do anything obvious when you run it, so don’t expect too much! I include it to show you how
little code is required to create and free a DirectDraw COM object. It really is that simple.
By switching into a special display mode, you take over the whole display. Normally, you can
run many regular Windows applications at the same time; their windows overlap and every-
thing is handled nicely for you by the GDI. But what happens if you try to run two or more
full-screen DirectDraw applications at the same time? The answer is that only one is allowed
full-screen access. DirectDraw controls this by requiring that you have exclusive access to the
display card before you change modes. You do this by setting the DirectDraw object’s coopera-
tive level to exclusive. DirectDraw allows only one application to hold exclusive level at a time.
If you try to get exclusive access and some other application has it, the call fails. Similarly, if
you try to change display modes without obtaining exclusive access, that call fails. So you try to
get exclusive access and then switch display modes.
One thing to note here is that you must supply a window handle to SetCooperativeLevel.
DirectDraw resizes this window automatically so that it fills the screen in the new display mode.
You must pass the handle of a form to SetCooperativeLevel. Because the window handle has
not been created by the time OnCreate is called, you must do all this in the OnShow event. List-
ing 32.2 shows how to do this.
So far so good—nothing complicated here. If you run the example now, your application
switches modes and you see your form filling the screen. If you move it or resize it, you see
Delphi behind it. You’re still looking at the GDI output surface. GDI can output happily in
these different modes, so you’ll see your ordinary Windows applications as long as this surface
is at the front. But because you’re creating a page-flipping application, that will not always
necessarily be the case.
The DDDemo2 directory contains the example so far.
offending line. The problem is that when page-flipping, you will probably not be able to see
the IDE and your application will appear to freeze. Even worse, if you manage to continue
execution, or have the Break on exception IDE option switched off, you may not see the mes-
sage box that comes up giving the exception message.
One way to avoid this scenario is to uncheck the Break on exception checkbox in the IDE
(Tools | Options menu) and install a special application-exception handler in your applica-
tion. This exception handler should switch to the GDI surface before showing the exception
message. This is a lot easier than it may sound. All you have to do is create a private method in
your form and assign it to Application.OnException in the form’s OnCreate. Remember to set
it back to nil in OnDestroy. The new handler can use the IDirectDraw object’s SwitchToGDISurface
method before calling MessageDlg. Listing 32.3 shows the exception handler.
begin
if Assigned( DirectDraw ) then DirectDraw.FlipToGDISurface ;
MessageDlg( E.Message, mtError, [ mbOK ], 0 ) ;
end ;
Remember to switch off Break on exception (in Tools | Options). Once you get a bit more
experienced, you can try switching it on again for specific debugging tasks. However, if your
application raises an exception while the GDI surface is not visible, the IDE will take over, and
you won’t see anything. Pressing F9 should restart execution, or press Ctrl-F2 to reset the ap-
plication and return you to Delphi.
Display Surfaces
Now you’re ready to create the display surfaces. In DirectDraw, a display surface represents a
linear region of display memory that can be directly accessed and manipulated. The display
surface that you see on the screen is called the primary surface.It represents the visible frame
buffer memory on the display card. You also can have nonvisible surfaces, which are termed
off-screen, or overlay, surfaces. These may exist either in regular system memory or in an off-
screen area of memory on the graphics card itself. To create a page-flipping situation, you need
the primary surface and at least one off-screen surface to flip with it. In order for the off-screen
surface to be flippable, it must reside in video memory. However, DirectDraw tries to create
surfaces in video memory by default, so you don’t have to do anything special.
There’s a way to create the primary surface and one or more flipping surfaces at the same time:
by creating a complex surface . Another useful aspect of creating a complex surface is that you
can free all surfaces in a complex flipping chain by freeing the complex surface. You’re about to
create a complex surface with one back-buffer surface for the example.
NOTE
Any background surfaces that are created as part of a complex surface are known as
implicit surfaces. There are a number of things you cannot do with implicit surfaces,
such as detach them from the primary surface or free them independently of the
primary surface. However, complex surfaces are much easier to create because
DirectDraw creates the back buffers for you and links them to the primary surface.
At this point I must touch on the complexity of DirectDraw, because you must fill in fields in
a TDDSurfaceDesc record. If you look this up in the DirectDraw Help file, you can see that it
looks rather daunting! But, as I said before, you can happily ignore most of these fields. Listing
32.4 shows the code you need to add to the OnShow handler to create the complex surface.
DDSurface is a local variable. The dwFlags field tells DirectDraw which of the other fields are
valid. This is what enables you to ignore most of them. In this case, you’re only using ddSCaps
and the dwBackBufferCount. (ddSCaps must always be filled in and valid.) Note how the record
has a dwSize field that must be filled in with its own size. If you don’t do this, the call to
CreateSurface will fail immediately. If you have a call failing and seem to be filling everything
in correctly, check to be sure that you’re doing dwSize. It’s easy to forget. PrimarySurface is a
private IDirectDrawSurface object that I’ve added to the form’s declaration after the DirectDraw
object.
You also must remember to free the surfaces, which you can do in the FormDestroy handler by
adding the following code before the DirectDraw object is freed:
if Assigned( PrimarySurface ) then PrimarySurface.Release ;
Remember, freeing a complex surface frees all implicit surfaces that were originally created with
it.
The first parameter to Flip is the surface to which you want to flip. Passing nil means flip to
the next surface. Flip always synchronizes with the vertical retrace. However, it returns an er-
ror if the retrace is not occurring when called. To avoid this, pass DDFLIP_WAIT to cause the
method to not return until the flip has happened. There are times when you’ll want to do other
processing while waiting for the flip to occur. In this case, call Flip repeatedly in your loop,
passing 0 as the second parameter and checking the result to see if it’s DD_OK.
If you create more than one background flipping surface, each call to Flip cycles to the next
surface in the cycle. What actually happens when you call Flip is that the display memory as-
sociated with each surface is cycled to the next surface. The IDirectDrawSurface objects don’t
change order. The primary surface always remains the primary surface, and the display memory
changes “underneath it.” Similarly, the next surface to be flipped to always is the surface im-
mediately after the primary surface, even when there are many back buffers. The only excep-
tion to this is when you call Flip and pass it a specific surface to which you want to flip.
You also must modify the exception-handler method to disable the timer while the exception
message is being displayed. If you don’t do this, page flipping will continue merrily, making
the message box difficult to read! This is pretty straightforward, as shown in Listing 32.6.
And that’s it! You’ll find the example so far in DDDemo3. If you run it you see the display
flipping between the GDI surface, containing the form sized to fill the screen, and the back
buffer, which probably is filled with various bits of display “remnants.” Remember, press Esc
or F12 (or Alt+F4, of course) to exit.
DDSCaps is a local variable of type TDDSCaps, which you add to the FormShow handler. You fill in
the flags for the attached surface you want and call GetAttachedSurface. In this case, you want
the back buffer. The method can return only a single surface. The call fails if more than one
attached surface matches the passed DDSCaps flags. However, no matter how many background
surfaces you created, there’s always only one with the back-buffer flag, and that’s the first one
in the flipping chain after the primary surface. If you need to get all the attached surfaces, you
can call the EnumAttachedSurfaces function.
Restoring Surfaces
One of the many idiosyncrasies of DirectDraw is that surfaces may lose their memory for vari-
ous reasons; for example, when the display mode changes. When this happens, you have to call
the Restore method of the surface to get your memory back. You also have to redraw the sur-
face. It’s a bit like having to paint your window in regular Windows programming when it
becomes uncovered and needs updating. Most of the IDirectDrawSurface functions can re-
turn the DDERR_SURFACELOST result. When this happens, you must restore the surface and re-
draw it. Many of those functions also can return DDERR_WASSTILLDRAWING, which basically means
that the hardware is busy and you have to repeat the request until it succeeds, or until you get
a different error.
Here’s the basic logic, using the Flip method. This is a bare-bones example just to give you the
idea. It doesn’t redraw the surfaces. See listing 32.9.
The annoying thing is that you need code like this for almost every call to an IDirectDrawSurface
method. Any time the call specification in the Game SDK Help file has DDERR_SURFACELOST as
a possible result, it’s necessary. But Pascal is a structured, high-level language, right? So why
not write a little utility method to help us? Here’s just such a method, with the name borrowed
from one of my favorite shows. (It helps me remember what I called it!) It’s shown in Listing
32.10.
This method restores the surface if necessary, and then calls the form’s RestoreSurfaces func-
tion, which I show you in a moment. But first, here’s how you use it, using Flip as in the first
example:
repeat
until MakeItSo( PrimarySurface.Flip( NIL, DDFLIP_WAIT ) ) ;
Now, I’m sure you’ll agree that’s a lot neater and nicer than having to keep duplicating the
code I showed you first. Flip (or whatever other method you have to call) is called repeatedly
until either it succeeds or a serious problem occurs. I could have raised an exception in MakeItSo
if an unrecoverable problem occurred, but I decided to ignore the failure this time. There’s no
hard and fast rule in this case. For example, the Game SDK samples, being written in C with-
out exception handling, just ignore error results.
However, if you want to use exceptions, change MakeItSo as shown in Listing 32.11.
OK, now for the RestoreSurfaces method, called by MakeItSo when necessary. Listing 32.12
shows the RestoreSurfaces method.
No surprises there. The Restore method of the primary surface object is called. Because you
created it as a complex object, it restores any implicit surfaces automatically. Therefore, you
don’t have to call Restore for the back buffer. If Restore successfully restored the surface memory,
you call DrawSurfaces, which I discuss in detail next.
continues
Notice that I don’t use MakeItSo here when I call GetDC. There are two reasons:
■ First, the surfaces should have been restored just before DrawSurfaces is called.
■ Second, DrawSurfaces usually is called by MakeItSo, and you want to avoid infinite
recursion. If the GetDC call fails, Windows just doesn’t create any output. And it’s
extremely unlikely that GetDC would fail. If you do want to get an error message
should this occur, you can check for success and raise an exception.
Freezing Up Windows
Windows 95 uses a lot of 16-bit legacy code from Windows 3.1. Because of this, calling cer-
tain GDI code that obtains the Win16 mutex effectively stops preemptive multitasking be-
cause it suspends any other task that tries to output to the display using GDI. For example,
this happens when you call IDirectDrawSurface.GetDC. It’s therefore essential that you keep
the DC for as short a time as possible, and then relinquish it immediately using the ReleaseDC
method. Failure to do this will bring Windows 95 to its knees. That is another reason why I
protected the allocation of the DC with try…finally blocks.
BorderStyle := bsNone ;
Color := clBlack ;
Cursor := crNone ;
The only remaining thing you must do is make sure the surfaces get drawn properly at the
start. Do this by calling DrawSurfaces in the form’s OnPaint event handler. If you don’t do this,
the primary surface initially would display the form; that is, the screen would be completely
black. Listing 32.14 shows the form’s OnPaint event handler.
interface
type
TDDCanvas = class( TCanvas )
private
FSurface : IDirectDrawSurface ;
FDeviceContext : HDC ;
protected
procedure CreateHandle ; override ;
public
constructor Create( ASurface : IDirectDrawSurface ) ;
destructor Destroy ; override ;
procedure Release ;
end;
implementation
destructor TDDCanvas.Destroy ;
begin
Release ;
inherited Destroy ;
end ;
procedure TDDCanvas.CreateHandle ;
begin
if FDeviceContext = 0 then begin
FSurface.GetDC( FDeviceContext ) ;
Handle := FDeviceContext ;
end ;
end ;
procedure TDDCanvas.Release ;
begin
if FDeviceContext <> 0 then begin
Handle := 0 ;
FSurface.ReleaseDC( FDeviceContext ) ;
FDeviceContext := 0 ;
end ;
end ;
end.
To use this class, you should copy the DDCanvas.pas unit into the Lib directory, which lives in
your Delphi 2.0 directory, or to another directory that’s on your library search path.
Remember the dreaded Win16 mutex that stops the multitasking? Well, once again I empha-
size the necessity of releasing the DC. The TDDCanvas class has a Release method for this pur-
pose. Always wrap any access to the canvas in a try…finally block, like this:
try
DDCanvas.TextOut( 0, 0, ‘Hello Flipping World!’ ) ;
{ etc… }
finally
DDCanvas.Release ;
end ;
So now you can add a couple of such canvases to the form declaration, creating them in FormShow,
like so:
{ create TDDCanvases for our two surfaces }
PrimaryCanvas := TDDCanvas.Create( PrimarySurface ) ;
BackCanvas := TDDCanvas.Create( BackBuffer ) ;
Now you can output to either the primary surface or the back buffer just by using these can-
vases. So you change DrawSurfaces to use them, simplifying the code greatly, as shown in List-
ing 32.16.
continues
Note the try...finally blocks with the call to Release. Apart from that, you’ve now gotten to
the stage of being able to draw on the DirectDraw surfaces without any nasty DirectDraw code—
just nice Delphi canvas methods!
begin
// try to load the bitmap as a resource, if that fails, try it as a file
Bitmap := LoadImage( GetModuleHandle( NIL ), PChar( BitmapName ),
IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION ) ;
try
if Bitmap = 0 then
Bitmap := LoadImage( 0, PChar( BitmapName ), IMAGE_BITMAP,
0, 0, LR_LOADFROMFILE or LR_CREATEDIBSECTION ) ;
if Bitmap = 0 then
Raise Exception.CreateFmt( ‘Unable to load bitmap %s’, [ BitmapName ] ) ;
You pass DDReLoadBitmap a DirectDraw surface and the name of the bitmap you want to load
into the surface. The procedure first tries to load from a resource, assuming BitmapName is a
resource name. If that fails, it assumes you passed a file name and tries to load it from there.
What actually is created by LoadImage in this case is a DIB section. This is a Windows Hbitmap
with the format of a device-independent bitmap (DIB). You can use such a DIB section as if it
were a regular Hbitmap; for example, selecting it into a DC and calling GDI’s BitBlt standard
function.
DDReLoadBitmap calls another utility routine, DDCopyBitmap, which copies the DIB section im-
age on to the DirectDraw surface. The try…finally block then disposes of the DIB section because
it’s no longer needed. Unlike Windows 3.x bit-map handling code, this procedure is pretty
trivial.
Now, how about DDCopyBitmap? It’s not much more difficult, as shown in Listing 32.18.
continues
After doing some parameter checking, DDCopyBitmap calls Restore to make sure the surface
memory is valid. It then goes through the normal Windows routine for copying a bitmap from
one DC to another. The source bitmap is selected into the first DC, a standard memory DC
obtained by a call to CreateCompatibleDC. Passing a width or height of zero to the routine tells
the routine to use the actual width and height of the bitmap. To get this information, the rou-
tine uses the GetObject function.
Next, a SurfaceDesc record is prepared by switching on the DDSD_HEIGHT and DDSD_WIDTH flags.
This is passed to GetSurfaceDesc, which responds by filling in the dwHeight and dwWidth fields
of the descriptor. The routine obtains the second DC from the surface by calling GetDC and
doing a simple StretchBlt. As usual, try..finally blocks are used to ensure that the DCs are
released. All pretty simple stuff. It dispels the old reputation that Windows bit-map code was
hard to write. Hopefully, you’ll now be able to jump into writing code like this without feeling
daunted by the prospect!
The DrawSurfaces code has been further simplified because the back buffer is now loaded else-
where using DDReloadBitmap. The simplified DrawSurfaces is shown in Listing 32.19.
I’ve added a palette object to the form’s declaration, loaded it in FormShow, and attached the
palette object to the primary surface as follows:
{ load the palette from the bitmap and attach it to the primary surface }
DDPalette := DDLoadPalette( DirectDraw, GetBitmapName ) ;
PrimarySurface.SetPalette( DDPalette ) ;
Having created it, you must free it from the primary surface in FormDestroy:
{ free the DD palette }
if Assigned( DDPalette ) then DDPalette.Release ;
Having made all the changes, you’re ready to try it. DDDemo5 has all the changes up to this
point.
NOTE
Bit-blitting is a term used to describe the transfer of areas of bitmaps to, from, or
within other bitmaps. The term is sometimes written more accurately as bitblting, but
this is difficult to read so you’ll often find it split into the two words bit-blitting as it is
here. BitBlt is short for the term BITmap BLock Transfer.
So, to work. Create this extra surface and call it Image. Add it to the form declaration. It’s just
an IDirectDrawSurface, so there’s no need to show that trivial line of code here.
Next, add code to FormShow that creates the bitmap. Using DDLoadBitmap, this is only one line!
Here it is:
Image := DDLoadBitmap( DirectDraw, GetBitmapName, 0, 0 ) ;
The other thing you must remember to do is add to the RestoreSurfaces method now that
you have a new, explicit surface. If the primary surface memory restoration went OK, try to
restore the Image surface memory. If both restorations work, call DrawSurfaces as shown in
Listing 32.20.
and end colors for the color key. You’re using only one color, so both start and end values are
the same. At the time of this writing, it appears that DirectDraw supports only a single color
source key in any case. Once the color values are filled in, call the SetColorKey method of
IDirectDrawSurface , telling it that you want to use this as the source key when blitting. Add
these lines to FormShow:
ColorKey.dwColorSpaceLowValue := 0 ;
ColorKey.dwColorSpaceHighValue := 0 ;
if Image.SetColorKey( DDCKEY_SRCBLT, ColorKey ) <> DD_OK then
Raise Exception.Create( ‘SetColorKey failed’ ) ;
Once again, note the try…finally around the TDDCanvas to make sure TDDCanvas gets freed.
As you can see, the Create constructor takes the parameters that define the sprite and stores
them away in the object’s fields. The property CurrentFrame tells us which frame of the sprite
should be displayed, given the current TickCount. The TickCount is actually the number of
milliseconds that have elapsed since Windows started up. You can obtain this simply by calling
the GetTickCount function. The timer used has a high resolution, down to one millisecond.
This is a lot better than using a TTimer component, which has a resolution of 55 milliseconds;
it can’t trigger faster than about 18 times a second, and that’s too slow for game use. Also, a
TTimer is not very accurate, and gives jerky results.
the time in FLastTickCount and wraps around to the start frame when the sequence goes past
the last one.
You add this code straight into the main form unit. It’s not a component, so it doesn’t have to
be added to the component palette. Instances are created only at runtime, with code. You do
this by adding a TList called Animations to the form declaration. This list is created in FormShow,
and you add three animation objects to it:
Animations := TList.Create ;
Animations.Add( TSimpleAnim.Create( 48, 60, 260, 120 ) ) ;
Animations.Add( TSimpleAnim.Create( 12, 60, 420, 248 ) ) ;
Animations.Add( TSimpleAnim.Create( 80, 60, 200, 320 ) ) ;
Note that the second parameter to Create, the number of frames in the animation sequence, is
60. What does this mean? Well, if you open ALL.BMP and scroll down, you see all the anima-
tion frames underneath the background image. There are 60 in all, arranged in six rows of 10.
The main image is 640 by 480 pixels, to match the display resolution I’m using. Each frame is
64 pixels square, making 10 fit nicely across the bitmap.
// now flip
repeat
The first thing this does is blit the background image to the back buffer. Remember that the
top part of ALL.BMP is the background. ARect is created to define the rectangular area of that
part of the bitmap and BltFast. BltFast functions more or less the same as Blt, but can’t stretch
the bitmap and doesn’t do any clipping. You just give it the source surface to blit from, the
rectangle on that surface that you want to blit, and the position on the destination where you
want it to go. The last parameter tells BltFast to ignore the color key you set up, because this
is the background and not one of the animation images. BltFast is quoted as being around 10
percent faster than Blt when done in software when there’s no accelerator. Otherwise, there’s
no difference.
Now blit each of the animations. Get the TickCount and then loop through the Animations
list. Obtain the current animation frame and then calculate the position of this frame in
ALL.BMP. Calculate the x coordinate by doing a modulo (remainder) operation between the
current frame and the number of frames per line. This works out to 10, which gives the index
of the frame in the row. This is multiplied by the width of each frame, which is 64.
The y coordinate is calculated by dividing the frame number by 10 to get the row number;
multiplying by the height of each row, which is 64; and adding 480. This is where the frames
start in ALL.BMP: immediately under the 640×480 background image. All that’s necessary
now is to use BltFast again to blit the frame on to the back buffer, this time using the color
key.
Once all animations have been drawn, the page flip is done. There is one vital thing to realize
at this point: the page flip does not occur until the vertical retrace, so the application sits in the
tight MakeItSo loop until this occurs. This sets the overall frame-per-second rate of the applica-
tion. There’s no need to control the animation with any timer. It simply goes as fast as it can,
using GetTickCount to determine which animation frame to display.
NOTE
The image on a computer screen is created by an electron beam which starts at the top
left of the picture and scans horizontal lines one under the other from left to right until
it reaches the bottom-right corner. The beam then returns to the top-left to start again.
The rate at which this scanning occurs is typically between 60Hz and 80Hz, although
it can vary from 50Hz to well over 100Hz with the latest display cards and screens.
The vertical retrace is the time when the electron beam is reset to the top-left in
readiness for the next scan. Changing the image being displayed at this point, before
the scan begins, results in perfectly smooth, flicker-free animation.
You may note an extra line at the end that calls a method to update the torus position. I de-
cided to make the demo more interesting by letting you move the animation around with the
keyboard. This is easy to do: the position is stored in the TSimpleAnim class. Listing 32.24 shows
the code for the Move method.
Listing 32.24. The Move method moves the animations around the screen.
procedure TForm1.Move( dx, dy : integer ) ;
var i, nFrom, nTo : integer ;
begin
// move the toruses
if Assigned( Animations ) then begin
if MoveOption = 0 then begin
nFrom := 0 ;
nTo := Animations.Count - 1 ;
end else begin
nFrom := MoveOption - 1;
nTo := nFrom ;
end ;
for i := nFrom to nTo do begin
with TSimpleAnim( Animations[ i ] ) do begin
X := X + dx ;
if X < 0 then X := 0 else
if X > 640 - 64 then X := 640 - 64 ;
Y := Y + dy ;
if Y < 0 then Y := 0 else
if Y > 480 - 64 then Y := 480 - 64 ;
end ;
end ;
end ;
end;
The method takes two parameters, which represent the distance in pixels to move each of the
animations in each axis. I added an extra field, called MoveOption, to the form. MoveOption takes
a positive integer value: 0 means to move all the animation objects, and any other number means
to move the corresponding animation object only. Having determined the range of animations
to move (either all of them or a single one), you do a loop through the Animations array, add-
ing dx and dy to each animation’s position. Move also checks to make sure the animation ob-
jects don’t move off the display.
If the Shift key is held down, the animations move four times slower, so the local variable Speed
is set up accordingly. I’ve added XVelocity and YVelocity fields, which are set if one of the
cursor keys is pressed. The Shift key, when pressed, slows the velocity down, if it’s not already
slow. The keys 0 through 9 set MoveOption, which controls which animations are moved. Note
that monitoring the press of a specific key means that the animation can respond differently to
different key combinations. In this case, you can move the sprites diagonally if you hold down
two of the cursor keys.
There now has to be an OnKeyUp handler to counter these effects when the key is released. This
is shown in Listing 32.26.
As you can see, movement is stopped in the horizontal or vertical direction when a cursor key
is released, and the speed is increased by a factor of four when the Shift key is released.
These are added to the private section of the form declaration like any other method. The
implementation is simple, mainly just calling UpdateDisplay. One crucial thing is to set Done
to False in IdleHandler to make sure the VCL doesn’t yield control awaiting a Windows message,
which would stop screen updating. This is shown in Listing 32.27.
Finally, add a FlippingEnabled property to the form, declaring it as shown in Listing 32.28.
public
property FlippingEnabled : boolean read FFlippingEnabled
write SetFlippingEnabled ;
end ;
This works by plugging or unplugging the Application event handlers, which is implemented
in the SetFlippingEnabled method, as shown in Listing 32.29.
FlippingEnabled is set to True at the end of FormShow once you’ve set everything up. It’s set
back to False temporarily in the exception handler to pause the application so you can see the
message box, as Listing 32.30 shows.
Having done that, you’re ready to run it. The code is in DDDemo6. If you play around with
it, moving the tori around with the cursor keys, you’ll see where we heading. We virtually have
true sprites. With a little more work, that’s exactly what we will have.
We Need Sprites!
You’ve seen how easy it is to manipulate animated objects with a simple animation class. Now
we go whole hog and do a generic TDDSprite class. The TSimpleAnim class was a bit too simple,
and was designed specifically for the last demo. Now let’s start from scratch. (The sprite class
I build here is a simplified version of the sprites from my commercial Game SDK Toolkit for
Delphi 2.0. See the Author information for details.)
First of all, define some characteristics that you’d like these sprites to have. This generic sprite
class should:
■ Be self-contained and able to be subclassed to satisfy specific needs
■ Be frame-based, supplying the frame number to display on demand
■ Not do any rendering itself, leaving that responsibility to another class or a subclass
■ Use fixed point coordinates on the display surface for fine control
■ Have a fixed-point velocity vector, and be able to move along this vector automatically
■ Have a fixed-point acceleration vector, and have this applied to the velocity automatically
■ Have the option of being constrained within a rectangular area
■ Have events that allow the position to be overridden, so that you can take manual
control over sprite movement and constraints
■ Be able to bounce off the edges of the constraining rectangle for test purposes
First, notice that I’ve specified fixed point for speed. You may ask, “Why not just use straight
integers?” However, consider the velocity issue. If you express a velocity as two integer compo-
nents, one horizontal and one vertical, you are severely limited in the number of possible angles
your sprite can move in. For example, say you wanted to move slowly, at a rate of one pixel per
time interval. You’d only be able to move in, at best, 45-degree steps. This is no good for a
useful sprite. If you’re going to have fixed-point velocity vectors, you also need a fixed-point
position. Otherwise, the fractional part of the velocity is lost with each movement. Similar
arguments apply to acceleration.
However, it’s easy to implement simple, fixed-point using longints. You simply multiply the
real number value by 65,536, and store the rounded result in an integer. You then can add
similar integers together without conversion, giving no loss in performance over integers. For
example, you can add the velocity and acceleration components using simple integer addition.
If you’re not too worried about the fractional part, you can even get the integer position by
simply accessing the high word of the longint.
To make this easier, declare a simple variant record to hold any coordinate-style value. I’ve
called this a TCoord, which you can see in Listing 32.31.
When you must do additions, you can access the full value by reading and writing the Value
field. If you need just the integer part, you can get at that by accessing Int.
Now, let’s see how to use this to build up the sprite class. First, let’s look at the position, veloc-
ity, and acceleration properties, as shown in Listing 32.32.
Notice that the position and size properties are integral. In the case of position, the fixed-point
value is rounded efficiently, without requiring any floating-point calculations, because the
position properties are stored internally as TCoords. Velocity and acceleration components are
similarly stored as TCoords, but are accessed as doubles. Conversion is required on read and
write, but this happens infrequently, when you must change the speed or acceleration. On the
other hand, the addition of these vectors occurs with every page flip, so it’s essential that this
function is efficient.
Next up is the animation-controlling properties. These are shown in Listing 32.33.
GetFrameNumber , and it’s event OnGetFrameNumber, enable you to override the current frame
number in the animation sequence. This lets you take complete control over the animation of
the sprite. CheckLimits and the OnCheckLimits event are called after the new sprite position has
been computed, letting you override that position, if necessary; for example, to constrain the
movement of the sprite. You also could set the position totally manually to make the sprite
move along whatever path you like, or you may modify the velocity and acceleration. This flex-
ibility lets you implement pretty much any kind of sprite movement and animation sequenc-
ing just by creating event handlers.
Finally, there are a couple of simple properties that control whether the sprite bounces around
inside its Limits rectangle instead of being able to move without limit in any direction, and
whether it is visible:
property AutoBounce : boolean read FAutoBounce write FAutoBounce ;
property Visible : boolean read FVisible write SetVisible default true ;
The implementation of the sprite class is pretty straightforward, and you can find it in the
DDSprite.pas file. I don’t go into the implementation details here because there’s really not
much to be gained, and it would take a lot of space. It’s all pretty simple. More important is
the way these sprites are used.
It would be nice if any sprite added to the form, either at design time or runtime, was auto-
matically added to this Sprites list. You can do this easily by overriding the form’s Notifica-
tion method, which is called whenever a component is added or removed. The Notification
method also is called when a component is removed from the form, so you can delete any ref-
erence to a sprite in the Sprites array. Listing 32.35 shows the Notification method.
begin
// watch for sprites being added or removed,
// and update the Sprites list accordingly.
inherited Notification( AComponent, Operation ) ;
if AComponent is TDDSprite then begin
if Operation = opInsert then begin
// if the sprite has no limit set, set limit to the full screen
with TDDSprite( AComponent ) do
if IsRectEmpty( Limits ) then Limits := Rect( 0, 0, 640, 480 ) ;
Sprites.Add( AComponent ) ;
end else
Sprites.Remove( AComponent ) ;
end ;
end ;
This checks to see whether the component is a TDDSprite using the is operator. If so, if the
component is being added, you add it to the Sprites list. If the component is not being added,
it’s being removed; you remove it from the list. I’ve added code here to set the Limits rect to
the full screen if it’s initially empty. An empty Limits rectangle means no limits; if a sprite is
moving, it would disappear off the edge of the surface.
However, there is a potential pitfall here because Notification is called during the processing
of TForm.Create. If any sprites are added to the form at design time, the Notification method
tries to add them to the Sprites list. Therefore, the list has to be created before the inherited
Create constructor is called. Fortunately, there is no problem doing this. You simply override
the constructor and create the Sprites list before calling the inherited constructor, as shown in
Listing 32.36.
begin
Sprites := TList.Create ;
inherited Create( AOwner ) ;
end ;
In FormShow, you call Randomize because you’re going to be using the Random function. Add a
single sprite to start with:
Randomize ;
AddSprites( 1 ) ;
The AddSprites function takes the number of sprites to add and creates them at random, add-
ing them to the Sprites list, as shown in Listing 32.37.
The code is self-explanatory. Notice the use of the fixed-point acceleration of 1.6 downward to
simulate gravity. We are using the same ALL.BMP torus frames for this example, so the width
and height are both 64 pixels and AnimationStop is 59 again. You would use similar code to
add sprites such as missiles, enemies, or players to your game. To delete a sprite, you simply
free it.
Having added the sprites, call UpdateStatsSurface, which updates statistical information dis-
played with each page flip.
Displaying Statistics
When writing DirectDraw applications, it’s often helpful to be able to display information as
the program is running. This is necessary because the program has taken over the whole screen,
and you can’t really use the IDE debugger effectively. I’ve done this in the sprite demo by cre-
ating another surface on which information, such as the frame rate, is drawn from time to time.
This surface is overlaid on the back buffer just before flip is called.
I’ve added the surface to the form as field Stats of type IDirectDrawSurface. It’s created in
FormShow, as shown in Listing 32.37.
You fill in the existing DDSurfaceDesc record. Use the form’s own Canvas to calculate the width
and height of the string you want to display, by calling TextWidth and TextHeight. This is used
to initialize StatsRect, which is a TRect I’ve added to the form. I’m creating a simple, off-screen
surface, so I just need to fill in the width and height fields, dwWidth and dwHeight, and then set
the flags in dwFlags to say that these fields are valid. Set the type of the surface in DDSCaps.dwCaps,
and then you’re ready to call DirectDraw.CreateSurface.
Next, set the color key as you do with the Image surface, because you’re going to be displaying
the statistical information as text overlaid on the top of the background and sprites.
I’ve added a StatsCanvas to the other TDDCanvas objects already there. Create it and initialize it
in FormShow so you don’t have to do that every time you must update the Stats surface, as
shown in Listing 32.38.
I need to know the number of milliseconds that have elapsed since the last sprite update, so I
call GetTickCount and subtract the result from LastTickCount, noted the last time around as
long as this is not the first time through (when LastTickCount will be zero). I also limit the
number of milliseconds in case some other system activity has prevented updating for much
longer than usual. This is quite common in Windows 95, and can be brought about by auto-
mated activity such as backups or disk scans. There may be several seconds’ delay, and I don’t
want the sprites to go orbital when they do move.
The code loops through the Sprites list and, for every visible sprite, calls the GetFrameNumber
method. This method returns the frame number that the sprite requests you to render. A quick
check is made to ensure that this number is not higher than 59, the number of frames in
ALL.BMP minus 1. You used the same calculation as last time to get the source rect for the
specific frame. Use BltFast to do the business, as before.
Now that the background and sprites have been drawn, you must overlay the statistical infor-
mation. This is simply a case of calling BltFast again with Stats as the source surface. Center
the output and move it a quarter of the way down the display:
Now you’re ready to do the page flip. This is the same as before, only this time you increment
FlipCount, which I added to the form. It’s an integer field used to keep a total of the number
of page flips that have been displayed for the frames-per-second calculation. Here is the modi-
fied flipping code:
// now flip
repeat
until MakeItSo( PrimarySurface.Flip( NIL, DDFLIP_WAIT ) ) ;
inc( FlipCount ) ;
Finally, move the sprites. This is a simple case of calling their Move method and passing the
number of ticks since the last move. The sprite class handles all of the details, calculating the
sprite’s new position based upon its velocity, updating that velocity by adding the acceleration,
and calling the OnCheckLimits event to enable you to override if you wish to. Update
LastTickCount before exiting the procedure. Listing 32.40 shows how you move the sprites.
// remember TickCount
LastTickCount := TickCount ;
end ;
Note that before each page flip, you blit the whole background area. This is 640 by 480 pixels,
or 307,200 bytes, and this defines the minimum blitting rate. If our monitor is refreshing at 75
Hertz, this produces a basic blit rate of 75 * 307,200, or just a touch under 22 MB per second,
even with no sprites! This kind of rate would be impossible over the old AT bus, and even with
local buses it would soak up a significant chunk of the available bandwidth. But we’re using
DirectDraw , and the pixels are just being blasted around the memory on the display card by the
display card’s blitter hardware. That’s why we can get such high blit rates.
You’ll often be able to see the blit rate going up as you add sprites. On a modern graphics card,
a rate of 40 MB per second is quite achievable. This is equal to around 60 of our sprites plus
the background; a pretty impressive result, considering it’s running at 75 frames per second.
Even more impressive, at a quite acceptable 25 frames per second we can have over 340 64×64
sized sprites—quite a feat!
Back to the code. In the OnTimer event we then call UpdateStatsSurface to output the text, as
shown in Listing 32.42.
Note the check for StatsCanvas.DrawingAllowed. If you press Alt-Tab to switch away from
your running application, DirectDraw minimizes its window and you lose your display surface
memory until the application is restored. But the timer keeps ticking away, calling this event.
If you attempt to draw on a TDDCanvas when the surface memory is unavailable, the canvas
handle 0 is returned and the VCL raises an exception with the message “Canvas does not allow
drawing.” Checking the DrawingAllowed function before using a TDDCanvas safeguards against
this.
The only other major change to the application is in the OnKeyDown handler. If you press up or
down cursor keys, a sprite is added or deleted, respectively. If you press Page Up or Page Down,
10 sprites are added or deleted at a time. Pressing Delete or Backspace clears off all the sprites.
Listing 32.43 shows the code for the FormKeyDown event handler.
Listing 23.43. The FormKeyDown handler with code to add and remove sprites.
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
// now handles keys for adding and deleting sprites.
case Key of
VK_ESCAPE, VK_F12 : Close ;
VK_UP : AddSprites( 1 ) ;
VK_DOWN : RemoveSprites( 1 ) ;
VK_PRIOR : AddSprites( 10 ) ;
VK_NEXT : RemoveSprites( 10 ) ;
VK_DELETE,
VK_BACK : RemoveSprites( Sprites.Count ) ;
end ;
end;
The final DirectDraw example, DDDemo8, does exactly this. It does away with the sprite-add-
ing and -removing code, the add/remove sprite key handling, and the instruction text. The
sprites are sitting on the form in the designer, already set up. You can play around with them
using the Object Inspector. You can make them invisible if you want to remove them tempo-
rarily from activity. The simple statistical code in the previous example just displays the total
number of sprites on the surface. I’ve modified this by adding an integer field to the form to
keep a tally of the number of visible sprites.
Summary
This brings us to the end of the DirectDraw section. There’s a whole lot more stuff in DirectDraw,
but it would need a whole book by itself to cover it all. But you now have enough information
and samples to churn out a page-flipping application in Delphi 2.0!
This chapter takes a look at the Games SDK as a tool for creating artificial worlds. You will see
how to create overhead views of large colorful maps that scroll back and forth across the screen.
You will also see how to plunge into a three-dimensional maze where you can wander about at
will.
The code shown in this chapter is not a fully developed game, or a complete simulation. In-
stead, it’s a framework out of which a game or artificial world could be built. This book is not
about end users; it’s about the people who program computers. My goal in this chapter is not
to entertain, but to show how to construct artificial worlds.
The code shown here is not overly fancy by today’s standards, but it would have turned more
than few heads as recently as five years ago. I include it because it represents a relatively easy-to-
understand example of how you can immerse the user in a computer-generated world. In other
words, the simulation is technically sophisticated enough to give the user the sense that he or
she is actually moving through a real world.
This is a realm of programming in which imagination is supreme, and what you do with this
code is limited only by your own ability to engineer an environment of interest to others. This
chapter is also meant to be more than a little fun, and you would perhaps not be remiss if you
found yourself taking the time to play with this code merely for its own sake.
fictitious land. The total map is approximately 8 screens wide by 7 screens high, for a total of
some 56 screens of information.
FIGURE 33.1.
An overhead view of the
world in the XFire
program.
FIGURE 33.2.
Another look at the
overhead view of the world
found in the XFire
program.
As I said earlier, all of the code in this book is free for you to use as you wish, so long as your
goal is not to teach others how to use Delphi. However, the land depicted in the XFire pro-
gram is copyrighted. I provide tools (MAKEWRLD.DPR) that would allow you to design your
own land, but you cannot reuse the world shown here in your own programs!
The second view of the world presented by the XFire program is a maze view, as shown in
Figures 33.3 and 33.4. This is a three-dimensional maze where you can wander about as you
wish. The walls are solid, and you cannot walk through them. You can, however, strike out in
various directions.
FIGURE 33.3.
A view of the maze found
in the XFire program.
FIGURE 33.4.
A second view of the maze
from the XFire program.
The code for XFire is shown in Listings 33.1 through 33.9. This is a large program, which has
a number of dependencies to the XXXBox programs found in the UNITS directory that ships
with this book. This dependency on units like StrBox and MathBox is typical of real-world
programs. In other parts of this book I have tried to limit such dependencies, but in this chap-
ter I have simply used whatever tools I needed to put this code together.
{ ----------------------------------------
Copyright (c) 1996 by Charlie Calvert
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
DDraw, PalBox, DrawBrd1,
OleAuto, ExtCtrls, Menus,
GameBrd1;
const
AColor = 254;
type
TGameModes = (gmWorld, gmCity, gmTest);
TForm1 = class(TForm)
MainMenu1: TMainMenu;
File1: TMenuItem;
Exit1: TMenuItem;
Options1: TMenuItem;
Dialog11: TMenuItem;
StartGame1: TMenuItem;
ReturntoGame1: TMenuItem;
Timer1: TTimer;
WorldMap1: TMenuItem;
CityMap1: TMenuItem;
NewTry1: TMenuItem;
N2: TMenuItem;
procedure StartWorldMode(Sender: TObject);
procedure Dialog11Click(Sender: TObject);
procedure ReturntoGame1Click(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
procedure Timer1Timer(Sender: TObject);
procedure WorldMap1Click(Sender: TObject);
procedure CityMap1Click(Sender: TObject);
procedure NewTry1Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
continues
var
Form1: TForm1;
implementation
uses
Status, smallmp1;
{$R *.DFM}
procedure TForm1.Pause;
begin
ReturntoGame1.Enabled := False;
Timer1.Enabled := False;
if FGameBoard.Pause then
DrawMenuBar(Handle);
end;
end.
Listing 33.2. The DrawBrd1 unit surfaces some of the key features of the Direct
Draw interface.
unit DrawBrd1;
///////////////////////////////////////
// Copyright (c) 1996 by Charlie Calvert
// Base Object for use with DirectDraw
///////////////////////////////////////
interface
uses
DDraw, Windows, Classes,
OleAuto, DDUtils, Dialogs,
SysUtils;
type
EDDError = class(Exception);
TDrawDirect = class
private
FActive: Boolean;
FBackGroundMap: string;
FDirectDraw: IDirectDraw;
FBackSurface: IDirectDrawSurface;
FDDSFront: IDirectDrawSurface;
FWorkSurface: IDirectDrawSurface;
FDDPal: IDirectDrawPalette;
FHandle: HWnd;
FTransColor: Integer;
function CreateFront: Boolean;
function SetUpBack: Boolean;
function GetDib(Instance: THandle; S: string): THandle;
function CreateSurface(Bitmap: THandle;
DirectDraw: IDirectDraw): IDirectDrawSurface;
function LoadPalette(Instance: THandle;
DirectDraw: IDirectDraw; const BitmapName: string): IDirectDrawPalette;
function MakeItSo(DDResult: HResult): Boolean;
protected
function BackGroundBlits: Boolean; virtual;
// function ExtraBitmaps: Boolean; virtual;
procedure DDTest(hr: HResult; S: string);
function CreateDDSurface(var DDS: IDirectDrawSurface;
BitsName: string): Boolean;
public
procedure ErrorEvent(hr: HResult; S: string);
constructor Create(Handle: Hwnd; BackGroundMapStr: string;
TransColor: Integer); virtual;
destructor Destroy; override;
procedure DestroyObjects; virtual;
procedure DoOneFlip; virtual;
procedure InitFailed;
function InitObjects: Boolean; virtual;
procedure Move(Value: Integer); virtual;
function Pause: Boolean; virtual;
procedure Restore;
property Active: Boolean read fActive write fActive;
implementation
uses
OleBox;
destructor TDrawDirect.Destroy;
begin
DestroyObjects;
inherited Destroy;
end;
continues
SurfaceDesc.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or
DDSCAPS_FLIP or
DDSCAPS_COMPLEX;
SurfaceDesc.dwBackBufferCount := 1;
hr := FDirectDraw.CreateSurface(SurfaceDesc, FDDSFront, nil);
if hr <> DD_OK then
ErrorEvent(hr, ‘CreateFront’)
else
Result := True;
end;
// a DIB color table has its colors stored BGR not RGB
// so flip them around.
for i := 0 to n - 1 do with APE[ i ], RGB^[ i ] do begin
peRed := rgbRed;
peGreen := rgbGreen;
peBlue := rgbBlue;
peFlags := 0;
end;
continues
// a DIB color table has its colors stored BGR not RGB
// so flip them around.
for i := 0 to n - 1 do with APE[ i ] do begin
Temp := peRed;
peRed := peBlue;
peBlue := Temp;
end;
end;
end;
Lib := LoadLibrary(‘BitDll’);
if Lib < 32 then Exit;
FreeLibrary(Lib);
end;
continues
procedure TDrawDirect.InitFailed;
begin
FActive := False;
DestroyObjects;
ShowMessage(‘Failed’);
end;
procedure TDrawDirect.Restore;
begin
FDDSFront.Restore;
end;
end.
Listing 33.3. The GameBrd1 unit defines the key Direct Draw objects used in the
application.
unit GameBrd1;
const
BackGround = ‘Back’;
AColor = 254;
var
Handle: THwnd;
FGameBoard: TWorldBoard;
begin
FGameBoard := TWorldBoard.Create(Handle, BackGround, AColor);
if not FGameBoard.InitObjects then Exit;
FGameBoard.DoOneFlip;
end;
10
1 4 7 11 10 11 12 13 14
2 5 8 12 7 8 9
3 6 9 13 4 5 6
14 1 2 3
}
interface
uses
DDraw, Windows, OleAuto,
DDUtils, Dialogs, Classes,
PalBox, MzTown, DrawBrd1,
Entity;
const
MaxMapRows = 12;
MaxMapCols = 21;
WorldBackGround = ‘Back’;
CityBackGround = ‘City’;
type
TCityWalls = array[1..14] of Byte;
continues
TWorldBoard = class(TDrawDirect)
private
FCreatureList: TCreatureList;
FHorzPos: Integer;
FTimerOdd: Boolean;
FVertPos: Integer;
procedure DestroyObjects; override;
procedure DrawText;
function GetRect(Col, Row: Integer): TRect;
procedure MoveGrid(Col, Row: Integer);
function BackGroundBlits: Boolean; override;
public
constructor Create(Handle: hwnd; BackGroundMap: string;
TransColor: Integer); override;
procedure Move(Value: Integer); override;
property CreatureList: TCreatureList
read FCreatureList write FCreatureList;
end;
TCityBoard = class(TDrawDirect)
private
FDDSForeWall: IDirectDrawSurface;
FDDSFrontWall: IDirectDrawSurface;
FDDSMidWall: IDirectDrawSurface;
mt: TMazeTown;
function BackGroundBlits: Boolean; override;
procedure DestroyObjects; override;
public
constructor Create(Handle: hwnd; BackGroundMap: string;
TransColor: Integer); override;
function InitObjects: Boolean; override;
procedure Move(Value: Integer); override;
procedure DrawScene;
function HandlePanel: Boolean;
end;
TTestBoard = class(TDrawDirect)
FrontWall: IDirectDrawSurface;
FTimerOdd: Boolean;
constructor Create(Handle: hwnd; BackGroundMap: string;
TransColor: Integer); override;
function BackGroundBlits: Boolean; override;
function InitObjects: Boolean; override;
end;
implementation
uses
SysUtils;
{ -- TWorldBoard -- }
procedure TWorldBoard.DrawText;
var
S: string;
DC: HDC;
// Font, OldFont: hFont;
begin
BackSurface.GetDC(DC);
// Font := GetStockObject(Mono_Font);
// OldFont := SelectObject(DC, Font);
SetBkMode(DC, Transparent);
S := ‘Horz: ‘ + IntToStr(FHorzPos);
TextOut(DC, 370, 410, PChar(S), Length(S));
S := ‘Vert: ‘ + IntToStr(FVertPos);
TextOut(DC, 370, 440, PChar(S), Length(S));
// SelectObject(DC, OldFont);
BackSurface.ReleaseDC(DC);
end;
procedure TWorldBoard.DestroyObjects;
begin
inherited DestroyObjects;
FCreatureList.Free;
end;
continues
procedure TCityBoard.DestroyObjects;
begin
if FDDSForeWall <> nil then FDDSForeWall.Release;
if FDDSMidWall <> nil then FDDSMidWall.Release;
inherited DestroyObjects;
end;
function TCityBoard.HandlePanel;
var
R: TRect;
begin
case mt.Direction of
North: R := Rect(575, 480, 640, 545);
South: R := Rect(575, 545, 640, 610);
East: R := Rect(575, 610, 640, 675);
West: R := Rect(575, 675, 640, 740);
end;
BackSurface.BltFast(23, 401, WorkSurface, R, DDBLTFAST_SRCCOLORKEY);
Result := True;
end;
continues
procedure TCityBoard.DrawScene;
var
i: Integer;
begin
for i := 14 downto 1 do begin
with mt.CurrentView[i] do
if Value <> - 1 then begin
{ DrawPlace(i, Value); }
end;
end;
end;
continues
end.
Listing 33.4. The MZTown unit defines the maze and its characteristics. It does the
grunt work for working with a maze.
unit mztown;
{ 10 1 2 3
1 4 7 11 10 11 12 13 14 4 5 6
2 5 8 12 7 8 9 7 8 9
3 6 9 13 4 5 6 10 11 12 13 14
14 1 2 3
}
interface
uses
Windows;
const
XStreets = 12;
YStreets = 10;
type
TDirection = (North, East, South, West);
TCityPlace = record
X, Y: Integer;
Value: Integer;
end;
TMazeTown = class
private
FDirection: TDirection;
FXSpot, FYSpot: Integer;
FCurrentView: TCityPlaceAry;
FViewChanged: Boolean;
function CheckForwardMove: Boolean;
procedure FillOutCurrentView;
function GetNewDir(K: ShortInt): TDirection;
public
constructor Create(X, Y: Integer; Dir: TDirection);
function GetWallSegment(X, Y: Integer): Integer;
procedure Move(Direction: TDirection);
procedure Key(vKey: Word);
property CurrentView: TCityPlaceAry read FCurrentView write FCurrentView;
property Direction: TDirection read FDirection write FDirection;
property ViewChanged: Boolean read FViewChanged;
end;
implementation
const
FTownAry: TTownAry =
((1,1,1,1,1,1,1,1,1,1),
(1,0,1,0,0,0,0,0,0,1),
(1,0,0,0,1,1,0,1,1,1),
(1,0,0,1,1,0,0,0,0,1),
(1,1,1,1,1,1,1,0,1,1),
(1,0,0,0,0,1,0,0,0,1),
(1,1,1,1,0,1,0,1,0,1),
(1,0,0,0,0,0,0,0,0,1),
(1,1,1,1,1,1,1,1,0,1),
(1,0,0,1,0,1,0,0,0,1),
(1,1,0,0,0,0,0,1,0,1),
(1,1,1,1,1,1,1,1,1,1)
);
continues
procedure TMazeTown.FillOutCurrentView;
begin
case FDirection of
North: begin
FCurrentView[1] := GetCityPlace(FXSpot - 1, FYSpot);
FCurrentView[2] := GetCityPlace(FXSpot, FYSpot);
FCurrentView[3] := GetCityPlace(FXSpot + 1, FYSpot);
FCurrentView[4] := GetCityPlace(FXSpot - 1, FYSpot - 1);
FCurrentView[5] := GetCityPlace(FXSpot, FYSpot - 1);
FCurrentView[6] := GetCityPlace(FXSpot + 1, FYSpot - 1);
FCurrentView[7] := GetCityPlace(FXSpot - 1, FYSpot - 2);
FCurrentView[8] := GetCityPlace(FXSpot, FYSpot - 2);
FCurrentView[9] := GetCityPlace(FXSpot + 1, FYSpot - 2);
FCurrentView[10] := GetCityPlace(FXSpot - 2, FYSpot - 3);
FCurrentView[11] := GetCityPlace(FXSpot - 1, FYSpot - 3);
FCurrentView[12] := GetCityPlace(FXSpot, FYSpot - 3);
FCurrentView[13] := GetCityPlace(FXSpot + 1, FYSpot - 3);
FCurrentView[14] := GetCityPlace(FXSpot + 2, FYSpot - 3);
end;
East: begin
FCurrentView[1] := GetCityPlace(FXSpot, FYSpot - 1);
FCurrentView[2] := GetCityPlace(FXSpot, FYSpot);
FCurrentView[3] := GetCityPlace(FXSpot, FYSpot + 1);
FCurrentView[4] := GetCityPlace(FXSpot + 1, FYSpot - 1);
FCurrentView[5] := GetCityPlace(FXSpot + 1, FYSpot);
FCurrentView[6] := GetCityPlace(FXSpot + 1, FYSpot + 1);
FCurrentView[7] := GetCityPlace(FXSpot + 2, FYSpot - 1);
FCurrentView[8] := GetCityPlace(FXSpot + 2, FYSpot);
FCurrentView[9] := GetCityPlace(FXSpot + 2, FYSpot + 1);
FCurrentView[10] := GetCityPlace(FXSpot + 3, FYSpot - 2);
FCurrentView[11] := GetCityPlace(FXSpot + 3, FYSpot - 1);
FCurrentView[12] := GetCityPlace(FXSpot + 3, FYSpot);
FCurrentView[13] := GetCityPlace(FXSpot + 3, FYSpot + 1);
FCurrentView[14] := GetCityPlace(FXSpot + 3, FYSpot + 2);
end;
{$ifDef LookingFromAbove}
South: begin
FCurrentView[1] := GetCityPlace(FXSpot - 1, FYSpot);
FCurrentView[2] := GetCityPlace(FXSpot, FYSpot);
FCurrentView[3] := GetCityPlace(FXSpot + 1, FYSpot);
FCurrentView[4] := GetCityPlace(FXSpot - 1, FYSpot + 1);
FCurrentView[5] := GetCityPlace(FXSpot, FYSpot + 1);
FCurrentView[6] := GetCityPlace(FXSpot + 1, FYSpot + 1);
FCurrentView[7] := GetCityPlace(FXSpot - 1, FYSpot + 2);
FCurrentView[8] := GetCityPlace(FXSpot, FYSpot + 2);
FCurrentView[9] := GetCityPlace(FXSpot + 1, FYSpot + 2);
FCurrentView[10] := GetCityPlace(FXSpot - 2, FYSpot + 3);
FCurrentView[11] := GetCityPlace(FXSpot - 1, FYSpot + 3);
FCurrentView[12] := GetCityPlace(FXSpot, FYSpot + 3);
FCurrentView[13] := GetCityPlace(FXSpot + 1, FYSpot + 3);
FCurrentView[14] := GetCityPlace(FXSpot + 2, FYSpot + 3);
end;
West: begin
FCurrentView[1] := GetCityPlace(FXSpot, FYSpot - 1);
FCurrentView[2] := GetCityPlace(FXSpot, FYSpot);
FCurrentView[3] := GetCityPlace(FXSpot, FYSpot + 1);
FCurrentView[4] := GetCityPlace(FXSpot - 1, FYSpot - 1);
FCurrentView[5] := GetCityPlace(FXSpot - 1, FYSpot);
FCurrentView[6] := GetCityPlace(FXSpot - 1, FYSpot + 1);
FCurrentView[7] := GetCityPlace(FXSpot - 2, FYSpot - 1);
FCurrentView[8] := GetCityPlace(FXSpot - 2, FYSpot);
FCurrentView[9] := GetCityPlace(FXSpot - 2, FYSpot + 1);
FCurrentView[10] := GetCityPlace(FXSpot - 3, FYSpot - 2);
FCurrentView[11] := GetCityPlace(FXSpot - 3, FYSpot - 1);
FCurrentView[12] := GetCityPlace(FXSpot - 3, FYSpot);
FCurrentView[13] := GetCityPlace(FXSpot - 3, FYSpot + 1);
FCurrentView[14] := GetCityPlace(FXSpot - 3, FYSpot + 2);
end;
{$else}
South: begin
FCurrentView[1] := GetCityPlace(FXSpot + 1, FYSpot);
FCurrentView[2] := GetCityPlace(FXSpot, FYSpot);
FCurrentView[3] := GetCityPlace(FXSpot - 1, FYSpot);
FCurrentView[4] := GetCityPlace(FXSpot + 1, FYSpot + 1);
FCurrentView[5] := GetCityPlace(FXSpot, FYSpot + 1);
FCurrentView[6] := GetCityPlace(FXSpot - 1, FYSpot + 1);
FCurrentView[7] := GetCityPlace(FXSpot + 1, FYSpot + 2);
FCurrentView[8] := GetCityPlace(FXSpot, FYSpot + 2);
FCurrentView[9] := GetCityPlace(FXSpot - 1, FYSpot + 2);
FCurrentView[10] := GetCityPlace(FXSpot + 2, FYSpot + 3);
FCurrentView[11] := GetCityPlace(FXSpot + 1, FYSpot + 3);
FCurrentView[12] := GetCityPlace(FXSpot, FYSpot + 3);
FCurrentView[13] := GetCityPlace(FXSpot - 1, FYSpot + 3);
FCurrentView[14] := GetCityPlace(FXSpot - 2, FYSpot + 3);
end;
West: begin
FCurrentView[1] := GetCityPlace(FXSpot, FYSpot + 1);
FCurrentView[2] := GetCityPlace(FXSpot, FYSpot);
FCurrentView[3] := GetCityPlace(FXSpot, FYSpot - 1);
FCurrentView[4] := GetCityPlace(FXSpot - 1, FYSpot + 1);
continues
begin
case vKey of
vk_Up: NewDir := FDirection;
vk_Left: NewDir := GetNewDir(-1);
vk_Right: NewDir := GetNewDir(1);
vk_Down: NewDir := GetNewDir(2);
else
NewDir := FDirection;
end;
Move(NewDir);
end;
North: begin
if FDirection = North then
CheckForwardMove
else begin
FDirection := North;
FViewChanged := True;
end;
end;
West: begin
if FDirection = West then
CheckForwardMove
else begin
FDirection := West;
FViewChanged := True;
end;
end;
East: begin
if FDirection = East then
CheckForwardMove
else begin
FDirection := East;
FViewChanged := True;
end;
end;
South: begin
if FDirection = South then
CheckForwardMove
else begin
FDirection := South;
FViewChanged := True;
end;
end;
end;
if ViewChanged then
FillOutCurrentView;
end;
end.
Listing 33.5. The Entity program helps the TWorldView object display an
overhead view of the world.
unit Entity;
interface
uses
Classes, dbList, SysUtils,
Windows, Blini;
const
MaxX = 255;
MaxY = 252;
ftSage = 100;
ftSnake = 101;
ftMonastery = 102;
const
MaxCreatures = 3;
CAry: array[0..(MaxCreatures - 1)] of string =
(‘Sage’, ‘Snake’, ‘Monastery’);
type
TMapType = (mtGrass, mtRoad, mtSwamp,
mtWater, mtFootHill, mtMountain,
mtWater2);
{ Its the job of this unit to make the user see it as Col, Row. So
Even though its stored backwards in the disk file, this unit flips
things around so that its users think its stored Col, Row. }
TMap = array[1..MaxY,1..MaxX] of Byte;
TCreature = class;
TCreatureList = class(TList)
private
ScreenFile: string;
CreatureFile: string;
FMap: TMap;
private
function GetMapItem(Col, Row: Integer): Byte;
procedure SetMapItem(Col, Row: Integer; Val: Byte);
public
constructor Create; virtual;
destructor Destroy; override;
function Load: Boolean;
function Store: Boolean;
procedure NewCreature(ID, Name: string;
AType, ACol, ARow, AChap, AQuest: Word);
{ procedure UpDate(Index: Integer; ID, Name: string;
AType, ACol, ARow, AChap, AQuest: Word); }
procedure FreeAll;
procedure FindCreatures;
function GetColor(i: Byte): TColorRef;
end;
TCreature = class
private
FID: string;
FName: string;
FKind: Word;
FCol, FRow: Word;
FChapter, FQuestion: Word;
function GetType: string;
public
procedure Store(Writer: TWriter);
procedure Load(Reader: TReader);
property ID: string read FID write FID;
property Name: string read FName write FName;
property TypeStr: string read GetType;
property Kind: Word read FKind write FKind;
property Col: Word read FCol write FCol;
property Row: Word read FRow write FRow;
property Question: Word read FQuestion write FQuestion;
property Chapter: Word read FChapter write FChapter;
end;
implementation
uses
Binary, UtilBox, Dialogs;
constructor TCreatureList.Create;
begin
inherited Create;
ScreenFile := PathToExe + ‘SCREEN.DTA’;
CreatureFile := PathToExe + ‘CLIST.DTA’;
FindCreatures;
end;
destructor TCreatureList.Destroy;
begin
inherited Destroy;
end;
continues
procedure TCreatureList.FreeAll;
var
i: Integer;
begin
First;
for i := 0 to Count - 1 do
TCreature(Items[i]).Free;
Clear;
end;
C.Chapter := AChap;
Add(C);
end;
{
procedure TCreatureList.UpDate(Index: Integer; ID, Name: string;
AType, ACol, ARow, AChap, AQuest: Word);
var
C: TCreature;
begin
C := Items[Index];
C.ID := ID;
C.Name := Name;
C.Kind := AType;
C.Col := ACol;
C.Row := ARow;
C.Chapter := AChap;
C.Question := AQuest;
end;
}
procedure TCreatureList.FindCreatures;
var
i: Integer;
C: TCreature;
Temp: Byte;
F: file of TMap;
begin
System.Assign(F, ScreenFile);
try
System.Reset(F);
except
on E:EInOutError do begin
E.Message := E.Message + ‘: ‘ + Screenfile;
raise
end;
end;
System.Read(F, FMap);
System.Close(F);
Load;
for i := 0 to Count - 1 do begin
C := Items[i];
Temp := FMap[C.Row, C.Col];
SetBit(7, 1, Temp);
FMap[C.Row, C.Col] := Temp;
end;
end;
continues
end.
Listing 33.6. The SMALLM1.PAS unit defines a high-level view of the world map.
unit smallmp1;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, ExtCtrls;
type
TWorldMap = class(TForm)
Image1: TImage;
private
procedure CreateParams(var Params: TCreateParams); override;
end;
var
WorldMap: TWorldMap;
implementation
{$R *.DFM}
end.
Listing 33.7. The Status dialog would allow the user to see the status of the
characters in the game.
unit status;
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Buttons, ExtCtrls, Gauges;
type
TStatusDlg = class(TForm)
Bevel1: TBevel;
BitBtn1: TBitBtn;
Gauge1: TGauge;
private
procedure CreateParams(var Params: TCreateParams); override;
public
procedure ShowModalPaint;
end;
var
StatusDlg: TStatusDlg;
implementation
{$R *.DFM}
procedure TStatusDlg.ShowModalPaint;
begin
ShowModal;
end;
end.
Listing 33.8. The DLL that contains the bitmaps used by XFire program.
library BitDll;
{
This library consists mostly of a huge Resource
that contains all the bitmaps for the XFire or
related programs.
{$R FIRES.RES}
begin
end.
When this program starts, you are presented with a small window with a menu on top. If you
choose the Start Game menu option, you will get an overhead view of the scrolling world. You
can move through this world by pressing the arrow keys.
If you select the F10 key, the menu will appear, allowing you to close the application or access
certain of its features. You can use the menu to shift between the world view and the maze
view. You can also pop up dialogs on top of the maze view or world view. The access to menus
and dialogs shows how you can use standard Delphi features to enhance a Direct Draw
program.
There is obviously more code here than could possibly be examined in one single chapter. So
rather than stepping through the code line-by-line in my usual fashion, I will instead try to
show you the structure of the program, and to focus on a few technical highlights.
An Overview of XFire
Below you will find a list of the main units used by the program, along with their classes, and
a brief description of their purpose:
DPR File
XFire
Creates Form1.
UNITS
Main
Form: Form1
GameBrd1 - GameBoard
Form: None
Classes: TWorldBoard, TCityBoard, TTestBoard
Handles the drawing tasks for both the over head view and the maze view.
DrawBrd1 - DrawBoard
Classes: TDrawDirect
Contains a class from which TWorldBoard and TCityBoard descend
Entity
Form none
Classess: TCreatureList, TCreature
Helper classes used by TWorldBoard when it draws an overhead view of the world.
MzTown - MazeTown
Classes: TMazeTown
Description: Helper calls that runs the city maze displayed by TCityBoard
Smallmp1 - SmallMap
Form: WorldMap
Classess: TWorldMap
Description: Show a small map of the entire world
Status:
Form: StatusDlg
Classess: TStatusDlg
Could display the status of the game and its characters or present other
information.
HELPER UNITS
BLIini
Binary
DBList
OleBox
MathBox
PalBox
StrBox
OBJECTS
TObject
TDrawDirect
TWorldBoard
TCityBoard
TTestBoard
IUnknown
IDirectDraw
IDirectDrawPalette
IDirectDrawClipper
IDirectDrawSurface
TList
TCreatureList
TObject
TCreature
RESOURCE FILES
XFire.Res
Fires.Res
Back BITMAP “PANEL.BMP”
City BITMAP “CITYPAN.BMP”
ForeWall BITMAP “FOREWALL.BMP”
MidWall BITMAP “MIDWALL.BMP”
FrontWall Bitmap “FRNTWALL.BMP”
You can see that this object has methods for creating and initializing Direct Draw, as well as
properties representing all the key fields in a Direct Draw program. All of these fields will be
initialized for you automatically. In particular, notice the DirectDraw, FrontSurface, BackSurface,
WorkSurface , and Palette properties. Here are the main Direct Draw objects ready for you to
use as you wish.
NOTE
Mike Scott and I did not work together on our code, so there are some slight differ-
ences in naming conventions between this chapter and the last. What I call the
FrontSurface Mike calls a PrimarySurface, and what I call the BackSurface Mike calls a
BackBuffer. My WorkSurface is a place for you to draw into as you wish. This surface,
or some portion thereof, can then be blitted into the BackSurface. The final step is to
flip the BackSurface up to the front so that the user can see it.
This method is the rough equivalent of the FormShow methods that Mike showed you in the
previous chapter. It initializes Direct Draw, sets up the screen, creates front, back, and work
drawing surfaces, and sets up the palette.
Most of the key code found in the InitObjects method is split off into separate methods such
as CreateFront:
function TDrawDirect.CreateFront: Boolean;
var
SurfaceDesc: TDDSurfaceDesc;
hr: HResult;
begin
Result := False;
FillChar(SurfaceDesc, sizeOf(TDDSurfaceDesc), 0);
SurfaceDesc.dwSize := sizeof(TDDSurfaceDesc);
SurfaceDesc.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
SurfaceDesc.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or
DDSCAPS_FLIP or
DDSCAPS_COMPLEX;
SurfaceDesc.dwBackBufferCount := 1;
hr := FDirectDraw.CreateSurface(SurfaceDesc, FDDSFront, nil);
if hr <> DD_OK then
ErrorEvent(hr, ‘CreateFront’)
else
Result := True;
end;
In short, I perform all the same steps that Mike showed you, and in the same order, but I prefer
to handle each chore in its own routine.
After you initialize the object, you will usually start a timer, which will call the DoOneFlip method
to flip the background surface to the front:
procedure TDrawDirect.DoOneFlip;
begin
try
Each time DoOneFlip is called, it ensures that a method called BackGroundBlits is called:
function TDrawDirect.BackGroundBlits: Boolean;
var
R: TRect;
hr: HResult;
begin
R := Rect(0, 0, 639, 480);
hr := BackSurface.BltFast(0, 0, WorkSurface, R,
DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY);
Result := Succeeded(hr);
if not Result then
ErrorEvent(hr, ‘BackGround’);
end;
This method provides you with your opportunity to draw to the back surface before it gets
flipped up front.
BackGroundBlits is declared as virtual; in most cases you will override this method and per-
form your own special blitting operations. Had I been designing components, I might have
made this an event rather than a method, but those kinds of changes could be made easily enough
in your own code.
This code first calls the inherited InitObjects, which ensures that Direct Draw is set up cor-
rectly, and that the front, back, and work surfaces are in place. The code then calls a method of
TDrawDirect that creates a new IDirectDrawSurface object. In particular, the object to be cre-
ated is called FrontWall, and the name of the bitmap to be displayed on the surface is FrontWall.
You can see that TDrawDirect makes it easy for you to work with Direct Draw. Instead of
writing some 75 lines of ugly code, you can just call the inherited InitObjects . The
CreateDDSurface method helps you perform a common task with a minimum of fuss.
This method calls the inherited BackGroundBlits and then goes on to perform some additional
processing. In particular, it manipulates a variable called FTimerOdd that flips back and forth
between True and False each time DoOneFlip is called. In short, its value is usually swapped
when a timer message is sent to the main window of the program. If FTimerOdd is True, then
one image is blitted to the screen. If it is False, a slightly different image is blitted to the screen.
Taken together, the calls to InitObjects and BackGroundBlits represent the minimum amount
of code that you need to write to do anything useful with the TDrawDirect object. Clearly, it
is simpler to use TDrawDirect and these two methods than it is to write a Direct Draw pro-
gram from scratch.
NOTE
You should note, however, that Mike Scott is creating, or has created, a more elaborate
system for working with Direct Draw. If you think Direct Draw is going to play a big
part in your life, you should consider getting his product, rather than working with the
simple objects I show here.
Creating a Maze
The maze shown with the TCityBoard object is defined internally as a simple two-dimensional
array found in MZTOWN.PAS:
FTownAry: TTownAry =
((1,1,1,1,1,1,1,1,1,1),
(1,0,1,0,0,0,0,0,0,1),
(1,0,0,0,1,1,0,1,1,1),
(1,0,0,1,1,0,0,0,0,1),
(1,1,1,1,1,1,1,0,1,1),
(1,0,0,0,0,1,0,0,0,1),
(1,1,1,1,0,1,0,1,0,1),
(1,0,0,0,0,0,0,0,0,1),
(1,1,1,1,1,1,1,1,0,1),
(1,0,0,1,0,1,0,0,0,1),
(1,1,0,0,0,0,0,1,0,1),
(1,1,1,1,1,1,1,1,1,1)
);
The 1s in this array represent the wall, and the 0s represent the open watery spaces in the maze.
When you first enter the maze, you are located in row 3, column 2. Your goal, if there is one,
might be to reach row 10, column 2. You should perhaps spend a few moments wandering
through the maze, seeing if you can compare the places you go to the zeroes and ones shown
above. You can change the shape of the maze by altering the patterns of ones and zeros shown
above.
Wherever you are standing in the maze, you can see only the area directly in front of you. In
particular, you can see part of the spaces immediately to your right and left, plus an additional
11 spaces out in front of you. When I say the word space, I am referring to a single element in
the TTownAry shown above.
Here is a simple depiction of spaces you can see from any one particular spot:
10
1 4 7 11
2 5 8 12
3 6 9 13
14
The user is always located on spot 2 in this scenario. The places to her immediate left or right
are 1 and 3. The other visible areas appear directly in front of the person running the maze.
If the person were to turn to the right, the following array of numbers would represent her
world view:
1 2 3
4 5 6
7 8 9
10 11 12 13 14
In this case the maze runner is looking down the page toward number 5.
The key point to grasp here is that if position 5 is 0 in the TTownAry, the user will be able to
see through to spot 8. If 8 is also a 0, the user can see to spot 12. If 12 is a 1, there will be a piece
of the wall in that position.
Your goal as an artist is to draw shapes that fit each of the possibilities. The background should
show the way things would look if all the visible areas are open. The next step is to draw pieces
of the wall that fit in each of the numbered locations. If you look at the bitmaps that come in
the directory for this application, you will find a series of 13 bitmaps, one depicting a wall in
each of the possible locations shown above. The 14th location, of course, is occupied by the
user.
To help you visualize the way this system works, I created a program called TestCoords that
shows the view from above the maze, as shown in Figure 33.5. As you can tell, this is just a
representation of a maze and does not give you the full three-dimensional sense that you get
from the XFire program. For instance, the visible walls in this program are shown as 1s and the
visible open spaces are shown as 0s. Anything outside the range of vision is simply blank.
FIGURE 33.5.
The view from above the
maze, with the visible
elements of the maze
depicted as numbers.
The TestCoords program (see Listing 33.10) is designed so that you cannot walk through walls
nor see through walls. Therefore, it gives a sense of what the user experiences while moving
through the maze. When you work with this program, you should have a picture of the
TTownAry next to you, or you should use the visual representation of the array shown in the
program. With its aid you should be able to navigate the maze. The point of this exercise, once
again, is to help you visualize how the maze used by the TCityBoard object is put together.
interface
uses
SysUtils, Windows, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
Grids, mzTown;
type
TForm1 = class(TForm)
sg: TStringGrid;
StringGrid1: TStringGrid;
procedure bNorthClick(Sender: TObject);
procedure bWestClick(Sender: TObject);
procedure bEastClick(Sender: TObject);
procedure bSouthClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
mt: TMazeTown;
procedure DrawAll;
procedure DrawGrid;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.DrawAll;
var
i, j: Integer;
begin
for j := 0 to YStreets + 1 do
for i := 0 to XStreets do
SG.Cells[i, j] := ‘’;
for i := 1 to 14 do
with mt.CurrentView[i] do
if Value <> - 1 then
SG.Cells[X, Y] := IntToStr(Value);
end;
procedure TForm1.DrawGrid;
var
i, j: Integer;
begin
for j := 0 to YStreets - 1 do
for i := 0 to XStreets - 1 do
StringGrid1.Cells[j, i] := IntToStr(mt.GetWallSegment(i, j));
end;
end.
The TMazeTown unit shown previously provides the methods necessary to aid the TCityBoard
object when it draws the maze. In particular, all the calculations regarding the current location
of the user, and her current view, are all performed inside TMazeTown. TCityBoard can then
leave the navigation of the maze up to TMazeTown, and can instead concentrate on drawing
the proper wall segments to the screen as necessary.
If you are interested in pursuing this subject further, you might want to see the book called
Dungeons of Discovery, written by Clayton Walnum and published by Sams. It is written in C
and uses WinG rather than the Games SDK, but it provides a more detailed explanation of the
type of maze shown in the XFire program.
type
TMap = array[1..MaxY,1..MaxX] of Byte;
Rather than having just 1s and 0s in the array, TMap has up to 7 different values, defined by the
following enumerated type:
TMapType = (mtGrass, mtRoad, mtSwamp,
mtWater, mtFootHill, mtMountain,
mtWater2);
In this array, the number 0 represents not an open watery place, but a grassy place. The num-
ber 1 represents a road, 2 is a swamp, 3 is water, 4 a foothill, 5 a mountain, and 6 an alternate
shade of water. By using a variable like the FTimerOdd Boolean value shown above, you can
switch back and forth between the two kinds of waters on each wm_Timer message, thereby at
least theoretically creating the illusion of waves.
The TWorldMap object blits a series of tiny bitmaps to the screen. The actual bitmaps shown
are dependent upon the values in the TMap array. For instance, if TMap says a particular location
is grassy, TWorldMap blits a small chunk of grassy looking bitmap to the screen. By iterating
through the array one element at a time, the code is able to blit a series of small watery, grassy
or mountainous bitmaps to the screen, thereby creating a map like view of a fictitious country.
The bitmaps sent to the screen are stored inside large bitmaps found in the in a DLL called
BITDLL.DLL. This DLL has a large resource file, called FIRE.RES. Notice that FIRE.RES
has a number of bitmaps in it, and is over a megabyte in size. This large resources is the expla-
nation for the large size of the BitDll. If you want to avoid working with such a large
executable, you should store your bitmaps in a DLL, and load them into the executable only
when they are needed, as shown in the XFire program. Or, if you prefer, you can store the
Bitmaps in a RES file linked into the main program. It depends on where you want to keep all
those large bitmaps.
Whenever the TWorldBoard object needs to know the value of a particular X and Y location
on the map, it calls the ReadMapItem method from the TCreatureList object in the Entity unit:
function TCreatureList.ReadMapItem(Col, Row: Integer): Byte;
begin
Result := FMap[Row, Col];
end;
This code is used to hide the TMap array from the outside world. As explained earlier, you should
never allow one object to directly access the data of anther object. One of the reasons for doing
this is shown here. The TMap array stores X and Y data in the opposite order from the one used
by the TWorldBoard object. ReadMapItem therefore switches the value of the row and column
information it stores. The act of reversing these values, however, is hidden from the consumers
of this object. This will allow me to continue to present a consistent interface to the world,
even if I later change the way I store this information.
If necessary, the TCreateList object can also convert the numerical values found at a particular
location in the TMap array into a string which is meaningful to programmers:
function TCreatureList.GetMapType(Col, Row: Integer): string;
{ GrassType, RoadType, SwampType,
WaterType, FootHillType, MountainType }
var
S: string;
AType: Integer;
begin
AType := ReadMapItem(Col + 1, Row + 1);
if BitOn(7, AType) then
AType := 50;
case AType of
0: S := ‘Grass’;
1: S := ‘Road’;
2: S := ‘Swamp’;
3: S := ‘Water’;
4: S := ‘FootHill’;
5: S := ‘Mountain’;
50: S := CordToType(Col, Row);
end;
Result := S;
end;
Notice that a bit operation is performed on the values stored in the array. The first 7 bits of this
value is used to depict the landscape, stating specifically whether it is grassy, mountainous, and
so on. The eighth bit, however, is turned on if some kind of creature is living at a particular
location on the map.
The actual type of creature is stored in a file called CLIST.DTA. You can look up information
in CLIST.DTA by the coordinates of the creatures location in the TMap array. It would be dif-
ficult to store the actual record defining the creature in the TMap array because there is a wide
range of information associated with these beasts. Some of that information, such as a name,
could not be easily encoded using bit operations. As a result, the TCreature object in the En-
tity unit is used for tracking the details associated with a particular animal or person that you
might find while wandering through the maze.
The code for creating CLIST.DTA is stored in the Entity unit. It’s a bit more difficult to cre-
ate the huge array stored in SCREEN.DTA. I did, however, write a program for creating such
an array, and provide it on disc as MAKEWRLD.EXE, stored in the MAKEWRLD directory.
That is all I’m going to say about the XFire program. Clearly the explanation provided here is
not complete. However, it should give you enough information to allow you to begin moving
through the code of the program and finding out how specific parts of it work.
Summary
This chapter covered COM objects, fast graphics, and artificial environments. It’s a fairly meaty
chapter that would have been almost impossible to attempt in a book of this sort before the
advent of several recent programming trends. With the advent of Delphi 2.0, the MCI inter-
face, the Games SDK, and 3DR, it is now possible to accomplish a great deal with only a mini-
mum of effort.
The degree to which computers simulate real world objects varies. For instance, the Notepad
program has only a passing similarity to a real world notepad. In fact, one might say that it
only makes use of a real world notepad as a metaphor, and does little else with the idea. The
CALC.EXE program, on the other hand, looks and feels very much like a real world calcu-
lator.
Between these two extremes lies a whole world of possibilities. Some of the most interesting
simulations performed by computers come from the game world. Programs such as SimCity,
Heretic, or King’s Quest VI allow us to enter an artificial environment that bares some faint
resemblance to the world in which we actually live. The act of creating an artificial world or
artificial environment is probably one of the most powerful tasks of which a computer is ca-
pable. It is also one of the most interesting for it weds the binary logic of a computer to the
tidal swings of our imagination.
Until quite recently, the act of creating one of these artificial worlds required enormous effort
on the part of skilled programmers. Now, however, we have programming environments, such
as Windows, and programming tools, such as Delphi, which can be combined with high per-
formance processors to give relatively easy access to the creation of artificial worlds.
Delphi Multimedia
Programs 34
You can easily add multimedia features, such as sounds, movies, and music, to Delphi pro-
grams. This chapter shows how to achieve these ends using the multimedia control that is built
into the Delphi environment.
When reading this chapter, you can expect to encounter five distinct sections. The first is a
general discussion of multimedia. The remaining four sections explain:
■ How to use the built-in Delphi multimedia control to play WAVE, MIDI, and AVI
files. This section of the chapter also discusses accessing CD-ROM files.
■ How to use Delphi to create presentation programs that utilize multimedia to explain
a topic in clear and easy-to-understand terms.
■ How to show an AVI file in a window on your form.
■ How to know when the user starts playing a file; when the user stops playing a file;
when a file reaches the end; when the user pauses a file; and, in general, how to track
runtime information about the state of the multimedia player.
This chapter contains some rather detailed explanations of a few fundamental programming
steps. I decided to include these programming basics because I wanted to be sure the relatively
entertaining material in this chapter was accessible to a wide range of readers. The only chal-
lenging code in this chapter is found at the very end, when I discuss some low-level details of
playing MIDI files.
As always, you can find the complete source code for all the programs on the Unleashed CD-
ROM. MIDI, WAVE, and AVI files also are available in the MULTIMED subdirectory. You
can use them to experiment with the sample code in this chapter.
Exploring Multimedia
Multimedia for Windows has developed over the last few years to the point where it is now
commonplace. However, it’s probably still worthwhile discussing the subject briefly in general
terms, just to be sure that there is a common definition of the concepts involved.
Multimedia is a generic term. It refers to almost any form of animation, sound, or movie that
is used on computers. Given this broad definition, it’s possible to say that this chapter deals
with a subset of multimedia that involves the following:
■ Showing movies using Microsoft’s Video for Windows files
■ Playing music using MIDI and WAVE files
■ Playing and recording sounds and voices using WAVE files
All of these formats can be handled by the Microsoft Multimedia Extensions to Windows, and
are encapsulated in the TMediaPlayer control found on the Additional page in the Compo-
nent Palette.
Every MIDI file is divided up into multiple tracks, and users generally record the sound one
track at a time, with only one instrument on each track. If you play five or six of these tracks at
the same time, the result can be the sound of an entire band. For instance, a typical MIDI file
might feature the sound of drums on one track, bass on a second track, piano on a third track,
and a horn on the fourth track. Depending on the hardware and software involved, you can
record between three and 15 instruments on any one file. You can record the instruments us-
ing a MIDI connection, or you can literally write out the notes one at a time, as if you were
composing sheet music. As a rule, MIDI records only instrumental notes, not vocal music. MIDI
doesn’t act like a tape recorder, capturing raw sound traveling through the air in waves. The
sound is entirely synthesized by the hardware. MIDI captures the notes, the type of instru-
ment to play the sound, and timing information; nothing more. It’s extremely mathematical,
and in that sense enables you to get very close to a pure musical form.
If you look at the same media from the point of view of the hardware, the formats appear in a
quite different light. For instance, you can play AVI files on any machine that comes equipped
with a functioning copy of Windows. The only requirements for playing AVI files are the
Microsoft Video for Windows drivers, which are distributed free from a number of different
sources. Decent reproduction of WAVE files, however, requires a sound card. Serious work
with MIDI requires both a sound card and the presence of an instrument attached to your
computer using a MIDI cable.
these are estimates, circa 1995, and by the time you read this any prices quoted here will al-
most certainly have dropped.
either/or proposition. We can have both hands-on teaching and multimedia computers. Clearly,
education can be enhanced through multimedia; indeed, this process is already well under way.
If multimedia is part of your job, or if its potential interests you, then the next step is to learn
how to manipulate the elements of multimedia programs. The remaining sections of this chapter
are dedicated to showing you ways to achieve this end that are both simple to implement and
powerful in their overall effect.
NOTE
Before proceeding further, you should make sure that you are able to play multimedia
files on your system. Use the MPLAYER.EXE tool that ships with Windows to test
your ability to play WAVE, MIDI, and AVI files. In Windows 95, this is not likely to
be a problem. As a result, I have cut out a section of this chapter included in the first
edition that discusses the drivers and applets used to run and test your multimedia
system. If you can’t play any of these files automatically, the issue is not a program-
ming problem, but an operating system set-up issue. You should get on MSN,
CompuServe, AOL, the Internet, or some other online service and see if you can find
out what is wrong. Windows 95 should be able to play all three types of files automati-
cally, so long as you have the hardware to support them.
FIGURE 34.1.
The TMediaPlayer
control rests in the middle
of a standard form.
After you have placed the control on a form, you will find that the Object Inspector contains
a FileName field, as shown in Figure 34.2. Fill in this field with the name of an AVI, MIDI, or
WAVE file. For instance, on my system I can write E:\AVI\CLOUDS.AVI to indicate the name of
a video file located on my CD-ROM. On some versions of Delphi, the ellipses icon (“...”) on
the right side of the property editor pops up a dialog box so that you can browse for files. In
other versions you have to enter the name manually. After choosing a valid filename, be sure
the AutoOpen property is set to True. This property ensures that Delphi automatically opens the
MULTIMEDIA services when you start your program.
FIGURE 34.2.
The TMediaPlayer
component as seen through
the Object Inspector.
After completing these simple steps, you’re ready to run the program. When it’s launched, click
the green button at the left of the TMediaPlayer to start the movie or sound file that you se-
lected. If you get an error, there are three probable sources of the problem:
■ You entered an invalid filename.
■ You don’t have multimedia set up correctly on your system. This means you either
don’t have the right hardware, or you have not installed the right drivers. Driver
configuration occurs in the Control Panel, and hardware requirements are specified in
detail in any good book on multimedia.
■ You have AutoOpen set to true, but have left the FileName property blank. This
produces the dreaded error message: “This command requires a parameter. Please
supply one.” What they mean to say, of course, is “The FileName property requires a
parameter. Please supply one.” As programmers, however, we love these kinds of
puzzles and are grateful for the fun challenges they provide. Right? Of course.
An sample program is available on disk as MEDIA1.DPR. By default, it plays THE MI-
CROSOFT SOUND.WAV, a file that ships with every copy of Windows 95. If you have deleted
this file, if you run Windows NT, or if you do not store this file in a subdirectory called
C: \WINDOWS\MEDIA, you must change the FileName property of this program.
If you experiment with the MEDIA1 program for a while, you find that you can play AVI,
MIDI, and WAVE files simply by specifying the name of a file. This flexibility is the result of
the control’s capability to parse the filename, detect the file type from the extension, and ap-
portion the act of actually playing the file to the appropriate internal routine.
When you run the program, you see a bar containing the universal symbols for playing a tape,
CD, or video device. Click once on the button to open the appropriate MCI device. Wait a
few moments for the buttons on the TMediaPlayer to change color, and then click once more
on the green arrow at the left of the control. The file you specified in the FileName section now
plays for you.
FIGURE 34.3.
The main form for the
Access program.
The code shown here is going to be executed every time the user presses the Select File button.
It has four purposes:
■ The first purpose is to close any MCI devices that might be currently open by calling
MediaPlayer1.Close . When you first open the program, this line won’t serve a useful
purpose, but after you have opened a file and wish to open a second one, you will find
that it helps clear the way. It is never an error to call Close on a TMediaPlayer.
■ The second purpose is to execute the OpenDialog. This allows users to roam at will
across the hard drive, network, and any available CD-ROMs. If they find a file they
wish to play, they can press the OK button to select it.
■ The third purpose only executes if the user selects a file. If the user chooses Cancel
from the Open Dialog, nothing happens. If the user picks OK, the code assigns the
filename selected by the user to the MCI control.
■ When an appropriate filename has been designated, the only step left is to open (or
reopen) the MCI device by calling MediaPlayer1.Open. Now the user is free to click
the green arrow to play the file.
You can make a few minor adjustments to this program to make it a bit more friendly for the
user. For instance, you can automatically allow the user to select files with only a particular
extension such as AVI, WAV, or MID. There are two different ways to achieve this end.
If you want to show all three types of files at once, enter the following string in the Filter
section of the Object Inspector:
Multimedia Files(*.avi;*wav;*.mid)|*.avi;*.wav;*.mid
Now the user can simultaneously select a WAVE, MIDI, or movie file.
Alternatively, you can change the TOpenDialog’s Filter property so that it contains the follow-
ing string:
AVI File(*.avi)|*.avi|WAVE File(*.wav)|*.wav| MIDI file(*.MID)|*.mid
To make it easier for the reader to parse this string, break it up into three sections:
AVI File(*.avi)|*.avi
WAVE File(*.wav)|*.wav
MIDI file(*.MID)|*.mid
Each of these sections contains one bar (|) symbol and is divided from the next by another bar
symbol. When the program is running and the Open Dialog is active, the first half of the string
(up to the bar) is shown in the List File Type section of the Open Dialog, and the second half
is shown in the FileName section. When shown this way, the user must select a different option
in the File Type drop-down combo box in order to see files with a different extension.
NOTE
Delphi includes a special property editor, available in the Object Inspector, that makes
it easier to input filters at design time. To use this dialog, simply enter the first half of
the previous string in the left portion of the dialog, and place the portion shown after
the bar in the right side of the dialog. Under most circumstances, you would use three
lines that would look like those in the previous code example to complete the chore.
If you want to assign values to the Filter field at runtime, you should use one of the
first two coding examples in this chapter. For instance, you could assign the following
string to the filter property at runtime:
Multimedia Files(*.avi;*wav;*.mid)|*.avi;*.wav;*.mid
Another field of OpenDialog that you might want to tweak is the InitialDir property. You can
use this to designate the drive or directory that you want users to see first when they open the
dialog. In my case, I prefer to enter the drive designating my CD-ROM:
g:
This allows the user to start the search for files on the drive most likely to contain a hit. Be-
cause I do not specify a particular directory, the user can select a particular directory and return
to it over and over until he or she has seen all the files of interest in that location.
The few simple steps outlined here are the only ones you must take in order to give a user full
access to all the media files on the system. Other additions you might consider making involve
setting the background color of the form to black, or having the form start maximized. A pro-
gram of this type is saved on disk as access.dpr, and the code for the program is shown below
in Listing 34.1.
Listing 34.1. The Access program allows the user to play a wide range of multi-
media files.
unit Main;
interface
continues
type
TForm1 = class(TForm)
Select: TButton;
OpenDialog1: TOpenDialog;
MediaPlayer1: TMediaPlayer;
procedure SelectClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
of file control by entirely circumventing the TMediaPlayer and dealing directly with a
set of low-level Windows commands found in the MMSYSTEM.PAS unit that ships
with Delphi. You also can use the MCI commands described in Chapter 35.
With these two caveats fresh in your mind, you are ready to learn about some of the more subtle
aspects of the TMediaPlayer.
You probably have noticed that the Events palette for the TMediaPlayer contains special capa-
bilities that you can add to your program. These functions come in two categories:
■ The first are the OnClick events that occur when the user presses part of the control.
For instance, the OnPlay event, replete with a parameter called Button, is sent to your
program whenever the user presses the green play arrow on the TMediaPlayer.
■ The second are the OnNotify events containing the mm_MciNotify messages that are
sent to your window when a file starts or stops playing, or when an error occurs.
I describe the first set of events in the next few paragraphs, and then move on to the second set.
There are eight major events generated by a direct click on the control, all of which are cap-
tured in the TMPBtnType:
■ btPlay occurs when the user presses the green play arrow.
■ btStop occurs when the user presses the square, red stop button.
■ btBack occurs when the user presses the blue “back one frame” button.
■ btStep occurs when the user presses the blue “forward one frame” button.
■ btNext occurs when the user presses the blue fast-forward button.
■ btPrev occurs when the user presses the blue rewind button.
■ btPause occurs when the user presses the yellow pause button.
■ btRecord occurs when the user presses the round, red record button.
If you want to see what it’s like to capture one of these events, first place four edit controls on
your form. Each of these edit controls will be used eventually, although they do not all play a
role in the first parts of the program I describe here. Create an OnClick event for the TMediaPlayer,
and fill it in so that it looks like this:
procedure TForm1.MediaPlayer1Click(Sender: TObject;
Button: TMpBtnType;
var DoDefault: Boolean);
begin
Edit1.Text := ‘Playing’;
end;
When you understand how this system works, it should take you only a moment to be able to
display an appropriate line of text whenever the user selects the TMediaPlayer. For instance,
here is a method that can be called whenever the user presses a media player button:
The information provided here, though useful, is not enough to enable you to keep track of
everything that occurs regarding the TMediaPlayer. For instance, to pause a file the user presses
the pause button, which sends a btPause message to the TMediaPlayer. To start playing again,
the user does not press the Play button but, instead, represses the pause button. In that case,
the code here receives a second btPause message, and no btPlay message. This can result in
confusion, and clearly additional information is needed to straighten the matter out. The next
step, then, is to find out how to get additional information, and to see some of the things you
can do with it.
To find out whether a piece has stopped playing, or whether an error has occurred, you can
take a look at OnNotify messages. When working directly with the operating system, these mes-
sages come in four flavors:
mci_Notify_Successful: Sent when a command is completed
mci_Notify_Superseded: Command aborted by another function
mci_Notify_Aborted : Current function is aborted
mci_Notify_Failure : An error occurred
Delphi does not send these messages to you directly, but instead converts them into constants
such as nvSuccessful, nvAborted, and so on. This system is adopted to provide consistency with
other portions of the Delphi environment.
It’s a bit tricky to understand exactly when each of these messages is sent by the Windows MCI
device driver. As a rule, you get these messages in these cases:
■ An nvSuccessful message comes when a piece finishes successfully.
■ An nvSuperseded message might come if the system has been paused, and the user
then presses the play button.
■ An nvAborted message might come if the user presses the stop button, or causes the
device to be closed.
Here’s a function that responds to OnNotify messages by displaying a short descriptive string in
an edit control:
Edit2.Text := S;
end;
This function is called automatically every time a significant event occurs that affects the MCI
device. You never explicitly call this function yourself. Instead, you just set it up and wait for
the function to be called by the system.
The heart of the MediaPlayer1Notify method is a case statement that hinges on the current
state of the NotifyValue field of a TMediaPlayer. For instance, if the NotifyValue field is set to
mci_Notify_Successful, a string specifying that fact is displayed in an edit control. Once again,
you don’t have to do anything to change the state of the NotifyValue field. It’s changed for you
automatically by the system. All you have to do is respond to an OnNotify event and then check
the NotifyValue field.
To be sure that a file has finished playing, you can wait for the reception of an
mci_Notify_Successful message, and then check to see the current mode.
The current mode of an MCI device is specified in the Mode property of a TMediaPlayer. Here
is a listing of the common values designated in this field:
mci_Mode_Not_Ready
mci_Mode_Stop
mci_Mode_Play
mci_Mode_Record
mci_Mode_Seek
mci_Mode_Pause
mci_Mode_Open
Each of these modes is self-explanatory. If the Mode field is set to mci_Mode_Stop, the device is
stopped. If it’s set to mci_Mode_Play, the device is playing. Once again, Delphi does not send
you these constants directly, but instead passes corresponding identifiers, such as mpStopped,
mpPlaying, and so on. I believe the developers substituted these constants because they were
easy to read, and also followed the style used by the rest of the Delphi objects.
If your application receives an mci_Notify_Successful message, and if the Mode field of the device
is set to mci_Mode_Stop, you can be certain that the current file has finished playing. That is
why the OnNotify method I showed you earlier checks for such a condition:
if (MediaPlayer1.NotifyValue = nvSuccessful) and
(MediaPlayer1.Mode = mpStopped) then
Edit1.Text := ‘File finished playing’;
The Access2 program also contains the following method, which can be used to track the cur-
rent state of the media player:
procedure TForm1.SetMode;
begin
case MediaPlayer1.Mode of
mpNotReady: Edit3.Text := ‘mci_Mode_Not_Ready’;
mpStopped: Edit3.Text := ‘mci_Mode_Stop’;
mpPlaying: Edit3.Text := ‘mci_Mode_Play’;
mpRecording: Edit3.Text := ‘mci_Mode_Record’;
mpSeeking: Edit3.Text := ‘mci_Mode_Seek’;
mpPaused: Edit3.Text := ‘mci_Mode_Pause’;
mpOpen: Edit3.Text := ‘mci_Mode_Open’;
else begin
Edit1.Text := ‘Device Inactive’;
Edit2.Text := ‘No special messages’;
Edit3.Text := ‘Unknown’;
Edit4.Text := ‘No file selected’;
end;
end;
end;
This routine gets called in response to an event generated by a TTimer that is placed on the
form. To create a timer, simply select it from the System page of the Component Palette, drop
it on a form, and create an OnTimer event handler. The method needs to do nothing more than
call the SetMode procedure. The Interval property of a TTimer component defines how often
OnTimer events are generated. If you leave Interval at the default 1000-millisecond level, SetMode
is called once every second, thereby ensuring that the user is kept reasonably well informed of
the current state of the TMediaPlayer.
The main form is shown in Figure 34.4.
FIGURE 34.4.
The Access2 program
displays the current state of
the media player in a series
of edit controls.
Listing 34.2. The Access2 program shows how to fine-tune the behavior of a
TMediaPlayer.
unit Main;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, SysUtils, Forms,
Controls, StdCtrls, MPlayer,
Dialogs, ExtCtrls;
type
TForm1 = class(TForm)
MediaPlayer1: TMediaPlayer;
BSelect: TButton;
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Edit4: TEdit;
OpenDialog1: TOpenDialog;
Timer1: TTimer;
procedure SetMode;
procedure MediaPlayer1Notify(Sender: TObject);
procedure BSelectClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure MediaPlayer1Click(Sender: TObject; Button: TMPBtnType;
var DoDefault: Boolean);
private
{ Private declarations }
Total: Integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
MMSystem;
{$R *.DFM}
procedure TForm1.SetMode;
begin
case MediaPlayer1.Mode of
mpNotReady: Edit3.Text := ‘mci_Mode_Not_Ready’;
mpStopped: Edit3.Text := ‘mci_Mode_Stop’;
mpPlaying: Edit3.Text := ‘mci_Mode_Play’;
mpRecording: Edit3.Text := ‘mci_Mode_Record’;
mpSeeking: Edit3.Text := ‘mci_Mode_Seek’;
mpPaused: Edit3.Text := ‘mci_Mode_Pause’;
mpOpen: Edit3.Text := ‘mci_Mode_Open’;
continues
end.
When you are examining the Access2 program, you should look for code that demonstrates
the following key points:
■ Whenever the user clicks on the media player, an event is automatically generated. For
instance, if the user clicks the green play arrow, a btPlay event is sent to your pro-
gram. To respond to this event, just create an OnClick method by clicking on the
appropriate part of the Object Inspector.
■ mm_MciNotify messages are sent to your application after key events occur in the life-
cycle of a multimedia file. For instance, if the file finishes playing, an
mci_Notify_Successful message is sent to your application. You can respond to this
event inside an OnNotify method.
■ You can query the system to find out the current state of the selected multimedia
device. To do this, simply see the value of the Mode property for the media player. For
instance, if MediaPlayer1.Mode = mpPlaying, you know that the device is playing. This
is part of one solution to the issue with the pause button discussed earlier in the
chapter.
The TMediaPlayer control completely protects you from the need to work with the Microsoft
MCI interface found in MMSYSTEM.PAS. However, if you want to become an expert in this
field, you should begin by studying that source file, and seeing how it is called from
MPLAYER.PAS. This subject is covered in Chapter 35.
To further customize the view, you can use the DisplayRect property to define the rectangle in
which you want the movie to play. For instance, if you want the movie to stretch to fit the
entire surface of the panel, you write the following:
An example of this technique is found in the Windavi program shown in Listing 34.3. The
form (shown in Figure 34.5) features two file-oriented listbox controls from the System page
of the Object Inspector.
FIGURE 34.5.
The Windavi program
shows members of the
Visual Basic development
team preparing to work on
some pressing optimization
issues.
Listing 34.3. The Windavi program enables the user to play a movie in a
predefined area.
unit main;
interface
uses
Windows, Messages, SysUtils,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
FileCtrl, ExtCtrls, MPlayer;
type
TForm1 = class(TForm)
FileListBox1: TFileListBox;
DirectoryListBox1: TDirectoryListBox;
DriveComboBox1: TDriveComboBox;
MediaPlayer1: TMediaPlayer;
Panel1: TPanel;
procedure FormCreate(Sender: TObject);
procedure FileListBox1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
This program is really a Windows AVI file viewer. It is custom made for that purpose, although
it will, in fact, play other kinds of multimedia files. The basic flow of the program enables you
to select AVI files from the directory tools on the left side of the form. When a valid file is
selected, the TMediaPlayer is automatically activated and the user can press the green play ar-
row.
Whenever the user chooses a file from FileListBox1, the following code is called:
procedure TForm1.FileListBox1Click(Sender: TObject);
begin
MediaPlayer1.FileName := FileListbox1.fileName;
MediaPlayer1.Open;
Mediaplayer1.Display := Panel1;
MediaPlayer1.DisplayRect :=
Rect(2, 2, Panel1.Width - 4, Panel1.Height - 4);
end;
The code assigns the proper filename, ensures that the media player is open, and finally dis-
plays output on the surface of Panel1. Notice that the program leaves the border around the
panel open so that the 3-D effect of the panel is preserved. You can, if you wish, add a call to
MediaPlayer1.Play at the end of this procedure, or you can let the user press the green play
button.
The rest of the code in the program ensures that the three listbox controls on the left side of the
form stay in sync. For instance, when you start the program, the following code is executed:
procedure TForm1.FormCreate(Sender: TObject);
begin
DirectoryListBox1.Directory := DriveComboBox1.Drive + ‘:\’;
end;
This just ensures that the directory listings start at the root of the current drive.
Note that in the Object Inspector the DirList property of the DriveComboBox is set to
DirectoryListBox1 , and the FileList property of DirectoryListBox1 is set to FileListBox1.
The mask property of FileListBox1 is set to *.avi;*.wav;*.mid. Here is an alternate FormCreate
method that performs all these actions for you in code:
A more sophisticated version of this program might ask the user to specify startup directory
information during setup, through the registration database or using an INI file.
Presentation Programs
From a programmer’s perspective, a presentation program written in Delphi does not have to
be a particularly demanding chore. The really complicated issues turn out not to be program-
ming related, but design related.
The Present program discussed next provides a multimedia QuickStart for the previous Ac-
cess2 program. Users of Access2 can run Present in order to learn how to use the Access2 pro-
gram. (This is not really necessary, of course, but it gave me something on which to focus the
presentation program.)
Present is written entirely in Delphi, and requires only a few very simple programming skills.
I chose the Access2 program as a topic because it’s easy to explain, and because it’s familiar to
all readers of this chapter. Clear, online explanation of how to run a program obviously is needed
in the computer industry. A more sophisticated version of the Present program would belong
to a family of multimedia applications for which there is an immediate and obvious market.
When running a presentation program, it’s probably best to set the MediaPlayer1’s Visible
property to False inside the Object Inspector. That way, the user never sees the control itself.
Of course, when you do this, you need to begin manipulating the control entirely through the
program’s code. In other words, you can’t expect the user to press the green arrow that starts
the sounds, nor can the user be responsible for closing one file and starting a second file. Every-
thing has to be controlled through code.
The basic steps you need to take to run the multimedia control from behind the scenes are
fourfold:
1. Assign a filename to the control.
2. Open the control.
3. Play the file.
4. Close the control.
The code required to perform these steps might look like this:
MediaPlayer1.FileName := Wave4;
MediaPlayer1.Open;
MediaPlayer1.Play;
MediaPlayer1.Close;
The trick here is to find the right way to arrange this code so that each method is called at the
right time. The Present program in Listing 34.4 shows one simple way to achieve this end.
The first step is to choose a few key screen shots from the Access2 program. In this particular
case, I’ve chosen three graphics:
Graphic One: A shot of the Access2 program’s main screen
Graphic Two: A shot of the OpenDialog
Graphic Three: A close-up shot of the TMediaPlayer
I’ve chosen these shots because I feel that a user will know how to run the Access2 program
when he or she understands what is on these screens and what they mean. After choosing the
graphics, I used a screen capture program and the Windows PaintBrush program to create three
BMPs: one for each shot.
When the graphics are secure, the next step is to create the text that accompanies each graphic.
This part of the job is a bit like a movie director’s work. For instance, I’ll map this out in a
series of four scenes. Each scene has a graphic associated with it, and a short paragraph of ex-
planation:
Scene One:
■ Visual: Graphic One
■ Text: This is the main screen for the Access2 program. This program is designed to
enable you to easily play a wide range of multimedia files. For instance, you can use
this tool to play movies, music, or sound.
The Access2 program uses the MCI interface to the Multimedia extensions to Win-
dows. MCI stands for Multimedia Control Interface. This is a midlevel programmer’s
interface, which allows relatively easy access to the commands you need to play
multimedia files.
Using the Access2 program is easy. To get started, click on the Select File button with
the mouse.
(End Text of Part I)
Scene Two:
■ Visual: Graphic Two
■ Text: After you press the Select File button, the OpenDialog appears. You can use this
dialog to select a WAVE file, MIDI file, or AVI file from your hard drive. To select a
file, simply click on its name with the mouse and select the OK button.
If you have the necessary hardware, you can then play any of these files via the MCI
control.
(End Text of Part II)
Scene Three:
■ Visual: Graphic One
■ Text: When the OpenDialog closes, you can then begin playing a file by pressing the
green Start button. The other buttons each have a specific purpose, which should be
intuitively obvious to most users.
(End Text of Part III)
Scene Four:
■ Visual: Graphic Three
■ Text: This is an MCI control. You can use it to play multimedia files. This control
works exactly like the similar button displays that you might see on a tape recorder or
CD player. There are nine separate buttons on this control.
When you want to begin playing a file, you can select the green arrow with the mouse.
To pause the file, select the yellow pause buttons. To stop the file, press the red stop
button.
The four blue buttons enable you to move backward and forward through the file.
The first two buttons enable you to move forward or backward to a particular frame.
The second two buttons move one frame forward or one frame backward through a
file.
The last two buttons enable you to record to a file or eject a CD from a CD drive.
(End Text of Part IV)
With this script to work from, you can use the TMediaPlayer to write a short program that
enables you to record your voice in a WAVE file. You should record each of the previous scenes
in a separate WAVE file, and save them to disk as MCINUM1.WAV, MCINUM2.WAV, and
so on. Copies of these files come with this book, but you can create your own if you wish.
With the graphics and the sound files in your hands, all that’s left to do is create the program.
Experienced programmers should be able to make short work of this part of the task.
To get started, create a main form and place two labels and two buttons in it. Use the labels to
write out the words Access Two and QuickStart. Name the first button BClose, and use it to
close the application:
procedure TForm1.BCloseClick(Sender: TObject);
begin
Close;
end;
Name the second button BStart. The response to this button is a few lines of code that drive
the actual Quick Start presentation:
procedure TForm1.BStartClick(Sender: TObject);
begin
FirstPic := TFirstPic.Create(Application);
FirstPic.FileName := Wave1;
FirstPic.ShowModal;
FirstPic.Free;
SecondPic := TSecondPic.Create(Application);
SecondPic.ShowModal;
SecondPic.Free;
FirstPic := TFirstPic.Create(Application);
FirstPic.FileName := Wave3;
FirstPic.ShowModal;
FirstPic.Free;
ThirdPic := TThirdPic.Create(Application);
ThirdPic.ShowModal;
ThirdPic.Free;
end;
To understand this code, you must first grasp the structure of the Present program. Present has
four separate forms, shown in Figures 34.6 through 34.9.
FIGURE 34.6.
The main screen of the
Present program has two
labels and two buttons.
FIGURE 34.7.
The second form has a
button on it labeled Next.
Above the button is a
TImage control.
FIGURE 34.8.
The third form also has a
button labeled Next. Above
the button is aTImage
control and a common
dialog box.
FIGURE 34.9.
The fourth form has a Next
button and a TImage
control with the close-up
shot of theTMediaPlayer.
Because each graphic is shown on a separate form, it’s easy to play the correct WAVE file at the
correct time. For instance, there are four methods associated with the form containing the screen
shot of the Access2 program. The first is called automatically by the system when the form is
opened:
procedure TFirstPic.FormActivate(Sender: TObject);
begin
MediaPlayer1.Visible := False;
MediaPlayer1.FileName := FileName;
MediaPlayer1.Open;
MediaPlayer1.Notify := True;
MediaPlayer1.Play;
ActivateCalled := True;
end;
This method is used to assign the name of the file to the TMediaPlayer, and then to open and
play the file. You can create the header for this file by double-clicking the space next to the
OnActivate event in the Object Inspector.
When the form is closed, an OnClose message is automatically sent by the system:
procedure TFirstPic.FormClose(Sender: TObject;
var CloseWindow: Boolean);
begin
MediaPlayer1.Close;
end;
To create a FormClose method, just click the space next to the OnClose event in the Object
Inspector. As you can see, this method does nothing more than close the MCI device.
There are two other methods inside this form. The first method automatically closes the form
when the associated WAVE file stops playing:
procedure TFirstPic.MediaPlayer1Notify(Sender: TObject);
begin
with MediaPlayer1 do
if ((NotifyValue = nvSuccessful) and
(Mode = mpStopped)) then FirstPic.Close;
end;
The logic for the if statement shown here is explained earlier in this chapter, in the section
called “A Closer Look at a Media Access Program.”
The other method included in the form responds to a click on the next button:
procedure TFirstPic.BNextClick(Sender: TObject);
begin
Close;
end;
Clearly, the only purpose of either of the last two methods is to close the form. The first method
closes the form when the WAVE file stops playing. The second method closes the form if the
user has grown impatient and has pressed the next button.
Returning now to the BStartClick method in the main form, you can see that after the first
form has been shown, the Present program automatically disposes the first form and calls the
next form:
FirstPic.ShowModal;
FirstPic.Free;
SecondPic := TSecondPic.Create(Application);
SecondPic.ShowModal;
SecondPic.Free;
Needless to say, the SecondPic form is structured exactly like the FirstPic form. It does the
following:
■ Responds to OnActivate by opening the MCI device and playing a WAVE file
■ Responds to OnClose messages by closing the MCI device
■ Responds to a click on the next button by closing the form
■ Responds to the notification of the end of the WAVE file by closing the form
You now should have all the information you need to grasp the structure of the Present pro-
gram. As you can see, it has a simple and robust architecture. The code for the program is shown
in Listings 34.4 through 34.8.
Listing 34.4. Here is the unit behind the main form for the Present program.
unit Main;
interface
uses
WinTypes, WinProcs,
Classes, Graphics, StdCtrls,
Controls, Printers, Forms;
type
TForm1 = class(TForm)
BStart: TButton;
BClose: TButton;
Label1: TLabel;
Label2: TLabel;
procedure BStartClick(Sender: TObject);
procedure BCloseClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
uses
Globals,
Pict1,
Pict2,
Pict3;
{$R *.DFM}
SecondPic := TSecondPic.Create(Application);
SecondPic.ShowModal;
SecondPic.Free;
FirstPic := TFirstPic.Create(Application);
FirstPic.FileName := Wave3;
FirstPic.ShowModal;
FirstPic.Free;
ThirdPic := TThirdPic.Create(Application);
ThirdPic.ShowModal;
ThirdPic.Free;
end;
end.
Listing 34.5. The PICT1 dialog, which is the first to be shown after the user
presses the Start button.
unit Pict1;
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
MPlayer, StdCtrls, ExtCtrls;
type
TFirstPic = class(TForm)
Next: TButton;
MediaPlayer1: TMediaPlayer;
Image1: TImage;
procedure NextClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
procedure MediaPlayer1Notify(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
ActivateCalled: Boolean;
public
FileName: String;
end;
var
FirstPic: TFirstPic;
implementation
uses
Globals,
MmSystem;
{$R *.DFM}
continues
end.
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
MPlayer, StdCtrls, ExtCtrls;
type
TSecondPic = class(TForm)
Image1: TImage;
MediaPlayer1: TMediaPlayer;
Next: TButton;
procedure NextClick(Sender: TObject);
procedure MediaPlayer1Notify(Sender: TObject);
procedure FormActivate(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
ActivateCalled: Boolean;
public
{ Public declarations }
end;
var
SecondPic: TSecondPic;
implementation
uses
Globals,
mmSystem;
{$R *.DFM}
end.
interface
uses
WinTypes, WinProcs, Classes,
Graphics, Forms, Controls,
MPlayer, StdCtrls, ExtCtrls;
type
TThirdPic = class(TForm)
Next: TButton;
MediaPlayer1: TMediaPlayer;
Image1: TImage;
procedure NextClick(Sender: TObject);
procedure MediaPlayer1Notify(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormActivate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
ActivateCalled: Boolean;
public
{ Public declarations }
end;
var
ThirdPic: TThirdPic;
implementation
uses
Globals,
mmSystem;
{$R *.DFM}
begin
if ActivateCalled then Exit;
MediaPlayer1.Visible := False;
MediaPlayer1.FileName := Wave4;
MediaPlayer1.Open;
MediaPlayer1.Play;
ActivateCalled := True;
end;
end.
Listing 34.8. The Globals unit, which defines some constants used by all the
units in the program.
unit Globals;
interface
var
Wave1, Wave2,
Wave3, Wave4: String;
implementation
uses
StrBox;
var
ThePath: String;
begin
ThePath := StripLastToken(ParamStr(0), ‘\’) + ‘\’;
Wave1 := ThePath + ‘mcinum1.wav’;
Wave2 := ThePath + ‘mcinum2.wav’;
Wave3 := ThePath + ‘mcinum3.wav’;
Wave4 := ThePath + ‘mcinum4.wav’;
end.
The design of this program is too simple to merit much comment. However, experienced pro-
grammers will note that it would be possible to merge the PICT1, PICT2, and PICT3 forms
into one unit. I kept them separate because most programs of this type need a separate object
to manage each form shown to the user.
Notice also that I call the StripLastToken function from the STRBOX unit that ships with this
book. This function effectively returns the directory from which the program was launched. It
does this by parsing the ParamStr variable, which usually is used to record the parameters passed
to a program. For instance, if you pass a single parameter to Present, you can retrieve the pa-
rameter with the following code:
S := ParamStr(1);
However, in this case I don’t pass any parameters to the program. Nevertheless, I can retrieve
the executable name and full path to where that executable is stored by writing the following:
S := ParamStr(0);
The ParamCount variable tells you how many parameters are passed to a program.
The Present program took about two hours to design and write. About half of that time was
dedicated to composing the script for the WAVE files and then recording them through a mi-
crophone. The actual job of writing the code was trivial. One of the goals of Delphi is to sim-
plify the mechanics of writing a Windows program. Certainly, the TMediaPlayer helps to achieve
that goal.
MIDI Details
On the UnleashedCD-ROM you will find a console application called Sounder. This program
uses low-level Windows multimedia calls to play MIDI notes. If a sound card and proper midi
drivers are present, this code will allow you to designate:
■ An instrument (such as piano, guitar, or drums)
■ A note to play
■ An approximate duration
■ A volume
Before playing a note, you must open the MIDI device by calling MidiOutOpen. When doing
so, you pass in a variable of type phMidiOut, which in Win32 resolves to a pointer to an integer,
that is, a pointer to an unsigned 32-bit value. This latter value is the handle you use to identify
your copy of the MIDI device, and you use it on all subsequent calls to MIDI services. In this
program, a global variable called hMidi is used as the handle to the MIDI device.
Next you should initialize the device by selecting an instrument. To do this, pass $C0
(ChangeInstrument) to the MidiOutShortMsg function.
MidiOutShortMsg takes two parameters. The first is the handle to the MIDI device (hMidi), and
the second is a 32-bit integer represented here by a structure of type TMidiMsg:
TMidiMsg = Record
Status, The status field usually issues a command such as PlayNote
Data1, Additional information, such as the note to play
After you are set up, you can use the MidiOutShortMsg functions to play notes, setting the sta-
tus field of the TMidiMsg structure to $90 (midiPlayNote);
Msg.Status := midiNotePlay;
Msg.Data1 := Note;
Msg.Data2 := Volume;
Msg.NoData := 0;
Err := MidiOutShortMsg(hMidi^, LongInt(Msg));
if Err <> 0 then WriteLn(Err);
When you are done, call MidiOutClose, passing it the handle to the MIDI device (hMidi^).
Once again, the complete sample program is found on disc as SOUNDER.DPR and two re-
lated files. Remember that sounder is a console application. As a bonus, the program shows
you a few tricks for handling keyboard input while in console applications.
Summary
In this chapter, you learned some of the basic facts about multimedia, and how to write two
different types of multimedia programs.
The major types of multimedia files covered in this chapter were as follows:
■ WAVE files for recording any type of sound
■ MIDI files for recording music
■ AVI files for recording movies
There were two types of multimedia applications covered:
■ Access programs give the user the ability to play a wide range of programs.
■ Presentation programs hide the internals of multimedia development from the user,
and simply play sounds and films as a way of entertaining or informing the user.
Clearly, Delphi offers a simple and powerful way to create multimedia applications. When
writing your own multimedia programs, use this chapter as a starting point, and your imagina-
tion as a guide.
Advanced Multimedia
Capability 35
This chapter covers one fairly complicated subject: using the Multimedia Control Interface
(MCI). In particular, these pages show how to play WAV files, MIDI files, AVI files, and the
type of music CDs that you would buy at a record store.
The technique used in this chapter is the same used to create the TMediaPlayer component
itself. Both this chapter and the TMediaPlayer base their technology on the MCI. This chapter
therefore provides two services:
■ It contains a commentary on the MCI and shows how to use it in your own programs.
The MCI interface is quite powerful and, if you understand it, you can array your
programs with a number of stunning multimedia features.
■ The chapter also explains the technology used in the TMediaPlayer, giving you the
ability to use the component to its capacity and to extend that when, or if, necessary.
The chapter starts off with some general comments about multimedia, then provides a few simple
MCI examples and, finally, presents a large program custom designed to play AVI, WAV, and
MIDI files, as well as standard CDs that you would buy in a music store. This program, called
Media32, is a Delphi port of an old OWL program called Player, first shown in the first edi-
tion of this book. The code has been completely revamped to run under 32-bit Delphi.
When calling this routine, I ask the user to pass in a string with at least 128 bytes of memory
allocated for it:
var
S: array[0..128] of Char;
RC: LongInt;
begin
… // Additional code here
GetErrorMessage(RC, S);
end;
In Delphi 2.0, I could easily upgrade this code to work with strings rather than PChars:
function GetErrorMessage(RC:LongInt; S: string): string;
begin
SetLength(S, MsgLen);
if not mciGetErrorString(RC, PChar(S), MsgLen) then
S := ‘No message available’;
Result := S;
end;
This might make the code somewhat easier for some programmers to read and use. However,
to do so would require calling SetLength, and the SetLength function is not supported in Delphi
1.0 or in Turbo Pascal.
Which is better: maintaining compatibility with Delphi 1.0 and Borland Pascal or writing code
that is slightly easier to read and use? For a beginner, the answer would be weighted in favor of
the latter of these two options. This book, however, is not aimed at beginners, and so I decided
to keep the code compatible with the widest possible range of tools.
In this context, it’s important to note that this book is being written in the Winter of ’95–’96.
At this time most professional programmers will be targeting both Windows 3.1 and Win32,
so compatibility with past versions of Delphi is a big issue. If you are reading this book in 1998
or 99, the need for backward compatibility will hopefully seem less pressing than it is at the
time of this writing. Such is the nature of the rapidly changing world of computers.
At this time it seems best to make the code as flexible as possible, so long as it still runs smoothly
in Win32, and so long as it is not extracting a price in terms of performance. Both of these
criteria are met in this code, so I have left the core multimedia commands compatible with
Delphi 1.0 wherever possible. Most of the interface code in this chapter will work only with
Delphi 2.0, but it would not be hard to port it to Delphi 1.0.
Another crucial advantage of this style of programming is that it gives the user a common in-
terface to a series of radically different pieces of hardware. For instance, at this time, the MCI
command interface works with the following different types of devices, which are listed here
opposite their official MCI names:
animation Animation device
avivideo Movie player
cdaudio CD player
dat Digital audio tape device
digitalvideo Digital video device
scanner Image scanner
sequencer MIDI
vcr Video tape player
videodisc Videodisc device
waveaudio A device that plays WAV files
What MCI has tried to do is to find the things that all these devices have in common, then use
these similarities to bind them together. In particular, it makes sense to ask all of these devices
to play something, to stop playing, to pause, to seek to a particular location in their media, and
so on. In other words, they all respond to these commands as the primary MCI messages.
Talk about device independence! The Windows multimedia extensions not only protect you
from the details of how a particular device might work, but they also frequently let you treat a
CD and a WAV file in almost exactly the same manner. For that matter, you can come close to
using the same code to play a WAV file as you would to play a MIDI. The only difference
would be that one time you would tell MCI that you want to work with a waveaudio device,
whereas the next time you would say that you want to work with a sequencer.
With the low-level API out of the way, that leaves only two remaining programming techniques.
The first is a string-based interface designed primarily to provide support for high-level lan-
guages such as Visual BASIC. You can call these functions from Delphi, but I have opted to
ignore them in favor of a third technique. The Media32 program has access to this third tech-
nique, which is a powerful, message-based interface.
The peculiar thing about this command-message interface is that it relies heavily on a single
routine, called mciSendCommand, which takes four parameters. Though this might sound fairly
limiting at first, in practice it turns out to be a reasonably flexible system.
Here is what a typical call to mciSendCommand might look like:
Return := mciSendCommand(videoId, mci_Close, 0, LongInt(@mciClose));
The next few paragraphs introduce you to the four parameters passed to this function.
The first parameter passed to mciSendCommand is a handle or ID number used to identify the
particular device in question. For instance, when you first open up a CD drive, you pass 0 in
this parameter because you don’t yet have an ID for the device. Thereafter, you pass in the ID
returned to you when you opened the device.
The key to this entire interface is the second parameter, passed as a message to the function in
order to specify a particular command. Here is a list of the 12 most common of these messages
and their meanings:
mci_Capability Queries the device’s capabilities
mci_Close Closes a device
mci_Info Queries the of type hardware being used
mci_Open Opens a device
mci_Play Plays a song or piece on a device
mci_Record Records to a device
mci_Resume Resumes playing or recording
mci_Seek Moves media forward or backward
mci_Set Changes the settings on device
mci_Status Queries about whether the device is paused, playing, and so on
mci_Stop Stops playing or recording
To complement these commands, there is a set of flags and records available that can give pro-
grammers the kind of fine-tuning they need to get the job done right. For instance, the mci_Play
message has four important flags that you can use together in an OR operation to form its third
parameter:
mci_Notify Post Mm_Notify message on completion
mci_Wait Complete operation before returning
mci_From Starting position is specified
mci_To Finish position is specified
You can use the last two flags here in an OR operation this way:
mci_From or mci_To
In this form, they inform MCI that a starting and finishing position will be specified in the last
parameter:
RC := mciSendCommand(wDeviceId, MCI_PLAY, mci_From or mci_To, Longint(@Info));
The fourth parameter is a pointer to a record. The record passed by mciSendCommand differs,
depending on the message being sent. In the preceding example, when an mci_Play message is
used, the structure looks like this:
Pmci_Play_Parms = ^Tmci_Play_Parms;
Tmci_Play_Parms = Record
dwCallback: LongInt;
dwFrom: LongInt;
dwTo: LongInt;
end;
Sometimes it’s necessary to fill out all three fields of this structure. At other times, some or
none of them can be filled out. For instance, if you set the mci_Notify flag in the second pa-
rameter of mciSendCommand, you probably will want to set dwCallback equal to the HWnd of the
window you want MCI to notify. Specifically, if you were inside a TForm descendant at the
time you started playing a WAV file, you would want to pass the dialog’s HWnd in dwCallback,
so that the dialog would be informed through an mm_Notify message when the WAV file stopped
playing.
Of course, if you set both the mci_From and mci_To flags, you would want to fill out both the
dwFrom and the dwTo fields of the Tmci_Play_Parms record (and so on). Remember that the
Tmci_Play_Parms recorded is associated explicitly with the mci_Play message. Other messages
have their own, unique record structures. For instance, the mci_Open message is associated with
the Tmci_Open_Parms structure.
All of the multimedia structures or constants discussed in this chapter are listed in
MMSYSTEM.PAS. Some versions of Delphi include an MMSYSTEM.HLP file. In some cases,
the MMSYSTEM.HLP file may enable you to search for a structure using only the C language
syntax; that is, without prefixing the name with a T. Therefore, you should search for
MCI_OPEN_PARMS , not Tmci_Open_Parms .
The only major aspect of the mciSendCommand function not yet discussed is its return value, which
happens to be an error number kept in the low-order word of a LongInt. Microsoft came through
with a nice touch at this point by adding the mciGetErrorString function, which provides you
with an explanation of any error in return for the result of mciSendCommand function.
mciGetErrorString even sends you back a pleasant little message telling you that all has gone
well, if that is indeed the case.
Playing a File
The first program I want to show you in this chapter provides the ability to play a WAV file.
This is a console application, which takes a single WAV file as a parameter:
play vivaldi.wav
The code for the Play program is shown in Listing 35.1. Remember when you are working
with console applications to go to the Project | Options | Linker menu choice and set Generate
Console Application to TRUE. If you’re working from the command prompt, you should enter
the following:
dcc32 -CC play.pas
where the -CC directive tells the compiler to produce a console application.
Listing 35.1. The Play program is a small console application that enables you to
play a single WAV file.
program Play;
uses
MMSystem;
const
MaxStr = 200;
var
DeviceID: LongInt;
DeviceID := mciOpen.wDeviceId;
mciPlay.dwFrom := 0;
Flags := mci_From or mci_Wait;
mciError(MciSendCommand(DeviceId, mci_Play, Flags, LongInt(@mciPlay)), ‘Play’);
begin
if ParamCount < 1 then Exit;
PlaySound(PChar(ParamStr(1)));
end.
The core functionality in the program comes in two parts, both presented in the PlaySound
function. In the Media32 program, these two steps are presented in two separate, reusable func-
tions. The goal here is to create a single function that does all the work needed to play a WAV
file: open the file, and then play it.
The following code opens the WAV file:
var
mciOpen: Tmci_Open_Params;
begin mciOpen.lpstrDeviceType := ‘waveaudio’;
mciOpen.lpstrElementName := FileName;
Flags := mci_Open_Element or mci_Open_Type;
mciSendCommand(0, mci_Open, Flags, LongInt(@MciOpen));
… // Additional code
end;
This code fragment appears a bit intimidating at first, but it should yield easily enough if you
concentrate on it for a few moments.
The Tmci_Open_Params structure is the centerpiece of lines shown above:
Tmci_Open_Parms = record
dwCallback: DWORD;
wDeviceID: MCIDEVICEID;
lpstrDeviceType: PAnsiChar;
lpstrElementName: PAnsiChar;
lpstrAlias: PAnsiChar;
end;
NOTE
If you’re double-checking my work, you will find that the structure shown above
actually is called Tmci_Open_ParmsA. The issue here, of course, is that there is an ANSI
version of this record and also a Wide, or Unicode, version of this record. In Delphi
2.0, you want to use the ANSI version of the record, which is called Tmci_Open_ParmsA,
in contrast to Tmci_Open_ParmsW. MMSYSTEM.PAS specifies this by making the
following declaration:
Tmci_Open_Parms := Tmci_Open_ParmsA;
This type declaration means that Tmci_Open_Parms actually does resolve to the record
shown above, even if it is not overtly declared that way.
This system of having two declarations for a record is necessary only if the declaration
has a string in one of its fields. If the types involved are not strings, there is no need for
a separate Unicode declaration. Object Pascal supports Unicode, but the VCL controls
are not Unicode ready. This means that Unicode is not practical in most Delphi
applications, so the environment defaults to using the ANSI declarations.
To use the Tmci_Open_Parms structure, fill in the device type and element name and then pass
in its address in the fourth parameter of mciSendCommand. A device ID identifying your handle
to the multimedia WAV services is returned in the wDeviceID field if the function succeeds.
Because you are passing in the element name and its type, you should pass in the following
flags when you call mciSendCommand:
Flags := mci_Open_Element or mci_Open_Type;
NOTE
For simple device types such as CD audio players, you don’t need a device element.
Compound device types require a device element. A compound device type requires
you to specify an element name along with a device type. In most cases, an element
name is just another way of referring to a file name. However, there are times when
this second half of the compound device is not a file name, so some very literal folks at
Microsoft called it an element, rather than a file. For all practical purposes, though, the
element name is a file name.
Here’s an example of opening a CD:
function OpenCD: Boolean;
var
Info : Tmci_Open_Parms;
ErrNo : LongInt;
Flags: LongInt;
S1: array[0..MsgLen] of Char;
begin
OpenCD := True;
FillChar(Info, SizeOf(Tmci_Open_Parms), #0);
Info.lpstrDeviceType := PChar(MCI_DEVTYPE_CD_AUDIO);
Flags := MCI_OPEN_TYPE or MCI_OPEN_TYPE_ID;
ErrNo := mciSendCommand(0, MCI_OPEN, Flags, Longint(@Info));
wDeviceId := Info.wDeviceId;
The second parameter passed to mciSendCommand is the specific command that you want to invoke:
mciSendCommand(0, mci_Open, Flags, LongInt(@MciOpen));
In this case you’re saying that you want to open a file, so you pass in mci_Open.
NOTE
There is a parallel between a command like mci_Open and the messages you use with
SendMessage or PostMessage . For instance, if you pass wm_SetText to SendMessage, you
are telling Windows that you want to set the text in some control. In the same way,
when you pass in mci_Open, you are telling Windows that you want to open some
device. Note also that SendMessage takes a window handle in the first parameter, just as
mciSendCommand takes a device ID in the first parameter. In general, there is a great deal
of similarity between SendMessage and mciSendCommand.
The first parameter is where you would normally pass in a device ID. Because the purpose of
this particular call is to retrieve a device ID, it’s natural that this parameter should be set to 0.
After opening up the WAV device, you should have a hold on the device ID:
DeviceID := mciOpen.wDeviceId;
However, there is some chance that something went wrong while opening the device. As a rule,
the only things that can go wrong involve a user not having the system set up correctly, or else
passing an invalid file name to Windows. In either case, the result returned by mciSendCommand
would be set to a non-zero value.
You can pass the integer returned from mciSendCommand to the following error function:
function mciError(Return: LongInt; Caller: string): Boolean;
var
S: array[0..MaxStr] of Char;
begin
if Return <> 0 then begin
mciGetErrorString(Return, S, MaxStr);
WriteLn(S + ‘ ‘ + Caller);
Halt;
end;
mciError := True;
end;
This function is tailor-made for the simple console application currently under discussion. In
particular, it checks the value returned from mciSendCommand. If this value is non-zero, it prints
a message and then terminates the application. Termination would be a bit too strong a reac-
tion in a standard application, but it is the appropriate thing to do in this case.
Assuming that you have successfully opened the device, the next step is to play the file:
mciPlay.dwFrom := 0;
Flags := mci_From or mci_Wait;
MciSendCommand(DeviceId, mci_Play, Flags, LongInt(@mciPlay));
The code shown here works with Tmci_Play_Parms rather than Tmci_Open_Parms:
Tmci_Play_Parms = record
dwCallback: DWORD;
dwFrom: DWORD;
dwTo: DWORD;
end;
The only way to know for sure which structure you should pass in is to look the information
up in a book like this, or in the online Help.
In this case, the dwTo field of Tmci_PlayParms is ignored. To tell Windows of your intentions,
you must leave out the mci_To flag when you call mciSendCommand:
Flags := mci_From or mci_Wait;
The mci_From flag states that you are going to designate where you want to start playing the file
from; in this case, point 0. The mci_Wait flags tells Windows that you don’t want this function
to return until the file has finished playing.
The only other thing you need to know in order to call the function is which command causes
a file to be played. Needless to say, mci_Play is the appropriate command to issue in this cir-
cumstance, so that parameter is passed in the second position.
The first parameter to mciSendCommand now is filled with the device ID retrieved during the call
to open the device. Except for a call accompanying the mci_Open command, you should pass in
a device ID when talking to the MCI a majority of the time.
Assuming that the code succeeded, and that the file played successfully, the only step left is to
close the device:
mciError(MciSendCommand(DeviceID, mci_Close, 0, LongInt(nil)), ‘Close’);
Notice that this command does not take a special record in the fourth parameter, nor is there
any need to pass in a special set of flags. As a result, the command is easy to issue, and you can
close the device with little or no fuss. The line of code quoted here shows how to wrap
mciSendCommand in a routine that checks for errors.
Recording Sound
The next program, called Records, is similar to the Play program. Once again, it contains only
two functions: the first called RecordSound and the second called mciError. The RecordSound
function enables you to first record a few words, and then to immediately play them back. The
function takes a single parameter, which is the number of milliseconds you want the recording
to last.
The code for the program is shown in Listing 35.2. Once again this is a console application, so
you should either compile from the command prompt with the -CC switch, or else tell the IDE
to Generate Console Applications, as described above.
You need a sound card and a microphone to use this program. Start the program from the
command prompt. A single line of text appears stating that you should “record.” Say whatever
is on your mind, giving yourself a little less than 10 seconds to complete the task. The moment
the 10 seconds are up, your words are repeated back to you. The program finishes by saving
your words in a file called MYWAVE.WAV.
Listing 35.2. The code for the Records program shows how to record and play
back sound from a WAV file.
program Records;
uses
MMSystem;
const
MaxStr = 200;
var
DeviceID: LongInt;
DeviceID := mciOpen.wDeviceId;
WriteLn(‘Record’);
mciRecord.dwTo := MMSecs;
Flags := mci_To or mci_Wait;
mciError(mciSendCommand(DeviceID, mci_Record,
Flags, LongInt(@mciRecord)), ‘Record’);
WriteLn(‘Stop’);
mciPlay.dwFrom := 0;
Flags := mci_From or mci_Wait;
mciError(mciSendCommand(DeviceId, mci_Play, Flags, LongInt(@mciPlay)), ‘Play’);
mciSave.lpfileName := ‘MyWave.Wav’;
Flags := mci_Save_File or mci_Wait;
begin
RecordSound(10000);
end.
After the in-depth examination of the Play program, there’s little need to go into much detail
about Records. The program begins by opening the MCI WAV device. Notice that this time
an empty element name is passed in, as you want to create a new file rather than play or modify
an old one.
The next step is to record your voice:
mciRecord.dwTo := MMSecs;
Flags := mci_To or mci_Wait;
mciError(mciSendCommand(DeviceID, mci_Record,
Flags, LongInt(@mciRecord)), ‘Record’);
You can ignore the dwCallback parameter, which is used to designate a callback window if you
want to receive mm_mciNotify messages (such as the mci_Notify_Successful message I talked
about in the last chapter).
The dwTo field is filled in with the amount of information you want to record, measured in
milliseconds. If you don’t fill in the dwTo field, the recording continues until you send an mci_Stop
command, as shown later in this chapter, in the Media32 program. You can record from one
point in a file to another point in a file using the dw_From and dw_To flags. That is, you can edit
the middle of the file.
Once again, the mci_Wait command is passed in to designate that you don’t want the function
to return until the recording is completed:
Flags := mci_To or mci_Wait;
After recording the file, the program plays it back to the user:
mciPlay.dwFrom := 0;
Flags := mci_From or mci_Wait;
mciSendCommand(DeviceId, mci_Play, Flags, LongInt(@mciPlay);
This is the same command given in the Play program, so there is no need for an explanation.
You can ignore the dwCallback field, but should designate the filename in the lpFileName field.
That’s all I’m going to say about the Records program. Really, the basic theory behind the
mciSendCommand interface is not that hard to understand. You have to spend some time wres-
tling with the flags, constants, and parameters, but the basic concepts involved are simple enough.
This is a fairly large project but, as you can see, it’s arranged in a sensible manner. All the files
that contain raw MMSYSTEM.PAS-related code end with INFO: WAVEINFO, MIDIINFO,
and so on. The Delphi front ends for these files all end in PLAY: WAVEPLAY, CDPLAY, and
so on. Then there are a few other files used to glue the program together, or to report on the
capabilities of the current system.
Listings 35.3 through 35.15 contain the code for the program. You can find the source on the
CD-ROM, along with some WAV, MIDI, and AVI files that you can use for experimenta-
tion. Remember that the StrBox unit referenced in the uses clause for this program ships on
the UnleashedCD-ROM, in a subdirectory called UNITS. To compile this program you need
to go to the Project | Options | Directories/Conditionals menu page and specify this directory
in the Search Path option, or else you should be sure this directory is listed in the Library Path
found in the Tools | Options | Library menu page.
Listing 35.3. The code for playing AVI files. Note that I have ported portions of
the Video for Windows headers from C to Pascal.
unit AviInfo;
interface
uses
WinProcs, MMSystem, WinTypes,
Classes;
const
MCI_DGV_OPEN_PARENT = $00020000;
MCI_DGV_OPEN_WS = $00010000;
MCI_MCIAVI_PLAY_WINDOW = $01000000;
MCI_MCIAVI_PLAY_FULLSCREEN = $02000000;
MCI_MCIAVI_PLAY_FULLBY2 = $04000000;
MCI_DGV_WHERE_SOURCE = $00020000;
MCI_DGV_STATUS_HWND = $00004001;
type
Tmci_Close_Parms = Tmci_Generic_Parms;
Pmci_dgv_Set_Parms = ^Tmci_dgv_Set_Parms;
Tmci_dgv_Set_Parms = record
dwCallback: LongInt;
dwTimeFormat: LongInt;
dwAudio: LongInt;
dwFileFormat: LongInt;
dwSpeed: LongInt;
end;
TMainAviHeader = record
dwMicroSecPerFrame: LongInt;
continues
pmci_dgv_Window_Parms = ^Tmci_dgv_Window_Parms;
Tmci_dgv_Window_Parms = record
dwCallback: LongInt;
HWindow: HWnd;
wReserved1: Word;
nCmdShow: Word;
wReserved2: Word;
lpstrText: PChar;
end;
Pmci_dgv_status_parms = ^Tmci_dgv_status_Parms;_DGV_STATUS_PARMS;
Tmci_dgv_status_parms = record
dwCallback: LongInt;
dwReturn: LongInt;
dwItem: LongInt;
dwTrack: LongInt;
lpstrDrive: PChar;
dwReference: LongInt;
end;
Pmci_dgv_Rect_Parms = ^Tmci_dgv_Rect_Parms;
Tmci_Dgv_Rect_Parms = record
dwCallback: LongInt;
{$ifdef MCI_USE_OFFEXT}
ptOffset: TPoint;
ptExtent: TPoint;
{$else}
rc: TRect;
{$endif}
end;
implementation
uses
Globals;
var
VideoID: LongInt;
MovieOpen: Boolean;
rcMovie: TRect;
MovieWindow: HWnd; { The movie screen itself }
OwnerWindow: Hwnd; { For Sending mm_Notify }
continues
GetWindowRect(MovieWindow, rcClient);
MoveWindow(MovieWindow, rcClient.Left, rcClient.Top,
rcMovieBounds.Right, rcMovieBounds.bottom + Caption, TRUE);
ShowWindow(MovieWindow, sw_ShowNormal);
end;
procedure SetMoviePosition;
var
V: Tmci_Anim_rect_parms;
R, R1: TRect;
Flags: LongInt;
ErrNo: LongInt;
begin
GetClientRect(MovieWindow, R1);
R := Rect(4, 4, R1.Right - 8, R1.Bottom - 8);
Flags := mci_Anim_Rect or mci_Anim_Put_Destination;
V.rc := R;
ErrNo := mciSendCommand(VideoID, mci_Put, Flags, LongInt(@V));
CheckForError(ErrNo, ‘SetMoviePosition’);
end;
{------------------------------------------------
Window will get the mm_Notify messages.
Screen is for showing them movie in another window.
------------------------------------------------}
function OpenFlick(Window: HWnd; Screen: HWnd; Path: PChar): Boolean;
var
mciOpen: Tmci_dgv_Open_Parms;
ErrNo: LongInt;
begin
OpenFlick := False;
OwnerWindow := Window; { For mm_Notify }
MovieWindow := Screen; { For sizeing he screen }
fillChar(mciOpen, SizeOf(mciOpen), #0);
mciOpen.lpstrDeviceType := ‘avivideo’;
mciOpen.lpstrElementName := Path;
mciOpen.dwStyle := ws_Child;
mciOpen.hWndParent := OwnerWindow;
ErrNo := mciSendCommand(0,
mci_Open,
mci_dgv_Open_Parent or
mci_Open_Element or
continues
videoID := mciOpen.wDeviceID;
OpenFlick := True;
end;
MovieOpen := TRUE;
PlayFlick := True;
end;
initialization
VideoID := -1;
finalization
CloseFlick;
end.
Listing 35.4. The Delphi front end for playing AVI files.
unit AviPlay;
interface
uses
SysUtils, Messages, Classes,
Graphics, Controls, Forms,
Dialogs, AviInfo, MMSystem,
WinProcs, WinTypes, StdCtrls,
ExtCtrls, Globals;
continues
TAviDialog = class(TForm)
Panel1: TPanel;
OpenDialog1: TOpenDialog;
Panel2: TPanel;
cbFullScreen: TCheckBox;
cbRepeat: TCheckBox;
bBrowse: TButton;
cbFitInWindow: TCheckBox;
cbUseScreen: TCheckBox;
bStop: TButton;
bStart: TButton;
procedure PlayFile(Sender: TObject);
procedure FormResize(Sender: TObject);
procedure bStartClick(Sender: TObject);
procedure bStopClick(Sender: TObject);
private
AviInfo: TMovieInfo;
FileName: String;
Normal, FullScreen: HWnd;
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
procedure SetInfoRecord(var AviInfo: TMovieInfo);
function GetInfo(path: PChar): Boolean;
function GetFullTitle: string;
{ Public declarations }
procedure mciNotify(var Msg: TMessage);
message mm_mciNotify;
procedure PlayAviFile;
end;
var
AviDialog: TAviDialog;
implementation
{$R *.DFM}
procedure TAviDialog.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
begin
GetInfo := False;
FillChar(AviInfo, SizeOf(TMovieInfo), #0);
{ StrCopy(AviWindow.AviInfo.Filename, Path); }
if findfirst(Filename, faArchive, SR) <> 0 then begin
MessageBox(Handle,’Error finding file’, nil, mb_Ok);
Exit;
end;
AviInfo.Filesize := SR.Size;
continues
n := Min(mmSub.cksize, sizeof(TMainAviHeader));
if(mmioRead(h, @AviInfo.Avi, n) <> n) then begin
MessageBox(Handle, ‘Error reading header chunk’, nil, mb_Ok);
mmioClose(h,0);
Exit;
end;
mmioClose(h,0);
GetInfo := True;
end;
procedure TAviDialog.PlayAviFile;
var
P: array[0..250] of Char;
begin
CloseFlick;
if not OpenFlick(Handle, Panel1.Handle, StrPCopy(P, FileName)) then Exit;
if AviInfo.UseScreen then SetDisplay;
if AviInfo.FitInWindow then SetMoviePosition;
PlayFlick(aviInfo.FullScreen);
end;
end.
Listing 35.5. The PLAYINFO unit containing many generic calls relied on by all
the devices.
unit PlayInfo;
{
Routines shared by all the Play objects. These are things
that everyone can do, or at least that can be done on more
than one device.
continues
uses
MMSystem, WinTypes, SysUtils,
WinProcs;
const
MsgLen = 200;
var
wDeviceID: Word;
PlayWindow: HWnd;
ErrorState: Boolean;
implementation
continues
begin
wDeviceId := 0;
end.
interface
uses
MMSystem, PlayInfo, SysUtils,
WinProcs, WinTypes;
implementation
end.
interface
uses
Messages, MMSystem, WinProcs,
TimeName, WaveInfo, WinTypes,
SysUtils, Forms, StdCtrls,
Controls, Classes, PlayInfo,
FileCtrl, ExtCtrls, Dialogs;
const
DevStr = ‘WaveAudio’;
type
TWaveDlg = class(TForm)
Play: TButton;
bStop: TButton;
FileListBox1: TFileListBox;
Timer1: TTimer;
Panel1: TPanel;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
DirectoryListBox1: TDirectoryListBox;
DriveComboBox1: TDriveComboBox;
StatInfo: TPanel;
TimeLapsed: TPanel;
DevInfo: TPanel;
LenText: TPanel;
bRecord: TButton;
SaveDialog1: TSaveDialog;
procedure SetUpWindow(Sender: TObject);
procedure WaveAbort(Sender: TObject);
procedure WavePlay(Sender: TObject);
procedure WaveRecord(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
Recording: Boolean;
Time: Word;
procedure CreateParams(var Params: TCreateParams); override;
procedure Loaded; override;
procedure GetDetails;
function GetTimeAndName: Boolean;
procedure MciNotify(var Msg: TMessage);
message mm_MciNotify;
function GetMode: string;
end;
var
WaveDlg: TWaveDlg;
implementation
uses
StrBox, Globals;
{$R *.dfm}
procedure TWaveDlg.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
procedure TWaveDlg.GetDetails;
var
S: array[0..100] of Char;
Result: LongInt;
begin
Result := GetLen;
StatInfo.Caption := GetMode;
StrPCopy(S, Format(‘%d ms’, [Result]));
LenText.Caption := S;
continues
end.
Listing 35.8. A dialog to designate settings when you are recording a file.
unit TimeName;
interface
uses
SysUtils, WinProcs, Messages,
Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
WinTypes, Buttons, ExtCtrls;
type
TTimeNameDlg = class(TForm)
BitBtn1: TBitBtn;
TimeSlicer: TRadioGroup;
BitBtn2: TBitBtn;
procedure OkPress(Sender: TObject);
public
Time: SmallInt;
function Run(var ITime: Word): Boolean;
end;
continues
implementation
{$R *.DFM}
end.
interface
uses
MMSystem, PlayInfo, SysUtils,
WinProcs, WinTypes;
procedure PauseMidi;
function GetMidiInfo(S: PChar): PChar;
function CheckForMapper: Boolean;
implementation
const
MsgLen = 200;
procedure PauseMidi;
var
Info: Tmci_Generic_Parms;
Flags: LongInt;
S1: array[0..MsgLen] of Char;
ErrNo: LongInt;
begin
Info.dwCallBack := 0;
Flags := 0;
ErrNo := mciSendCommand(wDeviceID, Mci_Pause, Flags, LongInt(@Info));
ErrorMsg(ErrNo, S1);
end;
StI := LoWord(StatusParms.dwReturn);
if (Sti <> Midi_Mapper) then begin
if(MessageBox(0, S, ‘Query’, mb_YesNo) <> IDYes) then begin
CloseMci;
exit;
end;
end;
CheckForMapper := True;
end;
end.
Listing 35.10. The Delphi front end for playing MIDI files.
unit MidiPlay;
{
MidiPlay
Programmer: Charlie Calvert
Copyright (c) 1996, by Charlie Calvert
Feel free to use this code as an adjunct to your own programs.
}
interface
uses
MidiInfo, Messages, MMSystem,
SysUtils, WinProcs, Classes,
Controls, StdCtrls, Forms,
WinTypes, PlayInfo, FileCtrl,
Buttons, ExtCtrls;
const
DevType:PChar = ‘Sequencer’;
type
TMidiDlg = class(TForm)
FileListBox1: TFileListBox;
bbAbort: TBitBtn;
bbOpen: TBitBtn;
bbPause: TBitBtn;
bbPlay: TBitBtn;
Panel1: TPanel;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
pCurMode: TPanel;
LenText: TPanel;
DevInfo: TPanel;
DirectoryListBox1: TDirectoryListBox;
DriveComboBox1: TDriveComboBox;
pCurTime: TPanel;
Label4: TLabel;
procedure MidiAbort(Sender: TObject);
procedure MidiOpen(Sender: TObject);
procedure MidiPause(Sender: TObject);
procedure MidiPlay(Sender: TObject);
procedure SetUpWindow(Sender: TObject);
private
FileName: string;
Location: LongInt;
DeviceIsOpen: Boolean;
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
procedure StartTimer;
public
procedure wmDestroy(var M: TMessage); message wm_Destroy;
procedure ReportStatus; virtual;
procedure MciNotify(var Msg: TMessage);
message mm_MciNotify;
procedure WmTimer(var Msg: TMessage);
message wm_Timer;
function GetMode: LongInt; virtual;
end;
var
MidiDlg: TMidiDlg;
implementation
uses
Globals;
{$R *.DFM}
procedure TMidiDlg.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
procedure TMidiDlg.ReportStatus;
begin
pCurMode.Caption := GetModeString(PlayInfo.GetMode);
pCurTime.Caption := IntToStr(GetLocation);
end;
continues
procedure TMidiDlg.StartTimer;
begin
if (SetTimer(Handle, PlayTimer, 1000, nil) = 0) then
MessageBox(Handle, ‘No Timers!’, ‘Whoops!’, mb_Ok + mb_IconExclamation);
end;
end.
interface
uses
MMSystem, PlayInfo, WinProcs,
WinTypes;
implementation
continues
wDeviceId := Info.wDeviceId;
procedure SetMSFasFormat;
var
Info: Tmci_Set_Parms;
ErrNo : LongInt;
S1 : array[0..MsgLen] of Char;
begin
Info.dwCallback := 0;
Info.dwTimeFormat := MCI_FORMAT_MSF;
Info.dwAudio := 0;
ErrNo:=mciSendCommand( wDeviceId,
MCI_SET,
MCI_SET_TIME_FORMAT,
Longint(@Info));
procedure SetTMSFasFormat;
var
Info: Tmci_Set_Parms;
ErrNo : LongInt;
S1 : array[0..MsgLen] of Char;
begin
Info.dwCallback := 0;
Info.dwTimeFormat := MCI_FORMAT_TMSF;
Info.dwAudio := 0;
procedure PlayCDOneTrack(StartTrack:Byte);
var
Info : Tmci_Play_Parms;
Flags,
ErrNo : LongInt;
S1 : array[0..MsgLen] of Char;
begin
FillChar(Info, SizeOf(Tmci_Play_Parms), 0);
Info.dwFrom := MCI_MAKE_TMSF(StartTrack,0,0,0);
procedure PlayMciCD(StartTrack,EndTrack:Byte);
var
Info : Tmci_Play_Parms;
Flags,
ErrNo : LongInt;
S1 : array[0..MsgLen] of Char;
begin
FillChar(Info, SizeOf(Tmci_Play_Parms), 0);
Info.dwFrom := MCI_MAKE_TMSF(StartTrack,0,0,0);
Info.dwTo := MCI_MAKE_TMSF(EndTrack, 0,0,0);
Info.dwCallBack := PlayWindow;
begin
Status.dwCallback := 0;
Status.dwReturn := 0;
Status.dwItem := MCI_STATUS_NUMBER_OF_TRACKS;
Status.dwTrack := 0;
ErrNo := mciSendCommand(wDeviceId, MCI_STATUS,
MCI_STATUS_ITEM, Longint(@Status));
if ErrNo<>0 then ErrorMsg(ErrNo, S1);
GetNumTracks := Status.dwReturn;
end;
continues
MSF :=Status.dwReturn;
Min := MCI_MSF_MINUTE(MSF);
Sec := MCI_MSF_SECOND(MSF);
Frame := MCI_MSF_FRAME(MSF);
end;
Flags := MCI_STATUS_ITEM;
ErrNo:=mciSendCommand(wDeviceID, MCI_STATUS, Flags, Longint(@Status));
procedure EjectCD;
var
Info : Tmci_Set_Parms;
Flags, ErrNo : LongInt;
S1 : array[0..128] of Char;
begin
FillChar(Info, SizeOf(Tmci_Set_Parms), 0);
Flags := mci_Set_Door_Open;
ErrNo:=mciSendCommand( wDeviceId, MCI_SET, Flags, Longint(@Info));
if ErrNo <> 0 then ErrorMsg(ErrNo, S1);
end;
end.
interface
uses
Messages, MmSystem, CDInfo,
SysUtils, Forms, Classes,
PlayInfo, WinProcs, WinTypes,
Controls, StdCtrls, Buttons,
ExtCtrls;
type
TCDDialog = class(TForm)
TrackList: TListBox;
Panel1: TPanel;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
pStatus: TPanel;
pCurTime: TPanel;
pCurTrack: TPanel;
pNumTracks: TPanel;
bbPlay: TBitBtn;
bbPause: TBitBtn;
bbPlayAll: TBitBtn;
bAbort: TBitBtn;
PlayList: TListBox;
bbPlaySel: TBitBtn;
Label5: TLabel;
Label6: TLabel;
procedure Abort(Sender: TObject);
procedure Pause(Sender: TObject);
procedure PlayAll(Sender: TObject);
procedure BeginPlay(Sender: TObject);
procedure SetUpWindow(Sender: TObject);
procedure TrackListDblClick(Sender: TObject);
procedure PlayListDblClick(Sender: TObject);
procedure bbPlaySelClick(Sender: TObject);
private
procedure Loaded; override;
procedure CreateParams(var Params: TCreateParams); override;
procedure wmDestroy(var M: TMessage); message wm_Destroy;
procedure FillTrackBox(HWindow: HWnd; NumTracks: LongInt);
procedure MoveItem(A, B: TListBox);
procedure SetAllTOPlayList;
public
NumTracks: LongInt;
NumToPlay: LongInt;
CurrentSelection: LongInt;
Paused : Boolean;
procedure StopNow; virtual;
procedure GetInfoFiles;
procedure ReportStatus; virtual;
procedure PlayNext;
function GetMode: LongInt; virtual;
procedure MciNotify(var Msg: TMessage);
message mm_MciNotify;
procedure WMTimer(var Msg: TMessage);
message wm_Timer;
procedure StartTimer;
end;
var
CDDialog: TCDDialog;
implementation
uses
Globals;
{$R *.DFM}
{--------------------------------------------------}
{ TCDPlayer’s method implementations: }
{--------------------------------------------------}
procedure TCDDialog.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
continues
procedure TCDDialog.StopNow;
begin
KillTimer(Handle, PlayTimer);
StopMCI;
end;
procedure TCDDialog.ReportStatus;
type
TTimeAry = Array[0..2] of Word;
var
S: string;
Track: LongInt;
Time: LongInt;
TimeAry: TTimeAry;
begin
{$IfDef Debug}
S := Format(‘CurrentSelection: %d’, [CurrentSelection]);
DC := GetDC(Handle);
TextOut(DC, 1, 1, PChar(S), Length(S));
ReleaseDC(Handle, DC);
{$EndIf}
PStatus.Caption := GetModeString(GetMode);
Track := GetCurrentCDTrack;
S := Format(‘%d’, [Track]);
pCurTrack.Caption := S;
Time := GetLocation;
TimeAry[2] := 0;
TimeAry[1] := mci_tmsf_Second(Time);
TimeAry[0] := mci_tmsf_Minute(Time);
if TimeAry[1] > 9 then
S := Format(‘%d:%d’, [mci_tmsf_Minute(Time), mci_tmsf_Second(Time)])
else
S := Format(‘%d:%d%d’, [mci_tmsf_Minute(Time), 0, mci_tmsf_Second(Time)]);
PCurTime.Caption := S;
end;
procedure TCdDialog.GetInfoFiles;
var
S: string;
begin
SetTMSFasFormat;
NumTracks := GetNumTracks;
S := Format(‘%d’, [NumTracks]);
pNumTracks.Caption := S;
FillTrackBox(Handle, NumTracks);
end;
continues
procedure TCDDialog.PlayNext;
var
S: string;
Start: Byte;
begin
if CurrentSelection = NumToPlay then begin
StopNow;
ReportStatus;
Exit;
end;
PlayList.SetFocus;
PlayList.ItemIndex := CurrentSelection;
PlayList.UpDate;
S := PlayList.Items.Strings[PlayList.ItemIndex];
Inc(CurrentSelection);
Start := Parse(PChar(S));
StartTimer;
if Start <> NumTracks then { Don’t pass in one more than total tracks }
PlayMciCD(Start, Start + 1)
else
PlayCDOneTrack(Start);
ReportStatus;
end;
procedure TCDDialog.SetAllTOPlayList;
begin
while PlayList.Items.Count <> 0 do begin
PlayList.SetFocus;
PlayList.ItemIndex := 0;
MoveItem(PlayList, TrackList);
end;
while TrackList.Items.Count <> 0 do begin
TrackList.SetFocus;
TrackList.ItemIndex := 0;
MoveItem(TrackList, PlayList);
end;
end;
procedure TCDDialog.StartTimer;
begin
if (SetTimer(Handle, PlayTimer, 1000, nil) = 0) then
MessageBox(Handle, ‘No Timers!’, ‘Whoops!’, mb_Ok + mb_IconExclamation);
end;
continues
end.
Listing 35.13. The Capabilities unit that reports on the capability of a particular
system in terms of what kind of multimedia files it can play.
unit Capable1;
interface
uses
SysUtils, Messages, WinProcs,
Classes, Graphics, Controls,
Forms, Dialogs, MMSystem,
StdCtrls, WinTypes, Globals;
type
TCapableDlg = class(TForm)
cbtcdAudio: TCheckBox;
cbtdat: TCheckBox;
cbtDigitalVideo: TCheckBox;
cbtMMMovie: TCheckBox;
cbtOther: TCheckBox;
cbtOverLay: TCheckBox;
cbtScanner: TCheckBox;
cbtSequencer: TCheckBox;
cbtVCR: TCheckBox;
cbtVideoDisc: TCheckBox;
cbtWaveAudio: TCheckBox;
cbtAviVideo: TCheckBox;
cbaNotSupported: TCheckBox;
cbaCanEject: TCheckBox;
cbaCanPlay: TCheckBox;
cbaCanPause: TCheckBox;
cbaCanStop: TCheckBox;
cbaCanRecord: TCheckBox;
cbaCanSave: TCheckBox;
cbaIsCompound: TCheckBox;
cbaHasAudio: TCheckBox;
cbaHasVideo: TCheckBox;
cbaUsesFiles: TCheckBox;
bcdAudio: TButton;
bDat: TButton;
bDigitalVideo: TButton;
bMMMovie: TButton;
bOther: TButton;
bOverLay: TButton;
bScanner: TButton;
bSequencer: TButton;
bVCR: TButton;
bVideoDisc: TButton;
bWaveAudio: TButton;
bAviVideo: TButton;
procedure SetUpWindow(Sender: TObject);
procedure TestAbilities(id: LongInt; OldResult: LongInt);
function TestMCI(DeviceType: PChar; DoTest: Boolean): Boolean;
procedure bcdAudioClick(Sender: TObject);
private
Box: TBox;
BoxAble: TBox;
{ Private declarations }
procedure ShowAbilities(var Msg: TMessage);
message wm_ShowAble;
procedure WmCommand(var Msg: TMessage);
message wm_Command;
public
{ Public declarations }
end;
var
CapableDlg: TCapableDlg;
implementation
uses
StrBox;
{$R *.DFM}
continues
end.
Listing 35.14. The Globals units contains some generic code used by a number of
units, especially CAPABLE1.PAS.
unit globals;
interface
uses
mmSystem, Windows, Messages;
const
MaxLen = 250;
PlayTimer = 1;
MidiError = - 251;
wm_ShowAble = wm_User + 3;
continues
end.
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls, Menus;
const
MaxDlg = 3;
dtAvi = 0;
dtWave = 1;
dtMidi = 2;
dtCD = 3;
type
TPlayMain = class(TForm)
MainMenu1: TMainMenu;
CD1: TMenuItem;
CDMenu: TMenuItem;
Midi1: TMenuItem;
Wave1: TMenuItem;
Avi1: TMenuItem;
Other1: TMenuItem;
Capability1: TMenuItem;
Help1: TMenuItem;
About1: TMenuItem;
procedure CmWave(Sender: TObject);
procedure CmCapability(Sender: TObject);
procedure CmAbout(Sender: TObject);
procedure FormResize(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
DlgAry: array[0..MaxDlg] of TForm;
procedure FreeDlgs;
procedure DoSize;
end;
var
PlayMain: TPlayMain;
implementation
uses
CDPlay, MidiPlay, WavePlay,
AviPlay, Capable1, About;
{$R *.DFM}
procedure TPlayMain.DoSize;
var
i: Integer;
begin
for i := 0 to MaxDlg do
if DlgAry[i] <> nil then begin
DlgAry[i].Left := 0;
DlgAry[i].Top := 0;
DlgAry[i].Width := ClientWidth;
DlgAry[i].Height := ClientHeight;
end;
end;
procedure TPlayMain.FreeDlgs;
continues
end.
When you launch the Media32 program, you are presented with a blank form. You might want
to start by pulling down the Capabilities dialog box, which you can use to find out what de-
vices are supported on your current system. This dialog box lists the possible devices on the left
and places a check mark in front of the ones that are supported. If you press the button in front
of any item, the specific capabilities associated with that item are checked off in the far right
column.
If you select any of the child windows listed in the File menu, you can play MIDI, WAV, or
AVI files, as well as CDs. If you elect to play a CD, you are presented with a list box that shows
the files available on the CD, as shown in Figure 35.1. To play a particular track for the CD,
move it into the right-side list box, select it, then press the Play button. If you move several
different files into the Selected list box, and then want to play them all in order, press the Play
Selected button. If you want to play all the files on the CD, choose the Play All button.
FIGURE 35.1.
The user selects three of the
21 tracks from a CD
containing Vivaldi’s Four
Seasons.
The WavePlay child window is shown in Figure 35.2. You can select this form when you want
to play or record WAV files. You can browse across the hard drive using the list boxes shown
on the right side of the form. If you want to record a file, press the Record button. A dialog
appears that enables you to select the time that you want your recording to last. For instance,
you can say: “I want to record for 20 seconds.” If you don’t know how long your recording is
going to last, choose the manual option. You can then record for as long as you want, and press
the Stop button when you are through.
FIGURE 35.2.
You can press the Record
button to start recording a
file, and the Stop button to
stop recording the file.
When you finish recording a file, a TSaveDialog appears. You can type a file name into this
dialog, then press OK to save the file.
Most of the time, all three devices can use the same code to play a file, close a file, stop a file, get
the length (in milliseconds) of a file, and pause a file. There are few variations used by the AVI
tools, so for now I am keeping it separate from the rest of this program.
NOTE
I put in a little additional work on this program from time to time, and will post the
changes to CompuServe, the Internet, and the Borland BBS. If you are interested, you
can check for updates. No promises, but I do tend to work on this thing every once in
a while. The old Turbo Pascal version of the program, called Player, also is available
online, and in the first edition of this book.
The Open command must be customized for each device. Its first parameter is the handle of the
dialog, window, or control that is playing the file. The second parameter is the name of the
file, and the third is the device type. Most of the major device types, such as sequencer,
waveaudio, and videodisc, are listed in the section called “Coming to Terms with
mciSendCommand .” Obviously, the last parameter is in some ways the most important because it
is the one that tells MCI whether you want to play a CD, a WAV file, or a MIDI file.
Next to these controls is a second set of check boxes with window text made from the follow-
ing array:
MMAbles:array[0..10] of PChar =
(‘Not Supported’, ‘Can Eject’, ‘Can Play’,
‘Can Pause’, ‘Can Stop’, ‘Can_Record’,
‘Can Save’, ‘Is Compound’, ‘Has Audio’,
‘Has Video’, ‘Uses Files’);
When Media32 starts, the check boxes associated with a particular device are marked if the
current machine supports that device. If the user selects the button with that device’s name on
it, Media32 tells the user if the device can eject, play, pause, stop, and so on. It queries Win-
dows to obtain this information by passing in one of the following flags in connection with the
mci_GetDevCaps function:
Tests:array[1..10] of LongInt =
(mci_GetDevCaps_Can_Eject,
mci_GetDevCaps_Can_Play,
mci_GetDevCaps_Can_Play,
mci_GetDevCaps_Can_Play,
mci_GetDevCaps_Can_Record,
mci_GetDevCaps_Can_Save,
mci_GetDevCaps_Compound_Device,
mci_GetDevCaps_Has_Audio,
mci_GetDevCaps_Has_Video,
mci_GetDevCaps_Uses_Files);
Here is the way the code in question actually appears in the program:
Info.dwItem := Tests[i];
Flags := mci_GetDevCaps_Item or mci_Notify;
Result := mciSendCommand(id, mci_GetDevCaps, Flags,
LongInt(@Info));
if Info.dwReturn > 0 then
SendMessage(BoxAble[i]^.HWindow, BM_SetCheck, 1, 0);
As you can see, the program sends a BM_SetCheck message to the appropriate check box if the
capability is supported on the user’s system. All of the preceding lines appear inside a loop that
enables the code to check for each capability in turn.
The end result of these activities is to inform the user immediately of what devices are available
on his or her system and what type of capabilities are associated with a particular device.
NOTE
The child windows in the Media32 program are like the child windows used in the
Music database program shown in Chapter 20, “Advanced InterBase Techniques.” In
particular, each child form is allocated memory explicitly by the main form, rather
than autocreated by Delphi. The child forms then override the Loaded and
CreateParams methods:
procedure TWaveDlg.Loaded;
begin
inherited Loaded;
Visible := false;
Position := poDefault;
BorderIcons := [];
BorderStyle := bsNone;
HandleNeeded;
SetBounds(0,0,Width,Height);
end;
By now, the structure of this program should be fairly clear to you. The Media32 program
consists of a main module called MAIN.PAS, which controls the main window and the four
submodules, called WAVEPLAY, MIDIPALY, AVIPLAY, and CDPLAY, each of which con-
trols a child form.
This code is similar to the PlayWave procedure except that, instead of using a Tmci_Play_Parms
record, it uses a Tmci_Set_Parms record:
Tmci_Set_Parms = record
dwCallback: Longint;
dwTimeFormat: Longint;
dwAudio: Longint;
end;
The key member of this structure is dwTimeFormat, which you use to select a particular time
format. The Media32 program uses milliseconds, although I could have chosen seconds and
minutes, or even the number of tracks that have been played.
Notice that mciSendCommand receives mci_Set_Time_Format as the third parameter. Other mes-
sages I could have passed in instead include mci_Set_Door_Closed or mci_Set_Door_Open. This
latter flag can be used to eject a CD from a CD player.
Some readers might not find it intuitively obvious to search out the mci_Set message as the
place to issue the command to eject a cassette. This highlights one possible criticism of MCI:
it lacks some of the intuitive feel of a more traditional API, which might feature a command
such as EjectCD. Of course, once you know the trick, there is no reason why you cannot create
your own EjectCD function. The key is to know the API well enough to find its hidden
secrets.
The preceding code implies the presence of a timer. The timer is used to check on the status of
the device being played. For instance, if a MIDI file is being played, the timer enables you to
check up on its progress at set intervals. In this particular program, the intervals are one-
quarter second in duration.
NOTE
You won’t get mm_Notify messages unless you specifically ask for them. The requests
occur in the XXINFO.PAS units:
mciSendCommand(wDeviceID, mci_Stop, mci_Notify, LongInt(@Info));
The third parameter of this call to mciSendCommand specifies that you want to get
mci_Notify messages that report on the program of the command. In particular, you
want to be notified when the device is stopped.
By selecting the times when you use the mci_Notify flag, you can choose when you
want to receive updates regarding the multimedia events that occur in your program. It
often is difficult to find the right balance between getting a confusing flurry of
messages and not getting enough messages. The goal is to be informed only when you
really need to know that something has occurred.
To fully understand the mci_Notify method, you have to understand that Media32 can ask to
receive a message whenever anything important happens to the file being played. For instance,
if the file ends or if the user presses the Pause button, an mm_Notify message is posted. Here is
the declaration for the MciNotify function:
procedure MciNotify(var Msg: TMessage);
virtual wm_First + mm_MciNotify;
If the file currently being played is finished, or if the user has asked to abort the play, the proper
response is to close the device. But this is not what you want to do if the user has simply paused
the file. To distinguish between these two different events, you process an mci_Status message
in the main program with the following code: ++++
function GetModeString(Mode: Integer): string;
var
S: string;
begin
case Mode of
MCI_MODE_NOT_READY: S := ‘Mode not ready’;
MCI_MODE_PAUSE: S := ‘Mode pause’;
MCI_MODE_PLAY: S := ‘Mode play’;
MCI_MODE_STOP: S := ‘Mode stop’;
MCI_MODE_OPEN: S := ‘Mode Open’;
MCI_MODE_RECORD: S := ‘Record’;
MCI_MODE_SEEK: S := ‘Seek’;
end;
Result := S;
end;
procedure TMidiDlg.ReportStatus;
begin
pCurMode.Caption := GetModeString(PlayInfo.GetMode);
pCurTime.Caption := IntToStr(GetLocation);
end;
This procedure first gets the current mode of the device by calling the following procedure:
function GetMode: Longint;
var
Info: Tmci_Status_Parms;
Flags,
ErrNo: LongInt;
S1: array [0..MsgLen] of Char;
begin
FillChar(Info, SizeOf(Tmci_Status_Parms), 0);
Info.dwItem := Mci_Status_Mode;
Flags := Mci_Status_Item;
ErrNo := MciSendCommand(wDeviceID, Mci_Status, Flags, LongInt(@Info));
if ErrNo <> 0 then begin
ErrorMsg(ErrNo, S1);
Result := -1;
exit;
end;
GetMode := Info.dwReturn;
end;
In my opinion, the GetMode function highlights the opacity to which the MCI command inter-
face can sometimes fall prey. Unless you actually looked up the mci_Status, mci_Status_Item,
and mci_Status_Mode messages in the MCI reference books, it is unlikely that you would guess
exactly what this function does just from looking at it.
Nevertheless, it does effectively meet the current needs. After calling it, you know whether the
device is paused or stopped, so you can handle the mm_Notify message with the appropriate
response. I set a variable called Mode to the value returned from GetMode, and then show the
user a string that explains the result of the query. The string is displayed in a static text control
that has been inserted in the object’s dialog.
Summary
Although it has not been possible for me to explore all of the many aspects of the MCI inter-
face in this chapter, I have given you enough information to get you up and running. If you
want to learn more, the best thing you can do now is study the included example and
MMSYSTEM.PAS interface. As soon as you have some feeling for how this code works, you
should begin writing your own code for manipulating multimedia files. There also are several
books out on this subject now, although most of them are based on the C language. With the
information included in this chapter, you should have no trouble reading those books, even if
the language is strange to you.
Overall, I think you’ll find the MCI interface a flexible, well-structured tool that appears to be
constructed so that it will withstand the ravages of time with a fair degree of aplomb. Certainly
its weaknesses are more than made up for by the excitement of multimedia programming. So,
go to it with a will and be sure to take the time to enjoy yourself.
VIII
IN THIS PART
PART
The Internet
The final part of this book, by guest author Ann Lynnworth, explores the
Internet. Ann and business partner Michael Ax have used Delphi to create
a series of Web-based tools that gives the user powerful access to some of
the most advanced features in contemporary programming. Ann shares
many of her technical secrets and a working copy of her code so that you
can see how the Internet tools that she presents are put together.
Introduction to CGI
Programming with 36
Delphi by Ann
Lynnworth
In this part of the book, I’m going to show you Delphi’s strengths as a platform for building
interactive, database-driven applications on the World Wide Web. I’ll give you a general in-
troduction to the problem and then discuss the various aspects of the solution.
First, let me tell you about my background. I have been writing CGI applications for the Web
since February 1995, which is a long time by Internet standards. Behind that, I have years of
experience as a consultant, earning my way using everything from Basic 1.0 in 1983 to C lan-
guage, Paradox, SQL, and now Delphi. I was lucky enough to stumble onto the Web ahead of
the wave, and most fortunate to strike a deal with an Internet Service Provider (ISP) who let
me set up an NT server early in the game. This led to my shipping a series of popular Delphi
shareware components for doing simple CGI programming—all back in June 1995.
Thereafter I set out to build something better: a solid, high-end CGI framework. I formed HREF
Tools Corp. with Michael Ax; he wrote WebHub, and here we are.
In presenting this material to you, I’m doing my best to be objective and present issues rather
than a sales pitch. Beyond that, all I can do is let you know my bias. So, without further ado...
NOTE
Delphi changed the way we write Windows applications in 1995, and it is now
creating a revolution in Internet programming.
In this chapter, you’ll begin to see how much Delphi offers in the context of the Web. Then in
Chapter 37, “Leveraging the BDE in Delphi,” we will look at specific “hot features” to prove
the point concretely.
Where Do We Start?
Let’s start with an experience that you’ve probably had by now. You’re surfing the Web with a
“browser” such as Netscape or Mosaic, and you’re about to type in http:// to go somewhere.
Have you wondered what http means? It stands for HyperText Transfer Protocol, the protocol
for exchange of data between a Web browser and server over the Internet. (There’s also SHTTP,
which is the secure version thereof.)
The http:// piece that you type to go somewhere on the Web is only the first of three strings
that comprise the destination URL—Uniform Resource Locator.
More generally, a URL is comprised of three parts, as shown in Table 36.1.
FIGURE 36.1.
An HTTP request for a site
on the World Wide Web.
When you ask your browser to go to a URL, it sends out a request (using TCP/IP as the net-
working protocol and http for the request itself ). This request is generally for “a page” located
on the indicated server. Often the URL that you type indicates only the name of a machine. In
that case, it’s the Web server’s job to return the default page, which is generally named
index.html.
The request can be for any type of file, including those shown in Table 36.2.
continues
The Web server’s job is to respond to the http request by returning a file. When CGI is in-
volved (see the following note), the server first runs a program which creates that file.
NOTE
CGI stands for Common Gateway Interface. A CGI program is any executable
program that conforms to certain rules about how input and output are treated. The
rules are discussed in detail below.
CGI programs can be written in any language that can create an executable for the
given operating system.
Figure 36.2 shows an overview of the interaction between a Web surfer, the server that responds
to the request, a CGI program that can be written in Delphi, and a database engine.
FIGURE 36.2.
Suzie Surfer can use her
browser to access infor-
mation in a database.
Keep this general overview in mind as we focus on the connection between surfer and Web
server; soon we’ll add the third and fourth pieces.
You can build HTML files using Notepad as your editor, or you can try any of the 100+ add-
on editors that are available. To get started, just make an ASCII file and save it with an .html
extension.
An excellent overview of HTML syntax is the Bare Bones Guide,available free online at http:/
/www.access.digex.net/~werbach/barebone.html.
The great, searchable resource site for HTML authors is http://www.stars.com. You are also
welcome to read my Infobahn Construction Workshop course notes at http://super.sonic.net/
ann/htmlsmnr.html.
Of course there’s more, but if you try out these commands and make them work, you’ll be at
least halfway along the HTML learning curve. (The form won’t do any action without a CGI
program.)
Netscape will let you open files that are stored on your local hard drive, so you can create
yourfile.html and then just File | Open it.
The most popular way to learn HTML is to study other people’s code. Go to a Web site and
then ask your browser to show you the source code. With the Netscape browser, the command
is View | Source. Of course, you can then copy the HTML to the Clipboard, or even do a File
| Save command to capture the entire document.
If you feel guilty doing this, remember that once you publish something wonderful, other people
will learn from your code in turn.
On the other hand, if you don’t really like the idea of everyone in the world benefiting so quickly
from your labors, read on—the source code behind CGI programs cannot be seen from a
browser.
NOTE
JBServer: http://www.utm.edu:80/~bbradley/zbs/
EMWACS: http://emwac.ed.ac.uk/html/internet_toolchest/https/CONTENTS.HTM
(free)
WebCommander: http://www.flicks.com/
FIGURE 36.3.
Planning ahead can save
you hundreds of dollars in
Internet service fees.
Let’s move past the topic of static pages and discuss how one makes dynamically generated
ones. If you’re surfing the Web and you see a URL that contains the phrase CGI, you can as-
sume that a program will be running to generate the requested HTML document on the fly.
Between the requirements of accepting the input and sending the output, the CGI program
can do just about anything—that’s where it gets interesting. A Delphi-based .EXE can run
queries on local and remote databases, send e-mail and faxes, generate graphics, conduct elec-
tronic transactions over the Internet or other off-the-net connections, run other programs, move
files around, and so on. When we talk about building a Web application, we are really talking
about all these additional useful tasks.
Thus, the capability to run CGI applications on your Web server machine is a prerequisite to
building interactive, database-driven Web sites. When evaluating Internet Service Providers,
you should find out your options on this point. The larger the provider, the less likely that they
will allow you to install and run custom CGI programs.
FIGURE 36.4.
A Web site is a collection of
individual pages, with a
suggested flow-through.
Once you have mapped the flow for a given Web site, you can identify which pages are basi-
cally static, and which ones need to be generated based on runtime information. In the dia-
gram in Figure 36.4, the Preliminary “answer” table, “Full detail,” and “Thanks” pages would
be generated after running a CGI program.
Here’s a quick summary of what each page would need to do:
■ Answer page: Run a query based on the surfer’s input criteria and generate the
HTML to display that result in a convenient format. Make a “hot link” on each
record so that the surfer can “drill down” to the next page. That hot link needs to
communicate the primary key of the desired record. This can be done by putting the
primary key in the URL, for example, /cgi-win/yourapp.exe?123 for item #123.
■ Detail page: Access the CGI Environment Variable called “Query String” to deter-
mine the primary key and then locate the appropriate record. (The Query String
would be “123” in our example.) Then generate the necessary HTML based on the
fields in that record (and perhaps other linked tables).
■ Thanks page: Collect all the inputs from the Feedback page and send them by e-mail
to the site owner, and/or post them to a database for subsequent analysis. Then
generate some simple HTML to say thanks.
If you agree that Delphi is a great development environment, you must then embark on a quest
for a Windows-based environment to use it in. (Yes, I hope it will ship for Linux and the
Macintosh someday.) That means selecting Windows 95 or NT and finding a Web server for
that platform.
NOTE
Bob Denny published one of the first Web servers for the Windows platform, Win-
HTTPD, which ran on Windows 3.1x. It is still available as shareware from http://
www.city.net/win-httpd/. He then went on to write a 32-bit version called WebSite,
which is published by O’Reilly and Associates and available from http://
Website.ora.com/ . Bob’s home computer is generally online with his latest achieve-
ments (and experiments) at http://solo.dc3.com.
Essentially, he stated that instead of using stdin and stdout to stream the data in and out of the
CGI application, one would use a set of files in a temporary directory to hold the information.
There would be a set of two or more files defining the HTTP request, and the CGI program
would be responsible for creating a third output file before terminating.
NOTE
or
/cgi-win/yourprog.exe/hello ( sets the CGIPathInfo to ‘hello’ ).
2. Pass unlimited data from fields on a form (this requires that the user enter the
data, and it requires the use of method=post in the HTML FORM command).
If you are building a link from an overview page to a full-detail page, you could use
technique #1 to pass the unique identifier of the item to the page that needs to display
all the detail for that item. If you are building a scrolling grid, you might want to pass
a command such as grid.next to indicate movement to the next set of records. In both
these examples, only one string is passed to the CGI program. The string is used to
signal the CGI program so that it knows what to do.
HTML forms are used in very different situations. Forms enable data entry, and all the
surfer’s data is passed to the CGI program.
Sometimes you might want to a combination of these two techniques so that you can
post the user’s data and signal the CGI program to do something special.
The temporary input files contain all the CGI environment data as defined on hoohoo, plus
any data submitted by the surfer on an HTML form. (See Listing 36.1.)
Listing 36.1. Sample .INI file showing CGI environment data from a hit on a
Classified Ads site.
[CGI]
Request Protocol=HTTP/1.0
Request Method=POST
Executable Path=/cgi-win/Webhub.exe
Query String=Fair:match:8257.5577
Server Software=WebSite/1.0k
Server Name=super.sonic.net
Server Port=80
Server [email protected]
CGI Version=CGI/1.2 (Win)
Remote Address=134.113.221.87
Referer=http://super.sonic.net/cgi-win/Webhub.exe
Authentication Method=Basic
Authentication Realm=Web Server
Content Type=application/x-www-form-urlencoded
Content Length=47
[System]
GMT Offset=-28800
Debug Mode=No
Output File=c:\temp\b37dws.out
Content File=c:\temp\b37dws.inp
[Form Literal]
Category=Computer Modems
city=
state=
shortzip=
[Accept]
*/*=Yes
image/gif=Yes
continues
[Extra Headers]
User-Agent=Mozilla/1.0N (Windows)
It is also possible to use the / separator instead of ?, and then the data is put into the
PathInfo variable rather than QueryString.
Content Type: This refers to the content of the request; I have not yet seen it vary.
Content Length: In this case, 47 bytes, meaning the length of the strings sent under
[Form Literals], as preserved in the temporary .INP file.
The [System] section is particular to win-cgi, and only the Web site server implements Debug
Mode. The GMT Offset must be used by your CGI program to calculate the Greenwich Mean
Time date, which goes on the response document back to the server. The Output File name
must be used when creating the response document for the surfer.
The [Form Literal] section is particular to win-cgi and itemizes information about <INPUT> fields
on the HTML form that called you (if applicable). All fields, radio buttons, and checkboxes
will be listed. Checkboxes are an exception; they are listed only when checked. Text areas will
show up under [Form Literal] if the surfer typed one line of text, and under [Form External] if
they entered more.
The [Form External] section was not needed in this sample. It itemizes any data that the Web
server does not want to put under [Form Literal], either because it is longer than 255 charac-
ters or because it appears that the data might be binary, not text. Thus even short form inputs
that involve foreign languages generally end up under [Form External].
The [Accept] section enables the browser to tell the CGI program what type of data it can accept.
The [Extra Headers] section can vary, but you will generally be able to look at User-agent and
find out what browser is in use.
Where’s the user name? You don’t generally find out the surfer’s name. In some cases, you will
get their e-mail address. If you request authentication, you will get their name and password.
The preceding apply to the Web site server. The concepts will apply to all servers, but the de-
tails may vary.
FIGURE 36.5.
An input form created
using HTML FORM
syntax.
You can check out the HTML source code with View | Source:
HTML source behind the form
<H2>Search Assistant</H2>
</FORM>
When analyzing an HTML form, look for the <FORM> tag and read the ACTION=part. The
ACTION defines the name of the CGI program that will run.
This would then be followed by whatever HTML was appropriate. Basically, the surfer’s browser
software reacts based on the Prologue and then displays the HTML.
NOTE
The TCGIEnvData component is included on the CD with this book, with source code
so that you can compile it for 16- or 32-bit Delphi. Just install the CGI.PAS file into
your component library.
Using that free component, you can create HTML documents on the fly. The Delphi source
code to do this is quite simple:
procedure TForm1.formCreate( sender : TObject );
begin
with CGIEnvData1 do begin
{ when this program runs under WebSite, parameter #1 will be the
name of the ini file. }
WebSiteINIFilename := paramstr(1);
application.onException := cgiErrorHandler;
application.processMessages; { need to have this ! }
createStdout;
sendPrologue;
send( ‘<HTML><HEAD>’ );
sendTitle( ‘Sample HTML page’ );
send( ‘</HEAD><BODY>’ );
sendHdr( ‘1’, ‘Sample HTML page’ );
send( ‘...created by Delphi CGI Components...<p>’ );
send( ‘Your IP address is: ‘ + CGIRemoteAddress^ );
send( ‘<p>It <b>works</b>!!!’ );
sendHR;
send( ‘Generated on ‘ + Webdate(now) );
send( ‘</BODY></HTML>’ );
closeStdout;
end;
closeApp( application );
end;
The Send() commands send a string to the correct output file; the rest is just initialization and
shutdown.
Once this program is compiled, I can invoke it from my browser using http://127.0.0.1/cgi-
win/demo1.exe , and, yes, it does work! You can see some of the sites built by Delphi developers
around the world by going to the “Hall of Fame” at http://super.sonic.net/ann/delphi/
cgicomp/hallfame.html.
What I found in building numerous Web applications with my components is that the larger
and more popular the site, the less useful TCGIEnvData was. In this next section, I’ll share the
main challenges of Web site development, and the solutions.
Performance
The first challenge is really easy to understand: performance. If you have a 450,000-byte .EXE
that needs to load and unload in response to each request, and maybe on top of that it needs to
initialize IDAPI to use a database, it will take many seconds per execution, even on a dedicated
Pentium 120 with 32 MB of RAM.
We really need a way to keep the program code in memory (a .DLL? an .EXE?) so that it doesn’t
need to reload each time. This would also solve the problem of keeping IDAPI initialized.
It does, however, create the problem of how to track “multiple threads” in that program.
Tracking Sessions
This is tied in with saving state—you need a way to uniquely identify each surfer and to store
each surfer’s data in a unique, structured, quickly accessible place. You want to do this in a
non-invasive way (that is, without requiring login), and in a way that works regardless of whether
the user comes in the “front door” or saves a bookmark to an unexpected page deeper in the
flow chart.
Saving State
The second challenge is that of “saving state.”
In general, in the world of CGI, each invocation of the executable program is a separate in-
stance, with no knowledge of the surfer and the surfer’s previous actions on the site.
As you saw from the [Form Literals] section of the .INI file in the previous section, it is pos-
sible to pass variables from any one page to the CGI program that runs directly off that page.
As you build larger sites, it will become necessary to have “global” variables that are available
across many pages. Furthermore, it will be necessary to attribute that data to a given surfer.
In general, you need to “save state” whenever you need to carry variables from page n to some-
where other that page n + 1. (That is, going from page 1 to 2 is easy, but from 1 to 3 is not.) If
you make a flowchart for your application, you should be able to quickly identify these
situations.
Saving state lets you differentiate surfers and accumulate variable data about each one. Once
you build in the generic ability to save state, you’ll find lots of uses for it! Figure 36.6 provides
a reminder that each page is processed separately, and that the “distance” between where you
get the data to where you use it is crucial.
FIGURE 36.6.
You need to save state when
there are one or more pages
between “get” and “use.”
■ User preferences: Perhaps you have a page where the surfer can set some preferences
for the site, such as graphics file type (.GIF, .JPG, .PNG), use of wallpaper, font size,
HTML table style, and so on. In this case, you would want to collect all that data on
one page, but make use of it on all other generated pages.
■ Latent query results: Perhaps on page one the surfer tells you about a general product
category they are interested in and a price range. To help them narrow the search, the
next screen gives them a table of possible product subcategories. After selecting a
subcategory, they are then presented with the specific products that fit the price range
entered on page 1. Here again, you need to remember information from page 1 and
carry it through to page 3.
■ Shopping cart: Figure 36.7 shows a flowchart for a Web site that allows order entry.
As the surfer identifies likely items for purchase, the CGI program accumulates an
order list and then makes that full list available on a checkout page.
FIGURE 36.7.
This cyberstore clearly
requires “saving state.”
Typically the way that CGI applications save state is to assign each user a numeric ID and then
pass that fact around the system through the URL. You can see this in action at many sites on
the Net, including Virtual Vineyards (http://www.virtualvin.com/) and our Fish Store site at
http://www.href.com/fish/ .
I should emphasize that it’s not enough to uniquely number each surfer; you also need a con-
venient way to track that surfer’s data.
An excellent goal is to separate the tasks of the “HTML artist” (who deals with HTML, page
layout, and graphics) from those of the “Delphi programmer” who should have the luxury of
focusing on things that can’t be done in HTML.
Let’s add to this the requirement that these “pieces” (we ended up calling them “chunks”) be
reusable across pages and across Web applications.
This will also lend some structure to the programmer’s dialog with the artist and the end-
customer, which is why component basics are so important. People will be able to say “give me
a grid with a default page height of 10, these two display sets, and these three index orders.”
The component structure provides a feature-set structure.
Business Rules
Once we get past all the annoyances of conforming to the CGI rules, we find ourselves facing
fundamental issues that apply to any database-oriented application.
If you are building an application with some tables that are not read-only, the question arises,
where should I define the “business rules?”
A simple, common example of such an application on the Web would be a “guest book” or
“mailing list sign-up” page. Where do you define the required fields? How do you implement
that requirement? (Many of you have probably built systems with much more sophisticated
rules; the implementation suggestion here should still apply.)
With Delphi, you can define and implement the business rules in the events of the TTable
object, such as BeforePost. You can make a custom table object for each relevant table, and
then reuse that component in every application that uses that table, and be guaranteed consis-
tent posting. This object-oriented strategy enables corporations to invest once in the Delphi
technology and then reuse that in Web and non-Web applications.
In a client/server situation, for example with Interbase, you might have the code use stored
procedures and triggers to implement certain business rules. You would use a different scheme
for peer-to-peer access. Regardless, developers can maintain the implementation and the un-
derlying assumptions about a system.
NOTE
In all fairness, sending a credit card number from a browser to a Web server when they both
use encryption is more secure than calling PC Connection on your cellular phone. And some
of the e-commerce options don’t advocate putting the actual credit card number on the Internet
(thereby moving the point of vulnerability but not eliminating it).
What we have here are millions of generally honest consumers and merchants who so desper-
ately want to conduct Internet commerce that they are ignoring some painful facts. For
example:
■ If you store credit card numbers on your server, you have to give careful thought to
security. You don’t want to invite hackers to download your collection of 10,000 card
numbers.
■ If you don’t store them on your server, but you want to accept them, you still run a
risk on the channel between the browser and the server, plus you have to figure out a
“secure” way to communicate them to your “off-the-Net” server.
■ If instead of taking credit cards, you adopt a “pin-based” e-commerce strategy, you are
then putting all your trust in that company’s ability to maintain security. You will
need to really read between the lines of their promotional material to find their
vulnerabilities. Ask questions like: Who knows the one-and-only-private password and
what if they were kidnapped? How many programmers worked on the one-and-only
batch-transfer application, and are they still happily employed at the firm? How many
years of security experience do they have? OK, it’s unlikely, but what if someone did
break through that firewall? Would one transaction be jeopardized, a batch, a day’s,
the whole company? How would merchants and consumers be treated fairly in such a
case?
■ Here’s a key point from Nathaniel Borenstein: “If you want it (encyrption keys) to
work, you should only use an encryption mechanism that has a well-designed mecha-
nism for specifying key lifetimes, so that encryption keys have a known and well-
specified lifetime after which they need to be updated. That way, when it is time to
use more bits for stronger encryption, it will be possible to gracefully and coherently
upgrade the whole user base.”
There are also important Big Brother questions such as: exactly who has access to all my finan-
cial transaction data? Systems like DigiCash are appealing because they maintain anonymity.
The implications for world commerce are staggering. On the optimistic side, the Web opens
the playing field and makes global distribution an almost overnight reality.
NOTE
Security, piracy, and electronic commerce on the Internet are clearly an increasing part of our
future. Today’s confusion about encryption will settle as people realize that it’s the virtual equiva-
lent of a lock on the front door and that e-commerce will bequeath us all a new universal
currency. The issues today are rooted in privacy and around “a constitutional right to
anonymity.”
Right now, the framework we’ve discussed can easily serve up Java applets as chunks. More
importantly, it can pass relevant data as parameters to the Java applet, making that key link
between the server-side, surfer-aware application and the client-side, somewhat disconnected,
Java applet. Listing 36.3 shows an HTML document with an embedded Java applet. Remem-
ber that the parameter values can be dynamically filled in by the CGI application.
Listing 36.3. HTML document with Java applet (contributed by Dan Dumbrill).
<title>Test For Ann</title>
<hr>
<applet code=”TestForAnn.class” width=450 height=50>
<param name=text value=”Fresh from the Fish Store”>
</applet>
<p>
<applet code=”Receipt.class” width=450 height=600>
<param name=Parameter1 value=”Guppy 2.00">
<param name=Parameter2 value=”Sunfish 3.00">
<param name=Parameter3 value=”---------------------------”>
<param name=Parameter4 value=”Total 3.00">
</applet>
Summary
In this chapter, I’ve introduced the main concepts of Web site design and CGI programming—
analyzing Web site requirements, the rules of CGI itself, the real need for a comprehensive
framework, and how Java fits in as a surfer-side enhancement. The next chapter focuses on
specific Web-application features and how to implement them with Delphi.
The previous chapter covered some general, theoretical aspects of building CGI programs for
the World Wide Web. This chapter looks at what it really takes to build Web applications
with Delphi and WebHub. This is a practical, do-it-yourself chapter. It’s okay if it takes you
more than an hour to read it and then experiment with the ideas presented here. You might
find yourself in a new phase of your career.
NOTE
Where’s the free code? To follow these examples, you will need a copy of the (free)
WebHub System and Trial Components, which are available for download on the
Web at http://www.href.com/. We typically ship a new release at least once a month,
and the WebHub system has an expiration date (just to keep everyone honest).
Therefore the components are not on the CD that accompanies this book.
The source code (.PAS) files are on the CD, although for best results, just use the
sample files that accompany the set of components you download from our Web site.
That way you’ll have the latest and greatest files.
I thought long and hard about how to provide these files to you, and in the end it was
clear that offering you state-of-the-art files was better than the short-term convenience
of having them on the CD.
NOTE
If you’d like to build “good neighbor” components, download Michael Ax’s TPack and
check out the TComponentExtension class.
ftp://ftp.sonic.net/pub/users/ann/tpack16s.zip
http://www.href.com/pascal/tshapes.html
Just Do It!
Some components have a simple job that they are expected to “execute.” I call these “action
components” and expect them to be able to do their job in the Delphi IDE as well as at runtime.
If the job is sending e-mail, you’ll want to be able to test that without compiling. If the job is
creating a page of .html output, you’ll want to see that, too.
Principles that are central to the extensibility of the WebHub development environment are
(a) action components inside your Web application can be executed by “the hub” simply by
“calling their name,” and (b) if you create components descended from TtpAction, those com-
ponents will be similarly cooperative and effective in the neighborhood. Delphi Run-Time Type
Information (RTTI) makes this possible.
TWebCommandLine
You’ll need a connection out to the CGI command line, and this is accomplished by dropping
in the TWebCommandLine component. You don’t have to change any default settings.
FIGURE 37.1.
TWebApps and their
descendants know how to
create the components they
depend on.
TWebHtmlMemo
This component is optional, but extremely useful during development. You can drop in this
descendant of TMemo and connect it to the TWebAppOutput component. (Specifically, set
the TWebAppOutput.showOutput property to True.) Later, when you test pages, the generated
HTML will display in the memo field for your review. This will work in the Delphi IDE and
at runtime. We advise deleting this component when you are ready to go into production mode.
TWebMenu
If you’d like a default menu for your application that will surface all the components’ property
editors and verbs, making them available for runtime customization—drop in a TWebApp
menu component. It will survey the neighborhood and create a complete menu system for you
in just a few seconds.
Right-click the TWebPage object and select Test. Figure 37.2 shows how the generated HTML
for that page will appear in the memo field. (You can also ask that the output be displayed in
your Netscape browser using DDE.)
FIGURE 37.2.
You can test page output
within the Delphi IDE.
You can now compile your project to an .EXE. Invoke that from a browser (assuming you have
an appropriate Web server installed, plus the WebHub system files), using this request:
http://127.0.0.1/cgi-win/Webhub.exe?fishapp:about
NOTE
127.0.0.1 is an IP address that you can use to loop back onto your own machine
(which is perfect when the Web server is installed there). You can substitute your real
IP address, or a domain name, depending on your situation. With WebSite server, you
can also use http://localhost/.
NOTE
What do I mean by “quite easy”? The surfer’s criteria will be in the Literal[] property
of the TWebApp component. For example, if you have an input field called
MinimumFishLength, then TWebApp.Literal[‘MinimumFishLength’] would return
the surfer’s input.
You would then build the TQuery.SQL based on that value, for example:
with query1.sql do begin
clear;
add( ‘select * from “biolife.db” ‘);
add( ‘WHERE (“biolife.db”.”Length (cm)” >= ‘ );
add( WebAppCustom.Literal[‘MinimumFishLength’] + ‘) ‘ );
end;
The HTML 3.0 specification includes “Tables,” which are basically self-sizing grids; see Fig-
ure 37.3.
FIGURE 37.3.
HTML table syntax can
easily accommodate a
database table.
In fact, HTML table syntax allows for cells that span multiple columns or rows, and even grids-
within-grids, as shown in Figure 37.4.
The HTML to create a table is not that hard, but it’s repetitive and bothersome to proof. Here’s
the HTML for the list of cars above:
<TABLE BORDER><TR><TH>Short Description<TH>Contact<TH>List Price<TH>City
➥<TH>State</TH></TR>
TTable.Name TableBiolife
TTable.TableName biolife.db
TDataSource.Name DataSourceBiolife
TWebDataSource.Name WebDataSourceBiolife
FIGURE 37.4.
Scroll buttons can be added
to any grid by setting
properties of the
TWebDataGrid object.
The WebHub system takes care of scrolling the table, allowing for multiple users on the same
table. (The TWebSession component encapsulates a camera snapshot of each surfer’s unique
data, including such things as their position in the table.)
The DisplaySet, which one enters on the TWebDataSource component, defines the columns
to be displayed and their order. The column labels were customized by setting the DisplayLabel
property of the fields, in the TTable’s AfterOpen event handler:
procedure TForm1.TableBiolifeAfterOpen(DataSet: TDataset);
begin
Drilling Down
Many, many database applications have a user interface where one proceeds through a series of
queries, narrowing the search or “drilling down” through the data.
On the Web, you can create such a user interface by making hot links inside your table. In
Figure 37.5, you can see the underlined <A HREF> links. This is done in two steps—identifying
the hot field and customizing the event handler that defines the link.
FIGURE 37.5.
Table with underlined
links.
Hot fields are identified by setting the Tag property of the TField to a constant:
procedure TForm1.TableBiolifeAfterOpen(DataSet: TDataset);
begin
with TTable(DataSet) do begin
fieldByName( ‘Common_Name’ ).tag := HotFieldTag;
end;
end;
The second step is customizing the OnHotField event handler of the TWebDataGrid compo-
nent. It’s helpful to keep in mind the HTML that you are trying to generate.
Hyperlink syntax: <A HREF=”URL”>visiblePhrase</A>
var
desc : string;
id:String;
begin
{note that fields[0] might not be the primary key due to display set changes}
id:=aField.DataSet.fieldByName( ‘SpeciesNo’ ).asString;
desc := aField.asString;
{ Here’s the most important line to understand. This will expand to:
<A HREF=”/cgi-win/Webhub.exe?fishapp:detail:####:ItemID”>fish name</A>
See the Webhub.hlp file for more info on the %=macros=%. }
s := ‘<TD><A HREF=”%=cgiApp=%:detail:%=session=%:’
+ id + ‘“>’ + desc + ‘</A>’;
end;
This event will be called for each row in the dataset, and for each hot field within that row. All
you have to do is make sure that variable s is set to some valid HTML (which will lead the
surfer to the next layer of your database/user interface).
Further Customizations
There are additional event handlers available: OnHotLabel, OnRowBegin, and OnRowEnd
so that you can create all sorts of special effects within the basic grid template.
If you turn on the ControlsWhere property of the TWebDataGrid, you enable the surfer to set
the grid page height, displayset, and indexorder. This is all handled by the WebHub system. If
you want to customize that user interface, you can create variations yourself. The DropDown
properties for DisplaySet and IndexOrder were surfaced to make this easy for you.
The DisplaySetDropDown property, for example, is basically a string list with this HTML con-
tent:
<b>Display Set</b>: <SELECT NAME=”gf.DisplaySet”>
<OPTION>ShortList
</SELECT>
You can access that HTML code from a “chunk” outside the Delphi application, thereby giv-
ing the user the ability to choose a DisplaySet. (Usually you’d have more than one defined!)
The same ideas apply for IndexOrder (which gives the surfer a choice of secondary index).
FIGURE 37.6.
Grids with checkboxes
empower the surfer to order
multiple items with
minimal effort.
The biggest difficulty with using check boxes is that according to CGI rules, you only receive
information about a check box when it is checked—there is no definitive False provided. This
creates a bit of a mess when you want to save state—how do you differentiate “surfer hasn’t
answered this yet” from “surfer said no” when there is no False provided? In particular, how do
you answer this about check boxes on a page other than the current one? (The current one is
easy; you look in the .INI file and if the item is not found, of course it’s False. But for noncur-
rent pages, you lose your reference guide.) The WebHub system compensates for this problem
by keeping a list of “pending” checkboxes when a page is generated.
If you use the CKB command in your form action URL, it will then set all the pending items
definitively to True or False. An example URL might look like this:
<A HREF=”%=cgiApp=%:myPage:%=session=%:CKB”>click me and remember</A>.
You also need to use the macro-on prefix to tell WebHub to pay attention, that is:
%=<INPUT TYPE=CHECKBOX NAME=SendInfo VALUE=YES> Send information
Check box values are accumulated in the Checked property of the TWebSession. When they
are used in conjunction with a grid as shown above, you can then perform special processing
on the “checked rows.” Specifically, you can use a TWebDataGrid and set its ScanMode prop-
erty to dsByChecked—and then when the grid is generated, only checked rows will be included.
Leveraging Delphi
This section explores the question, “What can Delphi and WebHub do together that other
CGI frameworks cannot?” This is a key question, because you will have people trying to sell
you a wide variety of “solutions” for building database-driven Web sites. Generally speaking,
those other frameworks will be limited by the very nature of the programming environment.
Often, all they can do is SQL.
Delphi can do SQL because that’s part of the TQuery component. And Delphi can do much,
much more—some of which is useful for Web applications. When you look at the Delphi
palettes and wonder how much of that terrific technology can be used for a Web application,
you begin to realize that most of it doesn’t apply, because it’s there to create a user interface
under Windows. Web applications have a minimal user interface; they are meant to run
unattended on the Web server! So we have minimal use for edit boxes, radio boxes, gauges,
and so on.
Nonetheless, the component-based nature of Delphi makes it ideal for taking core functional-
ity and extending it to other areas. This section will look at four such areas: string grids, e-mail,
outlines, and graphics.
String Grid
Sometimes using a database table to store tabular data is overkill. This is especially true when
the data rarely changes and is read-only from the surfer’s perspective.
Figure 37.7 shows an HTML table that was generated from a TStringList, rather than from a
TTable. Each string represents a row in the grid. The individual strings are formatted to indi-
cate which items are headers (and thus should be sent with <TH> instead of <TD>, and appear
bold in the figure) versus which items are regular cells. If you look at the sample .TXT file that
is loaded into the string list, you’ll see that ~ identifies headers, and | separates cells:
FIGURE 37.7.
Behind the scenes, this table
comes from a TStringList.
How do you code one of these? Well, it requires zero Pascal code! You do have to configure a
few things:
■ Drop a TWebListGrid component onto your form.
■ Create the necessary text file that contains the table data (for example, fish weight info
in preceding paragraph).
■ Define the DisplaySet list of text filenames (this applies if you have multiple sets of
data, for example, shipping by pound versus shipping by ounce).
■ Set up a page that calls the grid (just invoke it by name using WebHub macro
syntax—for example, %=WebListGrid1=%).
As with the TWebDataGrid, the DisplaySet controls are handled automatically by WebHub.
You can turn them off and further customize the user interface if you want.
Electronic Mail
Thinking of the Internet brings electronic mail to mind pretty easily. Electronic communica-
tion underlies much of the technology and culture of the Net.
There are now several sets of components that will help you send e-mail from within a Delphi
application. You can check out the components from StarTech at http://www.neosoft.com/
~startech/delphi/delphi.htm, or Silverwave at http://rampages.onramp.net/~silver/, or our
software at http://www.href.com/. There will probably be even more choices by the time you
read this book.
Once you have your e-mail component(s) ready, sending electronic mail from a Web applica-
tion is relatively easy. Here I’ll show you how it can be done with our suite of three compo-
nents: TWebSock, TWebMail, and TWebMailForm.
The context of this will be the extremely common goal of assembling all the inputs from an
HTML form and e-mailing those values to the owner of the Web site. This is probably the
most popular CGI program ever written, because it opens up so many possibilities for the Web
site owner. You can use it for feedback pages, trivia contests, order entry, surveys, and more.
Figure 37.8 shows the Fish Store’s sample feedback page.
The exact goal here is to e-mail the contents of the three fields: first name, e-mail address, and
message, to [email protected].
When Suzie clicks the [send] button, she “submits” her query to the Web server, which in turns
passes the information on to the CGI program. If the CGI program was written with WebHub,
all her data is automatically stored in the string list property, FormLiterals, of the TWebApp object.
FIGURE 37.8.
Form-to-mail programs are
extremely useful and
common.
There are four general steps involved in setting this up within a WebHub application:
■ Use four components.
■ Initialize the unchanging e-mail settings.
■ Define the variable e-mail setting (that is, the surfer’s address, upon request).
■ Execute the component so that mail is sent.
with WebMailForm1 do
WebMailInclude:=[mfvFormLiterals]; {email all form input vars}
end;
Basically, the preceding code defines the mail server’s hostname (which you find out from your
Internet Service Provider), the port number (usually 25), plus the message sender, recipient,
and subject. These values could also come from an .INI file, or from something in the runtime
environment.
The TWebMailForm component has a property called WebMailInclude, which lets you toggle
on various items for inclusion in the e-mail. Only one category is included here, the mfvLiterals.
If this e-mail were going anywhere important, it would also be necessary to set the TextAreas
property to FBAREA (the name of the HTML text area on the form).
All the setup is now complete. The next task is to customize the OnSection event handler for
the fb (feedback) TWebPage object. This event handler is called whenever a new page section
is sent to the surfer. Section 1 will be the page header, and this is a perfectly good time to finish
setting up the message.
procedure TForm1.FishApp_FBPSection(Sender: TObject; Section: Integer;
var Chunk, Options: OpenString);
var
surferEMail:string[128];
begin
if section>1 then exit;
surferEMail:=WebAppFishApp.Literal[‘email’];
if surferEMail=’’ then begin
with WebAppFishApp.WebOutput do begin
Send( ‘You have to enter your email address!<p>’ );
Close;
end;
exit;
end;
else begin
Webmail1.Sender.EMail:=surferEMail;
end;
end;
Most of the preceding code is just error-checking! The surfer’s e-mail address is easily available
as WebAppFishApp.Literal[‘email’]. After determining that it is not blank, the following one
line is all that’s required to finish the e-mail settings:
Webmail1.Sender.EMail:=surferEMail;
The TWebMailForm component will do all the hard work of assembling the rest of the field
inputs and creating the actual e-mail message.
You might be wondering what actually sends the message. The TWebMailForm component is
invoked by name from the fbp (feedback post) chunk as follows:
%=pageBegin=%fbp=FeedBack Post%=endInfo=%
%=Webmailform1=%
<h2>Thanks!</h2>
Thank you for the feedback, %=Firstname=%. Check your
email inbox — you should have a reply there soon.
TWebMailForm is an action component, and calling its name runs its Execute method.
If this were a survey page, or a mailing list signup page, rather than “feedback,” it would be
nice to additionally post the user’s data to a table for later analysis. You can do this with stan-
dard Delphi techniques; insert a record into a TTable that you set up for this purpose.
Interactive Outlines
One of the issues that we all struggle with is information overload. This is true in the context
of Web sites as well. Sites can have hundreds or thousands of pages, and surfers need some sort
of navigation tool to help them both “see the forest for the trees” and “find that one special
tree.”
Various user interface solutions present themselves. Many sites use clickable image maps to let
the surfer jump to a given area, but this interface breaks down as you increase the number of
important sections. Other sites have a search engine, so that surfers can find items by keyword.
That is very helpful, especially if the search is quick and reliable.
Another approach is to provide an outline of the site’s pages. Delphi provides at least half the
solution here with its TOutline component. This component knows how to manipulate strings,
tracking indentation levels and the idea of expanded and collapsed nodes.
The TWebOutline component extends that functionality so that it’s easy to generate HTML
from the outline strings, and so that the outline is usable from the Web site. Figure 37.9 shows
an example of this from the Table of Contents page at http://www.href.com/.
The icons are clickable, and they are set up to expand/collapse nodes so that the surfer can
explore the outline interactively.
To implement this in your Web application, you need to load the strings into a Delphi TOutline
component and drop in a TWebOutline component. Then invoke the TWebOutline compo-
nent by name, from a chunk, exactly as you did with e-mail before.
The important point here is that a Delphi Web application can do much more than run SQL
queries and generate grid responses! Furthermore, these features are easy to implement because
Delphi gives you components and a terrific IDE. Compare this to programming in Perl, or C,
or Visual Basic, and you’ll see that there is really no comparison. Delphi is in its own league.
FIGURE 37.9.
TWebOutline brings
Delphi’s TOutline to
the Web.
Custom Graphics
Graphics always make a good grand finale. There are so many fun features you can put in once
you realize that you can build custom graphics on a Web site. This example is fairly simple:
■ Accept some text
■ Customize the text color and font and the frame
■ Generate the bitmap
■ Convert that to a .gif file
■ Display the .gif for the surfer
Delphi takes care of the first three steps. The TWebPicture component includes .bmp to .gif
conversion, and HTML makes it easy to display the .gif.
Figure 37.10 shows the HTML form that prompts the surfer for the parts of the graphic to be
customized. (There is no other data input mechanism available.)
When the surfer clicks the [send] button, the request goes through the Web server and on to
our Web application. We intervene in the onSection event for the page object:
procedure TForm1.FishApp_DRPSection(Sender: TObject; Section: Integer;
var Chunk, Options: OpenString);
var
aColor:String[128];
shortFile:String[128];
begin
if Section<>3 then exit;
with WebLogo1, WebAppFishApp do begin
Caption:=Literal[‘Caption’];
Font.Name:=Literal[‘Font’];
aColor:=Literal[‘BrushColor’];
if aColor=’White’ then
Font.Color:=clWhite
else if aColor=’Maroon’ then
Font.Color:=clMaroon
else if aColor=’Green’ then
Font.Color:=clGreen;
aColor:=Literal[‘PenColor’];
if aColor=’Red’ then
Pen.Color:=clRed
else if aColor=’Teal’ then
Pen.Color:=clTeal
else if aColor=’Silver’ then
Pen.Color:=clSilver;
FrameWidth:=StrToIntDef(Literal[‘FrameWidth’],5);
shortFile:=getNextGifFilename;
filename:=WebAppFishApp.pathForTempGifs + shortFile;
Webpicture1.execute;
WebOutput.Send( ‘<IMG SRC=%=ImageDir=%’ + shortFile + ‘><P>’ );
end;
end;
FIGURE 37.10.
Delphi can create images
on the fly; why not display
them over the Web?
Most of the preceding code just sets the indicated properties of the TWebPicture component.
The .execute line makes the component create the bitmap and .gif file, and the .Send line
writes the necessary HTML out to the surfer. The result is shown in Figure 37.11.
FIGURE 37.11.
This is the output from the
TWebPicture component.
High-Traffic Issues
For sites that want to serve hundreds of surfers a minute rather than dozens per hour, addi-
tional care must be given to the overall Web application architecture. The WebHub frame-
work enables you to dedicate separate .exes to pages that require special attention, and it lets
you run those exe’s on separate machines (connected by TCP/IP) for distributed processing.
Summary
It is my sincere hope that these two chapters have broken through some of the mystique that
surrounds Web application building, transforming it into a domain that you feel comfortable
exploring. I have had more fun programming Web applications with Delphi than using any
other application framework to date.
See you on the Net.…
INDEX
I
SYMBOLS
+ plus operators (pointer arithmetic),
128-129
- minus operator (pointer arithmetic),
128-129
16-bit environment (Windows API
compatibility), 331-335
4GB virtual memory address space,
250-251
A
Access program (Listing 34.1),
1231-1232
access violations, 113-118
Access2 program, 1237-1239
acInternal servers, 1028
acMultiInstance server, 1028
acSingle server, 1028
ADDRESS2 program
Cats dialog box (Listing 18.3), 627
clipboard, 654
Colors menu, 644-649
data definition, 605-651
data search, 635-637
data sort, 633-635
Borland Pascal, see Object passing data to, 1338 Clock component code
Pascal performance issues, 1344 Listing 26.6, 924-927
break points (setting in Prologues, 1343 Listing 26.7, 927-928
stand-alone returning HTML as tables, CLSASDEF unit (OBJECT4
debugger), 33 1359-1364 program), 882-887
breakouts (Escape), 1140 sample application, CMPLIB32.DCL file
browsers (overview), 1356-1358 adding to environment,
1328-1330 saving state, 1345-1346 15-21
business rules (CGI separating programmer/ removing from environ-
programs), 1347 layout tasks, 1346-1347 ment, 14-15
buttons specification, 1338-1340 codes
Recalc (code for, Listing string grids, 1365-1366 in ADDRESS2 (detailed
21.2), 748 TCGIEnvData compo- review), 616-630
Start (code for, Listing nent, 1343-1344 comment update, 19
21.2), 747-748 tracking user sessions, Component Expert, 910
1345 for in-process servers,
CGI (Common Gateway 1061-1065
C Interface), 1330 in IMyClassFactory,
Char arrays, 66-72, 120-126 1068-1070
cached updates, 664-670
check boxes, 465-467, in IMyObject, 1068-1070
CacheUp program, 665
486-487, 1363-1364 in IUnknown, 1070
Calculated Fields (Fields
ChilPar program, poly- troubleshooting, 19
Editor), 583-585
morphism example color
callbacks (in FontsExp
(Listing 25.1), 870-871 setting for ADDRESS2
program), 476-479
CLASSDEF unit, program, 644-649
CallMethodPtrClick
OBJECT3 program TColorClock components,
program (Listing 23.4),
(Listing 24.2), 838-841 931
822-823
classes COM (Component Object
CD-ROMs
of exceptions, 168-170 Model), 1042-1043, 1133
AUTHOR.DB table, 597
TDDSprite, 1162-1172 SIMPOBJ.DLL file
BOOK.DB table, 597
client/server programming (Listing 30.4), 1052-
Lookup program, 595-597
queries, 538 1053
QLINKS program,
Client1 program MYOBJ.PAS program
creating, 554
batch file (Listing 28.8), (Listing 30.5), 1053-
Sounder program,
997 1057
1254-1255
Listing 28.4, 994-996 registry, 1057-1061
CGI
CLIENT1.RES RC file command line
database business rules,
(Listing 28.7), 997 compiling from
1347
clients DCC32.EXE, 1001
e-mail components, 1366-
DDE, 989-991, 991-1003 re-review, 328-331
1367, 1367-1369
OLE automation sample, command-message interface
frameworks, 1347-1348
1008-1009 (MCI), 1260-1261
HTML forms, 1342-1343
clipboard (for ADDRESS2 comment code update, 19
HTML tables, “drilling
program), 650-651 commercial Web sites,
down”, 1362-1363
CLOCK component, security, 1348-1350
.INI files, 1340-1341
923-933
DrawSurfaces program entry points (threads), 206 EXE (local) servers, creating,
calling (Listing 32.14), error handling, see excep- 1089
1145 tions ExecSQL statement versus
simplified program (Listing Escape breakouts, 1140 Open statement, 560-562
32.19), 1151 event handlers executing CGI programs,
Dreaded Sorts program creating, 958-961 1343
(Listing 7.9), 239-242 OnHotField, 1362 Explorer (databases),
“drilling down” through OnXXX, creating, 959-961 accessing, 505-506
HTML tables, 1362-1363 event-oriented program- Extending DOS (book) by
dynamic DLL, loading at ming, 764-771 Ray Duncan, 101
runtime, 416-422 FormCreate (in Music
dynamic methods, 812-826 program), 719
dynamic Web pages, loops, 769-771 F
compared to static, main form for EVENTS1
fast animation (Games
1334-1335 program (Listing 22.1),
SDK), 1174
767-768
fields
EVENTS1 program,
accessing, 513-516
E 767-768
for ADDRESS2 program,
EVENTS2 program
e-mail, 1366-1367, 1367- 609-610
Listing 22.3, 776-783
1369 Calculated Fields (Fields
VKeys unit (Listing 22.4),
EASTSTR program (Listing Editor), 583-585
779-781
3.1), 69-70 filtering with
Excel automation,
EasyFile program (Listing OnFilterRecord event,
1024-1026
3.3), 92-94 524
exceptions
EASYSQL program, sizing for ADDRESS2
allocating memory for,
541-542 program, 607-610
190-194
EASYTEXT.DPR program Fields Editor, 566, 579-585
classes of, 168-170
introduction, 341-344 file formats for multimedia,
creating (Listing 6.2), 176-
Listing 11.1, 342-344 1223-1224
179
EditorClass parameters, 938 FILEITER component
default exception handler,
editors (databases), 566 (Listing 27.1), 943
168
electronic commerce FileIter units, 955
default handler replace-
security, 1348-1350 filenames (date-based),
ment, 188-190
encapsulation, 832-836 76-78
handling
OBJECT3 program files
in DirectDraw,
(Listing 24.1), 838 CMPLIB32.DCL
1136-1137
CLASSDEF unit (Listing adding to environment,
Listing 32.3, 1137
24.2), 838-841 15-21
introduction, 166-168
ENoOle program (Listing removing from environ-
messages, Listing 32.6,
30.3), 1047-1048 ment, 14-15
1140
Entity program (Listing mapping into memory,
rasing (Listing 6.8),
33.5), 1200-1205 267
190-194
Entry module (in marking in ADDRESS2
SIMPEX program,
LINKLST2 program, program, 649-650
171-172
Listing 5.3), 148 memory mapped,
syntax, 170-175
270- 247
3.2 TESTSTR program, 8.1 VirtAll program, 13.12 VCL Fern unit,
85-86 254-257 433-435
3.3 EasyFile program, 8.2 HeapMem program, 13.14 DLL import
92-94 262 statements example, 437
4.1 pointer declaration to 8.3 MemFile program, 14.1 FontsExp phase one,
string, 109 267-269 449-456
4.2 access violation 8.4 MemMgr program, 14.2 FontStr module,
example, 114-115 275 456-458
5.1 PMath program, 9.1 Windows1 program, 14.3 FontsExp program
128-129 283-285 include file, 458
5.2 LINKLST2 program, 9.2 NWndProc program, 14.4 FontsExp program
141-147 304-306 resource file, 458
5.3 LINKLST2 with Entry 10.1 Window2 program, 16.1 Params2 program,
module, 148 312-315 548-549
5.4 POINTER5 applica- 10.2 Include file for 16.2 Params1 program,
tion, 153-154 Window2 program, 315 551-552
6.1 SIMPEX program, 10.3 Resource file for 17.1 MOVEGRID
171-172 Window2 program, program, 586-588
6.2 MyExcept program, 315-316 17.2 VisiForm unit for
176-177 10.4 Win32/Win16 MOVEGRID program,
6.3 RESERROR program, compatible Window2 588-589
180-181 program, 332-334 17.3 Lookup program, 595
6.4 RESMAIN.INC 13.1 Dll test module, 17.4 Lookup program, 596
program, 181 337-342 17.5 Lookup program, 597
6.5 RESMAIN.RC 13.2 DllTest library call, 18.1 source code for
converted to RES, 182 398 ADDRESS2 program,
6.6 try...finally blocks, 13.3 DATEINFO DLL, 616-626
185-186 403 18.2 ADDRESS2 program
6.7 OnExcept program, 13.4 DATEINFO DLL About dialog box, 626
188-189 main module, 403-405 18.3 Cats dialog box in
6.8 CreateExp program, 13.5 Small1 program, ADDRESS2 program,
190-191 409-415 627
7.1 Thread1 program, 13.6 Small2 program, 18.4 Filter dialog box in
204-206 416-418 ADDRESS2 program,
7.2 Thread2 program, 13.7 Windows API DLL, 628
210-213 419-422 18.5 Search dialog box in
7.3 Thread2 program 13.8 Windows API DLL, ADDRESS2 program,
resource file, 213 423-424 628-629
7.4 CritSect program, 13.9 Windows API project 19.1 CacheUp program,
219-222 source. 426-429 665-670
7.6 Thread1a program, 13.10 Pics form window, 19.2 many-to-many
232-233 430-431 relationships (databases),
7.7 TThread misuse 13.11 Squares form 671
example, 234 features TCanvas object), 19.3 Man2Man program,
7.8 TThread correct usage 431-433 671-672
example, 234-235
19.4 Relater form for 22.4 EVENTS2 program 26.1 component descen-
Man2Man, 672-673 VKeys unit, 779-781 dants, 908-909
20.1 main code for Music 23.1 Hierarchy, 811 26.2 TestEds program
program, 699-704 23.2 OBJECT2 program, 26.3 boilerplate output
20.2 Index form for Music 813-814 from Component Expert,
program, 704-705 23.3 FuncPtr program, 910-911
20.3 Artist form for Music 818-819 26.4 UNLEASH unit,
program, 706-707 23.4 CallMethodPtrClick, 915-917
20.4 Album form for 820-822 26.5 UNLEASH unit test
Music program, 707-711 23.5 METHPTR program, bed, 918-923
20.5 Book form for Music 823-824 26.6 Clock component
program, 711-714 23.6 RTTIOPS program, code, 924-927
20.6 New Artist form for 826-828 26.7 Clock component
Music program, 715 24.1 OBJECT3 program, code, 927-928
20.7 Globals form for 838 27.1 FILEITER compo-
Music program, 716 24.2 CLASSDEF unit in nent, 943-944
20.8 Search unit for Music OBJECT3 program, 27.2 ALLDIRS unit,
program, 716-717 838-841 944-950
21.1 macro code to 24.3 PROPTEST 27.3 FindAllW program
highlight database fields, program, 848-849 short code, 950-951
744-745 24.4 PROPTEST 27.4 DelAllDb program
21.2 Recalc button code program, main unit, main unit, 962-967
for report example, 748 849-851 28.1 OleObj1 program,
21.2 Start button code for 25.1 ChildPar program, 978-981
report example, 747 875 28.2 Server1 program, 992
21.4 code to populate 25.2 PolyFlds program, 28.3 Client1 application,
Items property in combo 877 994-996
box, 750 25.3 OBJECT4 project 28.4 MILTON.TXT file,
21.5 code to alter query file, 878 996-997
bind parameter, 751 25.4 OBJECT4 main 28.5 MILTON.TXT RC
21.6 query amendment form, 878-882 file, 997
code, 752 25.5 OBJECT4 program 28.6 CLIENT1.RES RC
21.7 query amendment CLASSDEF unit, file, 997
code, 753 882-887 29.1 VarArray program,
21.8 report example three 25.6 OBJECT4 program 1022-1024
creation, 758 REPORTS unit, 29.2 MyAuto unit (server
21.9 Standalone report 887-888 creation), 1030-1031
creation, 758 25.7 OBJECT4 program 29.3 DPR file for MyAuto
21.10 Dependent report SELLDLGS unit, unit (server creation),
creation, 759 889-890 1032
22.1 main form for 25.8 OBJECT4 program 30.1 DPR file for
EVENTS1 program, STATUS dialog box, DNOOLE.DLL,
767-768 890-892 1044-1045
22.3 EVENTS2 program, 25.9 OBJECT4 program 30.2 DNOOLE.DLL
776-783 Hierarchy dialog box, exported object, 1045
892-893
returning HTML as X
tables, 1359-1364
sample application, XFire program, 1174-1208
1356-1358 bitmap DLL (Listing
saving state, 1345-1346 33.8), 1207
separating programmer/ main unit (Listing 33.1),
layout tasks, 1177-1179
1346-1347 overview, 1208-1209
TCGIEnvData
component,
1343-1344 Z
tracking user sessions, zero-terminated strings,
1345 72-74
CGI specification,
1338-1340
database-driven Web sites,
1335
electronic commerce
security, 1348-1350
HTML (HyperText
Markup Language),
1331-1332
HTML forms, 1342-1343
.INI files for CGI
programs, 1340-1341
interactive Web sites, 1335
Java programming
language, 1350-1351
overview, 1328-1330
planning Web sites,
1336-1338
advantages of Delphi,
1337-1338
outline of pages,
1336-1337
public Web sites compared
to private, 1333
server software, 1333-1334
sites
custom graphics,
1370-1372
high-traffic issues, 1372
user input guidelines,
1369
static Web pages compared
to dynamic, 1334-1335