Overview
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 access specifiers
- 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 BCB treasure
chest. In particular, it 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 re-use. Properties are also cutting-edge tools, and their implementation
in BCB yields some surprising fruits, such as arrays that are indexed on
strings.
Encapsulation
The words encapsulation and object are 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 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:
class TAirplane: public TObject
{
int Altitude;
int Speed;
int NumPassengers;
void TakeOff();
void Land();
void Climb();
void Descend();
};
There is a sleek elegance in this simple object declaration. Its purpose and
the means for implementing its functionality are readily apparent.
Consider the class declaration from the current version of the Object3
program:
class THierarchy: public TMyObject
{
int FTextColor;
int FBackColor;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
THierarchy encapsulates a certain amount of functionality, including
the ShowHierarchy method it inherits from TMyObject. If you
want to call ShowHierarchy, you need to first instantiate an object of
type THierarchy and then use that object as a qualifier when you call
ShowHierarchy:
void main()
{
THierarchy *H = new THierarchy();
H->TextColor = YELLOW;
H->BackColor = BLUE;
H->ShowHierarchy(H);
delete H;
}
This kind of encapsulation is useful primarily because it makes you treat
everything about the THierarchy object as a single unit. There are,
however, other advantages to this system, which become apparent when you examine
the access specifiers used in the class declaration.
BCB defines four keywords meant to aid in the process of encapsulation by
specifying the access levels for the various parts of an object:
- private: Use this directive to declare a section in a
class that can be accessed only from inside the current object. The only way
around this limitation is to declare a class as a friend.
- protected: Code declared in a protected section can be
accessed only by the current object and by descendant objects. The point here
is that protected fields and methods are available primarily 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. Along with the
published section, it is 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. Because published properties
are for use in the Object Inspector, you should not 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.
All the data in your programs should be declared private and should
be accessed only through methods or properties. As a rule, it is a serious
design error to ever give anyone direct access to the data of an object. Giving
other objects or non-OOP routines direct access to data is a sure way to get
into deep trouble when it comes time to maintain or redesign part of a program.
To the degree that it's practical, you might even want the object itself to
access its own data primarily through properties or public routines.
The whole idea that some parts of an object should remain forever concealed
from other programmers is one of the hardest ideas for new OOP programmers to
grasp. In fact, in early versions of Turbo Pascal with Objects, this aspect of
encapsulation was given short shrift. Experience, however, has shown that many
well-constructed objects consist of two parts:
- Data and implementation sections hidden from the programmers who use the
object.
- 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 access 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 essentially modest beings. An object doesn't want to show the world
how it performs some task, and it is especially careful not to 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 have
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 primarily 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
BCB 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.
If you will allow me to mix my metaphors rather egregiously, and to introduce
a fairly strong set of images, I can provide one further way to think about the
internal and public sections of an object. Rather than thinking of the private
parts of an object from a sexual perspective, you can literally think of them as
the internal organs of the object. As such, they should, by definition, never be
exposed directly to the light of day. Instead, they are covered by skin and
bone, which is the public interface of the object.
Our face and hands are the public part of our being, and our internal organs
are covered with skin and bone and never see the light of day. The only reason
ever to expose these organs is if you are in the hospital--that is, during
object maintenance. Furthermore, any object that does expose its private parts
to the light of day is incomplete, a monster from Dr. Moreau's island, where
gruesome experiments roam free.
Simplicity: The Secret of Good Object Design
Before moving on, I want to talk about the importance of creating easy-to-use
interfaces. Notice, for instance, how easy it is to use the TTable,
TDataSource, and TDBGrid objects. These are examples of
well-constructed objects with easy-to-use interfaces. If you take these objects
away, however, and access the functions in the DbiProcs.c directly, you
see that TTable, TDBGrid, and TDataSource conceal a
great deal of complexity. In other words, if you have to call the raw BDE
functions directly to open and view a table, you will find that the process is
both tedious and error-prone. Using TTable, however, is very easy. You
might need to have someone tell you how it works the first time you see it, but
after that, the process of getting hooked up to a table and displaying data to
the user is trivial. All good programmers should strive to make their objects
this easy to use.
In the next few chapters, I discuss component creation. Components are very
useful because they help guide you in the process of creating a simple,
easy-to-use object. If you can drop an object onto a form and hook it up with
just a few clicks in the Object Inspector, you know that you have at least begun
to create a good design. If you drop an object onto a form and then need to mess
around with it for 15 or 20 minutes before you can use it, you know something is
probably wrong. Objects should be easy to use, whether they are placed on the
Component Palette or not.
Simplicity is important because it helps people write error-free programs. If
you have to complete 30 steps to hook up an object, there are 30 opportunities
to make a mistake. If you need to complete only two steps, it is less likely
that an error will occur. It is also easier for people to understand how to
complete two steps, whereas they often can become confused when trying to learn
a 30-step process.
I do not mean to say that the act of creating an object is simple or easy. I
have seen great programmers wrestle for months with the overwhelming minutiae of
creating a complex object that is easy to use. A good programmer doesn't think
the job is done just because the code works. Good code should not only work, but
it should also be easy to use! Conversely, I often suspect that an object is
poorly designed if it is difficult to use. In most cases, my suspicions turn out
to be true, and a complex interface ends up being a wrapper around a buggy and
an ineptly coded object.
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
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 methods are easy to use. The bottom line is
that the private methods should be designed to accomplish 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.
The scope of knowledge programmers must master today is huge. Professionals
are expected to understand the Windows API, OLE, the Internet, graphics, at
least two languages, object design, and database architectures. Some of these
fields, such as Internet programming, break down into many complex parts such as
ISAPI, CGI, WinINet, TCP/IP, electronic mail, HTTP, and FTP. Learning about all
these areas of programming is an enormously complex task. In fact, for all
practical purposes, it is impossible. No one person can know everything
necessary to write most contemporary programs from scratch. As a result, he or
she needs to find easy-to-use objects that encapsulate complexity and make it
usable.
The worst mistake an object creator can make is to insist that the consumer
of the object knows as much about the subject as its creator. It's not enough to
define a set of useful routines that can speed development if you already know a
subject inside and out. Instead, you need to create objects that someone can
use, even if he or she doesn't understand the details of how a particular
subject works.
The trick to good object design, of course, is to simultaneously present an
easy-to-use interface, while also giving experienced programmers access to the
fine points of a particular area. Once again, TTable and TQuery
serve as an example. TTable is an easy-to-use object providing
neophytes easy access to data. At the same time, it allows an expert to
manipulate one-to-many relationships, lookups,
filters, and indices. Add TQuery to the mix, and there are few
limits to what you can do with these objects in terms of manipulating databases.
TMediaPlayer is another example of a component that takes a
relatively complex API and makes it easy to use. It is arguable that this
component could have a bit more depth, but it allows programmers to run
multimedia applications without having to understand mciSendCommand and
its many complex parameters, structures and constants.
The bottom line is simplicity. If you create an object that is as hard to use
as the API it encapsulates, 95 percent of the people who see your object will
consider it useless. The reason programmers create objects is to provide an
easy, bug-free interface to a particular area of programming.
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 important for the top-level objects in your hierarchy.
A Concrete Example
Neither TMyObject nor THierarchy provides much scope for
exploring encapsulation. As a result, it's time to introduce a new class called
TWidget, which is a descendant of TCustomControl and uses a
descendant of THierarchy through aggregation. In Chapter 21,
"Polymorphism," TWidget becomes 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 TPentiumChip,
TPentiumProChip, TVideoBoard, and so on.
NOTE: I suppose one could
literally think of the TWidget descendants as representations of
concrete objects or as abstract representations of these objects. The meaning
I intend is the latter.
This later, more abstract, technique is similar to the process used in
object-oriented databases. Instead of having a table that stores rows of raw
data, the table could instead store objects that represent data. When a new
486 chip rolls off the line, a new T486Chip object is added to the
database to represent this physical object. You could then query the abstract
object and ask it questions: "When were you made? What part of the warehouse
are you stored in? What is your wholesale price? What is your street price?"
Thus, the abstract object would be a computerized representation of something
in the real world.
Creating computerized representations of physical objects is a very common use
of OOP, but it is not the primary reason for the existence of this technology.
The great benefits of OOP are related to the reuse, design, and maintenance of
software. Using OOP to represent real-world objects is only one branch of this
field of knowledge.
Note that object hierarchies always move from the general to the specific:
TWidget
TSiliconChip
TPentiumChip
The underlying logic is simple enough:
- 1. A TWidget could be almost any object that is bought and
sold.
2. A TSiliconChip is some kind of silicon-based entity.
3. A TPentiumChip is a specific kind of computer chip that has
a real-world counterpart.
The movement is from the abstract to the specific. It is almost always a
mistake to embody specific traits of an object in a base class for a hierarchy.
Instead, these early building blocks should be so broad in scope that they can
serve as parents to a wide variety of related objects or tools.
The main reason for this rule might not become apparent to all readers until
they have seen Chapter 21. The condensed explanation, however, looks like this:
- TWidget encapsulates certain basic functionality that applies to
all widgets. For instance, all widgets might keep track of the date they were
created, whereas only certain types of widgets might support the MX
instruction set. Therefore, you can use polymorphism to ask all your widgets
certain basic questions, such as when they were created. Conversely, there are
certain types of questions you can ask only of widgets that are also computer
processors. This system would not work if you did not move from the general to
the specific. In short, you want all widgets to have certain basic
functionality, and then as you move up the hierarchy, you can provide specific
domains of knowledge or functionality to particular types of objects.
The Widget1 program found on the CD that accompanies this book includes a
declaration for class TWidget. (See Listing 20.1.) This declaration
appears in a file called Widgets.h. TWidget descends from the
VCL class called TCustomControl and uses a descendant of THierarchy
through aggregation. As a result, you will need to add both MyObject
and the new Widgets files to your projects.
I have created a new file called Widgets because I am building a new type of
object that is distinct from THierarchy and TMyObject. I
could, of course, have put all the objects in one file, but I thought it made
more sense to separate the different types of objects into different files.
There is no hard-and-fast rule
governing this kind of decision, and you can take whichever course makes
sense to you on a case-by-case basis.
I have also added a new class to TMyObject:
class TListHierarchy: public TMyObject
{
private:
TStringList *FList;
protected:
virtual void PrintString(AnsiString S)
{ FList->Add(S); }
public:
TListHierarchy() { FList = new TStringList(); }
__fastcall virtual ~TListHierarchy() { delete FList; }
TStringList *GetHierarchy(TObject *AnObject)
{ ShowHierarchy(AnObject); return FList; }
};
As you can see, this class stores its information in a TStringList.
You can retrieve the list from the class by using the GetHierarchy
method:
ListBox1->Items = MyWidget->GetHierarchy();
After you make this call, the list box referenced in the code would contain
an object hierarchy.
TWidget and Its Destructor
The Widgets unit is very simple at this point. To create it, I went to the
files menu and chose New Unit. I then saved the file as Widgets.cpp and
edited the header so that it looked like this:
///////////////////////////////////////
// Widgets.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef WidgetsH
#define WidgetsH
#include "myobject.h"
class TWidget: public TCustomControl
{
private:
TListHierarchy *Hierarchy;
public:
virtual __fastcall TWidget(TComponent *AOwner): TCustomControl(AOwner)
{ Hierarchy = new TListHierarchy; }
virtual __fastcall ~TWidget()
{ delete Hierarchy; }
TStringList *GetHierarchy()
{ return Hierarchy->GetHierarchy(this); }
};
#endif
This object uses aggregation to enlist the functionality of the
TListHierarchy object for its own purposes. In particular, it declares the
object as private data and then allocates memory for it in its constructor, and
deallocates memory in its destructor:
virtual __fastcall ~TWidget()
{ delete Hierarchy; }
NOTE: A destructor is declared
by writing the name of the object with a tilde prefaced to it: ~TWidget.
You rarely have reason to call a destructor explicitly. Instead, a destructor
is called automatically when you use the delete operator on the
entire object, when you call Free, or when a local object goes out of scope:
delete MyWidget; // Automatically calls the
destructor
Destructors exist so you can have a place to deallocate any memory
associated with an object, or to perform any other cleanup chores. The
destructor exists for your convenience and serves no other purpose than to
give you a chance to clean up before your object shuts down.
All VCL objects inherit a virtual destructor declared in TObject. If
you create a destructor for one of your own VCL objects, it must have the same
signature used by the destructor in the TWidget object. That is, it
must be declared as virtual __fastcall. This is because of the
precedent set by the destructor found in TObject.
The GetHierarchy method of TWidget returns the result of a
call to the Hierarchy::GetHierarchy method:
TStringList *GetHierarchy()
{ return Hierarchy->GetHierarchy(this); }
As you can see, this function hardcodes a reference to the TWidget
object into this call. You therefore can't use this version of the Widget
object to get the hierarchy of another object, but only for retrieving its own
hierarchy.
To make sure you understand how I got started with this new series of files,
I have created a very simple program called Widget1 that contains the
Widgets unit and a main program that uses it. The code for the program is
available on this book's CD, and the output from the program is shown in Figure
20.1.
FIGURE 20.1. The output from the Widget1
program found on the CD that accompanies this book.
The main program contains one procedure that looks like this:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject
*Sender)
{
Memo1->Clear();
TWidget *W = new TWidget(Memo1);
Memo1->Lines = W->GetHierarchy();
delete W;
}
If you have questions on how to create this program, go to the Widget1
directory and study the example provided there. After you are set up, you can
read the note below and then copy the Widgets.cpp and Widgets.h
files to the Utils directory, where CodeBox is kept. This file
is used in Widgets2, which is described in the next section of this
chapter.
NOTE: Actually, you might want
to be careful copying the files. If you are following along by creating copies
of the files by hand, then go ahead and copy the files. If you are working
from my source, be careful that you don't overwrite my copy of the file when
you move the file to the Utils directory.
Working with Widgets
In the last section, you got started with the TWidget object. In the
Widget2 program, shown in Listings 20.1 through 20.5, I add data and methods to
the object so it can serve as the base class for an object that represents some
kind of widget such as a computer chip, a light bulb, a book, or whatever.
Because the object could have such a wide range of uses, I keep it very
abstract, giving it only a few traits such as a cost, a creation time, the
ability to draw itself, and the ability to stream itself.
-
NOTE: You will find that the
Widgets module gets rewritten several times over the course of the next few
chapters. As a result, the version of this unit used in the Widget2 program is
stored in the Widgets2 directory on the CD that accompanies this
book. There is a second version of this unit, stored in the Utils
subdirectory, that contains the final version of Widgets.cpp.
If you are working along with this text, creating your own version of the
program, it might be best to copy Widgets.cpp out to the Utils
directory and update it little by little as the next few chapters unfold. If
your efforts get fouled for one reason or another, you can always revert to
the versions stored on the CD.
Listing 20.1. The Main unit for the Widget2 program.
///////////////////////////////////////
// Main.cpp
// Learning about objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "widgets.h"
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::CreateBtnClick(TObject *Sender)
{
TWidget * Widget= new TWidget(this);
Widget->Left = ClientWidth / 4 - Widget->Width / 2;
Widget->Top = (ClientHeight / 2) + Widget->Height;
Widget->Parent = this;
Widget->Cost = 3.3;
Widget->Description = "This is a widget";
Widget->TimeCreated = Now();
Memo1->Lines = Widget->GetHierarchy();
Widget->Show();
Edit1->Text = Widget->TimeCreated;
WriteWidgetToStream("Afile.dat", Widget);
}
void __fastcall TForm1::ReadFromStreamBtnClick(TObject *Sender)
{
TWidget *Widget = ReadWidgetFromStream("AFile.dat");
Widget->Parent = this;
Memo1->Lines = Widget->GetHierarchy();
Widget->Show();
Edit1->Text = Widget->TimeCreated;
}
void __fastcall TForm1::FormResize(TObject *Sender)
{
Memo1->Left = ClientWidth / 2;
}
Listing 20.2. The core of the Widget2 program is the
Widgets unit. The header for that module is shown here.
///////////////////////////////////////
// Widgets.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef WidgetsH
#define WidgetsH
#include "myobject.h"
class __declspec(delphiclass) TWidget;
namespace Widgets
{
void __fastcall Register();
}
TWidget *ReadWidgetFromStream(AnsiString StreamName);
void WriteWidgetToStream(AnsiString StreamName, TWidget *Widget);
class TWidget: public TCustomControl
{
private:
TListHierarchy *Hierarchy;
Currency FCost;
TDateTime FTimeCreated;
AnsiString FDescription;
void __fastcall SetTimeCreated(AnsiString S);
AnsiString __fastcall GetTimeCreated();
protected:
virtual void __fastcall Paint(void);
public:
__fastcall virtual TWidget(TComponent *AOwner): TCustomControl(AOwner)
{ Hierarchy = new TListHierarchy(); Width = 25; Height = 25; }
__fastcall virtual TWidget(TComponent *AOwner, int ACol, int ARow);
__fastcall virtual ~TWidget() { delete Hierarchy; }
virtual AnsiString GetName() { return "Widgets"; }
TStringList *GetHierarchy() { return Hierarchy->GetHierarchy(this); }
void __fastcall WidgetMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
__published:
__property Currency Cost={read=FCost, write=FCost};
__property AnsiString TimeCreated={read=GetTimeCreated,
write=SetTimeCreated};
__property AnsiString Description={read=FDescription, write=FDescription};
};
#endif
Listing 20.3. The main file for the Widgets units.
///////////////////////////////////////
// Widgets.cpp
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "widgets.h"
#pragma link "myobject.obj"
void WriteWidgetToStream(AnsiString StreamName, TWidget *Widget)
{
TFileStream *Stream = new TFileStream(StreamName, fmCreate | fmOpenRead);
Stream->WriteComponent(Widget);
delete Stream;
}
TWidget *ReadWidgetFromStream(AnsiString StreamName)
{
Widgets::Register();
TFileStream *Stream = new TFileStream(StreamName, fmOpenRead);
TWidget *Widget = (TWidget *)Stream->ReadComponent(NULL);
delete Stream;
return Widget;
}
__fastcall TWidget::TWidget(TComponent *AOwner, int ACol, int ARow)
: TCustomControl(AOwner)
{
Hierarchy = new TListHierarchy();
Left = ACol;
Top = ARow;
Width = 25;
Height = 25;
OnMouseDown = WidgetMouseDown;
}
AnsiString __fastcall TWidget::GetTimeCreated()
{
return FTimeCreated.DateTimeString();
}
void __fastcall TWidget::SetTimeCreated(AnsiString S)
{
FTimeCreated = TDateTime(S);
}
void__fastcall TWidget::Paint()
{
Canvas->Brush->Color = clBlue;
Canvas->Rectangle(0, 0, ClientWidth, ClientHeight);
Canvas->Brush->Color = clYellow;
Canvas->Ellipse(0, 0, ClientWidth, ClientHeight);
}
void __fastcall TWidget::WidgetMouseDown(TObject *Sender, TMouseButton
Button,
TShiftState Shift, int X, int Y)
{
ShowMessage(Format("%m", OPENARRAY(TVarRec, (FCost))));
}
namespace Widgets
{
void __fastcall Register()
{
TComponentClass classes[1] = {__classid(TWidget)};
RegisterClasses(classes, 0);
}
}
Listing 20.4. The header file for MyObjects with the
new TListHiearchy object added to it.
///////////////////////////////////////
// MyObject.h
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MyObjectH
#define MyObjectH
#include <conio.h>
#include <vcl\stdctrls.hpp>
class TMyObject :public TObject
{
protected:
virtual void PrintString(AnsiString S);
public:
TMyObject() : TObject() {}
void ShowHierarchy(TObject *AnObject);
};
class TListHierarchy: public TMyObject
{
private:
TStringList *FList;
protected:
virtual void PrintString(AnsiString S)
{ FList->Add(S); }
public:
TListHierarchy() { FList = new TStringList(); }
__fastcall virtual ~TListHierarchy() { delete FList; }
TStringList *GetHierarchy(TObject *AnObject)
{ ShowHierarchy(AnObject); return FList; }
};
class __declspec(delphiclass) TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; textbackground(FBackColor); }
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
virtual void ClrScr();
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
class TVCLHierarchy : public THierarchy
{
TMemo *FMemo;
protected:
virtual void SetTextColor(int Color)
{ FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }
virtual void SetBackColor(int Color)
{ FBackColor = Color; FMemo->Color = TColor(FBackColor); }
public:
TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }
virtual void PrintString(AnsiString S)
{ FMemo->Lines->Add(S); }
virtual void ClrScr() { FMemo->Clear(); }
};
#endif
Listing 20.5. The main file of the MyObject module.
///////////////////////////////////////
// MyObject.cpp
// Learning how to use objects
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (True)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
if (AClassName == "TObject")
break;
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
void THierarchy::ClrScr()
{
clrscr();
}
The functionality associated with this program is still severely limited. An
object of type TWidget is created, and its hierarchy is shown. The
program assigns a price to the widget, and a bare representation of a widget is
displayed on the screen. The program also saves and loads the Widget
object to disk.
The output from this program is shown in Figure 20.2.
FIGURE 20.2. The output from the Widget2
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
BCB implements encapsulation:
class TWidget: public TCustomControl
{
private:
TListHierarchy *Hierarchy;
Currency FCost;
TDateTime FTimeCreated;
AnsiString FDescription;
void __fastcall SetTimeCreated(AnsiString S);
AnsiString __fastcall GetTimeCreated();
protected:
virtual void __fastcall Paint(void);
public:
__fastcall virtual TWidget(TComponent *AOwner): TCustomControl(AOwner)
{ Hierarchy = new TListHierarchy(); Width = 25; Height = 25; }
__fastcall virtual TWidget(TComponent *AOwner, int ACol, int ARow);
__fastcall virtual ~TWidget() { delete Hierarchy; }
virtual AnsiString GetName() { return "Widgets"; }
TStringList *GetHierarchy() { return Hierarchy->GetHierarchy(this); }
void __fastcall WidgetMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
__published:
__property Currency Cost={read=FCost, write=FCost};
__property AnsiString TimeCreated={read=GetTimeCreated,
write=SetTimeCreated};
__property AnsiString Description={read=FDescription, write=FDescription};
};
The private section of TWidget contains several fields of data and
two methods:
private:
TListHierarchy *Hierarchy;
Currency FCost;
TDateTime FTimeCreated;
AnsiString FDescription;
void __fastcall SetTimeCreated(AnsiString S);
AnsiString __fastcall GetTimeCreated();
All of the private data in the program has 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, or data stores.
Internal storage should always be declared private, and as such
cannot be accessed from outside of this unit. 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. The F in these variable names stands for field. If you
want, however, you can think of the F in these names as standing for forbidden,
as in "it is forbidden to directly access this data!"
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 problem is not that the error is embarrassing, but that it is going to cause
you grief!
The GetTimeCreated and SetTimeCreated 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
called Paint. This portion of the object can be accessed by descendants
of TWidget, but not by an instance of the class. For example, you will
have trouble if you write the following code:
{
TWidget *Widget = new TWidget(this);
Widget->Paint(); // this line won't compile
delete Widget();
}
Only a descendant of TWidget can explicitly call the Paint
method.
The methods in the public section of the object make it possible to
manipulate the widgets that you declare:
public:
__fastcall virtual TWidget(TComponent *AOwner): TCustomControl(AOwner)
{ Hierarchy = new TListHierarchy(); Width = 25; Height = 25; }
__fastcall virtual TWidget(TComponent *AOwner, int ACol, int ARow);
__fastcall virtual ~TWidget() { delete Hierarchy; }
TStringList *GetHierarchy() { return Hierarchy->GetHierarchy(this); }
void __fastcall WidgetMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
Here, you can see several constructors, a destructor, a routine that lets you
iterate through the hierarchy of the object, and a routine that handles mouse
clicks on the object. All of these are common activities and need to be declared
public.
The first constructor for the object sets up the aggregated THierarchy
object and then sets the object's width and height. The second constructor
allows you to establish the Left and Top properties for the
object.
You have now had an overview of all the code in the Widget program, except
for its published properties, which will be discussed in the next section. The
discussion so far has concentrated on the BCB 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 and implementations.
- 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, and variables are
available at runtime only.
- 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.
The Widget2 program contains three properties, as shown here:
__published:
__property Currency Cost={read=FCost, write=FCost};
__property AnsiString TimeCreated={read=GetTimeCreated,
write=SetTimeCreated};
__property AnsiString Description={read=FDescription, write=FDescription};
Because the TWidget class is a descendant of TComponent,
all these properties can be put in the published section, and therefore
could be seen from inside the Object Inspector if the object were compiled into
BCB's library. It usually does not make sense to create published sections in
objects that do not have TComponent in their ancestry.
Remember that properties in the published section have the advantages, and
the overhead, associated with a heavy dose of runtime type information. In
particular, properties placed in the public section will automatically be
streamed to disk!
There is no rule that says which properties should be declared in the
published or public sections. In fact, properties often appear in
public sections, although there is little reason for them to be in private
or protected sections.
The cost and description 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 Currency Cost={read=FCost, write=FCost};
The declaration starts with the keyword property, which performs the
same type of syntactical chore as class or struct. Every property must be
declared as having a certain type, which in this case is Currency.
Most properties can be both read and written. The read directive for
the Cost property states that the value to be displayed is FCost
and the value to write is FCost. In short, writing
{
Widget->Cost = 2;
int i = Widget.Cost
}
sets FCost to the value 2 and sets i to the value
of FCost (again, 2).
The reasons for doing this are twofold:
- 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 22, "Creating Descendants of Existing Components."
The Cost and Description 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.
The Cost and Description examples represent the simplest
possible case for a property declaration. The TimeCreated property
presents a few variations on these themes:
property AnsiString TimeCreated={read=GetTimeCreated,
write=SetTimeCreated};
Rather than reading a variable directly, TimeCreated returns the
result of a private function:
AnsiString __fastcall TWidget::GetTimeCreated()
{
return FTimeCreated.DateTimeString();
}
SetQuantity, on the other hand, enables you to change the value of
the FQuantity variable:
void __fastcall TWidget::SetTimeCreated(AnsiString S)
{
FTimeCreated = TDateTime(S);
}
GetTimeCreated and SetTimeCreated 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:
{
AnsiString S;
W->TimeCreated = "1/1/56 02:53:35 AM";
S = W->TimeCreated;
}
Note that when you are writing to the FTimeCreated variable, you
don't write
W->TimeCreated(Now());
Instead, you use the simple, explicit syntax of a direct assignment:
W->TimeCreated = Now();
BCB automatically translates 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:
{
AnsiString S;
W->SetTimeCreated(Now());
S = W->GetTimeCreated;
}
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. Furthermore, they allow you to completely hide the
implementation of your Get and Set methods if you so desire.
Streaming Classes
Published properties allow you to automatically stream the data of your
program. In particular, most published properties will be automatically written
to your DFM files and restored when they are reloaded.
The following code shows how to explicitly write a component to disk:
void WriteWidgetToStream(AnsiString StreamName, TWidget
*Widget)
{
TFileStream *Stream = new TFileStream(StreamName, fmCreate | fmOpenWrite);
Stream->WriteComponent(Widget);
delete Stream;
}
To call this method, you might write code that looks like this:
TWidget * Widget= new TWidget(this);
Widget->Parent = this;
Widget->Left = 10;
Widget->Top = 10;
Widget->Cost = 3.3;
Widget->Description = "This is a widget";
Widget->TimeCreated = Now();
WriteWidgetToStream("Afile.dat", Widget);
This code creates an instance of the Widget component, assigns
values to its data, and then writes it to disk in the last line of the code
quoted here. This creates a persistent version of the object and explicitly
preserves each property value.
The TFileStream component can be used to stream anything to disk;
however, it has a very useful WriteComponent method that will stream an
object automatically, taking care to store the current values of most published
properties. In some cases, you might find a property that the VCL does not know
how to stream. You can usually convert the property to an AnsiString, which the
VCL component will know how to stream. This is what I did in this example, when
I found that the VCL didn't want to write a variable of type TDateTime.
To construct an instance of TFileStream, you pass in the name of the
file you want to work with and one or more flags specifying the rights you want
when you open the file. These flags are listed in the online help and declared
in SysUtils.hpp:
#define fmOpenRead (Byte)(0)
#define fmOpenWrite (Byte)(1)
#define fmOpenReadWrite (Byte)(2)
#define fmShareCompat (Byte)(0)
#define fmShareExclusive (Byte)(16)
#define fmShareDenyWrite (Byte)(32)
#define fmShareDenyRead (Byte)(48)
#define fmShareDenyNone (Byte)(64)
#define fmClosed (int)(55216)
#define fmInput (int)(55217)
#define fmOutput (int)(55218)
#define fmInOut (int)(55219)
FileStreams have a Handle property that you can use if you need it
for special file operations or if you want to pass it to a handle-based C
library file IO routine.
This is not the place to go into a detailed description of how streaming
works in the VCL. However, you might want to open up Classes.hpp and
take a look at the TReader and TWriter classes, which are
helper objects that the VCL uses when it is time to stream an object. These
classes have methods such as ReadInteger, WriteInteger,
ReadString, WriteString, ReadFloat, and WriteFloat.
TReader and TWriter are for use by the VCL, but I have used
these classes for my own purposes on several occasions.
Here is how to read a component from a stream:
namespace Widgets
{
void __fastcall Register()
{
TComponentClass classes[1] = {__classid(TWidget)};
RegisterClasses(classes, 0);
}
}
TWidget *ReadWidgetFromStream(AnsiString StreamName)
{
Widgets::Register();
TFileStream *Stream = new TFileStream(StreamName, fmOpenRead);
TWidget *Widget = (TWidget *)Stream->ReadComponent(NULL);
delete Stream;
return Widget;
}
This code first registers the TWidget type with the system. This is
necessary because the VCL needs to know what type of object you want to stream.
Of course, when working with components that are placed on the Component
Palette, you can be sure the system has already registered the object for you.
However, if you did not drop a component on a form but created it by hand, you
might have to register the component before you can stream it.
The Register method needs to appear in its own namespace because
there will be many register functions in a typical application--at least one for
each component. Most of the time this function will be called automatically by
the compiler, and it is a bit unusual for you to have to call it explicitly.
Notice that the compiler will automatically construct an object for you if
you pass in NULL when calling ReadComponent:
TWidget *Widget = (TWidget
*)Stream->ReadComponent(NULL);
Alternatively, you can create the component yourself and then pass it to
ReadComponent so that its properties will be lifted from the stream.
In the last few pages, you had a good look at the Widget2 program. There are
several additional traits of properties that should be explored, however, before
moving on to the colorful warehouse simulation found in the next chapter.
More on Properties
BCB 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 choose
only 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 PropertyTest program (in Listing 20.6) 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.
Listing 20.6. The main unit for the PropertyTest
program.
///////////////////////////////////////
// Main.cpp
// Learning about properties
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "propertyobject1.h"
#pragma resource "*.dfm"
TForm2 *Form2;
//----------------------------------
__fastcall TForm2::TForm2(TComponent* Owner)
: TForm(Owner)
{
}
//----------------------------------
void __fastcall TForm2::bCreateObjectsClick(TObject *Sender)
{
TMyProps *M;
char Ch;
int i;
M = new TMyProps(this);
M->Parent = this;
M->SimpleProp = 25;
M->EnumProp = teEnum;
M->SetProp = TSetProp() << teEnum << teSet;
M->StrArrayProp["Jones"] = "Sam, Mary";
M->StrArrayProp["Doe"] = "John, Johanna";
ListBox1->Items->Add(M->StrArrayProp["Doe"]);
ListBox1->Items->Add(M->StrArrayProp["Jones"]);
for (i = 0; i < M->ObjectProp->Count; i++)
ListBox2->Items->Add(M->ArrayProp[i]);
Ch = M->Default1;
ListBox1->Items->Add(Ch);
}
Listing 20.7. The header file for the PropertyObject
unit.
///////////////////////////////////////
// PropertyObject.h
// Learning about properties
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef PropertyObject1H
#define PropertyObject1H
enum TEnumType {teSimple, teEnum, teSet, teObject, teArray};
typedef Set<TEnumType, teSimple, teArray> TSetProp;
class TCouple: public TObject
{
public:
AnsiString Husband;
AnsiString Wife;
TCouple() {}
};
class TMyProps: public TCustomControl
{
private:
int FSimple;
TEnumType FEnumType;
TSetProp FSetProp;
TStringList *FObjectProp;
char FDefault1;
AnsiString __fastcall GetArray(int Index);
AnsiString __fastcall GetStrArray(AnsiString S);
void SetStrArray(AnsiString Index, AnsiString S);
protected:
void virtual __fastcall Paint();
public:
virtual __fastcall TMyProps(TComponent *AOwner);
virtual __fastcall ~TMyProps();
__property AnsiString ArrayProp[int i]={read=GetArray};
__property AnsiString StrArrayProp[AnsiString i]=
{read=GetStrArray,write=SetStrArray};
__published:
__property int SimpleProp={read=FSimple, write=FSimple};
__property TEnumType EnumProp={read=FEnumType, write=FEnumType};
__property TSetProp SetProp={read=FSetProp, write=FSetProp};
__property TStringList *ObjectProp={read=FObjectProp, write=FObjectProp};
__property char Default1={read=FDefault1, write=FDefault1, default= `1'};
};
#endif
Listing 20.8. The source for the PropertyTest unit
shows how to work with properties.
///////////////////////////////////////
// PropertyObject.cpp
// Learning about properties
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "PropertyObject1.h"
#include "codebox.h"
__fastcall TMyProps::TMyProps(TComponent *AOwner)
:TCustomControl(AOwner)
{
Width = 100;
Height = 100;
Left = (((TForm*)(AOwner))->ClientWidth / 2) - (Width / 2);
Top = (((TForm*)(AOwner))->ClientHeight / 2) - (Height / 2);
FObjectProp = new TStringList();
Default1 = `1';
};
__fastcall TMyProps::~TMyProps()
{
int i;
for (i = 0; i < FObjectProp->Count; i++)
{
FObjectProp->Objects[i]->Free();
}
FObjectProp->Free();
}
void __fastcall TMyProps::Paint()
{
Canvas->Brush->Color = clBlue;
TCustomControl::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]);
};
AnsiString __fastcall TMyProps::GetArray(int Index)
{
return FObjectProp->Strings[Index];
}
AnsiString __fastcall TMyProps::GetStrArray(AnsiString S)
{
TCouple *Couple;
Couple = (TCouple*)(FObjectProp->Objects[FObjectProp->IndexOf(S)]);
return Couple->Husband + ", " + Couple->Wife;
}
AnsiString GetHusband(AnsiString S)
{
return StripLastToken(S, `,');
}
AnsiString GetWife(AnsiString S)
{
return StripFirstToken(S, `,');
}
void TMyProps::SetStrArray(AnsiString Index, AnsiString S)
{
TCouple *Couple;
Couple = new TCouple();
Couple->Husband = GetHusband(S);
Couple->Wife = GetWife(S);
FObjectProp->AddObject(Index, Couple);
}
The structure of the PropertyTest 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, as shown in Figure 20.3.
FIGURE 20.3. The main form of the
PropertyTest program.
TMyObject has five properties, one for each of the major types of
properties. These properties have self-explanatory names:
__property int SimpleProp={read=FSimple,
write=FSimple};
__property TEnumType EnumProp={read=FEnumType, write=FEnumType};
__property TSetProp SetProp={read=FSetProp, write=FSetProp};
__property TStringList *ObjectProp={read=FObjectProp, write=FObjectProp};
__property AnsiString ArrayProp[int i]={read=GetArray};
Before exploring these properties, I should mention that TMyProps is
descended from the native VCL 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.
Because TCustomControl is so intelligent, it is easy to use its
Paint method to write values to the screen:
void __fastcall TMyProps::Paint()
{
Canvas->Brush->Color = clBlue;
TCustomControl::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]);
};
Note that you do not need to explicitly call the Paint method.
Windows calls it for you whenever the object needs to paint or repaint itself.
This means that you can hide the window behind 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.
The first three properties of TMyProps are extremely easy to
understand:
__property int SimpleProp={read=FSimple,
write=FSimple};
__property TEnumType EnumProp={read=FEnumType, write=FEnumType};
__property TSetProp SetProp={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 = TSetProp() << 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 TStringList *ObjectProp={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 descendant 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--though
they will, of course, clean up the strings you hang on 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 BCB.
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:
AnsiString S = "StringConstant";
Couple = (TCouple*)(FObjectProp->Objects[FObjectProp->IndexOf(S)]);
While not completely beyond the pale, this is definitely not the kind of code
you want to have strewn indiscriminately through a program that is going to be
maintained by a mere mortal. 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:
__fastcall TMyProps::TMyProps(TComponent *AOwner)
:TCustomControl(AOwner)
{
... // Code omitted here
FObjectProp = new TStringList();
};
__fastcall TMyProps::~TMyProps()
{
int i;
for (i = 0; i < FObjectProp->Count; i++)
{
FObjectProp->Objects[i]->Free();
}
FObjectProp->Free();
}
The key point is that you must iterate through the string list at the end of
the program, deallocating memory for each object you store on the list. Then you
must deallocate the memory for the TStringList itself.
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 accessing it, and to
simplify the act of allocating memory for each object you store in it.
PropertyTest shows how this can be done.
The code entertains the conceit that you are creating a list for a party to
which only married couples are 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, PropertyTest 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 PropertyTest program, I store a simple object with two fields inside
the TStringList:
class TCouple: public TObject
{
public:
AnsiString Husband;
AnsiString Wife;
TCouple() {}
};
Note that this object looks a lot like a simple struct. 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 non-objects 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 it
needed to be used. Instead, PropertyTest asks the user to pass in first and last
names in this simple string format:
"HusbandName, WifeName"
PropertyTest also asks them to pass in the last name as a separate variable.
To simplify this process, I use a string array property:
__property AnsiString StrArrayProp[AnsiString i]=
{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 BCB 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:
AnsiString GetHusband(AnsiString S)
{
return StripLastToken(S, `,');
}
AnsiString GetWife(AnsiString S)
{
return StripFirstToken(S, `,');
}
void TMyProps::SetStrArray(AnsiString Index, AnsiString S)
{
TCouple *Couple;
Couple = new TCouple();
Couple->Husband = GetHusband(S);
Couple->Wife = GetWife(S);
FObjectProp->AddObject(Index, Couple);
}
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 and wife's names from the string by
calling two token-based functions from the CodeBox 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 destructor.
The twin of SetStrArray is GetStrArray. This function
retrieves a couple's last name from the TStringList whenever the user
passes in a last name. The syntax for retrieving information from the
StrArray property looks like this:
AnsiString S = M->StrArrayProp["Doe"];
In this case, S is assigned the value "Sam, Mary". Once
again, note the remarkable fact that BCB enables us to use a string as an index
in a property array.
The implementation for GetStrArray is fairly simple:
AnsiString __fastcall TMyProps::GetStrArray(AnsiString
S)
{
TCouple *Couple;
Couple = (TCouple*)(FObjectProp->Objects[FObjectProp->IndexOf(S)]);
return Couple->Husband + ", " + Couple->Wife;
}
The code retrieves the object from the TStringList and then performs
some simple hand-waving to re-create the original string passed in by the user.
Obviously, it would be easy to add additional 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.
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 AnsiString ArrayProp[int i]={read=GetArray};
ArrayProp uses the traditional integer as an index. However, note
that this array still has a special trait not associated with normal arrays: It
is read-only! Because no write method is declared for this property, it cannot
be written to; it can be used only to query the TStringList that it
ends up addressing:
AnsiString __fastcall TMyProps::GetArray(int Index)
{
return FObjectProp->Strings[Index];
}
You can call ArrayProp with this syntax:
AnsiString S = M->ArrayProp[0];
This is an obvious improvement over writing the following:
AnsiString S = M->FObjectProp->Strings[0];
Creating a simple interface for an object might not seem important at first,
but in day-to-day programming a simple, clean syntax is invaluable. For
instance, the PropertyTest program calls ArrayProp in the following
manner:
for (i = 0; i < M->ObjectProp->Count; i++)
ListBox2->Items->Add(M->ArrayProp[i]);
NOTE: Astute readers might be
noticing that BCB 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 Chapter 22, you will see that you can even replace the TListBox
object on the com-ponent 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 BCB so that it becomes a custom-made tool that fits your specific
needs.
If you bury yourself in the BCB source code, eventually you might notice the
default directive, which can be used with properties:
__property char Default1={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 BCB whether this value needs to be streamed when a
form file is being written to disk. If you make TMyProp into a
component, drop it onto a form, and save that form to disk, BCB 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 BCB thus
knows whether to write the value to disk. If the property in the Object
Inspector is equal to the default, BCB 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 BCB. 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:
__fastcall TMyProps::TMyProps(TComponent *AOwner)
:TCustomControl(AOwner)
{
... // Code omitted here
Default1 = `1';
};
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 BCB 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, and never will, perform this action. To gain this functionality you
must use the constructor, as shown in the PropertyTest application.
There are a few occasions when the constructor won't work for you because of
the current state of a property. In those cases, you can use the Loaded
or SetParent methods to initialize the value of a property. If you use
the Loaded method to initialize a property, the results of the
initialization won't show up at design-time but will become evident at runtime.
After reading this section, it should be clear that array properties
represent one of the more powerful and flexible aspects of BCB programming.
Though they are quite similar to operator overloading, they have their own
special qualities and advantages.
Summary
That wraps up this introduction to properties and encapsulation. The key
items you have explored are the private, protected, public,
and __published directives, as well as the art of creating 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.
|