Inheritance
This
chapter focuses on object-oriented programming (OOP) as it applies to the
VCL. Specifically, it takes a close look at inheritance, one of the
big-three topics in object-oriented code. The other two key topics are
encapsulation and polymorphism, which you learn about in the next two
chapters. Even if you already know all about OOP, you should still at least
skim this chapter so you can learn about the difference between VCL objects
and standard C++ objects.
In particular, this
chapter covers the following topics:
The text focuses on
several programs designed to show how objects are constructed. One of the
programs is developed in several stages so that you can see how an object
hierarchy emerges out of a set of raw ideas.
After you read the next
three chapters on inheritance, encapsulation, and polymorphism, the next
big step is to learn how to build components. In fact, the real
justification for learning this material is that it gives you the ability
to start creating your own components. Building your own components is one
of the most important tasks you can tackle in BCB, so I will lay the
groundwork for it carefully.
It is important to
understand that the next three chapters are aimed at programmers who want
to work inside the VCL. I make no attempt to do justice to all the complex
features of the C++ object model. Instead, I try to present you with a
subset of those features as they apply to the VCL. This means that I make
short shrift of interesting topics such as function and operator
overloading. My intent, however, is to show you how to create components.
You do not have to be an expert in C++ OOP theory to achieve that goal.
For all its wonders, I
don't think there is anything in C++Builder that even approaches the
significance of components. VCL components are the most amazing
technological achievement I have seen in contemporary programming. If you
want to do something really fantastic with your computer, then pay
attention to the next few chapters so that you can learn how to build great
components.
When reading this
chapter, you might want to make use of the ClassBrowser sample program that
ships with BCB. It allows you to explore the hierarchy of the VCL. This
program is found in the Examples/ClassBrw directory. It is far from perfect, but it
will serve to give you an overview of the VCL classes. You should also go
to www.object-domain.com and see whether they have a version of
Snorkle for C++Builder available. The versions of Snorkle for
About
Objects
It might seem a little
strange to start focusing on objects this late in the book. After all,
almost every program I have shown so far uses object-oriented code. So how
could I wait this long to begin talking seriously about objects? To answer
this question, I need to discuss two different issues:
The developers wanted
BCB to be very easy to use. By its very nature, OOP is not always a simple
topic. As a result, BCB goes to considerable lengths to hide some of the
difficulties of object-oriented programming from the user. The biggest
steps in this direction include the automatic construction of Form1 as an object and the existence
of the delegation model. The fact that the scaffolding for most methods is
produced automatically by the IDE is one of the key ways the product saves
time--and one of the key ways it eases the process of producing
applications.
The simple fact is that
some people would never be able to approach BCB if they had to go through
the process of writing all this every time they created a form:
//--------------------------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------------------------------------------------
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
//--------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:
private:
public: // User declarations
virtual __fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------------------------------
extern TForm1 *Form1;
//--------------------------------------------------------------------------
#endif
I'm leaving out the
implementation of the constructor, and a few other features, but in a
stripped-down form, this code is indeed the basis for most BCB units. It's
simple enough to write; nonetheless, it could form a barrier between the
product and certain types of programmers.
The next obvious
question is, "Why did the developers choose to write object-oriented
code if the subject itself can at times become somewhat complex? Why not
just use the relatively simpler framework provided by structured
programming?" The answer is that although it is simpler to create
small structured programs than small object-oriented programs, it's easier
to write large object-oriented programs than it is to write large
structured programs.
OOP brings discipline
and structure to a project. In the long run, this makes coding easier. The
problem is the learning curve associated with understanding OOP.
Almost everyone agrees
that it's easier to finish a group project if you appoint a leader for the
group; it's easier to win at sports if you practice regularly; and,
ultimately, it's easier to become a good musician if you sit through some
boring lessons with a professional. It also might seem at first as if
structured programs are simpler to learn how to write and, therefore, are
simpler to write, but this isn't true. Just as it helps to take lessons,
practice, and learn discipline if you want to become good at playing a
sport or a musical instrument, it helps to learn object- oriented code if
you want to write good programs.
Here's another way of
stating the same matter. There is nothing you can do with object- oriented
code that you can't also do with structured programming. It's just that OOP
makes it relatively easy to construct programs that are fundamentally sound
and easily maintained. This doesn't mean you can't write structured
programs that are every bit as architecturally sound as object-oriented
programs. The problem, however, is that it is very difficult to design a
structured program that is truly modularized and truly easy to maintain.
Object-oriented code, on the other hand, has a natural tendency to move you
in the direction of a sound, well-structured design.
The thesis of this
chapter is that object-oriented code is basically a technique for designing
robust, well-planned programs. The syntax of OOP emerged out of the desire
to help programmers design applications that work. It is perhaps arguable
as to whether or not OOP by itself succeeded in achieving its goal, though
certainly I personally believe that it is a success. However, I think it is
undeniable that OOP in conjunction with components is the answer to many
core programming problems. If your only experience with components is in
creating ActiveX controls, then you haven't yet seen what this technology
can do. The combination of OOP and components is something that can make
programmers many times more productive than they had ever imagined possible
when writing structured code, or when working with either objects or
components alone.
NOTE: It's probably worth pointing out
that OOP is not a separate subject from structured programming but its
natural child. OOP emerged out of the same types of thinking that generated
structured code. Much of what is true in structured programs is also true
in object-oriented programs, except OOP takes these theories much further.
Object-based programmers should know nearly everything that structured
programmers know and should then add another layer of information on top of
it.
OOP is certainly not
the end-all and be-all of programming. Rather, it is an intermediate step
in an ongoing process that might never have an end. BCB, with its heavy use
of components, already shows part of what the future holds. In particular,
the future is about components and visual manipulation of objects.
The Object Inspector
enables you to see inside objects and to start to manipulate them visually.
You can do this without having to write code. It is quite likely that this
trend will continue in the future, and you will start to see programs not
as code but as a series of objects depicted as a hierarchy. If programmers
want to manipulate these objects, they will be able to do so through tools
such as the Object Inspector or through other means currently being used
only in experimental languages.
To take this out of the
clouds for a moment, here is my list of what's best about BCB:
Here are the same ideas
looked at again from a slightly more in-depth perspective:
OOP, then, is part of a
theory of design that is moving increasingly in the direction of reusable,
visual components that can be manipulated with the mouse. Undoubtedly, this
means that some types of programs that are difficult to construct today
will become trivial to build in the future. BCB has already performed this
magic with databases. A 10-year-old child could use BCB to construct a
simple database application. However, creating complex programs will
probably always be difficult, simply because it is so hard to design a good
program that performs anything more than trivial tasks. First printing
presses, then typewriters, and finally word processors have made writing
much easier than it used to be, but they have not succeeded in making us
all into a race of Shakespeares.
NOTE: One thing that is not built into BCB that
can help you create robust programs is a good object-modeling tool. There
are some tools, such as the products called WithClass and Snorkle, that are
designed to work with the Delphi VCL and should soon appear in a BCB-based
format. BCB's object-oriented,
component-based architecture makes programming easier than it used to be.
That doesn't mean that now everyone will be able to program. It just means
that now the best programmers can make better applications. The key terms
are reuse, visual design tools, components, and objects. If you can find an
object-modeling tool that can aid in program development, you will be even
further ahead.
Creating
Simple Objects
To start a discussion
of objects, it might be a good idea to cut the VCL out of the picture as
much as possible. This will eliminate the complex object hierarchy
associated with the VCL. In its place, you can construct some very simple
objects with a known hierarchy that is easy to define. As the discussion
progresses, the VCL can be introduced into the programs in a planned and
sensible manner.
1. Start a new project.
Edit the main source
file for the project so it looks like this:
#include <stdio.h>
int main(void)
{
printf("Daughters of Time, the hypocritic Days,\n");
printf("Muffled and dumb like barefoot dervishes\n");
printf("-- Ralph Waldo Emerson");
return 0;
}
Save this file as Object1.mak. It is now a complete
application that circumvents the VCL. If you open up a DOS window and run
the program from the DOS prompt, the output looks like Figure 19.2. It might seem strange
to you that I have gone out of my way to eliminate so much of the object
hierarchy in a chapter that is about objects. My goal, however, is to clear
the boards so that you can view objects in a simplified state, thereby
clearly delineating their most salient points.
The program that
unfolds through the next few pages is called OBJECT1. This is a very simple
object-oriented program that you will build on the console application
framework established earlier. I'm not going to start by showing you the
code for the whole program, because I want you to build it one step at a
time so that its structure emerges little by little.
To begin, you should
create a small object at the top of the program:
class TMyObject
{
};
int main(void)
{
return 0;
}
All I have done here is
added a simple class definition and removed the printf()statements.
Delphi programmers
should note that this class is not a descendant of TObject, even though it would have been
in Object Pascal. One of the fundamental rules of Object Pascal programming
is that it is impossible to build an object that is not a descendant of TObject or one of TObject's children. The reason for this
rule is that TObject contains some RTTI-based intelligence that
is needed by all BCB objects. This same intelligence is present in the
metaclass that is part of the BCB version of TObject. However, you can create C++
objects that do not descend from TObject and thus do not include this
intelligence.
NOTE: You will find that I use the words class
and object almost completely interchangeably. This is technically correct,
although there is some merit in using the word class to describe the
written declarations that appear in a text file and object to refer to a
compiled class that is part of a binary file. In other words, programs are
made up of objects, whereas source files show class definitions. However,
this distinction is not one that I spend a great deal of time stressing in
this book.
To create a true VCL
object, you should change TMyObject's definition so that it reads as follows:
#include <vcl\vcl.h>
class TMyObject : public TObject
{
public:
__fastcall TMyObject(void) : TObject() {};
}
int main(void)
{
return 0;
}
Logically, there is now
a considerable difference between this declaration and the one you created
earlier. In particular, this is now a VCL object and must be created on the
heap. It also supports VCL specific syntax such as the __published directive.
All VCL objects must
have a constructor, and it should be declared __fastcall. Methods or functions declared __fastcall can have some of their
parameters passed in registers, rather than always being pushed on the
stack. This is the calling convention used by VCL constructors, so you
should conform to it.
All VCL objects that
are descendants of TComponent must have destructors declared __fastcall
virtual:
__fastcall virtual TComponent(TComponent* Aowner);
The reason for the __fastcall and virtual restrictions has to do with
conformance to the VCL programming model. In particular, the VCL declares
the constructor for TComponent as virtual, so C++ objects that descend
from TComponent must follow along. This means that all components you create must
be declared with virtual constructors, because all components are,
at least in practice, descendants of TComponent. I comment on this fact simply
because it is very unusual for C++ constructors to be declared virtual.
NOTE: Actually, it is theoretically possible for you
to create your own class that performs the same chores that TComponent performs. There is nothing
magical about TComponent; it simply contains standard VCL code that
makes it possible for an object to live on the Component Palette. It is not
practical to duplicate this effort in your own code, and so one could
perhaps go so far as to say that "by definition" all components
are descendants of TComponent. However, this is not strictly true, as
you could create your own class that appears on the Component Palette
without descending from TComponent. I personally cannot imagine any set of
circumstances that would justify the effort involved in duplicating the
work done in TComponent.
The declaration and
implementation for a C++ constructor can have two forms. They can appear
entirely inside a class declaration, or you can split them up, with the
declaration inside the class and the implementation outside:
#include <vcl\vcl.h>
class TMyObject : public TObject
{
public:
__fastcall TMyObject(void);
};
__fastcall TMyObject::TMyObject(void) : TObject()
{
}
int main(void)
{
return 0;
}
When you implement a
constructor, you should follow the header with a colon and a call to the
ancestor's constructor:
_fastcall TMyObject::TMyObject(void) : TObject()
Now that you have an
overview of a basic VCL object declaration, the next step is to declare a
variable of type TMyObject and then instantiate it and dispose of it:
class TMyObject : public TObject
{
public:
__fastcall TMyObject() : TObject() {}
};
int main(void)
{
TMyObject *MyObject = new TMyObject;
delete MyObject;
return 0;
}
The code shown here
doesn't do anything functional. Its only purpose is to teach you how
objects work. Specifically, it declares a variable of type TMyObject:
TMyObject *MyObject
Next, it allocates the
memory for the object:
new TMyObject;
Put together on one
line, the statement looks like this:
TMyObject *MyObject = new TMyObject;
This statement actually
creates a pointer variable of type TMyObject. In VCL programming, you have
to take this step if you want to use MyObject, and, furthermore, you must
dispose of this memory when you are finished with it.
There are two ways to
destroy an object. One is to call Free, and the other is to use the delete operator:
MyObject->Free();
delete MyObject;
Both techniques have
the same outcome. I believe the majority of people prefer delete, and it is what I use most
often in the code found in this book. However, there are reasons you might
want to call Free, so I will discuss it in the next few paragraphs.
When you free an
object, what you are really doing is calling the object's destructor. The
following code shows approximately what takes place in the Free method of TObject:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
The variable Self always points to the current
object. It plays the same role in Object Pascal that this plays in C++. If you are inside
one of the methods of an object, you can refer to that object by using Self. (Self is passed as an implicit
parameter to all BCB methods.) Here is how the VCL Free method would look in C++:
void __fastcall TObject::Free()
{
if (this != NULL)
~TObject();
}
NOTE: Programmers use the words descendant, child
object, derived class, and subclass as synonyms. I prefer to use either
descendant or child object, because subclass is also used in another
context and derived class seems unnecessarily obscure. My feeling is that
it's best to stick to one metaphor: parent, child, ancestor, and
descendant, where child and descendant are synonymous, and parent and
ancestor are synonymous.
Standard C++ does not
define a Free method. This is something specific to the VCL. It is added to the VCL to
make objects easier to use. It is very bad to call the destructor of an
object that no longer exists. As a result, the Free method is there to provide a
check that gives you some measure of protection against this error. Despite
this, the general consensus is that it is best to call delete. The great
virtue of delete is that it looks like standard C++ code, and C++
programmers care a lot about standards.
Now that you know how
to declare, allocate, and deallocate a simple object, it's time to narrow
the focus and tackle the subject of inheritance. The next two sections are
dedicated to this chore--specifically, to explaining the relationship
between a parent and child object.
Understanding
Inheritance
In general, a child
object can use any of its parent's methods. A descendant of an object gets the
benefit of its parent's capabilities, plus any new capabilities it might
bring to the table. I say that this is true in general, because the private directive can limit the
capability of a child to call some of its parent's routines. The private directive is explained in depth
later in this chapter.
Except for its
constructor, all of TMyObject's methods and fields are inherited:
class TMyObject : public TObject
{
public:
__fastcall TMyObject() : TObject() {}
};
This declaration is
somewhat deceiving because TObject contains many methods that are available
to instances of TMyObject. In other words, TMyObject is not quite as simple an
object as it appears at first.
So, what are all these
methods associated with TObject? Well, you can see their definitions, as
well as their implementations, if you open up the SysDefs.h file from the \BCB\Include\VCL subdirectory:
class __declspec(delphiclass) TObject
{
public:
__fastcall TObject() {}
__fastcall Free();
TClass __fastcall ClassType();
void __fastcall CleanupInstance();
void * __fastcall FieldAddress(const ShortString &Name);
static TObject * __fastcall InitInstance(TClass cls, void *instance);
static ShortString __fastcall ClassName(TClass cls);
static bool __fastcall ClassNameIs(TClass cls, const AnsiString string);
static TClass __fastcall ClassParent(TClass cls);
static void * __fastcall ClassInfo(TClass cls);
static long __fastcall InstanceSize(TClass cls);
static bool __fastcall InheritsFrom(TClass cls, TClass aClass);
static void * __fastcall MethodAddress(TClass cls, const ShortString &Name);
static ShortString __fastcall MethodName(TClass cls, void *Address);
...// Code omitted here
virtual void __fastcall Dispatch(void *Message);
virtual void __fastcall DefaultHandler(void* Message);
private:
virtual TObject* __fastcall NewInstance(TClass cls);
public:
virtual void __fastcall FreeInstance();
virtual __fastcall ~TObject() {}
};
You can find the entire
implementation of TObject in the Object Pascal System.pas unit that ships with BCB. Much
of it is actually in assembler, but the source is there if you want to
study it. You should, of course, also examine the declaration for TObject in SysDefs.h. The calls in SysDefs.h, however, ultimately resolve
into calls to the Pascal implementation in System.pas. I should perhaps add that the TObject declaration in SysDefs.h is very hard to understand, but
I promise you that it does end up resolving into calls that access the System.pas version of TObject.
NOTE: Although I have mentioned this subject
before, it's probably once again time to stress the importance of viewing
the Pascal source code to the VCL. Your version of BCB might or might not
ship with the source, but if you don't have it and can possibly afford to
buy it, you should think seriously about obtaining it. You should peruse
the BCB\Include\VCL subdirectory that contains the header files for the imported VCL
Pascal units. These files provide the interface for key BCB units. They are
not as good as having the source, but they are very valuable. I refer to
both the header files and the Pascal source continuously.
You can see that TObject has a few basic functions
declared right at the top:
__fastcall TObject() {}
__fastcall Free();
virtual __fastcall ~TObject() {}
The point to grasp here
is that TMyObject has a destructor and Free method because it inherits them from TObject.
To understand this
point, you can add a line of code to the nascent OBJECT1 program:
#include <conio.h>
#include <stdio.h>
int main(void)
{
TMyObject *MyObject = new TMyObject;
AnsiString S = MyObject->ClassName();
printf(S.c_str());
delete MyObject;
getch();
return 0;
}
This code enables the
object to write its name to the screen. The output from this program is a
single string:
TMyObject
When you run the
program, this string might flash by too quickly for leisurely perusal. To
remedy the situation, add a getch() at the very end of the code, right before
the return statement. To end this program, press Enter. (That's the way it used to be
done back in the DOS world.)
If you want to, you can
even get this object to say its parent's name:
int main(void)
{
TMyObject *MyObject = new TMyObject;
printf(Format("ClassName: %s\nParent's ClassName: %s",
OPENARRAY(TVarRec, (
MyObject->ClassName(),
MyObject->ClassParent()->ClassName()))).c_str());
delete MyObject;
getch();
return 0;
}
The output from this
code is the following:
ClassName: TMyObject
Parent's ClassName: TObject
The point, of course,
is that TMyObject inherits quite a bit of functionality from its parent, and, as a result, it
has numerous capabilities that might not be obvious from merely viewing its
declaration.
The ability to trace an
object's ancestry is relatively appealing, so it might be nice to add it to TMyObject as a method:
#include <vcl/vcl.h>
#include <stdio.h>
#include <conio.h>
#include "classrefs.h"
USEUNIT("ClassRefs.cpp");
class TMyObject : public TObject
{
public:
TMyObject() : TObject() {}
void PrintString(AnsiString S);
void ShowHierarchy();
};
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
int main(void)
{
ShowClassReferences();
TMyObject *MyObject = new TMyObject;
MyObject->ShowHierarchy();
delete MyObject;
getch();
return 0;
}
This version of the
OBJECT1 program includes two methods, listed in the TMyObject class declaration:
class TMyObject : public TObject
{
public:
TMyObject() : TObject() {}
void PrintString(AnsiString S);
void ShowHierarchy();
};
Take a look at the
implementation for ShowHierarchy. Perhaps the first thing you notice in it
is the class reference in the first line, which uses the TClass type.
The type TClass is an object reference and is
declared in Sysdefs.h as follows:
typedef TMetaClass* TClass;
Because ClassParent returns a variable of type TClass, it is obviously what needs to
be used here.
NOTE: An object reference is a special metaclass
that can be assigned to an object. Here is a unit that shows some legal
uses of an object reference:
///////////////////////////////////////
// ClassRefs.cpp
// Object1
// copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <forms.hpp>
#pragma hdrstop
#include "ClassRefs.h"
TClass AClass;
class TDescendant: public TObject
{
public:
TDescendant(): TObject() {}
};
void ShowClassReferences()
{
printf("** Start object references **\n");
AClass = __classid(TObject);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
AClass = __classid(TDescendant);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
AClass = __classid(TForm);
printf("%s\n", AnsiString(AClass->ClassName()).c_str());
printf("** End object references **\n\n");
} Notice that you do not
have to create an object before you can use it with an object
reference. In
general, you can call any of the static methods of TObject with a class reference. ObjectRef = __classid(TForm)
ObjectRef.Caption := `Sam';
WriteLn(ObjectRef.Caption); You will find a version
of the CLSREF unit in the same subdirectory as OBJECT1. You can use the Project Manager
to add this file to the project, and you can then call it in the second line
of the body of the OBJECT1 program. However, you should not leave this unit
as part of the project, because it will muddy the view of the object
hierarchy that you get in the Browser. When you get past the
object reference, the remaining portions of the ShowHierarchy method are fairly
straightforward:
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
This code first writes
the ClassName of the current object, which is TMyObject. Then it gets the ClassParent, which is TObject, and writes its name to the
screen. The code then tries to get TObject's parent and fails, because TObject has no parent. At this point, AClass is set to NULL and the code exits the while loop. The output for the
program is shown in Figure 19.3. In this section, you
have learned about the Create, Destroy, Free, ClassParent, and ClassName methods of TObject. The declaration of TObject shows that several other
methods are available to BCB programmers. However, I do not discuss these
methods in depth because they are either self-explanatory (InheritsFrom) or beyond the scope of this
book. I should mention, however, that some of these routines are used by
the compiler itself when dispatching routines or performing other complex
tasks that usually require RTTI support. These are advanced programming
issues that impact only a very small percentage of BCB programmers.
Virtual
Methods
Inheritance, in itself,
is an interesting feature, but it would not take on much significance were
it not for the presence of virtual methods. Virtual methods can be
overridden in a descendant class. As such, they provide the key to
polymorphism, which is a trait of OOP programs that enables you to give the
same command to two different objects but have them respond in different
ways. This chapter introduces polymorphism, but I will leave the more
complex aspects of this subject for Chapter 21, titled, appropriately
enough, "Polymorphism." Polymorphism is a relatively difficult
subject to grasp; therefore, I have stretched out a full explanation of it
over several chapters.
Unlike Object Pascal,
BCB has only one type of virtual method. This directive tells the compiler
to store the address of the function in a virtual method table.
NOTE: Delphi programmers should note that C++
does not support either the dynamic or message directives. In their place,
you can use the MESSAGE_MAP macro.
The OBJECT2 program
(shown in Listing 19.2) has one virtual method. The virtual method is overridden in a child
object. When you are creating the OBJECT2 program, you should start with
the source code for the OBJECT1 program. Modify the code by declaring PrintString as virtual and by creating a descendant of TMyObject called THierarchy. Also, don't forget to make
sure the program is set to work as a console application. If you don't have
this option checked, you can get an EInOutError exception. After changing the
setting, you should also rebuild your project so the new option takes
effect.
NOTE: When creating one project based on another,
you can often copy the code from the directory where the first project is
stored into a separate directory made for the second project. After copying
the project, it is probably simplest to delete everything but the actual
source files from the new directory. For instance, delete the DSK file, the
MAK file, and any other extraneous files you will not need. Then create a
new project, delete its main form, and add copies of the source files you
want to reuse from the previous project. Otherwise, you might find paths
hard-coded into your DSK or MAK files that address files stored in the
first program's directory. In this particular case, it is probably easiest
just to re-create the project entirely from scratch, but the information in
this note can be used as a general set of guidelines for use when copying
projects from one directory to another.
Listing
19.2. The source code for the main unit in the OBJECT2 program.
///////////////////////////////////////
// Object2.cpp
// Project: Object2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl/vcl.h>
#include <stdio.h>
#include <conio.h>
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
virtual void PrintString(AnsiString S);
};
class THierarchy: public TMyObject
{
int FColor;
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
__property int Color={read=FColor,write=FColor};
};
void TMyObject::PrintString(AnsiString S)
{
printf("%s\n", S.c_str());
}
void TMyObject::ShowHierarchy()
{
TClass AClass;
AnsiString AClassName = AnsiString(ClassName()).c_str();
PrintString(AClassName);
AClass = ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
textcolor(FColor);
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
int main(void)
{
TMyObject *MyObject = new TMyObject();
MyObject->ShowHierarchy();
MyObject->Free();
THierarchy *Hierarchy = new THierarchy();
Hierarchy->Color = YELLOW;
Hierarchy->ShowHierarchy();
Hierarchy->Free();
getch();
return 0;
}
In OBJECT1, the ShowHierarchy method wrote its output to the
screen. Suppose that you found this object somewhere and liked the way it
worked but wanted to change its behavior so it could also write its output
in color. The OBJECT2 program shows a preliminary version of how you might
proceed. After completing this first take on creating a descendant of TMyObject, I will revisit the subject and
show ways to improve the model shown here. The output from the program is
shown in Figure 19.4. In the old world of
structured programming, the most likely step would be to rewrite the
original ShowHierarchy method. However, rewriting an existing method can be a problem for
two reasons:
A combination of design
and maintenance issues might deter the impulse to rewrite the original
method. Many projects have been delayed or mothballed because changes in
their designs have broken existing code and thrown the entire project into
chaos.
OOP has a simple
solution to this whole problem. Instead of declaring TMyObject as
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
void PrintString(AnsiString S);
};
thoughtful programmers
declare it like this:
class TMyObject :public TObject
{
public:
TMyObject() : TObject() {}
void ShowHierarchy();
virtual void PrintString(AnsiString S);
};
The difference is that
in the second example, the PrintString method is declared as virtual.
If PrintString is declared as virtual, you can override it in a
descendant object, thereby changing the way the method works without ever
changing the original version of the method. This means that all the other
code that relies on the first version of the program continues to work, and
yet you can rewrite the function for your own purposes. Furthermore, this
technique would work even if you didn't have the source code for TMyObject! I will show you how this works
in just a moment.
Some readers might have
asked themselves earlier why I created the PrintString method in the first place. The
answer hinges on the fact that iterating through a hierarchy of VCL objects
can always be accomplished by the same algorithm. The same technique works
for all VCL objects. But the act of printing information to the screen
changes depending on your current circumstances. Are you in DOS? Are you in
Windows? Do you want to use colors? Each of these circumstances calls for a
different way of printing information to the screen. As a result, I
separated the screen IO portion of TMyObject from the portion of the object
that iterates through a hierarchy. Furthermore, there is no need to declare ShowHierarchy as virtual, but I must declare PrintString as virtual. The reasoning here is simply
that ShowHierarchy does not need to change in descendants of the object, but PrintString will need to change. In
particular, it will need to change so that it can write output in color. These
types of considerations are part of a subject known as object design.
At this point, you might think that using virtual methods seems like an unnecessarily opaque solution to this problem. Wouldn't it have been simpler to add new methods to the inherited class? Then the user of this second class could call these new methods rather than the ones from the first instance of the class. There are three problems with this technique:
The word virtual is
inherited from one class to the next. If a base class declares a method virtual, the descendants need not do
so, because they will inherit the virtual declaration for a particular
method from the base class. Delphi users take note, as this is the exact
opposite of what happens in Object Pascal. I should add that it is
generally considered bad form not to repeat the declaration in child
objects, as you want to be sure the reader of your code can see at a glance
how it is structured.
Here is a look at a
stripped-down descendant of TMyObject that overrides the PrintString method:
class THierarchy: public TMyObject
{
int FColor;
public:
THierarchy() : TMyObject() {}
virtual void PrintString(AnsiString S);
__property int Color={read=FColor,write=FColor};
};
This declaration states
that class THierarchy is a descendant of class TMyObject and that it overrides PrintString.
NOTE: Delphi programmers beware! The Pascal
object model performs the same chore by using the override directive. That's not the way
C++ works. This is a major change between the new BCB code and the old
Object Pascal techniques.
A first take on the new
version of the PrintString method looks like this:
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
textcolor(FColor);
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
This code depends on
functionality from Conio.h that allows you to print strings to the
screen in color.
You can see that a
field called FColor has been added to this object: THierarchy now contains not only
procedures but also data. One of the key aspects of class declarations is
that they can contain both methods and data, so that you can bring all the
code related to the THierarchy object together in one place. This is part
of a concept called encapsulation, explained in the next chapter.
NOTE: By convention, VCL objects declare private
data as starting with the letter F, which stands for field. This technique
is helpful, because it highlights the difference between an object's
properties and its data. A property is published to the world and does not
begin with the letter F. Private data is inaccessible to the rest of the
world and begins with the letter F.
When you run the
OBJECT2 program, the following code is executed in its main body:
int main(void)
{
TMyObject *MyObject = new TMyObject();
MyObject->ShowHierarchy();
MyObject->Free();
THierarchy *Hierarchy = new THierarchy();
Hierarchy->Color = GREEN;
Hierarchy->ShowHierarchy();
Hierarchy->Free();
getch();
return 0;
}
This code creates an
object of type THierarchy and then shows you how to use the new
functionality of the ShowHierarchy method. I also create an instance of TMyObject so that you can compare the two
classes demonstrated so far in this chapter.
NOTE: I should perhaps mention that it is more
expensive to declare or call virtual methods than it is to call a static
method. As a result, you need to weigh the whole issue of whether you want
to declare a method to be virtual. In this section, you
have learned about the virtual directive. This subject's true
significance won't be clear until you read about polymorphism. However,
before you tackle that subject, it's best to learn more about inheritance
and encapsulation. In particular, the next section of the chapter looks at
more issues involving object design.
Searching
for the Right Design
It is almost impossible
to find the right design for an object the first time you write it. As a
rule, the only way you can figure out the design for an object is by
creating it, discovering its limitations, and then making improvements to
its design. In short, object design is an iterative process.
There is no good way to
step you through the process of discovering the correct object design in a
book, because the written word is by its nature static, and the process I'm
describing is dynamic. Furthermore, it's confusing to the reader to show a
series of poorly designed objects that are successively improved in each
iteration. The problem with this technique is that the reader keeps seeing
the wrong way to create an object and can easily pick up bad habits or
fundamental misconceptions about object design. Even worse, the reader
tends to get frustrated with having to unlearn the techniques they just
acquired in the previous example that are now revealed as being flawed.
To avoid the problems
outlined in the preceding paragraph, I will simply show you a second
version of the THierarchy object and explain why it contains changes
to the original version of the object you saw in the last section. This
process does not tell you much about how I discovered the flaws in the
object, but it will show you what the flaws are and how I got around them. The
main point to grasp is that object design is an iterative process, and that
the correct way to find the flaws in an object is to implement them once as
best you can, and then look for problems.
NOTE: There is a second school of thought that
states that you can find the correct design for an object before
implementing it. Proponents of this technique often suggest that a team be
split in two, with part of the members designing objects and the other part
implementing them. I have to confess that I've never actually discussed the
results of this technique with someone who has used it successfully, as all
my experience has been with programmers who use the iterative technique I
discuss here. (Some of these programmers have also tried the second school
of programming, but it did not work for them.) The first problem with
the original version of the THierarchy object became clear when I wanted to find
a method to clear the screen. When doing so, I needed to first set the text
and background color to which I wanted the screen to be cleared. As a
result, I added new properties and new set methods to the object. The new
property let me add a background color, and the set methods let me change
the text and background colors at the same time I assigned them to the
private data:
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};
};
The implementation of PrintString now looks like this:
void THierarchy::PrintString(AnsiString S)
{
char Temp[250];
sprintf(Temp, "%s\n\r", S.c_str());
cputs(Temp);
}
Note that I have
removed the code that changed the color of the text. This code is no longer
necessary as the color gets changed in the SetTextColor method.
The extremely
straightforward implementation of ClrScr looks like this:
void THierarchy::ClrScr()
{
clrscr();
}
NOTE: C++ allows you to declare methods inline, as shown by the SetTextColor and SetBackColor method declarations. Conversely,
you can also declare a method outside of the object, as I do in the case of ClrScr. This latter technique is
called an out_of_line declaration, but that has such a ghastly ring in my ear that I
refuse to use it. You can, in fact, implement a method outside an object
and make it inline by using the inline directive:
inline void SetTextColor(int Color)
{
FTextColor = Color;
textcolor(FTextColor);
} Inline methods usually
execute faster than regular methods because they are placed directly in
your code and do not require the overhead associated with a function call. Whether
you implement them inside or outside of a class declaration, you should
declare only very small methods as inline, and they should not contain
any loops. The next change I made
to THierarchy involved a desire to increase the flexibility of the object. In
particular, it would be great to be able to use this object even if you do
not descend from it directly. One way to do this is via multiple
inheritance, but that technology is not supported by VCL objects. I should
perhaps add that I am not particularly partial to multiple inheritance as a
technology for use in real-world applications.
A much simpler way to
make the object more flexible is to pass the ShowHierarchy method a copy of the object
whose hierarchy you want to explore:
void TMyObject::ShowHierarchy(TObject *AnObject)
{
TClass AClass;
AnsiString AClassName = AnsiString(AnObject->ClassName()).c_str();
PrintString(AClassName);
AClass = AnObject->ClassParent();
while (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
AClass = AClass->ClassParent();
}
}
The interesting thing
about this change is that it occurs at the level of the TMyObject implementation and declaration.
In other words, it represents changes not to THierarchy but to TMyObject. If this object were of more
earth-shaking import and if I had released TMyObject to the public, I could not have
made this sort of change because I would be breaking the code of those who
already called the ShowHierarchy method. There are, I suppose, three things
you can learn from this example:
1. Try not to publish any part of an object
hierarchy until you are sure you can live with its current public
interface. After making these
changes, I decided that the object was ready to see the light of day. As a
result, I moved it into its own file called MyObject.cpp. I then made a program called
OBJECT3 that tests this new arrangement. The results of this effort are
shown on the CD-ROM that accompanies this book in a program called OBJECT3.
The code for this program is shown in Listings 19.3 through 19.5. ///////////////////////////////////////
// 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
{
public:
TMyObject() : TObject() {}
void ShowHierarchy(TObject *AnObject);
virtual void PrintString(AnsiString S);
};
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};
};
#endif
Listing
19.4. The implementation for TMyObject and THierarchy are shown here in
MyObject.cpp.
///////////////////////////////////////
// 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 (AClass)
{
AClassName = AnsiString(AClass->ClassName());
PrintString(AClassName);
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();
}
Listing
19.5. The test program for the MyObject unit.
#include <vcl\vcl.h>
#include <conio.h>
#pragma hdrstop
#include "myobject.h"
USEUNIT("MyObject.cpp");
void main()
{
THierarchy *H = new THierarchy();
H->TextColor = YELLOW;
H->BackColor = BLUE;
H->ClrScr();
H->ShowHierarchy(H);
delete H;
getch();
}
Notice that I include
code in the test program that "uses" the MyObject unit. I did not insert this
code manually but instead used the program manager to add the MyObject module to the project. This is
the best way to proceed, as it allows you to avoid editing the make file.
The program itself
simply sets a background and text color for the output, then clears the
screen to those colors and shows the hierarchy to the user. The
next-to-last line in the program calls delete rather than Free. You can use either technique,
depending on the dictates of your taste and background.
Showing
the Hierarchy of a VCL Program
To really appreciate
the new objects developed in the last section, you need to add them to a
regular BCB application. For instance, if you use them in a standard Form1 class, here is the output you
get:
TForm1
TForm
TScrollingWinControl
TWinControl
TControl
TComponent
TPersistent
TObject
This shows the whole
hierarchy of class TForm1, starting with this, moving to TForm, back to TScrollingWinControl, and so on, all the way back to TObject.
As you know, VCL
objects do not support multiple inheritance. As a result, I could not add
the functionality of THierarchy to TForm by letting the object inherit
it. This is fine with me, because I use multiple inheritance only very
reluctantly.
Instead of multiple
inheritance, I prefer to use a technique called aggregation. In
aggregation, you add the object you want to use as a field of your current
object and then expose its methods either by publishing the whole object as
a property, or by wrapping the methods of the object inside methods of your
current object. This technique allows you to easily take precise control of
the functionality from the new object, and it tends to support clean,
bug-free programming.
Before I show you how
to use aggregation, I need to point out that it is not possible to use THierarchy directly in a Windows program
because THierarchy uses Conio.h. The question then becomes, "Is THierarchy designed in such a way that I
can descend from it and change its functionality so that it works with a
Windows program?"
Well, part of the
answer should be obvious. The key method that had to be virtual, the one
that had to be overridden to make this work, is PrintString. And indeed, PrintString is declared virtual, so it is possible to change THierarchy's stripes. In fact, you will
find that I also override the setters and the ClrScr method.
NOTE: You might feel that having to override so
many methods in order to change the output of this program is an excessive
amount of work when compared to the relatively simple task of rewriting the ShowHierarchy method from scratch. Indeed,
when looked at from this perspective, the whole task of creating an object
in this case seems fruitless. I readily confess that it would indeed have
been simpler to write three versions of a non-OOP method called ShowHierarchyBlackandWhite, ShowHierarchyColor, and ShowHierarchyWindows. Here is a ShowHierarchy method tailored for use with a
VCL form-based program:
void TForm1::ShowHierarchy()
{
TClass AClass;
Memo1->Clear();
Memo1->Lines->Add(AnsiString(ClassName()));
AClass = ClassParent();
while (True)
{
AnsiString S = AnsiString(AClass->ClassName());
Memo1->Lines->Add(S);
if (AnsiString(AClass->ClassName()) == "TObject")
break;
AClass = AClass->ClassParent();
}
} Clearly, this object is
easier to write than the objects I have produced here. The key reason I
created an object like THierarchy is simply that it illustrates how OOP
works. In real life, objects often contain 20, 30, or even 50 methods. When
seen in that light, having to override three or four methods does not seem
like such a big chore. Furthermore, you can often use delegation to solve
these kinds of problems. In Listings 19.6
through 19.9, you will find the code for the new version of the Object1.cpp module, as well as the code for
a standard VCL form-based program called HIERARCHY that uses the object. The
output from the program is shown in Figure 19.5.
FIGURE
19.5. The HIERARCHY form sports a TButton and a TMemo component.
Listing
19.6. The new code for the MyObject header file.
///////////////////////////////////////
// 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)
{ FList->Clear(); ShowHierarchy(AnObject); return FList; }
};
class __declspec(delphiclass) TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
protected:
virtual __fastcall void SetTextColor(int Color)
{ FTextColor = Color; textcolor(FTextColor); }
virtual __fastcall 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 __fastcall void SetTextColor(int Color)
{ FTextColor = Color; FMemo->Font->Color = TColor(FTextColor); }
virtual __fastcall void SetBackColor(int Color);
public:
TVCLHierarchy(TMemo *AMemo): THierarchy() { FMemo = AMemo; }
virtual void PrintString(AnsiString S)
{ FMemo->Lines->Add(S); }
virtual void ClrScr() { FMemo->Clear(); }
};
#endif
Listing
19.7. The new code for the MyObject implementation.
///////////////////////////////////////
// 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();
}
void __fastcall TVCLHierarchy::SetBackColor(int Color)
{
FBackColor = Color;
FMemo->Color = TColor(FBackColor);
}
Listing
19.8. The code for the HIERARCHY header file.
///////////////////////////////////////
// Main.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "MyObject.h"
class TForm1 : public TForm
{
__published:
TButton *ShowHierarchyBtn;
TMemo *Memo1;
TPanel *Panel1;
void __fastcall ShowHierarchyBtnClick(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall TForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern TForm1 *Form1;
#endif
Listing
19.9. The code for the HIERARCHY program.
///////////////////////////////////////
// Main.cpp
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete Hierarchy;
}
void TForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
When you run this
program, it will display the hierarchy of the TForm object in a memo control. It
does so by creating an aggregate of the THierarchy object and the TForm1 object.
Creating
Friend Objects
Here is the declaration
for the descendant of THierarchy that works in Windows:
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(); }
};
Several changes to this
program should jump right out at you. First, notice that the constructor
now takes a parameter. This parameter is the memo control that will be used
to display text to the user. The SetTextColor, SetBackColor, PrintString, and ClrScr methods have all been rewritten
to take advantage of this new control.
As a result, the only
parts of the original object that have come through unchanged are as shown
in the following pseudocode:
class THierarchy: public TMyObject}
{
int FTextColor;
int FBackColor;
public:
void ShowHierarchy(TObject *AnObject); // inherited from TMyObject
__property int TextColor={read=FTextColor,write=SetTextColor};
__property int BackColor={read=FBackColor,write=SetBackColor};
};
In this particular case,
it is not possible to use the TextColor and BackColor properties to access the
private FTextColor and FBackColor data stores. Instead, I declare TVCLHierarchy to be a friend of THierarchy:
class TVCLHierarchy;
class THierarchy: public TMyObject
{
friend TVCLHierarchy;
int FTextColor;
int FBackColor;
... // Code omitted here
}
Notice that TVCLHierarchy is declared as a friend in the
first line of the THierarchy declaration. This means that TVCLHierarchy has access to the private data of THierarchy. Descendants of TVCLHierarchy will not inherit this trait.
Using
Aggregation
Rather than use
multiple inheritance to access TVCLHierarchy, TForm1 declares a field of this type:
class TForm1 : public TForm
{
... // Code omitted here
TVCLHierarchy *Hierarchy;
void ShowHierarchy();
public:
virtual __fastcall TForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
I have also added a
method called ShowHierarchy and a new property called Hierarchy so that descendants of TForm1 can have access to the BackColor and TextColor properties of TVCLHierarchy.
A fine point of object
design could be debated here. In this particular case, I have created a
property of type TVCLHierarchy that is exposed to consumers of TForm1. Alternatively, I could have
completely hidden TVCLHierarchy behind a set of properties that provided
access to the FTextColor and FBackColor fields of THierarchy:
class TForm1 : public TForm
{
... // Code omitted here
TVCLHierarchy *Hierarchy;
void ShowHierarchy(TObject *Object);
int GetHierarchyBackColor();
void SetHierarchyBackColor(int Color);
int GetHierarchyTextColor();
void SetHierarchyTextColor(int Color);
public:
virtual __fastcall TForm1(TComponent* Owner);
__property int HierarchyTextColor=
{read=GetHierarchyTextColor, write=SetHierarchyTextColor};
__property int HierarchyBackColor=
{read=GetHierarchyBackColor, write=SetHierarchyBackColor};
};
This technique is very
pure from an OOP-based point of view, and it is perhaps a more classic and
complete example of aggregation than the one I use. However, it is not
strictly necessary to create the HierarchyTextColor and HierarchyBackColor properties. Instead, I can make
the Hierarchy object public by making it a property of type TVCLHierarchy. The reason this approach is
acceptable is because FHierarchy, as well as the FTextColor and FBackColor fields of THierarchy, are all still hidden behind
properties.
It would be wrong, however,
to make FHierarchy public or to allow users to directly access the FTextColor or FBackColor fields of THierarchy. The reason this is wrong is
because it exposes your data to the public, thereby limiting your choices
when it comes time to maintain or improve your program.
The technique shown
here of exposing TVCLHierarchy via a property shows up everywhere in the
VCL. Most notably, it is present in the Items or Lines fields of TListBox, TComboBox, and TMemo. The point here is that you
want to use properties to protect your data and your implementation, but
you don't want to take things so far that your code becomes needlessly
bloated.
However, I do not feel
that it would be incorrect or wrong to completely hide FHierarchy from consumers of TForm1 and to instead expose its
properties via properties of TForm1. The great advantage of this technique
would be that it would allow a seamless aggregation of TForm1 and TVCLHierarchy.
NOTE: If you sense me waffling a bit here, that
is because I do not believe there is a hard and fast answer as to what is
best in a situation like this. It happens that there are good arguments on
both sides. Component design is an art, not a science. The moment it
becomes a science, most programmers are going to be out of a job.
In the constructor for TForm1, you should create the TVCLHierarchy object:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
As you can see, I have
chosen to hard code default values for the BackColor and TextColor properties of TVCLHierarchy into this constructor. This is
not necessarily the best thing to do from the point of view of a descendant
of TForm1,
but it will not cause any problems in the simple example I am creating
here.
At last, I am able to
create a wrapper for the ShowHierarchy method:
void TForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
Once again, you could
probably just as easily let consumers of TForm1 access this method via the Hierarchy property. However, I provide
this method in part because it is easier to use than a property, and in
part because I want to illustrate explicitly how aggregation works.
Finally, I have
included a method of TForm1 that calls the TForm1::ShowHierarchy method:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
This method does
nothing to forward my example of aggregation, and I include it solely for
expediency's sake. A real-world example of aggregation would leave TForm1 without features of this type
and would instead expect you to inherit from it in order to access the
functionality exposed via Hierarchy and ShowHierarchy. For an example of this type of
program, see the HIERARCHY2 program in the Chap19 directory.
A reader could object
that rather than using aggregation, I could have declared a single method
that looked like this:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
THierarchy *Hierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
Hierarchy->ShowHierarchy();
delete Hierarchy;
}
Once again, I have to
confess that this probably is a simpler technique in this particular case. However,
if you created descendants of TForm1, aggregation would allow them to call the ShowHierarchy method with one line of code. It
would, in effect, add the functionality of ShowHierarchy to TForm, so that TForm1 appeared to be an "aggregate"
of two objects. Admittedly, the simplicity of the example I show here makes
these advantages difficult to discern, but when you are working with
larger, more complex objects, this technology really starts to shine. OOP
is about managing complexity, but it is easier to teach OOP if you use
simple examples.
By this time, you
should have a fairly good grasp of inheritance and hierarchies. The key
point to understand is that, in general, a child object inherits the
capability to use any of its parent's methods. In other words, it comes
into an inheritance, where the inheritance is the methods, fields, and
properties of its parent. Except for TObject itself, all VCL objects have
parents that can trace their roots back to TObject.
Form
Inheritance and Aggregation
Form inheritance is a
powerful technique used by VCL programs. Here is a description of how to
use it.
When you are done, the
declaration for your main form should look like this:
///////////////////////////////////////
// HierarchyForm.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef HierarchyFormH
#define HierarchyFormH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
class THierarchyForm1 : public TForm
{
published:
TMemo *Memo1;
TPanel *Panel1;
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
protected:
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall THierarchyForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern THierarchyForm1 *HierarchyForm1;
#endif
Go into the declaration
for HierarchyForm1 and make sure that ShowHierarchy is declared in the public or protected
section.
Now add a button to the
main form called ShowHierarchy and add the following event handler to its OnClick event:
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
Now run the program, and when you click on ShowHierarchy, you should get the following result: TForm1
THierarchyForm1
TForm
TScrollingWinControl
TWinControl
TControl
TComponent
TPersistent
TObject
As you can see, the hierarchy now goes from TForm1 to HierarchyForm1 to TForm, and so on. TForm1 is descended from THierarchyForm1, and it therefore inherits the ability to all the things a regular form can do, plus it can show its own hierarchy. Once again, there are a number of quibbles you could make here. For instance, it might be better to remove the visual controls from the HierarchyForm, and so on. However, the main point of this exercise is merely to illustrate how form inheritance works, and the code shown here does that. For the sake of clarity, I have included the complete code to the
program in Listings 19.10 through 19.14. Remember that you need to use the
visual tools to inherit from the HierarchyForm,
or Form1 will not have the
correct appearance. ///////////////////////////////////////
// HierarchyForm.h
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef HierarchyFormH
#define HierarchyFormH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
class THierarchyForm1 : public TForm
{
__published:
TMemo *Memo1;
TPanel *Panel1;
void __fastcall FormDestroy(TObject *Sender);
private:
TVCLHierarchy *FHierarchy;
protected:
void ShowHierarchy(TObject *Object);
public:
virtual __fastcall THierarchyForm1(TComponent* Owner);
__property TVCLHierarchy *Hierarchy=
{read=FHierarchy, write=FHierarchy};
};
extern THierarchyForm1 *HierarchyForm1;
#endif
Listing 19.11. The main form for the HierarchyForm. ///////////////////////////////////////
// HierarchyForm.cpp
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "HierarchyForm.h"
#pragma resource "*.dfm"
THierarchyForm1 *HierarchyForm1;
__fastcall THierarchyForm1::THierarchyForm1(TComponent* Owner)
: TForm(Owner)
{
FHierarchy = new TVCLHierarchy(Memo1);
Hierarchy->BackColor = clBlue;
Hierarchy->TextColor = clYellow;
}
void __fastcall THierarchyForm1::FormDestroy(TObject *Sender)
{
delete Hierarchy;
}
void THierarchyForm1::ShowHierarchy(TObject *Object)
{
Hierarchy->ShowHierarchy(Object);
}
Listing 19.12. The header for the programs main form. ///////////////////////////////////////
// Main.h
// Hierarchy2
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include "Myobject.h"
#include "HierarchyForm.h"
class TForm1 : public THierarchyForm1
{
__published:
TButton *ShowHierarchyBtn;
void __fastcall ShowHierarchyBtnClick(TObject *Sender);
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 19.13. The main form for the program. ///////////////////////////////////////
// Main.cpp
// Hierarchy2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "HierarchyForm.h"
#pragma link "HierarchyForm"
#pragma link "HierarchyForm"
#pragma resource "*.dfm"
TForm1 *Form1;
//--------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: THierarchyForm1(Owner)
{
}
void __fastcall TForm1::ShowHierarchyBtnClick(TObject *Sender)
{
ShowHierarchy(this);
}
//--------------------------------------------------------------------------
Listing 19.14. The project source for the HIERARCHY2 program. #include <vcl\vcl.h>
#pragma hdrstop
USERES("Hierarchy2.res");
USEFORM("Main.cpp", Form1);
USEFORM("HierarchyForm.cpp", HierarchyForm1);
USEUNIT("..\..\Utils\MyObject.cpp");
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->CreateForm(__classid(THierarchyForm1), &HierarchyForm1);
Application->Run();
}
catch (Exception &exception)
{
MessageBox(NULL, exception.Message.c_str(), Application->Title.c_str(),
MB_OK);
}
return 0;
}
I have included the complete source for this program so you can
double-check your work against it in case anything goes wrong. The key
parts you need to concentrate on are the class declarations in the headers,
the declarations for the global variable for the HierarchyForm, and the headers for the
various methods used in the forms. The actual implementation of the methods
was already cleared up in the HIERARCHY program, so you don't need to check
them. The HIERARCHY2 program is shown in Figure 19.6. You should spend some time adding controls to the HierarchyForm and watching how they then automatically appear on the main form for the program. Notice that if you move a control on the HierarchyForm, the corresponding control on the main form will also move. If you move a control on the main form, however, that breaks the connection between that control and the corresponding control on the HierarchyForm. To restore the connection, right-click on the control in the main form and choose Revert To Inherited from the menu. You can disconnect and revert properties one at a time if you wish. For instance, if you move the left side of a component on the main form, that will break the connection for that one property, but it will leave the remaining properties still connected to the HierarchyForm. To restore the Left property, choose that property in the Object Inspector with the right mouse button and select Revert To Inherited from the menu. To test this, you might want to drop a single button in the middle of the HierarchyForm, then switch back to the main form and work on changing just one of the inherited button's properties, such as its Left property or its Caption. SummaryIn this chapter, you have had a good chance to start working with object-oriented programming. However, there are still several big topics to tackle, including encapsulation and polymorphism. Rather than trying to cover such big topics inside this already lengthy chapter, I have decided to break things up and give them their own chapters where they can have plenty of room to unfold naturally. Plenty of material was covered in this chapter, including inheritance, virtual methods, aggregation, and form inheritance. If you are new to this material, it would only be natural that some of it did not sink in the first time around. If so, you can either re-read the chapter or go on to the next chapter and see if some of it doesn't start to become clear when you work with new examples that approach the material from a slightly different angle. Inheritance and virtual methods are everywhere in the BCB, and the more you work with them, the easier it will be to understand the principles involved. However, this is material that you must master if you want to be good at using BCB--and particularly if you want to start creating your own components.
VMS Desenvolvimentos
Diversas Dicas, Apostilas, Arquivos Fontes,
Tutoriais, Vídeo Aula, Download de Arquivos Relacionado a Programação em C++ Builder.
|