Introduction
This article is my humble attempt at explaining how Inheritance and Polymorphism can be used to help you use an object orientated guideline for creating classes and creating descendants of these classes and a cool trick for creating the correct instance of the descendant class you want to create.
Background
Whether you know it or not, we use inheritance every time we use Delphi. When you create a new form, you are in fact using inheritance.
TForm1 = class(TForm)
end;
TForm1
is inheriting from the base class TForm
. This is how your new form knows exactly how to be a form (like how it must be drawn, what events it has, what it can do, etc.) without you needing to do anything extra. But how can you use this to make your applications more robust and leverage this kind of practice? Well, this is what I am hopefully going to help demystify.
What the Code Does
All the demonstration application does is draw two types of shapes in 4 different colours. I start by making a (base) class, TShape
, and create two descendant classes TEllipse
and TRectangle
. The aim of TShape
is to draw a shape onto a TPaintBox
component. When the TShape
class is defined, we know the following information:
- What color I need to draw with
- The co-ordinates where that needs to be drawn
So the class definition looks like this:
TShape = class(TObject)
private
FTop : Integer;
FBottom : Integer;
FRight : Integer;
FLeft : Integer;
FBrushColor : TColor;
procedure SetTopLeft(const Value : TPoint);
function GetBottomRight : TPoint;
function GetTopLeft : TPoint;
procedure SetBottomRight(const Value : TPoint);
public
constructor Create(
const ABrushColor : TColor;
const ATop : Integer;
const ALeft : Integer); overload;
constructor Create(
const ABrushColor : TColor;
const ATop : Integer;
const ALeft : Integer;
const ARight : Integer;
const ABottom : Integer); overload;
constructor Create(
const ABrushColor : TColor;
ATopLeft : TPoint); overload;
constructor Create(
const ABrushColor : TColor;
ATopLeft : TPoint;
ABottomRight : TPoint); overload;
procedure Draw(
ACanvas : TCanvas); virtual; abstract;
property Top : Integer read FTop write FTop;
property Left : Integer read FLeft write FLeft;
property Right : Integer read FRight write FRight;
property Bottom : Integer read FBottom write FBottom;
property TopLeft : TPoint read GetTopLeft write SetTopLeft;
property BottomRight : TPoint read GetBottomRight write SetBottomRight;
property BrushColor : TColor read FBrushColor;
end;
The only thing the TShape
object will not know is actually HOW to draw itself. This is where Polymorphism comes in. In the TEllipse
and TRectangle
, you will see an overridden method Draw
. This is how the TShape
will know how to actually do what it needs to do. And the definition for them looks like this:
TEllipse = class(TShape)
public
procedure Draw(
ACanvas : TCanvas); override;
end;
procedure TEllipse.Draw(
ACanvas: TCanvas);
begin
ACanvas.Brush.Color := BrushColor;
ACanvas.Ellipse(TopLeft.X, TopLeft.Y, BottomRight.X, BottomRight.Y);
end;
TRectangle = class(TShape)
public
procedure Draw(
ACanvas : TCanvas); override;
end;
procedure TRectangle.Draw(
ACanvas: TCanvas);
var
LRect : TRect;
begin
ACanvas.Brush.Color := BrushColor;
LRect.TopLeft := TopLeft;
LRect.BottomRight := BottomRight;
ACanvas.Rectangle(LRect);
end;
And how that Draw
method is implemented will determine how the shape is actually drawn. If you decide to build your own descendant from the TShape
object, you will know what I mean :).
PLEASE NOTE: The code in the source files has a lot of commenting in it to help explain what is happening. Please read the code to get a better indication of why I did things the way I did...
Now, in the main form, I need to ensure a few things:
- What colour I must use
- What shape I must use
- What are my co-ordinates so I know where to draw
- How many objects do I actually need to draw
This is why I use specific events and variables on my main form:
- 2 variables to know where the points of my shape need to be
- The mouse up and down events (on the paint box) to ensure that I can set the points of the shape correctly
- An object which will hold all the
TShape
objects I need
- Ensure that any
TShape
objects that are created are destroyed correctly
As such, my main form looks something like this:
TMainFrm = class(TForm)
procedure PaintBoxMouseDown(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X : Integer;
Y : Integer);
procedure PaintBoxMouseUp(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X : Integer;
Y : Integer);
procedure PaintBoxPaint(
Sender : TObject);
procedure FormCreate(
Sender : TObject);
procedure FormDestroy(
Sender : TObject);
private
FTopLeft : TPoint;
FBottomRight : TPoint;
FShapes : TObjectList;
end;
With the implementation looking like this:
procedure TMainFrm.PaintBoxMouseDown(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X : Integer;
Y : Integer);
begin
FTopLeft.X := X;
FTopLeft.Y := Y;
end;
procedure TMainFrm.PaintBoxMouseUp(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X : Integer;
Y : Integer);
var
LColor : TColor;
LShapeRef : Ref_Shape;
begin
case ComboBox1.ItemIndex of
0 : LShapeRef := TEllipse;
1 : LShapeRef := TRectangle;
else
LShapeRef := TEllipse;
end;
case ComboBox2.ItemIndex of
0 : LColor := clRed;
1 : LColor := clGreen;
2 : LColor := clBlue;
3 : LColor := clBlack;
else
LColor := clRed;
end;
FBottomRight.X := X;
FBottomRight.Y := Y;
FShapes.Add(LShapeRef.Create(LColor, FTopLeft, FBottomRight));
PaintBox.Repaint;
end;
procedure TMainFrm.PaintBoxPaint(
Sender : TObject);
var
LShape : TShape;
i : Integer;
LCanvas : TCanvas;
begin
LCanvas := TPaintBox(Sender).Canvas;
LCanvas.Brush.Color := clWhite;
LCanvas.FillRect(TPaintBox(Sender).Canvas.ClipRect);
for i := 0 to FShapes.Count - 1 do
begin
LShape := TShape(FShapes.Items[i]);
LShape.Draw(LCanvas);
end;
end;
procedure TMainFrm.FormCreate(
Sender : TObject);
begin
FShapes := TObjectList.Create(True);
end;
procedure TMainFrm.FormDestroy(
Sender : TObject);
begin
FreeAndNil(FShapes);
end;
Once again, for simplicity's sake, I left out the comments. To see how this all ties in, all you need to do is run the accompanying application in the download and you are good to go :).
Points of Interest
Inadvertently, you have now been introduced to the Template Design Pattern. It leverages the fact that the base class(es) you define do certain common things for you that your descendant classes use so that you don't need to re-code them. This enables you to have cleaner, more maintainable code (because instead of the common code being rewritten in several places, it is now only in one place). It also allows you to design your descendant classes to focus on what they were actually created for without worrying about the common methods/actions/properties because that has already been catered for in the ancestor class.
You will also see in my code that I have a really strange declaration:
Ref_Shape = class of TShape;
This is called a class reference declaration and what that means is that you can create an instance of a TShape
descendant using a variable of type Ref_Shape
. Which is why you will see the code the way you do in the PantBoxMouseUp
event. Cool trick hey? :).
For a more detailed example of how to use the topics I raised in this article, please take a gander at The Configurator[^].