Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics
 Delphi Component Building
See Also: Delphi Papers and Columns

Delphi has two kinds of users: end-users (application developers) and component writers. This session is especially intended for the latter group of Delphi programmers, who want to start building their own components for Delphi. While Delphi is a great tool for Client/Server and Rapid Application Building, I think the most important feature of Delphi is the ability to write components and add them to the component palette of the environment itself. I strongly believe that a Delphi Component is The Object of the '90s (and beyond).

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, Methods and Events
Components consist of encapsulated properties, methods and events. Properties are slots that give the component user the illusion of reading or writing the value of a variable in the component, while the component writer can use properties to hide the implementation details. In a sense, properties are the user interface to a component. Methods are procedures and functions that are encapsulated with properties in a component. Events are like reactions (event handlers) to messages (events) that occur during execution of the component. Examples of events are OnClick and OnEnter. Both methods and events can be made dynamic, which gives us the polymorphic ability of component classes.

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 Engine DLL
The engine part of the tic-tac-toe game component is contained in a dynamic link library called MAGIC.DLL, originally written quite some time ago in Borland Pascal for Windows, and now ported to Delphi (single source compilable). This DLL uses the 'magic square' algorithm, which is based on a gameboard with the following internal layout:

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.

The Import Unit
Using MAGIC.DLL by another Delphi application actually introduced a little problem at first. Instead of getting everything, compared with using a unit, a DLL only exports functions, and not type definitions, constants or variables. Consequently, we have no choice but to repeat (from the original source code of MAGIC.DLL) all constant declarations and type definitions we need to import the DLL functions.

(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 }

  function NewGame: HGame
     {$IFDEF WIN32} stdcall {$ENDIF};
  procedure EndGame(Game: HGame)
     {$IFDEF WIN32} stdcall {$ENDIF};
  procedure MakeMove(Game: HGame; ID: TPlayer; Place: TPlace)
     {$IFDEF WIN32} stdcall {$ENDIF};
  function NextMove(Game: HGame; ID: TPlayer): TMove
     {$IFDEF WIN32} stdcall {$ENDIF};
  function IsWinner(Game: HGame): TPlayer
     {$IFDEF WIN32} stdcall {$ENDIF};
  function GetValue(Game: HGame; Place: TPlace): TPlayer
     {$IFDEF WIN32} stdcall {$ENDIF};

implementation
Const
  DLL = 'MAGIC' {$IFDEF WIN32} +'.DLL' {$ENDIF};

  function NewGame;   external DLL index 1;
  procedure EndGame;  external DLL index 2;
  procedure MakeMove; external DLL index 3;
  function NextMove;  external DLL index 4;
  function IsWinner;  external DLL index 5;
  function GetValue;  external DLL index 6;

end.

The function NewGame returns a game handle needed to play. The procedure EndGame gets this game handle, and releases the game in order to re-use the handle for another game. Procedure MakeMove is used to tell the DLL that for a certain game, a certain player has made a move on a certain place. The DLL can make a counter-move at any time using the function NextMove, given a game handle and player. Note that it is thus possible to let the DLL play against itself. Finally, the last two functions, IsWinner and GetValue can be used for administration reasons to query whether or not we have a winner, and how the fields on TTT playing-board are filled in. Extensions to the functionality we choose not to implement at this time are MoveBack and Hint (note that a one-level Hint cannot be obtained using NextMove, since the latter actually moves and we have no move back (or undo) - we will actually implement this feature later).

The Delphi Component
As said in the introduction, the target of this session is to write a little tic-tac-toe game component that encapsulates the MAGIC.DLL engine. First, let's think about what this little component would look like...
The final component would be something with nine game buttons on it. Alternately, the computer and the user would "click" on one of the buttons to play the game. The Delphi component would really only need to concern itself with the user/DLL interface issues, while the tic-tac-toe game algorithm itself would be handled by calls to and internals of the MAGIC.DLL.

The Component Expert
Although component building is a non-visual task, we can get a some support from the Delphi Component Expert that we can find in the Repository (when we select File | New):

If we click on OK after we've selected a New Component, we get the Component Expert itself:

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.

The First Step
It should be clear that the generated code from the Component Expert only means to give us a template to write the things we need. The register part is especially nicely done for us!
Now, we need a constructor and destructor to obtain and free a game handle from MAGIC.DLL. A very important thing not to forget is that we must specify the keyword override with our constructor and destructor. This ensures that the virtual constructor and destructor of the parent class TWinControl can be called by this child class TTicTacToe:


unit TTT;
interface
uses SysUtils, Classes, Controls, StdCtrls, Dialogs, Magic;

Type
  TTicTacToe = class(TWinControl)
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

  private { Magic DLL handle }
    FGame: HGame;
  end {TTicTacToe1};

[..]

  constructor TTicTacToe.Create(AOwner: TComponent);
  begin
    inherited Create(AOwner);
    FGame := NewGame
  end {Create};

  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.

The Playing Fields
Unlike form designing, component building is a non-visual task. We have to edit the source file (with the Delphi source editor) to make the changes we need. In this case, we'd like to add nine playing buttons to the TTicTacToe component. So, we need to add an array of controls:

  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!

Adding Game Properties
While the game seems to run fine, we'd like to be able to put more control in it. For starters, the computer always starts to move. Also, we don't really like the 'X' vs '0' characters that are always used. Why not user-definable characters? Let's use properties to define these at design time or runtime of our TTicTacToe game component?

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:

  procedure TTicTacToe.SetUserChar(Value: Char);
  begin
    if Value = FCompChar then
    begin
      MessageDlg('Character '+Value+' already in use by CompChar!',
                  mtError, [mbOk], 0)
    end
    else
      FUserChar := Value
  end {SetUserChar};
Having done all this, we're now completely in control of the new game component, and are ready to install it for the first time.

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:

Resource Workshop

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).

Install Component

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}

  procedure TTicTacToe.SetUserChar(Value: Char);
  begin
    if Value = FCompChar then
    begin
    {$IFDEF EXCEPTIONS}
      raise EBadChar.Create(Value+' already in use by CompChar!')
    {$ELSE}
      MessageDlg('Character '+Value+' already in use by CompChar!',
                  mtError, [mbOk], 0)
    {$ENDIF}
    end
    else
      FUserChar := Value
  end {SetUserChar};
And the result (when trying to assign the UserChar to the same value as CompChar) is the following exception:

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).

Explicit Import Units
The TTicTacToe component we've built so far used an implicit import unit for the MAGIC.DLL. This means that the MAGIC.DLL needs to be present if we want to use the component. It also means that the MAGIC.DLL needs to be present when the component is added to the component palette! This actually means that the Component Library will not load when the MAGIC.DLL is not available (i.e. when we've accidently deleted MAGIC.DLL, the whole Component Library will be unusable).
It is possible to check for a DLLs presence before attempting to use it. This way, we can remove the implicit usage of the MAGIC.DLL, and make it an explicit dependency, which will enable the Component Library to load, but the component to fail (with a graceful exception) whenever MAGIC.DLL is not available. We have to change all procedure and functions declarations (from the first listing) to procedure and function variable declarations. By eliminating all the "external DLL index X;" clauses, we in fact eliminate the implicit reference to (and the automatic attempt to) load the MAGIC.DLL.
We load the MAGIC.DLL with LoadLibrary. If the resulting handle is > 32, we can use it to instantiate the procedure and function variables using GetProcAddress:


{$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! }

var NewGame: function: HGame
       {$IFDEF WIN32} stdcall {$ENDIF};
    EndGame: procedure(Game: HGame)
       {$IFDEF WIN32} stdcall {$ENDIF};
    MakeMove: procedure(Game: HGame; ID: TPlayer; Place: TPlace)
       {$IFDEF WIN32} stdcall {$ENDIF};
    NextMove: function(Game: HGame; ID: TPlayer): TMove
       {$IFDEF WIN32} stdcall {$ENDIF};
    IsWinner: function(Game: HGame): TPlayer
       {$IFDEF WIN32} stdcall {$ENDIF};
    GetValue: function(Game: HGame; Place: TPlace): TPlayer
       {$IFDEF WIN32} stdcall {$ENDIF};

implementation
{$IFDEF WINDOWS}
uses WinProcs;
Const SEM_NoOpenFileErrorBox = $8000;
{$ELSE}
uses WinAPI;
{$ENDIF}

var SaveExit: pointer;
    DLLHandle: Cardinal;

    procedure NewExit; far;
    begin
      ExitProc := SaveExit;
      FreeLibrary(DLLHandle)
    end {NewExit};

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).

  procedure TTicTacToe.StartButtonClick(Sender: TObject);
  var ButtonIndex: TPlace;
  begin
    if MagicLoaded then { explicit load of DLL succeeded! }
    begin
      FGame := NewGame;
      FGameEnded := False;
      StartButton.Visible := False;
      for ButtonIndex := Low(ButtonIndex) to High(ButtonIndex) do
        Button[ButtonIndex].Visible := True;
      if UserStarts then
        MessageDlg('You may start...', mtInformation, [mbOk], 0)
      else
        ComputerMove
    end
    else
    {$IFDEF EXCEPTIONS}
      raise EDLLNotLoaded.Create('MAGIC.DLL could not be loaded!')
    {$ELSE}
      MessageDlg('Error loading MAGIC.DLL...', mtInformation, [mbOk], 0)
    {$ENDIF}
  end {ButtonClick};
Now, the Component Library and hence the entire component palette will be unaffected when the MAGIC.DLL is removed (try it if you want). The TTicTacToeControl component will show a MessageDlg or an exception to tell us that something went wrong when loading the MAGIC.DLL, and that's all there is to it!

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):

Adding Custom Events
In the previous examples, we have examined the process of writing our own component based on a DLL engines, and adding a custom bitmap to it. In this part, we'll focus on adding custom events and event handlers to our components. Events consist of two parts: an event signaller and the event handler. The signaller must make sure that the component somehow gets a message of some sort to indicate that some condition has become true, and that the event is now born. The event handler, on the other hand, starts to work only after the event itself is generated, and responds to it by doing some processing of itself. Event signallers are typically based on virtual (or dynamic) methods of the class itself (like the general Click method) or Windows messages, such as notification messages. Event handlers are typically placed in event properties, such as the OnClick or OnChange event handler property. If event handlers are published, then the user of the component can enter some event handling code that is to be executed when the event is fired.

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:

  TNotifyEvent = procedure (Sender: TObject) of object;
The TNotifyEvent type is the type for events that only have the sender as parameter. These events simply notify the component that a specific event occurred at a specific TObject (the sender). For example, OnClick, which is of type TNotifyEvent, notifies the control that a click event occurred on the control Sender. If the parameter Sender would be omitted as well, then we'd only know that a specific event had occurred, but not at which control. Generally, we do want to know for which control the event just occurred, so we can act on the control (or on data in the control). As mentioned before, Event Handlers are placed in event properties, and they appear on a separate page in the Object Inspector (to distinguish them from the 'normal' properties). The basis on which the Object Inspector decides to split these two kinds of properties is the "procedure/function of Object" part. The "of object" part is needed, since we get the error message "cannot publish property" if we (try to) omit it, as can be experimented using the following unit:


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:

  private { game properties }
    FOnLose: TNotifyEvent;
    FOnWin: TNotifyEvent;

  published { user interface }
    property OnWin: TNotifyEvent
             read FOnWin write FOnWin;
    property OnLose: TNotifyEvent
             read FOnLose write FOnLose;

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):

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.


This webpage © 2000-2017 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.