Are you fed up of writing yet another routine to search a stringlist?
Do you know about procedural types? A procedural type is a native Pascal
data type of which you may not have heard, but which you are undoubtedly
using. In this article I'll explain where you are using them already,
dissect the syntax, and suggest some situations in which they are invaluable.
You'll never need to write code to search a stringlist again.
The Basics
From the Delphi 5 help file: "Procedural types allow you to treat
procedures and functions as values that can be assigned to variables
or passed to other procedures and functions". In other words, you
can store references to code in variables, which you can use to subsequently
call that piece of code. This takes a bit of getting used to; you're
used to using variables to store pieces of data, such as numbers or
strings. Procedural types, on the other hand, allow you to store references
to pieces of code. The variable containing the procedural type is actually
a pointer to the piece of code.
Let's start with
the basic syntax. Bare with me with this contrived example- you'll appreciate
the real uses in a moment. Imagine you have the following two procedures:
Procedure DisplayWarning;
Begin
ShowMessage( 'This
is a warning');
End;
Procedure DisplayError;
Begin
ShowMessage( 'This
is an error' );
End;
We'll now declare
a new type, which will allow us to declare variables which can reference
one of these procedures:
Type TProcedurePtr
= Procedure;
This declares
a new type, TProcedurePtr, as a reference, or a pointer, to a procedure.
We can then declare variables of that type:
Var
MyProc : TProcedurePtr;
Before you can
use the variable you must assign a procedure to it, as in:
MyProc
:= DisplayError;
An important
point to note is that this assignment does not call the procedure DisplayError.
Rather, it stores a reference to DisplayError in the variable MyProc
(actually it stores its address). Alternatively you could assign the
DisplayWarning procedure to the variable myProc:
MyProc
:= DisplayWarning;
In fact, as you
will see, you can assign any procedure which does not accept any parameters.
So now we have
a variable which references a procedure - at some time we will want
to run that procedure, which we do simply by using the variable in a
statement:
MyProc;
This simple statement
calls the subroutine referenced by the variable MyProc. Think of it
as an indirect call. This does not call a subroutine called MyProc;
indeed there isn't one, MyProc is a variable. It calls the subroutine
to which MyProc refers, in this case DisplayWarning. Run the following
through the debugger to convince yourself:
Var
MyProc : TProcedurePtr;
Begin
MyProc := DisplayError;
MyProc; // Call DisplayError
MyProc
:= DisplayWarning;
MyProc; // Call DisplayWarning;
That's the basics
of procedural types. You may not know how you would use them yet, but
by now you should understand the syntax.
Assigning
Procedural Types
You can assign procedural types to each other in the same way you assign
other variables - you are just copying the address of a procedure from
one variable to another:
Var
MyProc1 : TProcedurePtr;
MyProc2 : TProcedurePtr;
Begin
MyProc1 := DisplayError;
MyProc2 := MyProc1;
MyProc1;
// Call DisplayError
MyProc2; // Call DisplayError
As you'll see
in a moment, this is exactly what you do when you share event handlers
between two or more components.
Procedural Types
with Parameters
What you just saw were two trivial procedures which did not receive
parameters. You can also use procedural types with sub routines which
do receive parameters, but you must declare the parameters along with
the type. Previously we used:
Type TProcedurePtr
= Procedure;
To declare a
new type, TProcedurePtr, which was a reference to a procedure which
did not receive any parameters. If the procedure must receive parameters,
you must explicitly list them, with their type, as part of the type
declaration, as in:
Type TProcedureMsgPtr
= Procedure (msg : String );
The syntax on
the right hand side of the equals sign may take a little getting used
to - think of it as a normal procedure declaration without the procedure
name.
You can now declare
variables of type TProcedureMsgPtr:
Var
MyMsgProc : TProcedureMsgPtr;
But you must
assign the variable a procedure which accepts one parameter, of type
string:
Procedure DisplayMessage(
s : String );
Begin
ShowMessage( s );
End;
//
Var
MyMsgProc : TProcedureMsgPtr;
Begin
MyMsgProc := DisplayMessage;
End;
If you
attempt to assign MyMsgProc a procedure which does not accept a single
parameter, of type string, the compiler will complain.
Now, when
you invoke the procedure using the procedural type, you must supply
the parameter:
MyMsgProc('This
is my message');
This calls the
DisplayMessage routine, passing the string to the subroutine's formal
parameter named s. Take a moment to ensure you understand this. MyMsgProc
is a pointer to a subroutine which accepts a single parameter of type
string. When you call the procedure to which MyMsgProc points, in this
case DisplayMessage, you must pass the parameter. In fact, the following
two lines of code have the same effect:
MyMsgProc('This
is my message');
DisplayMessage('This is my message');
The first is
calling DisplayMessage indirectly via a pointer to it. Why bother you
might ask? In this case you wouldn't, you'd just call DisplayMessageDirectly;
bare with me a moment longer.
Procedural
Types as Methods
The procedural types you've seen so far have been references to stand
alone subroutines. You use a very similar syntax to work with methods
(in essence a method is only a subroutine which can access an object's
data). To declare a procedural type which references a method rather
than a stand alone subroutine, simply append the words Of Object to
the type declaration:
Type TMethodMsgPtr
= Procedure (msg : String ) Of Object;
Var
MethodMsgPtr : TMethodMsgPtr;
This is sufficient
to inform the compiler that TMethodMsgPtr is a pointer to a method of
an object, rather than a stand-alone procedure.
Then, when assigning
to the variable, use the object name followed by the method name:
MethodMsgPtr :=
SomeObject.MsgMethod;
And call it indirectly
in the same way you've already seen:
MethodMsgPtr( 'Some
string for the method');
Event handlers
are Procedural Types
At the start of this article I mentioned that you are already using
procedural types in your Delphi applications. Where have you been using
them? With your event handlers. Load Delphi's help for TForm and look
at the help for one of the events, such as the onActivate event. You
will see it declared as:
property
OnActivate: TNotifyEvent;
If
you follow the hyperlink to the declaration of TNotifyEvent, you will
see:
type TNotifyEvent
= procedure (Sender: TObject) of object;
which you should
now understand. Variables of type TNotifyEvent are pointers to methods
(note the of object in the declaration) which receive one parameter,
of type TObject. Events, then, are procedural types. Put another way,
events are implemented by having pieces of data contain pointers to
pieces of code. An object of type TForm, or likely a sub class of TForm,
contains many such pieces of data which reference pieces of code.
Similarly, controls'
events are declared in a similar way. Load help and look at the onClick
event of TButton:
property OnClick:
TNotifyEvent;
onClick is a
property which is a pointer to a method which accepts one parameter
of type TObject.
If you poke around
for long enough in the VCL source code, you can find the Pascal code
which responds to messages from the Windows API and calls the programmer
assigned event handlers. Since you are not obligated to write code for
events, the VCL must check whether you assigned handler, and only call
it if so. You will see the following sort of code throughout the VCL:
if Assigned(FOnDblClick)
then FOnDblClick(Self);
This is from
the TControl class, and is checking to see whether the programmer assigned
a handler for the double click event, calling it if so.
Most events are
declared as type TNotifyEvent, but not all. TForm's onClose event, for
example, is passed two parameters. Here are the pertinent declarations:
TCloseEvent = procedure(Sender:
TObject; var Action: 
TCloseAction)
of object;
property OnClose:
TCloseEvent;
onClose is declared
as being of type TCloseEvent, which in turn is declared as a pointer
to a method which receives two parameters, the first one of type TObject,
the second of type TCloseAction. The onClose property, then, is a procedural
type.
TForm's onCloseQuery
event is also different:
type TCloseQueryEvent
= procedure(Sender: TObject; var CanClose: Boolean) of object;
property OnCloseQuery: TCloseQueryEvent;
onCloseQuery is
also a property which is a procedural type. This time, however, the
procedural type is a procedure which accepts two parameters; the first
of type TObject and the second a variable boolean parameter.
When you use
the object inspector to define event handlers, Delphi is generating
entries in the DFM file which assigns the address of the event handler
to the property used to reference it. To verify this, open up a DFM
file and you'll see entries such as:
object Button1:
TButton
Left = 304
Top = 104
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 1
OnClick = Button1Click
As you see, this
entry is defining a TButton called button1, and declaring initial values
for some of its properties, including the onClick property. Button1Click
is a method of the form in which Button1 is declared.
It's very common
to have toolbar buttons use the same event as a menu item. You may have
a menu item labeled Open File, for example, and a toolbar button which
performs the same action. To accomplish this you write the code for
the onClick event of one of the components, then use the object inspector
to associate the same event with the other component. Delphi is simply
making entries in the DFM file to have the two onClick properties reference
the same event handler.
Note that the
object inspector only allows you to assign events which have the same
parameter lists. For example, if you've written code for the onCloseQuery
event, you cannot assign this event to a push button's onClick property.
OnClick is declared as a TNotifyEvent, which accepts one parameter of
type TObject. OnCloseQuery is declared as a TCloseQuery event which
expects two parameters. The two types are incompatible.
Dynamically
Creating Controls
Another
situation in which you will need to use procedural types is when you
create controls dynamically at run time - you must assign the event
handlers otherwise the controls won't do anything. Consider the code
in Listing 1, which is taken from a main form's onCreate event. The
event is reading a list of most recently used files from an IniFile
named MruFiles.Ini, stored in the same location as the executable file
itself. It is adding the names of these files as new menu items at the
bottom of an existing menu item named File1. Note how the code assigns
the procedure MruOpen to the onClick property of the new menuItem. When
the user clicks on any of these new menu items Delphi will call the
MruOpen event. You must declare MruOpen in the form's class declaration:
TForm1 = class(TForm)

MainMenu1:
TMainMenu;

File1:
TMenuItem;

Open1:
TMenuItem;

N1:
TMenuItem;

exit1:
TMenuItem;

OpenDialog1:
TOpenDialog;

Button1:
TButton;

procedure
Open1Click(Sender: TObject);

procedure
FormCreate(Sender: TObject);

procedure
Button1Click(Sender: TObject);
private

{
Private declarations }
public

{
Public declarations }

Procedure
mruOpen(Sender : TObject);
end;
Then implement
it as you would any other method you write:
procedure TForm1.mruOpen(Sender:
TObject);
begin
OpenFile( (Sender
AS TMenuItem).Caption );
end;
Note how mruOpen
is declared as a method which accepts one parameter of type TObject.
It must be declared that way, because a TMenuItem's onClick event is
declared that way.
procedure TForm1.FormCreate(Sender:
TObject);
Var
iniFile : TIniFile;
s : String;
sl : TStringList;
i : Integer;
mnuItem : TMenuItem;
begin
iniFile := Nil;
sl := Nil;
mnuItem := Nil;
Try

iniFile
:= TIniFile.Create(


ExtractFilePath(
Application.ExeName ) + 'MruFiles.Ini' );

sl
:= TStringList.Create;
iniFile.ReadSection(
'MruFiles', sl );
For i := 0 TO sl.Count
- 1 Do

Begin


s
:= IniFile.ReadString('MruFiles', sl.strings[i], 'N/A');


mnuItem
:= TMenuItem.Create( Self );


mnuItem.Caption
:= s;


mnuItem.onClick
:= MruOpen;


File1.Insert(
2 + i, mnuItem );

End;
// Add a separator
at the end
mnuItem := TMenuItem.Create(
Self );
mnuItem.Caption :=
'-';
File1.Insert( sl.Count
- 1 + 3, mnuItem );
Finally


iniFile.Free;


sl.Free;
End;
Listing 1 -
Reading a list of MRU files from an INI file and Creating menu items
for them
Parameterizing
Code
So
far you've seen how Delphi uses procedural types for event handlers,
and how you use them yourself to assign event handlers dynamically.
Now we'll look at a more general use of procedural types - as parameters
to subroutines. Remember that since procedural variables are simply
variables which contain pointers to pieces of code; you can pass them
as parameters just like other variables. To see why this is useful,
consider how you search a stringlist. As you may know, the TStringList
class has a method called IndexOf, which searches a stringlist for a
particular string and returns its position in the list. IndexOf is useless,
however, if you want a case insensitive search, a substring search,
or to search a stringlist's objects. Writing your own search routine
is pretty easy. Listing 2 shows how to perform a case insensitive search,
for example.
Function slInsensitiveSearch(
sl : TStringList; s : String) : Integer;
Var
i : Integer;
lFound : Boolean;
Begin
i := 0;
lFound := False;
While (i <= sl.count
- 1) and (not lFound) Do

Begin


If
UpperCase(sl.Strings[i]) = UpperCase( s ) Then



lFound
:= True


Else



i
:= i + 1;


End;

If
lFound Then


Result
:= i

Else


Result
:= -1
End;
Listing 2 -
A case Insensitive StringList search
This is a basic
stringlist search. It loops through each element of the stringlist,
terminating when either the item is found, or the stringlist is exhausted.
The routine uses the expression:
UpperCase(sl.Strings[i])
= UpperCase( s )
to determine
whether this is the desired element or not.
Now consider
listing 3, which searches a stringlist for a string where the item being
searched for can be a left substring of the item in the string list.
That is, if you are searching for the string Spence, and an element
in the stringlist cointains Spencer, that would be considered a valid
match (listing 2, by contrast, was an exact comparison).
Function slLeftSubString(
sl : TStringList; s : String) : Integer;
Var
i : Integer;
lFound : Boolean;
Begin
i := 0;
lFound := False;
While (i <= sl.count
- 1) and (not lFound) Do

Begin


If
Copy(sl.Strings[i], 1, Length(s)) = s Then



lFound
:= True


Else



i
:= i + 1;


End;

If
lFound Then


Result
:= i

Else


Result
:= -1
End;
Listing 3 -
A case sensitive substring search
This time the
routine uses the expression:
Copy(sl.Strings[i],
1, Length(s)) = s
to determine
whether this is the desired element or not, but the rest of the routine
is identical.
How would you
perform a case insensitive sub string search? You'd duplicate the routine,
using the following expression to perform the comparison:
UpperCase(Copy(sl.Strings[i],
1, Length(s))) = UpperCase(s)
How would you
search the stringlist for an object whose classname is passed as a parameter?
You'd use the following expression
sl.Objects[i].ClassName
= s
Do you see the
point? The only thing that is changing each time is the comparison.
It should be possible to parameterize this expression someway, and leave
the rest of the routine alone. A procedural parameter is the answer.
We need a generic search routine, where the routine's caller can pass
in an expression which defines the comparison to make. The loop of this
generic routine would then read something like:
While
(i <= sl.count - 1) and (not lFound) Do

Begin


If
CallUserSuppliedRoutine Then // ß---NOTE ---



lFound
:= True


Else

i
:= i + 1;
The user supplied
routine will return a true if this is the element it wants, false otherwise.
Obviously the user supplied routine must know which element is currently
being examined, and indeed which stringlist is being searched, so the
generic routine must pass these as parameters, as in the following pseudo
code:
While (i <= sl.count
- 1) and (not lFound) Do

Begin


If
CallUserSuppliedRoutine(sl, i) Then // ß---NOTE ---



lFound
:= True


Else



i
:= i + 1;
Let's look at
the syntax we need to make this happen. The user supplied routine, the
procedural type, must be a function which returns a logical, and receives
two parameters; the stringlist itself, and the position of the element
under consideration. We'll first declare a new type for this named TSLSearchFunc:
// This function
is passed the stringlist and the index of the current item
// being examined. The function must return a true if this is the
// desired element, false otherwise
Type TSLSearchFunc = Function(sl :TStrings; i : Integer) : Boolean;
The generic routine
must accept this as a parameter:
// Generic StringList
search with procedural type
// handling the comparison
Function SLSearch( sl : TStrings; Checker : TSLSearchFunc ) : Integer;
And must call
it from inside the loop, passing the stringlist and the element number
as parameters:
If
Checker(sl, i) Then
Listing
4 shows the entire generic search routine.
// This function
is passed the stringlist and the index of the current item
// being examined. The function must return a true if this is the
// desired element, false otherwise
Type TSLSearchFunc = Function(sl :TStrings; i : Integer) : Boolean;
// Generic StringList
search with procedural type
// handling the comparison
Function SLSearch( sl : TStrings; Checker : TSLSearchFunc ) : Integer;
Var
i : Integer;
lFound : Boolean;
Begin
i := 0;
lFound := False;
While (i <= sl.count
- 1) and (not lFound) Do

Begin


If
Checker(sl, i) Then // ß---NOTE ---



lFound
:= True


Else



i
:= i + 1;

End;
If lFound Then

Result
:= i
Else

Result
:= -1;
End;
Listing 4 - Generic
Stringlist search routine with procedural parameter handling comparison
So how do you
call this routine? First you must write the routine whose address you
will pass to SLSearch. Here's a case insensitive search looking for
a string stored in a global variable s:
Function InSensitive(sl
: TStrings; i : Integer) : Boolean;
Begin
Result := UpperCase(sl.Strings[i])
= UpperCase( s );
End;
Then pass this
to slSearch, as in:
i
:= slSearch( ListBox1.Items, Insensitive )
For each additional
type of search you want you only need to write one simple boolean function.
Summary
Procedural types are probably the most underused type in Object Pascal.
As this article showed, they are simply variables which contain the
address of a subroutine, and as such you can treat them much like any
other variable. You can assign them, and pass them as parameters. Delphi
uses procedural types for event handlers, and you can use them yourselves
to parameterize the logic of a subroutine, as we did here with a generic
stringlist search.
Author
Rick
Spence is technical director of Web
Tech Training & Development (formerly Database Programmers Retreat)
- a training and development company with offices in Florida and the
UK. You can reach Rick directly at 71760.632@compuserve.com.
General inquiries should be directed to training_usa@webtechcorp.com.
