En esta oportunidad tocaremos el tema de los eventos. Es un poco más complicado de entender, y lamentablemente se necesita ser un poco más técnico.
Lo bueno de ver este tema es que se puede comprender como es que funcionan los eventos en Delphi, acercándonos más a los ricos conceptos POO.
Intuitivamente hacemos uso de eventos constantemente: OnClick, OnDblClick, OnChange, OnActivate, OnCloseQuery, OnClose, etc. ¿Pero que son en realidad? ¿Cómo y cuando se ejecutan?
De foma muy vaga, decimos que un evento es una rutina a ejecutar. Pero esto no es cierto... en realidad los eventos son "disparadores" de rutinas. Los eventos se emplean para notificar al usuario (u objeto interesado) de que algo a cambiado en el objeto que lo dispara. ¿Que rutina ejecutará? Aquella rutina que nosotros indicamos cuando hacemos doble clic en evento interesado a capturar.
En realidad el objeto no sabe que rutina va a hacer... simplemente sabe la dirección de la memoria donde está la rutina.
Por si fuera poco, estas rutinas no están obligadas a implementarse. Es decir, que el objeto "notificará" de los "cambios" siempre y cuando uno le asigne una rutina. O en pocas: cuando uno le indique que hacer con dichos "cambios".
Dije que esto se maneja con direcciones de memoria. Para comprender el secreto de ésto necesito ir un paso atrás.
Comenzaré a hablar sobre punteros a funciones.
Punteros a funciones
Un Puntero, no es más que una variable que apunta a una porción de memoria determinada. Todo lo que declaramos en Delphi ocupa memoria. Por ejemplo tenemos una función como ésta:
interface uses StrUtils; function UnaFuncion(Valor: string): string; implementation function UnaFuncion(Valor: string): string; begin result := reversestring(Valor); end;
"UnaFuncion" ocupará una dirección xxx en memoria. Ahora, Delphi es un lenguaje fuertemente tipeado, por lo cual podemos declarar muchos tipos. Incluso, podemos hacer tipos de función.
Gracias a los tipo de función es que podemos tener punteros a funciones. ¿Porqué no hacer un puntero a la función anterior? ¿Se puede? ¡Claro!
1. Declaramos en type algo como esto:
type TFuncion = function(Param: string): string;
Como se puede ver, hemos declarado un tipo llamado TFuncion. Este tipo no es más que una función con un parámetro y un valor de salida string. Necesariamente debe coincidir la "declaración" de TFuncion con la función a la que deseamos apuntar.
2. Declaremos dos variables: una para el resultado de la función y otra del tipo TFuncion:
var texto: string; Fx: TFuncion;
3. Ahora podemos hacer algo esto:
begin // Asignamos a Fx la función "UnaFuncion" Fx := UnaFuncion; texto := Fx('Un texto'); ShowMessage(texto); end;
Como queda ilustrado, a Fx le asignamos la función "UnaFunción". Lo que hace el compilador es almacenar en Fx el valor de la memoria de UnaFunción. Si xxx es la dirección de memoria de UnaFunción, en Fx quedará xxx. Por tanto Fx "apunta" a UnaFunción. ¡Listo tenemos un puntero a una función!.
Al ejecutar la función almacenada en Fx, podemos ver que el texto que nos muestra el ShowMessage es "otxet nU".
De modo similar, los eventos son punteros. Punteros a métodos para ser exactos.
Nota: al igual que las funciones, existen los punteros a procedimientos. El principio es el mismo, lo que cambia es que no se trata de función, sino de procedimientos:
TProcedimiento = procedure(Param: string);
Punteros a métodos
He dicho antes que los eventos son punteros a métodos. Los punteros a métodos son muy similares, se declara un "tipo de función" y se le añade al final "Of Object". Por ejemplo, en Delphi contamos con el TNotifyEvent:
type TNotifyEvent = procedure(Sender: TObject) of object;
¿Le resulta similar a algo? Ejemplos de TNotifyEvent son OnClick y OnActivate. En pocas cualquier evento que vea en los componentes que tenga sólo el parámetro Sender es del tipo TNotifyEvent.
Este tipo de datos almacena la dirección de memoria de la rutina que será asociada al método y además el objeto sobre el cual se aplica (para poder identificarlos del resto).
Los punteros a métodos sólo se aplican en el contexto de un objeto. No se puede hacer uso de ellos en forma "suelta". Deben estar asociados a un objeto.
Probemos entonces con lo aprendido.
1. Declare el siguiente objeto y puntero a método.
type //punteros a métodos TGritoEvento = procedure(Valor: integer) of Object; // Una clase que usará los punteros a métodos TSujetoLoteria = class(Tobject) GritarValor: TGritoEvento; CantarValor: TGritoEvento; procedure Accion(NroAccion, Valor: integer); end;
2. Ahora dele la siguiente implementación a "Accion":
procedure TSujetoLoteria.Accion(NroAccion, Valor: integer); begin case NroAccion of 0: if Assigned(GritarValor) then GritarValor(Valor); 1: if Assigned(CantarValor) then CantarValor(Valor); else ShowMessage('Accion invalida'); end; // case end;
Como se vé, dependiendo del número de acción se dispará ya sea el evento "GritarValor" o "CantarValor" con el valor dado por parámetro.
Si se presta atención, primeramente, se evalúa si GritarValor o CantarValor apuntan a una dirección de memoria válida (esto se hace con Assigned).
3. Ahora nos falta implementar las rutinas que queremos que se realizen cuando "grite" o "cante". Implemente las siguientes rutinas como métodos de algun form (o si desea como métodos de la misma clase TSujetoLoteria):
procedure TForm1.RutinaGrito(Valor: integer); begin ShowMessage('EL NUMERO ES ' + IntToStr(Valor)); end; procedure TForm1.RutinaCanto(Valor: integer); begin ShowMessage('El número de hoy es ' + IntToStr(Valor)); end;
Yo lo hice dentro de un TForm para demostrar que no necesariamente la rutina debe estar en la misma clase. De hecho lo normal y esperado es que éstas rutinas sean ajenas a la clase. Recuerde que buscamos que el objeto nos "avise" de los cambios, el no sabe donde están las rutinas.
4. Ahora coloque un botón y realice lo siguiente:
procedure TForm1.Button1Click(Sender: TObject); var TChico: TSujetoLoteria; begin //creamos al objeto TChico := TSujetoLoteria.Create; // asignamos las rutinas a hacer en los eventos TChico.GritarValor := RutinaGrito; TChico.CantarValor := RutinaCanto; TChico.Accion(0,200); // Mensaje! TChico.Accion(1,40); // Mensaje! // liberamos todo TChico.GritarValor := nil; TChico.CantarValor := nil; TChico.Free; end;
Como puede verse, hemos hecho la asignación de las rutinas a los eventos. GritarValor y CantarValor apuntarán a la dirección de memoria de los métodos RutinaGrito y Rutinacanto respectivamente.
Pruebe el código, deberá recibir dos mensajes, uno con el "grito" de 200 y otro con el "canto" de 40.
¿Que hicimos? básicamente hemos declarado dos rutinas, que se asociarán a dos eventos que nos ofrece a disposición la clase TSujetoLoteria.
Ahora comente la asignación de RutinaGrito y pruebe nuevamente.
// asignamos las rutinas a hacer en los eventos //TChico.GritarValor := RutinaGrito; TChico.CantarValor := RutinaCanto;
¿Que sucede? Sólo recibe el mensaje de canto. ¿Porqué? Porque no se le asignó rutina al evento. Como he dicho: los eventos sólo se disparan si tienen una rutina asociada a ellos. Cuando da doble click sobre el OnClick de un botón está "creando" una respuesta (método) para dicho evento. Por tanto, internamente OnClick apuntará a la dirección de memoria del método Button1Cick (por ejemplo).
Bueno, en el proximo post continuaré explicando el tema de los métodos. Ya este post se hizo largo.