Custom Component Development in Delphi
Custom Component Development in Delphi
Fullscreen
Accessing protected members of a component
Many Delphi components have useful properties and methods that are marked invisible
("protected") to a Delphi developer. In this article you will find the workaround to this
problem - thus enabling you to access a DBGrid's RowHeights property, for example.
Creating Custom Delphi Components - Inside and Out
This tutorial will explain component writing to you, which should result in more code reuse.
It will go over properties, events and methods, and will also explain how to install
components. The final part of this tutorial is about Object-Oriented design.
Creating Custom Delphi Components, Part I
This first part demonstrates some of the best approaches to building components, and at the
same time provides tips on deciding on the best base class to inherit from, using virtual
declarations, the complexities of overriding, and so on.
Creating Custom Delphi Components, Part II
Quite often it is necessary to write components that perform more advanced functions.
These components often need to either reference other components, have custom property
data formats, or have a property that owns a list of values rather than a single value. We will
explore various examples covering these very subjects, starting with the most simple.
Ads
More resources
First, if you want more, consider buying a book on Developing custom components.
Second, why not try locating an existing (with source perhaps) component you are looking
for.
Third, when you are 100% sure there is no such question on custom component
development you can't answer...there will be something that you don't know. Everything
you have to do is to ask a question on the Delphi Programming Forum and wait for answers.
If you still need more samples .. try the second page ...
Articles, papers, tutorials
Here is a list of articles that deal with custom component development in Delphi.
Delphi, how to add properties, methods and custom events to them, how to wrap
them around DLLs, how to install them, how to design a palette bitmap and write online help to support the component user.
continue reading below our video
Let's say we need to hack a DBGrid. What we want to access is the RowHeights property.
Make sure you are editing the unit where the DBGrid is declared (for example: DBGrid1 dropped on
Form1, so we are editing the Unit1.pas)
Create a noticeably worthless derived class:
THackDBGrid = class(TDBGrid);
Apparently, the class THackDBGrid derived like this does not bring any new functionality to DBGrid.
Though, it does provide the *free* access to all the protected members of the DBGrid component.
To use once protected members, you must typecast the component you refer too to the new (descendant)
class.
Now, let's see some Delphi code. Assume you have a Delphi form (Form1) with a DBGrid (DBGrid1) displaying some
data. To access the protected RowHeights property of that DBGrid you first hact the DBGrid comonent. Here's the
portion of the Unit1.pas unit - where Form1 is declared:
uses
Windows, Messages, ..., Grids, DBGrids, ... ;
type THackDBGrid = class(TDBGrid);
type
TForm1 = class(TForm)
DBGrid1: TDBGrid;
....
Note the THackDBGrid declaration! Now, to change the height of the title row of the DBGrid1 you simply typecast
DBGrid1 to THackDBGrid like:
//makes the Title's row 20 px in height (if displayed)
THackDBGrid(DBGrid1).RowHeights[0] := 20;
Ok, that's it. RowHeights?! Yes, the RowHeights property of a TCustomGrid (the TDBGrid derives from) gives the
height (in pixels) of all rows in the grid. In TDBGrid this property is protected - in THackDBGrid it is editable!
Note that the next assignemet gives the compile time error: "Undeclared identifier: RowHeights".
DBGrid1.RowHeights[0]:=20;
It's working! ... Howcome?
Yep, it is. This is why: the THackDBGrid automatically inherits all the protected members of the TDBGrid (and all
public, private, etc. of course), due to the fact that THackDBGrid is in the same unit as the code that accesses the
protected property, the protected property is available.
Note: do not think that you can define the THackDBGrid in another unit - the code will no longer compile.
Another approach - no type-casting.
If you need to access the protected members of a component but do not want to use type casting, you can declare
the new (hacked) class with the same name as the old class:
type TDBGrid = class(DBGrids.TDBGrid);
Now, when accessing any protected member, you do not need to use the THackDBGrid(DBGrid1).Something
syntax; Delphi will use the modified component as if it were the old one. Therefore a line like
DBGrid1.RowHeights[0]:=20; will compile.
Warning!
This is a hack - a very bad OOP technique. There are reasons that components do not expose certain members of
their class declaration. Use the technique explained in this article at your own risk.
Note: "a hack" - means that it breaks standard OOP practices. This "hack" works (and will work in future Delphi
version) because it relies on a specific Delphi Pascal OOP feature.
Now, the read and write parts can specify direct data access (by putting the variable name) or via
procedures/functions (by putting the procedure/function name). To give another example, here's a read-only
property that gets its data from a function:
property SomeReadOnlyProperty: Boolean read GetMyValue;
This property cannot be written to, which safeguards the data nicely :-). The GetMyValue function would be of the
form: "function GetMyValue: Boolean;" Whenever the property is accessed, it would call this function to retrieve
the value... so the code "if SomeObject.SomeReadOnlyProperty = True then" would get translated into "if
SomeObject.GetMyValue = True then" behind the scenes.
Important note!
The variables (e.g. FMyInteger) and procedures/functions (e.g. GetMyValue, SetMyInteger) are not available to the
outside world! They are hidden away using "private" or "protected", so any direct calls would result in errors. This
is good, as otherwise the entire purpose of read/write specifiers would be defeated. Private, protected and other
such keywords will be explained (much) later.
Advantages of Properties
You should have noticed why properties are pretty cool, but here's a summary:
The first one is pretty obvious, really. If you declared your variables publicly they could be changed in any old
way. However, you can still allow direct access to your variables if you so desire. Properties allow you to change
the behind-the-scenes way of getting or setting data without having to rewrite any code that uses the component!
The second point is something that makes you really appreciate Delphi. In other languages (for example C++) you
do need to say "someObject.setThisValue(23)" everywhere. This makes your code less readable and just plain
sucks! It, in my view, reduces the readability as it detracts from your main intent: you want to change the value,
not call the function! Delphi has provided an elegant mechanism to avoid this kind of unnecessary code.
The last point is good for efficiency - if you have a graphical component, for example, you want to reduce the
drawing as much as possible. This can be done by a set procedure that checks if the old value is different from the
new value. Here's an example:
procedure TSomeComponent.SetBackgroundColour(NewColour: TColor);
begin
if NewColour <> FBackgroundColour then
begin
FBackgroundColour := NewColour;
Invalidate; // redraws the component
end;
end;
This means that your component will only redraw itself if it gets a different property value. This technique can be
applied elsewhere, with error checks and other such data checking.
Methods
Methods are very important! These are procedures or functions, and define the behaviour of your objects. They
are defined in the usual way, with one-line prototypes in the component and definitions later on.
Events
Events get defined by you, and are called whenever you want to notify the programmer of something. You can
usually ignore these until you get more experienced with components. They boil down to pointers to class
procedures, which means slight complications. Events will be covered later, so don't worry too much about them.
Question, Suggestions...
If you have any questions or comments to this (huge) article, please post them on the Delphi Programming Forum.
Discuss!
TComponent
TGraphicControl
TCustomControl
Form-type object
TCustomForm
This is, of course, a small snippet of the possible cases you might need. We're interested in a small visual control, so
TGraphicControl is the best base class for us. There's no need for TCustomControl because our component won't
need window focus.
We can get started now. Go to File->New and select component. You'll be presented with a dialogue box. Fill it out
like this:
The only two things you need to fill out are the "Ancestor type" and "Class name" boxes. The rest get filled out
automatically. Once you've done that, click the OK button and you get transported to the code editor with a
skeleton class written out.
First we need to let the compiler know what we'll be using. Each unit (a file, usually containing a class) has a "uses"
clause. This lists what other files will be needed to compile the unit. Without this the compiler would have a really
hard job so it's up to us to make sure it's correct.
Note:
The following section about adding Graphics to the uses clause is relevant for Delphi 6 (and possibly Delphi 5).
Delphi 4 adds Graphics automatically so if you start a new component and the uses clause has "graphics" in it then
you don't need to do the next part.
You usually don't notice the "uses" clause as it's filled out with a large number of units by default (such as classes,
sysutils, etc.). These units include most of the functionality you require. However, sometimes you have to add your
own units (which usually listed in the help files for the appropriate class/procedure/function/type). We'll be
needing the type TColor, which represents a colour. If you look this up in the help file you'll see it's within the
'Graphics' unit. Let's add that now before we start. Go to the very top of the file and find the uses part. It will look
like this:
// <-- name of file without .pas
unit Line;
interface
uses
Windows, Messages, SysUtils, Classes, Controls
You can see that each unit needed is simply separated by a comma. We'll add Graphics to the uses unit. Make this
change:
uses
Windows, Messages, SysUtils,
Classes, Controls, Graphics; //(note added unit)
Question, Suggestions...
If you have any questions or comments to this (huge) article, please post them on the Delphi Programming Forum.
Discuss!
Line width
Line colour
Direction
This is where the whole idea of customisation comes in. If you want to, you could add in any variables you think
are necessary... you could add in code to draw dotted lines, for example. However, we'll stick with the basics for
now.
One thing you should have spotted is that we don't actually need to store the line width and colour, as they are
properties of the canvas's pen. This is very handy. However, we will need to add in a variable for the direction, as
well as get and set functions. Change the private section of the component to look like this:
private
{ Private declarations }
FLineDir: TLineDirection;
function GetLineWidth: Integer;
function GetLineColour: TColor;
procedure SetLineWidth(const NewWidth: Integer);
procedure SetLineColour(const NewColour: TColor);
procedure SetLineDir(const NewDir: TLineDirection);
Even though we haven't got variables for the line width and colour, we still need functions to set and retrieve the
values from the component's canvas. This is because these three values will be properties of TLine, and properties
want you to say how they will read/write, remember? Note we don't need a get function for FLineDir because we
can just let the property read its value directly from the variable.
The "F" before LineDir is a bit of notation. It stands for "field", and just means the variable belongs to the class. This
makes it a bit easier when writing your component to avoid mixing up local variables with class members.
Right, we've declared the functions so I suppose we'd better implement them. Bung this code in after the
implementation section (look for the "implementation" keyword in your file and add this immediately after it):
function TLine.GetLineWidth: Integer;
begin
Result := Canvas.Pen.Width;
end;
function TLine.GetLineColour: TColor;
begin
Result := Canvas.Pen.Color;
end;
procedure TLine.SetLineWidth(const NewWidth: Integer);
begin
if NewWidth <> Canvas.Pen.Width then
begin
Canvas.Pen.Width := NewWidth;
Invalidate; // redraws the component
end;
end;
procedure TLine.SetLineColour(const NewColour: TColor);
begin
if NewColour <> Canvas.Pen.Color then
begin
Canvas.Pen.Color := NewColour;
Invalidate;
end;
end;
procedure TLine.SetLineDir(const NewDir: TLineDirection);
begin
if NewDir <> FLineDir then
begin
FLineDir := NewDir;
Invalidate;
end;
end;
The get functions are very self-explanatory. They just nab the values from the Canvas and pass them along.
However, the set functions are slightly more complex than just setting the variables.
When the set function gets called, we only want to change the appropriate value if necessary. After all, since it's a
visual control any unnecessary changes will be apparent to the user (e.g. setting the line direction to the same
value again shouldn't cause a redraw). Each of the set functions checks if the new value is different to the current
one. If, and only if, it is then we need to change the value and cause a redraw.
Adding Properties
The next step is to add properties to the component. We've laid the groundwork so it's simply a matter of bunging
them in, like so:
published
{ Published Declarations }
property Direction: TLineDirection read FLineDir write SetLineDir;
property LineColour: TColor read GetLineColour write SetLineColour;
property LineWidth: Integer read GetLineWidth write SetLineWidth;
There should be no great surprises in there as you've seen similar property declarations in the previous tutorial
part. Now, believe it or not, we've very nearly finished the component!
A quick note about "published": any properties in this section will be available in the Object Inspector at designtime. This is, of course, what we want. If you don't want the properties published (if, for example, you only create
the component at run-time) then you can place properties in the "public" section. Properties there will be available
to outside code, just like published properties, but will not appear in the Object Inspector. If you see "run-time"
properties in the help files, it means they're public.
The next part is pretty fundamental to TLine - its Paint procedure. We need to draw the component, so it's time to
add in some code for that. Since our component has TGraphicControl as a base class it gains a Paint procedure. We
just need to modify this to draw our code and we're almost there. Add in this code to the component's "Protected"
section:
protected
{ Protected declarations }
procedure Paint; override;
The override part is a fancy bit, and means our procedure will get called instead of the base class's Paint procedure.
We can write the good stuff now, namely the drawing procedure. Add this code in after the implementation part:
procedure TLine.Paint;
var
start: Integer;
begin
inherited;
case FLineDir of
drLeftRight:
begin
start := (Height - Canvas.Pen.Width) div 2;
Canvas.MoveTo(0, start);
Canvas.LineTo(Width, Start);
end;
drUpDown:
begin
start := (Width - Canvas.Pen.Width) div 2;
Canvas.MoveTo(start, 0);
Canvas.LineTo(start, Height);
end;
drTopLeftBottomRight:
begin
Canvas.MoveTo(0, 0);
Canvas.LineTo(Width, Height);
end;
drTopRightBottomLeft:
begin
Canvas.MoveTo(Width, 0);
Canvas.LineTo(0, Height);
end;
end;
end;
The only thing you shouldn't understand is the "inherited" bit. Because the Paint procedure belongs to the base
class and we're overriding it, we don't want to miss out on important bits the base class does. The base class here
might need to do things like set up the canvas to draw on, get device contexts, allocate resources, or whatever. If
we don't let the base class do its thing by calling it via "inherited" we might just find our code blows up. Best not to
let that happen, eh?
And that, my friend, is enough for a working component. Save the file and go to the Component->Install
Component menu, select your file and say OK when it asks if you want to rebuild your library. If all goes well it
should tell you that TLine has been registered. You can find it on the Samples palette page and try messing around
with it.
Question, Suggestions...
If you have any questions or comments to this (huge) article, please post them on the Delphi
Programming Forum. Discuss!
You should be able spot why this would be bad - what if you forget to call Init or Squish? You would end up with an
exception or memory leak (bad news) and would look like a fool. Well, luckily, constructors and destructors can
help here. Oh, carefully forget to notice that I actually used constructors and destructors in the above example ;-).
It's kinda hard in Delphi to avoid them.
Constructors
Constructors get called whenever you create an object. Their job is to set up the internals of the object (variables,
pointers, dynamic memory, etc.) as appropriate. The constructor is used to ensure you have a valid object - it
should do no more or less than this. If you find after calling a constructor you still need to call some Init function
then you haven't written an appropriate constructor, and should change it.
All objects is Delphi have a standard constructor ("Create"), which they get from TObject. This can be used to create
the object, like so:
SomeObject := TThisClass.Create;
where TThisClass is SomeObject's type. You must do this with classes before using them or you will get an
exception. When you use the constructor the object gets space allocated for it and can be used. What if you want
to change this constructor? Well, you can do this quite easily.
Every constructor can get declared like a procedure, but with "constructor" replacing "procedure" in the
declaration. Since every object has a "create" constructor available, why not modify this to suit your purposes?
Here's an example:
type
TSomething = class(TObject)
public
{ Public declarations }
constructor Create;
// etc.
end;
implementation
// and the actual constructor definition
constructor TSomething.Create;
begin
inherited;
// do whatever you need (allocate memory, initialise variables, etc.)
end;
The above is the simplest constructor you might think about. There's only one thing that needs discussing "inherited".
Inheritance is a concept in object-oriented design where a class can be a base for other classes. An example might
be a "creature" class, which you could use as a base for "cat" and "dog" classes. The cat and dog would get data
and behaviour from the "creature" base class, such as moving, having legs, fur, and so on, and would change this to
suit their own needs (such as adding "woof" behaviour for dogs). The first class from which others derive is called
the "base class" (or "super-class") and the ones that inherit from it are called "derived classes" (or "sub-classes").
Since every class is a sub-class of TObject, they automatically get its properties, methods, events, etc. (a slight
exaggeration, as you'll see later). This includes the standard constructors and destructors ("Create" and "Destroy").
Since we have added our own code for the TSomething constructor the other code won't get executed - if you
create a TSomething then TSomething.Create will get called, not TObject.Create. This is bad, as the base class might
be doing clever/important things. Using the "inherited" keyword lets you call the base class's same method. This
works for any constructor/destructor/procedure/function ("method").
Note that constructors can have any name, and any parameters. You're not just limited to "Create" and "Destroy",
so another potential constructor might look like this:
type
TMyClass = class(TSomething)
public
constructor InitWithStuff(const x: String);
end;
implementation
constructor TMyClass.InitWithStuff(const x: String);
begin
inherited Create;
// do something with your parameter "x"
end;
I'll first of all point out the slight change to "inherited". By default, inherited wants to use a method in the base
class with the same name and parameters. If there isn't one, you can specify the method manually, as I've done
here. You can also pass in parameters - you could, for example, do:
inherited SomeProcedure(x,y,z);
Now then, the above code makes a sub-class of TSomething, called TMyClass. It now has another constructor called
"InitWithStuff". This is NOT available in TSomething, as it has only been introduced in this new class. Note now that
TMyClass has two constructors, not just one! You could create a TMyClass like this:
someObject := TMyClass.InitWithStuff('hello');
As you'd expect, this uses the constructor "InitWithStuff" we've just added to this new class. However, remember
that TMyClass still has the Create constructor. This constructor didn't magically disappear when we created the new
class - it's a method of TSomething (from TObject), which means that it's a method of this new derived TMyClass as
well. This means it's syntactically correct to create a TMyClass like this:
someObject := TMyClass.Create;
However, this isn't such a good idea. If an object gives you a constructor you should use it! Because we are
bypassing TMyClass's constructor (it really wants us to use InitWithStuff) and are instead using the base class's
version (TSomething.Create) we could be creating problems (excuse the bad pun :-p). The object we want might be
missing out on some vital work work it needs because we're not using the thing provided. This leads to an
important tip:
It's usually best to use the same name for constructors for every derived class
You can have two constructors (or more) in one class. This means one potential way to get around the problem
might be this:
type
TMyClass = class(TSomething)
public
constructor Create;
constructor InitWithStuff(const x: String);
end;
implementation
constructor TMyClass.Create;
begin
raise Exception.Create('You must use InitWithStuff to create a TMyClass');
end;
constructor TMyClass.InitWithStuff(const x: String);
begin
inherited Create;
// do whatever stuff you need now
end;
This solution works, but it's a little silly. It prevents anyone from using Create as a constructor for TMyClass (or any
classes that are derived from it and use "inherited") because they'll end up with an exception. However, it's easier
just to name your object constructors consistently from base to sub classes (usually you can just stick with "Create",
as it's a standard name).
Tutorials and articles on using Delphi Visual Component Library (VCL) controls and
components more efficiently at design and run time.
TColorButton
To overcome the lack of color-related properties of the standard TButton
control, I've created the TColorButton custom Delphi component. The
TColorButton adds three new properties to the standard TButton: