Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Creación automática de componentes

Admito que el título de este truco puede prestarse a confusión. Me explico: el propósito de este mini-artículo es ofrecer consejos al desarrollador de componentes para que identifique etapas y técnicas de la creación de componentes que son completamente mecánicas; tan mecánicas que pueden diseñarse asistentes (¡de hecho, ya existen!) que realicen estos pasos por usted. El asistente para la creación de componentes que trae Delphi ya automatiza la parte inicial de la definición, creando una clase derivada del ancestro que usted elige, y declarando sus secciones principales. Bien, mi teoría es que se puede ir mucho más allá. Espero que las técnicas que presentaré no sean del todo triviales. Voy a enumerarlas a continuación:

1. Identifique cuáles propiedades de clase pertenecen al componente y cuáles no

Cuando me refiero a propiedades de clase, estoy hablando de propiedades cuyo tipo es una clase, ya sea derivada de TComponent o no. Como sabemos, estos tipos de propiedades almacenan punteros a otros objetos.

¿Qué quiere decir que la propiedad sea "propiedad" del componente. Aquí me he liado al intentar traducir al castellano. El segundo uso de "propiedad" se refiere a ownership: ¿debe el componente destruir el objeto apuntado al ser él mismo destruido? En tal caso, se dice que el componente es el "propietario" (owner, o dueño) del objeto al que apunta la propiedad. Por ejemplo, un cuadro de listas TListBox tiene una propiedad Items, de tipo TStrings, que apunta a un objeto derivado de esta clase:

property Items: TStrings read ... write ...

Cuando el cuadro de lista se construye, el cuadro es responsable de crear un objeto de una clase derivada de TStrings (en este caso, un TListBoxStrings). Cuando el TListBox se destruye, debe también liberar el objeto creado. Pero esto es también muy importante: cuando asignamos a la propiedad Items un objeto TStrings no estamos sencillamente asignando punteros (que es lo que Delphi haría por omisión), sino que debemos realizar una copia del objeto. Precisamente, nuestro primer koan trata acerca de esta característica.

Por el contrario, existen propiedades de tipo clase que apuntan a objetos con vida independiente a la del componente. Por ejemplo, tenemos la propiedad FocusControl del componente TLabel:

property FocusControl: TWinControl read ... write ...

El constructor debe dejar simplemente a nil el valor de esta propiedad, y el destructor no debe hacer nada especial al respecto. Una asignación a este tipo de propiedades se implementa como una simple asignación de punteros. Existe, sin embargo, un problema: el objeto independiente apuntado por FocusControl puede "morir" antes que el TWinControl que hace referencia a él. Entonces la propiedad FocusControl contendrá un puntero inválido...

Para resolver este problema, el componente debe redefinir (override) el método Notification, que hereda de la clase TComponent:

procedure TLabel.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (AComponent = FFocusControl) then
    FFocusControl := nil;
end;

El método Notification existe desde Delphi 1. Cuando un componente muere, el propietario (owner) del componente recorre la lista de los restantes componentes y llama a Notification para cada uno de ellos. Casi siempre, el propietario del componente es el formulario donde se encuentra. Ahora bien, ¿qué pasaría si el TWinControl está en un formulario y el TLabel en otro, como puede suceder a partir de Delphi 2? Para garantizar que el aviso de destrucción se envíe incluso cuando los propietarios son diferentes, debemos tomar precauciones especiales en el método de escritura de la propiedad FocusControl:

procedure TLabel.SetFocusControl(Value: TWinControl);
begin
  if Value <> FFocusControl then
  begin
    if Value <> nil then Value.FreeNotification(Self);
    FFocusControl := Value;
  end;
end;

El método FreeNotification añade la referencia a nuestro componente dentro de una lista privada mantenida por el componente independiente al cual apuntamos (el TWinControl). Cuando el componente independiente muere, su canto de cisne consiste en llamar a Notification para cada elemento de la lista privada.

¿Cómo sabemos si la propiedad que estamos creando debe ser independiente o no? No existe una regla completamente mecánica, pero las siguiente pista ayuda:

  • Si la propiedad es derivada de TComponent, probablemente deba ser una propiedad independiente.
  • Por el contrario, cualquier otro tipo de propiedad debe ser posesión de nuestro componente. Esto se aplica a TFont, a los derivados de TStrings e incluso a las colecciones, que son clases persistentes derivadas de TCollection.

Una vez que hemos realizado esta identificación, podemos pasar a programar mecánicamente los puntos que resumo a continuación:

  • Para propiedades independientes: redefinir Notification, y realizar asignar el puntero nulo cuando se detecta la destrucción del objeto referido. En el método de acceso de escritura, llamar a FreeNotification cuando el valor a asignar no es nulo.
  • Para propiedades dependientes: incluir en el constructor y destructor de nuestro componente el código de construcción y destrucción del objeto dependiente. En el método de acceso de escritura, utilizar Assign para copiar el objeto, no el puntero al mismo.

2. Identifique los valores por omisión de las propiedades escalares "planas"

Cuando digo "plana", quiero decir que se trata de una propiedad que no es de tipo clase: enteros, reales, valores lógicos, conjuntos, etc. Debemos utilizar la directiva default siempre que sea posible, pues salva espacio posteriormente en los ficheros dfm. Para cada directiva de este tipo, por supuesto, debe generarse una instrucción correspondiente en el constructor que inicialice la propiedad. Si el valor por omisión es 0, False o el conjunto vacío, recuerde que esta inicialización es innecesaria, pues al pedirse memoria para el nuevo objeto se limpia ésta con ceros. Recuerde también que la directiva default no puede utilizarse para cadenas de caracteres.

3. Esté atento a los eventos de notificación de ciertos tipos de propiedades

Clases como TFont y TStrings (aunque no son componentes) tienen eventos internos que se disparan cuando ocurren cambios en sus propiedades. Supongamos que estamos implementando un visor binario para ficheros: digamos que se llama THexView. Este componente tiene una propiedad Font para indicar el tipo de letra que utilizaremos al dibujar. Ahora bien, este tipo de letra puede variar en tiempo de ejecución. Por ejemplo, podemos tener instrucciones del siguiente tipo:

HexView1.Font.Style := [fsBold];

El problema es que esta asignación "salta" por encima del método de acceso de escritura SetFont, nunca llega a invocarlo. ¿Cómo nos damos cuenta de que ha cambiado el tipo de letra? Resulta que TFont dispara un evento OnChange para estos casos. Nuestro componente THexView debe crear un método, preferiblemente privado, en este plan:

procedure THewView1.TipoLetraCambiado(Sender: TObject);
begin
  Invalidate; // Redibujar el control
end;

El enlace entre el objeto Font y este receptor se debe realizar durante la construcción del objeto:

constructor THewView1.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FFont := TFont.Create;
  FFont.OnChange := TipoLetraCambiado;
end;

Recuerde que estas propiedades deben ser poseídas por el componente, así que sumamos estos cambios a la lista de cosas por programar que he presentado en el punto número uno.

4. Preste atención a las colecciones

Propiedades tales como Columns, de la rejilla de datos TDBGrid, o Panels, de la archiconocida TStatusBar se implementan a partir de clases derivadas de TCollection. De este modo, las definiciones de columnas o paneles pueden almacenarse de forma automática en el fichero dfm del componente. Desgraciadamente, hay que teclear bastante para poder utilizar propiedades derivadas de TCollection. Por ejemplo, para implementar Columns Delphi define las clases auxiliares TDBGridColumns y TColumn, y la mayor parte de la definición e implementación de estas clases es bastante mecánica. No quiero dar más detalles en esta página, para mantenerla con un tamaño manejable, pero tengo previsto escribir un artículo sobre este tema más adelante.

5. Muchas propiedades escalares llaman a Invalidate al ser asignadas

Si su componente desciende de TControl, es decir, si es un componente visible, es muy probable que tenga que llamar mecánicamente al método Invalidate del control para redibujarlo cuando se cambie el valor de una propiedad. Esto se realiza mecánicamente en el método de acceso de escritura de la correspondiente propiedad. Por ejemplo:

procedure TDBJPEG.SetStretch(Value: Boolean);
begin
  if FStretch <> Value then
  begin
    FStretch := Value;
    Invalidate;
  end;
end;

6. La mayor parte del código de un control de datos es mecánica

Las reglas del juego para los controles data-aware son bastante simples, pero requieren que el programador teclee mucho código: creación del TDataLink, intercepción de sus eventos y exportación de algunas de sus propiedades, manejos de mensajes internos de Delphi como CM_ENTER y CM_EXIT... Por los mismos motivos que he expuesto al hablar de las colecciones, no voy a profundizar ahora en este tema.

Voy a intentar crear un experto para la creación de componentes que automatice todas estas tareas y algunas más. Cuando lo tenga, lo pondré en la página de ejemplos.