Crear Componentes Delphi
Crear Componentes Delphi
Curso de creacin de
componentes en Delphi
Objetivo del curso.
Este curso pretende ofrecer un acercamiento al diseo de componentes desde un punto
de vista eminentemente prctico. En multitud de libros de programacin en Delphi se
trata el tema del diseo de componentes, pero solamente de una forma rpida y
superficial. Cierto es que uno de los manuales de Delphi est dedicado integramente a
este tema, pero, a mi modo de ver, adolece de falta de ejemplos prcticos que vayan
enseando paso a paso y de un modo gradual el desarrollo de componentes. Adems
determinados temas, (p.e el tema de editores de propiedades) se dejan muy en el aire.
Este curso intentar cubrir estas carencias. Este curso pretende ser sobre todo prctico:
la teora de la creacin de componentes se ir viendo segn se vaya necesitando. Se
comenzar viendo la teora bsica de creacin de componentes e inmediatamente se
aplicar a la creacin de componentes totalmente funcionales.
A lo largo del curso aprenderemos a crear componentes de muy diversos tipos.
Comenzaremos con componentes simples visuales y no visuales e iremos progresando
creando componentes grficos, editores de propiedades, componentes de base de datos...
Adems se explicar como crear los archivos de ayuda para hacer que nuestro
componente se integre plenamente en el entorno de desarrollo de Delphi.
Conocimientos previos.
Pero este no es un curso de programacin en Delphi, ni un curso de programacin
orientada a objetos. Se supone que el lector tiene ya un bagaje ms o menos amplio de
estos temas y conceptos tales como herencia, descendencia, etc. no le resultan
desconocidos.
Como requisito previo a la creacin de componentes resulta altamente recomendable
tener claros los siguientes conceptos:
Si necesitas alguna aclaracin sobre estos temas, puedes encontrar toda la informacin
necesaria en los propios manuales de Delphi, as como en la ayuda en lnea.
Propiedades
Las propiedades proporcionan al usuario del componente un fcil acceso al
mismo. Al mismo tiempo, permite al programador del componente "esconder" la
estructura de datos subyacente. Entre las ventajas de utilizar propiedades para
acceder al componente se pueden citar:
o
Eventos
Los eventos son las conexiones existentes entre un determinado suceso y el
cdigo escrito por el programador de componentes. Asi por ejemplo, ante el
suceso clic del ratn se podra mostrar un mensaje en pantalla. Al cdigo que se
ejecuta cuando se produce un determinado evento se le denomina manejador de
eventos (event handler) y normalmente es el propio usuario del componente
quin lo escribe. Los eventos m comnes ya forman parte de los propios
componentes de Delphi (acciones del ratn, pulsaciones de teclado...), pero es
tambin posible definir nuevos eventos.
Mtodos
Los mtodos son los procedimientos y/o funciones que forman parte del
componente. El usuario del componente los utiliza para realizar una determinada
accin o para obtener un valor determinado al que no se puede acceder por
medio de una propiedad. Ya que requieren ejecucin de cdigo, los mtodos
slo estn disponibles en tiempo de ejecucin.
Bien, ya sabemos como determinar el punto de partida. Veamos ahora como crear la
unidad que albergar el componente. Hay dos opciones, crear la unidad manualmente o
dejar que Delphi haga el trabajo "sucio" utilizando el experto de componentes. Si
optamos por la primera solucin, basta con hacer clic en new unit y ponernos manos a la
obra, pero de este modo tendremos que hacer todo a mano: derivar el nuevo
componente, registrarlo, etc. Por ello es ms recomendable la segunda opcin: utilizar el
experto de componentes.
Una vez introducidos estos campos, al pulsar sobre OK se nos desplegar el cdigo de
nuestra unidad. Si por ejemplo hemos introducido los siguientes datos en el experto de
componentes:
Class Name: TMiComponente
Ancestor Type: TComponent
Palette Page: Curso
Al hacer clic en aceptar, Delphi nos generara la siguiente unidad, lista para introducir
las propiedades y mtodos de nuestro componente:
unit Unit1;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs;
type
TMiComponente = class(TComponent)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Curso', [TMiComponente]);
end;
end.
Los tipos que definamos comenzarn con la letra T (de tipo). P.e. TNif
Los campos que almacenan los valores de propiedades comienzan con la letra F
seguida del nombre de la propiedad que almacenan. As el campo FDNI queda
claro que almacena el valor de la propiedad DNI.
Los nombres de los mtodos de lectura y escritura de los valores de una
propiedad se denominarn mediente el prefijo Get (para lectura) o Set (para
escritura) seguidos del nombre de la propiedad. P.e. el mtodo GetNIF.
interface
uses
Classes;
type
TNif = class(TComponent)
{Nuestro propiedad deriva de TComponent}
private
FDNI : LongInt;
{Almacenar el nmero de DNI}
function GetNIF : char;
{Calcula la letra del NIF}
protected
public
property NIF: char read GetNIF;
{Propiedad NIF: slo lectura}
published
property DNI: LongInt read FDNI write FDNI; {DNI: lectura y
escritura}
end;
procedure Register;
implementation
function TNIF.GetNIF : char;
{Calcula el NIF a partir del DNI}
Var aux1 : integer;
Const letras : string = 'TRWAGMYFPDXBNJZSQVHLCKE';
begin
aux1:=FDNI - ((FDNI div 23) * 23);
result:=letras[aux1+1];
Como ltimo detalle, si quieres utilizar este mismo bitmap, puedes utilizar un programa
de tratamiento de imgenes para cortarlo y pegarlo en el editor de imgenes de Delphi.
Nota: Es una buena idea antes de comenzar hacer una copia de seguridad del archivo
COMPLIB.DCL para evitar posibles problemas derivados de fallos al compilar, cadas
de tensin, etc.
Constructores y destructores.
Constructores
Los objetos que declaramos en un componente no existen en memoria hasta que el
objeto es creado (tambin se dice que se crea una instancia del objeto) por una llamada
al mtodo constructor del objeto.
Un constructor no es ms que un mtodo que proporciona memoria para el objeto y
apunta (mediante un puntero) hacia l. Llamando al mtodo create, el constructor
asigna la instancia del objeto a una variable.
public
constructor create(AOwner : TComponent);
override;
destructor
destroy; override;
constructor TComponent.Create(AOwner :
TComponent);
begin
inherited Create(AOwner);
...
end;
destructor TComponent.Destroy;
begin
...
inherited destroy;
end;
Implemementando el parpadeo.
En nuestro proceso de diseo del componente nos queda an un paso importante: cmo
hacer que el mensaje parpadee. Hemos dicho que cuando se produzca el evento
OnTimer se ejecutar el mtodo parpadea que se encargar de visualizar y ocultar el
mensaje, es decir, de producir el parpadeo.
La solucin es muy simple, ya que nuestro componente, al descender de TLabel
incorpor una propiedad que determina si el componente debe estar visible o no. Esta
propiedad se denomina visible.
De este modo, nuestro mtodo parpadea lo nico que har ser alternar el valor de la
propiedad booleana visible segn se produzca el evento OnTimer. Los detalles
concretos de implementacin se muestran en el cdigo fuente.
interface
uses
Classes, StdCtrls, ExtCtrls;
type
TBlinkLabel = class(TLabel)
{TBlinkLabel deriva de TLabel}
private
FVelocidad : integer;
{Frecuencia de parpadeo}
FTimer : TTimer;
{Timer para la frecuencia}
procedure SetVelocidad(valor : integer); {Almacena la velocidad}
protected
procedure parpadea(Sender : TObject);
public
constructor Create(AOwner : TComponent); override;
{Constructor}
destructor Destroy; override;
{Destructor}
published
property Velocidad : integer read FVelocidad write SetVelocidad
default 400;
end;
procedure Register;
implementation
constructor TBlinkLabel.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
{Llama al constructor original
(heredado)}
FTimer := TTimer.Create(Self); {Creamos el timer}
FVelocidad := 400;
{Frecuencia (velocidad) por defecto}
FTimer.Enabled:=True;
{Activamos el timer}
FTimer.OnTimer:=parpadea;
{Asiganamos el mtodo parpadea}
FTimer.Interval:=FVelocidad;
{Asignamos el intervalo del timer =
frecuencia parpadeo}
end;
destructor TBlinkLabel.Destroy;
begin
FTimer.Free;
{Liberamos el timer}
inherited destroy;
{Llamamos al destructor original (heredado)}
end;
procedure TBlinkLabel.SetVelocidad (valor : integer);
begin
If FVelocidad <> valor then
{Slo si el valor introducido es
distinto del almacenado}
begin
if valor < 0 then FVelocidad:=0;
FVelocidad:=Valor;
{Asigna la velocidad}
Operacin
Herramientas
Alto
Ahora no nos vamos a centrar en explicar todos y cada uno de las herramientas
disponibles para la creacin de grficos, sino que lo haremos segn vayamos
Como es fcil suponer, la funcin RGB toma tres valores de rojo, verde y azul, y
forma el color correspondiente a dichos colores bsicos.
Es importante hacer notar dos aspectos: el primero es que una redeclaracin slo puede
hacer menos restrictivo el acceso a una propiedad (p.e paso de protected a public), pero
no ms restrictivo (p.e paso de public a protected).
El segundo aspecto es que al redeclarar, no es necesario especificar el tipo de la
propiedad, basta con indicar su nombre. Lo que si podemos hacer en el momento de la
redeclaracin es definir un nuevo valor por defecto para dicha propiedad.
El resto de los detalles del cdigo fuente no creo que merezca la pena comentarlos, ya
que la teora y prctica de los conceptos mostrados ya se ha visto en anteriores unidades.
Se declaran las propiedades Direccion, ColorDesde y ColorHasta y se escriben los
mtodos necesarios para almacenar los valores correspondientes... De todas formas, si
alguien tiene dudas, ya sabe, me escribe donde siempre.
Como se ve, cada una de las tres implementaciones tiene sus ventajas e inconvenientes.
En nuestro componente vamos a implementar una combinacin del primer y tercer
mtodo. De este modo, definiremos una propiedad Alignment que especificar la
alineacin por defecto de las celdas del grid y al mismo tiempo, mediante un nuevo
evento, se podr determinar la alineacin de celdas a nivel individual.
La implementacin de la alineacin horizontal a nivel global no tiene ningn misterio:
basta definir la propiedad Alignment de tipo TAlignment (tipo ya incluido en Delphi).
El campo que guardar el valor de esta propiedad se denominar FAlignment. Para
escribir el valor de la propiedad utilizaremos el mtodo SetAlignment, mientras que la
Nos queda ver como implementamos la interfaz de la alineacin de las celdas a nivel
individual. Para ello vamos a crearnos un nuevo evento, que se disparar cada vez que
necesitemos saber el estado de alineacin de una celda en concreto.
Cmo ya se vi en la unidad 2, un evento (tambin denominado suceso) es un
mecanismo que vncula una accin a cierto cdigo. Ms concretamente, un evento es un
puntero a mtodo, un puntero que apunta a un mtodo especfico de una instancia de
objeto especfica toma ya que impresionante!;)
La forma de implementar un evento se hace mediante propiedades, es decir, los eventos
son propiedades. A diferencia de las propiedades estndard, los eventos no utilizan
mtodos para implementar las partes read y write. En su lugar, las propiedades de
eventos utilizan un campo privado del mismo tipo que la propiedad.
Pero basta de teora y manos a la obra. Como ya se ha mencionado, vamos a crear un
nuevo evento que se debe disparar cada vez que necesitemos obtener el valor de la
alineacin de una celda especfica. Lo primero que debemos hacer, por tanto, es definir
nuestro tipo de suceso. Esto lo hacemos mediante la siguiente sentencia:
TMultiGridAlignmentEvent=procedure(Sender:TObject;
ARow,ACol:LongInt;
var HorAlignment: TAlignment; var VerAlignment:
TVerticalAlignment) of object;
Convie ne hacer notar las palabras of object al final de la declaracin del tipo de evento.
Ya hemos definido el tipo de evento. Ahora debemos crear un campo que guarde el
estado de la propiedad OnGetCellAlignment. Esto lo realizamos en la parte private:
private
...
FOnGetCellAlignment : TMultiGridAlignmentEvent;
Slo nos queda disparar el vento cuando lo necesitemos. Aunque un poco ms adelante
veremos con ms detalle cuando queremos hacerlo en nuestro componente, el fragmento
que sigue nos permite ver el mecanismo general:
if Assigned(FOnGetCellAlignment) then
FOnGetCellAlignment(Self,ARow,ACol,HorAlineacion,VerAlineacion);
Importante: Antes de activar un evento, conviene mirar primero si dicho evento tiene
un manejador de evento asignado, ya que el usuario del componente no tiene porque
haber escrito dicho manejador. De ah la comparacin if Assigned: si hay un manejedor
de evento, se le llama, pero si no lo hay no se hace nada.
Celdas Multilnea.
Pasemos ahora a la implemantacin de celdas multilnea. Esta es una caracterstica que
se echa mucho de menos en el StringGrid estndard y que, como vamos a ver, no nos va
a costar casi nada implementar.
En primer lugar definiremos la interfaz. Para ello, vamos a crear una nueva propiedad
denominada MultiLinea (original, verdad? ;) ). Dicha propiedad se almacenar en el
campo FMultiLinea que ser de tipo boolean. Si FMultiLinea est a false, nuestro
componente se comportar como el StringGrid normal, mientras que si est a true se
permitirn las celdas multilnea.
private
FMultiLinea : Boolean;
...
Ya sabemos donde. Ahora falta por ver cmo. En principio podra asustarnos todo lo
que tenemos que hacer: calcular la alineacin horizontal y vertical, activar los eventos
de alineacin y color, fragmentar el contenido de la celda en varias lneas... Pero no hay
motivo: Delphi y el Api de windows vienen en nuestra ayuda y toda esta codificacin se
reduce a 20 o 30 lneas fcilmente entendibles. A continuacin se muestra el cdigo
correspondiente al mtodo DrawCell, el cal paso a comentar:
procedure TMultiGrid.DrawCell(ACol,ARow : LongInt; ARect : TRect;
AState : TGridDrawState);
Const
TextAlignments : Array[TAlignment] of Word = (dt_Left, dt_Right,
dt_Center);
Var
HorAlineacion : TAlignment;
VerAlineacion : TVerticalAlignment;
Texto : string;
Altura : integer;
CRect : TRect;
opciones : integer;
begin
Texto:=Cells[ARow,Acol];
HorAlineacion:=FAlignment;
VerAlineacion:=FVerticalAlignment;
if Assigned(FOnGetCellAlignment) then
FOnGetCellAlignment(Self,ARow,ACol,HorAlineacion,VerAlineacion);
if Assigned(FOnGetCellColor) then
FOnGetCellColor(Self,ARow,ACol,AState,Canvas.Brush,Canvas.Font);
Hay ms opciones para DrawText, de modo que si quieres ms informacin slo tienes
que mirar en la ayuda en lnea.
En la prctica necesitamos hacer dos llamadas a DrawText, una primera en la que al
incluir la opcin dt_CalcRect no se dibuja nada en pantallas, sino que slo se calcula la
altura requerida del rectngulo para el troceo de palabras (multilnea). Esta altura la
guardamos en la variable Altura. Es importante hacer notar que el parmetro Rect
pasado cmo parmetro se modifica, por lo que necesitaremos previamente guardar su
estado. Posteriormente, una vez reajustado el rectngulo se hace una segunda llamada
sin la opcin dt_Calcrect que es la que de verdad dibuja el texto en el canvas.
Volvemos ahora al flujo del programa. Despues de rellenar el fondo de la celda con
FillRect, copiamos en la variable CRect el rectngulo original (ARect) y se preparan las
opciones con que vamos a llamar a DrawText. Aqu surge un pequeo problema, ya que
HorAlineacin es del tipo TAlignment (ta_LeftJustify...) y DrawText no entiende este
tipo, por lo que es necesario una conversin entre dicho tipo y el que DrawText
entiende. Esta conversin la llevamos a cabo mediente una matriz constante
denominada TextAlignments. A continuacin, si la propiedad Multilinea est a true, se
aade dt_WordBreak a las opciones de DrawText.
Lo que se hace a continuacin es verificar si el usuario ha tocado el valor de la
propiedad DefaultDrawing. Si el valor de esta propiedad es false indica que es el
usuario el que se encarga de todo el proceso, en caso contrario, el componente se
encarga del dibujo.
Si es el componente el que se encarga de todo (para eso lo queremos, no?) hacemos la
primera llamada a DrawText para obtener la altura requerida del rectngulo de celda.
Con esta altura y, siempre en cuando sea posible (todo el texto multilnea quepa en la
celda), se pasa a centrar el texto en la celda (o a poner a top o bottom segn
corresponda). Una vez hecho esto, volvemos a llamar a DrawText para que se produzca,
ahora si, el dibujado del texto en pantalla. Ojo a que en esta ocacin pasamos ARect
como rectngulo. Esto es lgico, ya que nos nos podemos salir del rectngulo que tiene
la celda. Y voil!!! hemos terminado.
Este es a grandes rasgos el funcionamiento del mtodo DrawCell. Conviene que mires y
remires el mismo hasta que lo llegues a entender ya que es extremadamente potente y
puede servirte para otras ocasiones. Y si tienes dudas, ya sabes, me escribes ;)
Otros detalles.
Por ltimo quiero mencionar algunos pequeos detalles:
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Grids;
type
TVerticalAlignment = (vaTop, vaCenter, vaBottom);
TMultiGridAlignmentEvent=procedure(Sender:TObject;ARow,ACol:LongInt;va
r HorAlignment:TAlignment;var VerAlignment:TVerticalAlignment) of
object;
TMultiGridColorEvent=procedure(Sender:TObject;ARow,Acol:LongInt;AState
:TGridDrawState;ABrush:TBrush;AFont:TFont) of object;
TMultiGrid = class(TStringGrid)
private
FAlignment : TAlignment;
FVerticalAlignment : TVerticalAlignment;
FMultiLinea : Boolean;
FOnGetCellAlignment : TMultiGridAlignmentEvent;
FOnGetCellColor : TMultiGridColorEvent;
procedure SetAlignment(Valor : TAlignment);
procedure SetVerticalAlignment(Valor : TVerticalAlignment);
procedure SetMultiLinea(Valor : Boolean);
protected
procedure DrawCell(ACol,ARow : LongInt; ARect : TRect; AState :
TGridDrawState); override;
public
constructor Create(AOwner : TComponent); override;
published
property Alignment : TAlignment read FAlignment write SetAlignment
default taLeftJustify;
property VerticalAlignment : TVerticalAlignment read
FVerticalAlignment write SetVerticalAlignment default vaCenter;
property MultiLinea : Boolean read FMultiLinea write SetMultiLinea
default False;
property OnGetCellAlignment : TMultiGridAlignmentEvent read
FOnGetCellAlignment write FOnGetCellAlignment;
property OnGetCellColor : TMultiGridColorEvent read
FOnGetCellColor write FOnGetCellColor;
property Options default
[goFixedVertLine,goFixedHorzLine,goVertLine,goHorzLine,goRangeSelect,g
oRowSizing,goColSizing];
end;
procedure Register;
Ejemplo de utilizacin.
A continuacin se muestra un ejemplo de utilizacin de nuestro nuevo componente. Por
brevedad, slo se muestra el cdigo correspondiente a los eventos FormCreate y
GetCellCOlor y GetCellAlignment. Por supuesto, este no es un jemplo real, ya que por
simplicidad, el contenido de las celdas se determina en el FormCreate cuando lo normal
es que dicho contenido venga de otra parte (de clculos, bases de datos, etc.). Pero como
muestra de uso es suficiente.
procedure TForm1.FormCreate(Sender: TObject);
begin
MultiGrid1.Cells[0,1]:='Enero';
MultiGrid1.Cells[0,2]:='Febrero';
MultiGrid1.Cells[0,3]:='Total Ao';
MultiGrid1.Cells[0,4]:='Notas';
MultiGrid1.Cells[1,0]:='Zona A';
MultiGrid1.Cells[2,0]:='Zona B';
MultiGrid1.Cells[3,0]:='Resto de Zonas';
MultiGrid1.Cells[4,0]:='TOTAL';
MultiGrid1.Cells[1,1]:='1.000.000';
MultiGrid1.Cells[1,2]:='1.250.000';
MultiGrid1.Cells[1,3]:='9.150.000';
MultiGrid1.Cells[1,4]:='Incremento sobre ao anterior';
MultiGrid1.Cells[2,1]:='1.450.000';
MultiGrid1.Cells[2,2]:=' 950.000';
Mediante dos combo box el usuario podr elegir la tabla a mostrar mediante la
seleccin de un alias y de una tabla de dicho alias.
Lo primero que conviene hacer notar es la seccin pblica. En ella definimos tres
variables de tipo booleano: FReadOnly, FFiltrar y FBuscar. Estas tres variables forman
el interface entre el form y el componente. As, a la propiedad ReadOnly que
implementaremos en el componente le corresponde la variable FReadOnly del form y,
de forma anloga ocurre con las propiedades PermitirBucar (AllowSearch) y
PermitirFiltrar (AllowF ilter) del componente. Ser el componente, y no el form quin
en su mtodo execute asignar los valores correspondientes a dichas variables.
Conviene que para seguir la explicacin que viene ahora tengas a la vista el cdigo
fuente completo del form ya que voy a hacer referencias contnuas al mismo.
El mtodo Inicializar es el que se encarga de ajustar la visualizacin del form segn las
variables FReadOnly, FFiltrar y FBuscar. Este mtodo ser llamado por el propio
componente una vez asignado el valor a dichas variables y su cometido es el siguiente:
La propiedad ReadOnly determinar si el grid del form ser editable o no. Por
defecto toma el valor False, es decir, editable. Sus mtodos read y write se
limitan a leer y escribir sobre el campo FReadOnly del componente (no
confundir con el FReadOnly del form visto en la seccin anterior).
La propiedad AllowSearch determina si debe aparecer el panel de bsqueda en
el form. Por defecto su valor es True. Nuevamente sus mtodos read y write
actan directamente sobre el campo FAllowSearch.
La propiedad AllowFilter determina si se permite o no el filtrado de registros
en el form. Por defecto, su valor es True. De sus mtodos read y write que os
voy a decir que no os imagineis! ;)
El constructor se limita a llamar al constructor heredado y asignar los valores
por defecto para las propiedades.
El mtodo Execute se encargar de crear y mostrar el form y lo veremos en la
siguiente seccin.
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs;
type
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,
StdCtrls, DB, DBTables, Grids, DBGrids, ExtCtrls, Buttons, Menus;
type
TfrmVisor = class(TForm)
tTable: TTable;
dsDataSource: TDataSource;
dbGrid: TDBGrid;
pTop: TPanel;
pSeleccionTabla: TPanel;
pBuscar: TPanel;
gbTablas: TGroupBox;
Alias: TLabel;
Tabla: TLabel;
cbAlias: TComboBox;
cbTables: TComboBox;
gbBuscar: TGroupBox;
cbFields: TComboBox;
eSearch: TEdit;
Buscar: TSpeedButton;
BuscarSiguiente: TSpeedButton;
PopupMenu1: TPopupMenu;
Igual1: TMenuItem;
Distinto1: TMenuItem;
Menor1: TMenuItem;
Mayor1: TMenuItem;
N1: TMenuItem;
Activar1: TMenuItem;
Eliminar1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure FillcbFields;
procedure Inicializar;
procedure cbAliasChange(Sender: TObject);
procedure cbTablesChange(Sender: TObject);
procedure BuscarClick(Sender: TObject);
procedure BuscarSiguienteClick(Sender: TObject);
procedure dbGridColEnter(Sender: TObject);
procedure PopupMenu1Popup(Sender: TObject);
procedure Activar1Click(Sender: TObject);
procedure Eliminar1Click(Sender: TObject);
procedure Igual1Click(Sender: TObject);
procedure Distinto1Click(Sender: TObject);
procedure Menor1Click(Sender: TObject);
procedure Mayor1Click(Sender: TObject);
private
public
FReadOnly, FFiltrar, FBuscar : boolean;
end;
procedure PascalStr(var s : string);
Definir cmo debe ser editada la propiedad. Aqu hay dos posibilidades: el valor
de la propiedad puede ser modificado sobre el propio inspector de objetos (bien
sea introduciendo un nuevo valor o seleccionandolo de una lista de valores), o se
puede utilizar un cuadro de dilogo para dotar de mayor flexibilidad a la edicin
(p.e. la propiedad color)
TPropertyEditor
TIntegerProperty
TCharProperty
TEnumProperty
TSetProperty
Char
Tipos enumerados
Sets
TFloatProperty
TStringProperty
TClassProperty
TMethodProperty
TComponentProperty
Cualquier objeto
Cualquier mtodo (eventos)
Para propiedades que hacen referencia a componentes
Necesitamos obtener el valor que tiene en ese momento la propiedad para trabajar sobre
l. Para ello utilizamos el mtodo GetOrdValue , definido de nuevo en
TPropertyEditor, el cul se encarga de devolver el valor de la propiedad en forma de
ordinal (integer). De forma anloga, existen los mtodos GetFloatValue,
GetMethodValue, GetVarValue, etc. para utilizar con el tipo de propiedad
correspondiente.
Una vez almacenado el valor de la propiedad en la variable Num, comienza la
conversin del valor de decimal a binario, la cal es fcil de entender. Cabe hacer notar
que al mximo nmero de digitos binarios soportados es 16, margen ms que suficiente
para la mayora de aplicaciones.
Por ltimo tenemos que devolver un valor de tipo string como resultado de la funcin.
Para ello vamos almacenando en la variable Result el string a devolver. Para finalizar,
anteponemos la letra 'B' a la cadena para indicar que se trata de base binaria.
Y eso es todo en lo que a esta funcin respecta. De este modo, cuando el inspector de
objetos deba mostrar el valor de la propiedad, llamar a el mtodo GetValue el cul le
devolver el string correspondiente. Pero nos queda la otra mitad: estara bien que
pudieramas introducir el valor de la propiedad tanto en decimal como en binario segn
nos interesase, verdad? pues vamos a ello. ;)
Para conseguir esta funcionalidad debemos implementar (override) el mtodo SetValue ,
definido de nuevo en la clase TPropertyEditor. Cuando el usuario entra un nuevo valor
usando el inspector de objetos, este llama al mtodo SetValue, el cul debe efectuar la
traduccin inversa a la efectuada por el mtodo GetValue. Es decir, debe convertir el
string que contiene el nuevo valor de la propiedad al tipo de datos de dicha propiedad.
En nuestro caso, el string vendr en base decimal o binaria (en este ltimo caso, la
primera letra de la cadena ser una 'B') y convertirlo a base decimal. Para ello
implementaremos el mtodo SetValue de la siguiente forma:
...
type
TBinIntegerProperty = class(TIntegerProperty)
public
function GetValue : string; override;
procedure SetValue(const Value : String); override;
end;
procedure Register;
implementation
...
RegisterPropertyEditor(TypeInfo(integer),TMiComponente,'PropiedadBinaria',T
BinIntegerProperty) registra el editor slo y exclusivamente para la propiedad
'PropiedadBinaria' del componente 'TMiComponente'. Esta es la form ms
restringida de registrar un editor de propiedades.
Por tanto, las dos ltimas opciones son las ms recomendables dependiendo la eleccin
de una u otra del uso que se vaya a dar al editor de propiedades. De todas formas, no
olvideis aadir a la clausula uses las unidades correspondientes segn donde situeis el
editor.
En prximas unidades veremos ejemplos de distintas ubicaciones de los editores de
propiedades que iremos construyendo.
Introduccin
En la anterior unidad aprendimos el funcionamiento bsico de un editor de propiedades
y desarrollamos un ejemplo de un editor de propiedades que trabajaba sobre el inspector
de objetos (BinaryPropEd).
En esta unidad desarrollaremos cuatro! editores de propiedades y un componente que
nos servira para probar los editores.
Un componente de prueba
Antes de comenzar a desarrollar los editores de propiedades vamos a crear un
componente que nos permitir probarlos segn los completemos. Este componente lo he
denominado TPrueba y su cdigo es el siguiente:
unit Unidad9;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,
DsgnIntf, DB, PasswordForm;
type
TPrueba = class(TComponent)
private
Nada del otro mundo. Cuatro propiedades para cada uno de los editores de propiedades
a probar y un constructor de los ms normal para asignar un valor por defecto a la
propiedad fecha.
Lo que si conviene hacer notar es que por simplicidad desarrollaremos el componente y
los cuatro editores de propiedades en una nica unidad (hay una unidad adicional
necesaria para el editor TPasswordProperty). Como digo, lo haremos as por
simplicidad, pero no es lo ms correcto. En general, cada editor de propiedades debe ir
en una unidad independiente del propio componente, cmo ya vimos en la unidad 8.
Descripcin
Especifica que el editor debe mostrar una lista con los valores posibles
para la propiedad. Para rellenar la lista se utiliza el mtodo GetValues
Slo vlida si se selecciona paValueList. Especifica que la lista de
valores se mostrar ordenada.
paMultiSelect
paAutoUpdate
paReadOnly
paRevertable
Ms fcil imposible. Tan slo comentar que nos aseguramos de la liberacin de recursos
(en este caso del cuadro de dilogo) mediente el uso de la construccin try..finally
Ya est! Con slo 10 o 15 lneas de cdigo hemos aliviado el sufrimiento del pobre
usuario que se dejaba los dedos escribiendo nombres de ficheros. Si es que somos unos
santos! ;) Slo nos queda registrarlo con la sentencia:
procedure Register;
begin
...
RegisterPropertyEditor(TypeInfo(string),TPrueba,'Fichero',TFicheroProp
erty);
...
end;
La tarea que nos queda ahora es llenar la lista con los diversos alias disponibles. Para
ello, reimplementaremos (override) el mtodo GetValues. Este mtodo recibe un nico
parmetro: un puntero a mtodo. Realmente este puntero referencia el mtodo interno
Add para el string list interno utilizado para llenar la lista desplegable. Los diversos
elementos se aaden a la lista en el mtodo GetValues invocando el mtodo
referenciado por el puntero al mtodo y convirtiendolo en un valor tipo string. Suena
complicado, verdad? No os preocupeis, que no lo es tanto, tan slo significa que hay
que aadir una sentencia del tipo Proc(valor string) por cada elemento a aadir a la lista.
En nuestro caso, como queremos aadir los nombres de los alias existentes, haremos un
bucle que har una llamada a Proc(nombre de alias) para ir aadiendo todos los valores.
Previamente, habremos obtenido los alias existentes mediante el mtodo GetAliasList
del objeto TSession:
procedure TAliasProperty.GetValues(Proc : TGetStrProc);
Var
AliasList : TStringList;
{lista con los alias existentes}
i : integer;
begin
try
AliasList := TStringList.Create;
{Creamos la lista}
Session.GetAliasNames(AliasList);
{Obtenemos los alias
existentes}
for i:=0 to AliasList.Count - 1 do
{Por cada alias...}
Proc(AliasList[i]);
{...hacemos la llamada al mtodo Proc}
finally
AliasList.Free;
{Liberamos la lista}
end;
Con esto ya hemos construido nuestro nuevo editor de propiedades. Ya slo nos falta
registrarlo:
procedure Register;
begin
...
RegisterPropertyEditor(TypeInfo(String),TPrueba,'Alias',TAliasProperty
);
...
end;
Ya hemos construido el cuadro de dilogo, ahora slo nos resta "engancharlo" al editor
de propiedades. Para ello, efectuamos la llamada al form en el mtodo Edit del editor de
propiedades. Veamos como queda el cdigo del editor:
function TPasswordProperty.GetAttributes : TPropertyAttributes;
begin
Result:=[paDialog];
end;
function TPasswordProperty.GetValue : string;
begin
Result:=Format('(%s)',[GetPropType^.Name]);
end;
procedure TPasswordProperty.Edit;
begin
frmPassword := TfrmPassword.Create(Application);
try
frmPassword.Caption:=GetComponent(0).Owner.Name+'.'+
GetComponent(0).Name+'.'+GetName+' - '+
frmPassword.Caption;
frmPassword.PW1.Text:=GetStrValue;
frmPassword.PW2.Text:=frmPassword.PW1.Text;
if frmPassword.ShowModal = mrOK then
SetStrValue(frmPassword.PW1.Text)
Slo hay una cosa nueva : en el mtodo GetValue no queremos que se nos muestre el
valor de la contrasea, ya que entonces para que nos hemos tomado tantas molestias?
De modo que debemos mostrar otra cosa. Podra ser una cadena de asteriscos, pero un
convenio utilizado en estos casos es mostrar el tipo de la propiedad, en este caso un
string. Dicho esto, slo nos queda registrar nuestro tercer editor de propiedades:
procedure Register;
begin
...
RegisterPropertyEditor(TypeInfo(String),TPrueba,'Password',TPasswordPr
operty);
end;
Nada nuevo aqu. Tan slo nos limitamos a llamar a los mtodos GetFloatValue y
SetFloatValue segn nos interesa
Conclusiones
unit PasswordForm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,
StdCtrls, Buttons;
type
TfrmPassword = class(TForm)
lpwd: TLabel;
lVpwd: TLabel;
PW1: TEdit;
PW2: TEdit;
bOK: TBitBtn;
bCancel: TBitBtn;
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
private
public
end;
var
frmPassword: TfrmPassword;
implementation
{$R *.DFM}
procedure TfrmPassword.FormCloseQuery(Sender: TObject;
var CanClose: Boolean);
begin
if (ModalResult=mrOK) then
if (PW1.Text = '') then
begin
ShowMessage('Debe introducir una contrasea');
CanClose:=False;
end
else if (ModalResult=mrOK) and (PW1.Text <> PW2.Text) then
begin
ShowMessage('Verificacin fallida. Por favor reintente');
CanClose:=False;
end;
end;
end.
Consideraciones generales
Hemos definido las dos matrices constantes px y py con las coordenadas de los vrtices.
Adems, se define la matriz DigitsArray de 10 elementos (0..9) que contiene que
segmentos estn encendidos y apagados para cada dgito. Por ejemplo, el dgito 1 tiene
encendidos los segmentos 2y 3 y apagados el resto. Aqu no se tienen en cuenta los
segmentos extras (. y :).
Por ltimo a los segmentos correspondientes a el punto decimal y a los puntos de la
hora se les asigna un nmero y una constante para luego poder referirse a ellos con
mayor facilidad.
Ahora si, nuestro componente esta libre de parpadeos al 100% (bueno, quizs exagere
un poco y sea realmente al 99%) :) Y si no me creeis, hacer la prueba con el programa
mostrado al final de la unidad Incrdulos! ;)
Bien, una vez conocida la teora, es cuestin de aplicarla. Este es el mtodo Paint de
nuestro componente:
procedure TDigitalDisplay.Paint;
Var
ex, ey : single;
{Escala a la que dibujar el display}
Como se puede ver el mtodo Paint utiliza una serie de rutinas auxiliares para el
dibujado del display. Estos mtodos son:
Creo que no hace falta explicar cmo se procede al dibujado de cada dgito en el canvas,
ya que las funciones utilizadas para ello son de todos bien conocidas (Canvas.Pen,
Canvas.Brush y Canvas.Polygon). En cualquier caso si os surgen dudas no teneis ms
que escribirme :)
Vamos ahora con el ltimo aspecto interesante del componente: hacer que otros
desarrolladores puedan descender sus propios componentes de nuestro Display original
Lo primero que hace el mtodo es llamar al antecesor del mismo mediente la llamada
inherited. A continuacin se calcula si se ha hecho click sobre un dgito. En caso
afirmativo se ejecuta el procedimiento DigitClick pasndole como parmetro el nmero
de dgito sobre el que se ha hecho click. El mtodo DigitClick, por su parte, se encarga
de disparar el evento anteriormente definido:
procedure TDigitalDisplay.DigitClick(Digit : integer);
{Mtodo que dispara el evento OnDigitClick}
begin
if Assigned(FOnDigitClick) then
FOnDigitClick(Self,Digit);
end;
end.
Vamos a ver con ms detenimiento las propiedades y mtodos declarados en esta clase:
procedure Create. Aqu hay poco que decir, ya que este mtodo es un viejo
conocido de todos nosotros. ;-)
procedure Edit. Este mtodo se invoca cuando el usuario hace doble click sobre
el componente y es equivalente a la accin ExecuteVerb(0). Es decir, hacer
Una vez diseado el form, vamos a crear el editor de componentes propiamente dicho.
Implementado el editor
Nuestro editor de componentes deriva directamente de TComponentEditor. La
declaracin del mismo es la siguiente:
TDigitalDisplayEditor = class(TComponentEditor)
function GetVerbCount : integer; override;
function GetVerb(Index : integer) : string; override;
procedure ExecuteVerb(Index : integer); override;
procedure PrepararForm(aForm : TfrmDigitalDisplay);
procedure ActualizarComponente(aForm : TfrmDigitalDisplay);
end;
Nuestro mtodo se limita a comunicar que slo se aade un tem al men contextual. A
continuacin indicamos que el tem a aadir debe tener la leyenda "&Edit"
function TDigitalDisplayEditor.GetVerb(Index : integer) : string;
begin
Result:='E&dit...';
end;
Introduccin
En esta unidad aprendemos a crear componentes enlazados a bases de datos. Los
componentes de base de datos (data-aware components) son simplemente componentes
estndar, cmo los que hemos estado creando a lo largo del curso, a los que se les aade
la caracterstica de poder enlazarse con una base de datos. Este enlace puede ser de slo
lectura (el componente puede reflejar el estado del campo de la base de datos, pero no
actualizarlo), o de edicin (si es posible realizar esta actualizacin).
El componente que desarrollaremos ser un componente de edicin descendiente de
TTrackBar, el componente que incorpora Delphi en la pestaa Win95.
Los que se enlazan a un slo campo de la base de datos, cmo por ejemplo los
controles TDBEdit, TDBLabel, etc. y el componente que desarrollaremos en
esta unidad
Los que se enlazan a mltiples campos de la base de datos, cmo el control
TDBGrid.
Nuestro caso, aunque no lo parezca, es el segundo. Es cierto que con Delphi se entrega
el cdigo fuente de los componentes, pero esto no quiere decir que lo podamos
distribuir libremente. Se puede distribuir libremente el cdigo fuente de los
componentes que nosotros desarrollemos, pero no del componente antecesor del nuestro
si este forma parte de los controles estandar de Delphi. As que vamos a suponer que no
disponemos del cdigo fuente del componente TTrackBar, lo cul har la tarea an ms
interesante.
Antes de proseguir un pequeo inciso: ojo si partimos de un componente que ya tenga la
propiedad ReadOnly. Esto no indica nada. De hecho, tendramos que asegurarnos de
sincronizar la propiedad ReadOnly del control con el estado real del data source. Un
ejemplo de este caso es el control TEdit, que ya tiene la propiedad ReadOnly, la cual no
tiene nada que ver con el estado del data source asociado; de hecho, el componente
TEdit no est asociado a ninguna base de datos.
Una vez hecha est aclaracin, veamos en nuestro caso cmo podemos implementar la
propiedad ReadOnly en nuestro componente. La declaracin es sencilla:
TTrackBar = class(TrackBar)
private
FReadOnly : boolean;
...
published
property ReadOnly : boolean read FReadOnly write FReadOnly default
False;
...
end;
Ahora bien, un control en estado de slo lectura, no acepta las pulsaciones de teclas ni
los clics del ratn. Cmo conseguir este efecto en nuestro componente? Tenemos dos
posibilidades:
Esta es una aproximacin que suele funcionar en la mayora de los casos, pero que en
nuestro caso concreto no lo hace. El problema radica en que, aunque el valor de la
propiedad ReadOnly sea True, el componente sigue admitiendo las entradas procedentes
del teclado y el ratn. Esto es debido a que el cdigo fuente del componente TTrackBar,
Cuando queramos procesar de una forma particular un mensaje en concreto, bastar con
con redefinir el mtodo manejador del mensaje utilizando la directiva message (veremos
un ejemplo de esto un poco ms adelante). Pero si esto no nos basta, y lo que queremos
es que el mensaje no se llegue a distribuir a su manejador, debemos capturar el mensaje
en el mtodo WndProc, ya que este mtodo filtra los mensajes antes de pasarlos a
Dispatch, que a su vez los pasa finalmente al manejador de cada mensaje particular.
Esto es lo que realizamos en nuestro componente:
TDBTrackBar = class(TTrackBar)
...
protected
procedure WndProc(var Message: TMessage); override;
...
implementation
...
procedure TDBTrackBar.WndProc(var Message: TMessage);
begin
if not (csDesigning in ComponentState) then
begin
{Si el control est en read only o no est en estado de edicin,
obviamos las pulsaciones de teclas y los mensajes del ratn}
Implementando el DataLink
Ha llegado el momento de aadir a nuestro componente el enlace con la base de datos.
Para ello tenemos que construir un objeto TDataLink, concretamente un
TFieldDataLink, para conectar el componente a la base de datos. Veamos la declaracin
completa de nuestro componente:
TDBTrackBar = class(TTrackBar)
private
FReadOnly : boolean;
FDataLink : TFieldDataLink;
function GetDataField : string;
procedure SetDataField(const Value : string);
function GetDataSource : TDataSource;
procedure SetDataSource(Value : TDataSource);
procedure UpdateData(Sender: TObject);
procedure DataChange(Sender : TObject);
procedure EditingChange(Sender : TObject);
protected
procedure CMExit(var Message: TWMNoParams); message CM_EXIT;
procedure CNHScroll(var Message: TWMHScroll); message CN_HSCROLL;
procedure CNVScroll(var Message: TWMVScroll); message CN_VSCROLL;
Una vez creado, el usuario querr poderlo utilizar ;-) de modo que vamos a
proporcionarle las propiedades que todo control enlazado a datos tiene: DataSource y
DataField. Comencemos con la propiedad DataSource:
Primero comprobamos que haya un campo asignado en la propiedad Field del DataLink
y, en caso negativo, se devuelve un valor arbitrario como posicin del TrackBar (en este
caso se devuelve 0). En caso afirmativo, basta con leer el valor del campo de la base de
datos mediante FDataLink.Field.AsInteger y asignarlo a la propiedad position del
TrackBar.
As de simple. Pero conviene tener en cuenta dos aspectos:
1. Por conveniencia, leemos el valor del campo como entero, ya que es lo que nos
interesa a nuestros propsitos, ya que nuestro componente se enlazar a campos
numricos de la base de datos. Si el usuario intenta enlazarlo con un campo no
numrico, obtendr la excepcin 'xxx is not a valid numeric value', cosa perfectamente
normal.
2. Una vez ledo el valor del campo, hay que actualizar el componente para que refleje
el nuevo valor. Esta tarea variar mucho en funcin del componente , pero bsicamente
consistir en asignar el valor del campo a una determinada propiedad y llamar al mtodo
Paint para que se muestre el nuevo valor. En nuestro caso, basta con asignar el valor del
Con esto ya casi est. Nos queda un ltimo paso. Una vez que el usuario del
componente hace click con el ratn para modificar el valor de la propiedad position,
informamos al DataLink de que el campo va a cambiar llamando al mtodo Modified.
De este modo el objeto DataLink activar el evento OnUpdateData para asignar el
nuevo valor al campo, y este evento se disparar cuando sea requerido por el data
source. Pero tambien debemos disparar este evento cuando nuestro componente pierda
el foco: as nos aseguramos de que el campo se actualizar correctamente.
Para realizar esta tarea debemos reimplementar el mensaje CMExit, que se genera
cuando un componente pierde el foco:
procedure CMExit(var Message: TWMNoParams); message CM_EXIT;
...
procedure TDBTrackBar.CMExit(var Message: TWMNoParams);
begin
try
FDataLink.UpdateRecord; { Actualizamos el data link}
except
SetFocus; { Si falla la actualizacin, mantenemos el foco en el
control}
raise; { y relanzamos la excepcin}
end;
inherited;
end;
destructor TDBTrackBar.Destroy;
begin
FDataLink.Free;
FDataLink:=nil;
inherited Destroy;
end;
function TDBTrackBar.GetDataField : string;
begin
Result:=FDataLink.FieldName;
end;
procedure TDBTrackBar.SetDataField(const Value : string);
begin
FDataLink.FieldName:=Value;
end;
function TDBTrackBar.GetDataSource : TDataSource;
begin
Result:=FDataLink.DataSource;
end;
procedure TDBTrackBar.SetDataSource(Value : TDataSource);
begin
if FDataLink.DataSource<>Value then
begin
FDataLink.DataSource:=Value;
if Value<>nil then
Value.FreeNotification(Self);
end;
end;
procedure TDBTrackBar.Notification(AComponent : TComponent; Operation
: Toperation);
begin
inherited Notification(AComponent,Operation);
if (Operation=opRemove) AND (FDataLink<>nil) AND
(AComponent=DataSource) then
DataSource:=nil;
end;
procedure TDBTrackBar.DataChange(Sender : TObject);
begin
if FDataLink.Field=nil then
Position:=0
else
Position:=FDataLink.Field.AsInteger;
end;
procedure TDBTrackBar.CNHScroll(var Message: TWMHScroll);
begin
inherited;
FDataLink.Modified