Colecciones y persistencia
A pesar de su sencillez, uno de los aspectos menos comprendidos de la Programación de Componentes para Delphi es el uso de colecciones (collections). Hace poco descargué desde Internet un componente freeware para mejorar el trabajo con rejillas, al estilo de TStringGrid. El componente funcionaba bastante bien; de hecho, estaba inspirado en el TDBGrid oficial de Delphi, como reconocía el autor. El problema sobrevino cuando intenté configurar las columna de la nueva rejilla en tiempo de diseño: aunque el componente tenía una propiedad Columns, no estaba publicada para tiempo de diseño, y había que configurar toda esta información "a mano", en tiempo de ejecución. El autor se había cepillado todo el código relacionado con el almacenamiento y edición de la información vectorial de columnas. Claro, el código utilizaba esas "misteriosas colecciones"...
COLECCIONES Y ELEMENTOS
Si usted quiere crear una propiedad que almacene un número variable de objetos relacionados, es muy probable que lo que necesite sea una colección. ¿Una colección? ¿Por qué no una lista TList? Muy sencillo: TList desciende directamente de TObject, mientras que TCollection desciende de TPersistent, por lo que ya trae incorporado las técnicas básicas para que toda la colección o lista de elementos pueda almacenarse en disco, o en cualquier flujo de datos (stream) en general. Basta con que definamos una propiedad, cuyo tipo esté basado en TCollection, en la sección published de un componente para que Delphi o C++ Builder puedan almacenar sus datos en un DFM, y para que la propiedad aparezca en el Inspector de Objetos, del mismo modo que si se tratase de un vulgar Integer o String.
La primera peculiaridad que observamos cuando trabajamos con colecciones es la forma en que se crean. Esta es la definición del constructor:
constructor TCollection.Create(ItemClass: TCollectionItemClass);
Recuerde que TCollection desciende de TPersistent, no de TComponent, por lo cual no está obligada a tener un "propietario" (owner). El parámetro del constructor es una referencia de clase, y debe contener el nombre de una clase derivada de TCollectionItem. Esto quiere decir que la colección sabe de antemano qué tipo de objetos va a alojar. Ya en esto se diferencia de TList, que permite guardar punteros arbitrarios.
El próximo paso es saber cómo está definida TCollectionItemClass:
type
TCollectionItem = class(TPersistent)
// Etcétera
end;
TCollectionItemClass = class of TCollectionItem;
Esto quiere decir que los elementos que podemos almacenar dentro de una colección deben pertenecer a alguna clase derivada de TCollectionItem. Por ejemplo, no podemos almacenar objetos TTable directamente en una colección. Por el contrario, debemos crear una clase auxiliar TTableItem, que herede de TCollectionItem, y que apunte internamente a una tabla.
ELEMENTOS DE COLECCIONES Y PERSISTENCIA
Recuerde que el objetivo final de todo esto es poder almacenar listas de elementos más o menos arbitrarios en los ficheros DFM. Usted puede directamente crear propiedades que apunten a tablas (TTable) y, como esta clase deriva de TPersistent, Delphi puede automáticamente almacenar en un DFM los datos necesarios para reconstruir un objeto de esta clase. Más exactamente, lo que Delphi almacena en el DFM son los valores de las propiedades published del objeto. Se supone que estas son las propiedades "características" de la instancia.
Por lo tanto, Delphi también sabe cómo guardar las propiedades que definamos como published en una clase derivada de TCollectionItem. Si queremos guardar listas de punteros a tablas, necesitamos una clase auxiliar definida de este modo:
type
TTableItem = class(TCollectionItem)
private
FTable: TTable; // Recuerde que esto es un puntero
procedure SetTable(Value: TTable);
// Etcétera ...
published
property Table: TTable read FTable write SetTable;
end;
Cuando Delphi vaya a guardar una colección de estos objetos, recorrerá uno a uno los elementos de la colección, y los irá guardando individualmente. Para este objeto en particular, guardará el valor de la propiedad Table. Esta propiedad, que es concretamente un puntero a un componente, se almacena en el DFM utilizando el nombre del componente apuntado, junto al nombre del formulario o módulo en que se encuentra, si no está en el mismo formulario o módulo que el componente que contiene a la colección.
AYUDANDO AL INSPECTOR DE OBJETOS
Sin embargo, no terminan aquí nuestras responsabilidades. Es muy importante que redefinamos el método virtual Assign. De este modo, Delphi sabrá cómo copiar las propiedades importantes desde un elemento a otro. El código que viene a continuación sirve de muestra:
procedure TTableItem.Assign(Source: TPersistent);
begin
if Source is TTableItem then
// Una sola propiedad a copiar
Table := TTableItem(Source)
else
inherited Assign(Source);
end;
Es posible también que queramos redefinir el método GetDisplayName:
function TTableItem.GetDisplayName: string;
begin
if Assigned(FTable) then
Result := FTable.Name
else
Result := inherited GetDisplayName;
end;
¿Se ha fijado en que cuando crea una columna para una rejilla de datos, la columna aparece en el Editor de Columnas con el título TColumn, mientras que cuando a la columna le asigna uno de los campos del conjunto de datos asociado en el Editor de Columnas aparece el nombre del campo? Pues ése es el objetivo de GetDisplayName: indicar con qué nombre aparecerá el elemento de la colección al editarlo con ayuda del Inspector de Objetos.
Por último, si necesita un constructor para realizar alguna inicialización especial, tenga en cuenta que el constructor de TCollectionItem es un constructor virtual, y que no puede modificar sus parámetros. Esta es su declaración:
constructor TCollectionItem.Create(Collection: TCollection); virtual;
¿Se da cuenta de que al constructor se le pasa la colección a la cuál va a pertenecer el nuevo elemento? de este modo, al crear un elemento estaremos automáticamente insertándolo en su colección.
RETOCANDO LA COLECCIÓN
¿Y qué hay de la colección en sí? ¿Podemos declarar directamente propiedades de tipo TCollection? Pues casi, porque tenemos que ocuparnos solamente de un detalle: hay que redefinir el método GetOwner. Este método se utiliza en dos subsistemas diferentes de Delphi. En primer lugar, lo utiliza la función GetNamePath, que es a su vez utilizada por el Inspector de Objetos para nombrar correctamente a los elementos durante su edición. Volviendo al ejemplo del TDBGrid, cuando estamos modificando las propiedades de una columna, en el Inspector de Objetos aparece un nombre como el siguiente para la columna seleccionada:
DBGrid1.Columns[1]
DBGrid1 es el objeto que contiene a la colección, su propietario. Este propietario es también utilizado por el algoritmo que se utiliza en la grabación del componente en el DFM (podéis encontrar más detalles acerca de este proceso, en general, en el libro Secrets of Delphi 2, de Ray Lischner).
De todos modos, si solamente tenemos que redefinir el propietario de la colección, basta con utilizar la clase TOwnedCollection, cuyo constructor tiene el siguiente prototipo:
constructor TOwnerCollection.Create(AOwner: TComponent;
ItemClass: TCollectionItemClass);
Sin embargo, si vamos a programar bastante con la colección, es preferible derivar una clase a partir de TCollection y, además de redefinir GetOwner, introducir una propiedad vectorial por omisión, de nombre Items. TCollection ya tiene una propiedad con este nombre:
property TCollection.Items[I: Integer]: TCollectionItem;
Pero como notará, el tipo de objeto que devuelve es el tipo general TCollectionItem, lo que nos obligará a utilizar constantemente la conversión de tipos. Además, esta propiedad no es la propiedad vectorial por omisión de la clase. Para acceder a una de las tablas de la lista de tablas de nuestro hipotético componente, tendríamos que teclear una y otra vez cosas como ésta:
TTableItem(MiComponente.Tablas.Items[1]).Table.Open;
Así que es frecuente que ocultemos la antigua propiedad Items en la nueva clase del siguiente modo:
type
TTableCollection = class(TCollection)
private
function GetItems(I: Integer): TTable;
procedure SetItems(I: Integer; Value: TTable);
// Etcétera ...
public
property Items[I: Integer]: TTable read GetItems write SetItems;
default;
end;
function TTableCollection.GetItems(I: Integer): TTable;
begin
Result := TTableItem(inherited Items[I]).Table;
end;
procedure TTableCollection.SetItems(I: Integer; Value: TTable);
begin
TTableItems(inherited Items[I]).Table := Value;
end;
Ahora podemos abreviar la instrucción que hemos mostrado antes de este modo:
MiComponente.Tablas[1].Open;
Como en nuestro ejemplo lo importante era la tabla, y no el TTableItem, hemos hecho que Items devuelva la tabla asociada al elemento. Es más común, no obstante, que devuelva el puntero a todo el elemento.
INTEGRANDO LA COLECCIÓN EN EL COMPONENTE
Por supuesto, falta incorporar a la nueva clase dentro de nuestro componente, pero esto es muy sencillo. Sólo tiene que recordar que la colección será propiedad del componente, y que esto implica:
- El componente debe construir una colección internamente en el constructor, y destruirla en su destructor.
- El método de acceso a la propiedad para escritura debe utilizar Assign para copiar la colección que se asigna externamente a la colección interna mantenida por el componente.
Puede encontrar más detalles en un artículo anterior de este mismo sitio.
Existen, claro está, muchas más técnicas que podemos aprovechar en relación con las colecciones. Por ejemplo, podemos redefinir el método Update de la colección, para que ésta notifique a su propietario cuando se produzcan cambios en las propiedades de algunos de sus elementos.
COMPARACION DE COLECCIONES
Para terminar este artículo, quiero presentarle una técnica interesante: ¿sabe usted cómo Delphi y C++ Builder comparan dos colecciones entre sí, para ver si son iguales? La unidad Classes define con este propósito la siguiente función:
function CollectionsEqual(C1, C2: TCollection): Boolean;
La técnica utilizada por CollectionsEqual consiste en guardar las dos colecciones en un flujo de datos en memoria (TMemoryStream). Luego obtiene los dos punteros a ambas zonas de memoria, ¡y realiza una comparación binaria entre estas dos! Dicho de otra forma: "aplana" los dos objetos complejos, y compara sus representaciones binarias.
|