Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Funzioni di callback in Delphi

Funzioni di callback in Delphi

E-mail Stampa PDF

In questo articolo cercheremo di approfondire alcuni argomenti che riguardano le funzioni o le procedure di "callback".
Prima di iniziare a spiegare cosa sono le procedure di callback bisogna aver ben chiaro in mente cosa sono delle variabili di tipo "procedurale". Quest'ultimo tipo di dato consente infatti di trattare delle funzioni o delle procedure come dei valori che possiamo assegnare a delle variabili e quindi di trattarle come tali.
In pratica possiamo conservare un riferimento ad una funzione in una variabile, che a quel punto potremo usare per richiamare la funzione originaria.
Questo è quello che accade anche con altri tipi di dati, come ad esempio il tipo string, dove la variabile stringa contiene un puntatore alla struttura vera e propria che contiene l'array di lettere che la compongono.
Morale della favola i tipi procedurali non sono altro che puntatori, dato che che hanno la loro stessa dimensione e la loro stessa funzione, solo che il compilatore li tratta in modo diverso. Infatti quando una variabile viene dichiarata di tipo procedurale viene definita parametricamente, cioè vengono specificati tutti i sui parametri e, nel caso di una funzione, viene specificato il suo risultato.
Vediamo un esempio per capire meglio di cosa si parla:

Type
TMiaFunzione = function(a,b:integer):integer; //definizione del tipo procedurale
var
MiaF1,MiaF2: TMiaFunzione;  // dichiarazione di 2 variabili di tipo procedurale

Ora creiamo due funzioni che devono essere parametricamente equivalenti a nostro tipo procedurale:

function somma(a,b:integer):integer;
begin
result:=a+b;
end;

function differenza(a,b:integer):integer;
begin
result:=a-b;
end;

Ora che abbiamo il tipo procedurale e il codice delle due funzioni vediamo come può essere usato:

begin
MiaF1:=somma;            //assegno alla prima variabile il codice della funzione somma
MiaF2:=differenza;       //assegno alla seconda variabile il codice della funzione differenza

writeln(MiaF1(2,3));     //stampa a video il valore 5
writeln(MiaF2(6,2));     //stampa a video il valore 4
end;

Quindi il risultato di queste assegnazioni equivale a rinominare la funzione originaria, o anche ad assegnare un determinato codice ad una funzione inizialmente "vuota". Avremo potuto anche modificare il codice associato allo stesso nome:

begin
MiaF1:=somma;            //assegno alla prima variabile il codice della funzione somma
writeln(MiaF1(2,3));     //stampa a video il valore 5

MiaF1:=differenza;       //assegno sempre alla stessa variabile il codice della funzione differenza
writeln(MiaF1(6,2));     //stampa a video il valore 4
end;

Fino ad ora abbiamo fatto degli esempi sicuramente chiari e semplici, ma un po' inutili, dato che avremo potuto sempre chiamare direttamente la funzione originale col suo vero nome e non sarebbe cambiato assolutamente nulla. Ma allora dove sta il vantaggio di questo tipo di dati?
La risposta è molto semplice: se possiamo usare una funzione come una variabile allora potremo passarla come parametro ad un altra funzione!
Questo è precisamente quello che accade con una procedura di callback, la quale viene passata come parametro ad un'altra funzione quest'ultima ne userà il codice al suo interno. Questo sicuramente renderà modulare la programmazione, dato che potremo modificare e personalizzare la procedura di callback senza modificare il restante codice.

program TestCallback;

{$APPTYPE CONSOLE}

uses
SysUtils;

type
TmySort = procedure(var a,b:integer); //definizione del tipo procedurale

procedure OrdinaNumeri(x,y:integer;Ordinamento:TmySort);
begin
// usa la procedura di callback chiamata "Ordinamento" per ordinare i numeri
Ordinamento(x,y);
// una volta ordinati i valori li stampa a video
Writeln(Format('1: %d - 2: %d',[x,y]));
end;

//Implementazione delle 2 funzioni di ordinamento
procedure Decrescente(var a,b:integer);
var
temp:integer;
begin
if a<b then begin
  temp:=b;
b:=a;
a:=temp;
end;
end
;

procedure
Crescente(var a,b:integer);
var
temp:integer;
begin
if
a>b then begin
temp:=b;
b:=a;
a:=temp;
end
;
end
;

begin
OrdinaNumeri(2,5,Crescente);
OrdinaNumeri(2,5,Decrescente);
readln;
end.

Col precedente esempio abbiamo quindi dimostrato come decomporre una funzione, infatti non modificheremo mai più la funzione OrdinaNumeri mentre potremo personalizzare il tipo di ordinamento modificando al funzione di callback.

Fino ad ora abbiamo usato dei tipi procedurali riferiti a funzioni "stand-alone" e non inseriti in un contesto di programmazione a oggetti.
Per fare questo dovremo usare i dati di tipo procedurale in relazione ai metodi di un oggetto, che non sono altro che parti di codice all'interno di una classe.
Per definire un tipo procedurale che si riferisca ad un metodo piuttosto che ad una normale procedura dovremo aggiungere alla fine della definizione la direttiva "of object":

Type
TMioMetodo = procedure(messaggio:string) Of Object;   //definizione del tipo procedurale come metodo

var
MiaProc:TMioMetodo;  // dichiarazione di una variabile procedurale

...
MiaProc:=UnaClasseQualsiasi.MsgMetodo;  // assegna il metodo di una classe alla variabile procedurale
...
MiaProc('Ciao'); // chimando la variabile procedurale verrà usato indirettamente il metodo della classe
...

Da notare che il metodo presente nella classe "UnaClasseQualsiasi" dovrà essere parametricamente equivalente al tipo procedurale definito in precedenza.
Quindi la regola è che se la funzione di callback fa parte di un oggetto, nella definizione del suo tipo dovremo aggiungere la direttiva "of object" alla fine.

Quello che abbiamo detto prima sulle funzioni di callback è valido anche per le classi e i loro metodi, ed in particolare la gestione degli eventi non è altro che una implementazione di questa tecnica. Un "event handler" non è altro che una funzione definita, dichiarata e implementata esternamente alla classe, ma che, dopo essere stata associata ad essa, viene richiamata quando necessario.
Vediamo ora un esempio dove ci sarà una classe (TMiaClasse) che genererà un evento, che è una funzione di callback il cui codice, dovendo essere un metodo, deve risiedere all'interno di una classe (TMioGestore).

program TestEventHandler;

{$APPTYPE CONSOLE}

uses
SysUtils;

type
TMioEvento = procedure(Sender:TObject) of object;  // Definizione dell'evento in quanto tipo procedurale

TMiaClasse = class       // definizione della classe che genera l'evento
FOnSaluto: TMioEvento;
public
procedure Saluto;
property OnSaluto: TMioEvento read FOnSaluto write FOnSaluto;
end;

TMioGestore = class      // definizione della classe che contiene il codice di gestione dell'evento
procedure OnSalutoMio(Sender: TObject);
end;

procedure TMiaClasse.Saluto;     //implementazione del metodo "saluto" nella classe
begin
Writeln('ciao');
if Assigned(OnSaluto) then
OnSaluto(self);
Writeln;
end;

procedure TMioGestore.OnSalutoMio(Sender: TObject);  // mio gestore dell'evento
begin
Writeln('...a tutti');
end;

var
Mia:TMiaClasse;       // Classe che genera l'evento
Gestore:TMioGestore;  // Classe che contiene il codice eseguito al monento dell'evento

begin
Mia:=TMiaClasse.Create;  // crea l'istanza della classe
Mia.Saluto;              // usa la sua funzione di saluto prima di assegnare il gestore dell'evento

// Ora proviamo ad assegnare un gestore all'evento OnSaluto
// il codice di questo gestore deve essere un metodo di una classe, per questo motivo è stata creata una classe che conterrà il codice
// dato che della classe non verrà usata nessuna istanza, ma ci servirà solo il codice del gestore in essa contenuto
// non verrà chiamato il suo costruttore:
//  Gestore:=TMioGestore.Create;


Mia.OnSaluto:=Gestore.OnSalutoMio ;  // assegna all'evento il suo gestore
Mia.Saluto;                          // riprova la funzione di saluto dopo l'assegnazione del gestore dell'evento

Readln;
end.

L'esempio precedente è molto semplice e chiaro e mostra come è possibile generare un evento da una classe e creare il codice che viene eseguito in quel momento in un'altra. Questo rende modulare la programmazione e ne sono un esempio i vari componenti grafici della programmazione delphi sotto windows. Infatti genericamente i vari eventi sono per la maggior parte del tipo TNotifyEvent definito come segue:

Type TNotifyEvent = procedure(Sender : TObject) of object;

I gestori di eventi che andremo a definire altrove dovranno avere quindi questa forma, e cioè saranno dei puntatori a metodi che ricevono un solo parametro del tipo TObject.
Naturalmente questo è il caso più semplice dato che potremo avere eventi di diverso tipo con più parametri di tipo costante o variabile.


Ora ritorniamo a parlare di funzioni di callback vediamo come usarle in un caso pratico, come ad esempio l'uso della funzione EnumWindows nelle windows API. In questo caso il codice della funzione si trova su una DLL (user32.dll) che è stata compilata in C quindi la convenzione di chiamata da usare sarà STDCALL.
La funzione EnumWindows enumera tutte le top-level windows sullo schermo fornendo il loro handle ad una funzione di callback, definita e implementata esternamente. EnumWindows continuerà fino a che non viene raggiunta l'ultima finestra oppure la funzione di callback restituisce un valore FALSE.

Vediamo cosa dice la definizione della funzione in MSDN

Sintassi

 BOOL EnumWindows(      
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);

Parametri

  • lpEnumFunc [in] Puntatore ad una funzione di callback specificata esternamente.
  • lParam [in] Specifica una valore specificato esternamente da passare alla funzione di callback.


Valore restituito
Se la funzione ha funzionato correttamente, il risultato sarà diverso da zero.
Se la funzione fallisce restituirà un valore pari a zero. Per avere più informazioni sull'errore usare la funzione GetLastError.
Se la funzione di callback restituisce zero, allora la funzione EnumWindows farà altrettanto. In questo caso sarà la funzione di callback a chiamare la funzione SetLastError per avere un report sull'errore commesso da rigirare alla Funzione chiamante EnumWindows.  

Ricaviamo allo stesso modo le specifiche della funzione di callback:

Sintassi

BOOL CALLBACK EnumWindowsProc(      
HWND hwnd,
LPARAM lParam
);

Parametri

  • hwnd [in] L'handle della top-level window.
  • lParam [in] Parametro da passare a questa funzione di callback da parte delle funzioni EnumWindows o EnumDesktopWindows.


Valore restituito
Per continuare l'enumerazione la funzione deve restituire TRUE, mentre per fermarla il valore deve essere FALSE.


Ora che abbiamo le specifiche della funzione traduciamo tutto in delphi:

// La funzione di callback deve avere 2 parametri e restiruire un valore boolean
// la modalità di chiamata deve essere STDCALL

...
function EnumWindowsFunc(Handle: THandle; List: TStringList):boolean; stdcall;
var
caption: array[0..256] of Char;
begin
if GetWindowText(Handle, Caption, SizeOf(Caption)-1) 0 then begin //recupera la Caption della finestra
List.Add(Caption);                                                 //la aggiunge alla lista
SetWindowText(Handle, PChar('About - ' + Caption)) ;               //Modifica la caption
end;
result:=True;
end;

//uso della funzione
Memo1.Clear; //ripulisce il contenitore da passare come parametro alla funzione EnumWindows
EnumWindows(@EnumWindowsFunc, LParam(Memo1.Lines)); // TypreCast del puntatore alle stringlist
// nel memo in LParam che è un longint (intero a 32bit)

Qualcuno si potrebbe chiedere dato che esistono i tipi procedurali, perché abbiamo dovuto passare in maniera rude l'indirizzo della funzione?
La spiegazione sta nel come la funzione è stata importata, dato che il compilatore da quel momento in poi pretenderà gli stessi parametri.
Se andiamo a vedere nel file windows.pas troveremo l'importazione della funzione e le definizioni dei suoi parametri:

TFNWndEnumProc = TFarProc;
TFarProc = Pointer;
function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;

Quindi il compilatore pretenderà come primo parametro un puntatore e quindi l'indirizzo della funzione, ma se invece creiamo il nostro bel modello di tipo procedurale e dichiariamo la funzione in un altro modo potremo utilizzare una variabile procedurale:

// Definisco il tipo procedurale che ci serve
type
TEnumProcCallback = function(Handle: THandle; List: TStringList):boolean; stdcall;

// Importo la funzione Enumwindows dalla libreria usando il nuovo tipo procedurale
function EnumWindows(lpEnumFunc: TEnumProcCallback; lParam: LPARAM): BOOL; stdcall; external 'user32.dll';

//Implemento sempre la stessa funzione di prima
function EnumWindowsFunc(Handle: THandle; List: TStringList) : boolean ; stdcall;
var
caption: array[0..256] of Char;
begin
if GetWindowText (Handle, Caption, SizeOf(Caption)-1) 0 then begin
List.Add(Caption) ;
SetWindowText(Handle, PChar('About - ' + Caption)) ;
end;
result :=True;
end;

//Uso la funzione come se fosse una variabile procedurale
EnumWindows(EnumWindowsFunc, LParam(Memo1.Lines));

Vorrei terminare precisando che non per tutte le librerie vale la regola della chiamata STDCALL, infatti alcune potrebbero richiedere la modalità CDECL tipica delle librerie compilare in C++, quindi occorre fare attenzione alle modalità di chiamata, che solitamente non è documentata esplicitamente.






Ultimo aggiornamento ( Sabato 31 Luglio 2010 10:28 )  
Loading

Login




Chiudi