delphi
Screen.Cursor := crHourglass; try // Operación larga finally Screen.Cursor := crDefault; end;
Si usamos este patrón literalmente, tendremos problemas: ¿qué sucedería si la operación de larga duración llamase, a su vez, a otra operación escrita en Delphi... que también cambiase la forma del cursor? Evidentemente, al terminar la operación anidada, la forma del cursor cambiaría al cursor por omisión, aunque a la operación externa le faltase algún tiempo todavía para terminar. No es difícil corregir el problema, modificando el patrón:
delphi
var C: TCursor; begin C := Screen.Cursor; Screen.Cursor := crHourglass; try // Operación larga finally Screen.Cursor := C; end; end;
Lo malo es que ahora tenemos más instrucciones, y es fácil hartarse si hay que cambiar el cursor con cierta frecuencia. Aún peor es que la mayor parte del código lo consuma la instrucción try/finally, que no podemos encapsular; podemos, eso sí, definir métodos SalvarCursor y RestaurarCursor, pero nos ahorrarían sólo una pequeña parte de lo que tenemos que teclear. Este caso pide a gritos una solución basada en la Programación Orientada a Aspectos (AOP)... que Delphi no soporta con naturalidad en su estado actual. En la AOP, estos problemas se resolverían
añadiendo atributos declarativos, como en COM+ y en los lenguajes .NET. En este caso particular, añadiríamos un atributo LongDuration, o algo parecido, para marcar el método, en su declaración. Luego, en tiempo de ejecución, el entorno de ejecución debería realizar la operación conocida con el nombre de aspect weaving (tejido, o entramado de aspectos) para que el atributo añadido tuviese algún efecto real.
En COM+, el aspecto weaving se implementa mediante intercepción de llamadas, interponiendo proxies especializados entre el objeto y sus clientes, en el momento en que se instancia el objeto. Y algo parecido podría lograrse con un lenguaje .NET. A pesar de lo interesante que puede ser la AOP, entre otras cosas por su novedad, nos conformaremos con una solución mucho más simple, y casi tan efectiva. ¿Cuál es el problema que tenemos? Hay un patrón de código en el que se ejecutan ciertas instrucciones antes y después de una operación central. ¿Qué tal si aprovechamos un tipo de interfaz para ahorrarnos código? Vamos a crear una unidad, con dos funciones globales declaradas en su sección de interfaz:
delphi
unit Cursores; interface uses Controls, Forms; function SaveCursor: IUnknown; overload; function SaveCursor(C: TCursor): IUnknown; overload; implementation // ... end.
Un adelanto: sólo tendremos que incluir una llamada a una de estas funciones antes de iniciar una operación lenta. Ahora vamos a declarar una clase dentro de la implementación de la unidad:
delphi
type TSaveCursor = class( TInterfacedObject, IUnknown) private OldCursor: TCursor; constructor Create(C: TCursor); destructor Destroy; override; end; Esta sería la implementación de Create: constructor TSaveCursor.Create(C: TCursor); begin OldCursor := Screen.Cursor; Screen.Cursor := C; end;
Al crearse un objeto de la clase TSaveCursor, recordaríamos la forma actual del cursor dentro de uno de sus campos, y cambiaríamos el cursor de acuerdo al tipo de cursor pasado como parámetro al constructor. Esto se complementa con lo que sucede al destruir el objeto:
delphi
destructor TSaveCursor.Destroy; begin Screen.Cursor := OldCursor; inherited Destroy; end;
Al destruir el objeto, por lo tanto, se restaura la forma inicial del cursor. Para terminar con la unidad, veamos lo que hacen las dos versiones sobrecargadas de la función global SaveCursor:
delphi
function SaveCursor(C: TCursor): IUnknown; begin Result := TSaveCursor.Create(C); end; function SaveCursor: IUnknown; begin Result := SaveCursor(crHourglass); end;
Observe atentamente que ambos métodos crean un objeto de la clase SaveCursor... pero lo que devuelven al contexto que los llama es un puntero de interfaz, no el "puntero de clase" normal. Y todo este embrollo, ¿para qué nos sirve? El truco está en el tiempo de vida de los punteros de interfaz. Para verlo con más claridad, veamos cómo tendríamos que utilizar la función SaveCursor para cambiar el cursor dentro de un método que ejecuta una operación lenta:
delphi
procedure OperacionLenta; begin SaveCursor; // ... instrucciones lentas ... end;
Cuando llamamos a SaveCursor, éste nos devuelve un puntero de interfaz instanciado... y cambia el cursor, por supuesto. Los objetos apuntados por tipos de interfaz se destruyen cuando no quedan referencias a él. En este caso ese momento llega al terminar el método. En ese momento se ejecutaría el destructor de TSaveCursor, y restauraríamos el cursor a su estado inicial. ¿Y qué pasa con las operaciones anidadas? Hagamos una prueba: ponga un botón sobre un formulario e intercepte su evento OnClick.
delphi
begin SaveCursor(crHandPoint); Sleep(1000); SaveCursor; Sleep(1000); end;
Primero activamos el cursor de la mano que señala con un dedo, y esperamos un segundo. Luego, cambiamos al reloj de arena, y esperamos otro segundo. Visualmente, podemos comprobar que al terminar los dos segundos, el cursor vuelve a tomar su forma habitual. Y si traceamos la ejecución del evento, instrucción por instrucción, veremos que la restauración del cursor tiene lugar en el orden lógico que todos esperamos: primero se cambia el reloj de arena por la mano, e inmediatamente después, la mano se cambia por la flecha. Nos hemos ahorrado el try/finally, y unas cuantas líneas de código más...
NOTA: Esta información fue extraída de los boletines de Ian Marteens (su autor real).