Professional Documents
Culture Documents
Creating Events in Delphi 6 PDF
Creating Events in Delphi 6 PDF
For example, when the user clicks the mouse button, a WM_MOUSEDOWN message is sent to the Win32
system. Win32 passes that message to the control for which the message was intended. This control can
then respond to the message. The control can respond to this event by first checking to see whether
there is any code to execute. It does this by checking to see whether the event property points to any
code. If so, it executes that code, or rather, the event handler.
The OnClick event is just one of the standard event properties defined by Delphi. OnClick and other
event properties each have a corresponding event-dispatching method. This method is typically a
protected method of the component to which it belongs. This method performs the logic to determine
whether the event property refers to any code provided by the user of the component. For
theOnClick property, this would be the Click() method. Both the OnClick property and
theClick() method are defined by TControl as follows:
TControl = class(TComponent)
private
FOnClick: TNotifyEvent;
protected
end;
Here is the TControl.Click() method:
procedure TControl.Click;
begin
end;
One bit of essential information that you must understand is that event properties are nothing more than
method pointers. Notice that the FOnClick property is defined to be
a TNotifyEvent.TNotifyEvent is defined as follows:
This says that TNotifyEvent is a procedure that takes one parameter, Sender, which is of the
type TObject. The directive, of object, is what makes this procedure become a method. This
means that an additional implicit parameter that you don't see in the parameter list also gets passed to
this procedure. This is the Self parameter that refers to the object to which this method belongs. When
the Click() method of a component is called, it checks to see if FOnClick actually points to a
method, and if so, calls that method.
As a component writer, you write all the code that defines your event, your event property, and your
dispatching methods. The component user will provide the event handler when using your component.
Your event-dispatching method will check to see whether the user has assigned any code to your event
property and then execute it when code exists.
Event handlers are assigned to event properties either at runtime or at design time. In the following
section, we show you how to create your own events, event properties, and dispatching methods.
Defining Event Properties
Before you define an event property, you need to determine whether you need a special event type. It
helps to be familiar with the common event properties that exist in the Delphi VCL. Most of the time, you'll
be able to have your component descend from one of the existing components and just use its event
properties, or you might have to surface a protected event property. If you determine that none of the
existing events meets your need, you can define your own.
As an example, consider the following scenario. Suppose you want a component containing an event
that gets called every half minute based on the system clock. That is, it gets invoked on the minute and
on the half minute. Well, you can certainly use a TTimer component to check the system time and then
perform some action whenever the time is at the minute or half minute. However, you might want to
incorporate this code into your own component and then make that component available to your users so
that all they have to do is add code to your OnHalfMinute event.
The TddgHalfMinute component shown in Listing 1 illustrates how you would design such a
component. More importantly, it shows how you would go about creating your own event type.
Listing 1—TddgHalfMinute—Event Creation
unit halfmin;
interface
uses
type
{ Define a procedure for the event handler. The event property will
be of this procedure type. This type will take two parameters, the
the time that the event occurred. For our component this will be
every half-minute. }
TddgHalfMinute = class(TComponent)
private
FTimer: TTimer;
TTimeEvent. }
FOnHalfMinute: TTimeEvent;
protected
{ Define the dispatching method for the OnHalfMinute event. }
public
published
// Define the actual property that will show in the Object Inspector
end;
implementation
begin
inherited Create(AOwner);
begin
FTimer := TTimer.Create(self);
FTimer.Enabled := True;
FTimer.Interval := 500;
FTimer.OnTimer := FTimerTimer;
end;
end;
destructor TddgHalfMinute.Destroy;
begin
FTimer.Free;
inherited Destroy;
end;
This method gets the system time, and then determines whether or not
DoHalfMinute. }
var
DT: TDateTime;
Temp: Word;
begin
{ If not the same second when this method was last called, and if
DoHalfMinute(DT)
end;
begin
if Assigned(FOnHalfMinute) then
FOnHalfMinute(Self, TheTime);
end;
end.
When creating your own events, you must determine what information you want to provide to users of
your component as a parameter in the event handler. For example, when you create an event handler for
the TEdit.OnKeyPress event, your event handler looks like the following code:
begin
end;
Not only do you get a reference to the object that caused the event, but you also get a Charparameter
specifying the key that was pressed. Deep in the Delphi VCL, this event occurred as a result of
a WM_CHAR Win32 message that drags along some additional information relating to the key pressed.
Delphi takes care of extracting the necessary data and making it available to component users as event
handler parameters. One of the nice things about the whole scheme is that it enables component writers
to take information that might be somewhat complex to understand and make it available to component
users in a much more understandable and easy-to-use format.
Notice the var parameter in the preceding Edit1KeyPress() method. You might be wondering why
this method wasn't declared as a function that returns a Char type instead of a procedure. Although
method types can be functions, you shouldn't declare events as functions because it will introduce
ambiguity; when you refer to a method pointer that is a function, you can't know whether you're referring
to the function result or to the function pointer value itself. By the way, one function event in the VCL
slipped past the developers from the Delphi 1 days, and now it must remain. This event is
theTApplication.OnHelp event.
Looking at Listing 1, you'll see that we've defined the procedure type TOnHalfMinute as this:
This procedure type defines the procedure type for the OnHalfMinute event handler. Here, we
decided that we want the user to have a reference to the object causing the event to occur and
theTDateTime value of when the event occurred.
The FOnHalfMinute storage field is the reference to the user's event handler and is surfaced to the
Object Inspector at design time through the OnHalfMinute property.
The basic functionality of the component uses a TTimer object to check the seconds value every half
second. If the seconds value is 0 or 30, it invokes the DoHalfMinute() method, which is responsible
for checking for the existence of an event handler and then calling it. Much of this is explained in the
code's comments, which you should read over.
After installing this component to Delphi's Component Palette, you can place the component on the form
and add the following event handler to the OnHalfMinute event:
procedure TForm1.ddgHalfMinuteHalfMinute(Sender: TObject; TheTime:
TDateTime);
begin
end;
This should illustrate how your newly defined event type becomes an event handler.
OOP
Object Oriented Programming is based on (and extends) established ideas of
Structured Programming, and involves three basic principles: encapsulation,
inheritance and polymorphism.Encapsulation is the concept of placing data and
routines that operate on that data together and combining them to create a structure
(object) that contains both. Inheritance is the concept of new objects that are
derived from existing objects and can add or change the behaviour of their parent.
This is the main feature that leads to re-use of existing code. Finally,polymorphism is
the concept that causes different types of objects derived from the same parent object
to be able to behave differently when instructed to perform a same-named method
with a different implementation.
Although OOP has always been related to claims of code re-use and faster
development cycles, in practice, this has proven to be more often false than true. Two
main problems with practical code-reuse are determining and finding which object(s)
to use for a particular problem, and organising these objects in a usable and accessible
architecture. Delphi solves these problems in three ways. First of all, the component
architecture itself is the foundation for re-use. Second, by placing the re-usable objects
or components in a structured Component Palette, components can be logically
grouped together by type. The Component Palette is merely an organisation tool of the
components, but very important for the use, made possible by the component
architecture itself. Finally, with Delphi we are able to develop components and our
application in the very same environment.
Component Building
Delphi and Delphi Components are built upon the Visual Component Library application
framework. The VCL is already a very rich framework, which becomes clear if we take
a look at the Component Palette of Delphi: dozens of standard Windows controls like
editboxes, static lines, combo and listboxes, but also several advanced custom controls
like grid controls, tab controls, notebook controls, an outliner, OLE controls and data-
aware controls. The list goes on and on, and will go on and on, since Delphi includes
the ability to include new components in the Component Palette!
Creating a new component requires writing a .PAS unit file containing the source code
of the component, which will be compiled to a .DCU file. Additionally, we could include
a .DCR palette bitmap (a renamed .RES file with a 28x28 bitmap with the same name
as the component, to appear in the component palette), maybe even a .DFM form file,
a .HLP help file and a .KWF keyword file. All of these topics wills be covered in this
session. For the moment, however, we will only concentrate on the .PAS component
source file.
Before we can start writing a component ourselves, we have to identify the relevant
classes from VCL. The following class hierarchy shows the most important seven
classes for this task:
TObject
+- TPersistent
+- TComponent
+- TControl
+- TGraphicControl
+- TWinControl
+- TCustomControl
The VCL is not based on the old object model of Borland Pascal, where VMTs are stored
in the Data Segment. Instead, the VCL is based on a new class model (with the
emphasis on class). In this new class model, all object instances are automatically
dynamically allocated on the heap. This means that Delphi treats each class instance
we reference as a pointer to that class instance. It is no longer necessary to explicitly
declare a pointer type or to use the dereference symbol. This greatly reduces the
syntax complexity of ObjectPascal, as pointers have always been a complex topics for
(beginning) programmers.
Every class from the VCL is derived from the TObject root. The class TObject contains
the Create and Destroy methods that are needed to create and destroy instances of
classes. The class TPersistent, derived from TObject, contains methods for reading and
writing properties to and from a form file. TComponent is the class to derive all
components from, as it contains the methods and properties that allow Delphi to use
TComponents as design elements, view their properties with the Object Inspector and
place these components in the Component Palette. If we want to create a new non-
visual component from scratch, then TComponent is the class we need to derive from.
Visual component classes are derived from the TControl class, that already contains
the basic functionality for visual design components, like position, visibility, font and
caption. Derived from TControl are TGraphicControl and TWinControl. The
TGraphicControl contains a Canvas property to make painting easier. The TWinControl
contains a Windows' handle, so it can respond to input (i.e. it can get the input focus).
The TCustomControl is a combination of a TGraphicsControl and a TWinControl; it
contains both a Canvas and a Windows handle, so we can paint on it with ease, and it
can receive the input focus!
Properties have to be read and written, and hence contain a read (Get) and write (Set)
field or method. The Get method is a function that returns the property value, while
the Set method is a procedure that takes as a parameter the new property value. The
Set property method makes a great place to include some data validation rules. An
example of a property "Day" declaration is as follows:
private
FDay: Word;
protected
function GetDay: Word;
procedure SetDay(NewDay: Word);
published
property Day: Word read GetDay write SetDay;
Where GetDay and SetDay are property methods that have to be implemented in the
implementation section of the unit. The keyword private leaves the internal field FDay
only visible to the methods of the current class (or any code in the same unit, by the
way). The new keyword protected ensures us that only classes derived from the
current class can call or override the property methods GetDay and SetDay. The new
keyword published, finally, tells Delphi that the property Day should be visible in the
Object Inspector.
Published properties of components can be stored to and read from streams. This is
what happens in the DFM file, which is a stream file with the values of the properties of
the form, but also the components and properies of the components on the form. This
is why we can only see read/write properties in the Object Inspector; they have to be
read and written from the DFM file! We can specify a default value for a property (for
example, Day = 2). Even when using a default property value, the property still needs
to be initialised to this value (unless the default value is zero, since all initial values are
0 by default). If a property's value it equal to it's default value, then the value is not
streamed to the DFM file. Only if the value differs from the default value then the value
is streamed to the DFM file. Therefore, a thoughtful use of default values for properties
can reduce the size of the DFM file (and loading time of your form).
Design time extensions to the Delphi IDE can be written in the form of Property and
Component Editors. Please see my session on Property and Component Editors for
more information on this topic.
Enough talk for now, let's start with our TTicTacToe game component...
The magic square algorithm simply implies that three fields are a winning combination
if the sum of the identifiers of these fields is 15. Based on this general rule, the
routines inside MAGIC.DLL know how to find a move which at least prevents the
opponent of winning. I implemented the TTT strategy in a generic usable DLL, since I
wanted to ensure the DLL could be used by more than one game component (instance)
at once. But how does the player and DLL know which move applies to which game? I
decided to use a well known technique also used by, for example, Windows: Handles.
Each game starts with a call to NewGame(), which returns a unique Game Handle (or 0
in case of an error). For every move, up and including the EndGame() call, the player
has to supply the unique Game Handle for this particular game. Internally, the DLL has
room for 256 concurrent games, although I've never played more than seven
simultaneously at the same time. Although I have written the DLL some time ago, I
will not show the internals here. This time, our target is primarily to focus on how to
write import units for (possibly other, foreign) DLLs and encapsulate them into new
Delphi components. The internals of the DLL might only distract us from the
encapsulation itself.
(By the way, most of the time, we'll be faced with a DLL for which only a C header file
is available. In this situation, writing an import unit is even more daring, as we must
convert the C header file to ObjectPascal declarations also! Fortunately, a Delphi IDE
Expert called HeadConv is now available to assist in this conversion process. HeadConv
is part of "Dr.Bob's Collection of Delphi IDE Experts", which can be downloaded from
my homepage or from the DELPHI and BDELPHI32 fora on CompuServe as
file DRBOBxxx.ZIP).
The implicit import unit for the MAGIC.DLL, which we will use for prototyping the
component, is as follows:
{$A+,B-,D-,F+,G+,I-,K+,L-,N-,P-,Q-,R-,S+,T+,V-,W-,X+,Y-}
unit MAGIC;
{
File: MAGIC.IMP
Author: Bob Swart (aka Dr.Bob)
Purpose: implicit import unit for MAGIC.DLL
}
interface
Const
NoneID = 0;
UserID = 1;
CompID = 2;
Type
TPlayer = NoneID..CompID;
Const
NilPlace = 0; { move impossible }
FirstPlace = 1;
LastPlace = 9;
Type
TPlace = FirstPlace..LastPlace;
TMove = NilPlace..LastPlace;
Type
HGame = Word; { 16-bit Handle to a game }
implementation
Const
DLL = 'MAGIC' {$IFDEF WIN32} +'.DLL' {$ENDIF};
end.
First of all, we need to supply the class name of our new component: TTicTacToe.
Since we want to create an original Windows (game/custom) control, we must derive
from the basic TWinControl. Finally, the components will initially appear on the Dr.Bob
palette page.
After we press the OK-button, the Delphi Component Expert generates the following
code to start the component building task (note that we've saved the source file to the
unit Ttt.pas already):
unit Ttt;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
forms, Dialogs;
type
TTicTacToe = class(TWinControl)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Dr.Bob', [TTicTacToe]);
end;
end.
The Component Expert makes no assumptions on the number of classes and units we
need for our new component. Therefore, quite a lot of units are included in the uses
clause. In the following listing, I've removed all units which are not needed, and left
only the units that are really needed, including the import unit MAGIC, which was not
included in the first place.
The Component Expert also generated a component template with a few placeholders
for our properties and methods. The private declarations are perfectly suited for the
game property fields, which must remain hidden. The private parts of a component are
only accessible from within the unit itself or from within instances of the class. The
former is one of the reasons why we should put each component in its own unit (so no
component could access the private parts of another component from the same unit).
The access functions to these private property fields are best declared protected (so a
derived class can see and override them). The protected part can be seen as the
developer's interface to the component. The property names themselves are declared
as published, so we can see them in the property inspector. The published parts can
be seen as the designer's user interface to the component. Finally, the public part of
the component contains the public methods and fields from the component, such as
the constructor for example.
unit TTT;
interface
uses SysUtils, Classes, Controls, StdCtrls, Dialogs, Magic;
Type
TTicTacToe = class(TWinControl)
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
[..]
destructor TTicTacToe.Destroy;
begin
if (FGame > 0) then EndGame(FGame);
inherited Destroy
end {Destroy};
Note that we've made the FGame handle a private internal field of the TTicTacToe
component, as we don't want anybody to mess with it. The constructor and destructor
will make sure the handle is allocated and freed, and now it's time to move on to
design the actual playing parts of the component.
private
Button: Array[TPlace] of TButton;
to the TTicTacToe class declaration. In the constructor TTicTacToe.Create we make
sure the buttons are created as follows:
constructor TTicTacToe.Create(AOwner: TComponent);
var ButtonIndex: TPlace; { from MAGIC.DLL }
begin
inherited Create(AOwner);
FGame := NewGame;
for ButtonIndex := Low(ButtonIndex) to High(ButtonIndex) do
begin
Button[ButtonIndex] := TButton.Create(Self);
Button[ButtonIndex].Parent := Self; { important!! }
Button[ButtonIndex].Caption := '';
Button[ButtonIndex].OnClick := ButtonClick;
{ we'll get back to the OnClick event later }
end;
SetBounds(Left,Top,132,132)
end {Create};
Note that although the nine buttons are created in the constructor Create of the
TTicTacToe, it is very important to actually set the parent of the buttons to this control.
Otherwise, they will not be children of our component (and will not be visible at all).
At the end of the constructor, we call SetBounds to give the game component an initial
size from the current position where we dropped it. This will make sure the component
looks good from the first time we drop it on a form. Of course, to make a good
impression, the nine playing buttons will also need to be resized within the parent
game component itself. This leads us to override the SetBounds procedure as follows:
procedure TTicTacToe.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
Const Grid = 3; { 3x3 buttons }
GridX = 2; { 2 pixels space on X-axis between buttons }
GridY = 2; { 2 pixels space on Y-axis between buttons }
var X,DX,W,Y,DY,H: Word;
begin
Inherited SetBounds(ALeft,ATop,AWidth,AHeight);
X := GridX;
DX := (Width div (Grid * (GridX+GridX))) * (GridX+GridX);
W := DX - GridX;
Y := GridY;
DY := (Height div (Grid * (GridY+GridY))) * (GridY+GridY);
H := DY - GridY;
Button[8].SetBounds(X, Y, W,H);
Button[1].SetBounds(X, Y+DY, W,H);
Button[6].SetBounds(X, Y+DY+DY, W,H);
Inc(X,DX);
Button[3].SetBounds(X, Y, W,H);
Button[5].SetBounds(X, Y+DY, W,H);
Button[7].SetBounds(X, Y+DY+DY, W,H);
Inc(X,DX);
Button[4].SetBounds(X, Y, W,H);
Button[9].SetBounds(X, Y+DY, W,H);
Button[2].SetBounds(X, Y+DY+DY, W,H)
end {SetBounds};
The calculations above are only needed to make sure the nine buttons are properly
aligned with respect to each other and their parent component itself.
Play!
Now that the buttons are all in place, it's time to use the game handle and get the
game started. For this we need to know when a button is clicked (ButtonClick) and we
need to know what to do when the Computer moves (ComputerMove) and when the
User moves (UserMove):
private
FGameEnded: Boolean;
protected
procedure ButtonClick(Sender: TObject);
procedure ComputerMove;
procedure UserMove(Move: TPlace);
If the user clicks on a button, we need to know on which button the user clicked. Since
we used an array of buttons, we can simply walk the array and see if the sender (of
the buttonclick) as TButton equals one of the buttons. The "AS TButton" part is needed
to typecast the Sender TObject to the polymorphic object it actually IS (a TButton in
this case):
procedure TTicTacToe.ButtonClick(Sender: TObject);
var ButtonIndex: TPlace;
begin
for ButtonIndex := Low(ButtonIndex) to High(ButtonIndex) do
if Button[ButtonIndex] = Sender AS TButton then
UserMove(ButtonIndex)
end {ButtonClick};
The UserMove consists of nothing more than telling the DLL that the user decided to
move to this spot. Of course, we need to check whether or not the game is already
over, and whether or not the space is already occupied. We can use several functions
from MAGIC.DLL (supplying the game handle) for this task, namely IsWinner, GetValue
and MakeMove.
procedure TTicTacToe.UserMove(Move: TPlace);
begin
if IsWinner(FGame) <> NoneID then
begin
if IsWinner(FGame) = UserID then
MessageDlg('You have already won!', mtInformation, [mbOk], 0)
else
MessageDlg('I have already won!', mtInformation, [mbOk], 0)
end
else
begin
if FGameEnded then
MessageDlg('The game has already ended!', mtInformation, [mbOk],
0)
else
begin
if GetValue(FGame, Move) <> NoneID then
MessageDlg('This place is occupied!', mtWarning, [mbOk], 0)
else
begin
Button[Move].Caption := 'X';
MakeMove(FGame,UserID,Move);
if IsWinner(FGame) = UserID then
MessageDlg('Congratulations, you have won!',
mtInformation, [mbOk], 0)
else
ComputerMove
end
end
end
end {UserMove};
At the end of the UserMove, it's the computer's turn to move. The computer will
automatically detect whether or not the game is already over. Otherwise, it will call the
function NextMove from MAGIC.DLL to determine the next best move, according to the
magic square algorithm:
procedure TTicTacToe.ComputerMove;
var Move: TMove;
begin
if IsWinner(FGame) = NoneID then
begin
Move := NextMove(FGame,CompID);
if Move = 0 then
begin
FGameEnded := True;
MessageDlg('Neither has won, the game is a draw!',
mtInformation, [mbOk], 0)
end
else
begin
MakeMove(FGame,CompID,Move);
Button[Move].Caption := '0';
if IsWinner(FGame) = CompID then
MessageDlg('I have won!', mtInformation, [mbOk], 0)
else
begin
Move := NextMove(FGame,UserID);
if Move = 0 then
begin
FGameEnded := True;
MessageDlg('Neither has won, the game is a draw!',
mtInformation, [mbOk], 0)
end
else
Button[Move].SetFocus { hidden hint... }
end
end
end
end {ComputerMove};
Note that right after making the ComputerMove with MakeMove, the routine calls the
NextMove with the UserID to find out whether or not the game has ended. If not, the
best move for the user is given as a hint by giving the corresponding button the focus
with SetFocus. By adding the Boolean GameEnded property and an initial start button
over the control, we get the first complete working component:
Notice the button in the middle of the top row who got the focus right after the
computer move!
Well, that's exactly what we'll do. We can add three new private property fields to the
class declaration; FUserStarts, FCompChar and FUserChar. Next, we write three
property methods to actually set the new value of FUserStarts, FCompChar and
FUserChar. The first is the easiest, as it's either True or False. The last two are more
difficult, as we don't want the user and computer to play with the same
character (otherwise it'd be really easy to win, right?). So, we have to write one line of
code to check whether CompChar already has the value we want to assign to
FUserChar and vice versa:
Component Bitmap
But before we install it, it would be a good idea to design a bitmap for it, to identify it
more easily on the Component Palette. We can use the Image Editor that is provided
by Delphi itself, but personally I prefer to use Resource Workshop for this task:
Using Resource Workshop we can make a nice bitmap palette to use for this
component. It needs to be a 16-colour bitmap with dimensions that are no larger than
28 by 28 pixels. Other than that, it's important to give the bitmap the same name as
the component itself (in all upper case letters), and to save the resource file with a
.DCR extension. Actually, saving it as a .RC resource source file might be even better,
as we can then use the 16-bit and 32-bit command line resource compilers BRCC and
BRCC32 to compile the resource source file to either a 16-bit or a 32-bit binary
resource file. Depending on the version of Delphi, we need either the 16-bit or the 32-
bit DCR file to be in the same directory as the component source or .DCU file before we
install it.
Installation
We can install the TTicTacToe component onto the Delphi Component Palette with the
menu-option Component | Install. A dialog will follow in which we need to select the
source code for the component (TTT.PAS in this case).
After we click on the OK-Button, the entire Component Library is recompiled, and - if it
recompiled without errors - the old one is unloaded and the new version is loaded into
Delphi, to make our newly added Component immediately available to us (on the
Dr.Bob palette page). Make sure the MAGIC.DLL is in the Windows' search path.
Once we've added the component to the palette, we can drop it on a form and look at
it throught the Object Inspector. The new properties are ready to be modified:
Exceptional!
The SetXX methods for our properties (last paragraph) used MessageDlgs to notify the
user that an invalid value was assigned to CompChar or UserChar (i.e. a value that
was already in use by the other party). While this may seem adequate at first, it might
be a pain when we set these properties at run-time (from another part of a program).
In fact, even from the Property Inspector it's not the behaviour we'd really want (try
for yourself and see what happens).
Delphi supports exceptions, so why not use them? If we just derive a new type of
exceptions EBadChar from Exception, and raise one whenever we try to set a "bad
char" to one of the playing properties, then the TTicTacToe component gets the
behaviour it deserves. An exception is raised (and can be "caught" by the calling
program, whether it's the Property Inspector or a playing program that uses the game
component. If the exception is not caught by the try-except block, a messagebox in
fact similar to the MessageDlg pops up to tell us something went wrong!).
The code is as follows:
{$DEFINE EXCEPTIONS}
{$IFDEF EXCEPTIONS}
Type
EBadChar = class(Exception);
{$ENDIF EXCEPTIONS}
Since some Delphi users might find exceptions still a bit hard to use, I've added an
$IFDEF clause, so we can experiment with them (on and off).
{$A+,B-,D-,F+,G+,I-,K+,L-,N-,P-,Q-,R-,S+,T+,V-,W-,X+,Y-}
unit MAGIC;
{
File: MAGIC.PAS
Author: Bob Swart (aka Dr.Bob)
Purpose: explicit import unit for MAGIC.DLL
}
interface
Const
NoneID = 0;
UserID = 1;
CompID = 2;
Type
TPlayer = NoneID..CompID;
Const
NilPlace = 0; { move impossible }
FirstPlace = 1;
LastPlace = 9;
Type
TPlace = FirstPlace..LastPlace;
TMove = NilPlace..LastPlace;
Type
HGame = Word; { 16-bit Handle to a game }
Const
MagicLoaded: Boolean = False; { presume nothing! }
implementation
{$IFDEF WINDOWS}
uses WinProcs;
Const SEM_NoOpenFileErrorBox = $8000;
{$ELSE}
uses WinAPI;
{$ENDIF}
begin
{$IFDEF WINDOWS}
SetErrorMode(SEM_NoOpenFileErrorBox);
{$ENDIF}
DLLHandle := LoadLibrary('MAGIC.DLL');
if DLLHandle >= 32 then
begin
MagicLoaded := True;
SaveExit := ExitProc;
ExitProc := @NewExit;
@NewGame := GetProcAddress(DLLHandle,'NEWGAME');
@EndGame := GetProcAddress(DLLHandle,'ENDGAME');
@MakeMove := GetProcAddress(DLLHandle,'MAKEMOVE');
@NextMove := GetProcAddress(DLLHandle,'NEXTMOVE');
@IsWinner := GetProcAddress(DLLHandle,'ISWINNER');
@GetValue := GetProcAddress(DLLHandle,'GETVALUE')
end
end.
Note that this import unit can be compiled by Delphi 1 and 2 (and actually also by
Borland Pascal for a Windows or DPMI target). There are a few differences in
platforms, but other than that the import unit remains the same. This means that as
long as we have the correct 16-bit or 32-bit version of the DLL available, we can
actually use it with any version of Delphi on any platform (with a single source import
unit)!
Final Construction
The only thing we need to do now is to make sure in our TTicTacToe.StartButtonClick
routine that MagicLoaded is True before we try to attempt to use the imported
functions from MAGIC.DLL (which will not be available, of course, when the DLL is not
loaded).
By the way, if we raise an exception, then the IDE will show the following message
first (which we can un-set if we un-check the "Break on Exception" option):
For the TTicTacToe game component we can think of at least two events: winning and
losing. And we can define two event handlers: OnWin and OnLose.
Event Handlers
Event Handlers are methods of type Object. This means that they can be assigned only
to class methods, and not to ordinary procedures or functions. Consider the type
TNotifyEvent for the most general of event handlers:
unit BobEvent;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
Type
TEventNoObject = procedure;
TEventOfObject = procedure of object;
TEventComponent = class(TComponent)
private
{ Private declarations }
FEventNo: TEventNoObject;
FEventOf: TEventOfObject;
published
{ Published declarations }
property OnEventNoObject: TEventNoObject read FEventNo write
FEventNo;
{ error: this property cannot be published }
property OnEventOfObject: TEventOfObject read FEventOf write
FEventOf;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Dr.Bob', [TEventComponent])
end {Register};
end.
All this experimenting leads to the conclusion that we need to add two private fields
FOnWin and FOnLose of type TNotifyEvent to the TTicTacToe game component. Apart
from these two private fields, we also need to publish two properties OnWin and
OnLose of type TNotifyEvent who are connected to the private fields:
Event Signallers
Event signallers are needed to signal to an event handler that a certain event has
occurred, so the event handler can perform its action. In case of the OnWin and
OnLose events, we can just go to the source code for the TTicTacToe component and
look for the current MessageDlgs that tell the user that he's lost or won. We need to
change this code to check for tha availability of an event, and if so use the event
instead:
...
if IsWinner(Game) = UserID then
if Assigned(FOnWin) then OnWin(Self) { computer lost }
else
MessageDlg('Congratulations, you have won!', mtInformation, [mbOk],
0)
else
ComputerMove
After we've implemented all this, the events page of the TTicTacToe component is as
follows:
Component Help
I'm sure that the code for the TTicTacToe component hold little surprises for you; it's
just another component wrapper based on an explicit import unit. But what about the
casual Delphi user? How will the component user (instead of builder) react if he sees
the published properties? Personally, whenever I'm not sure about something, I always
hit the F1 key to get help on the component itself or one of its properties. Only, since
this is but a "third-party" custom component, Delphi cannot just make up some help
for us and instead responds with the rather disturbing message that no context
sensitive help can be found for your component.
OK, so this may just be the end of the usage period of my TTicTacToe component,
right? Wrong! Delphi is an open enough environment to let us even install our own
Component Help! And that's what we'll be doing in the rest of this article.
WinHelp
First of all, we need to create a WinHelp skeleton file (using a WinHelp Authoring tool
like ForeHelp) with only a few topics (each topic gets its own page with contents):
TTicTacToe
constructor Create
methods (popup page)
o procedure UserMove
(other methods)...
properties (popup page)
o property UserChar
(other properties)...
events (popup page)
o event OnWin
(other events)...
I used ForeHelp 1.04 to generate the winhelp file skeleton. A prime page for the
TTicTacToe components with three popup pages; properties, methods and events, and
one jump to the constructor. The three popup pages would each have some entries.
Consequently, the way the TTicTacToe component's winhelp works is comparable to
the general Delphi helpfiles (just drop a TEdit component and hit F1 to see the general
Component Help outline of Delphi itself).
After I generated the winhelp skeleton, I saved the project, and edited the .RTF file
with WinWord 2.0c (any RTF-editor is valid). Using WinWord, I could enter the
contents of the eight topics, and more important, I could add the special "B"-keywords
that Delphi needs in order to make your winhelp file really integrated with the Delphi
winhelp files. The "B"-keywords are only needed for the three topics that actually
integrate with Delphi, which are - again - the TTicTacToe main page and the three
properties, methods and events popups. The TTicTacToe main topic page needs a "B"-
footnote that says "class_TTicTacToe", i.e. the class name with "class_" as prefix. Also,
the property topics need "B"-footnotes with the name of the property and the "prop_"
prefix, and the events topics need "B"-footnotes with the name of the event and the
"event_" prefix. For class specific properties, we need to include the class name as
well, i.e. "prop_TTicTacToeUserChar".
Keywords
After we've added the three "B"-footnotes to the winhelp file (and even before we've
actually written the contents of the winhelp file), we can generate the keywords from
this file that are needed to integrate with the Delphi multihelp environment. At this
point, Delphi's own Component Writer's Guide seems to be a little out of date. First of
all, the KWGEN application is a Windows application, and not a DOS one anymore.
Second, we don't need to put the keyword file in the same directory as our compiled
unit and helpfile, as we'll see shortly. KWGEN is a Windows 3.1 application that allows
us to browse for any .HPJ file. It then opens the corresponding .RTF file and scans -
among others - for the "B"-footnotes to generate a special .KWF keyword file.
Installation
As I've said before, we don't need to put the generated .KWF file next to the compiled
unit and helpfile. Instead, we seem to be forced to place this file in the \DELPHI\HELP
directory where the other Delphi .KWF files can be found (such as DELPHI.KWF,
WINAPI.KWF and CWG.KWF). Now, we can use HELPINST.EXE to generate the Delphi
MultiHelp master index DELPHI.HDX file which contains references to all .KWF files in
the list. Remember to close Delphi before you attempt to do this, and to make a
backup of the DELPHI.HDX file (so if something goes wrong, you can always restore
your masterindex). The HELPINST program will start in the DELPHI\HELP directory, by
the way, while the DELPHI.HDX file resides in the \DELPHI\BIN directory. Just to let
you know... I found that if I place the TTicTacToe.KWF file anywhere else, then the
second time I fire up HELPINST it would say that the TTicTacToe.KWF file was not
found, so I had to enter it again. I overcame this problem by placing all .KWF files in
the same directory; not something I have a problem with anyway.
For the TTictacToe.HLP files holds a similar story. The Delphi Component Writer's Guide
recommends to place this helpfile in the same directory as the compiled component
unit, but I found that the easiest - and default - way for the TTicTacToe.HLP file to be
found was to place it inside the \DELPHI\BIN directory. If we want to place it anywhere
else, we need to modify the WINHELP.INI file (in the \WINDOWS directory) and add a
line that reads (for example):
TTicTacToe.HLP=C:\DELPHI\DRBOB\TTicTacToe.HLP
Component Help
Well, now that we've generated a keyword file and integrated it into the Delphi
MultiHelp masterindex, all we need to do is to finish our winhelp file itself, and place it
in the correct directory (\DELPHI\BIN by default). Then we can activate the TTicTacToe
component help in several ways:
Conclusions
In this session, we have seen just about every aspect of Delphi Component building.
We've seen how to build our own components for 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 on-line help to support the component
user.
Bibliography
If you want more interesting and deep-technical information on the Art of Component
Building using Delphi, then you should check out my articles in The Delphi Magazine or
the book The Revolutionary Guide to Delphi 2, ISBN 1-874416-67-2 published by
WROX Press.