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:
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.
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:
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 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.
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.
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:
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:
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.
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 destructorDestructors 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.
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 provide several advantages:
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:
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.
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.
BCB provides support for five different types of properties:
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.
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.
©Copyright,
Macmillan Computer Publishing. All rights reserved.