epelpad

El post que buscas se encuentra eliminado, pero este también te puede interesar

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

Creando componentes propios en Delphi (1/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"

Por qué, cuándo y cómo es necesario escribir un componente
Artículo publicado por: Peter Morris http://delphi.about.com/library/bluc/text/uc080701a.htm
"Este artículo apareció originalmente en Delphi Developer. Copyright Pinnacle Publishing, Inc. Todos los derechos reservados."

Esta primera parte muestra algunos de los mejores enfoques para crear componentes, y al mismo tiempo proporciona consejos para decidir cuál es la mejor clase base a usar como ancestro (herencia), el uso de declaraciones virtuales (virtual), las complejidades de la sobre escritura (override), y así sucesivamente. En la segunda parte se verá cómo escribir las propiedades avanzadas, como escribir el stream por defecto para esas propiedades y sub propiedades. La parte final cubrirá los editores de propiedades de los componentes, como escribir editores dedicados para las propiedades de los componentes, y la forma de escribir componentes "ocultos".

Las dos primeras cosas que hay que preguntarse son: por qué debería escribir un componente, y cuándo debería escribirlo.

La primer pregunta es fácil de responder, de hecho, tiene muchas respuestas.
Facilidad de uso: Código encapsulado significa que simplemente se coloca el mismo componente una y otra vez en varios formularios sin escribir una simple línea de código.
Depuración: El código centralizado facilita la reparación de toda la aplicación (o el conjunto de aplicaciones) corrigiendo el error en un simple componente y volviendo a compilarlo.
Costo: Hay un montón de compañías que están mas que felices de pagar por tener el privilegio de no reinventar la rueda.


La segunda pregunta no es muy difícil de responder tampoco. Cuando te encuentras que tienes que escribir el mismo código mas de una vez es una buena idea escribir un componente, especialmente si el código se comporta diferente en base a los parámetros de entrada.

Como son creados los componentes
El primer paso es decidir que clase se necesita para usar como base (ancestro) para el componente. Cuando se deriva (descendente) de otra clase se heredan las propiedades, métodos y eventos que esta clase posee. Debajo hay un ejemplo de cómo decidir qué clase se debería heredar cuando se escribe un componente propio.
Delphi cómo crear un componente (1/3)

En el caso del método A y el B, se presume que el componente en cuestión es TMemo, por poner un ejemplo, en realidad vamos a utilizar el que sea mas afín a nuestra necesidad.

Método / Solución
A / Deriva de Tmemo
B / Deriva de TcustomMemo
C / Deriva de TcustomControl
D / Deriva de TgraphicControl
E / Deriva de Tcomponent

Esto puede parecer un poco complicado, entonces vamos explicar el proceso.

A: Cuando sólo se necesita agregar funciones extras a un componente existente se deriva (descendente) de ese componente. Esto le dará automáticamente al nuevo componente toda la funcionalidad y propiedades del componente existente.

B: A veces no solo es necesario agregar funcionalidad, también quitarla al mismo tiempo. Comúnmente cuando se escribe un componente primero se escribe un componente TCustomXXXXX donde se declaran todas las propiedades, eventos y métodos dentro de la sección protegida (Protected) del componente. El componente real es derivado (descendente) de esta clase base abstracta. Entonces el truco no es ocultar funcionalidad, sino derivar (descender) el nuevo componente de la versión predefinida (custom) o abstracta del componente y solo publicar e implementar las propiedades que se quieren conservar (efectivamente remover las propiedades y eventos indeseados).

C: Cualquier componente que necesita recibir foco necesita un window handle. TwinControl es donde este handle se introduce por primera vez. TCustomControl es simplemente un TWinControl con su propia propiedad Canvas.

D: TGraphicControl no tiene un window handle y por lo tanto no puede recibir foco. Componentes como TLabel son derivados (descendentes) de esta clase.

E: Algunos componentes no son creados para tener interacción a través de la GUI pero hacen la vida más sencilla. Todo lo derivado (descendente) de TComponent es visible solo al momento de diseño. Un uso típico de estos componentes es para la conexión con bases de datos, timers, etc.


Creando el primer componente (Encapsulamiento)

Creando el primer componente
Una vez que hemos decidido que clase se usará de base, el siguiente paso es crear el componente. Desde el menú de componentes (Component), seleccionamos nuevo componente (New VCL Component) y veremos el siguiente cuadro de diálogo.

Ancestor Component: Este es el componente base (clase tipo) que necesitamos para hacer la herencia.
delphi

Class Name: Este es el nombre de la clase del nuevo componente.
Palette Page: Esta es la ficha (tab) de la paleta de componentes en la cual queremos que aparezca el componente, ingresando una ficha inexistente le decimos a Delphi que queremos crear una nueva.
Complemento

Unit: Una unidad es una clase o un conjunto de clases (type), que definen uno o varios componentes (objetos).
Package: Los componentes se suelen distribuir en paquetes, es similar a una librería.
programación

Escribiendo algo de código
Es momento de escribir algo. Este primer ejemplo no tendrá ninguna utilidad excepto demostrar algunos conceptos básicos de cómo escribir componentes.

Primero que nada, seleccionamos componente (Component) en el menú principal de Delphi y luego nuevo componente (New VCL Component). Ingresamos TComponent como el "Ancestor Component" >>.

TFirstComponent como nombre del nuevo componente. Elegimos la paleta (tool palette) donde el colocarlo >>.

A esta altura ya se puede instalar el nuevo componente en un paquete existente (un paquete contiene una colección de componentes) o en un nuevo paquete. Hacer click en "Install to New Package" >>.

componente


Una vez que hemos seleccionado un nombre de archivo proyecto (.dproj) y directorio para el nuevo paquete e ingresasmos la descripción hacer click en "Finish". 

En el siguiente cuadro de diálogo seleccionamos la ubicación y el nombre para el archivo de la unidad (.pas).

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


Luego el paquete se compila e instala automáticamente, una vez que el paquete fue compilado e instalado se registra el nuevo componente.

Hasta ahora hemos especificado la clase base, y también el nombre de nuestra nueva clase. Hemos creado un nuevo paquete que contenga nuestro componente y hemos sido presentados con el esqueleto de la estructura de una clase de Delphi.

Si vemos el código fuente generado por Delphi (Component1.pas) veremos las secciones Private, Protected, Public, y Published.
unit FirstComponent1;

interface

uses
SysUtils, Classes;

type
TFirstComponent = class(TComponent)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents('Samples', [TFirstComponent]);
end;

end.
Encapsulamiento
El encapsulamiento es un concepto simple de entender, pero extremadamente importante al escribir componentes. El encapsulamiento se implementa a través de cuatro palabras reservadas: Private, Protected, Public, y Published, vemos que Delphi agregó automáticamente estas secciones en el código fuente del nuevo componente.

Private: Los Métodos, Propiedades y Eventos declarados dentro de esta sección solo serán accesibles por la unidad que contiene al componente. Los componentes en una misma unidad pueden acceder unos a las elementos privados de los otros.

Protected: Los Métodos, Propiedades y Eventos declarados dentro de esta sección también estarán accesibles a cualquier clase descendiente de esta clase.

Public: Los Métodos, Propiedades y Eventos declarados dentro de esta sección son accesibles desde cualquier lugar.

Published: Esta sección permite declarar propiedades y eventos que aparecerán en el inspector de objetos (object inspector). Estas configuraciones son valores en tiempo de diseño que son almacenados con el proyecto.

Código del componente sin procesar; palabras reservadas Virtual, Dynamic, Abstract y Override

Comenzando a escribir el nuevo componente
Delphi ahora necesita saber todo sobre nuestro nuevo componente. Ingresamos las siguiente líneas en el código fuente del componente. Nótese que debe agregarse la unidad Windows en la cláusula uses.
{uses
SysUtils, Classes, Windows;}

private

{ Private declarations }
FStartTime,
FStopTime: DWord;

protected

{ Protected declarations }
function GetElapsedTime :
String; virtual;
public

{ Public declarations }
procedure Start; virtual;
procedure Stop; virtual;

property StartTime: DWord
read FStartTime;
property StopTime: DWord
read FStopTime;
property ElapsedTime: String
read GetElapsedTime;

published

{ Published declarations }

end;
Lo que hemos hecho es agregar dos variables FStartTime y FStopTime (es común anteponer la letra F al nombre de las variables). Hay dos métodos para controlar estas variables, Start y Stop. Hemos agregado la función GetElapsedTime la cual devuelve FStopTime - FStartTime como una cadena. Finalmente hemos agregado tres propiedades de solo lectura (read-only).

Presionando SHIFT-CTRL-C Delphi completará automáticamente el código para la clase o presionando el botón derecho del ratón y seleccionando "Complete class at cursor".
unit FirstComponent1;

interface

uses
SysUtils, Classes, Windows;

type
TFirstComponent = class(TComponent)
private

{ Private declarations }
FStartTime,
FStopTime: DWord;

protected

{ Protected declarations }
function GetElapsedTime :
String; virtual;
public

{ Public declarations }
procedure Start; virtual;
procedure Stop; virtual;

property StartTime: DWord
read FStartTime;
property StopTime: DWord
read FStopTime;
property ElapsedTime: String
read GetElapsedTime;

published

{ Published declarations }

end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents('Samples', [TFirstComponent]);
end;

{ TFirstComponent }

function TFirstComponent.GetElapsedTime: String;
begin

end;

procedure TFirstComponent.Start;
begin

end;

procedure TFirstComponent.Stop;
begin

end;

end.
Luego ingresamos el código respectivo para cada método.
{agregar la unidad Windows a la cláusula uses para poder utilizar GetTickCount}

{ TFirstComponent }

function TFirstComponent.GetElapsedTime: String;
begin
Result := IntToStr(FStopTime - FStartTime);
end;

procedure TFirstComponent.Start;
begin
FStartTime := GetTickCount;
end;

procedure TFirstComponent.Stop;
begin
FStopTime := GetTickCount;
end;

end.
El código completo queda así.
unit FirstComponent1;

interface

uses
SysUtils, Classes, Windows;

type
TFirstComponent = class(TComponent)
private

{ Private declarations }
FStartTime,
FStopTime: DWord;

protected

{ Protected declarations }
function GetElapsedTime :
String; virtual;
public

{ Public declarations }
procedure Start; virtual;
procedure Stop; virtual;

property StartTime: DWord
read FStartTime;
property StopTime: DWord
read FStopTime;
property ElapsedTime: String
read GetElapsedTime;

published

{ Published declarations }

end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents('Samples', [TFirstComponent]);
end;

{ TFirstComponent }

function TFirstComponent.GetElapsedTime: String;
begin
Result := IntToStr(FStopTime - FStartTime);
end;

procedure TFirstComponent.Start;
begin
FStartTime := GetTickCount;
end;

procedure TFirstComponent.Stop;
begin
FStopTime := GetTickCount;
end;

end.
Prueba piloto
Guardamos (Save) la unidad, y volvemos a abrir el paquete (File, Open Project desde el menú, y seleccionamos "Delphi Package" como tipo de archivo), una vez que el paquete este abierto hacemos click el botón "Compile". También se puede abrir el paquete seleccionando Component desde el menú principal y luego Install Packages. Seleccionamos el paquete y luego click el botón "Edit".

Ahora podemos agregar un TFirstComponent en un formulario (VCL Forms Application), de hecho, podemos colocar tantos como se desee. Agregamos dos botones Start y Stop (btnStart y btnStop) y agregamos el siguiente código en el formulario, y luego se ejecuta la aplicación de prueba.
delphi
procedure TForm1.btnStartClick(Sender: TObject);
begin
FirstComponent1.Start;
end;

procedure TForm1.btnStopClick(Sender: TObject);
begin
FirstComponent1.Stop;
Caption := FirstComponent1.ElapsedTime;
end;
Haciendo click en el botón "Start" se marcará el tiempo de inicio (GetTickCount es un comando de la API de Windows (WinAPI) que devuelve el número de milisegundos desde que Windows inicializó). Haciendo click en el botón "Stop" marcará el tiempo de detención, y cambiará el título del formulario. Hay que agregar la unidad Windows para poder utilizar esta función.

Complemento

Virtual, Dynamic, Abstract y Override
Vemos la declaración Virtual luego de Start, Stop y GetElapsedTime. El siguiente ejercicio explicará sus usos.

Creamos un nuevo componente, derivado (descendente) del componente TFirstComponent (lo llamamos TSecondComponent) y se instala.

Con las etiquetas Virtual y Dynamic los programadores de componentes le dicen a Delphi que el método puede ser reemplazado en un clase descendente. Si nosotros sobre escribimos (Override) un método en una clase, nuestro nuevo código será ejecutado en vez del código original.
Protected
{ Protected declarations }
function GetElapsedTime : String; override;
Luego implementamos el código de arriba como sigue.
function TSecondComponent.GetElapsedTime: String;
var
S : String;
begin
S := inherited GetElapsedTime;
Result := S + ' milliseconds or ' +
Format('%.2f seconds',
[StopTime - StartTime) / 1000]);
end;
Nuestro nuevo código es llamado en reemplazo del código GetElapsedTime original, aun las llamadas en TFirstComponent a GetElapsedtime llamarán ahora a nuestro nuevo código. El código original es invocado a través del uso del comando Inherited.

Nota: Si no sobre escribimos "override" un método base (porque este no fue declarado como Virtual o porque nos olvidamos), TSecondComponent llamará a nuestro nuevo código, pero hay que tener en cuenta que el código introducido en TFirstComponent continuará llamando al método original de TFirstComponent.

El identificador Abstract le dice a Delphi que no espere ningún código de este método. No se debería crear ninguna instancia de un objeto que contenga métodos abstractos (como es el caso de TStrings). Comunmente se crea una clase descendiente por cada clase abstracta y se sobre escriben (override) todos los métodos abstractos (como lo hace TStringList).

La diferencia entre Dynamic y Virtual es simplemente cuestión de velocidad vs tamaño. Un método dinámico (Dynamic) requerirá menos memoria en cada instancia, mientras que un método virtual (Virtual) se ejecutará más rápido con un pequeño costo de memoria.

Agregando eventos

Agregando eventos
Pocos pasos son los que hay que seguir para agregar eventos a un componente. Los eventos le permiten al componente comunicarse con la aplicación, notificarle cuando ha sucedido algo importante. Un evento es meramente la lectura / escritura de una propiedad, en lugar de ser un tipo de variable común (como string, integer, etc) es un procedimiento o una función.

Creamos un nuevo componente, descentende de TSecondComponent y lo llamamos TThirdComponent. Guardamos la unidad, instalamos el componente, y agregamos el siguiente código.
type
TState = (stStarted, stStopped);
TStateChangeEvent = procedure
(Sender : TObject; State : TState) of object;

TThirdComponent = class(TSecondComponent)
private
{ Private declarations }
FState : TState;
FOnStart,
FOnStop : TNotifyEvent;
FOnStateChange : TStateChangeEvent;
protected
{ Protected declarations }
public
{ Public declarations }
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
procedure Start; override;
procedure Stop; override;
property State : TState
read FState;
published
{ Published declarations }
property OnStart : TNotifyEvent
read FOnStart
write FOnStart;
property OnStateChange : TStateChangeEvent
read FOnStateChange
write FOnStateChange;
property OnStop : TNotifyEvent
read FOnStop
write FOnStop;
end;
Los eventos son simples procedimientos o funciones (las menos veces) que pertenecen a una clase (de ahí que vemos la cláusula "of object" en el evento TStateChangeEvent). Por ejemplo, TNotifyEvent es un tipo de evento estándard implementado por Delphi el cual solo pasa el objeto que ha desencadenado el evento, siempre es bueno enviar "Self" (Sender : TObject) como el primer parámetro de cualquier evento ya que el mismo código del evento puede estar siendo usado por múltiples componentes. TNotifyEvent se define como:
Type
TNotifyEvent = procedure (Sender: TObject) of object;
Para llamar un evento de un componente solo hay que verificar si el evento fue asignado y, en este caso, llamarlo. Aquí están sobre escritos los métodos Start y Stop de TSecondComponent para desencadenar (trigger) estos eventos.
procedure TThirdComponent.Start;
begin
inherited; //This calls TSecondComponent.Start
FState := stStarted;
if Assigned(OnStart) then OnStart(Self);
if Assigned(OnStateChange) then
OnStateChange(Self, State);
end;

procedure TThirdComponent.Stop;
begin
inherited; //This calls TSecondComponent.Stop
FState := stStopped;
if Assigned(OnStop) then OnStop(Self);
if Assigned(OnStateChange) then
OnStateChange(Self, State);
end;

constructor TThirdComponent.Create(AOwner: TComponent);
begin
inherited;
//This is were you initialise properties, and create
//and objects your component may use internally
FState := stStopped;
end;

destructor TThirdComponent.Destroy;
begin
//This is where you would destroy
//any created objects
inherited;
end;
Volvemos a compilar el paquete (no olvidar guardar el paquete cada vez que se agrega un componente). Al colocar un nuevo componente en un formulario veremos que hay tres eventos. OnStart, OnStop, y OnStateChange. En el ejemplo Demo3 podemos ver como se usan estos eventos.

OnStart pone "Started" en el título (caption).
OnStop muestra el tiempo transcurrido
OnStateChange Activa / Desactiva el botón correspondiente Start / Stop
procedure TForm1.ThirdComponent1Start(Sender: TObject);
begin
Caption := 'Start';
end;

procedure TForm1.ThirdComponent1Stop(Sender: TObject);
begin
Caption := ThirdComponent1.ElapsedTime;
end;

procedure TForm1.ThirdComponent1StateChange
(Sender: TObject; State: TState);
begin
btnStart.Enabled :=
ThirdComponent1.State = stStopped;
btnStop.Enabled :=
ThirdComponent1.State = stStarted;
end;

Consideraciones a tener en cuenta antes de escribir componentes

Normas al escribir componentes
Finalmente cubriremos algunos detalles sobre la implementación de componentes, incluyendo algunos aspectos sobre componentes base y normas de codificación.

Creando y destruyendo el componente
Los objetos son creados a través de un constructor y destruidos a través de un destructor.
Sobre escribir (override) un constructor tiene un propósito triple:
1. Crear cualquier objeto que él mismo contiene (sub objetos)
2. Inicializar los valores de la clase (propiedades, etc)
3. Levantar una excepción y evitar que se cree la clase

Es norma llamar al constructor heredado (del objeto base) desde dentro del constructor del componente nuevo así la clase padre puede hacer sus propias inicializaciones, aunque no sea necesario para crear el componente. (El nuevo componente es creado tan pronto como el constructor finalizó, no se crea llamando el constructor heredado)

El objetivo de sobre escribir un destructor es simplemente liberar cualquier recurso que fue reservado durante la vida del componente. Se llama al destructor heredado luego de haber liberado estos recursos.

Partes comunes de los componentes
Paint: Se puede sobre escribir este método para que el componente haga su representación gráfica personalizada.
Loaded: Este es llamado en tiempo de ejecución por Delphi, tan pronto como todas las propiedades han terminado de ser definidas cuando el formulario padre es creado. Se puede sobre escribir este método para realizar cualquier acción que dependa de un grupo de propiedades que ya han sido definidas (posición, tamaño, etc).
Invalidate: Siempre que el cambio de una propiedad afecte el aspecto visual de un componente se debe llamar a este método.
ComponentState: Esta propiedad es muy útil cuando se necesita verificar si el componente existe en tiempo de diseño o ejecución, o si sus propiedades están siendo leídas por un proceso en ejecución.

Encapsulando el componente correctamente
Es normal escribir un componente como una clase abstracta como TCustomMyClass y luego derivar (descender) el componente de esa clase base. La clase abstracta tendrá casi todos (sino todos) los métodos y propiedades del componente declarados en su sección Protected. Cuando se implementa la clase abstracta simplemente se vuelve a declarar las propiedades en las secciones Public o Published.
type
TCustomMyClass = class(TComponent)
private
FSomeString : String;
protected
procedure SetSomeString(const Value : String);
virtual;
property SomeString : String
read FSomeString
write SetSomeString;
end;

TMyClass = class(TCustomMyClass)
published
property SomeString;
end;
Lo bueno de hacer esto es que permitimos que si alguien quiere heredar este componente para hacer uno propio todavía puede quitar algunas propiedades.

Vemos como SetSomeString fue declarado como virtual dentro de la sección protected. Esto es bueno porque permite a las clases descendientes responder a los cambios de los atributos y las propiedades, sobre escribiendo los procedimientos que los establecen (leen y escriben). También se aplica a los eventos, donde hay un evento OnStateChange encontraremos a menudo un método DoStateChange, como en el siguiente ejemplo:
type
TCustomMyClass = class(TComponent)
private
FOnStateChange : TStateChangeEvent;
protected
procedure DoStateChange(State : TState); virtual;
published
property OnStateChange : TStateChangeEvent
read FOnStateChange
write FOnStateChange;
end;

procedure TCustomMyClass.DoStateChange(State : TState);
begin
if Assigned(OnStateChange) then
OnStateChange(Self, State);
end;
En vez de escribir la condición "If assigned(OnStateChange) then" cada vez que cambie de estado, simplemente llamamos "DoStateChange(NewState)". Aparte de ser una sentencia más corta, permite que las clases descendientes sobre escriban DoStateChange y determinen el código necesario que responda al evento.

Resumen

Hasta ahora hemos visto las normas para implementar componentes. También vimos como escribir nuestros componentes, y determinar la clase base a usar en la herencia de los mismos. Además discutimos los métodos Virtual y Dynamic, y como usarlos para “etiquetar” los componentes.

Fin de la parte 1/3

En el próximo post veremos como escribir propiedades personalizadas: datos binarios, colecciones, conjuntos y subpropiedades expandibles.

Nos vemos...

3 comentarios - Delphi cómo crear un componente (1/3)

Carlitoscz +1
Que post tan bueno, te ganastes mis 10 de hoy
heoss
tremendo! +10