InicioOfftopicDelphi cómo crear un componente (2/3)

Delphi cómo crear un componente (2/3)

Offtopic5/10/2011
Creando componentes propios en Delphi (2/3)
- el objetivo es proporcionar un buen punto de partida completamente en castellano
- se utiliza Delphi 2009 para hacer las capturas y demo, pero es equivalente en los anteriores y sucesivos
- es una traducción casera, no tiene una semántica perfecta sobre poo o delphi, se utilizan términos conocidos para hacerlo accesible a cualquier programador
- no se responderán preguntas, no tengo mayores conocimientos, hay libros y foros especializados en estos temas
- para ser buen "escritor" primero hay que ser buen "lector"

Cómo escribir las propiedades avanzadas de un componente
Artículo publicado por: Peter Morris
"Este artículo apareció originalmente en Delphi Developer. Copyright Pinnacle Publishing, Inc. Todos los derechos reservados."

En la primera parte cubrimos los aspectos básicos sobre la creación de componentes, ahora veremos como escribir propiedades mas avanzadas, los métodos (streaming) para estas propiedades, y las subpropiedades.

Referencias a componentes
Algunos componentes necesitan hacer referencia a otros componentes. Por ejemplo TLabel tiene una propiedad "FocusControl". Cuando se incluye un ampersand & en la propiedad "Caption" este subraya la letra siguiente, &Hello se convierte en Hello, presionar el método abreviado alt-H en el teclado desencadenará un evento en la etiqueta. Si la propiedad "FocusControl" esta activada, el foco pasará al control especificado.

Tener tal propiedad en un componente es bastante simple. Todo lo que hay que hacer es declarar una nueva propiedad, y establecerla en la clase ancestro mas elevado posible (en el arbol de herencia), que acepte este tipo de propiedad, (TWinControl permitirá a cualquier descendiente de TwinControl ser utilizarlo), pero tiene sus consecuencias.
type
TSimpleExample = class(TComponent)
private
FFocusControl : TWinControl;
protected
procedure SetFocusControl(const value : TWinControl);
virtual;
public
protected
property FocusControl : TWinControl
read FFocusControl
write SetFocusControl;
end;

procedure TSimpleExample.SetFocusControl(const Value : TWinControl);
begin
FFocusControl := Value;
end;
Tomemos el ejemplo de arriba. Es un ejemplo un poco simple (mas allá del nombre del componente) de como escribir un componente que hace referencia a otro componente. Si tenemos tal propiedad en un componente, el inspector de objetos (Object Inspector) nos muestra un combo con la lista de los componentes que encajan en este criterio (todos los componentes de TWinControl).

Nuestro componente puede hacer algo como esto:
procedure TSimpleExample.DoSomething;
begin
if (Assigned(FocusControl)) and
(FocusControl.Enabled) then
FocusControl.Setfocus;
end;
Primero verificamos si la propiedad fue asignada, si es así establecemos el foco a esta, pero hay situaciones en las cuales la propiedad no es Nil y sin embargo el componente al que apunta ya no es válido. Esto sucede a menudo cuando una propiedad hace referencia a un componente que ha sido destruido.

Afortunadamente Delphi provee una solución. Cada vez que un componente es destruído este notifica a su propietario (owner) que está siendo destruido. En este punto cada componente que es hijo (owned) por el mismo formulario (form) es notificado de este evento también. Para atrapar este evento debemos sobre escribir el método definido en el componente base TComponent, llamado "Notification".
type
TSimpleExample = class(TComponent)
private
FFocusControl : TWinControl;
protected
procedure SetFocusControl(const value : TWinControl);
virtual;
public
protected
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
property FocusControl : TWinControl
read FFocusControl
write SetFocusControl;
end;

procedure TSimpleExample.SetFocusControl(const Value : TWinControl);
begin
FFocusControl := Value;
end;

procedure TSimpleExample.Notification(AComponent : TComponent;
Operation : TOperation);
begin
If (Operation = opRemove) and
(AComponent = FocusControl) then
FFocusControl := Nil;
end;
Ahora cuando el componente al que hacemos referencia es destruído somos notificados, en este caso podemos establecer la referencia a Nil. Vale aclarar que solo serán notificados los componentes contenidos en el mismo formulario.

Esto nos trae otro problema. Solo recibiremos la notificación de que el componente está siendo destruído si se encuentra en el mismo formulario. Es posible que la propiedad esté apuntando (haciendo referencia) a un componentes de otros formularios (o también sin ningún formulario asociado), y cuando estos componentes son destruídos no recibimos notificación de ningún tipo. Una vez más, hay una solución.

TComponent introduce un método llamado "FreeNotification". El objetivo de FreeNotification es decirle al componente (FocusControl) que nos tenga en mente cuando es destruído.

Una implementación se vería como esta:
type
TSimpleExample = class(TComponent)
private
FFocusControl : TWinControl;
protected
procedure SetFocusControl(const value : TWinControl);
virtual;
public
protected
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
property FocusControl : TWinControl
read FFocusControl
write SetFocusControl;
end;

procedure TSimpleExample.SetFocusControl(const Value : TWinControl);
begin
if Assigned(FFocusControl) then
FFocusControl.RemoveFreeNotification(Self);

FFocusControl := Value;

if Assigned(FFocusControl) then
FFocusControl.FreeNotification(Self);
end;

procedure TSimpleExample.Notification(AComponent : TComponent;
Operation : TOperation);
begin
if (Operation = opRemove) and
(AComponent = FocusControl) then
FFocusControl := Nil;
end;
Cuando establecemos la propiedad FocusControl lo primero es verificar si ya está asociada a un componente. Si este es el caso necesitamos decirle al componente original que ya no necesitamos saber cuando es destruído. Una vez que la propiedad hace referencia al nuevo componente le informamos a este que necesitamos una notificación cuando es liberado. El resto de nuestro código continúa igual ya que el componente al que hacemos referencia sigue llamando a nuestro método de notificación.

Conjuntos

Conjuntos
Esta sección es realmente bastante simple y no tardaremos en cubrirla. Consideramos que ya conocemos como crear tipos ordinales.
type
TComponentOption = (coDrawLines,
coDrawSolid,
coDrawBackground);
Propiedades de este tipo se mostrarán en un combo con la lista de todos los valores posibles, pero aveces necesitamos una combinación de muchos de estos valores. Aquí es donde entran en juego los conjuntos.
Type
TComponentOption = (coDrawLines,
coDrawSolid,
coDrawBackground);
TComponentOptions = set of TComponentOption;
Publicar una propiedad del tipo TComponentOptions resultará en que aparece un [+] al lado del nombre de la propiedad. Este botón expande las opciones de la propiedad. Para cada elemento en TComponentOption veremos una propiedad Boolean (tilde check box), podemos incluir / excluir elementos del conjunto estableciendo el valor a True / False.

Como vemos, es sencillo verificar / alterar elementos en un conjunto en nuestro componente.
if coDrawLines in OurComponentOptions
then DrawTheLines;
o
procedure TsomeComponent.SetOurComponentOptions(const value : TComponentOptions);
begin
if (coDrawSolid in Value) and
(coDrawBackground in value) then
{raise an exception}

FOurComponentOptions := Value;
Invalidate;
end;

Propiedades Binarias

Propiedades Binarias
Aveces es necesario escribir nuestras propias rutinas de streaming para leer y escribir las propiedades de tipos propios (así es cómo Delphi lee / escribe las propiedades de arriba a la izquierda en los componentes no visibles sin tener que publicar las propiedades en el inspector de objetos).

Por ejemplo, una vez escribí un componente para dar forma al formulario en base a un bitmap. El código al momento de convertir el bitmap en una ventana era extremadamente lento y no posibilitaba su uso. La solución fue convertir los datos en tiempo de diseño, y hacer un stream con los datos resultantes de la conversión. Crear una propiedad binaria es un proceso de tres pasos.

1. Escribir un método para escribir los datos.
2. Escribir un método para leer los datos.
3. Decirle a Delphi que tenemos una propiedad binaria, y pasarle nuestros métodos de escritura / lectura.
type
TBinaryComponent = class(TComponent)
private
FBinaryData : Pointer;
FBinaryDataSize : DWord;
procedure WriteData(S : TStream);
procedure ReadData(S : TStream);
protected
procedure DefineProperties(Filer : TFiler); override;
public
constructor Create(AOwner : TComponent); override;
end;
Delphi llama DefineProperties cuando necesita hacer stream nuestro componente. Todo lo que necesitamos es sobre escribir este método, y agregarle una propiedad, ya se usando TFiler.DefineProperty o TFiler.DefineBinaryProperty.
procedure TFiler.DefineBinaryProperty(const Name: string;
ReadData, WriteData: TStreamProc;
HasData: Boolean);

constructor TBinaryComponent.Create(AOwner: TComponent);
begin
inherited;
FBinaryDataSize := 0;
end;

procedure TBinaryComponent.DefineProperties(Filer: TFiler);
var
HasData : Boolean;
begin
inherited;
HasData := FBinaryDataSize <> 0;
Filer.DefineBinaryProperty('BinaryData',ReadData,
WriteData, HasData );
end;

procedure TBinaryComponent.ReadData(S: TStream);
begin
S.Read(FBinaryDataSize, SizeOf(DWord));
if FBinaryDataSize > 0 then begin
GetMem(FBinaryData, FBinaryDataSize);
S.Read(FBinaryData^, FBinaryDataSize);
end;
end;

procedure TBinaryComponent.WriteData(S: TStream);
begin
//This will not be called if FBinaryDataSize = 0
S.Write(FBinaryDataSize, Sizeof(DWord));
S.Write(FBinaryData^, FBinaryDataSize);
end;
Primeramente sobre escribimos DefineProperties. Una vez que hecho esto definimos una propiedad binaria con los valores:
· BinaryData : El nombre de la propiedad invisible que usaremos.
· ReadData : El procedimiento responsable de leer los datos.
· WriteData : El procedimiento responsable de leer los datos.
· HasData : Si esto es falso ni siquiera se llama al procedimiento WriteData.

Persistencia
Una explicación rápida sobre persistencia es necesaria para poder referirnos a ella en las siguientes secciones. Persistencia es lo que hace posible que Delphi lea y escriba propiedades de todos sus componentes. TComponent deriva (desciende) de una clase llamada Tpersistent (ver imagen de más arriba, herencia de TWinControl). TPersistent es sencillamente una clase de Delphi con la capacidad de tener propiedades que son leídas y escritas por Delphi, lo que significa que todas las clases descendientes de ella tienen la misma característica.

Colecciones

Colecciones
A medida que progresamos en este artículo vamos viendo características más complejas de los componentes. Las colecciones (Collections) son uno de los tipos de propiedades (tipos de datos) estandar de Delphi mas complejas. Si colocamos un TDBGrid en un formulario y vemos sus propiedades en el inspector de objetos, veremos una propiedad llamada columnas (Columns).

Columns es una propiedad de tipo colección, cuando hacemos click en el botón […] vemos una pequeña ventana emergente (pop up). Esta ventana es el editor de propiedades estandar para las propiedades TCollection (y los descendientes de TCollection).



Cada vez que se presiona el botón New vemos que se agrega un nuevo elemento a la lista (un elemento TColumn), seleccionando este elemento en la lista aparecerán sus propiedades en el inspector de objetos, así se pueden cambiar sus propiedades y eventos. Cómo se hace esto?

La propiedad Columns desciende de TCollection. TCollection es similar a un arreglo, el cual contiene una lista de elementos TCollectionItem. Ya que TCollection desciende de TPersistent es posible hacer fluir (stream) esta lista de elementos, similarmente, TCollectionItem también es descendiente de TPersistent y tambien puede hacer fluir (stream) sus propiedades. Lo que tenemos es un elemento tipo arreglo capaz de hacer fluir todos sus elementos y propiedades.

Lo primero por hacer cuando creamos una estructura propia basada en TCollection / TCollectionItem es definir nuestro propio elemento CollectionItem.

type
TOurCollectionItem = class(TCollectionItem)
private
FSomeValue : String;
protected
function GetDisplayName : String; override;
public
procedure Assign(TPersistent); override;
published
property SomeValue : String
read FSomeValue
write FSomeValue;
end;
Lo que hacemos es crear una clase descendiente de TCollectionItem. Hemos agregado una propiedad token llamada "SomeValue", sobre escribiendo la función GetDisplayName (para modificar el texto que se muestra en el editor preestablecido (default)), y finalmente sobre escribimos el método Assign para permitir que TOurCollectionItem sea asignada a otro TOurCollectionItem. Si no realizamos este último paso el método Assign de nuestra clase Collection no funcionará!
procedure TOurCollectionItem.Assign(TPersistent);
begin
if Source is TOurCollectionItem then
SomeValue := TOurCollectionItem(Source).SomeValue
else
inherited; //raises an exception
end;

function TOurCollectionItem.GetDisplayName: String;
begin
Result := Format('Item %d',);
end;
La implementación de TOurCollection es mucho más compleja, y requiere un poco mas de trabajo.
TOurCollection = class(TCollection)
private
FOwner : TComponent;
protected
function GetOwner : TPersistent; override;
function GetItem(Index: Integer): TOurCollectionItem;
procedure SetItem(Index: Integer; Value:
TOurCollectionItem);
procedure Update(Item: TOurCollectionItem);
public
constructor Create(AOwner : TComponent);

function Add : TOurCollectionItem;
function Insert(Index: Integer): TOurCollectionItem;

property Items[Index: Integer]: TOurCollectionItem
read GetItem
write SetItem;
end;
Hay una serie de puntos a ver para cubrir la explicación de esta declaración, así que empezamos desde arriba y vemos uno por uno.

GetOwner es un método virtual introducido en TPersistent. Es necesario sobre escribirlo ya que el código por defecto de este método devuelve Nil. En nuestro caso modificamos el constructor para recibir solo un parámetro (AOwner : TComponent). Almacenamos este valor en FOwner, el cual es devuelto como resultado de GetOwner (TComponent desciende de TPersistent, por consiguiente el resultado es un tipo válido).
constructor TOurCollection.Create(AOwner: TComponent);
begin
inherited Create(TOurCollectionItem);
FOwner := AOwner;
end;

function TOurCollection.GetOwner: TPersistent;
begin
Result := FOwner;
end;

Create no solo almacena al dueño (owner) (el cual es requerido por el inspector de objetos para funcionar correctamente), también le dice a Delphi cual es nuestra clase CollectionItem haciendo la llamanda "inherited Create(TOurCollectionItem)".

GetItem / SetItem se declaran de la misma forma que en TCollection, pero en vez de trabajar sobre TCollectionItem trabajan en la nueva clase TOurCollectionItem. Luego son usados en nuestros elementos de propiedad (item property).

Update como el anterior es un reemplazo directo del original, solo que trabajando en nuestra nueva clase TCollectionItem.

Add / Insert ambos son responsables de agregar elementos a la lista, fueron reemplazados para devolver objetos del tipo (clase) apropiado.

Por último introducimos la propiedad "Items" para reemplazar a la original, así obtenemos un resultado de TOurCollectionItem en vez de TCollectionItem ahorrándonos el problema innecesario de moldear (cast) el tipo del resultado cada vez que es devuelto.

Finalmente vemos un ejemplo de como introducir esta característica en las propiedades de nuestro propio componente.
TCollectionComponent = class(TComponent)
private
FOurCollection : TOurCollection;
procedure SetOurCollection(const Value:
TOurCollection);
public
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
published
property OurCollection : TOurCollection
read FOurCollection
write SetOurCollection;
end;
Es tan sencillo como eso. El trabajo duro es escribir la clase TCollection. Nuestro constructor crea la clase colección, el destructor la destruye, y SetOurCollection hace esto:
constructor TCollectionComponent.Create(AOwner: TComponent);
begin
inherited;
FOurCollection := TOurCollection.Create(Self);
end;

destructor TCollectionComponent.Destroy;
begin
FOurCollection.Free;
inherited;
end;

procedure TCollectionComponent.SetOurCollection(
const Value: TOurCollection);
begin
FOurCollection.Assign(Value);
end;
Como ya vimos, el parámetro (Self) pasado a TOurCollectionItem.Create es almacenado en la variable FOwner de TOurCollection, la cual es devuelta como resultado de GetOwner. Algo a tener en cuenta es que en SetOurCollection no establecemos FOurCollection := value como reemplazando el objeto (los objetos son simples punteros), asignamos el valor a nuestra propiedad.

Versiones posteriores de Delphi hacen que esto sea aún mas simple. En vez de tener que sobre escribir GetOwner en nuestra clase Collection, en cambio ahora podemos derivar (descender) nuestra clase Collection de TOwnedCollection. TOwnedCollection es un contenedor para TCollection con este trabajo hecho por nosotros.

Sub-propiedades

Sub-propiedades
Anteriormente en este artículo vimos como es posible crear una propiedad extensible. La limitación de la técnica anterior era que cada sub-item aparecía como una propiedad Boolean. En la siguiente sección demostramos como crear propiedades extendibles que pueden contener propiedades de cualquier tipo.

Si un componente requiere una propiedad que es de tipo registro (record), esto podría implementarse fácilmente poniendo cada una de las propiedades en forma separada. Si nuestro componente necesita introducir dos o mas propiedades del mismo tipo de complejidad (records) la representación de nuestro inspector de objetos se vuelve repentinamente mas complicada.

La respuesta es crear una estructura compleja (parecida a un registro de objetos) y publicar esta estructura como una propiedad cuando sea necesario. El problema evidente es que Delphi no sabe como representar esta propiedad, a menos que le digamos como. Crear un editor de propiedades super desarrollado (con diálogos, etc) sería mortal, afortunadamente Delphi provee una solución. Como vimos anteriormente, los flujos (streams) internos de Delphi están basados en la clase TPersistent. El primer paso es entonces derivar (descender) nuestra estructura compleja de esta clase.
type
TExpandingRecord = class(TPersistent)
private
FIntegerProp : Integer;
FStringProp : String;
FCollectionProp : TOurCollection;
procedure SetCollectionProp(const Value:
TOurCollection);
public
constructor Create(AOwner : TComponent);
destructor Destroy; override;

procedure Assign(TPersistent); override;
published
property IntegerProp : Integer
read FIntegerProp
write FIntegerProp;
property StringProp : String
read FStringProp
write FStringProp;
property CollectionProp : TOurCollection
read FCollectionProp
write SetCollectionProp;
end;
En la estructura de arriba hemos creado un descendiente de TPersistent y le pusimos tres propiedades como ejemplo, un entero (integer), una cadena (string), y la colección que habiamos creado antes TOurCollection.

El constructor y el destructor simplemente se encargan de crear y destruir el objeto CollectionProp, y SetCollectionProp está puesto para impedir que la referencia a este objeto se pierda (hacemos Assign(value), en vez de FCollectionProp := value). Considerando que Assign está implementado para permitirnos asignar las propiedades de TExpandableRecord a otro TExpandableRecord (nuevamente, es innecesario porque necesitaremos Assign cuando este implementado finalmente como propiedad de un componente).
procedure TExpandingRecord.Assign(TPersistent);
begin
if Source is TExpandingRecord then
with TExpandingRecord(Source) do begin
Self.IntegerProp := IntegerProp;
Self.StringProp := StringProp;

//This actually assigns
Self.CollectionProp := CollectionProp;
end else
inherited; //raises an exception
end;

constructor TExpandingRecord.Create(AOwner : TComponent);
begin
inherited Create;
FCollectionProp := TOurCollection.Create(AOwner);
end;

destructor TExpandingRecord.Destroy;
begin
FCollectionProp.Free;
inherited;
end;

procedure TExpandingRecord.SetCollectionProp(const Value: TOurCollection);
begin
FCollectionProp.Assign(Value);
end;
Una vez que este código fue implementado el trabajo duro ya esta hecho. Ahora implementar esta clase como un objeto es algo directo.
TExpandingComponent = class(TComponent)
private
FProperty1,
FProperty2,
FProperty3 : TExpandingRecord;
protected
procedure SetProperty1(const Value :
TExpandingRecord);
procedure SetProperty2(const Value :
TExpandingRecord);
procedure SetProperty3(const Value :
TExpandingRecord);
public
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
published
property Property1 : TExpandingRecord
read FProperty1
write SetProperty1;
property Property2 : TExpandingRecord
read FProperty2
write SetProperty2;
property Property3 : TExpandingRecord
read FProperty3
write SetProperty3;
end;
El constructor tendrá que crear estos tres objetos usados como propiedades, el destructor obviamente tendrá que hacer lo propio. El procedimiento SetPropertyX hará Assign(Value) al objeto correspondiente.

Compilamos este componente en un paquete y ponemos un componente TExpandingComponent en nuestro formulario. Al mirar en el inspector de objetos vemos que todas nuestras propiedades TExpandingRecord tienen al lado un [+], este botón expande nuestra propiedad para revelar todas sus sub-propiedades.



A primera vista todo parece bien, nuestras propiedades se muestran con un sub-conjuto expandible de propiedades, pero no todo es tan perfecto como parece ser. Al hacer click en el botón […] de nuestra colección "CollectionProp" no se invoca al editor estandard de TCollection, de hecho, no pasa nada.

Y ahora qué hicimos mal?, la respuesta es nada. El error no es nuestro, la falla proviene de los desarrolladores de Delphi, Borland. Aunque me gusta pensar que los desarrolladores de Delphi son infalibles, no lo son, y aveces también cometen errores, y este es un ejemplo.

Al registrar un editor de propiedades se puede limitar el alcance de los componentes a los cuales se aplica. Se puede especificar que este solo trabaje con ciertos nombres de propiedades, o solo ciertos componentes, y esto es lo que hemos hecho. Aunque la arquitectura de Delphi define que el primer objeto con capacidad de streaming es TPersistent, alguien en Borland registró al editor de propiedades para trabajar solo con objetos descendientes de TComponent. Un descuido seguramente, pero no ayuda en nada.

La solución a este problema es hacer descender nuestro TExpandableRecord de TComponent en vez de TPersistent, entonces el editor de propiedades será invocado. El problema es que el editor de propiedades que trae Delfi por defecto muestra un combo para las propiedades de tipo TComponenet y sus descendientes, en vez de mostrar una lista expandible de sub-propiedades.

La solución total a este dilema radica en los editores de propiedades, y por último en este artículo veremos este tema.

Fin de la parte 2/3

En el próximo post veremos el editor de propiedades de los componentes, cómo escribir editores de propiedades para nuestros componentes y cómo crear componentes ocultos ´hidden´.

Nos vemos...
Datos archivados del Taringa! original
3puntos
3,351visitas
0comentarios
Actividad nueva en Posteamelo
0puntos
4visitas
0comentarios
Dar puntos:

Dejá tu comentario

0/2000

Autor del Post

A
Usuario
Puntos0
Posts3
Ver perfil →
PosteameloArchivo Histórico de Taringa! (2004-2017). Preservando la inteligencia colectiva de la internet hispanohablante.

CONTACTO

18 de Septiembre 455, Casilla 52

Chillán, Región de Ñuble, Chile

Solo correo postal

© 2026 Posteamelo.com. No afiliado con Taringa! ni sus sucesores.

Contenido preservado con fines históricos y culturales.