By Galina Tydykov                                                                    Copyright  1999. 


          Using TDEventRecorder Class to store Delphi events and change run-time 
          behavior of your application

               Events inside. 

     Using of Delphi components which support event-driven programming is simple and intuitive.
Component user has list of available events. To react on an event component user should define and
implement procedure - event handler. Type of event determines definition of event handler  -  list of
parameters and their types.   So from outside everything is simple: pick the event, define and implement
the event handler, link the event to the event handler. What is going on inside?
  
     Implementation of events for Delphi components is based on powerful feature of Object Pascal-
procedural (method) pointers. In Pascal we can call procedure in two ways: explicitly - using its name
and implicitly - using procedural pointer. Procedure can be called by name or by reference exactly the
same way as it works for variables. Using 'call by reference' technic gives us possibility to create
reusable code which knows only reference (procedural pointer) - not the actual name of the procedure.
This approach is used to implement event-driven mechanism for Delphi components. Component has
an event - a property which keeps reference to the event handler. Event handler itself is declared,
implemented and assigned to this property outside -  by application or other component.

     Technically adding new event to component is pretty simple. But before doing this we need to
decide if we really need this (especially for GUI). If we are writing a visual component it inherits all
mouse & keyboard events from TControl, TWinControl ( Thanks, Delphi! ). And often it is enough to
override method to make component react on existing event. But we still have to add new events for
non-visual components or classes which encapsulate business functionality.

      So, to add new event to the component (class) we need :

          1.  Add new property of procedural type.

     We can choose procedural type declared by Delphi. Usually TNotifyEvent is good if there is no
specific. Of course we can declare our own procedural type if needed.  To add new property we need to
declare private field where we are going to keep the procedural pointer to the event handler. Property
itself should be declared as published if we are going to access it at design time trough Object Inspector.
Read and Write parts of the property declaration  should refer to the private field we defined.

          2. Call the event handler from component  methods.

     Call of the event handler withing component code looks like this:

              if assigned (OnSomeEvent) then
                 OnSomeEvent(parametrs);
     
      Sometimes it is not so easy to find the right place and this usually causes problems like
 "I've added the event but nothing happened".

          3. Call  event handler from protected method.

     We can directly call an event handler from anywhere . But it would be nice to think about future
descendants. So we can make protected virtual (dynamic) method from where to call the event handler. 
And descendant class needs only to override this method to add new functionality. Remember protected
method Click for TControl? This is the same approach.

     Let's show how to add new event to the component. We will create simple component called
TFrog. This class has public method Jump. Application can move Frog -object of TFrog class by calling
this method. The component has also OnJump event to allow application to react on every jump of  the
Frog. 
          1.   First we need to declare class TFrog:

               type          
                   TFrog=Class(TComponent)
               private
                   fOnJump: TNotifyEvent;
               protected
                   procedure DoJump; Virtual;
               public
                   procedure Jump;
               published
                   property OnJump: TNotifyEvent read fOnJump write fOnJump; 
               end;

     We declared private member fOnJump and published property OnJump property of
TNotifyEvent type which refers to  fOnJump. We declared protected method DoJump which will call
OnJump event handler. We also declared Jump method which will encapsulate some implementation
details and  call DoJump to accomplish the task. We could also add some other methods ( method
Dance for instance) and call DoJump  there  to make the Frog to react on outside world events.

          2. Now we need to implement the methods.

               procedure TFrog.DoJump; 
               begin
                 if assigned (OnJump) then
                 OnJump(self);
               end

               procedure TFrog.Jump; 
               begin
                           {<Jump> functionality - here }
                 DoJump(self)
               end

     Method DoJump calls  the <Assigned> function to test if a pointer or procedural variable is nil. If
OnJump property is assigned to some event handler it calls the event handler. Method Jump  calls the
DoJump method . If we want to create a descendant class, TFatFrog for example, we should just
override DoJump method to add some specific to OnJump event - make sound for instance.

               type          
                 TFatFrog=Class(TFrog)
               Public
                 procedure DoJump; override;
               end;

               procedure TFatFrog.DoJump; 
               begin
                 {Make a heavy sigh here} 
                         <...>
                  inherited DoJump;
               end

               Storing events in the list.

      So, events are procedural pointers and they can be stored in the list as usual variables. The
model is based on two classes: TEventItem and TDEventRecorder (see Listing 1). Class TEventItem
defines an event to store and   TDEventRecorder holds list of TEventItem objects and provides access
to them. 

     TEventItem is basic class and does not define particular event types. It should be done by
descendant classes which have additional fields of desired procedural  types. This way we can store
events of  different types in the TDEventsRecorder. 

For example we can define TClickEventsItem and TimerEventsItem  classes:

          TClickEventItem = class(TEventItem)
          Public
            OnClick: TNotifyEvent;
          End;

          TTimerEventItem = class(TEventItem)
          Public
            OnTimer: TNotifyEvent;
          End;

     Or we can define TMultiEventItem with several  fields to handle group of events:

          TMultiEventItem = class(TEventItem)
          Public
            OnTimer: TNotifyEvent;
            OnClick: TNotifyEvent;
            OnMouseMove: TMouseMoveEvent;
            OnMouseDown: TMouseEvent;
            OnMouseUp: TMouseEvent ;
            End;

     Of course those classes can be more customized to keep additional information - parameters of
event handlers execution for example. 

     TEventItem class is very simple. It has the public constructor Create which has a formal
parameter <owner> points to the  object of  TDEventsRecorder class. When  TEventsItem object is
created it adds itself to the owner.

      TDEventRecorder class handles list of TEventItem objects and provides access to them using
EventItems property. It also has  Count property, Delete and Clear methods which are responsible for 
destroying of  TEventItem objects.

     How to use TDEventRecorder classes?

          1. Declare TDEventRecorder, TClickEventItem objects :

               Var
                    EventRecorder:TEventRecorder,
                    ClickItem: TClickEventItem ;            


          2. Create TDEventRecorder object :

               EventRecorder:=TEventRecorder.create;

          3. Add new Item to MyEventRecorder :

               ClickItem: =TClickEventItem.create(EventRecorder);

          4. Assign event handler to EventRecorder item:

               EventsRecorder.EventsItem[index].OnClck:=Button1.Click;
               {where   0<=index<=EventRecorder.count-1}
 
          5. Call event handler from TDEventRecorder object:

                TClickEventsItem( MyEventsRecorder.EventsItem[index]).OnClick(Sender).

          6. Delete Item from MyEventsRecorder :
         
                MyEventsRecorder.delete(index);

          7. Clear MyEventsRecorder :

               MyEventsRecorder.clear;

          8. Destroy MyEventsRecorder:
                MyEventsRecorder.free;

     The main point here is that user has to create every new instance of TEventItem but should not
destroy them. TEventRecorder is responsible for destroying items.


     What can  TDEventRecorder be used for? We can record any events (event handles calls) in
object of TDEventRecorder  and call them from there . For example we can implement record play back'
mechanism for event handler calls in application.


     Listing 1.  Source code for TDEventsRecorder Class

{  TDEventsRecorder class is designed to store method pointers for Event handlers
   in Delphi
  Copyright  1998. Galina Tydykov (Boston, USA, CompuServe 104052,2577).
  Published by permission of SonaMed Corp}

unit EvRecorder;

interface

uses
  Classes, Controls, forms;

(************************************************************************)

 type

  TDEventRecorder = class;

  TEventItem = class
  private
     fOwner:TDEventRecorder;
  protected
  public
     property Owner:TDEventRecorder read fOwner;
     Constructor Create(owner:TDEventRecorder); virtual;
  end ;

  TMultiEventItem = class(TEventItem)
  public
     OnTimer: TNotifyEvent;
     OnClick: TNotifyEvent;
     OnMouseMove: TMouseMoveEvent;
     OnMouseDown: TMouseEvent;
     OnMouseUp: TMouseEvent ;
   end ;

  TClickItem = class(TEventItem)
  public
     OnClick: TNotifyEvent;
   end ;

  TTimerItem = class(TEventItem)
  public
     OnTimer: TNotifyEvent;
   end ;

  TMouseDownItem = class(TEventItem)
  public
     OnMouseDown: TMouseEvent;
   end ;




  TDEventRecorder = class(TObject)
  private
    { Private declarations }
    fList: TList;
    function GetCount: integer;
    function GetEventItem(Index: Integer): TEventItem;
    procedure PutEventItem(Index: Integer; Item: TEventItem);
   protected
   public
    { Public declarations }
     property EventItem[Index: Integer]: TEventItem read GetEventItem write PutEventItem;
     property Count: Integer read GetCount;
     procedure delete(Index: Integer);
     Constructor Create;
     destructor Destroy; override;
     procedure Clear;
   end;

implementation

{  TEventItem }
 Constructor TEventItem.Create(owner:TDEventRecorder);
 begin
   fowner:=owner;
   fowner.flist.add(self);
 end;

{TDEventRecorder}

Constructor TDEventRecorder.Create;
begin
  fList:=TList.create;
end;

destructor TDEventRecorder.Destroy;
begin
  Clear;
  fList.free;
  inherited Destroy;
end;

function TDEventRecorder.GetCount: integer;
begin
  result:=fList.Count;
end;

procedure TDEventRecorder.Clear;
var
 i: integer;
begin
  for i:=0 to fList.count-1 do
    delete(fList.count-1);
end;


function TDEventRecorder.GetEventItem(Index: Integer): TEventItem;
begin
  if (Index < 0) or (Index >= fList.Count) then
       raise EListError.Create('EventsList Error');
  Result :=TEventItem(fList.items[Index]);
end;

procedure TDEventRecorder.PutEventItem(Index: Integer; Item: TEventItem);
begin
  if (Index < 0) or (Index >= fList.Count) then
      raise EListError.Create('EventsList Error');
  TEventItem((fList.items[Index])^):= Item;
end;

procedure TDEventsRecorder.delete(Index: Integer);
begin
  EventsItem[Index].free;
  flist.Delete(Index);
end;
end.


End of Listing 1


     Example of using TDEventRecorder.
                                                       
     Lets create the simple program 'Paint and Play' which will show how to use TDEventRecorder.
Project will have single form FrmPaintAndPlay (Figure1)  with the PanelPaint and three buttons 
BtnGreen, BtnYellow, BtnRed. Click on every button will change color for PanelPaint for Green, Yellow
or Red and also will "record" itself in EventRecorder - object of TDEventRecorder type. We can click
buttons in any sequence and then play back all event handlers from EventRecorder by clicking on
BtnPlayEvents button. Playing back events from  EventRecorder is implemented using timer to improve
performance - to see dynamical changing color of the PanelPaint.  BtnClearEvents.is used to clear
EventRecorder. Also there is ListBoxShowEvents ListBox - some kind of visualization of EventRecorder.
Every time when event is added to EventRecorder, ListBoxShowEvents also adds item and draws red,
green or yellow rectangle in the ListBox. 


Listing 2.  Source code for 'Paint and Play' sample program .


{ Paintplu.pas demonstrates how to use TDEventRecorder class in Delphi
  Copyright  1998. Galina Tydykov (Boston, USA, CompuServe 104052,2577).
  Published by permission of SonaMed Corp}

unit Paintplu;

interface

uses
  SysUtils, Windows, Messages, Classes, Graphics, Controls,Forms, Dialogs,
  StdCtrls, ExtCtrls, EvRecorder;

type
  TFrmPaintAndPlay = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    PanelPaint: TPanel;
    BtnPlayEvents: TButton;
    BtnClearEvents: TButton;
    BtnGreen: TButton;
    BtnYellow: TButton;
    BtnRed: TButton;
    TimerPlayBack: TTimer;
    ListBoxShowEvents: TListBox;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure BtnGreenClick(Sender: TObject);
    procedure BtnPlayEventsClick(Sender: TObject);
    procedure TimerPlayBackTimer(Sender: TObject);
    procedure BtnYellowClick(Sender: TObject);
    procedure BtnClearEventsClick(Sender: TObject);
    procedure ListBoxShowEventsDrawItem(Control: TWinControl; Index: Integer;
      Rect: TRect; State: TOwnerDrawState);
    procedure BtnRedClick(Sender: TObject);
    procedure ListBoxShowEventsKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure ListBoxShowEventsClick(Sender: TObject);
  private
    { Private declarations }
   EventRecorder: TDEventRecorder;{ EventsRecorder}
   CurrentIndex: longint;{current index of EventList}
   BrushGreen:TBrush;  {brushes to draw the colored rectangles for the items }
   BrushRed:TBrush;
   BrushYellow:TBrush;
   procedure StoreEvent(Sender: TObject);
  public
    { Public declarations }
  end;

var
  FrmPaintAndPlay: TFrmPaintAndPlay;

implementation

{$R *.DFM}

procedure TFrmPaintAndPlay.FormCreate(Sender: TObject);
begin
  {Create objects}
   EventRecorder:= TDEventRecorder.create;
   BrushGreen:=TBrush.create;
   BrushRed:=TBrush.create;
   BrushYellow:=TBrush.create;

   BrushGreen.color:=ClLime;
   BrushRed.color:=ClRed;
   BrushYellow.color:=ClYellow;
end;

procedure TFrmPaintAndPlay.FormDestroy(Sender: TObject);
begin
  {Free objects}
   EventRecorder.free;
   BrushGreen.free;
   BrushYellow.free;
   BrushRed.free;
end;

procedure TFrmPaintAndPlay.StoreEvent(Sender: TObject);
begin
  {Store Event  EventsRecorder}
  if Sender is TButton then
  begin
    TClickItem.create(EventRecorder);
    TClickItem(EventRecorder.EventItem[EventRecorder.count-1]).OnClick:=(Sender as
TButton).OnClick;
  end;
end;

procedure TFrmPaintAndPlay.BtnGreenClick(Sender: TObject);
begin
  with PanelPaint do { Paint the PanelPaint }
  begin
    color:=ClLime;
    repaint;
  end;
  if sender=BtnGreen then
  begin
    {This call is made by clicking button - not from from Events List}
    StoreEvent(BtnGreen); {add event to the EventsRecorder }
    ListBoxShowEvents.items.addObject('',BrushGreen);{add Brush to the ListBoxShowEvents}
    ListBoxShowEvents.update;
  end;
end;


procedure TFrmPaintAndPlay.BtnYellowClick(Sender: TObject);
begin
  with PanelPaint do
  begin
   color:=ClYellow;
   repaint;
  end;
  if sender=BtnYellow then
  begin
    ListBoxShowEvents.items.addObject('',BrushYellow);
    ListBoxShowEvents.update;
    StoreEvent(BtnYellow);
  end;
end;

procedure TFrmPaintAndPlay.BtnRedClick(Sender: TObject);
begin
  with PanelPaint do
  begin
    color:=clRed;
    repaint;
  end;
  if sender=BtnRed then
  begin
    ListBoxShowEvents.items.addObject('',BrushRed);
    ListBoxShowEvents.update;
    StoreEvent(BtnRed);
  end;
end;

procedure TFrmPaintAndPlay.BtnPlayEventsClick(Sender: TObject);
begin
  CurrentIndex:=0;
  ListBoxShowEvents.repaint;
  TimerPlayBack.enabled:=true; {Playing of stored events}
end;

procedure TFrmPaintAndPlay.TimerPlayBackTimer(Sender: TObject);
begin
  {Playing of stored events}
  TimerPlayBack.enabled:=false   ;
  if CurrentIndex<=EventRecorder.count-1 then
  begin
    ListBoxShowEvents.repaint;
    {Play current Event}
    TClickItem(EventRecorder.EventItem[CurrentIndex]).OnClick(sender);
    CurrentIndex:=CurrentIndex+1;
    {Move to next Item}
    TimerPlayBack.enabled:=true;
  end;
end;


procedure TFrmPaintAndPlay.BtnClearEventsClick(Sender: TObject);
begin
   EventRecorder.clear;
   ListBoxShowEvents.clear;
end;

procedure TFrmPaintAndPlay.ListBoxShowEventsDrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var
  CurBrush: TBrush;
begin
  with (Control as TListBox).Canvas do
  begin
    CurBrush:=TBrush((Control as TListBox).Items.Objects[Index]);
    if CurBrush <> nil then
    begin
      {draw colored rectangle}
      Brush.Color:= CurBrush.Color;
      FillRect(Rect);
      if Index=CurrentIndex then
      begin
        {Add sliding effect:                                                    }
        {      draws black frame on rectangle corresponding to the CurrentIndex }
        Brush.Color:= clBlack;
        FrameRect(Rect);
      end
    end;
  end;
end;

procedure TFrmPaintAndPlay.ListBoxShowEventsKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  {We can delete any event from EventsRecorder  - on Delete key}
  if Key=VK_DELETE then
  begin
    EventRecorder.delete(CurrentIndex);
    ListBoxShowEvents.items.delete(CurrentIndex);
    if CurrentIndex>EventRecorder.count-1 then
     CurrentIndex:=CurrentIndex-1;
  end
end;

procedure TFrmPaintAndPlay.ListBoxShowEventsClick(Sender: TObject);
begin
  CurrentIndex:=ListBoxShowEvents.ItemIndex;
  ListBoxShowEvents.repaint;
  TClickItem(EventRecorder.EventItem[CurrentIndex]).OnClick(sender);
end;

{ This software is provided "AS IS," without a warranty of any kind.}

end.

End of Listing 2


               TPanelButton as an Event Re-Player.

     TPanelButton is another example of using TDEventRecorder.  This is a button which can accept
other controls dropped on. This component was designed for medical device company to meet specific
requirements for GUI. The screen of our product  should  look like front panel of medical device: panel
with big buttons which has detailed instructions with different fonts, images etc.. TPanelButton met this
criteria because different labels, images could be dropped on and their properties ( sizes, fonts,
positions) could be set  - at  design time .
     Component design of TPanelButton is usual for owner drawn buttons. Ability to accept child
controls is built in TControl - ancestor for all controls. Graphical part - moving child controls along with
the PanelButton - was not also a problem - VCL has enough examples. The  problem was how to handle
mouse and click events of child controls. Other words what happens if we press PanelButton and mouse
is over the child control? And child control has its own event handlers - changes color on MouseMove, 
for example? So TEventRecorder was designed to solve the problem.
     PanelButton has access to child controls events and can manipulate them. First It stores event
handlers references of child controls in EventRecorder. Then PanelButton changes their links -
reassigns events of child controls to its own methods.  Those methods provide moving of the child
controls up and down along with PanelButton and then call delayed' event handlers stored in
EventRecorder and  PanelButton's own event handlers if necessary .

     Detailed PanelButton materials are uploaded to CompuServe Delphi forums, Component Design 
Library


Published by permission of SonaMed Corp


Author information:  Galina Tydykov, Boston, USA, CompuServe 104052,2577, tydykov@sonamed.com .

