Documentos de Académico
Documentos de Profesional
Documentos de Cultura
La Bibilia de Delphi 7
La Bibilia de Delphi 7
La Bibilia de Delphi 7
. .
Agradecimientos .......................................................................................................................
Contactar con el autor ..............................................................................................................
6
7
29
31
32
......................................................................................................................... 33
1. Delphi 7 y su IDE .............................................................................................................. 35
.
Parte I Bases
..........................................................................89
.. .........................................................................
135
.............................................................................169
Posesion ............................................................................................................................
La matriz Components .........................................................................................
Cambio de propietario ..............................................................................................
La propiedad Name .........................................................................................................
.
Elimination de campos del formulario .........................................................................
Ocultar campos del formulario ......................................................................................
La propiedad personalizada Tag ....................................................................................
Eventos ...................................................................................................... :............................
Eventos en Delphi ...........................................................................................................
Punteros a metodo .......................................................................................................
Los eventos son propiedades .........................................................................................
Listas y clases contenedores ...............................................................................................
Listas y listas de cadena ...............................................................................................
Pares nombre-valor (y extensiones de Delphi 7) ...................................................
Usar listas de objetos .................................................................................................
Colecciones ......................................................................................................................
Clases de contenedores .............................................................................................
. .
.
Listas asociativas de verification ............................................................................
Contenedores y listas con seguridad de tipos ..........................................................
Streaming ...............................................................................................................................
La clase TStream .............................................................................................................
Clases especificas de streams .........................................................................................
Uso de streams de archivo ..............................................................................................
Las clases TReader y TWriter ........................................................................................
Streams y permanencia ...................................................................................................
Compresion de streams con ZLib ..................................................................................
Resumen sobre las unidades principales de la VCL y la unidad BaseCLX ...................
La unidad Classes ...........................................................................................................
Novedades en la unidad Classes ..............................................................................
. .
Otras unidades prlncipales .............................................................................................
180
181
182
184
185
186
188
188
188
189
190
193
193
194
195
196
196
198
199
202
202
204
205
206
207
213
215
215
216
217
...........................................................................................................
219
220
222
223
224
226
226
226
227
228
229
230
231
232
232
233
5 Controles visuales
..
..............................................................................283
................................................................................................345
...............................................401
8. La arquitectura de las aplicaciones Delphi ...............................................................403
.
428
429
430
432
433
436
439
442
444
446
447
450
451
452
.................................................................................
455
..
10 Bibliotecas y paquetes
................................................................................................. 527
504
508
508
512
516
517
520
521
522
522
524
528
528
529
530
531
532
534
535
537
537
539
540
540
542
544
546
548
550
551
553
555
558
561
...................567
568
569
569
571
572
574
575
576
576
12 De COM a COM+
578
580
582
582
584
585
585
587
587
590
593
595
..................................................................................................... 597
598
599
601
603
604
605
608
609
610
611
612
614
617
618
619
621
622
624
626
627
627
628
629
630
633
633
635
636
636
638
639
640
642
ActiveForms .....................................................................................................................
Interioridades de ActiveForm ...................................................................................
El control ActiveX XClock ......................................................................................
ActiveX en paginas Web ................................................................................................
COM+ ....................................................................................................................................
Creacion de un componente COM+ ..............................................................................
Modulos de datos transaccionales .................................................................................
Eventos COM+ ................................................................................................................
COM y .NET en Delphi 7 ....................................................................................................
644
644
645
646
648
649
651
653
656
................................ 659
13. Arquitectura de bases de datos Delphi .....................................................................661
.
Acceso a bases de datos: dbExpress. datos locales y otras alternativas .......................... 662
La biblioteca dbExpress .................................................................................................. 662
Borland Database Engine (BDE) .................................................................................. 664
InterBase Express (IBX) ................................................................................................ 664
MyBase y el componente ClientDataSet ....................................................................... 665
dbGo para ADO ............................................................................................................... 665
MyBase: ClientDataSet independiente ............................................................................... 666
Conexion a una tabla local ya existente ....................................................................... 667
De la DLL Midas a la unidad MidasLib ....................................................................... 669
Formatos XML y CDS .................................................................................................... 669
Definition de una tabla local nueva .............................................................................. 670
Indexado ........................................................................................................................... 671
Filtrado ............................................................................................................................. 672
Busqueda de registros ..................................................................................................... 673
Deshacer y Savepoint ................................................................................................ 674
Activar y desactivar el registro ................................................................................ 675
Uso de controles data-aware ................................................................................................ 675
Datos en una cuadricula ................................................................................................. 676
DBNavigator y acciones sobre el conjunto de datos ................................................... 676
Controles data-aware de texto ....................................................................................... 677
Controles data-aware de lista ........................................................................................ 677
El ejemplo DbAware ................................................................................................. 678
Uso de controles de busqueda ........................................................................................ 679
Controles grAficos data-aware ....................................................................................... 681
El componente DataSet ........................................................................................................ 681
El estado de un Dataset .................................................................................................. 686
Los campos de un conjunto de datos .................................................................................. 687
Uso de objetos de campo ................................................................................................ 690
Una jerarquia de clases de campo ................................................................................. 692
.,
Adicion de un campo calculado ..................................................................................... 695
Campos de busqueda ....................................................................................................... 699
Control de 10s valores nulos con eventos de campo .................................................... 701
Navegacion por un conjunto de datos ................................................................................. 702
El total de una columna de tabla ...................................................................................703
Uso de marcadores .......................................................................................................... 704
............................................................................
727
.......................................................................................................... 803
.............................................................................
832
834
836
839
840
841
843
844
845
847
...........................................................
877
..
893
897
898
902
907
911
915
917
918
919
920
924
.............................................................................
931
932
933
934
934
934
935
936
936
938
939
941
942
942
942
943
943
944
944
944
945
946
947
949
949
950
951
951
951
952
953
954
955
955
..............................................................................................959
19. Programacidn para Internet: sockets e Indy .........................................................961
....................................................997
1030
1031
1031
1034
1035
1036
1036
1036
1039
1041
1043
1043
1045
1047
...........................................................................1049
............................................................................................ 1079
1080
1080
1082
1083
1084
1085
1087
1090
1094
1098
1099
1103
1108
1109
1110
1111
1116
...............................................................................................
1117
1118
1119
1121
1123
1123
1125
1129
1130
1130
1131
1134
1135
1136
1137
1139
1139
1140
1142
1143
1144
1145
1145
1148
1148
1149
1151
1151
1153
............................................................................................................
ApCndice A. Herramientas Delphi del autor ...............................................................
1157
1159
1162
1162
1163
1164
...........................................................................
1165
Parte V ApCndices
1159
lntroduccion
La primera vez que Zack Urlocker me enseiio un product0 aun sin publicar
denominado Delphi, me di cuenta de que cambiaria mi trabajo (y el trabajo de
muchos otros desarrolladores de software). Solia pelearme con bibliotecas de
C++ para Windows y, Delphi era, y todavia es, la mejor combinacion de programacion orientada a objetos y programacion visual no solo para este sistema operativo sino tambien para Linux y pronto para .NET.
Delphi 7 simplemente se suma a esta tradicion, sobre las solidas bases de la
VCL, para proporcionar otra impresionante herramienta de desarrollo de software que lo coordina todo. iEsta buscando soluciones de bases de datos, clientel
servidor, multicapa (multitier), Intranet o Internet? iBusca control y potencia?
~ B U Suna
C ~rapida productividad? Con Delphi y la multitud de tecnicas y trucos
que se presentan en este libro, sera capaz de conseguir todo eso.
mas importante era el lenguaje Pascal orientado a objetos, que es la base de todo
lo demas.
iDelphi 2 era incluso mejor! Entre sus propiedades aiiadidas mas importantes
estaban las siguientes: El Multi Record Object y la cuadricula para bases de datos
mejorada, el soporte para Automatizacion OLE y el tipo de datos variantes, el
soporte e integracion totales de Windows 95, el tip0 de datos de cadena larga y la
herencia de formulario visual. Delphi 3 aiiadio la tecnologia Code Insight, el
soporte de depuracion DLL, las plantillas de componentes, el Teechart, el Decision
Cube, la tecnologia WebBroker, 10s paquetes de componentes, 10s ActiveForms y
una sorprendente integracion con COM, gracias a las interfaces.
Delphi 4 nos trajo el editor AppBrowser, nuevas propiedades de Windows 98,
mejor soporte OLE y COM, componentes de bases de datos ampliados y muchas
mas clases principales de la VCL aiiadidas, como el soporte para acoplamiento,
restriccion y anclaje de 10s controles. Delphi 5 aiiadio a este cuadro muchas
mejoras en el IDE (demasiadas para enumerarlas aqui), soporte ampliado para
bases de datos (con conjuntos de datos especificos de ADO e InterBase), una
version mejorada de MIDAS con soporte para Internet, la herramienta de control
de versiones Teamsource, capacidades de traduccion, el concept0 de marcos y
nuevos componentes.
Delphi 6 aiiadio a todas estas propiedades el soporte para el desarrollo
multiplataforma con la nueva biblioteca de componentes para multiplataforma
(CLX), una biblioteca en tiempo de ejecucion ampliada, el motor para base de
datos dbExpress, un soporte excepcional de servicios Web y XML, un poderoso
marco de trabajo de desarrollo Web, mas mejoras en el IDE y multitud de componentes y clases, que siguen comentandose en las paginas siguientes.
Delphi 7 proporciono mas robustez a estas nuevas tecnologias con mejoras y
arreglos (el soporte de SOAP y DataSnap es lo primer0 en lo que puedo pensar) y
ofrece soporte para tecnologias m h novedosas (como 10s temas de Windows XP
o UDDI), per0 lo mas importante es que permite disponer rapidamente de un
interesante conjunto de herramientas de terceras partes: el motor de generacion de
informes RAVE, la tecnologia de desarrollo de aplicaciones Web IntraWeb y el
entorno de diseiio ModelMaker. Finalmente, abre las puertas aun mundo nuevo a1
ofrecer (aunque sea como prueba) el primer compilador de Borland para el lenguaje PascallDelphi no orientado a la CPU de Intel, si no a la plataforma CIL de
.NET.
Delphi es una gran herramienta, per0 es tambien un entorno de programacion
completo en el que hay muchos elementos involucrados. Este libro le ayudara a
dominar la programacion en Delphi, incluidos el lenguaje Delphi, 10s componentes (a usar 10s existentes y crear otros propios), el soporte de bases de datos y
clientelservidor, 10s elementos clave de programacion en Windows y COM y el
desarrollo para Web e Internet.
No necesita tener un amplio conocimiento de estos temas para leer el libro,
per0 es necesario que conozca las bases de la programacion. Le ayudara conside-
y 10s fragmentos clave de 10s listados deberian ayudarle en ese sentido. El libro
utiliza unicamente unas cuantas convenciones para resultar mas legible.
--
--
--
Bases
Delphi 7
Ediciones de Delphi
Antes de pasar a 10s pormenores del entorno de programacion de Delphi, resaltaremos dos ideas clave. En primer lugar, no hay una unica edicion de Delphi,
sino muchas. En segundo lugar, cualquier entorno Delphi se puede personalizar.
Por dichas razones, las pantallas de Delphi que aparecen en este capitulo pueden
ser distintas a las que vea en su ordenador. Las ediciones de Delphi actuales son
las siguientes:
La edicion "Personal": Dirigida a quienes empiezan a utilizar Delphi y a
programadores esporadicos. No soporta programacion de bases de datos ni
ninguna de las caracteristicas avanzadas de Delphi.
La edicion "Professional Studio": Dirigida a desarrolladores profesionales. Posee todas las caracteristicas basicas, mas soporte para programacion de bases de datos (corno soporte ADO), soporte basico para servidores
Web (WebBroker) y algunas herramientas externas como ModelMaker e
IntraWeb. En el libro se asume que el lector trabaja como minimo con la
edicion Professional.
La edici6n "Enterprise Studio": Esta dirigida a desarrolladores que crean
aplicaciones para empresas. Incluye todas las tecnologias XML y de servicios Web avanzados, soporte de CORBA, internacionalizacion, arquitectura en tres niveles y muchas otras herramientas. Algunos capitulos del
libro tratan sobre caracteristicas que solo posee esta version de Delphi y
asi se ha especificado en esos casos.
La edicihn "Architect Studio": Aiiade a la edicion Enterprise el soporte
de Bold, un entorno para la creacion de aplicaciones dirigidas en tiempo de
ejecucion por un modelo UML y capaces de proyectar sus objetos tanto
sobre una base de datos como sobre una interfaz de usuarios, gracias a una
gran cantidad de componentes avanzados. El soporte de Bold no se trata en
este libro.
Ademas de las distintas versiones disponibles, existen varias formas de personalizar el entorno Delphi. En las capturas de pantalla presentadas a lo largo del
libro, se ha intentado utilizar una interfaz estandar (corno la que resulta de la
instalacion tal cual). Sin embargo, en ciertos ejemplos, pueden aparecer reflejadas algunas preferencias del autor como la instalacion de muchos aiiadidos, que
pueden reflejarse en el aspect0 de las pantallas. La version Professional y superiores de Delphi 7 incluyen una copia funcional de Kylix 3, en la edicion de
lenguaje Delphi. Ademas de referencias a la biblioteca CLX y a las caracteristicas multiplataforma de Delphi, este libro no trata Kylix ni el desarrollo sobre
Linux. Puede buscar otras obras para conseguir mas informacion sobre este tema.
(No hay muchas diferencias entre Kylix 2 y Kylix 3 en la version de lenguaje
Delphi. La caracteristica nueva mas importante de Kylix 3 es su soporte del
lenguaje C++.)
rn
blpbi w-pefliiik,~mmpi&p
91cbdi& +g G¶, qvfwnicqk bajc
Fioux. Reipte~eaante:vt&w CLS De& 7, ya que ip versi6n para
lenguaje lM@idc K y b se dkstribuyejunto con el pv&tr;topam ~ind'ms,
Al crear un nuevo proyecto o abrir uno que ya existe, la Component Palette
se reorganiza para mostrar solo 10s controles relacionados con la biblioteca en
uso (aunque en realidad la mayoria de 10s controles son compartidos). Cuando se
traba con un diseiiador no visual (como un modulo de datos), las pestaiias de la
Component Palette que muestran solo 10s componentes visuales se ocultan de
la vista.
[Alignmentpalette]
Create=l
Visible=O
----
----
TRUCO:Si se abre Delphi y no se puede ver el formulario u otras vent.nas, es recornendable cornprobar (o borrar) las configuraciones dti 'escritorio (en el directorio bin de Delphi). Si se abre un proyecto recibido de un
usuario clistinto y no se pueden ver algunas de las ventanas o no gusta la
disposici6n del escritorio, lo mejor es volver a cargar las c o n f i s e i o n e s
.
de 10s escritorios globales o borrar el archivo DSK del proyccto.
Environment Options
Unas cuantas de las ultimas mejoras tienen que ver con el habitual cuadro de
dialogo Environment Options. Las paginas de este cuadro de dialogo se reorganizaron en Delphi 6, desplazando las opciones del Form Designer de la pagina
Preferences a la nueva pagina Designer. En Delphi 6 tambien existian unas
cuantas opciones y paginas nuevas:
La pagina Preferences del cuadro de dialogo: Time una casilla de verificacion que impide que las ventanas de Delphi se acoplen automaticamente
entre si.
La pagina Environment Variables: Permite inspeccionar las variables
del entorno del sistema (como las rutas predefinidas y parametros del SO)
y establecer variables definidas por el usuario. Lo bueno es que se pueden
utilizar ambos tipos de variable en cada uno de 10s cuadros de dialogo del
IDE (por ejemplo, se puede evitar escribir explicitamente rutas usadas
habitualmente, sustituyendolas por una variable). En otras palabras, las
variables del entorno funcionan de manera similar a la variable $DELPHI,
que hace referencia a1 directorio base de Delphi per0 puede ser definida
por el usuario.
L a pagina Internet: En ella se pueden escoger cuales son las extensiones
de archivo predefinidas para 10s archivos HTML y SML (basicamente por
el marco de trabajo WebSnap) y tambien asociar un editor externo con
cada extension.
TO-DOList
Otra caracteristica aiiadida en Delphi 5 pero que aun sigue sin usarse como
deberia es la lista de tareas pendientes. Se trata de una lista de tareas que aun se
debe realizar para completar un proyecto (es un conjunto de notas para el programador o programadores, que resulta una herramienta muy util en un equipo).
Aunque la idea no es novedosa, el concept0 clave de la lista de tareas pendientes
en Delphi es que funciona como una herramienta de dos vias.
Se pueden aiiadir o modificar elementos pendientes a esta lista afiadiendo comentarios TODO al codigo fuente de cualquier archivo de un proyecto; se pueden
ver las entradas correspondientes en la lista. Ademas, se pueden editar visualmente
10s elementos de la lista para modificar el comentario correspondiente en el codigo fuente. Por ejemplo, este es el aspect0 que mostraria un elemento de la lista de
tareas pendientes en el codigo fuente:
procedure TForml.FormCreate(Sender: TObject);
begin
/ / TODO - o M a r c o : A i i a d i r c d d i g o d e creacidn
end;
I t e m del menu desplegable del editor (o la combinacion Control-MayusT ) se genero este comentario:
To-Do
TODO 2 - o M a r c o : B u t t o n p r e s s e d }
Delphi trata todo lo que aparezca tras 10s dos puntos (hasta el final de la linea
o hasta la Have de cierre, segun el tipo de comentario), como el texto del elemento
de tarea pendiente.
---.
_.
I!
1
*
A c m llcm
7 Check comp~lerfelhngs
.I ~ o a ~ e
--'
10-
-A
IWWY
Marco
Figura 1.3. La ventana Edit To-Do Item puede usarse para modificar un elemento de
tarea pendiente, una operacion que tambien puede realizarse directamente en el
codigo fuente.
Finalmente, en la ventana TO-DOList se puede elirninar la marca de un elemento para indicar que se ha completado. El comentario del codigo fuente cambiara de T O D O a DONE.Tambien se puede cambiar manualmente el comentario
en el codigo fuente.
Uno de 10s elementos mas potentes de esta arquitectura es la ventana principal
TO-DOList, que puede recopilar automaticamente informacion de tareas pendientes a partir de archivos de codigo fuente a medida que se escribe, ordenarla,
filtrarla y esportarla a1 Portapapeles como texto simple o una tabla HTML. Todas estas opciones estan disponibles en el menu de contesto.
muestre 10s resultados en una pagina diferente, para que 10s resultados de la
operacion de busqueda anterior sigan disponibles:
Se pueden utilizar las combinaciones Alt-Av Pag y Alt-Re Pag para recorrer
de manera ciclica las pestaiias de esta ventana. (Los mismos comandos sirven
para otras vistas con pestaiias.)
Si suceden errores de compilador, puede activarse otra ventana nueva mediante el comando View>Additional Message Info. A medida que se compila un
programa, esta ventana Message Hints proporcionara informacion adicional para
algunos mcnsajes de error frecuentes, proporcionando sugerencias sobre como
solucionar estos errores:
Este tipo de ayuda esta destinada mas a programadores novatos, per0 podria
ser practico tener presente esta ventana. Es importante darse cuenta de que esta
informacion cs bastante facil de personalizar: un director de desarrollo de un
proyccto pucdc introducir descripciones apropiadas de errores comunes en un
formulario que signifiquen algo especifico para nuevos desarrolladores. Para hacer esto, siga las instrucciones del archivo que guarda los parametros de esta
caracteristica, el archivo msginfo70,ini que se encuentra en la carpeta b i n de
Delphi.
El editor de Delphi
Aparentemente el editor de Delphi no ha cambiado mucho en la version 7 del
IDE. Sin embargo, en el fondo, se trata de una herramienta completamente nueva.
Ademas de emplearlo para trabajar con archivo escritos en lenguaje Pascal orientad0 a objetos (o-en lenguaje Delphi, como prefiere llamarlo ahora Borland), se
puede usar ahora para trabajar con otros archivos relacionados con el desarrollo
en Delphi (como archivos SQL, XML, HTML y XSL), al igual que con archivos
de otros lenguajes (entre 10s que se incluyen C++ y C#). La edicion de XML y
HTML ya estaba disponible en Delphi 6, per0 10s cambios en esta version son
importantes. Por ejemplo, durante la edicion de un archivo HTML se tiene soporte tanto para resaltado de sintaxis como para acabado de codigo.
Las configuraciones el editor para cada archivo (incluido el comportamiento
de teclas como Tab) dependen de la estension del archivo que se abra. Se pueden
configurar estos parametros mediante la nueva pagina Source Options del cuadro de dialogo Editor Properties, que muestra la figura 1.4. Esta caracteristica
se ha ampliado y abierto aun mas para que incluso pueda configurarse el editor
mediante un DTD para formatos de archivo basados en XML o mediante un
asistente personalizado que proporcione el resaltado de sintaxis para otros lenguajes de programacion. Otra caracteristica del editor, las plantillas de codigo,
son ahora especificas del lenguaje (las plantillas predefinidas para Delphi tendran
poco sentido en HTML o C # ) .
Figura 1.4. Los diversos lenguajes soportados por el IDE de Delphi se pueden
asociar con varias extensiones de archivo mediante la pagina Source Options del
cuadro de dialogo Editor Properties.
NOTA:C#es el nuevo lenguaje que present6 Micmsofkjunto con su arquitectura .NET. Borland espera soportar C# en su propio entorno .NET,que
actualmente tiene el nombre en codigo de Galileo.
Si solo se considera el lenguaje Delphi, el editor incluido en el IDE no ha
cambiado mucho en las versiones recientes. Sin embargo, tiene unas cuantas caracteristicas que muchos programadores de Delphi desconocen y no utilizan, asi
que se merece un poco de analisis.
El editor de Delphi nos permite trabajar con varios archivos a la vez, usando
una metafora de "bloc de notas con fichas". Se pasa de una ficha del editor a la
El Code Explorer
L a ventana Code Explorer, que por lo general esta anclada en el lateral del
editor, lista sencillamente todos 10s tipos, variables y rutinas definidas en una
unidad, mas otras unidades que aparecen en sentencias u s e s . En el caso de tipos
complejos, como las clases, el Code Explorer puede listar informacion
pormenorizada, como una lista de campos, propiedades y metodos. Cuando comenzamos a teclear en el editor, toda la informacion se actualizara.
Exploracion en el editor
Otra caracteristica del editor es la Tooltip symbol insight (Ventanas de sugerencia sobre simbolos). A1 mover el raton sobre un simbolo del editor, una ventana de sugerencia nos mostrara el lugar en el que se declara el identificador. Esta
caracteristica puede resultar especialmente importante para realizar el seguimiento de identificadores, clases y funciones de una aplicacion que estamos escribiendo y tambien para consultar el codigo fuente de la biblioteca de componentes
visuales (VCL).
-
ADVERTENCIA: Aunque pueda parecer buena idea en principio, no podemos usar la ventana de sugerencia sobre simbolos para averiguar que
unidad declara un identificador que queremos emplear. En realidad. la ventana de sugerencia no aparece, si no se ha incluido todavia la unidad correspondiente.
Sin embargo, la autentica ventaja de esta funcion, es que el usuario puede
transformarla en un instrumento auxiliar para desplazarse. Si mantenemos pulsada la tecla Control y movemos el raton sobre el identificador, Delphi creara un
enlace activo con la definicion, en lugar de mostrar la ventana de sugerencia.
Dichos enlaces aparecen en color azul y estan subrayados, estilo tipico de 10s
exploradores Web, y el punter0 se transforma en una mano siempre que se situa
sobre el enlace.
Class Completion
El editor de Delphi tambien puede generar parte del codigo fuente, completando lo que ya se haya escrito. Esta caracteristica se llama Class Completion, y se
activa a1 pulsar la combinacion de teclas Control-Mayus-C. Aiiadir un controlador de eventos a una aplicacion es una operacion rapida, porque Delphi aiiade
automaticamente la declaracion de un nuevo metodo que controle el evento y nos
proporciona el esquema del metodo en la seccion de implementacion de la unidad.
Esto forma parte del soporte para programacion visual de Delphi.
De un mod0 similar, las ultimas versiones de Delphi han conseguido facilitar
el trabajo de 10s programadores que escriben codigo extra detras de 10s
controladores de evento. De hecho, la nueva caracteristica de creacion de codigo
afecta a 10s metodos generales, a 10s metodos de control de mensajes y a las
propiedades. Por ejemplo, si tecleamos el siguiente codigo en la declaracion de
clase:
public
procedure Hello (MessageText: string) ;
y a continuacion, pulsamos Control-Mayus-C, Delphi nos ofrecera la definicion del metodo en la parte de implementacion de la unidad y crea las siguientes
lineas de codigo:
{
TForml
procedure TForml.Hello(MessageText:
string);
begin
end;
Esto resulta mas comodo que copiar y pegar una o mas declaraciones, aiiadir
10s nombres de clase y por ultimo duplicar el codigo begin. . . end en cada
metodo copiado. La funcion C 1ass C omp1etio n tambien puede funcionar a
la inversa: podemos escribir la implementacion del metodo directamente con su
codigo y despues pulsar Control-Mayus-C para crear la entrada necesaria en la
declaracion de clase.
El ejemplo mas importante y util de esta funcion de completitud de clases es la
generacion automatica de codigo para dar soporte a las propiedades declaradas en
las clases. Por ejemplo, si en una clase se escribe
p r o p e r t y Value:
Integer;
I n t e g e r r e a d fValue w r i t e S e t v a l u e ;
Code Insight
Ademas del Code Explorer, la funcion de completitud de clases y las funciones de desplazamiento, el editor de Delphi soporta la tecnologia Code Insight. En
conjunto, las tecnicas Code Insight se basan en un analisis sintactico continuo en
segundo plano, tanto del codigo fuente que escribimos como del codigo fuente de
las unidades del sistema a las que se refiere nuestro codigo.
La funcion Code Insight implica cinco capacidades: Code Completion, Code
Templates, Code Parameters, Tooltip Expression Evaluation y Tooltip Symbol
Insight. Esta ultima caracteristica se trato durante la seccion sobre la exploracion
en el editor. Todas estas caracteristicas se pueden habilitar, inhabilitar y configurar en la pagina Code Insight del cuadro de dialog0 Editor Properties.
Code Completion
La funcion Code Completion permite escoger la propiedad o metodo de un
objeto simplemente buscandolo en una lista o escribiendo sus letras iniciales.
Para activar esta lista, solo hay que teclear el nombre de un objeto, como Buttonl,
aiiadir el punto y esperar. Para que forzar la aparicion de la lista, hay que pulsar
Control-Barra espaciadora; para quitarla cuando no queramos verla, hay que
pulsar Esc. La funcion Code Completion permite ademas buscar un valor adecuado en una sentencia de asignacion.
Cuando comenzamos a teclear, la lista va filtrando su contenido de acuerdo
con la parte inicial del elemento que hemos escrito. La lista Code Completion
emplea colores y muestra mas detalles para ayudarnos a distinguir elementos
diferentes. En Delphi, se pueden personalizar estos colores mediante la pagina
..
L.
Code Templates
Esta caracteristica permite insertar una de las plantillas de codigo predefinidas,
como una declaracion compleja con un bloque interior b e g i n . . . . e n d . Las
plantillas de codigo deben activarse de forma manual, usando Control-J para
obtener una lista de todas ellas. Si tecleamos unas cuantas letras (como una palabra clave) antes de pulsar Control-J, Delphi listara solo las plantillas que
comiencen por dichas letras.
Tambien se pueden aiiadir plantillas de codigo personalizadas, para crear metodos abreviados para 10s bloques de codigo que usemos normalmente. Por ejemplo, si empleamos con frecuencia la funcion M e s s a g e D l g , podemos aiiadir una
plantilla para la misma.
Para modificar plantillas, mediante la pagina Source Options del cuadro de
dialogo Editor Options, hay que seleccionar Pascal en la lista Source File Type
y hacer clic sobre el boton Edit Code Templates. A1 hacer esto, aparecera el
nuevo cuadro de dialogo Code Templates de Delphi 7.
En este momento, si hacemos clic sobre el boton Add, escribimos un nuevo
nombre de plantilla (por ejemplo, d e s o r d e n ) , escribimos tambien una descripcion y, a continuacion, aiiadimos el siguiente texto a1 cuerpo de la plantilla en el
control memo Code:
MessageDlg
('
I'
mtInformation,
[mbOK]
0) ;
Ahora, cada vez que necesitemos crear un cuadro de dialogo de mensaje, simplemente escribiremos d e s o r d e n y, a continuacion, pulsaremos Control-J para
obtener el texto completo. El caracter de linea vertical indica la posicion dentro
del codigo fuente en la que estara el cursor en el editor despues de haber desplegado la plantilla. Deberiamos escoger la posicion en la que queremos comenzar a
teclear para completar el codigo producido por la plantilla.
Aunque pueda parecer que las plantillas de codigo, a primera vista, se corresponden con palabras clave del lenguaje, estas son en realidad un mecanismo mas
general. Se guardan en el archivo DELPHI32.DC1, un archivo de texto en un
formato bastante simple que puede editarse con facilidad. Delphi 7 tambien permite exportar la configuracion para un lenguaje a un archivo e importarla, lo que
facilita que 10s desarrolladores intercambien sus propias plantillas personalizadas.
Code Parameters
La funcion Code Parameters muestra, en una ventana de sugerencia, el tip0 de
datos de 10s parametros de un metodo o funcion mientras 10s tecleamos. A1 escribir el nombre de la funcion o metodo y abrir el parentesis, apareceran inmediatamente 10s nombres y tipos de parametro en una ventana de sugerencia contextual.
Para que forzar a que aparezcan 10s parametros de codigo, podemos pulsar Control-Maylis-Barra espaciadora. Ademas, el parametro en uso aparece resaltado
en negrita.
Diagram View
La vista de diagrama muestra las dependencias entre componentes, como las
relaciones padrelhijo, de posesion, las propiedades enlazadas y las relaciones
genericas. En el caso de componentes de un conjunto de datos, tambien soporta
relaciones maestroldetalle y conexiones de busqueda. Podemos incluso aiiadir
comentarios en bloques de texto enlazados a componentes especificos.
El diagrama no se crea de forma automatica. Debemos arrastrar 10s componentes desde la vista en arb01 a1 diagrama, en el que automaticamente apareceran
las relaciones entre 10s mismos. Podemos seleccionar diversos elementos desde la
Object TreeView y arrastrarlos todos a la vez a la ficha Diagram.
Lo agradable es que podemos definir propiedades simplemente dibujando flechas entre 10s componentes. Por ejemplo, despues de mover un control Edit y una
etiqueta a1 diagrama, podemos seleccionar el icono Property Connector, hacer
clic sobre la etiqueta y arrastrar el cursor del raton sobre el control Edit. Cuando
soltemos el boton del raton, el diagrama establecera una relacion de posesion
basada en la propiedad F o c u s C o n t r o 1,que es la unica propiedad de la etiqueta que se refiere a un control Edit. Esta situacion se muestra en la figura 1.6.
Como se puede ver, la definicion de propiedades es direccional: si arrastramos
la linea de relacion de propiedad desde el control Edit a la etiqueta, en realidad,
estamos intentando usar la etiqueta como valor de una propiedad del cuadro de
edicion. Dado que eso no es posible, veremos un mensaje de error que nos indica
el problema y nos ofrece la posibilidad de conectar 10s componentes en la direccion opuesta.
El Diagram View nos permite crear varios diagramas para cada unidad Delphi
(es decir, para cada formulario o modulo de datos). Simplemente se proporciona
un nombre a1 diagrama y se puede aiiadir tambien una descripcion, haciendo clic
sobre el boton New Diagram, se prepara otro diagrama y se puede pasar de un
diagrama a otro usando el cuadro combinado de la barra de herramientas de la
vista en diagrama.
--
tih e
Q+fcrolim
J
Ths IS a simple version
rM base
NOTA: Si se desea experimentar con la vista en diagrama, se puede comenzar abriendo el proyecto DiagramDemo. El formulario del programa
, , , :+
.,
,..,
A,
A: ,
,
,
,
-,,,,,:,J,-.
,
,I .I1, C
,1 L
LIGIIG UW3 U l i l g l i U l l i l 3 i l ? ~ U b l i l U W S .U l l U GI1 G I U G lil I l g U l i l l .V
.Y..-,,-.I,..,
,A,
U l l U 1IIUC;llU IllilJ
Form Designer
Otra ventana de Delphi con la que vamos a interactuar muy a menudo es el
Form Designer; una herramienta visual para colocar componentes en 10s formularios. En el Form Designer, se puede seleccionar directamente un componente
con cl raton o a traves dcl Object Inspector o la Object Treeview, mttodos
ultimos utiles en caso de quc un control quede oculto. Si un control cubre a otro
por complete, se puedc emplear la tecla Esc para seleccionar el control padre del
control actual. Se pucdc pulsar la tecla Esc una o mas veces para seleccionar el
formulario o pulsar y mantener pulsada la tecla Mayus mientras sc hace clic
sobre cl componcntc scleccionado. Esto desactivara la selection del componente
en uso y seleccionara el formulario por defecto.
Esisten dos alternativas al uso del raton para fijar la posicion de un componcnte. Se pueden definir valores para las propiedades L e f t y TOP, o bien usar
las teclas de cursor mientras se mantiene pulsada la tech Control. El uso de las
teclas de cursor resulta util sobre todo para precisar la posicion de un elemento
(cuando la opcion Snap To Grid se encuentra activada), a1 igual que mantener
pulsada la tecla Alt y utilizar el raton para mover el control. Si se pulsa la
combinacion Control-Mayus junto con una tecla de cursor, el componente se
mover6 solo a intervalos de cuadricula.
Del mismo modo, a1 pulsar las teclas de cursor mientras se mantiene pulsada
la tecla Mayus, podemos precisar el tamaiio de un componente, algo que tambien
se puede hacer mediante la tecla Alt y el raton.
Para alinear diversos componentes o hacer que tengan el mismo tamaiio, se
pueden sclcccionar todos ellos y establecer las propiedades Top, L e f t , Width
o H e i g h t de todos a1 mismo tiempo. Para seleccionar varios componentes, podemos haccr clic sobre ellos con el raton mientras mantenemos pulsada la tecla
Mayus o. si todos 10s componentes se encuentran en zonas rectangulares, se
puede arrastrar el raton hasta "dibujar" un rectangulo que 10s rodee. Para seleccionar 10s controles hijo (por ejemplo, 10s botones que se encuentran dentro de un
panel), arrastraremos el raton dcntro del panel mientras que mantendremos pulsada la tecla Control, ya que de no ser asi desplazaremos el panel. Cuando ya esten
seleccionados 10s diversos componentes, tambien se puede fijar su posicion relativa utilizando el cuadro de dialog0 Alignment (con la ordcn A l i g n del menu de
metodo abreviado del formulario) o Alignment Palette (a la que accedemos
mediante la orden del menu View>Alignment Palette).
Object lnspector
Para visualizar y modificar las propiedades dc 10s componentes de un formulario (u otro disciiador) en tiempo de diseiio, se puede utilizar el Object Inspector.
En comparacion con las primeras versiones de Delphi, el Object lnspector dispone de unas cuantas caracteristicas nuevas. La ultima, presentada en Delphi 7,
es cl uso de una fuente en negrita para resaltar las propiedades que tienen un valor
distinto dcl predefinido. Otro importante cambio (en Delphi 6) es la capacidad del
Object lnspector de desplegar las referencias de 10s componentes emplazados.
Las propiedades que se refieren a otros componentes aparecen en un color difercntc y pueden desplegarse seleccionado el simbolo + de la izquierda, como ocurre con 10s subcomponentes internos. A continuacion, se pueden modificar las
propiedades de ese otro componente sin tener que seleccionarlo. La siguiente
figura muestra un componente conectado (un menil desplegable) expandido en el
Object lnspector mientras que se trabaja con otro componente (un cuadro de
lista):
Esta caracteristica de ampliacion de la interfaz tambien soporta subcomponentes, tal y como demuestra el nuevo control LabeledEdit.Una caracteristica relacionada del Object lnspector es que podemos seleccionar el componente
a1 que hace referencia una propiedad. Para ello, hacemos doble clic sobre el valor
de la propiedad con el boton izquierdo del raton mientras mantenemos pulsada la
tecla Control. Por ejemplo, si tenemos un componenteMainMenu en un formulario y estamos echando un vistazo a las propiedades del formulario en el Object
Inspector, podemos seleccionar el componente MainMenu moviendonos a la propiedad MainMenu del formulario y haciendo doble clic sobre el valor de dicha
propiedad mientras mantenemos pulsada la tecla Control. Con esto se selecciona
el menu principal indicado junto con el valor de la propiedad en el Object Inspector. A continuacion aparece una lista de algunos cambios recientes del Object
Inspector:
La lista situada en la parte superior del Object Inspector muestra el tip0 de
objeto y permite escoger un componente. Puede eliminarse para ahorrar
algo de espacio, ya que se puede seleccionar componentes en la Object
Treeview.
Las propiedades que hacen referencia a un objeto son ahora de un color
diferente y pueden ampliarse sin cambiar la seleccion.
Opcionalmente se pueden ver tambien las propiedades de solo lectura en el
Object Inspector. Por supuesto, estan en gris.
El Object lnspector posee un cuadro de dialog0 Properties, que permite
personalizar 10s colores de diversos tipos de propiedades y el comportamiento general de esta ventana.
Desde Delphi 5, la lista desplegable de una propiedad puede incluir elementos graficos. Esta caracteristica la utilizan propiedades como color
y Cursor,y es particularmente util para la propiedad ImageIndex de
10s componentes conectados a una ImageList .
NOTA: Las propiedades de la interfaz plleden coafigurarse ahora en tiempo de diseKo utilimdo d meet ImpMor. Este n s a s l linodeloInterfaced
por hterfaz) presentaComponent Reference ('Rdeienciasde G~m~pobetrte
do en KyIix/Delpbi 6, en &qxk IT& c?mp&&nk$ pueden implemmtar y
mantener referenciae*a b interfaces siempre qae Iw ihterfaces eat&
impl&entadas por coapmenteJ, Este mbdelb hnciicma d igrlal qae h
antiguas y simp~es.referencbr componentes, p m IBS propidides de
interfaz pueden enlazaise c m 4QUier componente que implemente la
interfaz necesaria. Las prcjpiahdes #e i n t e e no egt4.11lhfiitadas a mtip
de componentes especjfico (ma elwe o so's clases derivadu). Cuands hacemos clic sobm la%&,despggableen el edihr de %& Y n s w i p a r a
. - - . - - - . -- - . . obtener una propiedad deinterfaz, aparecen todos 10s componentes de1 for-mulario actual (y formularios relacionados) que irnpleme& la interfaz.
B F d
ITFant)
Charsel
'DEFAULT-CHARSET
Cob
' W cWindowText
Existe una segunda forma, miis compteja, de personalizar el Object Ins. personalizada para todo el Object Inspector, para que
pector: una. fuente
su texto resulte mhs visible. Esta caracteristica resulta especialmente util
en el caso de presentaciones publicas.
P
Categorias
propiedades
csta opcion, las propiedades no se listaran alfabeticamente sino que se organizaran por grupos, con la posibilidad de que cada propiedad aparezca cn diversos
grupos.
Las categorias tienen la ventaja de reducir la complejidad del Object Inspector. Se puede usar el submenu View presente en cl menu de metodo abreviado
para ocultar propiedades de determinadas categorias. sea cual sea el mod0 en que
aparezcan (es decir, incluso aunque se desee el tradicional orden por nombrc, aun
asi se podran las propiedades de algunas categorias).
Object TreeView
Delphi 5 introdujo una vista en arbol para modulos de datos: en la que se
podian ver las relaciones entre 10s componentes no visuales, como 10s conjuntos
de datos, 10s campos, las acciones, etc. Delphi 6 amplio esta idea a1 proporcionar
una Object TreeView para cada diseiiador, como en el caso de 10s formularios
simples. La Object TreeView se situa por defecto sobre el Object Inspector.
La Object TreeView muestra todos 10s componentes y objetos del formulario
en un arbol en el que se representan sus relaciones.
La relacion mas obvia es la relacion padrelhijo: si colocamos un panel sobre
un formulario, un boton dentro de cste y uno fuera del panel, en el arbol apareceran 10s dos botones, uno bajo el formulario y el otro bajo el panel, tal como
mucstra la figura:
Ld h l -+ +
New (Newl)
em, Open (Open11
bM,Save (Save1 )
hn
A veces, en la vista en arbol aparecen tambien nodos falsos, que no corresponden a un objeto real sino a uno predefinido. Como ejemplo de este comportamiento, si desplegamos un componente Table (desde la ficha BDE), veremos dos
iconos en gris que corresponden a la sesion y a1 alias. Tecnicamente, la Object
TreeView usa iconos en gris para 10s componentes que no permanecen en tiempo
de diseiio. Son componentes reales (en tiempo de diseiio y en tiempo de ejecucion), per0 como son objetos predefinidos que estan construidos en tiempo de
ejecucion y no contienen datos permanentes que se puedan editar en tiempo de
diseiio, el Data Module Designer no nos permite editar sus propiedades. Si
colocamos una Table en el formulario, veremos tambien elementos que tienen a
su lado una interrogacion en rojo dentro de un circulo amarillo. Este simbolo
indica elementos parcialmente definidos.
La Object TreeView soporta varios tipos de arrastre:
Podemos escoger un componente de la paleta (haciendo clic sobre el, no
arrastrandolo), mover el raton sobre el arbol y hacer clic sobre un componente para dejarlo ahi. Esto nos permite dejar un componente en el contenedor que corresponda (formulario, panel y otros), aunque su superficie
este totalmente cubierta por otros componentes, algo que evita, a su vez,
que dejemos el componente en el diseiiador sin reorganizar primer0 10s
demas componentes.
En la vista en arbol, podemos arrastrar componentes, Ilevandolos, por ejemplo, de un contenedor a otro. Con el Form Designer, en cambio, solo
podemos utilizar la tecnica de cortar y pegar. Mover, en lugar de cortar,
nos ofrece la ventaja de conservar las conexiones entre 10s componentes, si
las hubiera, y de que no se pierdan como ocurre al eliminar el componente
durante la operacion de cortar.
Podemos arrastrar 10s componentes desde la vista en arbol a la vista en
diagrama.
Al pulsar con el boton derecho del raton sobre cualquier elemento de la vista
en arbol, aparece un menu de metodo abreviado, similar a1 menu de componentes
que obtenemos cuando el componente esta en un formulario (y en ambos casos, en
el menu de metodo abreviado pueden aparecer elementos relacionados con 10s
1Folrnl
0 Bullon2
[-I IJ
-
,:
Columns
4 o .r L ~ r c o l w m
L: 1 TL~~tCalurnn
2 . TL~stCalumn
4 3 - T L~slCd.mm
Copiar esta descripcion y pegarla en el formulario creara un boton en la posicion especificada con la etiqueta Mi Boton con una fuente Arial.
Para utilizar esta tecnica, es necesario saber como editar la representacion
textual de un componente, que propiedades son validas para ese componente en
particular y como escribir 10s valores para las propiedades de cadena, de conjunto
y otras propiedades especiales.
Cuando Delphi interpreta la descripcion textual de un componente o formulario, tambien puede cambiar 10s valores de otras propiedades relacionadas con
aquellas que se han modificado y podria cambiar la posicion del componente, de
forma que no solape con una copia previa. Por supuesto, si escribimos algo totalmente incorrect0 e intentamos pegarlo en un formulario, Delphi mostrara un mensaje de error indicando lo que ha fallado.
Se pueden seleccionar diversos componentes y copiarlos a otro formulario o
bien a1 editor de textos a1 mismo tiempo. Puede que esto resulte util cuando
necesitamos trabajar con una serie de componentes similares. Podemos copiar
uno en el editor, reproducirlo una serie de veces, realizar las modificaciones apropiadas y, a continuacion, pegar todo el grupo de nuevo en el formulario.
TRUCO: Un grupo de programadores en Delphi puede compartir plantillas de componentes si las guarda en un directorio comb, W i e n d o a1
Registro la entrada CCLibDir bajo la claw \SoftwareABorland\
Delphi\7.0\ComponentTemplates.
La plantillas de componentes son muy comodas cuando hay distintos formularios que necesitan el mismo grupo de componentes y controladores de eventos
asociados. El problema es que una vez que se ha colocado una instancia de la
plantilla en el formulario, Delphi hace una copia de 10s componentes y de su
codigo, que ya no se referira a la plantilla. No hay ninguna forma de modificar la
definicion de la propia plantilla, ni tampoco es posible realizar el mismo cambio
en todos 10s formularios que usan dicha plantilla. Pero si lo podemos hacer gracias a la tecnologia de marcos de Delphi.
Un marco es una especie de panel con el que se puede trabajar en tiempo de
diseiio de un mod0 similar a un formulario. Simplemente se crea un nuevo marco,
se colocan algunos controles en el y se aiiade el codigo a 10s controladores de
eventos. Cuando el marco esta listo, se abre un formulario, se selecciona el
pseudocomponente Frame desde la ficha Standard de la Component Palette y
se escoge uno de 10s marcos disponibles (del proyecto actual). Despues de colocar
el marco en el formulario, lo veremos como si 10s componentes se hubieran copiado en el. Si modificamos el marco original (en su propio diseiiador), las modificaciones apareceran reflejadas en cada una de las instancias del marco.
T
Framel
I1
IS Smt Smi
(@dad
Framel
All
A1 igual que 10s formularios, 10s marcos definen clases, por lo que encajan
dentro del modelo orientado a objetos de la VCL con mayor facilidad que las
plantillas de componentes. Como cabe imaginar a partir de esta introduccion
breve, 10s marcos son una tecnica nueva realmente potente.
Gestionar proyectos
El Project Manager de Delphi (View>Project Manager) funciona con un
grupo de proyectos, que puede englobar a su vez uno o mas proyectos. Por ejemplo, un grupo de proyectos puede incluir una DLL y un archivo ejecutable o
varios archivos ejecutables. Todos 10s paquetes abiertos apareceran como proyectos en la vista del Project Manager, incluso aunque no se hayan aiiadido a1
grupo de proyecto.
En la figura 1.9, podemos ver el Project Manager con el grupo de proyecto
del presente capitulo. Como se puede ver, el Project Manager se basa en una
vista en arbol, que muestra la estructura jerarquica del grupo de proyectos, 10s
proyectos y todos 10s formularios y unidades que lo componen. Podemos emplear
la barra de herramientas y 10s menus de metodo abreviado mas complejos del
Project Manager para trabajar con el. El menu de metodo abreviado funciona de
acuerdo con el contexto: sus opciones dependen del elemento seleccionado. Hay
elementos del menu para afiadir un nuevo proyecto o un proyecto esistente a1
grupo de proyecto, para compilar o crear un proyecto especifico o para abrir una
unidad.
Fata
P&h
- - -
- - --
C Wchnos de programa\Borland\Delph17\Prolecls
- !
3 w e r n ~me
d 9 OtagrarnForrn
ttl
ToDoTesl exe
n
Fiamesl.ere
,-' Fum
Fnm pas
Form1
13 @ Frame
@ Flame pas
Fianel
a
J
D \rn~code\~~\~~agram~erno
D Lnd7caJe\Ol\D1agramDemo
D \md7code\0l\ToDoTest
D \rnd7caJe\Ol\Fiarnesl
D hd7mde\Ol\Framesl
D \md7wdeWl\Frametl
D \md7codeWl\Fiamesl
D \rnd7codeWl \Frames1
D \md7code\Ol \Frames1
D hd7mde\O1 \Flames1
1
qudes abiaitos, hdusoaunque no selfiayaediadido sl grupo de proyedos.
TRUCO: d i r d e Belphi 6; e f e e c t Manager muestra tambih 1bs pa-
M&o se vex&&
a6elante.
De todos 10s proyectos de un grupo, solo hay uno que esta activo y ese es el
proyecto sobre el que trabajamos cuando seleccionamos una orden como
Project>Compile. El menu desplegable Project posee dos ordenes para compilar o crear todos 10s proyectos del grupo. Cuando tengamos que crear diversos
proyectos, podemos establecer un orden relativo usando las ordenes Build
S o o n e r y Build Later.Estas dos ordenes basicamente reorganizan 10s proyectos de la lista.
Project1 .exe
Opciones de proyecto
El Project Manager no ofrece una forma de definir las opciones para dos
proyectos diferentes a la vez. Sin embargo, se puede recurrir a1 dialog0 Project
Options desde el Project Manager en el caso de cada proyecto. La primera
ficha de Project Options (Forms) muestra la lista de 10s formularios que se
deberian crear automaticamente cuando arranca el programa y 10s formularios
que crea el propio programa. La siguiente ficha (Application) se usa para establecer el nombre de la aplicacion y el nombre de su archivo de ayuda y para
S - , T-
{$WARN
{$WARN
{$WARN
$ WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
{$WARN
ISWARN
ON)
ADVERTENCIA: Del@
-
yarn-
- -
--.
3
?. Unit idenlifier does m t match hle name
% Na sonl~gu~at~an
files laund
-
,JJUser message
@ Er~orconverl~qlocals s l i i to
~ Unicode
,4
Imagebase e not a rmkiile d 64k
LI'.Unsafe typecast
Figura 1.10. La nueva pagina Compiler Messages del cuadro de dialogo Project
Options.
TambiCn se pueden habilitar o inhabilitar algunas de estas advertencias mediante opciones de compilador como estas:
( $Warn UNSAFE-CODE OFF)
{ $Warn UNSAFE-CAST OFF)
($Warn UNSAFE-TYPE
OFF)
En general, es mejor mantener estas opciones fuera del codigo fuente del programs, algo que finalmente permite Delphi 7.
BPG
Desarrollo
Borland Project Group
(Grupo de proyectos
Borland): archivos que usa
el nuevo Project Manager.
Es una especie de
makefile.
BPL
Se distribuiran a otros
desarrolladores Delphi
y opcionalmente a
usuarios finales.
CAB
Compilacion
Distribuido a usuarios
.CFG
Archivo de configuracion
con las opciones de proyecto. Similar a 10s archivos DOF.
Desarrollo
Necesario
han definido opciones
especiales de compilacion.
.DCP
Necesario cuando
usamos paquetes.
Solo se distribuira a
otros desarrolladores
.DCU
Solo si el codigo
fuente no esta disponible. Los archivos
DCU de las unidades
que escribimos son un
paso intermedio, por
lo que favorecen una
compilacion mas
rapida.
. DDP
5).
.DFM
Desarrc
.-DF
Copia de seguridad de
Delphi Form File (DFM)
Desarrollo
DFN
DLL
Vease .EXE
DOF
Necesario solo si se
han instalado opciones especiales del
compilador.
DPK y
~ h o r atamlien .DPKW
r .DPKL
Desarrollo
Si.
DPR
Desarrollo
-DP
Desarrollo
DSK
Cornpilacion
Enlace
DSM
EXE
HTM
0 .HTML (Hypertext
Markup Language, Lenguaje de rnarcas con
hipertexto): el forrnato de
archivo usado para paginas Web.
LIC
Asistente
Los archivos de licencia
relacionados con un archi- ActiveX y otras
vo OCX.
herrarnientas
OBJ
. OCX
.PAS
Cornpilacion
(pero solo si se
ha activado la
opcion Save
Symbols)
Paso intermedio
de compilacion,
generalmente no
se usa en
Delphi.
Vease .EXE.
-PA
Copia de seguridad de un
archivo Pascal (.PAS).
Desarrollo
RES, .RC
.RPS
Translation Repository
(parte de Integrated
Translation Environment).
Desarrollo (ITE)
.TLB
Este es un archivo
que pueden necesitar
otros prograrnas OLE.
TODO
.UDL
Desarrollo
rn
presenta una breve lista de las extensiones que merece la pena conocer. La mayoria de estos archivos estan en formatos propietarios no documentados, por lo que
poco se puede hacer con ellos.
Tabla 1.2. Extensiones de archivo d e personalizacion del IDE d e Delphi
seleccionadas.
.DC I
.DRO
Delphi Object Repository (Object Repository d e Delphi) (Deberia modificarse el Repository con la orden Tools>
Repository.)
.DMT
.DBI
.DEM
.DCT
.DST
Desktop Settings File (Archivo d e configuracion del escritorio): uno para cada configuracion d e escritorio definida.
- ..
-- - - -
- .
- -
------
Ademas de 10s dos archivos que describen el formulario (PAS y DFM), hay un
tercer archivo que resulta vital para volver a construir la aplicacion. Este es el
archivo de proyecto de Delphi (DPR), otro archivo de codigo f~ienteen Pascal,
que se crea automaticamente y que rara vez es necesario modificar manualmente.
Puede verlo con la orden del menu View>Project Source.
Algunos de 10s demas archivos menos relevantes creados por el IDE usan la
estructura de archivos IN1 de Windows, en la que cada seccion se indica mediante
un nombre que va entre corchetes. Por ejemplo, este es un fragment0 de un archivo de opciones (DOF).
[Compiler]
A= 1
B=O
[Linker]
MinStackSize=16384
MaxStackSize=1048576
ImageBase=4194304
[Parameters]
Runparams=
HostApplication=
El Object Repository
Delphi tiene varias ordenes de menu que se pueden usar para crear un nuevo
formulario, una nueva aplicacion, un nuevo modulo de datos, un nuevo componente, etc. Dichas ordenes estan situadas en el menu File>New y en otros menus
desplegables. Si seleccionamos sencillamente File>New>Other, Delphi abre el
Object Repository, que se usa para crear nuevos elementos de cualquier tipo:
formularios, aplicaciones, modulos de datos, objetos thread, bibliotecas, componentes, objetos de automatizacion y muchos mas.
El nuevo cuadro de dialogo (que se ve en la figura 1.12) posee varias fichas,
contiene todos 10s elementos que puede crear, 10s formularios existentes y 10s
proyectos almacenados en el Repository, asistentes Delphi y 10s formularios del
proyecto actual.
Las fichas y las entradas de este cuadro de dialogo con solapas dependen de la
version especifica de Delphi, por lo que no las mencionaremos.
cripcion, autor y fecha de la herramienta, una informacion que resulta sobre todo importante cuando se echa un vistazo a los asistentes, proyectos o
formularios que hemos aiiadido a1 Repository.
Cwone*
TI
Package
Flame
Ploiect GI-
Console
Applcat~on
DCC W ~ z a d
Fam
Resource DLL
Wnald
Service
Figura 1.12. La primera pagina del cuadro de dialogo New Items, conocida
generalmente corno Object Repository.
__:_*_-A_
__LZ__
--
Wc_hName
' V W Controls
' ' expected but end of lde found
TRad~oButton Symbol was el~mtnatedby l~nker
rn
rn
_ -_
..-
El lenguaje
de programaclon
Delphi
Objetos y memoria.
Herencia.
Metodos virtuales y polimorfismo.
Conversion de tipos segura (informacion de tip0 en tiempo de ejecucion).
Interfaces.
Trabajo con excepciones.
Referencias de clase.
Clases y objetos
Delphi se basa en 10s conceptos de la orientacion a objeto y, en particular, en
la definition de nuevos tipos de clase.
El uso de OOP esta forzado en parte por el entorno de desarrollo visual, ya que
para cada formulario nuevo definido en tiempo de disefio, Delphi define
automaticam'ente una clase nueva. Ademas, cada componente situado visualmente
en un formulario es un objeto de un tip0 de clase disponible en la biblioteca del
sistema o afiadido a ella.
(m, d, y: Integer) ;
TRUCO:Si se pulsa Controi-Maylis-C mientras que el cursor se m'cwtra sobre la definicion de clase, la ~aracteristieaClass Completion
del editor de Delphi generara el esqueleto de la deWci6n d&10s rn6bcbs
declarados en una clase.
Es asi como se puede usar un objeto de la clase definida anteriormente:
var
ADay: TDate;
begin
// c r e a un o b j e t o
A D a y : = TDate.Create;
tr~
// u s a e l o b j e t o
A D a y - S e t V a l u e ( 1 , 1 , 2000);
if A D a y - L e a p Y e a r then
ShowMessage ( ' A d o b i s i e s t o :
finally
// d e s t r u y e e l o b j e t o
ADay. Free;
end ;
end :
' +
IntToStr ( A D a y - Y e a r ) ) ;
en lugar de en la pila. Este proceso hace que las llamadas a metodo resulten mucho mas rapidas.
Esta informacion es necesaria para crear un componente boton en esa posicion. Veamos el codigo de este metodo:
procedure TForml.FormMouseDown (Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
Btn: TButton;
begin
Btn : = TButton-Create (Self);
Btn-Parent : = Self;
Btn-Left : = X;
B t n - T o p : = Y;
Btn-Width := Btn.Width + 50;
Btn-Caption : = Format ( ' B o t d n e n %d, % d l , [ X , Y]);
end ;
Con este codigo, se crean botones en las posiciones en las que se haga clic con
el raton, como muestra la figura 2.1. En el codigo anterior, fijese en concreto en el
uso de la palabra clave S e 1f , tanto como parametro del metodo c r e a t e (para
especificar el dueiio del componente), como valor de la propiedad P a r e n t .
Figura 2.1. El resultado del ejernplo CreateCornps, que crea componentes boton en
tiernpo de ejecucion.
Encapsulado
Una clase puede tener cualquier cantidad de datos y cualquier numero de metodos. Sin embargo, para conseguir una buena tecnica orientada a objetos, 10s datos
deberian estar ocultos o encapsulados dentro de la clase que 10s usa. Cuando se
accede a una fecha, por ejemplo, no tiene sentido cambiar solo el valor del dia
directamente. De hecho, si se cambia el valor del dia podria resultar una fecha no
valida, como el 30 de febrero, por ejemplo. Si se usan metodos para acceder a la
representacion interna de un objeto, se limita el riesgo de generar situaciones
erroneas, puesto que 10s metodos pueden verificar si la fecha es valida y negarse
a modificar el nuevo valor si no lo es. El encapsulado es importante porque permite que la persona que escribe las clases modifique la representacion interna en una
version futura.
El concepto de encapsulado se describe normalmente como una "caja negra",
en la que no se conoce el interior: simplemente se sabe como interactuar con ella
o como usarla, sin tener en cuenta su estructura. La parte "modo de empleo",
denominada interfaz de clase, permite que otras partes de un programa tengan
acceso y utilicen 10s objetos de dicha clase. Sin embargo, cuando se emplean 10s
objetos, la mayor parte de su codigo esta oculto. Rara vez se conocen 10s datos
internos que contiene el objeto y normalmente no hay manera de acceder directamente a 10s datos. Por supuesto, se supone que utilizamos 10s metodos para acceder a 10s datos, que estan protegidos contra accesos no autorizados. Esta es la
tecnica orientada a objetos del concepto de programacion clasico conocido como
ocultacion de informacion. Sin embargo, en Delphi existe un nivel adicional de
ocultacion, mediante propiedades.
Delphi implementa este encapsulado basado en clases per0 todavia soporta el
encapsulado clasico basado en modulos, que usa la estructura de unidades. Todo
identificador que se declare en la seccion de interfaz de una unidad resulta visible
a otras unidades del programa, siempre que se utilice una sentencia u s e s que se
refiere a la unidad que define el identificador. Por otro lado, 10s identificadores
declarados en la seccion de implernentacion de la unidad seran locales a esa unidad.
Fijese en que debido a que la unica modificacion se realiza en la seccion privadaj no habra que modificar ninguno de 10s programas existents que usen la clase.
Esa es la ventaja del encapsulado.
---L--
J-
---A_-
--1_-
_I--
mite modificar la clase ampliamente sin que afecte a1 codigo que la utiliza. Una
buena definicion de propiedades es la de campos virtuales. Desde la perspectiva
del usuario de la clase que las define, las propiedades poseen m a apariencia
exactamente igual a la de 10s campos, ya que, por lo general se puede leer o
escribir su valor. Por ejemplo, se puede leer el valor de la propiedad C a p t i o n de
un boton y asignarla a la propiedad T e x t de un cuadro de edicion con el siguiente
codigo:
Parece que estuvidramos leyendo y escribiendo campos. Sin embargo, las propiedades pueden proyectarse directamente a datos, asi como a metodos de acceso,
para leer y escribir el valor. Cuando las propiedades se proyectan a metodos, 10s
datos a 10s que acceden pueden formar parte del objeto o estar fuera de el y
pueden producir efectos secundarios, como volver a pintar un control tras haber
cambiado sus valores. Tecnicamente, una propiedad es un identificador que esta
proyectado a datos o metodos que usan una clausula r e a d y otra w r i t e . Por
ejemplo, aqui tenemos la definicion de una propiedad M o n t h para una clase de
fecha:
property Month:
Integer
Normalmente, 10s datos reales y 10s metodos de acceso son privados (o protegidos) mientras que la propiedad es publica. Esto significa que hay que usar la
propiedad para tener acceso a aquellos metodos o datos, una tecnica que ofrece
tanto la version simplificada como la extendida del encapsulado. Se trata de un
encapsulado ampliado, porque no solo se puede cambiar la representacion de 10s
datos y sus funciones de acceso, sino tambien aiiadir o eliminar funciones de
acceso sin cambiar el codigo de llamada en absoluto. Un usuario solo necesita
volver a compilar el programa usando la propiedad.
popiedad y el punto y coma, a1 pulsar Control-Mayus-C, Delphi proporcionara una definicion completa y el esqueleto del mttodo de escritura. Si
se escribe G e t delante del nombre del identificador despues de la palabta clave read tambitn se conseguira un m e t o d ~de lectura sin apenas
escribir.
.'
Encapsulado y forrnularios
Una de las ideas clave del encapsulado es reducir el numero de variables globales
cmpleadas por el programa. Se puede acceder a una variable global desde todas
las partes de un programa. Por esa razon, un cambio en la variable global afecta
al programa entero. Por otra parte, cuando se cambia la representacion de un
campo de clase, solo hay que cambiar el codigo de algunos metodos de dicha clase
y nada mas. Por lo tanto, podemos decir que la ocultacion de informacion se
refiere a 10s cambios de encapsulado.
Cuando tengamos un programa con diversos forrnularios, podemos hacer que
algunos datos estkn disponibles para todos 10s formularios, si 10s declaramos
como variable global en la parte de interfaz de la unidad de uno de 10s formularios:
var
Form1: TForml;
nClicks:
Integer;
Esto funciona pero tiene dos inconvenientes. En primer lugar, 10s datos no
estan conectados a un caso especifico del formulario, sino a1 programa entero. Si
creamos dos formularios del mismo tipo, compartiran 10s datos. Si queremos que
cada formulario del mismo tip0 tenga su propia copia de 10s datos, la unica
solucion es aiiadirlos a la clase de formulario:
type
T F o r m l = class ( T F o r m )
public
nClicks: Integer;
end;
palabra clave p r o p e r t y delante de ella y a continuacion, pulsar Control-MayusC para activar la funcion C o d e Comple t ion.Delphi generara automaticamente
todo el codigo adicional necesario.
El codigo completo para esta clase de formulario esta disponible en el ejemplo
FormProp y la figura 2.3 muestra el resultado. El programa puede crear multiples
instancias del formulario (es decir, multiples objetos basados en la misma clase
de formulario), cada una con su propia cuenta de clic.
NOTA:~ i ~ ceg
s equo el a & d ~ p & i ~ d d a dpun f o n d ario, no 3.
~ del f a h~d ! del Qbject
e
B p e cs t o r .
Mia&
-~4'
a'la M a de ~
Esta es una costumbre muy comun en Delphi, pero no es una buena costumbre,
porque no ofrece encapsulado de la estructura de formulario ni de sus componentes. Si hay un codigo similar en una aplicacion y mas tarde se decide modificar la
interfaz de usuario del formulario (y reemplazar S t a t u s B a r por otro control o
activar diversos paneles), habra que adaptar el codigo en muchos sitios. La alternativa es utilizar un metodo o, incluso mejor, una propiedad para ocultar un
control concreto. Esta propiedad puede definirse como:
property StatusText: string read GetText write SetText;
demas formularios del programa simplemente se puede hacer referencia a la propiedad S t a t u s T e x t del formulario y si la interfaz de usuario cambia, solo se
veran afectados 10s metodos de lectura y escritura.
Constructores
Para asignar la memoria a1 objeto, podemos llamar a1 metodo C r e a t e . Este
es un constructor, un metodo especial que podemos aplicar a una clase para
asignar memoria a una instancia de dicha clase. El constructor devuelve la instancia, que puede asignarse a una variable para almacenar el objeto y usarlo mas
tarde. El constructor por defecto TObj e c t .C r e a t e inicializa todos 10s datos
del nuevo caso a cero. Para que 10s datos de dicho caso comiencen con un valor
diferente a cero, hay que escribir un constructor personalizado.
El nuevo constructor se puede denominar c r e a t e o tener otro nombre, y hay
que usar la palabra clave c o n s t r u c t o r delante de el. Fijese en que no es
necesario llamar a TOb j e c t .C r e a t e : es Delphi el que asigna memoria para el
nuevo objeto, no el constructor de clase. Todo lo que hay que hacer es iniciar la
base de clase.
Aunque se puede usar cualquier nombre para el constructor, deberia ajustarse
a1 nombre estandar, c r e a t e . Si se usa otro nombre distinto de c r e a t e , el
constructor C r e a t e de la clase basica TOb j e c t aun estara disponible, per0 un
programador que llame a1 constructor por defecto podria pasar por alto el codigo
de inicializacion ofrecido porque no reconoce el nombre.
A1 definir un constructor c r e a t e con algunos p a r h e t r o s , reemplazamos la
definicion predeterminada por una nueva y hacemos que su uso resulte obligatorio. Por ejemplo, despues de haber definido:
type
TDate = class
public
constructor Create
(y, m, d: I n t e g e r ) ;
ADay: TDate;
begin
// Error, no c o n p i l a :
A D a y : = TDate.Create;
// OK:
A D a y : = TDate.Create (1, 1, 2000);
ellos create.Este enfoque hace que 10s constructores resulten faciles de recordar y sigan una via estandar proporcionada por otros lenguajes de orientacion a
objetos en 10s que 10s constructores deben de tener todos el mismo nombre. Como
ejemplo, podemos aiiadir a la clase dos constructores create distintos; uno sin
parametros, que oculta el constructor predeterminado; y otro con valores de
inicializacion. El constructor sin parametros usa el valor predefinido de la fecha
de hoy (como se puede ver el codigo completo del ejemplo Dataview):
'=we
TDate = c l a s s
public
c o n s t r u c t o r Create; overload;
c o n s t r u c t o r C r e a t e ( y , m, d : I n t e g e r ) ; o v e r l o a d ;
objeto TDay
Asignacion de objetos
Podemos preguntarnos que ocurriria si una variable que mantiene un objeto
solo contiene una referencia a1 objeto en memoria y se copia el valor de dicha
variable. Supongamos que escribimos el metodo BtnToda yCli ck del ejemplo
ViewDa te del siguiente modo:
procedure TDateForm.BtnTodayClick(Sender:
var
NewDay: TDate;
begin
NewDay : = TDate-Create;
TheDay : = NewDay;
LabelDate.Caption : = TheDay-GetText;
end;
TObject);
objeto TDate
TheDay
(Button: TButton);
Button.Caption
end;
/ / llamar..
CaptionPlus
: = Button.Caption
' + I ;
.
(Buttonl)
Esto significa que el objeto se pasa por referencia sin el uso de la palabra clave
var y sin ninguna otra indicacion obvia de la semantica de paso por referencia,
lo que confunde a 10s novatos. Cabria preguntarse lo que sucede si realmente se
quieren cambiar 10s datos de un objeto existente, para que se corresponda con 10s
datos de otro objeto. En este caso, hay que copiar cada campo del objeto, lo cual
es posible solo si son todos publicos, u ofrecer un metodo especifico para copiar
10s datos internos. Algunas clases de la VCL tienen un metodo Assign, que
realiza esta operacion de copia. Para ser mas precisos, la mayoria de las clases de
la VCL que heredan de TPers is tent, per0 no de TComponent, tienen el
metodo Ass ign. Otras clases derivadas de TComponent tienen este metodo
per0 lanzaran una excepcion cuando se llama.
En el ejemplo Da t eCopy, se ha aiiadido un metodo Assign a la clase TDa te
y se le ha llamado desde el boton Today, con el siguiente codigo:
p r o c e d u r e TDate .Assign (Source: TDate) ;
begin
fDate : = Source.fDate;
end ;
p r o c e d u r e TDateForm.BtnTodayClick(Sender:
var
NewDay: TDate;
begin
NewDay : = T D a t e - C r e a t e ;
TheDay .Assign (NewDay);
LabelDate.Caption : = TheDay.GetText;
NewDay.Free;
end ;
TObject);
Objetos y memoria
La administracion de memoria en Delphi esta sujeta a tres normas, a1 menos si
se permite que el sistema trabaje en armonia sin violaciones de acceso y sin
consumir memoria innecesaria:
Todo objeto ha de ser creado antes de que pueda usarse
Todo objeto ha de ser destruido tras haberlo utilizado.
Todo objeto ha de ser destruido solo una vez.
El tener que realizar estas operaciones en el codigo o dejar que Delphi controle
la administracion de memoria, dependera del modelo que escojamos entre las
distintas tecnicas que ofrece Delphi.
NOTA: Podriamos preguntarnos por que se puede llamar a Free con total
seguridad si la referencia del objeto es n i l , pero no se pue& llamar a
D e s t r o y . La razon es que F r e e es un mbodo conocido en una posicibn
de memoria dada, rnientras que la funcion virtual Destroy se defrne en
tiempo de ejecucion a1 ver el tip0 de objeto, una operacibn muy peligrosa si
el objeto ya no existe.
Para resumir todo esto, hemos elaborado una lista de directrices:
Llamar siempre a F r e e para destruir objetos, en lugar de llamar a1 destructor D e s t r o y .
Utilizar F r e e A n d N i 1 o cambiar las referencias de objeto a n i 1 despues
de haber llamado a F r e e , a no ser que la referencia quede inmediatamente
despues fuera de alcance.
.. .
Fijese en que estas sentencias solo verifican si el puntero no es nil, no verifican si se trata de un puntero valido. Si se escribe el siguiente codigo, se realizara
la verificacion, per0 se obtendra un error en la linea de llamada a1 metodo del
objeto:
ToDestroy.Free;
i f ToDestroy <> n i l then
ToDestroy.DoSomething;
Esta definicion indica que la clase T F o r m l hereda todos 10s metodos, campos, propiedades y eventos de la clase T F o r m . Se puede llamar a cualquier metodo public0 de la clase T F o r m para un objeto del tipo T F o r m l . T F o r m , a su vez,
hereda algunos de sus metodos de otra clase, y asi sucesivamente hasta la clase
basica TOb j e c t . Como ejemplo de herencia, podemos cambiar una nueva clase
a partir de T D a t e y modificar su funcion G e t T e x t . Se puede encontrar este
codigo en la unidad Date del ejemplo NewDate:
tYPe
TNewDate = c l a s s (TDate)
pub1i c
f u n c t i o n GetText: string;
end :
Figura 2.6. El resultado del programa NewDate, con el nombre del mes y del dia de
acuerdo con la configuracion regional de Windows.
CCI;I
VaE
Ob'j: TTest;
.begin
Obj : = TTest . C r e a t e :
O b ] ProtectedData := 20;
/ / no v a
compiler
Obj: TTest;
begin
Ob j := TTest. Create;
TTestHack (Obj).ProtectedData := 20; // ;conpila!
----.---:IA:---
---A
biar sin que nos demos cuenta). En segundo lugar, la DBGrid es una vista
virtual de 10s datos. Cuando nos desplazamos hacia arriba en una DBGrid,
10s d a b s pueden moverse bajo ella, bero la fila seleccionada en ese momento podria no cambiar.
protegidos miembros de una clase) se describe normalmente como un hack
o apaKo y deberia evitarse siempre que sea posible. El problema no esth en
acceder a datos protegidos de una cIase en la misma unidad sino en declarar
.,.
.. .
.
una clase con el unico tin de acceder a datos protegldos de un ObJetO exlstente de una clase distinta. El peligro de esta tecnica esth en la conversion
de tipos codificada directamente de un objeto de una clase a otra diferente.
- .
--
- -
MyAnimal : T A n i m a l ;
MyDog: T D o g ;
begin
MyAnimal : = MyDog;
MyDog : = MyAnimal;
// E s t o es correcto
// ; E s t o es u n error!
-- -
.-
. .-
-.
.-
i-
[Y
de
lineas de c d i g o que la usan. Por supuesto, existe una condicion: las clases
ascendientes de la jerarquia ban de disekrse con mucho cuidado.
GI
GSGIILU IIIIICS
que tenga el mismo nombre. Un metodo definido como virtual, sigue manteniendo
el enlace posterior de cada subclase (a menos que se oculte con un metodo estatico, que resulta algo bastante alocado). No hay ningun mod0 de cambiar este
comportamiento, debido a la forma en que el compilador genera un codigo diferente para 10s metodos con enlace posterior.
Para redefinir un metodo estatico, hay que aiiadir un metodo a una subclase
que tenga 10s mismos parametros o parametros diferentes que el original, sin
ninguna otra especificacion.
Para sobrescribir un metodo virtual, habra que especificar 10s mismos
parametros y usar la palabra clave o v e r r i d e :
type
TMyClass = class
procedure One;
procedure Two;
end;
TMyDerivedClass =
procedure One;
procedure Two;
end;
virtual;
(metodo estdtico)
class (TMyClass)
override;
...
/ / llamada a 1 p r o c e d i m i e n t o M y C l a s s . O n e
inherited One ;
end;
. . 4
1 L
Manejadores de mensajes
Tambien se puede usar un metodo de enlace posterior para manejar un mensaje
de Windows, aunque la tecnica es algo distinta. Con este proposito, Delphi ofrece
otra directiva, message, para definir 10s metodos de control de 10s mensajes,
...
El nombre del procedimiento y el tip0 de 10s parametros dependen del programador, aunque esisten varios tipos de registros predefinidos para 10s diversos
mensajes de Windows. Podria generarse mas adelante este mensaje, invocando a1
metodo correspondiente, como en:
PostMessage (Form1.Handle, vm-User,
0, 0) ;
Esta tecnica puede resultar extremadamente util para un programador veterano de Windows, que lo sepa todo sobre 10s mensajes y las funciones de la API de
Windows. Tambien se puede enviar inmediatamente un mensaje mediante la llamada a la API de SendMessage o a1 metodo Perform de la VCL.
Metodos abstractos
La palabra clave abstract se usa para declarar metodos que se van a definir solo en subclases de la clase actual. La directiva abstract define por completo el metodo, no es una declaracion que se completara mas adelante. Si se
intenta definir el metodo, el compilador protestara. En Delphi se pueden crear
instancias de clases que tengan metodos abstractos. Sin embargo, a1 intentarlo, el
compilador de 32 bits de Delphi emite un mensaje de advertencia "Constrtrcting
instance of <class name> containing abstract methods" (Creando caso de +ombre de clase> que contiene metodos abstractos). Si se llama a un metodo abstract0
...
Esta misma operacion se puede realizar directarnente mediante el segundo operador RTTI, as, que convierte el objeto solo si la clase solicitada es compatible
con la real. Los parametros del operador as son un objeto y un tip0 de clase, y el
resultado es un objeto convertido a1 nuevo tipo de clase. Podemos escribir el
siguiente fragment0 de codigo:
MyDog : = MyAnimal a s TDog;
Text : = MyDog. Eat;
Ambos operadores RTTI resultan muy utiles en Delphi para escribir codigo
generico que se pueda usar con diversos componentes del mismo tipo o incluso de
distintos tipos. Cuando un componente se pasa como parametro a un metodo de
respuesta a un evento, se usa un tipo de datos generico (TOb j ect), por lo que
normalmente es necesario convertirlo de nuevo a1 tip0 de componente original:
procedure TForml.ButtonlClick(Sender:
begin
if Sender is TButton then
TObject);
...
end;
Uso de interfaces
Cuando se define una clase abstracta para representar la clase basica de una
jerarquia, se puede llegar a un punto en el que la clase abstracta sea tan abstracta
que so10 liste una serie de funciones virtuales, sin proporcionar ningtin tip0 de
implernentacion real. Este tip0 de clase puramente abstracta puede definirse tambien mediante una tecnica concreta, una interfaz. Por esta razon, nos referimos a
dichas clases como interfaces.
Tecnicamente, una interfaz no es una clase, aunque puede parecerlo, porque se
considera un elemento totalmente a parte con caracteristicas distintivas:
Los objetos de tipo interfaz dependen de un recuento de referencias y se
destruyen automaticamente cuando no hay mas referencias al objeto. Este
Es importante fijarse en que las interfaces soportan un modelo de programacion orientada a objetos ligeramente distinto a1 que soportan las clases. Las
interfaces ofrecen una implernentacion del polimorfismo menos restringida. El
polimorfismo de las referencias de objetos se basa en una rama especifica de una
jerarquia. El polimorfismo de interfaces funciona en toda una jerarquia. Ademas,
el modelo basado en interfaces es bastante potente. Las interfaces favorecen el
encapsulado y proporcionan una conexion mas flexible entre las clases que la
herencia. Hay que resaltar que 10s lenguajes orientados a objetos mas recientes,
de Java a C#, poseen el concept0 de interfaces. Veamos la sintaxis de la declaracion de una interfaz (que, por convencion, comienza con el caracter I):
type
ICanFly = interface
['{EAD9C4B4-ElC5-4CF4-9FAO-3B812C880A21]']
function Fly: s t r i n g ;
end;
--
--
- - - -- -
---
Cuando hayamos declarado una interfaz, se puede definir una clase que la
implemente, como en:
type
TAirplane = class (TInterfacedObject, ICanFly)
f u n c t i o n Fly: string;
end;
La RTL ya ofrece unas cuantas clases basicas para implementar el comportamiento fundamental que necesita la interfaz II n t e r f ace. Para 10s objetos internos, se usa la clase T I n t e r f acedOb j ect , utilizada en el codigo anterior.
Se pueden implementar mktodos de interfaz con metodos estiticos (como en el
codigo anterior) o con metodos virtuales. Se pueden sobrescribir mktodos virtuales
en subclases utilizando la directiva o v e r r i d e . Si no se usan metodos virtuales,
aun asi se puede ofrecer una nueva implementacion en la subclase, volviendo a
declarar el tipo de interfaz en la subclase y a enlazar 10s metodos de interfaz con
nuevas versiones de 10s metodos estaticos. A primera vista, el uso de metodos
virtuales para implementar interfaces parece permitir un codigo mas limpio en las
subclases, per0 ambos enfoques son igual de potentes y flexibles. Sin embargo, el
uso de metodos virtuales afecta a1 tamaiio del codigo y de la memoria necesaria.
I
NQTA:
mi-
Ahora que hemos definido una implementacion de las interfaces, podemos escribir algo de codigo para usar un objeto de esa clase, mediante una variable de
tipo interfaz:
var
Flyerl: ICanFly;
begin
Flyerl : = TAirplane.Create;
Flyerl.Fly;
end;
:=
T A i r p l a n e - C r e a t e as ICanFly;
--
en intediuxs, por lo
general deberiamos acceder a ellos s6io w n las variables da objeto o sblo
con las variables de interfaz. Si se m&+zlsahs dw ~~, el s h a de
recuenta de referencias de Del* se interrump y pue&.ariginar errores de
diffciles de 1-ar:
J3i la piktim, si
memoria que sean extre-entc
hemos decidido usar interfa~eer.probabkmeot~deberiamosiusa r f i w e n te variables basadas en inte&e$.' $i &d asi debcanps m e z w las vdrhbles, lo msls aconsejable es inhev6ilitar d reopanto de-re-fer&as-esd&do
m a clase base propia en lugar de usar T 1 n ter fa c e d ~ jbd ct .
. I
L-
,I 1-
programa ofrece uno, como suele suceder con las aplicaciones de Delphi), en
lugar de seguir la ruta estandar de ejecucion del programa. Asi que el autentico
problema no consiste en saber como detener una excepcion sin0 como ejecutar
codigo incluso aunque se lance una excepcion.
Consideremos esta seccion de codigo (parte del ejemplo TryFinally), que realiza algunas operaciones para las que emplea bastante tiempo y usa el cursor en
forma de reloj de arena para mostrar a1 usuario que esta haciendo algo:
Screen.Cursor : = crHourglass;
// g r a n a l g o r i t m o . . .
Screen.Cursor : = crDefault;
En caso de que se produzca un error en el algoritmo (corno el que se ha incluido a proposito en el ejemplo TryFinally), el programa se detendra, per0 no volvera a establecer el cursor predefinido. Es para esto para lo que sirve un bloque
try/f inally:
Screen.Cursor : = crHourglass;
try
// g r a n a l g o r i tmo . . .
finally
Screen.Cursor : = crDefault;
end ;
Cuando el programa ejecuta esta funcion, siempre reinicia el cursor, haya una
excepcion (de cualquier tipo) o no. Este codigo no controla la excepcion, simplemente hace que el programa sea robusto en caso de que se Cree un una excepcion.
Un bloque t r y puede ir seguido de una sentencia e x c e p t o f i n a l l y , per0 no
por ambas a1 mismo tiempo. La solucion mas comun para controlar tambien la
excepcion consiste en usar dos bloques t r y anidados. En ese caso, hay que
asociar el interno con una sentencia f i n a 11y y el externo con una sentencia
e x c e p t o viceversa, segun lo requiera la situacion. Aqui tiene el esquema del
codigo para el tercer boton del ejemplo T r y F i n a l l y :
Screen.Cursor : = crHourglass;
try try
/ / g r a n a l g o r i tmo . . .
finally
Screen.Cursor : = crDefault;
end;
except
.
on E: EDivByZero do
end;
..
Cada vez que haya algun codigo de finalizacion a1 concluir un metodo, hay que
situar dicho codigo en un bloque f i n a l l y . Siempre se deberia, invariablemente
y de forma continuada proteger el codigo con sentencias f i n a l l y , para evitar
problemas de recursos o de goteos de memoria en caso de que se Cree una excepcion.
.-
----
--
. .
..
...
Clases de excepciones
En las sentencias de control de escepciones mostradas anteriormente, captamos la excepcion EDivBy Zero, que define el RTL de Delphi. Otras excepciones como esta se refieren a problemas en tiempo de ejecucion (como una conversion
dinamica erronea), problemas de recursos de Windows (como 10s errores por falta
de memoria), o errores de componentes (como un indexado erroneo). Los programadores pueden definir tambien sus propias excepciones. Se puede crear una
nueva subclase de escepciones predefinidas o de una de sus subclases:
type
EArrayFull
class
(Exception) ;
Cuando se aiiade un nuevo elemento a una matriz que ya esta llena (probablemente por un error en la Iogica del programa), se puede establecer la excepcion
correspondiente, creando un objeto de esa clase:
if MyArray.Ful1 then
r a i s e EArrayFull .Create
'Ma t r i z l l e n a ')
try
// e r r o r s i B e s i g u a l a 0
Result := A d i v B;
// h a c e o t r a c o s a .
o b v i a r s i s e c r e a una e x c e p c i o n
Result : = Result d i v B;
Result : = Result + 1;
except
on EDivByZero do
..
begin
Result : = 0;
MessageDlg ('Dividir por cero corregido.', mtError,
[ m b O K l , 0);
end ;
on E : Exception do
begin
Result : = 0 ;
MessageDlg (E.Message, mtError, [mbOK] , 0 ) ;
end;
end; / / finaliza except
end;
'VCLEAbort Exceptions
lndy EIDConnCbsedG~acelul?yEnceplm
Registro de errores
La mayor parte del tiempo, no se sabe que operacion va a crear una excepcion
y no se puede (ni se debe) envolver cada una de las partes del codigo en un bloque
try/except.La tecnica general consiste en dejar que Delphi controle todas las
escepciones y finalmente pasarselas todas a1 usuario, mediante el control del
evento OnException del objeto global Application. Esto se puede hacer
de un mod0 mas sencillo con el componente ApplicationEvents. En el
ejemplo ErrorLog, se ha aiiadido a1 formulario principal una copia del componente Appl icat ionEvent s y un controlador para su evento OnExcept ion:
var
Filename: string;
LogFile : TextFile;
begin
// prepara un a r c h i v o de r e g i s t r o
Filename : = ChangeFileExt (Application.Exename, ' . l o g 1 ) ;
AssignFile (LogFile, Filename) ;
i f FileExists (FileName) then
Append (LogFile) // abre un a r c h i v o e x i s t e n t e
else
Rewrite (LogFile); // c r e a r uno nuevo
tr~
// e s c r i b e e n u n a r c h i v o y m o s t r a r e r r o r
Writeln (LogFile, DateTimeToStr (Now) + ' : ' + E-Message);
i f not CheckBoxSi1ent.Checked then
Application. ShowException (E);
finally
// cierra e l archivo
CloseFile (LogFile);
end:
--
. - -.
- .- -- - --- NOTA: El ejemplo E r r o r h g usa el soporte de archivos de texto que pro-
. .
.
.-
.-
..
--
~~
porciona el tradicional t i p de datos Turbo Pascal TextFile. Se puede asignar una variable de archivo de texto a un archivo real y despues leerlo o
escribirlo.
En el controlador de excepciones global, se puede escribir en el registro, por
ejemplo, la fecha y hora del evento y tambien decidir si mostrar la excepcion
como suele hacer Delphi (ejecutando el metodo ShowException de la clase
TApplicat ion). De hecho, Delphi ejecuta ShowExcept ion de manera predeterminada solo si no hay instalado un controlador OnException. La figura
2.8muestra el programa ErrorLog en ejecucion y una excepcion de muestra abierta
en ConTEXT (una practico editor para programadores incluido con Delphi y
disponible en w~vw.fixedsys.com/context).
Referencias de clase
La ultima caracteristica del lenguaje que trataremos en este capitulo son las
referencias de clase, lo cual implica la idea de manipular las propias clases dentro
del codigo. El primer punto que hemos de tener en cuenta es que la referencia de
clase no es un objeto; es sencillamente una referencia a un tipo de clase. Un tipo
de referencia de clase establece el tip0 de una variable de referencia de clase.
Aunque esto suene confuso, con unas cuantas lineas de codigo quedara un poco
mas claro.
.-.
-
Div by 0
........
119 - -
- .-
17/05/2003
17/05/2003
7/05/2003
7/05/2003
7/05/2003
7/05/2003
..............
......................
ll:37:48:Divisiun bv zero
ll:37:53: raise button pressed
11:37:56:Divislon b y zero
ll:37:58:raise button pressed
ll:37:59:raise button pressed
11:38:00:raise button pressed
I
I
: = AC1assRef.Create;
En concreto, el tipo de referencia de clase TC la s s se puede usar para almacenar una referencia de cualquier clase que se escriba en Delphi, porque toda clase
se deriva en ultimo termino de TOb j ec t . La referencia T FormClas s, en cambio, se usa en el codigo fuente de la mayoria de 10s proyectos Delphi. El metodo
Create Form del objeto Appl i cat ion, en realidad, requiere como parametro
la clase del formulario que va a crear:
Application. CreateForm(TForm1,
Forml) ;
referencia de clase, declarado como C l a s s R e f : T C o n t rolclass. Almacena un nuevo tipo de datos cada vez que el usuario hace clic sobre uno de 10s tres
botones de radio, con asignaciones como C l a s s R e f := T E d i t . La parte
interesante del codigo se ejecuta cuando el usuario hace clic sobre el formulario.
Hemos escogido de nuevo el evento O n M o u s e D o w n del formulario para tener
acceso a la posicion del cursor del raton:
procedure TForml.FormMouseDown(Sender:
TMouseButton;
Shift: TShiftState; X, Y: Integer) ;
TObject;
Button:
var
NewCtrl: TControl;
MyName: String;
begin
// crea e l control
NewCtrl := ClassRef-Create (Self);
/ / l o o c u l t a t e m p o r a l m e n t e , para e v i t a r e l parpadeo
NewCtrl.Visible : = False;
// d e f i n e padre y p o s i c i d n
NewCtrl Parent := Self;
NewCtrl-Left := X;
NewCtrl.Top : = Y;
/ / c a l c u l a e l nombre u n i c o ( y e l t i t u l o )
Inc (Counter);
MyName : = ClassRef .ClassName + IntToStr (Counter);
Delete (MyName, 1, 1);
NewCtrl.Name : = MyName;
// l o rnuestra ahora
NewCtrl.Visible : = True;
end ;
La primera linea del codigo de este metodo es la clave. Crea un nuevo objeto
del tipo de datos de clase almacenados en el campo C l a s s R e f . Esto se consigue
simplemente aplicando el constructor c r e a t e a la referencia de clase. Ahora se
puede establecer el valor de la propiedad P a r e n t , fijar la posicion del nuevo
componente, darle un nombre (que se usa tambien automaticamente como
C a p t i o n o T e x t ) y hacerlo visible.
biblioteca
en tiempo
D
AD
Dephi 5 necesite ntilizar
nueva unidad Variants para volver a compiesta
i.
7--o--
------ -- r-Y
te posible en programas que utilizan exclusivamente llamadas de bajo nivel. Se pueden enwntrar ambas versiones en el c6digo fuente del archivo de
ejemplo.
Fijese, de todos modos, en que las decisiones de este tip^ siempre implican
una sene de concesiones. A1 eliminar el encabezamientode variantes de las
aplicaciones Delphi qure no las usan, por ejemplo, Borland ha aiiadido una
carga extra a ias aplicaciones que si lo h a m . La ventaja real de esta operacibn, sin embargo, estA en el reducido tamafio en memoria que necesitan las
aplicaciones Delphi que no usan variantes, como consecuencia de no tener
que introducir varios megabytes debido a las bibliotecas de sistema Ole2.
Lo realmente importante, en mi opini&n,es el tamaiio de las grandes aplicaciones Delphi basadas en paquetes en tiempo de ejecuci6n. Una sencilla
prueba con un programa que no hace nada, el ejemplo Minipack, muestra
un
ejecutable de 17.408 bytes.
En 10s siguientes apartados encontrara una lista de las unidades de la RTL en
Delphi, asi como de todas las unidades disponibles (con el codigo fuente completo) que se encuentran en el subdirectorio Source\Rtl\Sys del directorio Delphi
y algunas de las disponibles en el subdirectorio Source\Rtl\Common. Este
segundo directorio contiene el codigo fuente de las unidades que conforman el
nuevo paquete de la RTL, que engloba tanto la biblioteca basada en funciones
como las clases centrales comentadas m h adelante.
que indica aplicaciones de consola; I s M u l t i T h r e a d , que indica si esisten hilos de proceso secundarios; y la cadena de la linea de comandos
CmdLine. (La unidad incluye tambien P a r a m c o u n t y P a r a m S t r para
poder acceder mas facilmente a 10s parametros de la linea de comandos.)
Algunas de estas variables son especificas de la plataforma Windows, otras
estan tambien disponibles en Linux, mientras que otras son especificas de
Linux.
El codigo de soporte de hilos de proceso (threads), con las funciones
B e g i n T h r e a d y E n d T h r e a d ; registros de soporte de archivos y rutinas relacionadas con archivos; rutinas de conversion de cadenas anchas y
cadenas OLE; asi como muchas otras rutinas de sistema y de bajo nivel
(junto con una serie de funciones de conversion automaticas).
La unidad que acompaiia a System, denominada SysInit, incluye el codigo de
inicializacion, con funciones que rara vez se utilizaran directamente. Esta es otra
unidad que siempre se incluye de forma implicita, puesto que la unidad System
hace uso de ella.
VER-PLATFORM-WIN32-NT:
end:
Otra caracteristica poco conocida de esta unidad es la clase T M u l t i R e a d ExclusiveWriteSynchronizer (probablemente la clase VCL de nombre
mas largo). Borland ha definido un alias para la clase, que es mucho mas corto:
TMREWSync (ambas clases son identicas). Esta clase soporta multithreading:
permite trabajar con recursos que pueden usar diversos threads a1 mismo tiempo
para leer (multilectura), pero que a1 escribir han de utilizar un unico thread (escritura exclusiva). Esto significa que no se puede comenzar a escribir hasta que
todos 10s threads de lectura hayan terminado su labor.
La implementation de la clase TMultiReadExclusiveWriteSync h r o n i z e r se ha actualizado en Delphi 7, pero mejoras similares estan disponibles en forma de un parche que aparecio tras la segunda actualizacion de Delphi
6. La nueva version de la clase esta mas optimizada y menos sujeta a bloqueos,
que suelen ser un problema habitual del codigo de sincronizacion.
-
.a
'FALSE' p o r d e f e c t o
La funcion inversa es S t r T o B o o l , que puede convertir una cadena que contenga uno de 10s valores de las dos matrices booleanas mencionadas anteriormente o un valor numerico. En este ultimo caso, el resultado sera verdadero si el valor
numerico es distinto de cero. Se puede ver una sencilla demostracion del uso de
las funciones de conversion booleanas en el ejemplo StrDemo. Otras funciones
aiiadidas a SysUtils estan relacionadas con las conversiones de coma flotante en
Por ultimo, en las ultimas versiones se han aiiadido algunas funciones mas de
soporte para varias plataformas. La funcion Ad j us tLineBrea ks puede reahzar ahora diferentes tipos de ajustes en las secuencias de retorno de carro y de
avance de linea, y se han introducido nuevas variables globales para archivos de
texto en la unidad System. La funcion Fi 1eCrea te tiene una version sobrecargada en la que se pueden especificar derechos de acceso a archivos a la manera
Unix. La funcion ExpandFi 1eName puede localizar archivos (en sistemas de
archivos que distinguen entre mayusculas y minusculas), incluso cuando su tipografia no se corresponde exactamente. Las funciones relacionadas con 10s
delimitadores de ruta (barra inversa o barra oblicua) son ahora mas genericas que
en las versiones precedentes de Delphi, por lo que se les han asignado nombres
nuevos de acuerdo con ello. (Por ejemplo, la vieja funcion I ncludeTralingBackslash ahora es mas conocida como IncludingTrailingPathDelimiter).
Ya que hablamos de archivos, Delphi 7 aiiade a la unidad SysUtils la funcion
Get Fi levers ion,que lee el numero de version a partir de la informacion de
version que se aiiade opcionalmente a un archivo ejecutable de Windows (que es
por lo que esta funcion no funcionara sobre Linux).
La unidad Math
La unidad Math (matematica) csta compuesta por un conjunto de funciones
matcmaticas: unas cuarenta funciones trigonomdtricas, funciones logaritmicas y
esponenciales, funciones de redondeo, evaluaciones polinomicas; casi treinta funciones estadisticas y una doccna dc funciones economicas.
Describir todas estas funcioncs seria bastante aburrido, aunquc algunos lectores probablementc se cncuentren muy intcresados en las capacidadcs matematicas
dc Delphi. Es por esto, que hemos decidido centrarnos en las funciones matcmaticas prescntadas en las illtimas vcrsiones de Delphi (en particular Delphi 6) y
tratar dcspues un tema espccifico que suele confundir a 10s programadorcs dc
Delphi, el redondeo.
Veamos algunas dc las funcioncs matematicas mas nuevas.
? : del lenguaje C/
C++,que es muy util porque permite reemplazar una sentencia completa
// a c t u a s o l o s i e l v a l o r e s t d e n t r e e l m i n y e l max
if InRange (value, min, max) then
/ / s e a s e g u r a q u e e l v a l o r e s t d e n t r e min y m x
value : = EnsureRange (value, min, m a x ) ;
Otro grupo muy util de funciones esta relacionado con las comparaciones. Los
numeros de coma flotante son basicamente inexactos. Un numero de coma flotantc cs una aproximacion de un valor real teorico. Cuando realizamos operaciones
matematicas con numeros de coma flotante, la inesactitud de 10s valores originales se acumula en 10s resultados. Si multiplicamos y dividimos por el mismo
numero puede que no consigamos exactamente el numero original, sino uno muy
proximo a 121. La funcion samevalue permite verificar si dos valores se aproximan lo suficiente como para ser considerados iguales. Se puede especificar el
grado de aproximacion que deberian tener dos numeros o dejar que Delphi calcule
un rango de error razonable para la representacion que estamos utilizando. (Por
csta razon se sobrecarga la funcion.)
Del mismo modo, la funcion Iszero compara un numero con cero, mediante
esta misma "logica borrosa".
La funcion C o m p a r e v a l u e usa la misma norma para 10s numeros de coma
flotante per0 esta disponible tambien para enteros. Devuelve una de las tres constantes LessThanValue, EqualsValue y GreaterThanValue (que se
corresponden con -1,O y 1). Del mismo modo, la nueva funcion sign devuelve
-1,0 y 1 para indicar un valor negativo, cero o un valor positivo.
La funcion D i v M o d es equivalente a las operaciones de division y resto, devolviendo el resultado de la division del entero y del resto al mismo tiempo La
funcion RoundTo nos permite especificar el digito de redondeo (permite, por
ejemplo, redondear hasta el millar mas proximo o hasta dos decimales):
RoundTo
RoundTo
(123827, 3 ) ;
(12.3827,
-2);
// e l r e s u l t a d o e s 1 2 4 . 0 0 0
// e l r e s u l t a d o e s 1 2 , 3 8
. .
ADVERTENCIA: Fijese en que la funci6n RoundTo usa un nhnero positivo para indicar la potencia de diez h a s h la que hay que redondear (por
ejemplo, 2 para centenas) o un n6mero negativo para el numero de cifras
decimales. Esto es exactamente lo contrario de la funci6n Round utilizada
por hojas de calculo como Excel.
TambiCn ha habido algunos cambios en las operaciones de redondeo estandar
de la funcion Round: ahora, se puede controlar el mod0 en que la FPU (la Unidad
de Coma Flotante de la CPU) realiza el redondeo llamando a la funcion
SetRoundMode. Existen tambien funciones de control del mod0 de precision de
la FPU y sus excepciones.
El programa tambidn utiliza otro tipo de redondeo proporcionado por la unidad Math mediante la funcion SimpleRoundTo, que utiliza un redondeo aritmktico asimetrico.
En este caso, todos 10s numeros ,5 se redondean a1 valor superior. Sin embargo, tal y como se recalca en el ejemplo de redondeo, la funcion no actua como se
esperaria cuando se redondea hasta un digito decimal (es decir, cuando se pasa un
segundo parametro negativo). En este caso, debido a 10s errores de representacion
de 10s numeros de coma flotante, el redondeo recorta 10s valores; por ejemplo
convierte 1,15 en 1,l en lugar del esperado 1.2.
La solucion es multiplicar el valor por diez antes de redondear, redondearlos
hasta cero digitos decimales, y despues dividirlo, como se muestra a continuacion:
(SimpleRoundTo
d *10
0 ) / 10 )
a
,
La unidad DateUtils
La unidad DateUtils es una nueva coleccion de funciones relacionadas con la
fecha y la hora. Engloba nuevas funciones para seleccionar valores de una variable TDa t e T i m e o contar valores de un intervalo dado como:
// e s c o g e r v a l o r
function DayOf (const AValue : TDateTime) : Word;
function HourOf (const AValue : TDateTime) : Word;
/ / v a l o r en r a n g o
function WeekOf Year (const AValue : TDateTime) : Integer;
function HourOfWeek (const AValue: TDateTime) : Integer;
function SecondOfHour (const AValue: TDateTime) : Integer;
Algunas de estas funciones son bastante extraiias, c o m o M i l l i S e c o n d O f M o n t h o S e c o n d o f w e e k , pero 10s desarrolladores de Borland han decidido
suministrar un con.junto de funciones complete, sin importar lo poco practicas
que parezcan. (Realmente he utilizado algunas de estas funciones en mis e.jemplos.)
Existen funciones para calcular el valor final o inicial de un intervalo de tiempo dado (dia, semana, mes, aiio) como la fecha actual y para verificacion del
rango y consultas. Por e.jemplo:
function DaysBetween (const ANow, AThen: TDateTime) : Integer;
function WithinPastDays(const ANow, AThen: TDateTime;
const ADays: Integer) : Boolean;
Otras funciones abarcan el increment0 y decrement0 por parte de cada intervalo de tiempo posible, codificando y "recodificando" (reemplazando un elemento
del valor T D a t e T i m e , como el dia, por uno nuevo) y realizando comparaciones
"borrosas" (comparaciones aproximadas en las que una diferencia de una milesima de segundo haria que dos fechas fuesen iguales).
En general, DateUtils resulta bastante interesante y no es excesivamente dificil
de utilizar.
La unidad StrUtils
La unidad StrUtils es una nucva unidad presentada en Delphi 6 con algunas
nucvas funciones relacionadas con cadenas. Una de las caractcristicas clave dc
csta unidad es la existencia de muchas funcioncs de comparacion de cadenas. Hay
funciones basadas en un algoritmo "soundex" ( A n s i R e s e m b l e T e x t ) ; y algunas que ofrecen la capacidad de realizar busquedas en matrices de cadenas
(Ans iMatc h T e x t y Ans i I n d e x T e x t ) , localizar y sustituir subcadenas (como
3-
-..-L-_
L-
l-L-_
..-A
A_-_
TForml.ButtonMatchesClick(Sender: TObject);
De Pos a PosEx
Delphi 7 aporta su granito de arena a la unidad StrUtils. La nueva funcion
PO sE X resultara muy practica para muchos desarrolladores y merece que hable-
original (que supone una ligera perdida de tiempo). Por eso. el codigo anterior
puedc simplificarsc como:
function C o u n t S u b s t r ( t e x t , s u b : s t r i n g ) : I n t e g e r ;
var
nPos : I n t e g e r ;
begin
R e s u l t : = 0:
n p o s : = PosEx ( s u b , t e x t , 1 ) ; // predeterminado
while nPos > 0 do
begin
Inc ( R e s u l t );
n p o s := PosEx ( s u b , t e x t , nPos + L e n g t h ( s u b ) )
end ;
end ;
La unidad Types
La unidad Types (de tipos) almacena tipos de datos comunes a diversos sistemas operativos. En las anteriores versiones de Delphi, la unidad de Windows
definia 10s mismos tipos; ahora se han desplazado a esta unidad comun, compartida por Delphi y Kylix. Los tipos definidos aqui son sencillos y engloban, entre
otros. las estructuras de registro TPoint,T R e c t y TSmallPoint mas sus
tipos de punter0 relacionados.
usa las API dcl sistcma para manipular datos de variantcs: cn Kylis usa codigo
particularizado quc Ic proporciona la biblioteca RTL.
Como ejemplo dc un tip0 de variante personalizada, Delphi aporta una interesante definicion para 10s numeros complejos en la unidad VarCmplx (disponibles en formato de codigo fuente en el directorio Rtl\Common). Se pueden crear
variantes complejas utilizando una de las funciones sobrecargadas VarcomplexCreate y usarlas en cualquier espresion, como se demuestra en el siguiente
fragment0 de codigo:
var
vl, v2: Variant;
begin
vl : = VarComplexCreate (10, 1 2 ) ;
v 2 : = VarComplexCreate (10, 1) ;
ShowMessage (vl + v2 + 5) ;
Los numeros complejos se definen en realidad utilizando clases, per0 la superficie quc adoptan es la de variantes, mediante la herencia de una nueva clase de la
clase TCus tomVar iantT ype (definida en la unidad Variants), sobrescritura
de una serie de funciones abstractas virtuales y creacion de un objeto global que
se encarga del registro dentro del sistema.
Ademas de estas definiciones internas, la unidad incluye una larga lista de
rutinas para operar con variantes, como las operaciones matematicas y
trigonometricas.
l e n t o v i s t i c a s be orlentaci6n a 04et.o~y se
ha de escribir un codigo bastante mas complejo.
La unidad DelphiMM define una bibliotcca de administrador de mcmoria altcrnativa para utilizarla a1 pasar cadenas de un ejecutable a una DLL (una bibliotcca de enlace dinamico de Windows), ambas construidas en Delphi. Esta biblioteca
dc administrador de memoria se encuentra compilada de manera predeterminada
cn el archivo de biblioteca Borlndmrn. dl1 que habra que distribuirjunto con el
programa.
La interfaz de este administrador de memoria se define en la unidad ShareMem.
Esta es la unidad que se habra de incluir (es obligatoria como primera unidad) en
10s proyectos del Gecutablc y de la biblioteca (bbibliotecas).
_LC---
Convertir datos
Delphi incluye un nuevo motor de conversion, definido en la unidad ConvUtils.
El motor por si mismo no incluye definicion alguna de las unidades de medida
reales; en cambio, posee una serie de funciones principales para 10s usuarios
--
Eamiks
D~stance
lvper
LghlYeats
Parsecs
Fathom
Furbngs
Hands
Paces
Snnple Ted
Base lype:
Cmlirnetus
&&:
Qerlinalica Type
Cmvwted Am&
1100
Cham
tad0 en el cuarto cuadro de edicion, que esta en gris. En caso de errores, aparecera el mensaje correspondiente en el mismo cuadro. Veamos el codigo:
procedure TForml.DoConvert(Sender: TObject);
var
BaseType, DestType: TConvType;
begin
// o b t i e n e y v e r i f i c a e l t i p o b d s i c o
i f not DescriptionToConvType(CurrFamily, E d i t T y p e - T e x t ,
BaseType) then
EditType.Font.Color : = clRed
else
EditType.Font.Color : = clBlack;
// o b t i e n e y v e r i f i c a e l t i p o d e d e s t i n o
i f not DescriptionToConvType (CurrFamily,
EditDestination-Text,
DestType) then
EditDestination.Font.Color : = c l R e d
else
EditDestination.Font.Co1or : = clBlack;
if
Si todo esto no resulta interesante, hay que tener en cuenta que todos 10s tipos
de conversion proporcionados en el ejemplo son solo una muestra: se puede personalizar completamente el motor para que proporcione las unidades de medida en
que se este interesado, como se comentara a continuacion.
iConversiones de divisas?
La conversion de divisas no es exactamente lo mismo que la conversion de
unidades de medida, ya que 10s valores de las divisas cambian constantemente. En
teoria, se puede registrar un valor de cambio en el motor de conversion de Delphi.
De vez en cuando, se comprobara el nuevo indice de cambio, se desregistrara la
conversion ya existente y se registrara la nueva. Sin embargo, mantener la tasa de
cambio real implica modificar la conversion tan a menudo que la operacion podria no tener mucho sentido. Ademas, habra que triangular conversiones: hay que
definir una unidad base (probablemente el euro, si vive en Europa) y convertir a/
y desde esta divisa incluso si la conversion se realiza entre dos divisas distintas.
Por ejemplo, antes de la adopcion del euro como divisa de la Union Europea, lo
mejor era utilizar esta divisa como base para las conversiones entre las divisas de
10s estados miembros, por dos motivos. En primer lugar, 10s tipos de cambio eran
fijos. En segundo lugar, la conversion entre las divisas euro se rcalizaban legalmente convirtiendo una cantidad a euros y convirtiendo dcspues esa cantidad en
euros a la otra divisa. el comportamiento exacto del motor de conversion de Delphi.
Existe un pequeiio problema: debcria aplicarse un algoritmo de redondco en cada
paso de la conversion.
Considerarcmos este problema mas adelante, tras ofrccer el codigo base para
integrar las divisas euro con cl motor de conversion de Delphi.
NOTA: El ejemplo Conver I t disponible entre 10s ejemplos Delphi ofrece soporte para las conversiones a1 euro, utilizando un enfoque de redondeo
ligeramente distintos, aunque no tan precis0 como el requerido por las reglas de conversion de divisas europeas. Aun asi, resulta aconsejable mantener este ejemplo porque es bastante instructive en relacion con la creaci6n
de un nuevo sistema de medida.
El ejemplo, llamado EuroConv,muestra como registrar cualquier nueva unidad de medida con el motor. Siguiendo la plantilla que proporciona la unidad
S tdConvs creamos una nueva unidad (llamada EuroConvCons t). En la seccion de la interfaz, declaramos las variables para la familia y las unidades especificas:
interface
var
// U n i d a d e s d e C o n v e r s i o n d e D i v i s a s E u r o p e a s
c b E u r o C u r r e n c y : TConvFamily;
cuEUR: TConvType;
cuDEM: TConvType;
cuESP: TConvType;
cuFRF: TConvType;
// y e l r e s t o .
. .
// A l e m a n i a
// E s p a f i a
// P r a n c i a
initialization
/ / T i p o de l a familia de divisas europeas
cbEuroCurrency : = RegisterConversionFamily ( ' D i v i s d s E u r o ) ;
c u E U R : = RegisterConversionType(
cbEuroCurrency, 'EUR', 1) ;
c u D E M : = RegisterConversionType(
cbEuroCurrency, IDEM', 1 / DEMPerEuros) ;
c u E S P : = RegisterConversionType(
cbEuroCurrency, 'ESP', 1 / E S P P e r E u r o s ) ;
c u F R F : = RegisterConversionType(
cbEuroCurrency, ' F R F ', I / FRFPerEuros) ;
( 1 2 0 , cuDEM, cuITL)
El programa de ejemplo hace algo mas: ofrece dos cajas de lista con las divisas
disponibles, extraidas como en el ejemplo anterior, y cajas de edicion para el
valor de entrada y el resultado final. La figura 3.4 muestra el formulario.
Belg~anFrancs [BEF)
Dutch Gu~lders[NLG]
Austrian Sch~ll~ngi
[ATS]
Poituguese Escudos [PTE)
F~nn~sh
Marks [FIM]
G~eekDrachms [GRD]
Luembou'g F'-f
[LUFI
Cmml
Figura 3.4. La salida del ejemplo EuroConv, que muestra el uso del motor de
conversion de Delphi con una unidad de medida personalizada.
El programa funciona bien per0 no perfectamente, ya que no se aplica el redondeo correcto; deberia redondearsc no solo el resultado final de la conversion
sin0 tambien el valor intermedio. Mediante el motor de conversion no se puede
realizar este redondeo directamente de manera sencilla. El motor permite ofrecer
una funcion de conversion o una tasa de conversion particularizadas. Pero escribir funciones de conversion identicas para todas las divisas parece una mala idea,
asi que hemos escogido un camino diferente. (Puede ver ejemplos de funciones de
conversion personalizadas en la unidad StdCo nvs, en la seccion relacionada
con las temperaturas.)
En el ejemplo EuroConv,aiiadimos a la unidad con las tasas de conversion
una funcion EuroConv personalizada que realiza la conversion correcta. Llamando simplemente esta funcion en lugar de la funcion Convert estandar conseguiremos el efecto deseado (y no parece existir ningun inconveniente, ya que en
este tip0 de programas es extraiio mezclar divisas con distancias o temperaturas).
De manera alternativa, podriamos haber heredado una nueva clase a partir de
TCo nvT ype Fact o r, proporcionando una nueva version de 10s metodos
FromCommon y Tocommon; o haber utilizado la version sobrecargada de
Regi sterconversionType que acepta estas dos funciones como parametros.
Sin embargo, ninguna de estas tecnicas habria permitido enfrentarse a casos especiales, como la conversion de una divisa a si misma.
Este es el codigo de la funcion EuroConv, que utiliza la funcion interna EuroRound para redondear a1 numero de digitos especificado en el parametro Decimals (que debe estar entre 3 y 6, de acuerdo con las reglas oficiales):
type
TEuroDecimals
3. . 6 ;
D \md7code\O2\ClassRel\ClassRel dpr
D \md7cude\02\C1ealeComps\CreateComps
dp~
~:\rnd7code\02\~ale~rdp\~ale~rd~d~
D:W7code\U2\Dalesl\Datesl dpr
D-\md7code\OZ\DBGridCol\DBG11dCol,dpr
D \rnd7coJe\02\Erro1Log\ErrorLog dpf
D \md7code\02\Excepbun1\Excepbornl.dpr
D.\md7wde\O2\F~mPfopU01mProp.dp
D:\md7code\02\1IDrecl1veUID~ecl1ve.dp1
D:Lnd7code\02\lnllDerno\lnllDemo dpr
D hd7code\02\NewDale\NewDale d p ~
D \md7c~de\02\Polu4n1mals\Polu9nmals.&r
faDirectory then
end ;
Finalmente, el programa utiliza una interesante tecnica para solicitar a1 usuario que seleccione el directorio inicial para la busqueda de archivos, mediante una
llamada a1 procedimiento S e l e c t D i r e c t o r y . (Vease la figura 3.6.)
i f SelectDirectory
then . . .
I ,
CurrentDir)
La clase TObject
La definicion de la clase T O b j e c t es un elemento clave de la unidad System,
"la madre de todas las clases Delphi". Cada clase del sistema es una subclase de la
clase TObj e c t , directa (si se especifica TOb j e c t como la clase base), implicitamente (a1 no indicarse la clase base), o indirectamente (cuando se especifica
otra clase como antecesor). Toda la jerarquia de las clases de un programa en
Pascal orientado a objetos posee una raiz unica. Esta permite usar el tipo de datos
TOb j e c t como substituto del tipo de datos de cualquier tipo de clase del sistema.
Por ejemplo, 10s controladores de eventos de componentes normalmente tienen
un parametro Sender de tipo TObj e c t . Esto significa sencillamente que el
objeto Sender puede pertenecer a cualquier clase, puesto que cada clase se
deriva en ultima instancia de Tob j e c t . El inconveniente mas habitual de esta
tecnica es que para trabajar sobre el objeto, es necesario conocer su tipo de datos.
De hecho, cuando se tiene una variable o un parametro del tipo TObj e c t , se le
pueden aplicar solo 10s metodos y propiedades definidas por la propia clase
TOb j e c t . Si esta variable o parametro se refiere por casualidad a un objeto del
tipo TButton, por ejemplo, no se puede acceder directamente a su propiedad
C a p t i o n . La solucion a este problema recae en el uso de 10s operadores de
conversion segura de tipos siguientes o en 10s operadores de informacion de tip0
en tiempo de ejecucion (RTTI) (10s operadores i s y as).
Existe otra tecnica. Para cualquier objeto, se puede llamar a 10s metodos definidos en la misma clase TObj e c t . Por ejemplo, el metodo ClassName devuelve una cadena con el nombre de la clase. Debido a que es un metodo de clase, se
puede aplicar tanto a un objeto como a una clase. Supongamos que hemos definido una clase TButton y un objeto B u t t o n 1 de dicha clase. En ese caso, las
siguientes sentencias tendran el mismo efecto:
Text
Text
:=
:=
Button1.ClassName;
TButton.ClassName;
Hay ocasiones en las que es necesario usar el nombre de una clase, per0 tambien puede ser util recuperar una referencia de clase a la propia clase o a su clase
basica. La referencia de clase, de hecho, permite trabajar en la clase en tiempo de
ejecucion, mientras quc cl nombre de clase es simplemente una cadena. Podemos
obtener estas referencias de clase con 10s metodos C l a s sType y C l a s s Parent.
El primer0 devuelve una referencia de clase a la clase del objeto, el segundo a su
clase basica. Cuando tengamos una referencia de clase, podemos aplicarle cualquier metodo de clase TOb j e c t , por ejemplo, para llamar a1 metodo ClassName.
Otro metodo que podria resultar util es I n s t a n c e s i z e , que devuelve el
tamaiio en tiempo de ejecucion de un objeto. Aunque se podria pensar que la
funcion global S i z e o f ofrece dicha informacion, esa funcion en realidad devuelve el tamaiio de una referencia al objeto (un punter0 que siempre tiene cuatro
bytes), en lugar del tamaiio del objeto en si.
En el listado 3.1, se puede encontrar la definicion completa de la clase
TOb j e c t , extraida de la unidad System. Ademas de 10s metodos mencionados,
fijese en que I n h e r i t s From proporciona una comprobacion muy similar a la
del operador is, pero que se puede aplicar tambien a clases y referencias de clase
(mientras el primer argument0 dc i s habra dc ser un objeto).
Listado 3.1. La definicion de la clase TObject (en la unidad System de la RTL).
type
TObject = class
constructor Create;
procedure Free;
class function Init Instance (Instance: Pointer) : TObj ect;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(
const Name: string): Boolean;
class function Classparent: TClass;
class function ClassInfo: Pointer;
class function Instancesize: Longint;
class function InheritsFrom(AC1ass: TClass) : Boolean;
class function MethodAddress (const Name : ShortString) :
Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress (const Name: ShortString) : Pointer;
function GetInterface (const IID: TGU1D;out Obj) : Boolean;
class function GetInterfaceEntry(
const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer) : HResult; virtual;
procedure Afterconstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
Estos metodos de TObj ect estan disponibles para 10s objetos de cada clase,
puesto que TObj ect es el antcccdente comun de cada clase. Veamos como podemos usar estos metodos para acceder a informacion de clase:
procedure TSenderForm.ShowSender(Sender: TObject);
begin
Memo1 .Lines.Add ( 'Nombre de clase:' ' + Sender .ClassName);
if Sender.ClassParent <> nil then
' +
+ IntToStr
El codigo verifica si la Classparent es nil, en caso de que se este utilizando realmente una instancia del tipo TOb ject, que no tiene tipo basico.
Este metodo Showsender es parte del ejemplo I f Sender del CD.El metodo esta conectado con el evento OnClic k de diversos controles: tres botones,
una casilla de verificacion y un cuadro de edicion. Cuando hacemos clic sobre
cada control, se recurre a1 metodo Showsender con el control correspondiente
como remitente. Uno de 10s botones es en realidad un boton Bitmap, un objeto de
una subclase TButton. Se puede ver un ejemplo de este programa en tiempo de
ejecucion en la figura 3.7
Se pueden usar otros metodos para realizar pruebas. Por ejemplo, se puede
verificar si el objeto Sender es de un tipo especifico con el siguiente codigo:
if Sender-ClassType = TButton then
.. .
Tambien se puede verificar si el parametro Sender se corresponde a un objeto dado, con este test:
if Sender = Button1 then.. .
En lugar de verificar una clase o objeto concreto, sera necesario, por lo general, comprobar la compatibilidad de tip0 de un objeto con una clase dada, es
decir, sera necesario verificar si la clase del objeto es una clase determinada o una
clase de sus subclases. Esto permite saber mejor si se puede trabajar sobre el
objeto con 10s metodos definidos para dicha clase. Esta comprobacion se puede
realizar utilizando el metodo InheritsFrom,a1 que tambien se llama cuando
se usa el operador is.Las siguientes pruebas son equivalentes:
if Sender. InheritsFrom (TButton) then
if Sender i s TButton then . .
...
Se usa una referencia de clase en la parte principal del bucle while, que
comprueba la ausencia de una clase padre (de mod0 que la clase actual es
TOb j ect). Como alternativa, podiamos haber escrito la sentencia while de
una de las siguientes formas:
w h i l e not MyClass.ClassNameIs ('TObject')
w h i l e MyClass <> TObject do...
d o ...
Cuando se inicia el programa, se usa la matriz para mostrar todos 10s nombres
de clase en un cuadro de lista. A1 seleccionar un elemento del cuadro de lista se
desencadena la presentacion visual de sus datos y sus clases basicas.
-
La biblioteca
de clases
principales
Ya vimos que Delphi incluye una gran cantidad de funciones y procedimientos, per0 la autentica potencia de la programacion visual en Delphi reside en la
gigantesca biblioteca de clases que proporciona.
La biblioteca de clases estandar de Delphi contiene cientos de clases, con miles
de metodos, y es tan inmensa que no se puede proporcionar una referencia detallada en este libro. En su lugar, exploraremos diversas areas de esta biblioteca a
partir de este punto.
Este capitulo esta dedicado a las clases principales de la biblioteca a1 igual que
a algunas tecnicas estandar de programacion, como la definicion de eventos. Exploraremos las clases mas habitualmente utilizadas, como las listas, listas de
cadenas, colecciones y streams o flujos. La mayor parte del tiempo exploraremos
10s contenidos de la unidad c1a s se s, per0 tambien examinaremos otras unidades principales de la biblioteca.
Las clases de Delphi pueden utilizarse completamente desde el codigo o desde
el diseiiador visual de formularios. Algunas de ellas son clases componentes, que
apareceran en la paleta de componentes, y otras son de proposito mas general.
Los terminos clase y componente puede usarse casi como sinonimos en Delphi.
Los componentes son 10s elementos centrales de las aplicaciones Delphi. Cuando
se escribe un programa, basicamente se escoge un cierto numero de componentes
y se definen sus interacciones, y ya esta.
Antes de comenzar con este capitulo, seria necesario disponer de una buena
compresion del lenguaje, en temas como la herencia, las propiedades, 10s metodos
virtuales, las referencias de clases y demas. Este capitulo trata 10s siguientes
temas:
El paquete RTL, CLX y VCL.
TPersistent y published.
Controles
(oomponentes visuales)
Controles no
de ventana
(TComponent)
(subclases de
TGraphicControl)
Componentes no visuales
(otras subclases de
TComponent)
Ademas de 10s componentes, la biblioteca incluye clases que heredan directamente de TOb j ect j1 de TPers istent. Estas clases se conocen de mod0
colectivo como Objects en parte de la documentacion, un nombre bastante confuso. Estas clases no componentes se utilizan normalmente para valores de propiedades o como clases de utilidad empleadas en el c6digo; a1 no heredar de
TComponent , no se pueden utilizar directamente en programacion visual.
--
--
Componentes n o visuales: Son todos 10s componentes que no son controles, todas las clases que descienden de T C o m p o n e n t pero no de
T C o n t r o l . En tiempo de diseiio, un componente no visual aparece en el
formulario o modulo de datos como un icono (con un titulo debajo opcional
en 10s formularios). En tiempo de ejecucion, algunos de estos componentes
pueden resultar visibles (por ejemplo, 10s cuadros de dialogo estandar) y
otros estan visibles siempre (por ejemplo, el componente de tabla de la
base de datos).
01 o componente en
el Form Designer, s e puede ver una sugerencia sobre herramientas con su
nombre y tip0 de clase (y alguna information ampliada). Se puede utilizar
tambien una opcion del entorno, show Component Captions, parai
vet el nombre del componente no visual bajo su icono.
Esta es la subdivision tradicional de VCL, muy comun para 10s programadores
Delphi. A pesar de la introduccion de CLX y de algunas estructuras de denominacion nuevas, 10s nombres tradicionales sobreviviran probablemente y sc mezclaran en la jerga de 10s programadores en Delphi.
La estructura de CLX
Borland se refiere ahora a distintas secciones de la biblioteca CLX empleando
una terminologia para Linux y una estructura de nombrado ligeramente distinta
(y menos clara) en Delphi.
Esta nueva subdivision de la biblioteca multiplataforma representa areas mas
logicas que la estructura de la jerarquia de clases:
BaseCLX: Forma el nucleo principal de la biblioteca de clases: las clases
mas altas (corno T C o m p o n e n t ) y diversas clases de utilidades generales
(corno listas, contenedores, colecciones y streams). En comparacion con
las clases correspondientes de la VCL, BaseCLX ha cambiado poco y
resulta muy facil de transportar entre las plataformas Windows y Linux.
Este capitulo se dedica en gran medida a explorar BaseCLS y las clases
principales comunes de VCL.
VisualCLX: Es la coleccion de componentes visuales, por lo general llamados controles. Esta es la parte de la biblioteca que esta relacionada mas
estrechamente con el sistema operativo: VisualCLX se implementa en la
parte superior de la biblioteca Qt, disponible tanto en Windows como en
Linux. Utilizar VisualCLX permite una capacidad de transporte total de la
parte visual de una aplicacion entre Delphi en Windows y Kylix en Linux.
Sin embargo, la mayoria dc 10s componentes VisualCLX poscen sus co-
La clase TPersistent
La primera clase principal de la biblioteca de Delphi que veremos es su clase
T Pe r s is t e n t , que es bastante atipica: tiene poco codigo y casi no tiene uso
directo, per0 ofrece la base para la idea global de la programacion visual. Se
puede ver la definicion de la clase en el listado 4.1.
Como su nombre indica, esta clase controla la permanencia (es decir, el hecho
de guardar el valor de un objeto en un archivo para usarlo mas tarde y volver a
crear el objeto en el mismo estado y con 10s mismos datos). La permanencia es un
elemento clave de la programacion visual. De hecho, en tiempo de diseiio en
Delphi manipulamos objetos reales, que se guardan en archivos DFM y se vuelven a crear en tiempo de ejecucion a1 mismo tiempo que el contenedor especifico
(formulario o modulo de datos) del componente.
-
- ----
. .-
NOTA: ~ ~ 1 d o
re aplica tarnbien a
10s archivos XF M, el ~ o m r ae
o arcolvo que u u ~ ras
m t@xwiones CLX.
El formato es idCntico. La diferencia en la extens&n ts 2@Mante porque
Delphi la utiliza para detenninar si el formularib se h a en CLS/Qt o en
Vf'T
AUinrlnrtre
En U x r l i v tnrln Fnrmn.lnAm m m
v u r
r r r u w r r u . uu x r j u n , wuv r w l u r u x u l v v u ~n f~rmulfwripCLXIQt, sin
importar la extension que se emplee; por eso, la extensi~nXF'IWDFM no
tiene importancia en Kylix.
. ..
n~,.a~.m..;r.
aC...-;h;..
A.LXA;~A- A . . . . ~ I Y ~ ~ + ~
1 G J U l U l I a UGkGJCIl 1 U G a b 1 I U l l G J G b V U I ~ V
lIlLUIU~lIIIGllCG.
T . .
~~~
<-
..
n
d
e ~ ~ e~ t h &ignar
o d~ ~un objeto
& a1 & n p o
D a t a , se puede obtener un puntero a metodo completo. En este punto, para
llamar a1 metodo se debe convertir a tip0 de puntero a metodo correcto.
Este es un fragment0 de cbdigo que recalca 10spuntos clave de esta tecnica:
var
Method: m e t h o d ;
Evt: TNotifyEvent;
begin
Method.Code : = MethodAddress ('ButtonlClickl);
Method-Data := Self;
Evt := TNotif yEvent (Method);
Evt (Sender); // llamada a1 d t o d o
RTTI de propiedades es algo posible gracias a un grupo de subrutinas no documentadas, parte de la unidad TypInfo
(GetPropValue
(Buttonl, '
' C a p t i o n ' )) ;
Se puede ver el efecto que se obtiene a1 pulsar el boton Fill List mientras se
usa el valor predefinido C a p t i o n del cuadro de edicion en la figura 4.2. Se
puede probar con cualquier otro nombre de propiedad. Los numeros se convertiran en cadenas mediante una conversion de variante. Los ob-ietos (como el valor
de la propiedad Font) apareceran como direcciones de memoria
!?!~ow~Y
Captron
I ~ a b e lCapl~on
l
= &Pmpe~ty
INO
Ed61 Caption
eFn Lnl
Figura 4.2. El resultado del ejemplo RunProp, que accede a las propiedades por
nombre en tiempo de ejecucion.
- ADVERTENCIA: No conviene usar con frecuencia la unidad Typ In f o
en lugar del polimorfismo y de otras thcnicas de acceso a propiedades. Hay
que usar primer0 el acceso a propiedades de clase basica o la conversion de
tipos segura (as) cuando sea necesario, y reservar el acceso RTTI para
propiedades como ultimisimo recurso. Usat tknicas T y p I n f o ralentiza el
codigo, lo hace d s complejo y mris proclive a1 error humano; de hecho,
o&
la verificacibn de tipos en tiernpo de compilacibn.
La clase TComponent
Si la clase T P e r s i s t e n t es realmente mas importante de lo que parece a
primera vista, la clase clave en el corazon de la biblioteca de clases basada en
componentes de Delphi es TComponent, que hereda de T P e r s i s t e n t (y de
T O b je c t ) . La clase TComponent define muchos elementos principales de componentes, per0 no es tan compleja como se puede pensar, puesto que las clases
basicas y el lenguaje ya ofrecen la mayoria de 10s elementos realmente necesarios.
Posesion
Una de las caracteristicas principales de la clase TComponent es la definicion de posesion. Cuando se crea un componente, se le puede asignar un componente propietario, que sera responsable de destruirlo. Asi, cada componente puede
tener un propietario y puede ser tambien el propietario de otros componentes.
En realidad, existen varios metodos y propiedades publicos de la clase que se
dedican a controlar las dos partes de la posesion. Veamos una lista, extraida de la
declaracion de clase (en la unidad Classes de la VCL):
type
TComponent = class(TPersistent, IInterface,
IInterfaceComponentReference)
public
constructor Create(A0wner: TComponent); virtual;
procedure DestroyComponents;
function FindComponent(const AName: string): TComponent;
procedure InsertComponent(AComponent: TComponent);
procedure RemoveComponent(AComponent: TComponent);
property Components [Index: Integer] : TComponent read
Getcomponent;
property Componentcount: Integer read Getcomponentcount;
property ComponentIndex: Integer
read GetComponentIndex write SetComponentIndex;
property Owner : TComponent read FOwner;
D e s i g n e r del IDE, ese formulario o modulo de datos poseera a cualquier componente que se deje caer sobre el. Si siempre se crean componentes con un propietario (la operacion predefinida a1 usar el diseiiador visual del IDE), solo sera
necesario recordar destruir estos contenedores de componentes cuando ya no 10s
necesitemos y podemos olvidarnos de 10s componentes que contengan. Por ejemplo, se elimina un formulario para destruir de una sola vez todos 10s componentes
que contenga, lo que supone una gran simplification en comparacion con tener
que acordarse de liberar todos y cada uno de 10s objetos individualmente. En una
escala mayor, 10s formularios y modulos de datos generalmente perteneceran a1
objeto A p p l i c a t i o n , que es destruido por el codigo de cierre de la VCL que
libera todos 10s contenedores de componentes, con lo que se liberaran 10s componcntes que contenga.
La matriz Components
La propiedad c o m p o n e n t s se puede usar tambicn para acceder a un componente que tiene un propietario, digamos, por ejemplo, un formulario. Esta propiedad puede resultar muy util (comparada con el uso direct0 de un componente
especifico) para escribir codigo generico, que actue en todos o en muchos componentes a la vez. Por ejemplo, se puede usar el siguiente codigo para aiiadir a un
cuadro de lista 10s nombres de todos 10s componentes de un formulario:
procedure TForml.ButtonlClick(Sender: TObject);
var
I: Integer;
begin
ListBoxl.Items.Clear;
0; I : = 0 to Componentcount - 1 do
ListBoxl.1tems.Add (Components [I].Name);
end;
ULYi Y p
~
~
de un formulario
~
o cuadro
~
de grupo
t pard f
ma~ersep&doRcontroleS.biiop se puede usar Ia propicdad components
ikl form&.&.r para 'haw& pot d o s lo$ componentes. sea cual sea su
PW.
,
A1 utilizar la propiedad Components.siempre podemos acceder a cada componente de un formulario. Sin embargo, si hay que acceder a un componente
especifico, en lugar de comparar cada nombre con el nombre del componente que
buscamos, podemos dejar que lo haga Delphi, utilizando el metodo FindComponent del formulario. Este metodo simplemente recorre la matriz Components
en busca del nombre correspondiente.
Cambio de propietario
Hemos visto que casi todos 10s componentes tienen un propietario. Cuando se
crea un componente en tiempo de diseiio (o desde el archivo D F M resultante); su
propietario sera siempre su formulario. Cuando se crea un componente en tiempo
de ejecucion, el propietario se pasa como parametro a1 constructor create.
owner es una propiedad de solo lectura, por lo que no se puede cambiar. El
propietario s e establece en el momento de la creacion y por lo general no deberia
cambiar durante la vida util de un componente. N o se deberia cambiar el propietario de un componente en tiempo de diseiio ni tampoco su nombre libremente,
porque para hacerlo, se puede llamar a 10s metodos Insertcomponent y
RemoveComponent del propietario, que pasan el componente actual como
parametro. Sin embargo, no se pueden aplicar directamente a ningun controlador
de eventos de un formulario, como se pretende aqui:
procedure TForml.ButtonlClick(Sender:
begin
RemoveComponent (Buttonl);
Form2.InsertComponent (Buttonl);
end;
TObject);
Este codigo produce una violacion del acceso a la memoria, porque cuando se
llama a R e m o v e C o m p o n e n t , Delphi desconecta el componente del campo del
formulario (Buttonl), definiendolo como nil.La solucion es escribir un procedimiento como el siguiente:
procedure ChangeOwner (Component, Newowner: TComponent);
begin
Component.Owner.RemoveComponent (Component);
NewOwner.1nsertComponent (Component);
end;
componente padre. Las dos ordenes combinadas desplazan el boton por completo
a otro formulario, cambiando su propietario:
procedure TForml.ButtonChangeClick(Sender:
begin
if Assigned (Buttonl) then
begin
// c a m b i a e l p a d r e
Buttonl.Parent : = Form2;
// c a m b i a el p r o p l e t a r i o
Changeowner (Buttonl, Form2) ;
end;
end;
TObject);
1
Figura 4.3. En el ejemplo Changeowner, al hacer clic sobre el boton Change se
mueve el componente Buttonl a1 segundo formulario.
Para demostrar que el propietario del componente Buttonl cambia realmente, hemos decidido aiiadir otra funcion a ambos formularies. El boton List rellena
el cuadro de lista con 10s nombres de 10s componentes que posee cada formulario,
usando el procedimiento que aparece en el apartado anterior. Al hacer clic sobre
10s dos botones List antes y despues de mover el componente, veremos lo que
Delphi hace internamente. Como caracteristica final, el componente Butt on1
tiene un sencillo controlador para su evento Onclick,para mostrar el titulo del
formulario propietario:
procedure TForml.ButtonlClick(Sender: TObject);
begin
ShowMessage ('Mi p r o p i e t a r i o es ' + ( (Sender as
TButton) .Owner as TForm) .Caption);
end ;
La propiedad Name
Cada componente en Delphi deberia tener un nombre. El nombre ha de ser
unico dentro del componente propietario, que por lo general. es el formulario en el
que se coloca el componente. Esto significa que una aplicacion puede tener dos
formularios diferentes, cada uno con un componente con el mismo nombre. Por lo
general, para que no haya confusion, es mejor mantener nombres de componente
unicos a lo largo de una aplicacion.
Es muy importante establecer un valor adecuado para la propiedad Name: si
es demasiado largo, sera necesario teclear un monton de codigo para usar el
objeto y si es demasiado corto, se pueden confundir diferentes objetos. Normalmente, el nombre de un componente tiene un prefijo con el tip0 de componente.
Esto hace que el codigo resulte mas facil de leery permite que Delphi agrupe 10s
componentes en el cuadro combinado o b j e c t I n s p e c t o r , donde se clasifican por nombre.
Existen tres elementos importantes relacionados con la propiedad Name de 10s
componentes:
Primero, en tiempo de diseiio, el valor de la propiedad Name se usa para
definir el nombre del campo de formulario en la declaracion de la clase del
formulario. Este es el nombre que normalmente se va a usar en el codigo
para referirse a1 objeto. Por esa razon, el valor de la propiedad Name ha de
ser un identificador de lenguaje Delphi legal (sin espacios y que empiece
con una letra, no con un numero).
Segundo, si se establece la propiedad Name de un control antes de modificar su propiedad C a p t i o n o T e x t , el nuevo nombre se copia normalmente en el titulo. Es decir, si el nombre y el titulo son identicos, entonces
a1 cambiar el nombre tambien cambiara el titulo.
Tercero, Delphi usa el nombre del componente para crear el nombre
predefinido de 10s metodos relacionados con estos eventos. Si tenemos un
componente B u t t o n l , el controlador predefinido del evento OnCl i c k
se llamara B u t t o n l C l i c k , a no ser que se especifique un nombre diferente. Si mas tarde se cambia el nombre del componente, Delphi modificara 10s nombres de 10s metodos relacionados en fincion de ello. Por ejemplo,
si se cambia el nombre del boton aMyButton, el metodo B u t t o n l C l i c k
se transforma automaticamente en MyBut t onC 1i c k.
Como antes mencionamos, si tenemos una cadena con el nombre de un componente, se puede obtener su instancia llamando a1 metodo F i n d c o m p o n e n t de
su propietario, que devuelve n i l en caso de no encontrar el componente. Por
ejemplo, se puede escribir:
var
Comp: TComponent;
begin
Comp : = Findcomponent ( ' B u t t o n 1 ' ) ;
if Assigned (Comp) then
with Comp as TButton do
/ / algo d e c o d i g o . . .
,..-U ,
-,c,,,,
SG
G IGLIGIGU
,..I,,:,
I I I U I ~ I I W~
,,
,
---- ,-a,
a
w.mlpunGuiGs
ms - L - 1
-..---I,
,.,
slaicura ,
r ; i r ~ g u ~
, -1
, -&, : ,
I Ldual, wauuu GI
TObject);
Hemos listado estos metodos solo para mostrar que en el codigo de un formulario normalmente nos referimos a 10s componentes disponibles, definiendo sus
interacciones. Por esa razon, parece imposible librarse de 10s campos que corresponden a1 componente. Sin embargo, lo que se puede hacer es ocultarlos y mover10s de la parte publicada predefinida a la parte privada de la declaracion de clase
del formulario:
TForml = class (TForm)
procedure ButtonlClick (Sender: TObject) ;
procedure EditlChange(Sender: TObject);
procedure FormCreate (Sender: TObj ect) ;
private
Buttonl: TButton;
Editl : TEdit ;
ListBoxl: TListBox;
end;
Si ejecutamos el programa ahora, tendremos problemas: el formulario se cargara bien, per0 debido a que no se inicializan 10s campos privados, 10s eventos
anteriores utilizaran referencias de objeto n i l . Delphi inicia normalmente 10s
campos publicados del formulario utilizando 10s componentes creados desde el
archivo DFM. Podemos preguntarnos que ocurre, si lo hacemos nosotros mismos,
con el siguiente codigo:
procedure TForml .FormCreate (Sender: TOb j ect) ;
begin
.
Buttonl := FindComponent ( ' B u tton1 ' ) as TButton;
Editl : = FindComponent ( 'Editl I ) a s TEdit;
ListBoxl := FindComponent ( ' ListBoxl ' ) a s TListBox;
end;
proyecto grande que deba desarrollarse de acuerdo con 10s principios de la programacion orientada a objetos.
Eventos
En realidad, 10s componentes de Delphi, se programan usando PME: propiedades, metodos y eventos. Aunque a estas alturas, 10s metodos y propiedades deberian estar claros, 10s eventos todavia no se han comentado. La razon es que 10s
eventos no implican una nueva funcion del lenguaje, sin0 que son una tecnica
estandar de programacion. Un evento, de hecho, es tecnicamente una propiedad,
con la unica diferencia de que se refiere a un metodo (un tip0 de puntero a metodo,
para ser precisos) en lugar de a otros tipos de datos.
Eventos en Delphi
Cuando un usuario hace algo con un componente, como hacer clic sobre el, el
componente genera un evento. Otros eventos 10s produce el sistema, en respuesta
a una llamada de metodo o un cambio de una de las propiedades del componente
(o incluso de un componente diferente). Por ejemplo, si ponemos el foco en un
componente, el componente que tenga el foco en ese momento lo pierde y se
desencadena el evento correspondiente. Tecnicamente, la mayoria de 10s eventos
Delphi se desencadenan a1 recibir el mensaje correspondiente del sistema operativo, aunque no existe un mensaje individual para cada evento. Los eventos de
Delphi suelen ser de mayor nivel que 10s mensajes del sistema operativo y Delphi
ofrece una serie de mensajes adicionales entre componentes.
Desde un punto de vista teorico, un evento es el resultado de una peticion
enviada a un componente o control, que puede responder a1 mensaje. Siguiendo
ese enfoque, para controlar el evento clic de un boton, seria necesario crear una
subclase para la clase TButton y aiiadir el nuevo codigo del controlador de
eventos dentro de esa nueva clase.
En la practica, crear una nueva clase para cada componente que queramos
usar es demasiado complicado como para resultar razonable. En Delphi, el controlador de eventos de un componente es normalmente un metodo del formulario
que contiene el componente, no del propio componente. En otras palabras, el
componente confia en su propietario para controlar eventos. Esta ttcnica se denomina delegation y resulta basica para el modelo basado en componentes de Delphi.
De este modo, no hay que modificar la clase T B u t t o n , a no ser que queramos
definir un nuevo tip0 de componente, sino sencillamente personalizar su propietario para modificar el comportamiento del boton.
Punteros a metodo
Los eventos dependen de una caracteristica especifica del lenguaje Delphi: 10s
punteros a metodo. Un tipo de puntero a metodo es como un tip0 de procedimiento, pero uno que se refiere a un metodo. Tecnicamente, un tip0 de puntero a
metodo es un tip0 de procedimiento que tiene un parametro Self implicito. En
otras palabras, una variable de un tip0 de procedimiento almacena la direccion de
una funcion a la que Ilamar, dado que tenga un conjunto de parametros. Una
variable puntero a metodo almacena dos direcciones: la direccion del codigo del
metodo y la direccion de un caso del objeto (datos). La direccion de la instancia
del objeto aparecera como self dentro de la estructura del metodo, cuando se
llame al codigo del metodo utilizando este puntero a metodo.
t m e
IntProceduralType = procedure (Num: Integer);
IntMethodPointerType = procedure (Num: Integer) o f object;
(Sender: TObject) ;
var
Form1 : TForml ;
...
protected
p r o c e d u r e Dochange;
...
dynamic;
public
p r o p e r t y OnChange : T N o t i f y E v e n t
read FonChange w r i t e FOnChange;
.-.
end;
Asi, el metodo D o C h a n g e se llama cada vez que uno de 10s valores cambia,
como en el siguiente metodo:
class (TForm)
...
p r o c e d u r e DateChange (Sender: TObj ect) ;
TObject);
usando la notacion de matriz ([ y I), tanto para leer como para cambiar elementos.
Existe una propiedad Count, asi como metodos de acceso comunes, como Add,
Insert, Delete, Remove y metodos de busqueda (por ejemplo, Indexof).
La clase TLis t posee un metodo Assign que, ademas de copiar 10s datos
fiente, puede realizar operaciones fijas en las dos listas, como and, or y xor.
Para rellenar una lista de cadenas con elementos y, mas tarde, verificar si uno
de ellos esta presente, se puede escribir un codigo como el siguiente:
var
sl: TStringList;
idx: Integer;
begin
s l : = TStringList.Create;
try
sl.Add ('uno') ;
sl.Add ('dos');
sl.Add ('tres') ;
/ / despues
idx := sl. IndexOf ( ' dos ' ) ;
i f idx >= 0 then
ShowMessage ( 'Encontrada cadena ' ) ;
finally
sl.Free;
end ;
end;
+ Random
A1 final del codigo anterior, antes de que podamos hacer una conversion de
tipos siguientes con as, es necesario convertir manualmente el puntero que ha
devuelto TList en una referencia TOb j ect. Este tipo de expresion puede causar una excepcion de conversion de tipos no valida o generar un error de memoria,
si el puntero no es una referencia a un objeto.
Para demostrar que las cosas pueden salir realmente mal, hemos aiiadido un
boton m b , que aiiade un objcto T B u t t o n a la lista mediante una llamada a
L i s t D a t e . A d d ( S e n d e r ) . Si se hace clic sobrc este boton y despues se actualiza una de las listas, resultara en un error. Por ultimo, hay que recordar que
cuando sc destruye una lista de objetos, hay que destruir primer0 todos 10s objetos
de la lista. El programa L i s t Demo usa para esto el metodo F o r m D e s t r o y del
formulario:
procedure TForml.FormDestroy(Sender: TObject);
var
I: Integer;
begin
for I : = 0 to ListDate.Count - 1 do
TObject (ListDate [I]) .Free;
ListDate.Free;
end;
Colecciones
El segundo grupo, las colecciones, contiene so10 dos clases, T C o l l e c t i o n
y T C o l l e c t i o n I t e m . T C o l l e c t i o n define una lista homogenea de objetos, poseidos por la clase coleccion. Los objetos dc la coleccion han de descender
de la clase T C o l l e c t i o n I t e m . Si se necesita una coleccion que almacene
objetos especificos, hay que crear una subclase de T C o l l e c t i o n y una subclase
correspondiente de T C o l l e c t i o n 1 t e r n . Las colecciones se usan para especificar valores de propiedades de componentes; resulta muy poco frecuente trabajar
con colecciones para almacenar objetos propios.
Clases de contenedores
Las versiones recientes de Delphi incluyen una serie de clases de contenedores,
definida en la unidad C o n t n r s . Las clases contenedores amplian las clases T L i s t
ADVERTENCIA: A diferencia de TOb je c t L i s t , las clases TOb j e c t Stack y T O b jectQueueno poseen 10s objetos insertados y no destruiqbjetos que queden en la estructura de datos cuando se
destruyan. Simplernente se pueden &ar todos esrtos elementos, destruidos
cuando se hayam terminado de usar y despues destruir el contenedor.
...,..'.""
r i n an1lpll-a
'UY
ble ListDate a TOb jectList.En el metodo Formcreate,hemos modificad0 la creacion de la lista con el siguiente codigo, que activa la posesion de lista:
ListDate
:=
) ;
A1 pulsar 10s dos botones, se puede ver que cuando se llama a pop para cada
contenedor, devuelve el ultimo elemento. La diferencia es que la clase TQueue
inserta 10s elementos a1 principio y la clase T Sta c k 10s inserta a1 final.
~--A.~'
..*-S
l . - ?
~
3.4-
- -
r -
.
I
TBucketProc;
1"
-I""-
-4-
..---.A-
mmL-:
----A-
L--*--*-
-<*:I
--
---a-
generafes, y dispone de un buen algoritmo para calcular el valor de verification de una cadena.
de fechas. Para garantizar que 10s datos de una lista Sean homogtneos, se puede
verificar el tipo de 10s datos estraidos antes de insertarlos. Pero como medida de
seguridad adicional, tal vez queramos verificar el tip0 de datos durante la extraccion. Sin embargo, afiadir verificaciones en tiempo de ejecucion ralentiza un programa y es arriesgado (un programador podria no verificar el tip0 en algunos
casos).
Para resolver ambos problemas, se pueden crear clases de lista especificas
para unos tipos de datos determinados y adaptar el codigo de las clases T L i s t o
TObje c t L i s t existentes (o de otra clase de contenedor). Existen dos tecnicas
para realizar esto:
Derivar una nueva clase de la clase de lista y personalizar el metodo A d d
y 10s metodos de acceso, relacionados con la propiedad I terns. Esta tecnica es la utilizada por Borland para las clases de contenedores, que se
derivan todas de T L i s t .
r
NOTA: Las ~ l a s e de
s contenedores de Delphi utilizan sobrescritura esthtica para realizar las adaptaciones simples de tip0 (sesultados de parhetros
y funciones del tip0 deseado). La sobrescritura estiitica no es lo mismo que
el polimorfismo; alguien que utilice una clase de contenedor mediante una
variable TLis t no Uamara a las funciones espe~iaha&s del contenedor.
La sobrescritura esthtica es una tecnica eficaz y sencilla, pero tiene una
importante restriccion: 10s metodos del descendiente no deberian hacer nada
rnlic nJlli
AP
rrnn e n n v ~ r c i A nA- t i n n c c p n e i l l a
nnrnrre nn
hrrv
uarantirrc
Ae
// b a s a d o e n h e r e n c i a
T D a t e L i s t I = class (TObj e c t L i s t )
protected
p r o c e d u r e S e t O b j e c t ( I n d e x : I n t e g e r ; Item: TDate) ;
f u n c t i o n GetObj e c t ( I n d e x : I n t e g e r ) : TDate;
public
f u n c t i o n Add ( O b j : T D a t e ) : I n t e g e r ;
p r o c e d u r e I n s e r t ( I n d e x : I n t e g e r ; Obj : T D a t e ) ;
p r o p e r t y O b j e c t s [ ~ n d e x : I n t e g e r ] : TDate
r e a d GetObject w r i t e SetObject; d e f a u l t ;
end;
// b a s a d o e n e n v o l t o r i o
TDateListW = class ( T O b j e c t )
private
FList: TObjectList;
f u n c t i o n G e t O b j e c t ( I n d e x : I n t e g e r ) : TDate;
p r o c e d u r e S e t o b j e c t ( I n d e x : I n t e g e r ; Obj : T D a t e ) ;
f u n c t i o n GetCount: I n t e g e r ;
public
constructor Create;
d e s t r u c t o r Destroy; o v e r r i d e ;
f u n c t i o n Add ( O b j : T D a t e ) : I n t e g e r ;
f u n c t i o n Remove ( O b j : T D a t e ) : I n t e g e r ;
f u n c t i o n IndexOf ( o b j : T D a t e ) : I n t e g e r ;
p r o p e r t y Count: I n t e g e r r e a d GetCount;
p r o p e r t y O b j e c t s [ I n d e x : I n t e g e r ] : TDate
r e a d GetObject w r i t e SetObject; d e f a u l t ;
end;
TObject);
La llamada a Update List lanza una excepcion, que se muestra directamente en el cuadro de lista, porque hemos utilizado una conversion de tipos as en las
clases de lista personalizadas. Un programador inteligente jamas escribiria el
codigo anterior. Para resumir, escribir una lista personalizada para un tip0 especifico hace que un programa resulte mucho mas robusto. Escribir una lista envoltorio en lugar de una basada en herencia suele ser algo mas seguro, aunque requiere
mucho mas codigo.
Streaming
Otro ambito principal de la biblioteca de clases de Delphi es el soporte de
streaming, que incluye adrninistracion de archivos, memoria, sockets y otras fuentes
de informacion organizadas de forma secuencial. La idea del streaming consiste
en moverse a traves de 10s datos mientras 10s leemos, de un mod0 muy parecido a
las tradicionales funciones Read y Write utilizadas por el lenguaje Pascal.
La clase TStream
La VCL define la clase abstracta Tst ream y diversas subclases. La clase
padre, TStream,posee solo unas cuantas propiedades y jamas se creara una
instancia de la misma, per0 posee una interesante lista de metodos que, por lo
general, se utilizara cuando se trabaje con clases stream derivadas.
La clase TStream define dos propiedades, size y Position.Todos 10s
objetos stream tienen un tamaiio especifico (que generalmente aumenta si escribimos algo despues del final del stream) y habra que especificar una posicion dentro
del stream en la que se quiere leer o escribir la informacion.
La lectura y escritura de bytes depende de la clase de stream utilizada, per0 en
ambos casos no es necesario saber mucho mas que el tamaiio del stream y la
posicion relativa dentro del stream para leer o escribir datos. De hecho, esta es
una de las ventajas de usar streams. La interfaz basica sigue siendo la misma si
manipulamos un archivo del disco, un campo de objeto binario ancho (BLOB) o
una secuencia larga de bytes en memoria. Ademas de las propiedades size y
Position,la clase Tstream define tambien varios metodos importantes, la
mayoria de 10s cuales son virtuales y abstractos. (En otras palabras, la clase
TSt ream no define lo que hacen estos metodos, por lo tanto, las clases derivadas
son responsables de su implementacion.) Algunos de estos metodos solo son importantes en el context0 de lectura y escritura de componentes dentro de un stream
(por ejemplo, Readcomponent y Writecomponent), per0 algunas son utiles tambien en otros contextos. En el listado 4.2, se puede encontrar la declaracion de la clase TSt ream,extraida de la unidad Classes.
Listado 4.2. La seccion publica de la definicion de la clase TStream.
TStream = class (TObject)
public
// lee y escribe un buffer
function Read (var Buffer; Count: Longint) : Longint; virtual;
abstract;
function Write (const Buffer; Count : Longint) : Longint;
virtual; abstract;
procedure ReadBuf fer (var Buffer; Count: Longint) ;
procedure WriteBuffer(const Buffer; Count: Longint);
// m u e v e a una p o s i c i d n especifica
function Seek (Offset: Longint; Origin: Word) : Longint;
overload; virtual ;
function Seek (const Offset: Int64; Origin: TSeekOrigin) :
Int64;
overload; virtual ;
// copia el s t r e a m
function CopyFrom(Source: TStream; Count: Int64) : Int64;
// l e e o e s c r i b e un c o m p o n e n t e
function ReadComponent(1nstance: TComponent): TComponent;
function ReadComponentRes(1nstance: TComponent) : TComponent;
procedure WriteComponent(1nstance: TComponent);
procedure WriteComponentRes(const ResName: string; Instance:
TComponent) ;
procedure WriteDescendent(Instance, Ancestor: TComponent);
procedure WriteDescendentRes(
const ResName: string; Instance, Ancestor: TComponent) ;
procedure WriteResourceHeader(const ResName: string; out
FixupInf o: Integer) ;
procedure FixupResourceHeader (FixupInfo: Integer) ;
procedure ReadResHeader;
// p r o p i e d a d e s
property Position: Int64 read GetPosition write SetPosition;
property Size: Int64 read GetSize write SetSize64;
end ;
stream: TStream;
n: integer;
str: string;
begin
n : = 10;
str : = ' c a d e n a de prueba' ;
stream : = TFileStream.Create ( 'c:\ t m p \ t e s t l , fmcreate) ;
stream.WriteBuffer (n, sizeof (integer)) ;
stream-WriteBuffer (str[1], Length (str)) ;
stream.Free;
Una tecnica totalmente alternativa consiste en permitir que componentes especificos guarden o carguen datos en 10s streams. Muchas clases de la VCL definen
un metodo LoadFromStream o SaveToStream,como por ejemploTStrings,
TStringList, TBlobField, TMemoField, TIcon y TBitmap.
TFileStream;
begin
if 0penDialogl.Execute then
begin
S : = TFileStream.Create
fmOpenRead) ;
(OpenDialogl.FileName,
try
(S) ;
finally
S . Free;
end;
end ;
end;
NOTA: De 10s diferentes modos, 10s mas importantes son fmshareDenywrite, que se usa cuando simplemente se leen datos de un archivo
mai'
Otro uso importante de 10s streams es controlar 10s campos BLOB de bases de
datos u otros campos grandes directamente. Se pueden exportar esos datos a un
stream o leerlos desdc uno, llamando simplemente a 10s metodos SaveToStream
y LoadFromStream de la clase TBlobField.
-
----
Esta vez, el archivo real tambien incluira 10s caracteres de marca adicionales,
para que se pueda leer de nuevo este archivo utilizando unicamente un objeto
T R e a d e r . Por esta razon, el uso de T R e a d e r y TWr i t e r se reduce generalmente a1 streaming de componentes y rara vez se aplica en la administracion de
archivos general.
Streams y permanencia
En Delphi, 10s streams tienen una hncion bastante importante para la permanencia. Por ese motivo, muchos metodos de T S t r e a m e s t h relacionados con las
acciones de guardar y cargar un componente y sus subcomponentes. Por ejemplo,
se puede almacenar un formulario en un stream a1 escribir:
Cuatro funciones distintas, con 10s mismos parametros y nombres que contienen el nombre Resource en lugar de Binary (como en Obj ect ResourceToText), convierten el formato del recurso obtenido por WriteComponentRes.
Un ultimo metodo, TestStreamFormat, indica si un DFM contiene una representation binaria o textual.
En el programa FormToText, se ha utilizado el metodo O b jectBinaryToText para copiar la definicion binaria de un formulario en otro stream
y, a continuacion, se ha mostrado el stream resultante en un campo de texto, como
muestra la figura 4.5. Este es el codigo de 10s dos metodos utilizados:
p r o c e d u r e TformText.btnCurrentClick(Sender:
var
MemStr: TStream;
begin
MemStr : = TMemoryStream.Create;
try
TObject);
end;
end;
procedure TformText.ConvertAndShow
(aStream: TStream);
var
ConvStream: TStream;
begin
aStream.Position : = 0;
ConvStream : = TMemoryStream.Create;
try
Obj ectBinaryToText (astream, ConvStream) ;
ConvStream.Position : = 0;
Memo0ut.Lines.LoadFromStream (ConvStream);
finally
ConvStream-Free
end ;
end :
PadO W
Fmm in Ex
TOP- 113
W d h - 545
H c , ~ .374
A c t w ~ o n l l o l blnCtwrenl
Caphon Fmm To Te4
C d n = clBlnFace
F d Charel DEFAULT-CWSET
F a d r h -&hdowTexl
F a n l H c M - 11
F m Nme M S Sans Sell?
Fwd St* = [I
O W C ~ r a l d r d n= T~ue
Vabk T w
P m l s P r d n d 96
TeulHmit4 = 13
&led m r d u l TMemo
Ldl 0
Top-41
Wdlh 537
- -
Hrn.306
&n
Kl~enl
Scret3rus = r N c l m a l
TabClldcr = 0
Fijese en que si hacemos clic varias veces sobre el boton Current Form Object,
obtendremos mas y mas texto y el texto del campo memo se incluira en el stream.
Despues de unas cuantas lineas, toda la operacion resultara extremadamente lenta, hasta que el programa parezca haberse colgado. En este codigo, empezamos a
ver parte de la flexibilidad de utilizar streams (podemos escribir un procedimiento
generic0 y utilizarlo para convertir cualquier stream).
A1 hacer clic sobre 10s botones de manera consecutiva (o modificar el formulario del programa), se puede comparar el formulario guardado en el archivo DFM
con el objeto real en tiempo de ejecucion.
- .-- --
--.
11
- -
-~
El valor de la clave se suma sencillamente a cada uno de 10s bytes guardados en un archivo y se resta cuando se leen 10s datos. Veamos el codigo
completo de 10s metodos Write y Read, que utiliza punteros con bastante
frecuencia:
constructor TEncodedStream.Create( const FileName: string;
Mode: Word) ;
begin
inherited Create (FileName, Mode) ;
FKey := 'A'; / / p r e d e f i n i d o
end ;
f u n ~ t i o nTEncodedStream.Write(const Buffer; Count: Longint):
~ongint;
var
pBuf, pEnc: PChar;
I, EncVal: Integer;
begin
// a s i g n a memoria para e l b u f f e r c o d i f i c a d o
GetMem (pEnc, Count) ;
try
// usa e l b u f f e r como una m t r i z d e c a r a c t e r e s
pBuf : = PChar (@Buffer) ;
// para cada c a r a c t e r d e l b u f f e r
for I := 0 to Count - 1 d o
begin
// c o d i f i c a e l v a l o r y l o a l m c e n a
EncVal := ( Ord (pBuf[I]) + Ord(Key) ) mod 256;
pEnc [I] := Chr (EncVal):
end;
// e s c r i b e e l b u f f e r c o d i f i c a d o para e l a r c h i v o
~ e s u l t := inherited Write (pEncA, Count) ;
finally
FreeMem (pEnc, Count) ;
end;
Load PI&.
a n ~ ~ e ~ o - n & i i en
i i ell primer campo de
memo, el segundo boton guarda el texto de este primer campo de memo en
un archivo codificado y el ultimo b o t h carga de nuevo el archivo codificado en el segundo campo de memo, tras decodificar el archivo. En este ejemplo, tras haber codificado el archivo, lo hemos vuelto a cargar en el primer
carnpo de memo como un archivo de texto normal a la izquierda, que por
supuesto resulta ilegible.
Como disponemos de la clase del stream codificado, el codigo de este programa es muy similar a1 de cualquier otro programa que use streams. Por
ejemplo, veamos el metodo usado para guardar el archivo codificado (se
puede comparar con el codigo de ejemplos anteriores basados en streams):
-bow&
-.
Como cjemplo dcl uso de estas clases, esta un pequeiio programa llamado
ZCompress que comprime y descomprime archivos. El programa tiene dos cuadros de edicion en 10s que se puede escribir el nombre del archivo a comprimir y
el nombre del archivo resultante, que se crea en caso de que no exista previamente. Cuando se hace clic sobre el boton Compress, el archivo original se utiliza
para crear el archivo destino; a1 hacer clic sobre el boton Decompress, se lleva el
archivo comprimido de vuclta a un stream de memoria. En ambos casos, el resultado de la compresion o descompresion se muestra en un campo de mcmo. La
figura 4.6 mucstra el resultado para el archivo comprimido (que resulta ser cl
codigo fuentc del formulario del programa actual).
Orginal file
ID\md7code\04VCnmpressVCompressForm
pas
Decompress
1
Figura 4.6. El ejemplo ZCompresss puede comprimir un archivo mediante la
biblioteca ZLib.
Para conseguir que el codigo de cste programa resulte mas reutilizable, podemos escribir dos funciones para comprimir o descomprimir un stream en otro
stream. Este es el codigo:
procedure C o m p r e s s S t r e a m (aSource, a T a r g e t : T S t r e a m ) ;
var
comprStream: TCompresssionStream;
begin
c o m p r S t r e a m : = TCompressionStream.Create
clFastest, aTarget) ;
t rY
comprStream.CopyFrom(aSource, aSource
comprStream.CompressionRate;
finally
comprStream.Free;
end ;
end;
procedure D e c o m p r e s s S t r e a m (aSource, a T a r g e t : T S t r e a m ) ;
var
decompstream: TDecompressionStream;
n R e a d : Integer;
B u f f e r : array [O. .I0231 of C h a r ;
begin
decompstream : = TDecompressionStream.Create(aSource);
try
/ / a S t r e a m D e s t . C o p y F r o m (decompstream, size) no funciona
/ / c o r r e c t a m e n t e ya q u e s e d e s c o n o c e el tamado a priori,
/ / a s i q u e utilizamos ~ 7 nc o d l g o s i m z l a r "manual"
repeat
n R e a d : = decompstream. Read (Buffer, 102 4 ) ;
a T a r g e t .Write (Buffer, n R e a d ) ;
until nRead = 0;
finally
decompStream.Free;
end ;
end ;
La unidad Classes
Esta unidad es el corazon de las bibliotecas VCL y CLX y aunque ha sufrido
muchos cambios internos desde la ultima version de Delphi, para el usuario medio
las novedades son pocas. (La mayoria de 10s cambios estan relacionados con una
integracion modificada con el IDE y estan dirigidos a 10s escritores de componentes expertos.)
Veamos una lista de lo que se puede encontrar en la unidad classes,una
unidad a la que todo programador deberia dedicar algun tiempo:
Diversos tipos enumerados, 10s punteros a metodo estandar (como
TNot i fyEvent) y muchas clases de excepcion.
Clases principales de la biblioteca, como TPersistent y TComponent,
per0 tambien muchas otras que rara vez se usaran directamente.
Clases de listas, como TList,TThreadList (una version con seguridad de threads de la lista), TInterfaceList (una lista de interfaces,
usada internamente), T C o l l e c t i o n , T C o l l e c t i o n I t e m ,
TOwnedColle ction (que sencillamente es una coleccion con un propietario), TStrings y TStringList.
-.
VV
Nuevas clases relacionadas con la interfaz, como TInter faced Per sistent, cuyo objetivo es ofrecer un mayor soporte para interfaces.
Esta clase concreta permite que el codigo Delphi mantenga una referencia
a un objeto TPersistent o a cualquier descendiente que implemente
interfaces y es un elemento principal del nuevo soporte para objetos con
interfaces del Object Inspector.
La nueva clase TRecall,usada para mantener una copia temporal de un
objeto, sobre todo para recursos basados en graficos.
La nueva clase TClassFinder, usada para encontrar una clase registrada en lugar del metodo Findclass.
La clase TThread, que ofrece el nucleo del soporte independiente del
sistema operativo para aplicaciones multithreaded.
Controles
visuales
Tecnicamente, existen grandes diferencias en el ambito interno entre una aplicacion originaria de Windows creada con la VCL y un programa transportable Qt
desarrollado con la VisualCLX. Basta decir que a1 nivel mas bajo, Windows usa
las llamadas de funcion de la API y 10s mensajes para comunicarse con controles,
mientras Qt usa metodos de clase y callbacks (rctrollamadas) de metodo direct0 y
no tiene mensajes internos. Tecnicamente, las clases Qt ofrecen una arquitectura
orientada a objetos de alto nivel, mientras que la API de Windows esta todavia
ligada a su legado de C y a un sistema de mensajes que data de 1985 (aAo en el que
se sac6 a la venta Windows). VCL ofrece una abstraccion orientada a objetos en
la parte superior de una API de bajo nivel, mientras que VisualCLX proyecta una
interfaz ya de alto nivel en una biblioteca de clases mas familiar.
NOTA: Microsoft ha llegado a1 punto de comenzar a abandonar la tradicional API de bajo nivel de Windows por ma biblioteca nativa de clases de
alto nivel, park de la arquitectura de .NET.
Si las arquitecturas subyacentes de la API de Windows y de Qt son relevantes,
las dos bibliotecas de clases de Borland (VCL y CLX) igualan la mayoria de las
diferencias, haciendo que el codigo de las aplicaciones Delphi y Kylix sea extremadamente similar. Tener una familiar biblioteca de clase sobre una plataforma
totalmente iiueva es la ventaja que adquieren 10s programadores de Delphi a1 usar
VisualCLX en Linux. Desde fuera, un boton es un objeto de la clase T B u t t o n
para ambas bibliotecas y tiene mas o menos el mismo conjunto de metodos, propiedades y eventos. En muchas ocasiones, se pueden volver a compilar 10s programas existentes para la nueva biblioteca de clase en cuestion de minutos, si no
se usan llamadas a funciones API de bajo nivel, caracteristicas dependientes de la
plataforma (como ADO o COM) o caracteristicas heredadas (como BDE).
lista). Ya que Qt es una biblioteca de C++, no se puede invocar directamente desde el cMigo en Delphi. La API de Qt es accesible a traves de una capa
de enlace, definida en la unidad Qt.pas.
Esta capa de enlace consiste en una larga lista de envoltorios para casi cada
clase Qt con el sufijo finalde H. Asi, por ejemplo, las clase de Qt Q P a i n t e r
se convierte en el tipo Q P a i n t e r H en la capa de enlace. La unidad @pas
tambitn incluye una larga lista con todos ios r n h d o s pfiblicos de
clases, transformados en funciones esthdar (no en m h d o s de clase) que
k&
1T--.
---a
-0
- -
a d,a~FcT~&
InloBsrt I Weffiavices
i
lnteld&
~
Figura 5.1. Una comparacion de las tres primeras fichas de la Component Palette
para una aplicacion CLX o una VCL.
Es aconsejable experimcntar con la creacion de una aplicacion CLX. Se encontraran pocas difcrencias en cl uso dc 10s componentcs y probablementc sc
aprccie mas esta biblioteca.
DFM y XFM
Cuando creamos un formulario cn tiempo de diseiio, este se guarda en un
archivo de definicion de formulario. Las aplicaciones tradicionales VCL usan la
extension DFM (Delphi Form Module, Modulo de formulario Delphi). Las aplicaciones CLX usan la extension XFM (Cross-platform (X) jbrm modules, Modu10s dc formulario multiplataforma o plataforma X). Un modulo de formulario es
el resultado del streaming del formulario y de sus componentes, y ambas bibliotecas comparten el mismo codigo streaming, por lo que producen un efecto bastante
similar. El formato de 10s archivos DFM y XFM, que puede basarse en una
rcpresentacion textual o binaria, es iddnlico.
Por eso, el motivo de usar dos estensiones diferentes es una simple indicacion
para programadores y para el IDE de 10s tipos de componentc que se deben esperar en esa definicion; no sc trata de trucos internos del compilador o de formatos
incompatibles.
Si quercmos convertir un archivo DFM en un archivo XFM, sencillamente
podemos dark a1 archivo otro nombre. Sin embargo, cabe esperar ciertas diferencias cn las propicdades. eventos y componcntes disponibles, de tal mod0 quc a1
abrir de nuevo la definicion de formulario para una biblioteca diferente, se ocasionarin probablemente algunas advertencias.
TRUCO:Aparentemente el IDE de Delphi escoge la biblioteca activa observando la extension del m6dulo de formulario, ignorando las referencias
de las sentencias uses. Por esa razon, hay que modificar la extension si
planearnos utilizar CLX. En Kylix, una extensibn diferente es totalmente
inutil, porque cualquier formulario se abre en el IDE como un formulario
CLX, sea cual sea su extension. En Linux, solo existe la biblioteca CLX
basada en Qt, que es la biblioteca de multiplataforma y la originaria.
Como ejemplo, hemos creado dos sencillas aplicaciones identicas, LibComp y
QLibComp, que so10 tienen algunos componentes y un controlador de eventos. El
listado 5.1 presenta las definiciones de formulario textuales de las dos aplicaciones, que han sido construidas en el IDE de Delphi siguiendo 10s mismos pasos,
tras haber escogido una aplicacion CLX o VCL. Hemos marcado las diferencias
en negrita, como se podra ver, hay unas cuantas, la mayoria relacionadas con el
formulario y su fuente. La propiedad O l d C r e a t e O r d e r es una propiedad de
legado, utilizada para que sea compatible con Delphi 3 y con un codigo mas
antiguo, 10s colores estandar tienen nombres diferentes y CLX guarda 10s rangos
de las barras de desplazamiento.
Listado 5.1. Un archivo XFM (izquierda) y un archivo equivalente DFM (derecha).
object Forml : TForml
Left = 192
Top = 107
Width = 350
Height = 210
Caption = ' Q L i b C o r n p l
Color = clBackground
VertScrollBar .Range = 161
HorzScrollBar.Range = 297
TextHeight = 13
Textwidth = 6
PixelsPerInch = 96
object Buttonl: TButton
Left = 56
Top = 64
Width = 75
Height = 25
Caption = ' A d d '
TabOrder = 0
OnClick = ButtonlClick
end
object Editl: TEdit
Left = 40
Top = 32
Width = 105
Height = 21
TabOrder = 1
Text = ' m y name'
end
object ListBoxl: TListBox
Left = 176
Top = 32
Width = 121
Height = 129
Rows = 3
1tems.Strings = (
'marc0 '
'john'
'helen' )
TabOrder = 2
end
end
'john'
'helen' )
TabOrder = 2
end
end
Sentencias uses
Las unicas diferencias entre ambos ejemplos estan relacionadas con las sentencias u s e s . El formulario de la aplicacion CLX tiene el siguiente codigo inicial:
u n i t QLibCompForm;
interface
uses
SysUtils, Types, Classes, QGraphics, QControls, QForms,
QDialogs, QStdCtrls;
Ejecucion en Linux
La cuestion real sobre la eleccion de bibliotecas se reduce a la importancia que
tenga Linux o Unicode para nosotros y para 10s usuarios. Es muy importante
destacar que si se crea una aplicacion CLX, se podra compilar de nuevo inalterada
(con el codigo fuente esacto) en Kylis que crea una aplicacion originaria de
Linux, a no ser que se haya hecho algo de programacion con la API de Windows,
en cuyo caso la compilacion condicional resulta esencial.
Como ejemplo, se ha vuelto a compilar el ejemplo QLibComp y se puede ver
en ejecucion en la figura 5 . 2 , donde tambien aparece el IDE de Kylix en accion en
un sistema KDE.
Figura 5.2. Una aplicacion escrita con CLX puede volver a compilarse directamente
bajo Linux con Kylix.
- - --
TRUCO:Para evitar que una aplicacibn CLX compile si contiene referencias a unidades VCL, se pueden desplazar las unidades VCL a un directorio
.r n&+sr ;nel..;r m n t n f i a m a t r
wvl-wIUIL
en la rub de busqueda. De
este modo. ias referkcias a unidades VCIL W e s que queden causariin an
error "Unitnot found"' (Unidad no encontrada) .
rl;4Lran+a hain
1 l4 u
h
vw,v r
u u w l w u r u
)I
La tabla 5.1 es una comparacion de 10s nombres de las unidades visuales VCL
y CLX, esceptuando la parte de la base de datos y algunas unidades a las que
raramente se hace referencia:
Tabla 5.1. Nombres de unidades equivalentes VCL y CLX.
ActnList
QActn List
Buttons
QButtons
Clipbrd
QClipbrd
ComCtrls
QComCtrls
Consts
QConsts
Controls
QControls
Dialogs
QDialogs
ExtCtrls
QExtCtrls
Forms
QForms
Graphics
QGraphics
Grids
QGrids
ImgList
QlmgList
Menus
QMenus
Printers
QPrinters
Search
QSearch
StdCtrls
QStdCtrls
Tambien podriamos convertir referencias a Windows y Messages en referencias a la unidad Qt. Algunas estructuras de datos de Windows estan ahora disponibles en la unidad Types, por lo que tal vez queramos aiiadirla a nuestros
programas CLX. Sin embargo, hay quc tener en cuenta que la unidad QTypes no
esta en la version CLX de la unidad Types de VCL; estas dos unidades no estan
relacionadas en absoluto.
ADVERTENCIA: iPreste atenci6n a las sentencias uses! Si por casualidad compilamos un proyecto que incluya un formulario CLX, per0 no actualizamos el codigo fuente del proyecto y dejarnos en el la referencia a la
unidad Forms de la VCL, el programa se ejecutara pero se detendra inmediatamente. La raz6n es que no se creo ningrin formulario VCL, por lo que
el programa finaliza directamente. En otros casos, intentar crear un formulario CLX dentro de una aplicacion VCL originaria errores en tiempo de
ejecucion. Por ultimo, el IDE de Delphi podria aiiadir incorrectamente referencias a las sentencias uses de la biblioteca erronea Y asi acabariarnos
teniendo una unica sentencia uses, que se referiria a la misma unidad,
para ambas bibliotecas, pero solo la segunda de ellas seria efectiva. Esto en
I
Parent y Controls
La propiedad P a r e n t de un control indica que otro control es responsable de
mostrarlo. Cuando dejamos un componente en un formulario en el Form Designer,
el formulario se transformara tanto en padre como en propietario del nuevo control.
Pero si dejamos el componente en un Panel, ScrollBox u otro componente
contenedor, este se convertira en su padre, mientras que el formulario seguira
siendo el propietario del control.
Cuando creamos el control en tiempo de ejecucion, sera necesario establecer el
propietario (usando el parametro del constructor C r e a t e ) , per0 habra que establecer tambien la propiedad P a r e n t o el control no sera visible.
A1 igual que la propiedad Owner, la propiedad P a r e n t posee su inverso. La
matriz C o n t r o l s , de hecho, lista todos 10s controles hijos del actual, enumerados de 0 a C o n t r o l s c o u n t - 1.
Se puede analizar esta propiedad para trabajar con todos 10s controles que
aloje otro control, utilizando en ultimo termino un metodo recursivo que opere
sobre 10s controles hijos de cada subcontrol.
dad V i s i b l e como F a l s e . Sin embargo, hay que tener en cuenta que leer el
estado de la propiedad V i s i b l e no indica si el control es realmente visible. En
realidad, si el contenedor de un control esta oculto, incluso aunque el control este
configurado como V i s ib l e , no se puede ver. Por esta razon, existe otra propiedad, Showing, que es una propiedad solo de lectura en tiempo para determinar
si el control es realmente visible para el usuario; es decir, si es visible, su control
padre tambien lo es, el control padre del control padre tambien lo es, y asi sucesivamente.
Fuentes
Normalmente se usan dos propiedades para personalizar la interfaz de usuario
de un componente, Co l o r y F o n t . Hay diversas propiedades relacionadas con
el color. La propiedad c o l o r se refiere normalmente a1 color de fondo del componente. Ademas, existe una propiedad C o l o r para las fuentes y muchos otros
elementos graficos. Muchos componentes tienen tambien las propiedades
P a r e n t c o l o r y P a r e n t F o n t , que indican si el control deberia utilizar la
misma fuente y color que su componente padre, que suele ser el formulario. Se
pueden usar estas propiedades para cambiar la fuente de cada control en un formulario configurando sencillamente la propiedad F o n t de este ultimo.
Cuando se configura una fuente, introduciendo valores para 10s atributos de la
propiedad en el o b j e c t I n s p e c t o r o utilizando el cuadro de dialog0 estandar
de seleccion de fuente, se puede escoger una de las fuentes instaladas en el sistema. El hecho de que Delphi permita usar todas las fuentes instaladas en el sistema
tiene tanto ventajas como inconvenientes. La principal ventaja es que si se tiene
instalado un cierto numero de agradables fuentes, el programa podra utilizarlas.
El inconveniente es que si se distribuye la aplicacion, estas fuentes puede que no
se encuentren disponibles en 10s ordenadores de 10s usuarios.
Si el programa utiliza una fuente que el usuario no tiene, Windows elegira
alguna otra fuente para reemplazarla. Un resultado cuidadosamente formateado
del programa puede verse arruinado por la sustitucion de fuentes. Por esta razon,
probablemente deberiamos confiar solo en las fuentes estandar de Windows (corno
MS Sans Serif, System, Arial, Times New Roman, etc.).
Colores
Existen diversas formas de fijar el valor de un color. El tip0 de esta propiedad
es T C o l o r , que no es un tipo de clase sino simplemente un tipo entero. Para
propiedades de este tipo, se puede escoger un valor de una serie de constantes de
nombre predefinidas o introducir directamente un valor. Las constantes para 10s
colores son entre otras c l B l u e , c l s i l v e r , c l W h i t e , c l G r e e n , c l R e d y
muchas mas (incluidas las aiiadidas con Delphi 6: clMone yGreen, c l SkyBlue,
c l C r e a m y clMedGray). Como una alternativa mejor, se puede utilizar uno de
10s colores usados por el sistema para indicar el estado de algunos elementos.
Este conjunto de colores es diferente en VCL y CLX.
VCL incluye colores predefinidos de Windows como el fondo de una ventana
( c l w i n d o w ) , el color del texto de un menu resaltado ( c l H i g h t l i g h t T e x t ) ,
el titulo activo ( c l A c t i v e c a p t i o n ) y el color de la cara ubicua del boton
( c l B t n F a c e ) . CLX contiene un conjunto diferente e incompatible de colores
del sistema, como c l B a c k g r o u n d , que es el color estandar de un formulario,
c l B a s e , utilizado por 10s cuadros de edicion y otros controles visuales,
c l A c t i v e F o r e g r o u n d , el color de primer plano para 10s controles activos y
c l D i s a b l e d B a s e , el control de fondo para 10s controles de texto desactivados.
Todas las constantes mencionadas aqui estan listadas en 10s archivos de ayuda de
la VCL y CLX bajo el titulo "TColor type" (Tipo TColor).
Otra opcion consiste en especificar un T C o l o r como un numero (un valor
hexadecimal de 4 bytes) en lugar de utilizar un valor predefinido. Si se utiliza esta
tecnica, se deberia saber que 10s tres bytes menores de dicho numero representan
las intensidades RGB de color del azul, verde y rojo respectivamente. Por ejemplo, el valor SO 0 FFO 0 0 0 se corresponde a un color azul puro, el valor
$ 0 0 0 0 ~ ~ 0a10verde, el valor SOOOOOOFF a1 rojo, el valor $ 0 0 0 0 0 0 0 0 a1
negro y el valor $0 0 FFFFFF a1 blanco. A1 especificar valores intermedios, se
puede obtener cualquiera de 10s 16 millones de colores posibles.
En lugar de especificar directamente estos valores hexadecimales, deberiamos
utilizar la funcion RGB de Windows, que tiene tres parametros, todos entre el 0 y
el 255. El primer0 indica la cantidad de rojo, el segundo la cantidad de verde y el
ultimo la cantidad de azul. Utilizar la funcion RGB hace que 10s programas Sean
por lo general m b faciles de leer que si usamos una constante hexadecimal sola.
En realidad, RGB es casi una funcion de la API de Windows. Esta definida por las
unidades relacionadas con Windows y no por las unidades de Delphi, per0 no
existe una funcion similar en la API de Windows. En C, existe una macro que
tiene el mismo nombre y efecto. RGB no esta disponible en CLX, por lo que hemos
escrito una version propia del siguiente modo:
function RGB ( r e d , g r e e n , b l u e : B y t e ) : C a r d i n a l ;
begin
R e s u l t : = b l u e + g r e e n * 256 + r e d * 256 * 256;
end:
imitar el color solicitado a1 dibujar un ajustado modelo de pixeles con 10s colores
disponibles. En adaptadores de 16 colores (VGA) y a gran resolution, con frecuencia acaban por verse extraiios modelos de pixeles de diferentes colores y no
del color que se tenia en mente.
NOTA:
mit&
que para cada operacion, existen diversas alternativas. Por ejemplo, se puede
mostrar una lista de valores utilizando un cuadro de lista, un cuadro combinado,
un grupo de botones de radio, una malla de cadena (srring grid), una vista en lista
o incluso una lista en arb01 si existe un orden jerarquico. Para seleccionar una de
ellas; debemos considerar cual sera la tarea de la aplicacion. Hemos elaborado un
resumen bastante conciso de las opciones alternativas para realizar algunas tareas muy comunes.
El componente Edit
El componente Edit permite a1 usuario introducir una unica linea de texto.
TambiCn se puede mostrar una linea de texto con un control Label o un control
StaticText, pero estos componentes se utilizan, por lo general, solo para texto fijo
o para salidas generadas por el programa, no para entradas. En CLX, tambien
hay un control originario de digito LCD que se puede usar para mostrar numeros.
El componente Edit usa la propiedad Text, mientras que muchos otros controles usan la propiedad Caption para referirse al texto que muestran. La unica
condicion que se puede imponer al usuario es el numero de caracteres aceptados.
Si queremos que se acepten solo unos caracteres especificos, se puede controlar el
evento OnKeyPress del cuadro de edicion. Por ejemplo, se puede escribir un
metodo que compruebe si el caracter es un numero o la tech Retroceso (quc tiene
un valor numeric0 de 8). Si no es asi. cambiamos el valor de la t e c h a1 caracter
cero (#0)>de mod0 que el control de edicion no lo procese y se produzca un sonido
de advertencia:
procedure TForml.EditlKeyPress(
Sender: TObject; var Key: Char) ;
begin
/ / v e r i f i c a s i l a t e c h es u n numero o r e t r o c e s o
(Key i n [ ' 0 ' . . ' 9 ' , # 8 ] ) then
i f not
begin
Key : = #O;
Beep;
end ;
end ;
El control LabeledEdit
Delphi 6 aiiadio un control llamado LabeledEdit, que es un control Edit
con una etiqueta adjunta. La etiqueta aparece como propiedad del control compuesto, que hereda de TCustomEdit.
Este componente es muy comodo, porque nos permite reducir el numero de
componentes de nuestros formularios, moverlos mas facilmente y tener una organizacion mas consistente para las etiquetas de todo un formulario o una aplicacion. La propiedad EditLabel esta conectada con el subcomponente, que tiene
las propiedades y eventos normales. Dos propiedades mas, LabelPosit ion y
Labelspacing, nos permiten configurar las posiciones relativas de 10s dos
controles.
Date
Long Tme
Shod T~me
ILL.-
kid*...
I T (
06/27/94
09 05 15PM
Hdp
Test Html
Test text with bold
Selection de opciones
Existen dos controles estandar Windows que permiten a1 usuario escoger diferentes opciones, asi como otros dos controles para agrupar conjuntos de opciones.
I: Integer;
Text: string;
begin
for I : = 0 to GroupBoxl.ControlCount - 1 do
if (GroupBoxl.Controls [I] as TRadioButton) .Checked then
Text : = (GroupBoxl.Contro1s[I] as TRadioButton) .Caption;
El componente RadioGroup
Delphi posee un componente similar que se puede utilizar de forma especifica
para botones de radio: el componente RadioGroup. Un RadioGroup es un cua-
El componente ListBox
La seleccion de un elemento en un cuadro de lista usa las propiedades ~t ems
e ItemIndex como en el codigo anterior para el control RadioGroup. Si hay
que acceder con frecuencia a1 texto de 10s elementos del cuadro de lista seleccionado, se puede escribir una funcion envoltorio como esta:
f u n c t i o n SelText
var
nItem: Integer;
begin
n I t e m : = List.ItemIndex;
i f n I t e m >= 0 then
Result : = List. Items [nItem]
else
Result := ' ' ;
end;
un usuario selecciona diversos elementos haciendo clic sobre ellos, mientras que
en el segundo caso, el usuario puede usar las teclas Mayus y Control para seleccionar diversos elementos consecutivos o no consecutivos, respectivamente. Esta
segunda opcion la determina el estado de la propiedad ExtendedSele ct .
En el caso de un cuadro de lista de seleccion multiple, un programa puede
recuperar informacion sobre un numero de elementos seleccionados utilizando la
propiedad selcount y se puede establecer que elementos estan seleccionados
examinando la matriz Sele cted. Dicha matriz de valores booleanos tiene el
mismo numero de entradas que un cuadro de lista. Por ejemplo, para concatenar
todos 10s elementos seleccionados en una cadena, se puede buscar en la matriz
Sele cted del siguiente modo:
var
SelItems: string;
nItem: Integer;
begin
SelItems : =
' I ;
f o r nItem : = 0 t o ListBoxl.Items.Count
i f ListBoxl. Selected [nItem] then
SelItems : = SelItems
1 do
En CLX (no como en la VCL), se puede configurar una ListBox para que
utilice un numero fijo de columnas y filas, utilizando las propiedades columns,
Row, ColumnLayout y RowLayout.De ellas, la ListBox de la VCL tiene
solo la propiedad columns.
El componente ComboBox
Los cuadros de lista acaparan mucho espacio en pantalla y ofrecen unas opciones fijas (es decir, un usuario puede escoger solo entre 10s elementos de la lista y
no puede introducir ninguna opcion que el programador no haya tenido explicitamente en cuenta). Se pueden solucionar ambos problemas utilizando un control
ComboBox,que combina un cuadro de edicion y una lista desplegable. El comportamiento de un componente ComboBox cambia mucho dependiendo del valor
de su propiedad sty1e :
pucsto que podemos sencillamente usar la propiedad Text.Un truco util y habitual para 10s cuadros combinados consiste en aiiadir un nuevo elemento a la lista
cuando un usuario introduce texto y pulsa la tech Intro. El siguiente metodo
comprueba primer0 si el usuario ha pulsado esa tecla, analizando el caracter con
el valor numeric0 (ASCII) de 13. A continuacion, verifica que el texto del cuadro
combinado no este vacio y que no esta ya en la lista (si su posicion en la lista es
menor que cero). Veamos el codigo:
procedure TForml.ComboBoxlKeyPress(
Sender: TObject; var Key: C h a r ) ;
begin
/ / s i el usuario pulsa l a tecla Intro
i f Key = Chr ( 1 3 ) then
with C o m b o B o x 3 do
i f (Text <> " I
and (1tems.IndexOf (Text) < 0 ) then
1tems.Add (Text);
end :
El componente CheckListBox
Otra ampliacion del control de cuadro de lista la representa el componente
CheckListBox, un cuadro de lista con cada uno de 10s elementos precedidos
10s grupos de colores que queremos ver en la lista (colores estandar, colores
ampliados, colores de sistema. etc.).
El componente ValueListEditor
Las aplicaciones de Delphi utilizan normalmente la estructura nombrelvalor
que ofrecen en principio las listas de cadena. Delphi 6 introdujo una version del
componente StringGrid (tecnicamente una clase descendiente de TCustomDraws t r i n g ) que se ha hecho concordar especificamente con este tip0 de listas de
cadena. El ValueListEditor tiene dos columnas en las que puede mostrar y dejar
que el usuario edite 10s contenidos de una lista de cadena con parejas nombrel
valor: como muestra la figura 5.4. Esta lista de cadena la indica la propiedad
S t r i n g s del control.
La potencia de este control se basa en que se pueden personalizar las opciones
de edicion para cada posicion de la cuadricula (grid)o para cada valor clave,
usando la propiedad solo en tiempo de ejecucion de matriz I t e m P r o p s . Para
cada elemento, se puede indicar:
Si es solo de lectura.
El numero masimo de caracteres de la cadena.
Una mascara de edicion (solicitada en ultimo termino en el evento
OnGetEditMask).
Plain Mema
rone=~
rwo=2
three-3
Figura 5.4. El ejemplo NameValues usa el componente ValueListEditor, que muestra 10s
pares nombrelvalor o clave/valor de una lista de cadena, tambien visible en un simple
campo de memo.
FirstItemProp.PickListAdd(1ntToStr
(I));
Memol.Lines : = ValueListEditor1.Strings;
ValueListEditorl.ItemProps [0] : = FirstItemProp;
f o r I : = 0 t o ValueListEditor1.Strings.Count - 1 do
ValueListEditorl.1temProps [I] : = SharedItemProp;
end ;
&a h .
Rangos
Por ultimo, existen unos cuantos componentes que podemos usar para seleccionar valores dentro de un rango. Los rangos se pueden usar para entradas numericas y para seleccionar un elemento de una lista.
El componente ScrollBar
El control independiente ScrollBar es el componente original de este grupo,
per0 rara vez se utiliza por si solo. Las barras de desplazamiento (scroll bars) se
asocian normalmente con otros componentes, como cuadros de lista y campos de
memo o se asocian directamente con formularios. En todos estos casos, la barra
de desplazamiento se puede considerar parte de la superficie de otros componentes. Por ejemplo, un formulario con una barra de desplazamiento es en realidad un
formulario que tiene una zona que parece una barra de desplazamiento pintada en
su borde, una caracteristica regida por un estilo especifico de Windows de la
ventana formulario. Con parecer, nos referimos a que no es tecnicamente una
ventana separada del tipo de componente ScrollBar. Estas barras de desplazamiento "falsas" se controlan normalmente en Delphi usando propiedades especificas del formulario y 10s otros componentes que las alojan: V e r t S c r o l l B a r y
HorzScrollBar.
El componente UpDown
Otro control relacionado es el componente U p D o w n , que suele estar conectado
a un cuadro de edicion de forma que el usuario pueda teclear un numero en el o
aumentar y disminuir el numero utilizando dos pequeiios botones de flecha. Para
conectar 10s dos controles, se define la propiedad As s o c i a t e del componente
UpDown. Podemos utilizar el componente U p D o w n como un control independiente, que muestre el valor actual en una etiqueta, o de cualquier otro modo.
I
El componente PageScroller
El control PageScroller de Win32 es un contenedor que permite desplazar el
control interno. Por ejemplo, si se coloca una barra de herramientas en la barra de
desplazamiento de la pagina y la barra de herramientas es mas grande que el
espacio disponible, el PageScroller mostrara dos pequeiias flechas en el lateral. Si
hacemos clic sobre dichas flechas se desplazara la zona interna. Este componente
se puede usar como una barra de desplazamiento, per0 tambien sustituye en parte
a1 control ScrollBox.
El componente ScrollBox
El control ScrollBox representa una zona de un formulario que se puede desplazar independientemente del resto de la superficie. Por esta razon, el ScrollBox
tiene dos barras de desplazamiento utilizadas para mover 10s componentes insertados. Podemos colocar facilmente otros componentes dentro de un ScrollBox,
como en el caso de un panel. De hecho, un ScrollBox es basicamente un panel con
barras de desplazamiento para mover su superficie interna, un elemento de la
interfaz usado en muchas aplicaciones de Windows. Cuando tenemos un formulario con muchos controles y una barra de herramientas o barra de estado, podriamos usar un ScrollBox para cubrir la zona central del formulario, dejando sus
barras de desplazamiento y barras de estado fuera de la zona de desplazarniento.
A1 confiar en las barras de desplazamiento del formulario, se podria permitir que
el usuario moviera la barra de herramientas y la barra de estado fuera de vista
(una situation muy extrafia).
Comandos
La categoria final de componentes no es tan clara como en 10s casos anteriores, y tiene que ver con 10s comandos. El componente basico de este grupo es el
T B u t t o n (o boton pulsador, en la jerga de Windows). Mas que botones independientes, 10s programadores de Delphi utilizan botones (u objetos T T o o l B u t t o n )
dentro de barras de herramientas (en las primeras fases de Delphi, se usaban
botones de atajo dentro de paneles). Ademas de botones y controles similares, la
otra tecnica clave para invocar comandos es el uso de 10s elementos de menu,
parte de 10s menus desplegables enlazados con 10s menus principales de 10s formularios o 10s menus desplegables locales que se activan mediante el boton derecho del raton.
Los comandos relacionados con el menu o la barra de herramientas entran en
distintas categorias dependiendo de su proposito y de la retroalimentacion que
ofrece su interfaz a 10s usuarios:
Comandos: Elementos del menu utilizados para ejecutar una accion.
Definidores d e estado (state-setters): Elementos del menu utilizados para
activar o desactivar una opcion o para cambiar el estado de un elemento
concreto. Los elementos de estado de estas ordenes normalmente tienen
una marca de verificacion a su izquierda para indicar que estan activos (se
puede conseguir automaticamente este comportamiento usando la propiedad A u t o c h e c k ) . Los botones generalmente se pintan en un estado presionado para indicar el mismo estado (el control ToolButton tiene una
propiedad Down).
Elementos de radio: Elementos del menu que posecn una marca circular y
estan agrupados para representar las selecciones alternativas, como 10s
botones de radio. Para obtener 10s elementos de radio del menu, hay que
configurar sencillamente la propiedad RadioItem como True y establecer la propiedad GroupIndex para 10s elementos alternatives del menu
con el mismo valor. De un mod0 similar, se pueden agrupar botones de la
barra de herramicntas que Sean mutuamente exclusives.
Enlaces d e didogo: Elementos que hacen que aparezca un cuadro de dialogo y normalmente estan indicados por tres puntos (...)despues del texto.
Comandos y acciones
Como se vera, las aplicaciones modernas de Delphi tienden a usar el componente Act ionlist o su extension ActionManager para gestionar comandos del
mcnu o dc la barra de herramientas. En pocas palabras, se define una serie dc
objetos dc accion y se asocia cada uno de ellos con un boton de la barra de
hcrramicntas ylo un elemento del menu. Se puede definir la ejccucion del comando en un unico lugar pero actualizar tanibien la interfaz de usuario conectandola
simplemente con la accion: el control visual relacionado reflejara automaticamente
cl estado del objeto de accion.
Menu Designer
Si simplemente se necesita inostrar un menu sencillo en la aplicacion, se puede
colocar un componentc MainMenu o PopupMenu en un formulario y hacer
doble clic sobre dl para lanzar el Menu Designer, que muestra la figura 5.5. Se
pueden aiiadir nuevos clementos de menu y proporcionarles una propiedad
Caption,usando un guion (-) para separar las etiquetas de 10s elementos del
menu.
Delphi crea un nuevo componente para cada elemento de menu que se aiiada.
Para dar nombre a cada componente, Delphi usa el titulo que introducimos y
adjunta un numero (de tal mod0 que Open se convierta en Openl). Debido a que
Delphi elimina espacios y otros caracteres especiales del titulo cuando crea el
nombre, si no queda nada, Delphi aiiade la IetraNal nombrc. Finalmente adjunta
el numcro, asi que 10s elementos de separation del menu se denominaran N1,N2
y asi sucesivamente. A1 saber lo que suele hacer Delphi de manera predefinida, se
deberia pensar cn editar el nombre en primer lugar. lo que es necesario si se quiere
acabar con un esquema dc nombrado de componentes sensato.
-
Una tecnica alternativa es el uso del evento OnContextMenu.Este, introducido en Delphi 5, ocurre cuando un usuario hace clic con el boton derecho del
raton sobre un componente (exactamente lo que hemos rastreado anteriormente
con la comprobacion if But ton = mbRight). La ventaja esta en que el mismo
evento ocurre tambien en respuesta a la combinacion de teclas Mayus-F10, asi
como mediante las teclas del menu de metodo abreviado de algunos teclados.
Podemos utilizar este evento para mostrar un menu contextual con el siguiente
codigo:
procedure TFormPopup.LabellContextPopup(Sender: TObject;
MousePos: TPoint; var Handled: Boolean);
var
ScreenPoint: TPoint;
begin
// a d a d e e l e m e n t o s d i n d m i c o s
PopupMenu2. Items .Add (NewLine);
PopupMenu2. Items .Add (NewItem (TimeToStr (Now), 0, False,
True, nil, 0 , ' I ) ) ;
/ / muestra el menu c o n t e x t u a l
ScreenPoint : = ClientToScreen (MousePos);
PopupMenu2.Popup (ScreenPoint.X, ScreenP0int.Y);
Handled : = True;
// e l i m i n a 10s e l e m e n t o s dindmicos
PopupMenu2. Items [4]. Free;
PopupMenu2. Items [ 3 ] .Free;
end;
Este ejemplo aiiade algo de comportamiento dinamico a1 menu de atajo, aiiadiendo un elemento temporal que indica cuando se muestra el menu contextual.
Este resultado no es particularmente util, per0 demuestra que si se necesita mostrar un simple menu contextual, se puede usar sin problemas la propiedad
PopupMenu del control en cuestion o de uno de sus controles padre. Gestionar el
evento OnContextMenu tiene sentido solo cuando se desea aiiadir algo de procesamiento adicional.
El parametro Handled se preinicia como False, de mod0 que si no hacemos nada en el controlador de eventos, el menu contextual se procesara con normalidad. Si hacemos algo en el controlador de eventos para sustituir el
TObject;
O n E n t e r u O n E x i t . Esto permite definir y personalizar el orden de las operaciones de usuario. Algunas de estas tecnicas se demuestran en el ejemplo InFocus,
que crea una ventana bastante comun de contrasefia y nombre de usuario. El
formulario tiene tres cuadros de edicion con etiquetas que indican su significado,
como muestra la figura 5.7. En la p a r k inferior de la ventana esta la zona de
estado con mensajes de peticion que guian a1 usuario. Cada elemento habra de
introducirse de forma consecutiva.
w
o
k bled ntab order
I
Activecontrol = EditFirstName
Caption = ' I n P o c u s '
o b j e c t Labell: TLabel
Caption = ' & F i r s t name'
FocusControl = EditFirstName
end
o b j e c t EditFirstName: TEdit
OnEnter = GlobalEnter
OnExit = EditFirstNameExit
end
o b j e c t Label2 : TLabel
Caption = ' & L a s t n a m e '
FocusControl = EditLastName
end
o b j e c t EditLastName: TEdit
OnEnter = GlobalEnter
end
o b j e c t Label3 : TLabel
Caption = ' & P a s s w o r d 1
FocusControl = Editpassword
end
o b j e c t Edit Password: TEdit
Passwordchar = ' * '
OnEnter = GlobalEnter
end
o b j e c t StatusBarl: TStatusBar
Simplepanel = True
end
end
El programa es muy sencillo y solo realiza dos operaciones. La primera consiste en identificar, en la barra de estado, el control de edicion que tiene el foco.
Esto lo consigue manejando el evento O n E n t e r de 10s controles, utilizando un
controlador de eventos generic0 para no repetir codigo.
En el ejemplo, en lugar de almacenar informacion adicional para cada cuadro
de edicion, hemos verificado cada control del formulario para determinar que
etiqueta esta conectada a1 cuadro de edicion actual (que indica el parametro
Sender):
procedure TFocusForm. GlobalEnter (Sender: TObject) ;
var
I: Integer;
begin
f o r I : = 0 t o Controlcount - 1 do
// s i e l c o n t r o l e s u n a e t i q u e t a
if
(Controls [ I ] i s TLabel) and
// y l a e t i q u e t a e s t d c o n e c t a d a a 1 c u a d r o d e e d i c i o n a c t u a l
(TLabel (Controls [I]) .FocusControl = Sender) then
// c o p i a e l t e x t o , s i n e l c a r d c t e r i n i c i a l
StatusBarl.Simp1eText : = ' E n t e r ' + Copy
(TLabel (Controls [I]) .Caption, 2, 1000) ;
&
end ;
// n o d e j a s a l i r a 1 u s u a r i o
EditFirstName. SetFocus;
MessageDlg ( 'Es n e c e s a r i o e l n o m b r e '
mtError,
[mbOK] , 0) ;
end
else i f EditFirstName.Text
'Adrnin' then
begin
// c u b r e e l s e g u n d o c u a d r o d e e d i c i 6 n y s a l t a a 1 t e r c e r o
EditLastNarne.Text : = ' A d r n i n ' ;
EditPassword.SetFocus;
end ;
end ;
-
--
Anclajes de control
Para permitir la creacion de una interfaz de usuario agradable y flexible, con
controlcs que se adapten a1 tamaiio real del formulario, Delphi permite determinar
la posicion relativa de un control con la propiedad Anchors. Antes de que se
introdujera esta caracteristica en Delphi 4, todo control situado en un formulario
tenia unas coordenadas relativas a 10s bordes superior e izquierda, a no ser que se
encontrara alineado con el borde inferior o de la derecha. La alineacion es aconsejable para algunos controles, per0 no para todos ellos, en particular para 10s
botones.
A1 usar anclajes. podemos hacer que la posicion de un control sea relativa a
cualquiera de 10s lados del formulario. Por ejemplo, para tener un boton anclado
en la esquina inferior derecha del formulario, colocamos el boton en la posicion
deseada y configuramos su propiedad Anchors como [akRight , a kBottom] .
A1 cambiar el tamaiio del formulario, la distancia a1 boton desde 10s laterales a 10s
que se ancla se mantiene fija, el boton permanecera en su esquina.
Por otra parte, si colocamos un componente grande como un Memo o un ListBox
en medio de un formulario, podemos configurar su propiedad Anchors para
incluir 10s cuatro lados. De este modo, el control se comportara como un control
alineado y aumentara o disminuira segun el tamaiio del formulario, per0 habra
cierto margen entre el y 10s lados del formulario.
- ..
--
m a n t e s posi6fe-mos
'deesta ca-&
i
disefiamos el formulario y tambib en tiempo de ejecuei6n.
mi&trT
Figura 5.8. Los controles del ejemplo Anchors se mueven y cambian de tamaiio
automaticamente con el formulario. No se necesita ninghn codigo adicional, solo usar
correctamente la propiedad Anchors.
Wh
-..-
Dog
cat
zard
11
hr~~np
ug
ee
Rhi Lzf
Hare
She,Sheep
para cada control del formulario, incluso para aquellos no contiguos al divisor.
Es aconsejable probar el ejemplo Split 1, para que se comprenda completamente como afecta el divisor a sus controles configuos y al resto de componentes del
Lower
Teclas aceleradoras
Desde Delphi 5, no se necesita aiiadir el caracter & a la propiedad Caption
de un elemento de menu, que proporciona una tecla aceleradora automatica si se
omite una. El sistema automatico de teclas aceleradoras de Delphi tambien puede
averiguar si hemos insertado teclas aceleradoras que resulten conflictivas y ajustarlas sobre la marcha. Esto no significa que debamos dejar de aiiadir teclas
aceleradoras personalizadas con el caracter &, porque el sistema automatico utiliza sencillamente la primera letra disponible y no sigue 10s esthdares predefinidos.
Podriamos encontrar claves mnemotecnicas mejores que las elegidas por el sistema.
Esta caracteristica es controlada por la propiedad Auto Hotkeys,disponible en el componente menu principal y en cada uno de 10s menus desplegables y
elementos del menu. En el menu principal, esta propiedad tiene de manera
predefinida el valor maAutomat i c , mientras que en 10s menus desplegables y
en 10s elementos del menu es maparent,de manera que el valor fijado para el
componente menu principal lo utilizaran de forma automatica todos 10s
subelementos, a no ser que tengan un valor especifico maAutomat i c o
maManua1.
El motor que se esconde tras este sistema es el metodo Re thin kHotkeys de
la clase TMenuItem y su compaiiero InternalRethinkHotkeys.Existe
tambien un metodo llamado RethinkLine s,que verifica si un menu desplegable posee dos separadores consecutivos o comienza o termina con un separador.
En todos esos casos, se elimina automaticamente el separador.
Una de las razones por las que Delphi incluye esta caracteristica es el soporte
para traducciones. Cuando se necesita traducir el menu de una aplicacion, resulta
comodo no tener que trabajar con teclas aceleradoras o a1 menos no tener que
preocuparse de 10s posibles problemas entre dos elementos de un mismo menu. A1
tener un sistema que pueda resolver automaticamente problemas similares, contamos en definitiva con una gran ventaja. Otro motivo era el propio IDE de Delphi.
Con todos 10s paquetes que se pueden cargar de forma dinamica, que instalan
elementos del menu en el menu principal del IDE o en 10s menus contextuales, y
con diferentes paquetes cargados en distintas versiones del producto, resulta casi
imposible conseguir teclas aceleradores no conflictivas en cada menu. Por esa
razon, este mecanismo no es un asistente que realiza un analisis estatico de 10s
menus en tiempo de diseiio, sino que se creo para resolver el problema real de la
administracion de 10s menus creados de forma dinamica en tiempo de ejecucion.
ADVERTENCIA: Esta caracteristica es realmente 6til, pero al estar activada por defecto, puede estropear el c6digo existente. Un problema puede
ser que si se usa el titulo en el cbdigo, 10s caracteres & adicionales pueden
romperlo. Aun asi, el cambio es bastante simple: todo lo que es necesario
hacer es establecer la propiedad AutoHot keys del componente menu
principal como maManual.
Sugerencias flotantes
Otro elemento habitual en las barras de herramienta es la sugerencia de la
barra, tambien llamada sugerencia flotante, un texto que describe brevemente el
boton que se encuentra en ese momento bajo el cursor. Este testo suele mostrarse
en un cuadro amarillo junto al cursor del raton que haya permanecido parado
durante un boton durante una cantidad de tiempo dada.
Para aiiadir sugerencias a la barra de herramientas de una aplicacion, sencillamente hay que definir su propiedad ShowHints como T r u e y escribir texto
para la propiedad Hint de cada boton. Podria desearse habilitar las sugerencias
para todos 10s componentes de un formulario o para todos 10s botones de una
barra de herramientas o panel.
Si queremos tener mayor control sobre como aparecen las sugerencias, podemos usar algunas de las propiedades y eventos del objeto Application. Este
objeto global tiene, entre otras; las siguientes propiedades:
Hintshortpause
// e s t a b l e c e l a cadena d e s u g e r e n c i a
HintStr
/ / se r n u e s t r a s o b r e e l e l e r n e n t o
HintPox : = HintControl.ClienteToScreen (Point ( 0 ,
ListBoxl.ItemHeight * (nItem - ListBoxl.TopIndex)));
end
else
Canshow : = False;
end :
El resultado final es que cada linea del cuadro de lista parece tener una sugercncia especifica, como muestra la figura 5.10. La posicion de la sugerencia se
calcula de tal manera que cubra el testo del elemento actual, extendiendose mas
alla del borde del cuadro de lista.
Figura 5.10. El control ListBox del ejemplo CustHint rnuestra una sugerencia
diferente, dependiendo del elemento de la lista sobre el que se encuentre el raton.
Basicamente, dichos controles saben como dibujarse. Sin embargo, como alternativa, cl sistema permite que el propietario de estos controles, un formulario por lo
general, 10s dibuje. Esta tecnica, disponible para botones, cuadros de lista, cuadros combinados y elementos de menu, se denominaowner-draw (dibujo por parte del propietario).
En la VCL, la situacion es ligeramente mas compleja. Los componentes pueden encargarse de dibujarse a si mismos en este caso (como en la clase T B i t Btn
para 10s botones de mapas de bits) y posiblemente de activar 10s eventos correspondientes. El sistema envia la solicitud para dibujar al poseedor (normalmente el
formulario) y el formulario reenvia el evento de nuevo al control adecuado, activando sus controladores de eventos. En CLX, algunos de estos controles, como
ListBoxes y ComboBoxes, presentan eventos aparentemente muy similares a la
tecnica de dibujo por parte del propietario de Windows, pero 10s menus no 10s
tienen. El enfoque nativo de Qt consiste en usar estilos para establecer el comportamiento grafico de todos 10s controles del sistema, de una aplicacion concreta o
de un control dado.
1
".
I
.
.
.. .
i\
.-
Los tres controladores para este evento de 10s elementos del menu desplegable
Shape son todos ellos distintos, aunque usan un codigo similar:
p r o c e d u r e TForml.EllipselDrawItem(Sender: TObject; ACanvas:
TCanvas ;
ARect : TRect; Selected: Boolean) ;
begin
// f i j a e l c o l o r d e l f o n d o y l o p i n t a
i f Selected then
ACanvas.Brush.Color
: = clHighlight
else
ACanvas.Brush.Color : = clMenu;
ACanvas .FillRect (ARect);
// d i b u j a l a e l i p s e
ACanvas.Brush.Co1or : = clwhite;
InflateRect (ARect, -5, -5) ;
ACanvas.Ellipse (ARect.Left, ARect.Top, ARect.Right,
ARect .Bottom) ;
end;
-- ..
NOTA: Acabaq~psd e s c r i b i r Text Height como un atributo dcl formulario, no toma"una propiedad. No se trata de una propiedad, sino de un
valor 1 4 dtsl fmulario, Si no as propiedad, cabria preguntarse cirmo es
queBelphi k Haida en-&arch*! DFM.La respuesta es que el mecanismo
de streamhg.deDelphi se basa en propiedades m i s unos clones especiales
Ile propjabdes krcadospor cl mctodo Def ineproperties.
Dado quc TextHeight no es una propiedad, aunque aparece en la lista de la
descripcion del formulario, no podemos acceder a el directamente. A1 estudiar el
codigo fuente dc la VCL, se ve que este valor se calcula mediante una llamada a
un metodo privado del formulario: GetTextHeight.A1 ser privado, no podemos llamar a esta funcion, pero podemos duplicar su codigo dentro del metodo
Formcreate dcl formulario, tras haber seleccionado la fuentc dcl cuadro dc
lista:
Canvas.Font : = ListBoxl.Font;
ListBoxl. IternHeight := Canvas .TextHeight ( ' 0 ' ) ;
I: Integer;
begin
f o r I : = L o w (Colors) t o High (Colors) d o
ListBoxl.Items.Add0bject (ColorToString ( C o l o r s [ I ] ) ,
TObject ( C o l o r s [ I ]) ) ;
end ;
Para compilar la version CLX de este codigo, hemos aiiadido la funcion RGB
descrita anteriormente. El codigo usado para dibujar 10s elementos no es especialmente complejo. Sencillamente obtenemos el color asociado con el elemento, lo
establecemos como el color de la fuente y despues dibujamos el testo:
p r o c e d u r e TODListForm.ListBoxlDrawItem(Control:
Index: Integer;
Rect : TRect; State: TOwnerDrawState) ;
begin
w i t h Control as TListbox d o
begin
Twincontrol;
// elimina
Canvas. FillRect (Rect);
// dibuja el elemento
Canvas.Font.Color : = TColor ( I t e m s - O b j e c t s [Index]);
Canvas.TextOut(Rect.Left, R e c t - T o p , Listboxl.Items[Index]);
end ;
end :
Si se intenta usar esta capacidad, se vera que algunos de 10s colores aiiadidos
se transforman en nombres de color (una de las constantes de color de Delphi),
mientras que otros se convierten en numeros hexadecimales.
Ft
W bbb
// g u a r d a e l i n d i c e
List .Add ( ' @ ' + IntToStr (ListViewl.Items [I].ImageIndex) ) ;
// g u a r d a 10s s u b e l e m e n t o s ( s a n g r a d o s )
for J : = 0 to ListViewl.Items[I].SubItems.Count - 1 do
List-Add (#9 + ListViewl. Items [I].SubItems [J]);
end;
List.SaveToFile (ExtractFilePath (App1ication.ExeName) +
I t e m s . txt ') ;
finally
List.Free;
end;
end;
El programa posee un menu que podemos emplear para escoger una de las
distintas vistas que soporta el control ListView y para aiiadir casillas de verificacion a 10s elementos, como en un control CheckListBox. Se pueden ver las distintas combinaciones de estos estilos en la figura 5.12.
Otra caracteristica importante, que es habitual en la vista detallada o de informe del control, consiste en dejar que un usuario clasifique 10s elementos de una de
las columnas. En la VCL, esta tecnica requiere tres operaciones. La primera es
View
I-
Borland
Develo..
in Java
Heb
Delohi
Delohi
Delahi
Masterinq
The
Delohi
Delphi ...
fib ~ i mr ~ l p
nQ
TObject;
Aunque este ejemplo no incluye todas las caracteristicas, muestra parte del
potencial del control ListView. Tambien hemos activado la caracteristica de "seguimiento activo", que permite que la vista en lista resalte y subraye el elemento
que se encuentra bajo el raton. Las propiedades mas relevantes de ListView podemos verlas en su descripcion textual:
Para crear la version CLX de este ejemplo, QRefList, hub0 que emplear solo
una de las listas de imagenes y desactivar 10s menus de imagenes pequeiias e
imagenes grandes, dado que ListView esta limitado a 10s estilos de vista detallada
y en lista.
Los iconos grandes y pequeiios estan disponibles en un control diferente, denominado Iconview. Como ya se comento, el soporte de clasificacion ya se encuentra ahi, lo que podria haber ahorrado la mayoria del codigo de este ejemplo.
Un arbol de datos
Ahora que hemos visto un ejemplo basado en ListView, podemos examinar el
control TreeView (arbol de datos). El TreeView posee una interfaz de usuario que
es flexible y potente (con soporte para editar y arrastrar elementos). Tambien es
estandar, porque es la interfaz de usuario del explorador de Windows. Existen
propiedades y diversos modos de personalizar el mapa de bits de cada linea o de
cada tipo de linea.
Para definir la estructura de nodos del TreeView en tiempo de diseiio, podemos
emplear el editor de propiedades TreeView Items:
de
(3
eded
rkd
Sin cmbargo, cn este caso, hcmos decidido cargarlo en 10s datos del TreeView
a1 arrancar, de un mod0 similar a1 del ultimo ejemplo.
La propicdad Items del componente TrccView time muchas funciones mienibro que podcmos emplear para modificar la jerarquia de cadenas. Por ejcmplo,
podemos crcar un arb01 de dos niveles con las siguientes lineas:
var
Node: TTreeNode;
begin
Utilizando cstos dos metodos (Add y Addchild), podemos crear una compleja estructura en tiempo de ejecucion. Para cargar la informacion, podemos
emplear dc nuevo una StringList en tiempo de ejecucion, cargar un archivo de
testo con la informacion y analizar la estructura gramaticalmente.
Sin embargo. dado que el control Treeview posee un mdtodo LoadFromFile,
10s ejemplos DragTree y QDragTree utilizan el siguiente codigo, mucho mas
sencillo:
procedure TForml.FormCreate(Sender:
begin
TreeViewl.LoadFromFile
(Application.ExeName) +
'TreeText. txt ' ) ;
TObject);
(ExtractFilePath
end;
en Delphi.) Los datos que hemos preparado para TrccView corrcsponden a1 organigrama dc una cmpresa multinacional. Los datos preparados para el TrceView
forman cl diagrama de la organizacion de una emprcsa multinacional, como muestra
la figura 5 . 1 3 .
3 US Hsadquarle~s
Q Board d Dnectors
Mon~caRoss
Sales
Fat E j s t
Steve Fubens
Palls
Tck1o
Sirmoilre
Terty Merks
Q Sidney
John Calgary
Matk R m n
!3 J o h Rcitsr
Ian Green
El AdmLi~sl~ation
Figura 5.13. El ejernplo DragTree despues de cargar 10s datos y expandir las ramas.
En lugar dc cspandir 10s elementos de 10s nodos uno a uno. tambien se puedc
usar cl mcnu File>Expand All de este programa. quc llama a1 metodo
FullExpand del control TreeView o e.jecuta el codigo cquivalente (en este caso
especifico dc un arb01 con un elemento raiz):
TreeViewl. Items
[0] .Expand ( T r u e );
Ademas de cargar 10s datos, el programa 10s guarda a1 finalizar y asi hacc que
10s cambios Sean permanentes. Tambien hay unos cuantos elementos de menu
para personalizar la fucnte del control TreeView y modificar otros sencillos
paramctros. La caracteristica especifica implementada en cste ejemplo es el soporte para arrastrar elementos y subarboles enteros. Hemos definido la propiedad
DragMode del componente como dmAutomatic y escrito 10s controladores de
eventos para 10s eventos OnDragOver y OnDragDrop.
En el primer0 de 10s dos controladores, el programa se asegura que el usuario
no esta intentando arrastrar un elemento sobre un elemento hijo (que seria desplazado junto con el elemento y originaria una repeticion infinita):
p r o c e d u r e TForml.TreeViewlDragOver(Sender, Source: TObject;
X, Y: Integer; State: TDragState; var Accept: Boolean);
var
TargetNode, SourceNode: TTreeNode;
begin
TargetNode : = TreeViewl GetNodeAt (X, Y ) ;
/ / a c e p t a a r r a s t r e d e s d e e l mismo
i f (Source = Sender) a n d (TargetNode <> nil) t h e n
begin
Accept : = True;
/ / determina origen y destino
SourceNode : = TreeViewl-Selected;
// busca la cadena padre destino
while (TargetNode.Parent <> nil) and (TargetNode <>
SourceNode) d o
TargetNode : = TargetNode.Parent;
/ / si se encuentra el origen
if TargetNode = SourceNode then
/ / no permite el arrastre a un hijo
Accept : = False;
end
else
Accept : = False;
end;
El efecto conseguido por este codigo es que (a escepcion del caso concreto que
se necesita evitar) un usuario puede arrastrar un elemento del TreeView a otro. Es
sencillo escribir el codigo necesario para desplazar 10s elementos, porque el control TreeView ofrece soporte para dicha operation, mediante el metodo MoveTo
de la clase TTreeNode.
procedure TForml.TreeViewlDragDrop(Sender, Source: TObject; X,
Y: Integer) ;
var
TargetNode, SourceNode: TTreeNode;
begin
TargetNode : = TreeViewl .GetNodeAt ( X , Y) ;
if TargetNode <> nil then
begin
SourceNode : = TreeViewl.Selected;
SourceNode.MoveTo (TargetNode, naAddChildFirst);
TargetNode .Expand (False);
TreeViewl.Selected : = TargetNode;
end;
end;
- -
--
---
--
--
NOTA: Entre las demos incluidas con Delphi, hay una muy interesante que
muestra un control TreeView de dibujo personalizado. El ejemplo se encuentra en el subdirectorio CustomDraw.
La version adaptada de DragTree
Y a que este programa puede usarse en demostraciones de adaptacion y
portabilidad, hemos creado una version que se puede compilar como una aplicacion VCL nativa con Delphi y como una aplicacion CLX con Kylix. Esto es algo
distinto de la mayor parte de 10s programas de este libro, que pueden adaptarse a
Delphi usando VisualCLX y tambien Qt sobre Windows. Seguir un camino dis-
tinto de vez en cuando puede resultar interesante. Lo primer0 que resulta necesario hacer es usar dos conjuntos distintos de sentencias uses,mediante la compilacion condicional. La unidad del ejemplo PortableDragTree comienza de esta
manera:
unit TreeForm;
interface
uses
SysUtils, Classes,
{SIFDEF LINUX]
Una directiva condicional similar se usa en la seccion inicial de la implementacion, para incluir el archivo de recursos adecuado para el formulario (10s dos
archivos de recursos son distintos):
{ $ IFDEF
LINUX I
{ $ R * .xfm)
{SENDIF]
{ $ IFDEF MSWINDOWS]
{SR *.dfm]
{SENDIF]
LINUX]
+ '/
ShGetSpecialFolderPath (Handle, P C h a r ( p a t h ) ,
CSIDL-PERSONAL, False) ;
path : = PChar (path); // cadena d e longitud fija
filename : = path + ' \TreeText.txt'
{SENDIP}
TreeViewl.LoadFromFile
end ;
(filename);
Como esta tecnica es muy comun, hemos creado un ejemplo para explicarla de
forma pormenorizada. El ejemplo CustomNodes no se centra en un caso del
mundo real, sin0 que ilustra una situacion bastante compleja, en la que existen
dos clases de nodos de arbol personalizados diferentes, derivados el uno del otro.
La clase basica aiiade una propiedad E x t r a C o d e , proyectada a metodos virtuales,
y la subclase sobrescribe uno de esos metodos. En el caso de la clase basica, la
funcion G e t E x t r a C o d e devuelve sencillamente el valor, mientras que en el de
la clase derivada, el valor se multiplica por el valor del nodo padre. Veamos las
clases y este segundo metodo:
type
TMyNode = c l a s s (TTreeNode)
private
FExtraCode: Integer;
protected
p r o c e d u r e SetExtraCode (const Value: Integer) ; virtual;
function GetExtraCode: Integer; virtual;
public
property ExtraCode: Integer r e a d GetExtraCode w r i t e
SetExtraCode;
end ;
TMySubNode = class (TMyNode)
protected
f u n c t i o n GetExtraCode: Integer; override;
end ;
f u n c t i o n TMySubNode.GetExtraCode: Integer;
begin
Result : = fExtraCode
end:
El programa establece esta referencia de clase antes de crear nodos para cada
tipo, por ejemplo, con un codigo como el siguiente:
var
MyNode : TMyNode ;
begin
CurrentNodeClass : = TMyNode;
MyNode : = TreeViewl. Items .Addchild
(nValue)) as TMyNode;
MyNode.ExtraCode : = nValue;
'I
Creacion
de la interfaz
de usuario
arquitectura Action Manager una interfaz mejor y mas moderna, que soporta la
apariencia y comportamiento de XP. En Windows XP se pueden crear aplicaciones que se adapten a1 tema activo, gracias sobre todo a1 nuevo codigo interno de la
VCL. Este capitulo trata 10s siguientes temas:
Formularios de varias paginas.
Paginas y pestafias.
Componentes ToolBar y StatusBar.
Temas y estilos.
Acciones y listas de acciones.
Acciones predefinidas en Delphi.
Los componentes ControlBar y CoolBar.
Anclaje de barras de herramientas y otros controles
La arquitectura Action Manager.
Pagecontrols y Tabsheets
Como es habitual, cn lugar de repetir la lista de propiedades y metodos dcl
sistcma dc ayuda del componente PageControl. hemos creado un ejenlplo quc
bosqueja sus capacidadcs y permite modificar su comportamiento en ticmpo de
ejccucion. El cjcmplo, denominado Pagcs, tienc un PageControl con tres paginas.
La estructura del PageControl y de otros componentes clave se muestra en el
listado 6.1.
Listado 6.1. Secciones clave del DFM del ejemplo Pages.
object Farml: TForml
BorderIcons = [biSystemMenu, biMinimize]
Borderstyle = bssingle
Caption = ' P a g e s T e s t '
OnCreate = Formcreate
object PageControll: TPageControl
Activepage = TabSheetl
Align = alClient
HotTrack = True
Images = ImageListl
MultiLine = True
object TabSheetl: TTabSheet
Caption = ' P a g e s '
object Label3 : TLabel
object ListBoxl: TListBox
end
object TabSheet2: TTabSheet
Caption = 'Tab S i z e '
ImageIndex = 1
object Labell: TLabel
// o t r o s c o n t r o l e s
end
object TabSheet3: TTabSheet
Caption = ' T a b t e x t '
ImageIndex = 2
object Memol: TMemo
Fijese en que las solapas estan conectadas a mapas de bits mediante un control
ImageList y en que algunos controles usan la propiedad A n c h o r s para mantener
una distancia fija con 10s bordes derecho o inferior del formulario. Aunque el
formulario soporte un reajuste del tamaiio (esto hubiera resultado mucho mas
complicado de realizar con tantos controles), las posiciones pueden variar cuando
las solapas aparecen en varias lineas (simplemente aumenta la longitud de 10s
titulos) o en el lateral izquierdo del formulario.
Cada objeto T a b S h e e t tiene su propio C a p t i o n , que aparecera en la solapa de la hoja. En tiempo de diseiio, podemos usar el menu local para crear fichas
nuevas y para movernos por ellas. Podemos ver el menu local del componente
P a g e C o n t r o 1 en la figura 6.1, junto con la primera ficha. Esta ficha contiene
un cuadro de lista y un pequeiio titulo y comparte dos botones con las otras fichas.
Si colocamos un componente en una ficha, esta disponible solo en dicha ficha.
Para tener el mismo componente (en este caso, dos botones de mapas de bits) en
cada ficha, sin duplicarlo, sencillamente hay que colocar el componente en el
formulario, fuera del Pagecontrol (o antes de alinearlo con la zona de cliente) y,
a continuacion, moverlo hacia delante de las fichas, mediante la orden B r i n g
To F r o n t del menu local del formulario. Los dos botones que hemos colocado
en cada ficha se pueden usar para mover las fichas adelante y atras, y ofrecen una
alternativa a1 uso de las solapas. Veamos el codigo asociado a uno de ellos:
procedure TForml.BitBtnNextClick(Sender:
begin
PaqeControll.SelectNextPaqe (True);
end;
TObject);
a,
Tabs T&
I
Ckk on lha Mbcx
to change papc
New_P q e
Mxt Pagl
Control
Add tu R_epo.;itory.. .
frcw as Text
TextDFM
- .-
Figura 6.1. La prlmera h o p de Pagecontrol del ejemplo Pages con su menu local.
La segunda pagina contiene dos cuadros de edicion (conectados a dos componentes UpDown), dos casillas de verificacion y dos botones de radio, como muestra la figura 6.2. El usuario puede escribir un numero (o escogerlo, pulsando
sobre 10s botones de Flecha arriba o Flecha abajo con el raton o pulsando las
teclas de cursor arriba o abajo mientras el foco esta en el cuadro de edicion que
corresponda), marcar las casillas de verificacion y 10s botones de radio y, a continuacion, hacer clic sobre el boton Apply para realizar las modificaciones:
p r o c e d u r e TForml.BitBtnApplyClick(Sender: TObject);
begin
// e s t a b l e c e a n c h o , a l t o y l i n e a s de l a s o l a p a
PageControll.TabWidth : = StrToInt (EditWidth.Text);
PageControll.TabHeight : = StrToInt (EditHeight.Text);
PageControll.MultiLine : = CheckBoxMu1tiLine.Checked;
// muestra u o c u l t a l a dltima solapa
TabSheet3.TabVisible : = C h e c k B o x V i s i b l e . C h e c k e d ;
/ / f i j a l a p o s i c i o n de l a s o l a p a
i f RadioButtonl-Checked t h e n
PageControll.TabPosition : = tpTop
else
PageControll.TabPosition : = tpleft;
end;
Figura 6.2. La segunda pagina del ejemplo puede ser usada para ajustar el tamaiio y
posicion de las pestaiias, que aqui se muestran a la izquierda de la pagina.
Con este codigo, podemos cambiar cl ancho y la altura de cada solapa (recuerde que 0 significa que el tamaiio se calcula automaticamente a partir del espacio
que ocupa cada cadena). Podemos escoger tener diversas lineas de solapas o dos
pequeiias flechas para recorrer la zona de la solapa. y podemos moverlas al lateral izquierdo de la ventana. El control tambien permite situar las solapas en la
parte inferior o a la derecha, pero no nuestro programa, porque de ese mod0 la
colocacion de 10s otros controles resultaria bastante compleja. Tambien podemos
ocultar la ultima solapa del Pagecontrol, que corresponde a1 componente
T a b s h e e t 3 . Si ocultamos una de las solapas definiendo su propiedad
T a b V i s i b l e como F a l s e , no podemos alcanzar dicha solapa haciendo clic
sobre 10s botones Next ni Previous, que se basan en el metodo S e l e c t N e x t Page.
En lugar de eso, habria que usar la funcion F i n d N e x t P a g e , que seleccionara
esa pagina incluso aunque la pestaiia no sea visible. La Nueva version del controlador de eventos OnCl i c k del boton Next muestra una llamada a1 metodo
FindNext Page:
procedure TForml.BitBtnNextClick(Sender: TObject);
begin
PageControl1.ActivePage : = Pagecontroll-FindNextPage (
PageControll.ActivePage, True, False);
end;
Finalmente, el ultimo boton, Add Page, nos permite aiiadir una nueva hoja de
solapa a1 control de ficha, aunque el programa no aiiada ningun componente a la
misma. El objeto hoja de solapa (en blanco) se crea usando el control ficha como
su propietario, per0 no funcionara a no ser que tambien se configure la propiedad
P a g e c o n t r o l . Sin embargo, antes de hacer esto, deberiamos hacer que la nueva solapa fuera visible. Veamos el codigo:
procedure TForml.BitBtnAddClick(Sender: TObject);
var
strcaption: string;
NewTabSheet: TTabSheet;
begin
strcaption : = ' N e w t a b ' ;
if InputQuery ( ' N e w t a b ' , ' T a b C a p t i o n ' , strcaption) then
begin
/ / s e a d a d e una n u e v a f i c h a e n b l a n c o a 1 c o n t r o l
NewTabSheet : = TTabSheet-Create (PageControll);
NewTabSheet.Visible : = True;
NewTabSheet.Caption : = strcaption;
NewTabSheet-Pagecontrol : = PageControll;
PageControl1.ActivePage : = NewTabSheet;
// s e a d a d e a ambas l i s t a s
TRUCO:Siempre que escribimos un formulario basado en un PageControl, debemos recordar que la primera ficha que aparece en tiempo de ejecucion es la ficha en la que nos encontrabamos antes de compilar el codigo.
Esto significa que si estamos trabajando en la tercera ficha y a continuacion compilamos y ejecutamos el programa, este arrancara mostrando dicha ficha. Una forma comun de resolver este problema consiste en aiiadir
una linea de codigo al metodo Formcreate para fijar el Pagecontrol o
el cuaderno de notas en la primera ficha. De este modo, la ficha actual en
tiempo de disefio no determinara la ficha inicial en tiempo de ejecucion.
'
Figura 6.3. La interfaz del visor de mapas de bits del ejemplo BmpViewer, con
pestahas dibujadas por el propietario.
Dcspues de mostrar las nuevas solapas, tenemos que actualizar la imagen para
que se corresponda con la primera solapa. Para esto, el programa llama a1 metodo
concctado con el evento OnChange de Tabcontrol, que carga el archivo
correspondiente a la solapa actual en el componente imagen:
procedure TFormBmpViewer.TabControllChange(Sender:
begin
Imagel.Picture.LoadFromFi1e (TabControll.Tabs
[TabControll.TabIndex]);
end;
TObject);
TObject);
begin
// i n t e n t a c o l o c a r l a f i c h a
T a b N u m : = T a b C o n t r o l l .Tabs. IndexOf ( ' C l i p b o a r d ' ) ;
i f TabNum < 0 then
// c r e a u n a n u e v a f i c h a p a r a C l i p b o a r d
T a b N u m : = TabControll. Tabs .Add ( ' C l i p b o a r d ' ) ;
// v a a l a f i c h a C l i p b o a r d y h a c e q u e s e p i n t e d e n u e v o
TabControll.TabIndex : = TabNum;
TabControllChange (Self);
end;
Este programa pega el mapa de bits del portapapeles cada vez que cambiamos
de solapa. El programa almacena solo una imagen cada vez y no tiene forma de
almacenar el mapa de bits Clipboard. Sin embargo, si el contenido del portapapeles
cambia y el formato de mapa de bits ya no esta disponible, la solapa Clipboard se
borra automaticamente (como se puede ver en el listado anterior). Si no quedan
mas solapas, el componente Image esta oculto.
Una imagen tambien puede eliminarse utilizando una de las dos ordenes del
menu: Cut o Delete . Cut elimina la solapa despues de hacer una copia del
mapa de bits en el portapapeles. En la practica, el metodo Cut lClick no hace
nada a parte de llamar a 10s metodos CopylClick y DeletelClick. El
TObject);
else
TabBmp.LoadFromFile (TabText);
OutRect .Left : = 0utRect.Left - BmpSide - 3;
OutRect .Right : = OutRect .Left + BmpSide;
Contro1.Canvas.StretchDraw
(OutRect, TabBmp);
end;
El programa tiene tambien soporte para imprimir el mapa de bits actual, tras
haber mostrado un formulario de vista previa de la ficha, en el que el usuario
puede seleccionar la escala apropiada. Esta parte adicional del programa no se
comenta en detalle, per0 el codigo esta ahi para quien desee examinarlo.
Tras activar el boton Back comiin. el programa cambia la ficha activa y, por
ultimo, dcsplaza la park grafica a la nucva ficha. Como este codigo ha de repetirse para cada boton, lo hemos colocado en un metodo despues de aiiadir unas
cuantas caractcristicas adicionales. Este es cl codigo actual:
p r o c e d u r e TForml.btnNextlClick(Sender:
begin
i f Check1nprise.Checked then
MoveTo (TabSheet2)
else
M o v e T o ( T a b S h e e t 3 );
end;
TObject);
Bevell-Parent : = PageControll.ActivePage;
Imagel-Parent : = PageContro1l.ActivePage;
end;
11)
Con este codigo, el usuario puede volver varias fichas a t r b hasta que la lista
quede vacia, punto en el que el boton Back se desactiva. La complicacion a la que
hemos de enfrentarnos es que mientras nos movemos desde una ficha concreta,
sabemos que ficha es su ficha "posterior" y "anterior", per0 no sabemos de que
ficha se procede, porque hay diversas rutas para llegar a una ficha. Solo podemos
retroceder de forma segura, si guardamos la pista de 10s movimientos con una
lista.
El resto del codigo del programa, que simplemente muestra algunas direcciones de sitios Web, es muy sencillo. Lo bueno es que podemos reutilizar la estructura de desplazamiento de este ejemplo en nuestros programas y modificar solo la
parte grafica y el contenido de las paginas. En realidad, como la mayoria de las
etiquetas de 10s programas muestran direcciones HTTP, un usuario puede hacer
clic sobre dichas etiquetas para abrir el explorador predefinido para que muestre
esa pagina. Para ello, se extrae la direccion HTTP de la etiqueta y se llama a la
funcion ShellExecute.
procedure TForml.LabelLinkClick(Sender: TObject);
var
Caption, StrUrl: string;
begin
Caption : = (Sender as TLabel) .Caption;
Este metodo esta enganchado a1 evento oncl i c k de varias etiquetas del formulario, que se han transformado en enlaces a1 configurar su cursor como una
mano. Esta es una de las etiquetas:
o b j e c t Label2 : TLabel
Cursor = crHandPoint
Caption = 'Main site: http://www. borland. corn'
OnClick = LabelLinkClick
end
El control ToolBar
Para crear una barra de herramientas, Delphi incluye un componente ToolBar
especifico, que encapsula el control comun de Win32 correspondiente o el widget
Qt correspondiente en VisualCLX. Dicho componente proporciona una barra de
herramientas, con sus propios botones y tiene muchas capacidades avanzadas.
Para usarlo, lo colocamos en un formulario y, a continuacion, usamos el editor de
componentes (el menu de metodo abreviado activado con un clic del boton derecho del raton) para crear unos cuantos botones y separadores.
L a b a r r a de herramientas esta compuesta por objetos de l a clase
TToo lBut ton.Dichos objetos tienen una propiedad bbica, Style,que determina su comportamiento:
NOTA: En una aplicacitin, por lo general deberiamos crear barras de herramientas utilizando una ActionList o la reciente arquitectura Action Manaizer. En ese caso.,aDenas asinnaremos com~ortamientoalrmno a 10s botones
de la barra de herramientas, puesto que sus propiedades y eventos serhn
administrados por 10s componentes de accion. Aun mas, se acabad usando
El ejemplo RichBar
Como ejemplo del uso de una barra de herramientas, hemos creado la aplicacion RichBar, que tiene un componente RichEdi t con el que se puede trabajar
utilizando la barra de herramientas. El programa tiene botones para cargar y
guardar archivos, para las operaciones de copiar y pegar y para cambiar algunos
de 10s atributos de la fuente en uso.
Pretendemos centrarnos aqui en caracteristicas especificas de la ToolBar utilizadas por el ejemplo y visibles en la figura 6.5. Esta barra de herramientas tiene
botones, separadores e incluso un menu desplegable y dos cuadros combinados.
This document explains how do you create a simple editor based on the RichEdit control using Delphi
6 The program has a toolbar andmplements a number of features, mcluding a complete scheme for
opening and saving the text Ues, drscussed m this document. In fact, we want to be able to ask the
user to save any modified Ue before opening a new one, to avoid losmg any changes. Sounds k e a
professional apphcaboh doesn't it?
l ~ i l eOperations
The most complex part of this program is implemenhng the commands of the File pull-down menuNew. Ope& Save, and Save As. In tach case. we need to track Mether the current Ue has changed,
C I A ,,l..:C:rLr.
nr-
AA..ld
.,.
*.I.,
...,- .,..,
a-
i
,
-
,
,
,
,
6,.
,i'
Es un poco mas avanzado conocer cuando deberian habilitarse estas operaciones (y 10s botones correspondientes). Podemos activar 10s botones Copy y Cut
cuando se selecciona algo de texto, en el evento onselect ionchange del
control RichEdit:
p r o c e d u r e TFormRichNote.RichEditSelectionChange(Sender:
TObject) ;
begin
tbtnCut.Enabled : = R i c h E d i t - S e l L e n g t h > 0;
t b t n C o p y . E n a b l e d : = tbtnCut.Enabled;
end;
TObject;
procedure TFormRichNote.BoldExecute(Sender:
begin
with RichEdit.SelAttributes do
i f fsBold i n Style then
Style : = Style - [fsBold]
else
Style : = Style + [fsBold];
end;
TObject);
Cada elemento del menu tiene un indicador del tamaiio real de la fuente, que se
activa mediante un controlador de eventos compartido:
p r o c e d u r e TFormRichNote.SetFontSize(Sender: TObject);
begin
RichEdit.SelAttributes.Size : = (Sender as TMenuItem) .Tag;
end;
(RichEdit.Font.Name)
El cuadro combinado muestra inicialmente el nombre de la fuente predeterminada utilizada en el control RichEdit, configurada en tiempo de diseiio. Ese valor
se calcula de nuevo cada vez que la seleccion actual cambia, utilizando la fuente
del texto seleccionado, junto con el color actual para el ColorBox:
p r o c e d u r e TFormRichNote.RichEditSelectionChange(Sender:
TObject) ;
begin
ComboFont.ItemIndex : = ComboFont.Items.IndexOf
(RichEdit.Se1Attributes.Name);
ColorBoxl.Selected : = RichEdit.SelAttributes.Co1or;
end;
Cuando seleccionamos una nueva fuente del cuadro combinado, ocurre lo contrario. El texto del elemento en uso del cuadro combinado se asigna como nombre
de la fuente para cualquier texto seleccionado en el control RichEdit:
realizar las mismas operaciones mediante el Object Tree View.) Cada subpanel
posee sus propios atributos graficos. que podemos personalizar usando el Object
Inspector. Otra caracteristica del componente barra de estado es la zona de
"control del tamaiio", aiiadida en la esquina inferior derecha de la barra, que
rcsulta muy util para ajustar el tamaiio del propio formulario. Sc trata de un
elemento comun de la interfaz de usuario de Windows y que podemos controlar en
parte con la propiedad SizeGrip (se auto-inhabilita cuando el formulario no
resulta redimensionable).
Una barra de estado tiene varias funciones. La mas comun es mostrar informacion sobre el elemento dcl menu que el usuario haya seleccionado. Ademas de
esto. una barra de estado normalmente muestra otra informacion sobre el estado
de un programa: la posicion del cursor en una aplicacion grafica, la linea de testo
actual en un procesador de textos, el estado de las teclas de bloqueo de mayusculas y del teclado numkrico, la hora y la fecha, etc. Para mostrar informacion en un
panel, simplemente usamos su propiedad T e x t , por lo general utilizando una
expresion como:
StatusBarl. Panels [l] .Text
:=
'rnensaje';
En el ejemplo RichBar, hay una barra de estado con tres paneles: para sugerencias sobre ordenes: el estado de la tecla BloqMayiis y la posicion de edicion
activa. El componente StatusBar del ejemplo tiene en realidad cuatro paneles (es
necesario definir el cuarto para delimitar la zona del tercer panel). El ultimo panel
siempre es lo suficientemente grande como para cubrir la superficie que queda en
la barra de estado.
Los paneles no son componentes independientes, por lo que no podemos acceder a ellos por su nombre, solo por posicion, como en el anterior fragment0 de
codigo. Una buena solucion para mejorar la facilidad de lectura de un programa
consiste en definir una constante para cada panel que queramos usar y, a continuacion, usar dichas constantes al hacer referencia a 10s paneles. Este es el codigo
de ejemplo:
En el primer panel de la barra de estado se va a mostrar el mensaje de sugerencia del boton de la barra de herramientas. El programa consigue este efecto con-
II
l h s document explams how do you create a sunple ecttor based on the F x h a t control usmg Delph
6 The program has a toolbar and unplements a number of feahues, mcludmg a complete scheme for
opening and savmg the text files, hscussed m h s document In fact, we want to be able to ask the
user to save any modified file before opening a new one, to avold losmg any changes Sounds Wte a
profess~onalapphcahon doesn't ~ t ?
I~ile
Operations
The most complex p a t o f h s program 1s rnplemenhng the commands of the Fie pull-down menuNew, Open Save, and Save As In each case, we need to track whether the current file has changed,
.LA CIA -1..
.c.* L A -
rCIA- Ihs
- s c k t m to the dpboard
.Z,"
-L-..IA
*----.
.-
.L*
*-..*
.LA CIA
h--
*L-
A-a-*-.
7
-
Figura 6.6. La barra de estado del ejernplo RichBar muestra una descripcion mas
detallada que la sugerencia contextual
,.
.- .
<
>
TRUCO:Cuando la sugerencia de un mntrol estP Eompuesta dc dos cadLnas, podemos usar 10s rnktodos GetSho~tHinty GetLongHint par#
e x h e r la primera (corta) y la segunda Wga) subcada partir da
cadePa qge pasamos como pmbnetxo, qu+ sronnaltneate es.gl valor de la
brbpiedad Hint,
Por ultimo, el programa usa el tercer panel para mostrar la posicion actual de
cursor (medida en lineas y caracteres por linea) cada vez que cambia la seleccion.
Debido a que 10s valores C a r e t P O S se basan en cero (es decir, la esquina superior derecha es la linea 0, caracter O), hemos decidido aiiadir uno a cada valor
para que resulten mas razonables para un usuario que desconozca este detalle:
procedure TFormRichNote.RichEditSelectionChange(Sender: TObject);
begin
...
// a c t u a l i z a l a p o s i c i o n e n l a b a r r a d e e s t a d o
Status~ar.
Panels [sbpposition] .Text : = Format ( ' % d / % d l ,
[RichEdit .CaretPos .Y + 1, RichEdit .CaretPos.X + 11 ) ;
end;
Temas y estilos
En el pasado, un sistema operativo basado en una interfaz grafica determinaba
todos 10s elementos de la interfaz de usuario para 10s programas que se ejecutaban sobre el.
~ltimamente,Linux ha comenzado a permitir que 10s usuarios personalicen la
apariencia tanto de la ventana principal de las aplicaciones como de 10s controles
de la interfaz de usuario como 10s botones. La misma idea (que suele referirse
como slnn, pie1 o tema) ha aparecido en numerosos programas con un impact0 tan
positivo que incluso Microsoft ha comenzado a integrar este concept0 (a1 principio en programas y despues en todo el sistema operativo).
Estilos CLX
Como ya se ha comentado. en Linux (para ser mas precisos en X Window) el
usuario generalmente puede escoger el estilo de la interfaz de usuario de 10s
controles. Este enfoque esta completamente soportado por Qt y por el sistema
KDE que se basa en el. Qt ofrece unos cuantos estilos basicos, como la apariencia
de Windows, el estilo Motif y otros. Un usuario tambien puede instalar nuevos
estilos en el sistema y ponerlos a disposicion de las aplicaciones.
- --- --
- .-
- -
--
NOTA: Los estilos de 10s que hablaremos se refieren a la interfaz de usuario de 10s controles, no de 10s formularios y sus bordes. Nonnalmente esto
es configurable en 10s sistemas Linux, per0 tbcnicamente se trata de un
elemento separado de la interfaz de usuario.
El efecto es que a1 hacer doble clic sobre el cuadro de lista, se puede cambiar
el estilo actual de la aplicacion y comprobar inmediatamente su efecto en pantalla, como muestra la figura 6.7.
Temas de Windows XP
Con la aparicion de Windows XP, Microsoft ha creado una nueva version,
independiente, de la biblioteca de controles habituales. La antigua biblioteca sigue estando disponible por cuestiones de compatibilidad, de manera que un programa que se ejecute sobre XP puede escoger cual de las dos bibliotecas usar. La
principal difercncia de la nueva biblioteca es que no tiene un motor de representacion fijo, sino que confia en cl motor de temas de XP y delcga la interfaz de
usuario de 10s controles sobre cl tema actual.
Figura 6.7. El prograrna StylesDernos, una aplicacion para Windows que tiene en
este momento un poco habitual aspect0 Motif.
En Delphi 7, la VCL soporta completamentc temas, debido a una gran cantidad de codigo intcrno y a la biblioteca de administracion de temas desarrollada
originalmente por Mike Lischke. Algunas de estas nuevas caracteristicas de represcntacion son utilizadas por 10s controles visuales de la arquitectura Action
Manager. independientemente del sistema operativo sobre el que funcione. Sin
embargo, el soporte total de temas solo esta disponible para un sistema operativo
que disponga de esta caracteristicas (por el momento, Windows XP).
Incluso en XP, las aplicaciones de Delphi usan de manera predefinida el enfoque tradicional. Para soportar temas XP, se debc incluir un archivo de manifiesto
cn el programa. Se puede hacer de muchas maneras:
Colocar un archivo de manifiesto en la misma carpeta que la aplicacion. Se
trata de un archivo XML que indica la identidad y las dependencias del
programa. El archivo tiene el mismo nombre que el programa ejecutable
con una estension adicional .manifest a1 final (como MiPrograma.
exe .manifest). El listado 6.2 muestra un ejemplo de este tip0 de
archivo.
Afiadir la misma informacion en un archivo de recurso compilado dentro
de la apIicacion. Se debe escribir un archivo de recurso que incluya un
archivo de manifiesto. En Delphi 7, la VCL tiene un archivo de recurso
compilado WindowsXP .res, que se consigue a1 recompilar el archivo
WindowsXP . rc disponible entre 10s archivos fuente de la VCL. El
/>
</dependentAssernbly>
</dependency>
</assembly>
figura 6.8. Se puede comparar con las figuras 6.1 y 6.2 que muestran el mismo
programa con el tema clasico de Windows XP
Pages
Tabs S~ze
Tabs Text
[F]
Figura 6.8. El ejemplo Pages usa el tema de Windows XP actual, ya que incluye un
archivo de manifiesto.
El Componente ActionList
La arquitcctura de eventos de Delphi es mug abierta: se puede escribir un
scncillo controlador de eventos y conectarlo a 10s eventos O n C l i c k de un boton
de la barra dc hcrramientas y a un menil. Se puede incluso conectar el mismo
controlador de eventos a diferentes botones o elementos de menu, dado que el
controlador puede utilizar el parametro S e n d e r para referirse a1 objeto que
lanzo el evento. Es algo mas dificil sincronizar el estado de 10s botones de la barra
de herramientas y 10s elementos de menu. Si tenemos un elemento de menu y un
boton de la barra de herramientas y ambos accionan la misma operacion, cada vez
que se activa dicha operacion, hay que aiiadir la marca de comprobacion a1 elemento de menu y cambiar el estado del boton para que aparezca como pulsado.
Para superar este problema, Delphi incluye una estructura de gestion de eventos basada en acciones. Una accion (u orden) indica tanto la operacion que se
realiza cuando se pulsa un elemento de menu o boton que determina el estado de
todos 10s elementos conectados a dicha accion. La conexion de la accion con la
interfaz de usuario de 10s controles enlazados resulta muy importante y es el
ambito en el que podemos entender las autenticas ventajas de esta estructura.
En esta estructura de manipulacion de eventos participan diversos agentes. La
accion principal la realizan 10s objetos de la accion. Un objeto de accion tiem un
nombre, como cualquier otro componente y otras propiedades que se aplicaran a
10s controles enlazados (llamados tambien clientes de la accion). Entre dichas
propiedades estan C a p t i o n , la representacion grafica ( I m a g e I n d e x ) , el estado ( C h e c k e d , E n a b l e y V i s i b l e ) y la information para el usuario ( H i n t y
H e l p c o n t e x t ) . Tambien estan S h o r t c u t y una lista de S e c o n d a r y S h o r t C u t s , la propiedad A u t o c h e c k para acciones de dos estados, el soporte de
ayuda y una propiedad C a t e g o r y utilizada para organizar las acciones en gru-
pos logicos.
La clase basica para todos 10s objetos de accion es TBas i c A c t i o n , que
introduce el comportamiento abstract0 fundamental de una accion, sin ningun
enlace especifico ni correccion (ni siquiera a elementos de menu ni controles). La
clase derivada TC o n t a i n e dAc t i o n introduce propiedades y metodos que permiten que las acciones aparezcan en una lista de acciones o administrador de
acciones. La clase derivada TCus t omAc t i o n introduce soporte para las propiedades y metodos de 10s elementos de menu y controles que estan enlazados a
10s objetos de accion. Por ultimo, esta la clase derivada lista para ser usada,
TAction.
Cada objeto de accion esta conectado a uno o mas objetos clientes a traves de
un objeto A c t i o n L i n k. Como indica su propiedad A c t i o n , posiblemente varios controles de diferentes tipos pueden compartir el mismo objeto de accion.
Tecnicamente, 10s objetos A c t i o nL i n k mantienen una conexion bidireccional
entre el objeto cliente y la accion. El objeto ~ ci otn ~ i n k es necesario porque la
conexion funciona en ambas direcciones. Una operacion realizada sobre el objeto
(como un clic) se reenvia a1 objeto de accion y origina una llamada a su evento
O n E x e c u t e ; y una actualizacion del estado del objeto de accion se refleja en 10s
controles clientes conectados. En otras palabras, uno o mas controles cliente pueden crear un ActionLink, que se registra con el objeto de accion.
No se deberian definir las propiedades de 10s controles de cliente que se conecten a una accion, ya que esta accion sobrescribe 10s valores de propiedad de 10s
control& de cliente. Por esa razon, normalmente se deberian escribir primer0 las
acciones y despues crear 10s elementos de menu y 10s botones que se quieran
conectar con ellas. Fijese en que cuando una accion no tiene un controlador
O n E x e c u t e , el control de cliente se desactiva automaticamente (o aparece en
gris), a menos que se haya definido la propiedad D i s a b l e I f N o H a n d l e r como
False.
Normalmente, 10s controles de cliente que se conectan a acciones son elementos de menu y diversos tipos de botones (botones pulsador, casillas de verificacion, botones de radio, botones de velocidad, botones de la barra de herramientas
y similares), per0 tambien se pueden crear nuevos componentes que encajen en
esta estructura. Incluso se pueden definir nuevas acciones y nuevos objetos de
accion de enlace. Ademas de un control de cliente, algunas acciones pueden tener
tambien un componente destino. Algunas acciones predefinidas se conectan con
un componente destino especifico. Otras acciones buscan automaticamente un
componente destino en el formulario que soporte la accion especificada, empezando por el control activo.
Por ultimo, 10s objetos de accion se encuentran dentro de un componente
A c t i o n L i s t o A c t i o n M a n a g e r , la unica clase de la estructura basica que
AI I
Figura 6.9. El editor del cornponente ActionList, con una lista de acciones predefinidas
que se pueden w a r .
o b j e c t ActionBold: TAction
Category = ' E d i t '
Caption = ' & B o l d 1
Shortcut = <Ctrl+B>
OnExecute = ActionBoldExecute
end
o b j e c t ActionEnable: TAction
Category = ' Test'
Caption = ' & E n a b l e N o A c t i o n '
OnExecute = ActionEnableExecute
end
o b j e c t ActionSender: TAction
Category = ' T e s t '
Caption = ' T e s t & S e n d e r 1
OnExecute = ActionSenderExecute
end
end
, - -
teclas Control y Alt. En este y otros listados a lo largo del libro, se han
reemplazado 10s numeros por 10s valores literales, que se insertan entre 10s
simbolos < y >.
1 Calmofies:
Actions:
BQ
Las tres acciones predeterminadas del menu Edit no tienen controladores asociados, per0 estos objetos especiales tienen un codigo interno para realizar la
accion relacionada con el control de edicion o de memo activo. Estas acciones se
activan y desactivan tambien a si mismas, dependiendo del contenido del
portapapeles y de la existencia de texto seleccionado en el control de edicion
activo. La mayoria de las otras acciones tienen un codigo personalizado, menos
en el caso del objeto NoAction. A1 no tener codigo, el elemento de menu y el
boton asociado a esta orden estan desactivados, aunque la propiedad Enabled
de esta accion esta definida como True.
Hemos afiadido a1 ejemplo y a1 menu Test otra accion que activa el elemento
de menu conectado a1 objeto NoAct ion:
procedure TForml.ActionEnableExecute(Sender:
begin
TObject);
NoAction.Disable1fNoHandler : = False;
NoAction.Enabled : = True;
ActionEnable.Enabled : = False;
end ;
Definir Enabled como True,producira el resultado durante un corto periodo de tiempo, a menos que se defina la propiedad DisableIfNoHandler,
como se ha visto en el apartado anterior. Tras haber realizado esta operacion, hay
que desactivar la accion en uso, porque no es necesario dar de nuevo la misma
orden. Esta situacion es distinta a la que se produce cuando activamos una accion, como el elemento del menu Edit>Bold y su correspondiente boton de velocidad. A continuacion, vemos el codigo para la accion Bold (que tiene su propiedad
Aut oChec k fijada como True,para que no resulte necesario modificar el estado de la propiedad Checked en el codigo):
procedure TForml.ActionBoldExecute(Sender:
begin
with Memo1 . Font do
i f fsBold i n Style then
Style
:=
Style
:=
Style + [fsBold] ;
TObject);
[fsBold]
else
Style
end ;
El objeto Actioncount tiene un codigo muy sencillo, per0 muestra el funcionamiento de un controlador Onupdate. Cuando el control de memo esta
vacio, se desactiva automaticamente. Se podria haber conseguido el mismo resultad0 controlando el evento OnChange del control de memo, per0 normalmente
no es posible ni facil determinar el estado de un control controlando simplemente
uno de sus eventos. A continuacion, aparece el codigo de 10s dos controladores de
esta accion:
procedure TForml.ActionCountExecute(Sender:
begin
TObject);
ShowMessage ( ' C h a r a c t e r s :
end;
) ;
Por i~ltimo,hemos aiiadido una accion especial que comprueba el objeto remitente del controlador de eventos de la accion y obtiene otra informacion sobre el
sistema. Ademis de mostrar la clase y nombre del objeto, hemos aiiadido un
codigo que accede a1 objeto de la lista de acciones, bisicamente para mostrar
como acceder a esta informacion:
procedure TForml.ActionSenderExecute(Sender: TObject);
begin
Memol .Lines .Add ( ' C l a s e r e m i t e n t e : ' + Sender .ClassName);
Memol.Lines.Add ( ' N o m b r e d e l r e m i t e n t e : ' + (Sender as
TComponent) .Name) ;
Memol. Lines .Add ( ' C a t e g o r i a : ' + (Sender as TAction) .Category) ;
Memol.Lines.Add ('Action l i s t n a m e : ' + (Sender as
TAction) .ActionList.Name);
end;
Se puede ver el resultado de este codigo en la figura 6.1 1, junto con la interfaz
dc usuario del ejemplo. Observe que el S e n d e r no es el elemento de menu seleccionado, aunqbe el controlador esta conectado a el. El objeto S e n d e r que activa
el evcnto es la accion que intercepta la operacion de usuario.
I Fle
Edt
Test
Figura 6.11. El ejemplo Actions, con una descripcion detallada del Sender del evento
OnExecute de un objeto de accion.
Por ultimo. hay que tcner presente que tambien se pueden escribir controladorcs
para evcntos del propio objeto ActionList. que jueguen el papel de controladores
globales para todas las acciones de la lista y para el objeto global Appl i c a t i o n ;
que se dispara para todas las acciones de la aplicacion. Antes de invocar a1 evento
O n E x e c u t e de la accion, Delphi activa el evento O n E x e c u t e de la A c t i o n L i s t y el evento OnAct i o n E v e n t del objeto global A p p l i c a t ion.Estos
eventos se fiaran en la accion, ejecutando eventualmente algo de codigo compartido, y despues detendran la ejecucion (mediante el parametro H a n d l e d ) o dejaran que se propague hasta cl siguiente nivel.
Si no se asigna ningun controlador de eventos para responder a la accion, ni en
la lista de acciones, ni la aplicacion, ni en el ambito accion, la aplicacion trata de
identificar un objetivo a1 quc se pueda aplicar dicha accion.
-
- -
--
-- .
- -
NOTA: Cuando se ejecuta una accion, esta busca un control como destino
de la accion, fijhdose en el control activo, el formulario activo y en otros
controles del formulario. Por ejemplo, las acciones de edicion se refieren a1
control activo en cada momento (si hereda de T C u s tomEdi t ) y 10s controles de conjuntos de datos buscan el conjunto de datos conectado con la
fuente de datos del control data-aware que tiene el foco de entrada. Otras
acciones seguirin distintos enfoques para encontrar un componente destino, pero la idea general es compartida por la mayoria de las acciones
esthdar.
TObject);
p r o c e d u r e ~ ~ o r m ~ i c h ~ o t e . a c C u t U p d a t e ( S e n d eTObject);
r:
begin
acCut.Enabled : = RichEdit.SelLength > 0;
acCopy.Enabled : = acCut.Enabled;
end :
estilo actual, lo cual evita que no se elimine la marca de las otras dos acciones
directamente.
Este codigo es parte del evento OnUpdate de la lista de accion, ya que se
aplica a multiples acciones:
procedure TFormRichNote.ActionListUpdate(Action: TBasicAction;
v a r Handled: Boolean);
begin
// v e r i f i c a l a a l i n e a c i o n d e l p d r r a f o c o r r e s p o n d i e n t e
c a s e RichEdit.Paragraph.Alignment o f
taLeftJustify: acLeftAligned.Checked : = True;
taRightJustify: acRightAligned.Checked : = True;
tacenter: acCentered.Checked : = True;
end;
// v e r i f i c a e l e s t a d o d e l a t e c l a BloqMayus
Checkcapslock;
end ;
TFormRichNote.ChangeAlignment(Sender:
RichEdit.Paragraph.Alignment
: = TAlignment
TObject);
((Sender a s
T A c t i o n ) .Tag) ;
end;
.-- .
.,. .
4 .
tonor
. a n m n n g AD h;tn onmn f n n A n AD I n n r r n m n n n n n t m
. r u r a u u r r l u p u uu u l c a r w l r l w r w m u w ur u u u w l l r p w u r l l r u
Pnn 1R
LL'
uL
3 r
nnr
L ~ ~ pL1 ,1
..
D ; D ~ ~ ] ~ .
rjr111
--"
n m i r n t w mn
.--"-----"
-- -----------dp
-- rnntrnlpr
-------" r-----"
--- diqtintac
r n c rlictintsrc v ~ r e i n n ~dp
c 1% h i h l i n t p r n
a
"
"
-"w-
ControlBar
La barra de control (ControlBar) es un contenedor de controles y se crea simplemente colocando otros controles dentro de ella, como si lo hicieramos en un
panel (en ella no hay lista de B a n d s ) . Cada control colocado en la barra consigue
su propia zona de arrastre o agarradera (un pequeiio panel con dos lineas verticales, a la izquierda del control), incluso un boton solitario:
Por ello. generalmente, se deberia evitar colocar botones especificos dentro del
ControlBar, en su lugar se deberian colocar contenedores en 10s que se incluyan
botones. En lugar de un panel, como norma general, se deberia usar un control
ToolBar para cada seccion del ControlBar.
El ejemplo MdEdit2 es otra version de la prueba creada en este capitulo. Basicamente, se han agrupado 10s botones en tres barras de herramientas (en lugar de en
una) y dejado 10s dos cuadros combinados como controles independientes. Todos
estos componentes estan dentro del componente C o n t r o l B a r , para que el usuario 10s pueda organizar en tiempo de ejecucion, como muestra la figura 6.12.
El siguiente fragment0 de listado DFM del ejemplo MdEdit2 muestra la forma
en que se incluyen varias barras de herramientas y controles en un componente
ControlBar:
object ControlBarl: TControlBar
A l i g n = alTop
AutoSize = True
ShowHint = True
PopupMenu = BarMenu
object ToolBarFile: TToolBar
Flat = True
Images = Images
Wrapable = False
object ToolButtonl: TToolButton
Action = acNew
end
/ / mds botones. . .
end
object ToolBarEdit: TToolBar . . .
object ToolBarFont: TToolBar . . .
object ToolBarMenu: TToolBar
AutoSize = True
Flat = True
Menu = MainMenu
end
object ComboFont : TComboBox
Hint = ' F a m i l y fonts'
Style = csDropDownList
OnClick = ComboFontClick
end
object ColorBoxl: TColorBox.. .
end
Para conseguir el efecto estandar, hay que desactivar 10s bordes de 10s controles de la barra de herramientas y definir su estilo como plano. Ajustar el tamaiio
de 10s controles del mismo modo, para poder obtener una o dos filas de elementos
con la misma altura, no es tan facil como parece. Algunos controles tienen ajuste
de tamaiio automatico o diversas restricciones. Concretamente, para que el cuadro combinado tenga la misma altura que la barra de herramientas, hay que ajustar el tip0 y tamaiio de su fuente. Reajustar el tamaiio del control no tiene ningun
efecto.
La barra de control tiene tambien un menu de metodo abreviado que permite
mostrar u ocultar cada uno de 10s controles que contiene. En lugar de escribir un
codigo especifico para este ejemplo, hemos implementado una solucion mas generica (y reutilizable). El menu de metodo abreviado, llamado BarMenu, esta vacio
en tiempo de diseiio y se llena cuando arranca el programa:
procedure TFormRichNote.FormCreate(Sender:
var
I: Integer;
mItem: TMenuItem;
begin
TObject);
...
// l l e n a e l m e n u d e l a b a r r a d e c o n t r o l
for I : = 0 to ControlBar .Controlcount - 1 do
begin
mItem : = TMenuItem-Create (Self);
mItem.Caption : = ControlBar.Controls [I].Name;
mItem.Tag : = Integer (ControlBar.Controls [I]) ;
mItem-OnClick : = BarMenuClick;
BarMenu.Items.Add (mItem);
end;
El procedimiento BarMenuCl i c k es un controlador de eventos sencillo, utilizado por todos 10s elementos de menu. Usa la propiedad T a g del elemento de
menu Sender para referirse a1 elemento de la barra de control asociado a1 elemento en el metodo F o r m c r e a t e :
procedure TFormRichNote.BarMenuClick(Sender: TObject);
var
aCtrl: TControl;
begin
aCtrl : = TControl ( (Sender as TComponent) .Tag);
aCtrl-Visible : = not aCtrl.Visible;
end;
Por ultimo, el evento OnPopup del menu se usa para refrescar la marca de
verificacion de 10s elementos del menu:
procedure TFormRichNote.BarMenuPopup(Sender: TObject);
var
I: Integer;
begin
// a c t u a l i z a l a s r n a r c a s d e v e r i f i c a c i o n d e l m e n u
for I : = 0 to BarMenu.Items .Count - 1 do
BarMenu. Items [I].Checked : = TControl (BarMenu.Items
[I] .Tag) .Visible;
end ;
-TENCIA:
h a s t a r directamente una ba<a de h e n a m i e n t a s m
de
la
barra
de
c
o
n
t
d
superior a la inferior no fimciona. La barra de control
,
no ajusta su tamaiio @ra dojar la barra de herramientas durante la operaci6n de arra&re, corn hace si se arrastra la barra de herramientas a una
posicion flotantd y d&ub a la barra de conttol inferior. Se trata de un
fa110 en la VCL,,y &I muy dificil encontrar un rodeo.Como se vera en el
ejemplo MdEdit3. sepuede conseguir el efecto correct0 con un codigo distinto de soporte a la Vm.
Cuando se saca una de las barras de herramientas del contenedor, Delphi crea
automaticamente un formulario flotante. Podriamos sentirnos tentados a recuperarla cerrando el formulario flotante. Pero, eso no funciona, porque el formulario
flotante se elimina junto con la barra de herramientas que contiene. Sin embargo,
sc puede utilizar el menu de metodo abreviado de la barra de control superior,
unido tambitn a la otra barra de control, para mostrar esta barra de herramientas
oculta.
El formulario flotante creado por Delphi para albergar controles no anclados
tienc un titulo muy pequeiio, llamado "titulo de barra de herramientas", que por
defect0 no tiene ningun testo. Por ello, hemos aiiadido algo de codigo a1 evento
OnEndDock de cada control anclable para fijar el titulo del formulario recien
creado en que se ancla el control.
Para evitar una estructura de datos personalizada para esta informacion, hemos usado el texto de la propiedad Hint de estos controles (que basicamente no
se utiliza) para proporcionar un titulo aceptable:
procedure TFormRichNote.EndDock(Sender, Target: TObject; X, Y:
Integer) ;
begin
i f Target i s TCustomForm then
TCustomForm(Target) .Caption : = GetShortHint ( (Sender as
TCont rol) .Hint) ;
end;
Se puede ver el resultado del ejemplo MdEdit2 en la figura 6.13. Otra posible
ampliacion de este ejemplo podria ser aiiadir zonas de anclaje en ambos laterales
del formulario. El unico esfuerzo adicional necesario seria una rutina para orientar las barras de herramientas en vertical en lugar de en horizontal. Hacer esto
requiere conmutar las propiedades L e f t y Top de cada boton despues de inhabilitar el dimensionamiento automatico.
Figura 6.13. El ejemplo MdEdit2 permite anclar las barras de herramientas (pero no el
menu) en la parte superior o inferior del formulario, o hacerlas flotar.
Figura 6.14. El ejemplo DockTest con tres controles anclados en el formulario principal.
TObject; Source:
X, Y: Integer);
begin
Caption : = 'Docked: ' + IntToStr (Panell.DockC1ientCount);
end;
Este codigo funciona bien mientras que todos 10s controles estan anclados
inicialmente. Cuando se guarda el programa, si algun control permanece flotante,
no se vera cuando se vuelvan a cargar 10s parametros. Sin embargo, debido a1
codigo de inicializacion insertado con anterioridad, el control aparecera de todos
modos anclado a1 panel, y aparecera cuando se arrastren otros controles. No es
necesario decir que se trata de una situacion complicada. Por este motivo, despues de cargar 10s parametros, hemos aiiadido este codigo:
for i : = Panell.DockClientCount - 1 downto 0 do
begin
aCtrl : = Panell.DockClientes[i];
Panell.DockManager.GetControlBounds(aCtr1, aRect);
.-
---
--
N0TA:May quc recordar que, awque 10s panebs de h & j e hacen que una
que sur banas de herramientas puedan desaparecer o ~ s t n ren unsporic ih
diferente a la que e s h acostumbrados. No conviene abusar de 1;iS oaractensticas de anclaje, ya que a l g h usuario inexpcrto podrh-pcrderse,
Anclaje a un PageControl
Otra caracteristica importante de 10s controles de ficha es su soporte especifico para anclaje. Al anclar un nuevo control sobre un PageControl, automaticamente
se aiiade una nueva ficha que lo alberga, como se puede ver en el entorno Delphi.
Para realizar esto, simplemente hay que designar el PageControl como anclaje
anfitrion y activar el anclaje para 10s controles clientes. Esto funciona mejor si
tenemos formularios secundarios que queremos albergar. Ademas, para mover el
PageControl cornpleto a una ventana flotante y despues anclarlo otra vez, sera
necesario un panel de anclaje en el formulario principal.
Esto es exactamente lo que hemos hecho en el ejemplo Dockpage, que tiene
un formulario principal con 10s siguientes valores:
object Forml: TForml
Caption = ' Docking p a g e s '
TObject;
Button:
eat
lun
mar
n d
iue
we
.ab
dom
5
12
19
26
6
13
20
27
2
3
4
7 8 9 1 0 1 1
14 15 16
18
21 22 23 24 25
28 29 30 31
tun
mar
nw
lue
vle
db durn
1
2
9
16
3
10
17
24
4
11
18
25
5
12
19
6
13
20
27
7
14
21
28
26
8
15
22
29
30
+3Hoy: 17/05/2003
Figura 6.15. El formulario principal del ejemplo DockPage despues de que se haya
anclado un formulario al control de ficha de la izquierda.
La arquitectura de ActionManager
Hemos visto que las acciones y el componente A c t ionManager pueden
representar un papel principal en el desarrollo de las aplicaciones Delphi, ya que
permiten separar mejor la interfaz de usuario del codigo real de la aplicacion. Asi,
la interfaz de usuario puede cambiar ahora sin que eso tenga un gran impact0 en
el codigo. El inconveniente de esta tecnica es que el programador tiene mas trabajo. Para crear un nuevo elemento de menu, hay que aiiadir primer0 la accion
correspondiente, moverse a1 menu, aiiadir el elemento de menu y conectarlo con la
accion.
Para resolver este asunto y para ofrecer a 10s desarrolladores y usuarios finales algunas caracteristicas avanzadas, Delphi 6 introdujo un nuevo tip0 de estructura, basada en el componente ActionManager, que amplia sobremanera la funcion
de las acciones. De hecho, el ActionManager no solo posee una coleccion de
acciones, sino tambien una coleccion de barras de herramientas y menus asociados a ellas. El desarrollo de estas barras de herramientas y menus es completamente visual: se arrastran las acciones desde un editor de componente especial del
ActionManager hacia las barras de herramientas para acceder a 10s botones necesarios. Ademas, se puede permitir a1 usuario final de 10s programas realizar la
misma operacion y reagrupar sus propias barras de herramientas y menus, empezando por las acciones que se le ofrezcan.
En otras palabras, utilizar esta arquitectura permite construir aplicaciones con
una interfaz de usuario moderna y personalizable por el propio usuario. El menu
puede mostrar solo 10s elementos usados mas recientemente (como muchos programas de Microsoft), permitir animaciones, y muchos detalles mas. Esta estructura se centra en el componente ActionManager, per0 incluye tambien otros
componentes que se encuentran al final de la ficha Additional de la paleta:
El componente ActionManager: Es un sustituto de ActionList (pero puede utilizar tambien uno o mas ActionList existentes).
El control ActionMainMenuBar: Es una barra de herramientas usada
para mostrar el menu de una aplicacion basada en las acciones de un componente ActionManager.
El control ActionToolBar es una barra de herramientas utilizada para
albergar botones basados en las acciones de un componente ActionManager.
El componente CustomizeDlg: Contiene el cuadro de dialog0 que se puede utilizar para permitir a 10s usuarios personalizar la interfaz de una
aplicacion basada en el componente A c t ionManager.
El componente PopupActionBarEx: Es un componente adicional que deberia usarse para permitir que 10s menus desplegables sigan la misma
interfaz de usuario que 10s menus principales. Este componente no se incluye con Delphi 7, sino que se encuentra disponible como una descarga
separada.
el mejor soporte para este tip0 de descripcion). Para crear un programa de ejemplo basado en esta estructura, hay que poner en un formulario un componente
A c t ionManager,hacer doble clic sobre 61 para abrir su editor de componente,
que se muestra en la figura 6.16. Observe que este editor no es modal, asi que se
puede mantener abierto mientras realizamos otras operaciones en Delphi. Ademas
hay que tener en cuenta que este cuadro de dialogo lo muestra tambien el componente C u s tomizeDlg,aunque con algunas caracteristicas limitadas (por ejemplo, aAadir nuevas acciones esta desactivado).
Figura 6.16. Las tres paginas del cuadro de dialogo del editor de ActionManager.
rn
1.
Para que 10s valores de usuario sean permanentes, hemos conectado un archivo
(llamado settings) a la propiedad Fi leName del componente A c t ionManager.
Cuando se asigna esta propiedad, hay que introducir el nombre del archivo que se
quiere usar; a1 iniciar el programa, el ActionManager creara el archivo. La perma-
para realizar un seguimiento de la actividad del usuario. Esto es esencial para que
el sistema pueda eliminar elementos del menu que no se hayan utilizado durante
algun tiempo, desplazandolos a un menu extendido, de manera que se use la
misma interfaz de usuario adoptada por Microsoft (vease la figura 6.18).
El ActionManager no solo muestra 10s elementos utilizados con menos frecuencia, tambien permite personalizar este comportamiento de una forma mu?;
precisa. Cada barra de accion tiene una propiedad S e s s i o n c o u n t que realiza
el seguimiento dcl numero de veces que se ha ejecutado la aplicacion. Cada
A c t i o n C l i e n t e I t e m tiene una propiedad L a s t S e s s i o n y una propiedad
u s a g e c o u n t utilizada para el seguimiento de las operaciones del usuario. Observe que el usuario puede volver a poner a cero toda esta informacion dinamica
usando el boton Reset Usage Data del dialog0 de personalizacion.
El sistema calcula el numero de sesiones en las que no se ha utilizado la accion
y procesa la diferencia entre el numero de veces que se ha ejecutado la aplicacion
( s e s s i o n c o u n t ) y la ultima sesion en la que se us6 dicha accion ( L a s t s e s s i o n ) . El valor de U s a g e c o u n t se usa para mirar en el P r i o r i t y S c h e d u l e el numero de sesiones en las que no se usa el elemento que hay
establecidas para eliminarlo. En otras palabras, modificando el P r i o r i t y S c h e d u l e se puede determinar la velocidad con la que se eliminan 10s elementos, en caso de que no se usen. Tambien se puede evitar que se active este sistema
en el caso de acciones especificas o grupos de acciones. La propiedad I terns de
mos cambiar para desactivar esta caracteristica para todo un menu. Para que un
elemento especifico sea siempre visible, no importa cual sea su uso real, tambien
se puede fijar su propiedad U s a g e c o u n t como - 1.Sin embargo, las configuraciones de usuario pueden sobrescribir este valor.
Para entender un poco mejor el funcionamiento de este sistema, hemos aiiadido
una accion personalizada (Act i o n s h o w s t a t u s ) a1 ejemplo AcManTest. La
accion tiene el siguiente codigo que guarda la configuration actual del administrador de accion en un stream de memoria, lo convierte en texto y lo muestra
dentro del campo de memo:
procedure TForml.ActionShowStatusExecute(Sender: TObject);
var
memStr, memStr2: TMemoryStream;
begin
memStr : = TMemoryStream.Create;
try
memStr2 : = TMemoryStream.Create;
try
ActionManagerl.SaveToStream(memStr);
memStr.Position : = 0;
ObjectBinaryToText(memStr, memStr2);
memStr2.Position : = 0;
RichEditl.Lines.LoadFromStream(memStr2);
finally
memStr2. Free;
end ;
finally
memStr. Free;
end ;
end;
. . .>
OnItemSelected = ListActionItemSelected
end
o b j e c t VirtualListActionl: TVirtualListAction
Caption = ' Items'
OnGetItem = VirtualListActionlGetItem
OnGetItemCount = VirtualListActionlGetItemCount
OnItemSelected = ListActionItemSelected
end
object ListControlCopySelectionl: TListControlCopySelection
Caption = ' Copy'
Destination = ListBox2
Listcontrol = ListBoxl
end
object ListControlDeleteSelectionl: TListControlDeleteSelection
Caption = 'Delete'
end
object ListControlMoveSelection2: TListControlMoveSelection
Caption = 'Move'
Destination = ListBox2
Listcontrol = ListBoxl
end
end
Figura 6.19. La aplicacion ListActions tiene una barra de herrarnientas que hospeda
una lista estatica y una lista virtual.
0 Trabajo
con formularios
Si se han leido 10s capitulos anteriores, ahora deberia poder utilizar 10s componentes visuales de Delphi para crear la interfaz de usuario de una aplicacion.
Es hora de fijarse en otro elemento central del desarrollo en Delphi: 10s formularios. Se han venido usando desde el principio del libro, per0 nunca se ha descrito
en detalle lo que se puede hacer con un formulario, que propiedades puede usar o
que metodos de la clase TForm resultan de interes particular.
Este capitulo analiza algunas de las propiedades y estilos de formularios y las
tecnicas de dimcnsionamiento y position, a1 igual que su escalado y desplazamiento. Tambien hablaremos de las aplicaciones con varios formularios, el uso de
10s cuadros de dialog0 (personalizados y predefinidos), marcos y herencia visual
de formularios. Por ultimo, dedicaremos algo de tiempo a1 sistema de entrada en
un formulario, tanto mediante el teclado como mediante el raton.
Este capitulo trata 10s siguientes temas:
Estilos de formularios, de bordes e iconos de bordes.
Entrada de raton y teclado.
Dibujo direct0 sobre el formulario y efectos especiales.
Posicion, escala y desplazamiento de formularios.
Creacion y cierre de formularios.
La clase TForm
La clase T Form,incluida en la unidad Forms de la VCL, define 10s formularios en Delphi. Ahora, existe tambien una segunda definicion de 10s formularios
en la biblioteca VisualCLX. Aunque a lo largo del presente capitulo, nos referiremos principalmente a la clase de la VCL, intentaremos resaltar tambien las diferencias con la version multiplataforma que proporciona la biblioteca CLX.
La clase TForm forma parte de la jerarquia de controles de ventana, que
comienza con la clase TWinControl (o TWidgetControl). En realidad,
TForm hereda de la "casi completa" TCustomForm,que a su vez hereda de
TScrollingWinControl (o TScrollingWidget). A1 tener todas las
funciones de sus clases basicas, 10s formularios tienen una gran serie de metodos,
propiedades y eventos. Por este motivo, no vamos a intentar mostrar una lista de
todos ellos. En su lugar, a lo largo del capitulo, explicaremos una serie de tecnicas interesantes relacionadas con 10s formularios. Remarcaremos las pocas diferencias existentes entre 10s formularios VCL y 10s formularios CLX. Para la
mayoria de 10s ejemplos existe una version CLX, para que se pueda comenzar a
experimentar a1 instante con formularios y cuadros de dialogo en la CLX, a1 igual
que con la VCL. La inicial de estas versiones CLX de cada ejemplo sera la Q.
ademas de esto, este codigo realiza de forma dinarnica algo que, por lo general, se
hace con el disefiador de formularios.
Escribir este codigo es sin duda alguna pesado, per0 permite una gran flexibilidad, porque cualquier parametro puede depender de las configuraciones
externas.
La funcion Showstring Form anterior no la ejecuta un evento de otro formulario, puesto que en este programa no hay formularios tradicionales. En cambio, hemos modificado el codigo fuente del proyecto del siguiente modo:
program DynaForm;
uses
Forms,
DynaMemo in ' DynaMerno .pas ' ;
var
str: string;
begin
str : = 1 1 .
Randomize;
while Length (str) < 2000 do
str : = str + Char (32 + Random (74));
ShowStringForm (str);
Application.Run;
end.
A1 ejecutar el programa DynaForm, se obtiene un formulario de extraAa apariencia cubierto con caracteres aleatorios (como muestra la figura 7.1).
*---
-.:-A-
Figura 7.2. Forrnularios de ejemplo con diversos estilos de borde, creados por el
ejernplo Borders.
En tiempo de diseiio, el formulario siempre se muestra con el valor predeterminado para la propiedad BorderSylte, bs Si zeab le. Este valor se corresponde
con un estilo de Windows conocido como "marco fino". Cuando una ventana
principal tiene un marco fino a su alrededor, un usuario puede ajustar su tamaiio
arrastrando su borde. Este estado se manifiesta mediante unos cursores de
redimensionamiento especiales (con la forma de una flecha de dos puntas) que
aparecen cuando el usuario mueve el raton sobre el borde de esta ventana.
Una segunda opcion bastante importante de esta propiedad es bsDialog. Si
la seleccionamos, el formulario utiliza como borde el tipico marco de cuadro de
dialogo (un marco grueso que no permite que se reajuste su tamaiio).
Ademas de este elemento grafico, observe que si seleccionamos este valor
bsDialog, el formulario se transforma en un cuadro de dialogo. Esto implica
una serie de cambios: por e.jemplo, 10s elementos de su menu de sistema son
distintos y el formulario ignora algunos de 10s elementos de la propiedad
Border Icons.
seiio no ~ r o d u c ninein
e
efecto visible. De hecho. hav
diversas ~ r or ~ i e d a d e s
a
de componentes que no tienen n i n g h efecto en tiempo de diseilo, porque
evitarian que pudiesemos trabajar en el ~ o m ~ o n e n ~ m i e n tdeshrofiaras
-I
-:
-..--- ..-- _ > - r
-mos
el
programa. rno-r-ejemplo,
no poariamos reajusrar el ramano aer Prormulario con el rat6n si se convirtiera en un cuadro de c2ihlogo. Pero hay que
recordar que cuando se ejecuta la aplicaci6n, el fomulario usarh el borde
indicado.
z
~.-:---L--
-1
--
3-1
lag &nu -fbs Iform border style, estilo del h d e del formuhrio). Asi
tdremos 'Pbssingle, fbsDialog, etc.
Para comprobar el efecto y comportamiento de 10s distintos valores de la propiedad Borderstyle, hemos creado un sencillo programa llamado Borders,
tambidn disponible en version CLX como QBorders. En la figura 7.2 ya hemos
visto su resultado, per0 si se e.jecuta el e.jemplo y se observa su funcionamiento
durante algun tiempo, se entenderan mejor las diferencias entre 10s formularios.
El formulario principal dc este programa contiene solo un grupo de radio y un
boton. Tambien hay un formulario secundario, sin componentes y con la propiedad Posit ion predefinida como poDef a u l t PosOnly. Esto afecta a la posicion inicial del formulario secundario que crearemos a1 hacer clic sobre el boton.
El codigo del programa es muy sencillo. Al hacer clic sobre el boton, se crea
un nuevo formulario de forma d i n h i c a , dependiendo del elemento seleccionado
del grupo de radio:
p r o c e d u r e TForml.BtnNewFormClick(Sender: TObject);
var
NewForm: TForm2 ;
begin
NewForm : = TForm2.Create (Application);
NewForm.BorderStyle : = TFormBorderStyle
(BorderRadioGroup.ItemIndex);
NewForm. Caption : =
BorderRadioGroup.Items[BorderRadioGroup.ItemIndex];
NewForm. Show;
end;
Este codigo usa en realidad un truco: convierte el numero del elemento seleccionado en la enumeracion T FormBorderSt yle. Esta tecnica funciona porque hemos colocado 10s botones de radio en el mismo orden que 10s valores de
esta enumeracion.
El metodo BtnNew FormClic k copia a continuation el testo del boton de
radio en el titulo del formulario secundario. Este programa remite a1 T Form2, el
formulario secundario definido en una unidad secundaria del programa, guardado
como SECOND.PAS. Por esa razon, para compilar el ejemplo, habra que aiiadir
las siguientes lineas a la seccion de implernentacion de la unidad del formulario
principal:
uses
Second;
dio w fcmrddo.
El ejemplo Blcons demuestra el comportamiento de un formulario con diferentes iconos en el borde y muestra el mod0 de cambiar esta propiedad en tiempo
de ejecucion. El formulario de este ejemplo es muy sencillo: solo tiene un menu,
con un desplegable que contiene cuatro elementos de menu, uno para cada elemento posible del conjunto de iconos de borde. Hemos creado un unico metodo,
conectado con las cuatro opciones, que lee las marcas de verificacion de 10s
elementos del menu para establecer el valor de la propiedad BorderIcons.Por
tanto, este codigo sirve tambien para practicar con el trabajo con conjuntos:
procedure TForml.SetIcons(Sender: TObject);
var
BorIco: TBorderIcons;
begin
(Sender a s TMenuItem) .Checked : = not
T M e n u I t e m ) .Checked;
i f SystemMenul.Checked t h e n
BorIco : = [biSystemMenu]
else
BorIco : = [ I ;
i f MaximizeBoxl.Checked t h e n
Include (BorIco, biMaximize) ;
i f MinimizeBoxl.Checked t h e n
Include (BorIco, biMinimize) ;
i f Helpl.Checked t h e n
Include (BorIco, biHelp) ;
BorderIcons : = BorIco;
end;
(Sender a s
Figura 7.3. El ejemplo Blcons. Al seleccionar el icono de ayuda dt borde y hacer clic
sobre el boton. aparece la ayuda.
rn
( v a r Params: T C r e a t e P a r a m s ) ; override;
Este es el unico mod0 de usar 10s peculiares estilos de ventana que no cstan
directamente disponibles mediante las propiedades del formulario. Para ver una
lista de 10s estilos y estilos ampliados de ventana, se pueden estudiar en la ayuda
de la API temas como "CreateWindowMy "CreateWindowEs". Se vera que la API
de Win32 tiene estilos para estas funciones, incluidos aquellos relacionados con
las ventanas de herramientas.
Para mostrar la utilization de este metodo, hemos creado el ejemplo NoTitle,
que pcrmite crear un programa con un titulo personalizado. Primero tenemos que
eliminar el titulo estandar. per0 mantener el marco que permite ajustar el tamaiio,
definiendo 10s estilos correspondientes:
p r o c e d u r e TForml.CreateParams ( v a r Params: T C r e a t e P a r a m s ) ;
begin
i n h e r i t e d C r e a t e P a r a m s ( P a r a m s );
P a r a m s . S t y l e : = (Params.Style or ws-Popup) a n d n o t ws-Caption;
end;
Figura 7.4. El ejemplo NoTitle no posee un titulo real sin0 uno falso creado con una
etiqueta.
Para que el titulo falso funcione, debemos decirle a1 sistema que una operacion
dc raton en esta zona se corresponde con una operacion de raton sobre el titulo.
Para ello, sc puede interceptar el mensaje de Windows wm N C H i t T e s t , que
normalmente se le envia a Windows para establecer el lugar en el que esti en ese
momcnto cl raton. Cuando la accion de pulsado se realiza en la zona del cliente y
cn la etiqueta, podemos simular que el raton esta sobre el titulo definiendo el
rcsultado adecuado:
procedure TForml.HitTest ( v a r Msg: TWrnNCHitTest);
// mensaje w m _ N c H i t T e s t
begin
inherited;
i f (Msg.Result = htclient) and
(Msg.YPos < Labell.Height + Top + GetSystemMetrics
(sm-cyFrame) ) then
Msg.Result : = htcaption;
end;
..
. .
..
..
..
I '-
..I
..
..
..
..
,"On=
Aid,
....
. . . . . . . . . . . . . . . . .
. . . .l~dill
. .
......................
.
.
. . . . . . . .
.
. . . . . . . . . . .
. , . . Edit2
.
.
....
. . . . . . .
. . . .
, ....
. . . . . . . . . . . . . . . .
, . . , Edit3
. . .
.
.
.
....
. . . . . . . . . . . .
I . . . . .
I . . . . . . . . .: : : : : . : : : : . . .
.
.
.
.
I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
::::I
Por defect0 el programa no hace nada especial, a escepcion de que 10s diversos
botones de radio que se usan para activar la vista previa de la tecla:
procedure TForml.RadioPreviewClick(Sender: TObject);
begin
KeyPreview : = RadioPreview.ItemIndex <> 0;
end;
En el primer caso, si el valor del parametro Key es #13, valor que corresponde a la tecla Intro, desactivamos la operacion (definiendo Key como cero) y, a
continuacion, imitamos la activacion de la tecla Tab. Existen muchas formas de
hacer esto, per0 en este caso hemos escogido una bastante particular. Hemos
enviado el mensaje CM -DialogKey al formulario, pasando el codigo para la
tecla Tab (VK-TAB):
1: // I n t r o = T a b
i f Key = #13 t h e n
begin
Key : = #O;
Perform (CM-DialogKey,
end;
VK-TAB,
0 );
--
'O',
'U'] then
..-
.n..nhr, ,
-a,..,SL.
-AAX+;I
ry. n31:1r1 a
r..srnr
s n r r A r , +rrL....:..Ar-a,
,
,altr-r,,
IIIULUV
wu,
wpqu~uv
yv~ulur
paw G
~ J ~ Wu
Va. u a j a u w ~
G~
~ I
GIILVIIIVD
&
m- I
oars sop&.a;-
--
.^^l^l^&:I:-....
-L-.^-.:^l^
----
--^:^-^-
~ U I G V I ~ U U para U~GIUIIGS
J^
----
-.
uc IIIG~U y w s a s
asi.
NOTA: Para dibujar en el formulario, usamos una propiedad muy espeI T ? obieto TCanvas tiene dos caracteristicas distintivas:
contiene una colec ion de herramientas de dibujo (como un lhpiz, una brocha y una fuente) Y posee algunos metodos de dibujo, que usan las herra,-,..,I,,
I
A, -LA:-A, f
:
L
.
.
:
,
A:,-+,
A,
,:-,Ih
,
,
,
~ l u c u ~ ia
r s
w u a l c a . El L I ~ U uo ~uulgu
uc u v u j u UIIGGLU uc csw GJGIU~IU
uu GS
correcto, porque la imagen en pantalla no es pennanente: a1 mover otra
ventana sobre la actual se eliminara su efecto.
,+,I.
V'UI.
,
a
,
,
,
:
,
bC111VC1a.
"L
b:,,
Se puede usar esta caracteristica del programa para entender mejor como funciona el raton. Se puede hacer esta prueba: se ejecuta el programa (esta version
simple o la version completa) y se ajusta el tamafio de las ventanas del escritorio
para que el formulario del programa MouseOne o QMouseOne quede detras de
otra ventana e inactivo per0 con el titulo visible. Si ahora se mueve el raton sobre
el formulario, se podra ver que las coordenadas cambian. Este comportamiento
significa que se envia el evento OnMouseMove a la aplicacion incluso aunque su
ventana no se encuentre activa, y demuestra lo ya comentado: 10s mensajes de
raton siempre se dirigen a la ventana que se encuentra bajo el cursor del raton. La
unica excepcion a esto es la operacion dc captura de raton que enseguida comentaremos .
Adcmas de mostrar la posicion en cl titulo de la ventana, el ejemplo Mouseonel
QMouseOne puede realizar un seguimiento de 10s movimientos del raton, pintando pequeiios pixeles en el formulario si el usuario mantiene pulsada la tecla Mayus.
(De nuevo estc codigo de dibujo directo produce un resultado no permanente.)
procedure TMouseForm.FormMouseMove(Sender: TObject; Shift:
TShiftState;
X, Y: Integer);
begin
// rnuestra l a p o s i c i o n d e l r a t o n e n e l t i t u l o
Caption : = Format ( ' M o u s e i n x = % d , y = % d l , EX, Y]) ;
if ssShift in Shift then
// m r c a p u n t o s e n a m a r i l l o
Canvas.Pixels [ X I Y] : = clYellow;
end ;
mediante 10s eventos y operaciones de raton de bajo nivel. En la VCL, 10s formularios no pueden originar operaciones de arrastre, por lo que en este caso tendremos que usar una tecnica de bajo nivel. El objetivo de este ejemplo consiste en
dibujar un rectangulo desde la posicion original de la operacion de arrastre a la
final, aportando a 10s usuarios indicaciones visuales sobre la operacion que estan
realizando. La idea que se encuentra tras la operacion de arrastre es sencilla. El
programa recibe una secuencia de mensajes sobre la pulsacion del boton, su movimiento y el soltado del boton. Cuando se pulsa el boton, comienza el arrastre,
aunque las acciones ocurren en realidad solo cuando el usuario mueve el raton
(sin soltar el boton del raton) y cuando termina el arrastre (cuando llega el mensaje de soltado del boton). El problema de esta tecnica basica es que no es fiable.
Una ventana normalmente recibe eventos solo cuando el raton esta sobre la zona
de cliente, por lo que si el usuario pulsa el boton del raton, mueve el raton sobre
otra ventana y, a continuacion, suelta el boton, la segunda ventana recibira el
mensaje de soltado del boton.
Existen dos soluciones a este problema. Una (poco usada) es el recorte del
raton. Utilizando una funcion de la API de Windows (Clipcursor), se puede
obligar a que el raton no abandone una cierta zona de la pantalla. Cuando intentamos moverlo fuera de dicha zona, choca contra una barrera invisible. La segunda
solution, mas comun, consiste en capturar el raton. Cuando una ventana captura
el raton, todas las entradas de raton subsiguientes se envian a dicha ventana. Esta
es la tecnica que utilizaremos en el ejemplo MouseOne/QMouseOne.
El codigo del ejemplo se organiza en torno a tres metodos: FormMouseDown,
FormMouseMove y FormMouseUp. A1 pulsar con el boton izquierdo del raton sobre el formulario comienza el proceso, activando el campo booleano
fDrag g ing del formulario (que indica que el arrastre esta activo dentro de 10s
otros dos metodos). El metodo usa una variable TRect para realizar un seguimiento de la posicion inicial y la actual de arrastre. Veamos el codigo:
procedure TMouseForm.FormMouseDown(Sender:
TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
i f Button = mbLeft then
begin
Dragging : = True;
Mouse.Capture : = Handle;
fRect.Left : = X;
fRect.Top : = Y;
fRect-BottomRight : = fRect.TopLeft;
dragstart : = fRect.TopLeft;
Canvas .DrawFocusRect (fRect);
end ;
end ;
TObject; Button:
TRUCO: El objeto global Mouse permite obtener infonnaci6n global sobre el rat6n, como su presencia, tipo y posicibn actudes, ademh & definir
algunas de sus caracteristicasglobales. Este objeto global oculta unas cuantas funciones de la API, que simplifican el c6digo y lo hacen de miis fhcil
adaptation. En la VCL, la propiedad Capture es de tipo Handle, mientras que en Iet'CLX es de tipo TControl (el objeto del componente que
captura d rat6n). Por eso, el c6digo de esta secci6n se convertirh en
Mouse.Capture := self, como se puede cornprobar en el ejemplo
QMoweOnc.
Cuando el arrastre esta activo y el usuario mueve el raton, el programa dibuja
un rectangulo con una linea de puntos que se corresponde a la posicion del raton.
En realidad, el programa llama a1 metodo D r a w F o c u s R e c t dos veces. La primera vez que se llama a este mktodo, borra la imagen actual, gracias a que dos
Ilamadas consecutivas a D r a w F o c u s Rec t recuperan la situacion original. Desputs de actualizar la posicion del rectangulo, el programa llama a1 metodo por
segunda vez:
procedure TMouseForm.FormMouseMove(Sender: TObject; Shift:
TShiftState;
X, Y: Integer) ;
begin
// rnuestra l a p o s i c i o n d e l r a t o n e n e l t i t u l o
Caption : = Format ( ' M o u s e i n x = % d , y = % d ' , [X, Y]) ;
i f fDragging then
begin
// elirnina y d i b u j a d e n u e v o e l r e c t i n g u l o d e a r r a s t r e
Canvas.DrawFocusRect (fRect);
i f X > dragStart.X then
fRect.Right := X;
else
fRect.Left : = X;
i f Y > dragStart.Y then
fRect.Bottom : = Y;
else
fRect.Top : = Y;
Canvas.DrawFocusRect ( f R e c t ) ;
end
else
i f ssShift i n Shift then
// marca 1 0 s p u n t o s e n a m a r i l l o
Canvas .Pixels [ X , Y] : = clYellow;
end;
Figura 7.6. El ejernplo MouseOne usa una linea de puntos para dibujar, durante la
operacion de arrastre, un rectangulo.
m a part"eTE'~iniimil,
jintara de nuevo dicha z o h . Para ello, se
pueden usar las funciones Inva:
En realidad, esta funci6n es un a m ae oome n ~ o a. s una recnlca muy
potente, que puede mejorar la velocidad y reducir el Imrpadeo causado por
operaciones de pintado frecuentes. Por otra parte, tam b i h puede originar
..
,
una salida incorrecta. Un problema bastante comun consiste en que solo se
modifican en realidad algunas de las zonas afectadas pcIr las operaciones de
usuario, mientras que otras siguen como estaban aunql~eel sistema ejecute
.,
c: .
,
UIU vywawvu
el codino fuente aue su~uestamentedeberia actualizarl~.51
de pintho se da &era de la regi6n actualizada, el r:istermla ignora, como si
estuviera fuera de la zona visible de la ventana.
!.A
~-
,
,
.
,
,
,
a
,
,
Fijese en que hay que llamar a1 metodo show a1 final para que el comportamiento del formulario sea el adecuado. Tambien se puede conseguir un efecto de
animation similar al modificar la propiedad AlphaBlendValue en un bucle.
La API AnimateWindow se puede usar tambien para controlar el mod0 en que
se presenta el formulario: empezando desde el centro (con el indicador A w C E N T E R ) o desde uno de sus lados (AW H O R P O S I T I V E , AW -H O R NEGATIVE, AW V E R POSITIV, o AW
Esta misma fuhci6nie puede aplicar tambitin-a 10s controles de ventana para
darles un aspect0 transparente en lugar de su habitual apariencia directa. No
queda muy claro el gasto de ciclos de CPU que causan estas animaciones, pero si
se aplican correctamente y en el programa apropiado, pueden mejorar la interfaz
de usuario.
VER NEGATIVE).
Conviene tener en cuenta que el efecto de la propiedad constraints,despues de haberla definido, es inmediato incluso en tiempo de diseiio, cambiando el
tamaiio del formulario si esta fuera de la zona permitida.
Delphi utiliza tambien las restricciones maximas para las ventanas maximizadas,
produciendo un efecto algo extraiio. Por este motivo, generalmente deberia
inhabilitarse el boton de maximizar de una ventana que tenga un tamaiio maximo.
En algunos casos las ventanas maximizadas con un tamaiio limite pueden tener
sentido (este es el comportamiento de la ventana principal de Delphi). Si se necesita modificar las restricciones en tiempo de ejecucion, tambien se puede considerar usar dos eventos especificos, OnCanRes ize y OnConstrainedRes ize.
El primer0 de 10s dos tambien puede usarse para inhabilitar el ajuste de tamaiio de
un formulario o control bajo determinadas circunstancias.
Desplazar un formulario
Cuando se crea una aplicacion simple, un solo formulario podria albergar
todos 10s componentes necesarios. Sin embargo, a medida que crece la aplicacion,
tal vez haya que reducir el espacio para 10s componentes y juntarlos mas, aumentar el tamaiio del formulario o aiiadir formularios nuevos. Si se juntan mas 10s
componentes, se podria aiiadir la capacidad de modificar su tamaiio en tiempo de
ejecucion, posiblemente dividiendo el formulario en dos zonas diferentes. Si decidimos aumentar el tamaiio del formulario, podriamos usar las barras de desplaza-
miento para permitir que el usuario se mueva por un formulario que sera mas
grande que la pantalla (o a1 menos mas grande que su zona visible en la pantalla).
Aiiadir una barra de desplazamiento a un formulario es sencillo. De hecho, no
hay que hacer nada. Si se colocan varios componentes dentro de un gran formulario y se reduce su tamaiio, automaticamente se aiiadira una barra de desplazamiento a1 formulario, siempre que no se haya cambiado el valor de la propiedad
AutoScroll predefinida como True.
Junto con A u t o s c r o 1 1 , 10s formularios tienen dos propiedades,
HorzScrollBar y VertScrollBar, que se pueden usar para definir diversas propiedades de 10s dos objetos T FormScro 11Bar asociados con el formulario.
La propiedad Visible indica si esta presente la barra de desplazamiento, la
propiedad Posit ion determina el estado inicial del control de desplazamiento y
la propiedad Increment determina el efecto que se obtiene a1 hacer clic sobre
una de las flechas situadas en 10s extremos de la barra de desplazamiento. Sin
embargo, la propiedad mas importante es Range.
La propiedad Range de una barra de desplazamiento establece el tamaiio
virtual del formulario, no el rango real de valores de la barra de desplazamiento.
Supongamos que se necesita un formulario que aloje diversos componentes y que
por tanto necesite ser de 1000 pixeles de ancho. Podemos usar este valor para
definir el "rango virtual" del formulario, cambiando el Range de la barra de
desplazamiento horizontal.
La propiedad Position de la barra de desplazamiento variara entre 0 y
1000 menos el tamaiio actual de la zona de cliente. Por ejemplo, si la zona de
cliente de un formulario tiene 300 pixeles de ancho, podemos desplazarnos 700
pixeles para ver el extremo mas alejado del formulario (el pixel milesimo).
En el formulario del ejemplo se han colocado dos cuadros de lista sin ninguna
funcion y se podria haber obtenido el mismo rango de barra de desplazamiento
colocando el cuadro de lista fijo a la derecha de tal manera para que su posicion
(Left) mas su tamaiio (Width) fuese igual a 1000.
La parte interesante del ejemplo es la presencia de una ventana de cuadro de
herramientas que muestra el estado del formulario y de su barra de desplazamien-
to horizontal. Este segundo formulario tiene cuatro etiquetas, dos con texto fijo y
dos con la salida. Ademas de eso, el formulario secundario (Ilamado st at us)
tiene un estilo de borde bsToolWindow y es una ventana que siempre estara
por encima. Tambien deberiamos definir su propiedad V i s ible como True,
para que su ventana se muestre automaticamente a1 arrancar:
object Status: TStatus
BorderIcons = [biSystemMenu]
Borderstyle = bsToolWindow
Formstyle = fsStayOnTop
Visible = True
object Labell: T L a b e l ...
...
r1
Si queremos cambiar el resultado cada vez que el usuario desplace el contenido del formulario, no podemos usar un controlador de eventos de Delphi, porque
Es importante aiiadir la llamada a i n h e r i t e d , que activa el metodo relacionado con el mismo mensaje en el formulario de clase basica. La palabra clave
i n h e r i t e d en 10s controladores de mensajes de Windows llama a1 metodo de la
clase basica que estamos sobrescribiendo, que se encuentra asociado con el correspondiente mensaje de Windows (incluso aunque el nombre del procedimiento
sea distinto). Sin esta llamada, el formulario no se desplazara en absoluto.
NOTA:Debido a que en CLX no podems mtrolar 10s rnensajes de desp l a z ~ e n t oa bajo nivel, no puece que haya un modo fslcil de crear un
program similar a Scroll 1. En las aplimiones del mundo real, esto no
resulta extremadamente importante, puem que el siseema de desplazamimta
es a u t d t i c o y probablernente se pwde realizar conectando la biblioteca
CLX a un nivel inferior.
Desplazamiento automatic0
La propiedad R a n g e de la barra de desplazamiento puede parecer extraiia
hasta que se comienza a usar continuamente. Entonces, se empieza a pensar en las
ventajas de la tecnica del "rango virtual". En primer lugar, la barra de desplazamiento se elimina automaticamente del formulario cuando la zona de cliente del
formulario es lo suficientemente amplia como para acomodar el tamaiio virtual y
Por lo tanto, el tamaiio virtual horizontal del formulario seria de 9 7 7 (la suma
de 10s dos valores anteriores). Este numero se copia automaticamente en el campo
Range de la propiedad Horz S c r o l l B a r del formulario, a menos que se cambie manualmente para conseguir un formulario mas grande (corno en el ejemplo
Scroll 1, en el que se usaban un valor de 1 0 0 0 para que hubiera algo de espacio
entre el ultimo cuadro de lista y el borde del formulario). Podemos ver dicho valor
en el Object Inspector o realizar la siguiente prueba: ejecutar el programa,
establecer el tamaiio deseado para el formulario y mover el control de desplazamiento hasta el extremo derecho. Cuando aiiadimos el tamaiio del formulario y la
posicion del control, siempre obtendremos 1 0 0 0, la coordenada virtual del pixel
situado mas a la derecha, sea cual sea su tamaiio.
TObject);
// d i b u j a una l i n e d a m a r i l l a
Canvas.Pen.Width : = 30;
Canvas.Pen.Color : = clYellow;
Canvas .MoveTo (30-XI, 30-Y1) ;
Canvas .LineTo (1970-XI, 1970-Y1) ;
// y a s i s u c e s i v a m e n t e
...
2000 Pixeles
2000
'ixeles
500 Pixeles
Figura 7.9. Las lineas a dibujar sobre la superficie virtual del formulario
// d i b u j a una l i n e a a m a r i l l a
Canvas.Pen.Width : = 30;
Canvas.Pen.Color
: = clYellow;
Canvas .MoveTo (30, 3 0 ) ;
C a n v a s .LineTo ( 1 9 7 0 ,
1970) ;
// y a s i s u c e s i v a m e n t e
...
Esta es la version del programa que encontrara en el codigo fuente del libro. Se
puede probar el programa y comentar la llamada a SetWindowOrgEx para ver
lo que sucede si no se usan las coordenadas virtuales. Se puede ver que el resultado del programa no es correct0 (no se desplazara, y siempre permanecera la
misma imagen en la misma posicion, sin importar las operaciones de desplazamiento). Observe tambien que en la version QtICLX del programa, denominada
QScroll2, no se usan las coordenadas virtuales sino que simplemente se restan las
posiciones de desplazamiento a cada coordenada codificada manualmente.
Escalado de formularios
Cuando sc crea un formulario con multiples componentes, se puede escoger un
borde de tamaiio fijo o permitir que el usuario ajuste el tamaiio del formulario y se
aiiadan automaticamente barras de desplazamiento para poder acceder a 10s componcntes que se encuentren fuera de la parte visible del formulario, como ya se ha
visto. Tambien podria suceder esto porque un usuario de la aplicacion utilizara
un controlador de pantalla con un numero de pixeles mucho menor que en el
desarrollo. En lugar de reducir el tamaiio del formulario y desplazar el contenido,
podria quercrse reducir el tamaiio de cada uno de 10s componentes al unisono.
Esto ocurre automaticamente si el usuario tiene una fuente de sistema con una
tasa dc piselcs por pulgada distinta que la usada para el desarrollo. Para enfrentarse a estos problemas, Delphi disponc de unas apreciables caracteristicas de
escalado, per0 no son completamente intuitivas.
El metodo S c a l e B y de formulario permite ajustar la cscala del formulario y
de cada uno dc sus componentes. Las propiedadcs P i x e l s P e r I n c h y S c a l e d
permiten que Delphi modifique el tamaiio de una aplicacion de forma automatica,
cuando esta se ejecuta con un tamaiio de fuente de sistema diferente, normalmente
debido a una resolution de pantalla distinta. En ambos casos, para que el formulario establezca la escala de su ventana, hay que asegurarse de definir tambien la
propiedad A u t o s c r o l l como F a l s e . De otro modo, el contenido del formulario se ajustara a la escala, pero el borde de dicho formulario no.
->--A-
---A-
(3, 4);
Normalmente, resulta mas sencillo usar porcentajes. Se obtiene el mismo efecto usando:
ScaleBy
(75, 1 0 0 ) ;
Cuando se ajusta la escala de un formulario, se mantienen todas las proporciones, per0 si se superan ciertos limites minimos o maximos, las cadenas de texto
pueden modificar ligeramente todas sus proporciones. El problema es que en
Windows, 10s componentes se pueden colocar y se puede ajustar su tamafio solo
en pixeles enteros, mientras que el ajuste de la escala casi siempre implica una
multiplication por numeros fraccionarios. Por lo tanto, cualquier parte fraccionaria
del origen o tamaiio de componente se vera truncada.
Hemos creado un sencillo ejemplo, Scale (o QScale), para mostrar como se
puede ajustar manualmente la escala de un formulario, respondiendo a una solicitud realizada por el usuario. El formulario de esta aplicacion tiene dos botones,
una etiqueta, un cuadro de edicion y un control UpDown conectados a el (mediante la propiedad A s s o c i a t e ) . Con esta configuration, un usuario puede escribir
numeros en el cuadro de edicion o pinchar sobre las dos pequeiias flechas para
aumentar o disminuir el valor (en la cantidad indicada por la propiedad
I n c r e m e n t ) . Para extraer el valor de entrada, se puede usar la propiedad T e x t
del cuadro de edicion o la propiedad P o s i t i o n del control UpDown. Cuando se
hace clic sobre el boton Do Scale, el valor de la entrada actual se utiliza para
determinar el porcentaje de escalado del formulario:
procedure TForml.ScaleButtonC1ick(Sender:
begin
AmountScaled : = UpDownl.Position;
ScaleBy (AmountScaled, 100) ;
UpDownl.Height : = Editl.Height;
ScaleButton. Enabled : = False;
RestoreButton.Enab1ed : = True;
end;
TObject);
- --
NOTA: Si queremos ajustar la escala del texto del formulario correctamente, y tambien de 10s titulos de 10s componentes, 10s elementos de 10s
cuadros de lista, etc.. ., deberiamos utilizar exclusivamente fhentes TrueType.
La fuente de sistema (MS Sans Serif) no se ajusta a la escala correctamente. El problema de la fhente resulta importante porque el tamaiio de muchos
componentes depende de la altura del texto de sus titulos y, si el titulo no se
ajusta bien a la escala, el componente podria no fun~ionar~correctamente.
Por esa razon, en el ejemplo Scale hemos usado una fuente Arial.
Esta misma tecnica de ajuste de escala funciona tambien en CLX, como se
pucde ver al ejecutar cl ejemplo QScale. La imica diferencia real es que hemos
sustituido el componente UpDown (y cl cuadro de edicion relacionado) por un
control SpinEdit, puesto quc el primer0 no cxiste en Qt.
permiten incluso que el usuario defina el tamaiio de la fuente de sistema segun una
escala arbitraria. En tiempo de diseiio, el valor P i x e l s P e r I n c h de la pantalla,
que es una propiedad so10 de lectura, se copia en cada formulario de la aplicacion.
Delphi usa entonces el valor de P i x e l s P e r I n c h , si la propiedad S c a l e d esta
definida como T r u e , para ajustar el tamaiio del formulario cuando se inicia la
aplicacion .
Como hemos dicho, tanto el ajuste automatico de la escala como el realizado
por el metodo S c a l e B y modifican el tamaiio de la fuente de 10s componentes. El
tamaiio de cada control, en realidad, depende de la fuente que se use. Con el ajuste
de escala automatico, el valor de la propiedad P i x e l s P e r I n c h del formulario
(el valor en tiempo de diseiio) se compara con el valor del sistema en ese momento
(indicado por la propiedad correspondiente del objeto S c r e e n ) y se usa ese
resultado para modificar la fuente de 10s componentes del formulario. Para mejorar la precision de este codigo, la altura final del texto se compara con la altura
del texto en tiempo de diseiio y se ajusta su tamaiio si ambas alturas no se corresponden.
Gracias a1 soporte automatico de Delphi, una misma aplicacion ejecutada en
un sistema con un tamaiio de fuente de sistema distinto ajustara su escala de
forma automatica, sin ningun codigo especifico. Los controles de edicion de la
aplicacion tendran el tamaiio adecuado para mostrar su texto en el tamafio de
fuente preferido por el usuario y el formulario tendra el tamaiio adecuado para
alojar dichos controles. Aunque el ajuste automatico de escala tiene problemas en
algunos casos especiales, si se respetan las siguientes normas, 10s resultados deberian ser 10s correctos:
Definir la propiedad S c a l e d de 10s formularios como T r u e . (Es el valor
predefinido.)
Usar solo fuentes TrueType
Usar fuentes pequeiias de Windows (96 dpi) en el ordenador que se use
para desarrollar 10s formularios.
Definir la propiedad A u t o s c r o l l como F a l s e si se quiere ajustar a
escala del formulario y no solo sus controles. ( A u t o S c r o l l esta
predefinida como T r u e , por lo que conviene recordar este paso.)
Definir la posicion del formulario o bien proxima a la esquina superior
izquierda o en el centro de la pantalla (con el valor p o s c r e e n c e n t e ~ )
para evitar tener un formulario fuera de la pantalla.
r --Man
fam
l~olrn~
Figura 7.10. La ficha Forms del cuadro de dialogo Project Options de Delphi.
Ahora, si ejecutamos este programa, no pasara nada. Finaliza de forma inmediata porque no se crea ninguna ventana principal. El efecto de la llamada a1
metodo C r e a t e F o r m de la aplicacion crea una nueva instancia de la clase de
formulario que se pasa como primer parametro y la asigna a la variable pasada
como segundo parametro.
En el ambito interno sucede algo mas. Cuando se llama a C r e a t e F o r m , si en
ese momento no hay un formulario principal, se asigna el formulario actual a la
propiedad M a i n F o r m de la aplicacion. Por esa razon, el formulario indicado
como Main Form en el cuadro de dialog0 que aparece en la figura 7.10 se corresponde con la primera llamada a1 metodo CreateForm de la aplicacion (es decir,
cuando se crean diversos formularios a1 arrancar).
A1 cerrar la aplicacion, ocurre lo mismo. Si se cierra el formulario principal,
finaliza la aplicacion, sin tener en cuenta 10s otros formularios. Para realizar esta
operacion desde el codigo del programa, sencillamente hay que llamar a1 metodo
close del formulario principal, como hemos hecho en diversas ocasiones en 10s
ejemplos anteriores.
2. OnShow indica que se esta mostrando el formulario. Ademas de 10s formularios principales, este evento tiene lugar despues de que se define como
True la propiedad Visible del formulario o se llama a 10s metodos
Show o ShowModal. Este evento ocurre de nuevo si el formulario se
oculta y aparece de nuevo.
3. OnActivate indica que el formulario se transforma en el formulario
activo de la aplicacion. Este evento ticne lugar cada vez quc nos movemos
desde otro formulario de la aplicacion a1 actual.
4. Otros eventos, como OnRes i ze y On Paint,indican operaciones realizadas siempre a1 arrancar pero repetidas, a continuacion, varias veces.
.
NOTA: En Qt, el evento OnRes i ze no se lanza como en Windows cuando se crea el formulario. Para que el &go resulte rnh adaptable de Delphi
a Kylix, CLX simula este evento, aunque tendria mas sentido retocar la
VLC para evitar que se diera este e x t r s o comportamiento (un comentario
en el c ~ d i g ofuente de CLX comenta esta situacion).
,
- -
Como se puede ver, cada evento tiene una funcion especifica ademas de la
inicializacion del formulario, a escepcion del evento Oncreate,a1 que se llama
solo una vez de manera garantizada cuando se crea el formulario.
Sin embargo, existe un enfoque alternativo para aiiadir codigo de inicializacion
a un formulario: sobrescribir el constructor. Esto se hacc del siguiente modo:
constructor TForml.Create(A0wner:
begin
inherited Create (AOwner);
TComponent);
// c o d i g o d e i n i c i a c i o n a d i c i o n a l
end;
--
- -
.-
..
. I
Cerrar un formulario
Cuando cerramos un formulario utilizando el metodo c l o s e o mediante el
tipico mdtodo (Ah-F4, el mcnu de sistcma o el boton Close), llamamos a1 evento
O n C l o s e Q u e r y . En este caso, se puede pedir a1 usuario que confirme la accion, sobrc todo si hay datos sin guardar en el formulario. Veamos la sencilla
cstructura dcl codigo que podemos escribir:
procedure TForml.FormCloseQuery(Sender: TObject; var CanClose:
Boolean) ;
begin
if MessageDlg ( ' A r e y o u s u r e y o u w a n t t o e x i t ? ' ,
mtconfirmation, [&Yes,
&No] , 0 ) = idNo then
CanClose : = False;
end:
-Ai--
A.
-.-_.-
_L--.._.___
-_-a-
El uso dcl evento intermedio onclose esta en que nos ofrece otra oportunidad para no cerrar la aplicacion o podemos especificar "acciones de cierrc" altcrnativas. De hecho, el metodo ticnc un parametro Action que se pasa mcdiante
rcferencia. Podemos asignar 10s siguientcs valorcs a dicho parametro:
caNone: No sc pcrmitc a1 formulario que se cicrrc. Corrcsponde a la configuration del paramctro Canclose del metodo OnCloseQuery como
False.
caHide: No se cierra cl formulario, solo se oculta. Esto solo sc dcbe haccr
si hay otros formularios cn la aplicacion, si no, el programa finaliza. En cl
caso de 10s formularios secundarios es el comportamiento predefinido y
csa es la razon de quc hapamos tenido que controlar el evento onclose
en el ejemplo antcrior para poder cerrar 10s formularios secundarios.
caFree: Se cierra el forn~ulario~
sc libcra su memoria y por ultimo finaliza
la aplicacion si ese era el formulario principal. Esta es la accion predefinida
cn el caso del formulario principal y la accion que deberiamos usar cuando
sc crean varios formularios dc forma dinamica (si se desea eliminar las
ventanas y destruir cl corrcspondiente objeto Delphi cuando sc cicrrc cl
formulario).
cahlinimize: No se cierra el formulario, solo se minimiza. Esta es la accion predefinida en el caso de formularios hijo MDI.
-
- -.
3."
,
. . canclose
uncloseyuery aerina
el pararnerro
como 'Lrue.
A
- 3
TRUCO: Los formularios secundarios se crean automaticamente en el archive de codigo fuente del proyecto dependiendo del estado del cuadro de
comprobacidn Auto Create Forms de la phgina Designer del cuadro de
dialogo Environment Options. Aunque la creation automatica es la tecnica mas sencilla y mas fiable para 10s desarrolladores noveles y proyectos
sin perfeccionar, conviene desactivar esta casilla de verification en todos
aquellos proyectos de desarrolIo importantes. Si la aplicaci6n contiene cientos
de formularios, no se deberian crear todos a1 iniciar la aplicacion. Lo mejor
es crear las instancias de fonnularios secundarios cuando y donde Sean
necesarios y liberarlos cuando no se necesiten.
Cuando hayamos preparado el formulario secundario, simplemente podemos
definir su propiedad V i s i b l e como T r u e y ambos formularios apareceran a1
arrancar el programa. En general, 10s formularios secundarios de una aplicacion
se dejan "invisibles" y, mas tarde, se muestran llamando a1 metodo Show (o
definiendo la propicdad V i s i b l e en tiempo de ejecucion). Si utilizamos la funcion Show, el segundo formulario aparecera como no modal, de mod0 que podemos movernos de nuevo a1 primer0 mientras el segundo esta visible. Para cerrar el
segundo formulario, podriamos usar su menu de sistema o pulsar el boton o elemento del menu que llama a1 metodo C l o s e . Tal y como acabamos de ver, la
accion de cierre predefinida (vease el evento O n c l o s e ) en el caso de un formu-
( A p p l i c a t i o n ) do
Cada vez que hacemos clic sobre el boton, se crea una nueva copia del formulario. Hay que darse cuenta de que no utilizamos la variable global F o r m 3 porque no tiene mucho sentido asignar a esta variable un nuevo valor cada vez que se
crea un nuevo objeto de formulario. Sin embargo, lo importante es no referirse a1
objeto global F o r m 3 en el codigo del formulario o en otras partes de la aplicacion. La variable F o r m 3 sera invariablemente un punter0 a n i l . Lo mas recomendable es que en un caso como este se elimine de la unidad para evitar cualquier
confusion posible.
--
-..
-- - .
TRUCO:En el c M g o de un formulario que puede tener mmiltiples instancias, nunca se deben'a hacer referencia explicita al formulario utihzando la
variable global que Delphi define para 61. Por ejemplo, supongarnos que en
el codigo de T Form3 hacemos referencia a Form3 C a p t i o n . Si creamos un segundo objeto del mismo tipo (la clase TForm3), la expresion
Form3 C a p t i o n se referira siempre a1 titulo del objeto de formulario al
que hace referencia la variable Form3, que podria no ser el objeto actual
que ejecuta el codigo. Para evitar dicho problerna, hay que referirse a la
propiedad C a p t i o n en el m6todo del formulario para indicar el titulo del
objeto de formulario actual y usar la palabra clave self cuando se necesi.- -. - - - .
te hacer reterencla especifica a1 0bjet0 del formulario en uso. Para evltar
cualquier problema al crear diversas copias de un formulario, se puede
elirninar el objeto global formulario de la parte de interfaz de la unidad que
declara el formulario.
No hacer esto conllevara un gran consumo de memoria, ya que todos 10s formularios que se creen (tanto las ventanas como 10s objetos Delphi) se mantendran
en memoria y se ocultaran.
Con este codigo, se crea el formulario la primera vez que se necesita y despues
se guarda en memoria, visible en pantalla u oculto. Para evitar consumir memoria
y recursos del sistema de forma innecesaria, debemos destruir el formulario secundario cuando se cierre. Para ello podemos escribir un controlador del evento
OnClose:
procedure TForm2.FormClose(Sender:
TCloseAction) ;
begin
Action : = caFree;
/ / i m p o r t a n t e : d e f i n i r p u n t e r 0 como n i l
Form2 : = n i l ;
end ;
1,
A
,,
~a
a
,
,
.
a
I ~ L U I GJ
I
~
"
,
A
*,",,
A,"+,.:,
,I C,-..l..,,
uu
G ~ J U G I I I V J U G J C I U I ~GI 1 ~ 1 1
,
,
+
a
,
,
,
,
A,
:
,
a
+
,
.
.
,
1 1 ~ 1 4 CULCGJ
1 1 ~
UG ~
A,
U CGIIIIIUGII
G
UG
dales son mas habituales que 10s no modales. Es justo a1 contrario que con 10s
formularios: generalmente deberian evitarse 10s formularios modales, porque no
es lo que espera un usuario.
J _
$1-
-..-__
a la lista y define todos sus valores. Para vaciar 10s cuadros de edicion del dialogo, el programa llama a1 metodo personalizado c l e a r , que reinicia el texto de
cada cuadro de edicion:
procedure TFormItem.Clear;
var
I: Integer;
begin
// b o r r a c a d a c u a d r o d e e d i c i o n
for I : = 0 to Controlcount - 1 do
if Controls [I] is TEdit then
TEdit (Controls[I]) .Text : =
end;
'';
[O];
[I];
// l o r n u e s t r a
if FormItem.ShowModa1 = mrOK then
begin
// l e e 1 0 s v a l o r e s n u e v o s
ListViewl.Se1ected.Caption : =
FormItem.EditReference.Text;
ListViewl.Selected.ImageIndex : =
FormItem.ComboType.ItemIndex;
ListViewl.Selected.Sub1tems [O] : =
Form1tem.EditAuthor.Text;
ListViewl.Selected.SubItems [I] : =
Form1tem.EditCountry.Text;
end ;
end ;
end;
Se puede ver el efecto de este codigo en la figura 7.11. Observe que el codigo
utilizado para leer el valor de un elemento nuevo o modificado es similar. En
general, hay que evitar este tip0 de codigo duplicado y colocar las sentencias de
codigo compartidas en un metodo aiiadido a1 cuadro de dialogo. En este caso; el
metodo podria recibir como parametro un objeto TList I t e m y copiar en el 10s
valores adecuados.
Marca Canlu
Eapaiia
Figura 7.11. El cuadro de dialogo del ejernplo RefList2 utilizado en modo edicion.
-
. .- - - ...
..
.Bob
C njy
Name
Jane
Jelf
John
Name
J iha
Ma~k
Martha
Name
Mn,,
Name
Name
Sample label
Figura 7.12. Los tres forrnularios del ejernplo DlgApply en tiempo de ejecucion (un
formulario principal y dos cuadros de dialogo).
Cuando el usuario hace clic sobre el boton Apply, el programa copia el estilo
de la etiqueta de muestra en cada una de las etiquetas del formulario, en lugar de
tener en cuenta 10s valores de las casillas de verificacion:
procedure TStyleDial.ApplyBitBtnClick(Sender: TObject);
begin
Forml.Labell.Font.Style : = LabelSarnple.Font.Sty1e;
Forrnl.Label2.Font.Style : = LabelSample.Font.Style;
La ventaja de este enfoque es que se pueden crear 10s nombres de las diversas
etiquetas dentro de un bucle f o r :
p r o c e d u r e TStyleDial.ApplyBitBtnClick(Sender:
var
I: Integer;
TObject),
begin
for I : = 1 to 5 d o
(Forml.Findcomponent ( 'Label' + IntToStr
TLabel) .Font.Style : = LabelSample.Font.Style;
end;
(I)) as
TRUCOi El metmdb.Appf.yBit~tn~lick
tambih jodria haberse escrito para.que.aa&ase .fa matriz Controls en un b :le, como en otros
Esta segunda version del codigo es realmente mas lenta, porque tiene que
realizar mas operaciones, per0 la diferencia no se podra percibir porque sigue
siendo muy rapido.
Por supuesto, este enfoque tambien es mas flesible: si se aiiade una nueva
etiqueta, solo se necesita cambiar el limite superior para el bucle f o r , siempre
que todas las etiquetas tengan numeros consecutivos.
Hay que darse cuenta de que cuando el usuario hace clic sobre el boton Apply,
no se cierra el cuadro de dialogo (solo el boton Close tiene este efecto). Hay que
considerar tambih que este cuadro de dialogo no necesita codigo de inicializacion
porque el formulario no se destruye, y sus componentes mantienen su estado cada
vez que se muestra el cuadro de dialogo. Sin embargo, en la version CLX del
programa, QDlgApply, el dialogo es modal, incluso aunque se llame con el metodo Show.
0 Tfehuchet US
0 Tunga
IE
16
0 Verdana
Elector - -
r Eismph -
Los cuadros d e dialogo Find y Replace: Son verdaderos cuadros de dialogo no modales, per0 tenemos que implementar la funcionalidad de busqueda y reemplazo nosotros mismos, como se ha hecho en parte en el
ejemplo CommDlgTest. El codigo personalizado esta conectado a 10s botones de 10s dos cuadros de dialogo proporcionando 10s eventos O n F i n d y
---
--
----
~- -~
El procedimiento ShowMessage: Muestra un cuadro de mensaje mas sencillo, con el nombre de la aplicacion como titulo y solo un boton OK. El
procedimiento ShowMessagePos hace lo mismo, per0 tambien se puede
indicar la posicion del cuadro de mensaje. El procedimiento ShowMessage Fmt es una variation de ShowMes sage, que tiene 10s mismos
parametros que la funcion Format.Corresponde a una llamada a Format
dentro de una llamada a ShowMessage.
El mCtodo MessageBox del objeto Application: Permite especificar el
mensaje y el titulo. Tambien ofrece varios botones y funciones. Esto es un
encapsulado direct0 y sencillo de la funcion de la API de Windows
MessageBox,que pasa como parametro de ventana principal el controlador del objeto Appl i cat ion.Este se necesita para que el cuadro de
mensaje se comporte como una ventana modal.
La funci6n InputBox: Pide a1 usuario que escriba una cadena. Tenemos
que proporcionar un titulo, una consulta y una cadena predeterminada. La
funcion InputQuery tambien pide al usuario que escriba una cadena.
La unica diferencia entre ambas funciones estriba en su sintaxis. La funcion InputQuery tiene un valor de retorno booleano que indica si el
usuario ha hecho clic sobre OK o sobre Cancel.
Para mostrar algunos de 10s cuadros de mensaje disponibles en Delphi, hemos
escrito otro programa de muestra, con un enfoque similar a1 anterior ejemplo
CommDlgTest.
En el ejemplo MBParade, existe una gran cantidad de opciones (botones de
radio, casillas de verificacion, cuadros de edicion y controles de edicion e incremento) para definir antes de hacer clic sobre uno de 10s botones que muestra un
cuadro de mensaje. El ejemplo QMbParade solo carece del boton de ayuda, que
no esta disponible en 10s cuadros de mensaje de CLX.
Figura 7.14. El formulario principal del ejemplo Splash, con la pantalla inicial (se
trata de la version Splash2).
Existen tres versiones de este programa (ademas de las tres versiones correspondientes para CLX). A1 ejecutar SplashO, el problema es que para la operacion
inicial, que se realiza en el metodo FormCreate, se emplea mucho tiempo.
Cuando arrancamos el programa, tarda varios segundos en mostrar el formulario
principal. Si el ordenador es muy rapido o muy lento, podemos cambiar el limite
superior del bucle for del metodo FormCreate para que sea mas rapido o mas
lento. Este programa tiene un cuadro de dialogo sencillo con un componente de
imagen, un titulo y un boton de mapa de bits, todo colocado en un panel que ocupa
toda la superficie del cuadro "Acerca de". Este formulario aparece cuando seleccionamos la opcion del menu Help>About. Pero lo que queremos en realidad es
mostrar el cuadro "Acerca den mientras arranca el programa. Podemos ver este
efecto a1 ejecutar Splashl y Splash2, que muestran una pantalla inicial mediante
dos tecnicas distintas.
En primer lugar, hemos afiadido un metodo a la clase TAboutBox. Este
metodo, llamado Make S p l a s h, cambia algunas propiedades del formulario para
que la pantalla inicial encaje como formulario de pantalla inicial. Basicamente
elimina el borde y el titulo, oculta el boton OK, hace que el borde del panel sea
grueso (para sustituir el borde del formulario) y, a continuacion, muestra el formulario y lo pinta inmediatamente:
procedure TAboutBox.MakeSplash;
begin
Borderstyle : = bsNone;
BitBtnl-Visible : = False;
Panell.BorderWidth : = 3;
Show;
Update;
end;
A este metodo se llama tras haber creado el formulario en el archivo de proyecto del ejemplo Splashl. Este codigo se ejecuta antes de crear 10s otros formularios (en este caso solo el formulario principal) y entonces se elimina la pantalla
inicial antes de ejecutar la aplicacion. Dichas operaciones suceden en un bloque
t r y/fi na 11y. Veamos el codigo del bloque principal del archivo de proyecto
del ejemplo Splash2:
var
SplashAbout: TAboutBox;
begin
Application.Initialize;
// c r e a y m u e s t r a e l f o r m u l a r i o i n i c i a l
SplashAbout := TAboutBox .Create (Application);
try
SplashAbout.MakeSp1ash;
// c o d i g o e s t d n d a r .
Application.CreateForm(TForml, Forml);
// e l i m i n a e l f o r m u l a r i o i n i c i a l
SplashAbout.Close;
finally
SplashAbout.Free;
end;
..
Application.Run;
end.
Esta tecnica solo tiene sentido solo si se tarda en crear el formulario principal
de la aplicacion, para ejecutar su codigo de arranque (como en este caso) o para
abrir tablas de bases de datos. Fijese en que la pantalla inicial es el primer formu-
lario que se crea, per0 como el programa no usa el metodo CreateForm del
objeto Application, este no se transforma en el formulario principal de la
aplicacion. En ese caso: cerrar la pantalla inicial finalizaria el programa.
Un enfoque alternativo es mantener el formulario inicial en pantalla algo mas
de tiempo y utilizar un temporizador para librarse de el. Esta tecnica se utiliza en
el ejemplo Splash2. Este ejemplo tambien utiliza un enfoque distinto para crear el
formulario inicial: en lugar de crear el formulario inicial en el codigo fuente del
proyecto, lo crea a1 comienzo del metodo FormCreate del formulario principal.
procedure TForml. FormCreate ( S e n d e r : T O b j e c t ) ;
var
I: Integer;
SplashAbout: TAboutBox;
begin
// c r e a y m u e s t r a e l f o r m u l a r i o i n i c i a l
SplashAbout : = TAboutBox.Create ( A p p l i c a t i o n ) ;
SplashAbout.MakeSp1ash;
// c o d i g o l e n t o ( o m i t i d o ) . . .
// e l i m i n a e l f o r m u l a r i o i n i c i a l , d e s p u e s d e u n t i e m p o
SplashAbout.Timerl.Enab1ed : = True;
end ;
Hay algo mas que solucionar. El formulario principal aparecera mas tarde y
delante del formulario inicial, a menos que lo convirtamos en un formulario fijo
por encima de la pantalla. Por esa razon, hemos aiiadido una linea a1 metodo
Makesplash del cuadro "Acerca de" en el ejemplo Splash2:
Parte II
Arquitecturas
orientadas
a objetos
en Delphi
La arauitectura de
las aplicaciones
Delphi
A pesar de que se han presentado ejemplos de programas Delphi desde el
principio de este libro, estos no han estado centrados en la estructura y arquitectura de las-aplicaciones desarrolladas con las bibliotecas de clases de Delphi. Por
ejemplo, no se ha entrado en profundidad a explicar el objeto global
A p p l i c a t i o n , las tecnicas para realizar el seguimiento del desarrollo de 10s
formularios creados, el flujo de mensajes del sistema, ni otros elementos afines.
En un capitulo anterior se ha descrito como crear aplicaciones con multiples
formularios y cuadros de dialogo, per0 no como se pueden relacionar esos formularios entre si, como se pueden compartir caracteristicas comunes entre formularios, o como trabajar con varios formularios similares de una manera consistente.
El objetivo del presente capitulo es explicar todos estos conceptos, cubriendo
tanto tecnicas basicas como avanzadas, incluyendo la herencia de formularios
visuales, el uso de marcos, y el desarrollo MDI, asi como el uso de interfaces para
construir jerarquias complejas de clases de formularios.
Este capitulo trata 10s siguientes temas:
LosobjetosglobalesApplicationy Screen.
El objeto Application
Se ha mencionado el objeto global Application en multiples ocasiones,
per0 dado que este capitulo se centra en la estructura de las aplicaciones Delphi,
pasemos a describir en detalle este objeto global y su clase correspondiente.
App 1icat ion es un objeto global de la clase TApp 1icat ion,definido en la
unidad Forms y creado en la unidad Controls. La clase TApplication es un
componente, per0 no se puede utilizar en tiempo de diseiio. Algunas de sus propiedades pueden definirse directamente en la ficha Application del cuadro de
dialog0 Project Options, otras deben asignarse en el codigo.
Para controlar estos eventos, en cambio, Delphi incluye un componente muy
comodo, App 1icat ionEvent s. Ademas de permitir asignar controladores en
tiempo de diseiio, la ventaja de este componente es que permite usar multiples
controladores. Si situamos una instancia del componente App 1icationEvents
en dos formularios diferentes, cada uno de ellos podra controlar el mismo evento
y se ejecutaran ambos controladores. En otras palabras, multiples componentes
App 1icat io nEve nt s pueden encadenar sus controladores.
Algunos eventos que afectan a toda la aplicacion, como OnAct ivat e,
OnDeactivate, OnMinimize y OnRestore, permiten realizar un seguimiento del estado de la misma. En el caso de otros eventos, 10s controles que 10s
reciben 10s reenvian a la aplicacion, como en el caso de OnActionExecute,
OnActionUpdate,OnHelp,OnHint,OnShortCut y OnShowHint.Por
ultimo, existe un controlador global de excepciones, 0nEx ception,el evento
O n ~ d l eutilizado para ejecutar procesos en segundo plano y el evento
OnMessage, que se activa siempre que se envia directamente un mensaje a
cualquier ventana o control de ventana de la aplicacion.
Aunque su clase hereda directamente de T C o m p o n e n t , el objeto
Application tiene asociada una ventana. La ventana de la aplicacion esta
oculta per0 aparece en la barra de tareas. Por este motivo Delphi llama Form1 a
la ventana y Pro j e ct 1 a1 icono correspondiente en la barra de tareas.
La ventana relacionada con el objeto Application ( la ventana de la aplicacion) sirve para mantener todas las ventanas de una aplicacion juntas. El hecho
de que todos 10s formularios de alto nivel de un programa tengan invisible esta
ventana propietaria resulta fundamental, por ejemplo, cuando se activa la aplica-
cion. De hecho. cuando las ventanas de un programa estan detras de las de otros
programas. a1 hacer clic sobre una ventana de la aplicacion todas las ventanas de
la aplicacion apareceran en primer termino. Es decir, la ventana invisible se utiliza para conectar 10s diferentes formularios de la aplicacion. En realidad, la ventana de la aplicacion no esta oculta; porque eso afectaria a su comportamiento, solo
tiene una altura y anchura nulas y, por lo tanto, no se ve.
--
--,
Cuando se crea una nueva aplicacion en blanco, Delphi genera un codigo para
el archivo de proyecto. que incluye lo siguiente:
begin
Application.Initialize;
Application.CreateForm(TForml,
Application.Run;
end.
Forml);
TRUCO: Para evitar discrepancias entre 10s dos titulos, se puede cambiar
el titulo de la aplicacion en tiempo de disefio. En caso de que cambie en
tiempo de ejecucion, se puede copiar el titulo del formulario a1 de la aplicacion con el siguiente codigo: Application.Title := Forml.
Caption.
En la mayoria de las aplicaciones, no tenemos en cuenta la ventana de la
aplicacion, aparte de fijar su titulo e icono y controlar algunos eventos. Sin embargo, podemos realizar operaciones sencillas. Si definimos la propiedad
ShowMainForm como False en el codigo fuente del proyecto, indicamos que
el formulario principal no deberia aparecer a1 arrancar. Dentro de un programa,
en cambio, se puede usar la propiedad MainForm del objeto Application
para accedcr a1 formulario principal.
Las dos funciones GetWindowLong y SetWindowLong de la API acceden a la informacion del sistema relacionada con la ventana. En este caso, utiliza-
mos el parametro gwl Style para leer o escribir el estilo de la ventana, lo que
incluye su borde, tituloTmenfi de sistema, iconos del borde, etc. El c6digo anterior
obtiene 10s estilos actuales y aiiade (usando una sentencia or) un borde estandar
y un titulo a1 formulario. Generalmente no es necesario implementar algo como
esto en 10s programas. Pero saber que el objeto aplicacion tiene una ventana
conectada a el y que se puede modificar es un aspect0 importante para comprender la estructura por defect0 de las aplicaciones Delphi.
IntToStr
Figura 8.1. El ejemplo ActivApp muestra si la aplicacion esta activa y cual de sus
formularios esta activo.
Hay que tener en cuenta que debe desactivarse la creacion del formulario secundario mediante la pagina Forms del cuadro de dialog0 Project Options.
Una de las partes clave del programa es el controlador del evento oncreate del
formulario, que rellena la lista por primera vez y, a continuacion. conecta un
controlador a1 evento OnAc tiveFormchange:
procedure TMainForm.FormCreate(Sender: TObject);
begin
FillFormsList (Self);
/ / define el contador de formularios secundarios a 0
nForms : = 0;
/ / define un controlador de eventos para el objeto en
pantalla
Screen.0nActiveFormChange : = FillFormsList;
end;
(Sender: TObject) ;
I: Integer;
begin
/ / ornite codigo en la fase de destruccidn
if Assigned (FormsListBox) then
begin
ADVERTENCIA:Resulta muy importante no ejecutar este codigo mientras se destruye el formulario principal. Como alternativa a comprobar que
el cuadro de lista no este definido como nil,tambitn podemos probar el
Component state del formulario para el indicador csDestroying.
Otra tkcnica seria destruir el controlador de eventos OnAc tiveForrnChange antes de salir de la aplicacion, es deck, controlar el evento
.
.
.1
.u
nc; o
s e oer Prormulario
principal y asignar n. 1 1 a- s; c_ r
e e .n.
OnActiveFormChange.
. -
nombre del activo. Cuando hacemos clic sobre el boton New, el programa crea
una instancia del formulario secundario, le aiiade un nuevo titulo y lo muestra. El
cuadro de lista Forms se actualiza automaticamente debido a1 controlador del
evento OnAc t i v e Fo r m C ha nge instalado. En la figura 8.2, aparece el resultado de este programa cuando se han creado diversos formularios secundarios.
MI
Aclive Farn : S e c d 3
Farns: 4
TSecondForm . Second 3
TSecondForm - Second 2
TSecondForrn .Second 1
TMa~nForrn- Sc~eenInfo
Figura 8.2. El resultado del ejemplo Screen con algunos formularios secundarios.
Cada uno de 10s formularios secundarios tiene un boton Close que podemos
pulsar para eliminarlos. El programa controla el evento onclose,definiendo el
parametro Action como caFree,de mod0 que en realidad el formulario se
destruye cuando lo cerramos. Este codigo cierra el formulario, per0 no actualiza
la lista de ventanas como es debido. El sistema desplaza primer0 el foco a otra
ventana, activando el evento que actualiza la lista y destruye el antiguo formulario solo despues de dicha operacion.
Una priinera aprosimacion puede plantear que para actualizar la lista de ventanas adecuadamente puede introducirse un retraso, enviando un mensaje de
Windows definido por el usuario. Pero debido a que el mensaje enviado es encolado y no es tratado inmediatamente, aunque el envio se realice a1 final de la esistencia del formulario secundario, el formulario principal lo recibira cuando el
otro formulario sea destruido. El truco esta en poder enviar el mensaje con el
controlador del evento OnDestroy del formulario secundario. Para ello, es necesario referirse al objeto MainForm,aiiadiendo una sentencia uses en la parte
de implernentacion de esta unidad. Hemos enviado un mensaje wm User,controcomo podelado por un mCtodo message especifico del formulario
mos ver a continuacion:
public
procedure Childclosed (var Message: TMessage) ; message
-User;
procedure TMainForm.ChildC1osed
begin
FillFormsList
( S e l f );
end:
El problema es que si cerramos la ventana principal antes de cerrar 10s formularios secundarios; el formulario principal sigue existiendo, per0 su codigo ya no
se puede ejecutar. Para evitar otro error del sistema, solo se debe enviar el mensaje si el formulario principal no se esta cerrando. Para poder determinar si se esta
cerrando se puede aiiadir un indicador a la clase TMainForm y modificar su
valor cuando se cierra el formulario principal, para asi poder probar el indicador
desde el codigo de la ventana secundaria. Esta es una buena solucion, tan buena
que la VCL ya proporciona una funcionalidad similar con la propiedad
Componentstate y su indicador csDestroying,como ya se ha mencionado anteriormente. Por tanto, podemos utilizar el siguiente codigo:
procedure T S e c o n d F o r m . F o r r n D e s t r o y (Sender: TObject);
begin
i f not (csDestroying i n MainForm.ComponentState) then
0, 0) ;
end ;
Con este codigo, el cuadro de lista siempre muestra todos 10s formularios de la
aplicacion.
Existe otra alternativa, una solucion mas orientada a Delphi. El truco esta en
considcrar que cada vez que un componente es destruido, avisa a su propietario
acerca del evento llamando a1 metodo Notification definido en la clase
TComponent.Dado que 10s forn~ulariossecundarios son propiedad del formulario principal, como se ha mostrado en el codigo del metodo NewButtonClick,
puede sobrecargarse este metodo y simplificarse el codigo (vease el directorio
Screen2 de 10s ejemplos adjunto para ver el codigo de esta version):
,
procedure TMainForm.Notification
(AComponent: TComponent;
Operation: Toperation) ;
begin
i n h e r i t e d Not if ication (AComponent, Operation) ;
i f (Operation = opRemove) and Showing and (AComponent i s
TForm) then
FillFormList ;
end ;
De eventos a hilos
Para comprender como funcionan las aplicaciones de Windows internamente,
dedicaremos unos momentos a explicar como soporta este entorno la multitarea.
Es necesario comprender tambien, el papel de 10s temporizadores (y el componente T i m e r ) y 10s calculos en segundo plano (o en espera), asi como el metodo
P r o c e s s M e s s a g e s del objeto global A p p l i c a t i o n .
Resumiendo, debemos sumergirnos mas en la estructura orientada a eventos de
Windows y su soporte a la multitarea. Dado que este libro esta dedicado a la
programacion con Delphi no entraremos en detalles sobre este tema, per0 daremos una vision global para aquellos lectores que tengan poca experiencia en
programacion con la API de Windows.
_-A!_.-
-1
L!
cion se detendra por completo durante el tiempo que emplee para procesar dicho
algoritmo. Para que el usuario sepa que se esta procesando algo, podemos usar el
cursor en forma de reloj de arena o una barra de progreso, per0 esta solucion no
sera la mejor para el usuario. Win32 permite que otros programas sigan ejecutandose, per0 el programa en cuestion se congelara, ni siquiera actualizara su propia
interfaz de usuario si se solicita que se vuelva a pintar. De hecho, mientras el
algoritmo se esta ejecutando, la aplicacion no podra recibir ni procesar ningun
otro mensaje, como 10s mensajes de representacion.
La solucion mas sencilla a este problema consiste en llamar a 10s metodos
ProcessMessages y HandleMessage descritos anteriormente. El problema de hacerlo asi esta en que el usuario podria pulsar de nuevo el boton o las
teclas que iniciaron dicho algoritmo. Para solucionarlo, se pueden desactivar 10s
botones y ordenes que no queramos que el usuario seleccione y mostrar el cursor
en forma de reloj de arena (que tecnicamente no evita que tenga lugar un evento de
pulsado del raton, pero si sugiere a1 usuario que deberia esperar antes de realizar
otra operacion).
Para realizar algunos procesos secundarios de baja prioridad, tambien podemos dividir el algoritmo en trozos mas pequeiios para ejecutar cada trozo de uno
en uno, dejando a la aplicacion que responda a todos 10s mensajes pendientes
mientras 10s procesa. Podemos usar un temporizador para hacer que el sistema
nos notifique cuando se ha consumido un interval0 de tiempo. Aunque podemos
usar temporizadores para implementar alguna forma de procesamiento secundario, esta no es una buena solucion. Seria mejor ejecutar cada paso del programa
cuando el objeto Application recibe el evento OnIdle.La diferencia entre
llamar a ProcessMes sages y usar el evento OnIdle esta en que a1 llamar a
ProcessMessages daremos a1 codigo mas tiempo de procesado que con la
tecnica OnIdle.Llamar a ProcessMessages es un buen mod0 de dejar que
el sistema realice otras operaciones mientras el programa esta procesando. Utilizar el evento OnIdle es una forma de dejar que la aplicacion realice las tareas
secundarias cuando no hay solicitudes del usuario pendientes.
Multihilo en Delphi
Cuando es necesario realizar operaciones en segundo plano, o cualquier proceso no estrictamente relacionado con la interfaz de usuario, se puede utilizar la
aproximacion mas correcta desde el punto de vista tecnico: crear un hilo de ejecucion separado dentro del propio proceso. La programacion multihilo puede parecer un tema complejo, pero, realmente, no es tan complicado, aunque tenga que
ser considerado cuidadosamente. Es conveniente conocer a1 menos 10s fundamentos de la programacion multihilo porque, en el mundo de 10s sockets y la programacion para Internet, hay pocas cosas que se puedan hacer sin hilos.
La biblioteca RTL de Delphi contiene una clase TThread que permite crear y
controlar hilos. La clase TThread no se utiliza nunca directamente dado que es
una clase abstracta (una clase con un metodo abstracto virtual). Para usar hilos,
sc hereda de T T h r e a d y se utilizan las caracteristicas de esta clase base.
La clase T T h r e a d tiene un constructor con un unico parametro
( C r e a t e s u s p e n d e d ) que permite elegir entre arrancar el hilo inmediatamente
o dejarlo en espera hasta mas tarde. Cuando el objeto hilo arranca automaticamente,
o cuando se reanuda su ejecucion, mantiene el metodo E x e c u t e en funcionamiento hasta el final. La clase proporciona una interfaz protegida que incluye dos
mctodos basicos para 10s hilos:
procedure Execute:virtual; abstract;
p r o c e d u r e Synchronize(Method: TThreadMethod);
pasado mediante una propiedad publica (Max), y dos valores internos (FTo t a l
y FPo s i t i o n ) usados para sincronizar la salida de 10s metodos S ho wTo t a 1y
U p d a t e P r o g r e s s . Esta es la declaracion completa para el objeto hilo:
tYPe
TPrimeAdder = c l a s s (TThread)
private
FMax, FTotal, FPosition: Integer;
protected
procedure Execute; override;
procedure ShowTotal;
procedure UpdateProgress;
public
property Max: Integer read FMax write FMax;
end ;
( FTotal) ) ;
procedure TPrimeAdder-Updateprogress;
begin
Forml.ProgressBar1.Position : = Position;
end :
El objeto hilo se crea a1 hacer clic sobre un boton y es automaticamente destruido cuando termina el metodo E x e c u t e :
',
nil ) ;
Application.CreateForm(TForm1, Forml);
Application.Run;
end
else
SetForegroundWindow
end.
(Hwnd)
Uso de un mutex
Un mutex, u objeto de exclusion mutua, es una tecnica totalmente distinta. Es
una tecnica comun de Win32, utilizada normalmente para sincronizar hilos. Aqui,
utilizamos un mutex para sincronizar dos aplicaciones diferentes o, para ser mas
precisos, dos instancias de la misma aplicacion.
Cuando una aplicacion ha creado un mutex con un nombre determinado, puede
probar si el objeto ya lo posee otra aplicacion, llamando a la funcion de la API de
Windows Wait ForSingleOb jec t.De no ser asi, la aplicacion que llama a
esta funcion se transforma en la aplicacion propietaria. Pero si el mutex ya tiene
un propietario, la aplicacion espera hasta que se consume el tiempo (el segundo
parametro de la funcion). Entonces devuelve un codigo de error.
Para implementar esta tecnica, podemos usar el siguiente codigo fuente del
proyecto:
var
hMutex: THandle;
begin
hMutex : = CreateMutex (nil, False, 'OneCopyMutex ' ) ;
if WaitForSingleObject (hMutex, 0 ) <> wait-Timeout then
begin
Application.Initialize;
Application.CreateForm(TForm1,
Application.Run;
end;
end.
Forml);
Si ejecutamos dos veces este ejemplo, veremos que se crea una nueva copia
temporal de la aplicacion (aparece el icono en la barra de tareas) y despues se
destruye cuando se ha consumido el tiempo Esta tecnica es realmente mas robusta
que la anterior, per0 tiene un pequeiio problema que esta en activar la instancia
existente de la aplicacion y encontrar su formulario, para lo que podemos emplear
una tecnica mejor.
end;
end;
end;
Esta funcion, a la que se llama para cada ventana no hijo del sistema, verifica
el nombre de cada clase de ventana, buscando el nombre de la clase TForml.
Cuando encuentra una ventana con esta cadena en su nombre de clase, usa
GetModule Fi lename para extraer el nombre del archivo ejecutable de la
aplicacion que pertenece a1 formulario correspondiente. Si el nombre del modulo
corresponde a1 del programa en uso (que se extrajo anteriormente con un codigo
similar), podemos estar casi seguros de que encontraremos una instancia anterior
del mismo programa. Veamos el mod0 en que podemos llamar a la funcion enumerada:
var
FoundWnd: THandle;
ModuleName: string;
begin
i f Wait ForSingleObj ect
- ..
then
else
begin
/ / o b t i e n e e l n o m b r e d e l m o d u l o en u s o
SetLength (ModuleName, 200) ;
GetModuleFileName (HInstance, PChar (ModuleName), Length
(ModuleName)) ;
ModuleName : = PChar (ModuleName); // a j u s t a l a l o n g i t u d
// b u s c a una v e n t a n a d e una i n s t a n c i a p r e v i a
EnumWindows (@EnumWndProc, 0) ;
SetForegroundWindow (FoundWnd);
end:
App en lugar de wm U s e r ;
algunas ventanas del sistema usan wm ~ s e r , p o lo
r que no hay gar&ia de
enviarhn este mensaje. Esta es la
que otras aplicaciones o el sistema
razon por la que Microsofi introdujo wm ~ p para
p 10s mensajes que estim
limitados a la interpretacibn de la ap1icaci6n.
no
NOTA: Microsofi se aleja cada vez mas del modelo MDI utilizado en 10s
dias de Windows 3. Incluso las versiones recientes de Office tienden a
utilizar ventanas principales especificas para cada documento, la tCcnica
clasica SDI (Interfaz de Documento ~ n i c o ) En
. cualquier caso, MDI no
esta muerto y, a veces, puede ser una estructura util.
ChildForm: TChildForm;
begin
+ IntToStr
Tambien podemos abrir ventanas hijo, minimizar o maximizar cada una de las
mismas, cerrarlas y usar el menu desplegable Window para movernos de unas a
otras. Supongamos ahora que queremos cerrar alguna de estas ventanas hijo, para
despejar el area cliente del programa. Si hacemos clic en las cajas Close de
alguna de estas ventanas, esta se minimiza, en lugar de ocultarse como ocurria
hasta ahora. Los formularios cerrados en Delphi siguen existiendo aunque no
Sean visibles.
En el caso de las ventanas hijo, ocultarlas no funcionara porque el menu MDI
Window y la lista de ventanas continuara mostrando las ventanas hijo existentes,
aunque esten ocultas. Por esta razon, Delphi minimiza las ventanas MDI hijo
cuando intentamos cerrarlas. Para resolverlo, debemos borrarlas cuando son cerradas dando el valor ca Free a1 p a r h e t r o de referencia Act ion del evento
OnClose.
El procedimiento ArrangeIcons: Organiza todas las ventanas hijo reprcsentadas por iconos. Los formularios que estan abiertos no se mueven.
Como una alternativa mejor a la llamada a estos metodos, sc puede colocar un
A c t i o n L i s t en el formulario y aiiadirlo a una serie de acciones MDI
predefinidas. Las clases relacionadas son: TWi ndowArrange, TWi ndowCas cade, TWindowClose,TWindowTileHorizonta1,TWindowTileVertical y TWindowMinimizeAll. Los elementos del menu conectados
El ejemplo MdiDemo
Hemos creado un primer ejemplo para demostrar la mayoria de las funciones
de una aplicacion sencilla MDI. MdiDemo es en realidad un editor de testos MDI
completo, porque cada ventana hijo aloja un componente Memo y puede abrir y
guardar archivos de testo. El formulario hijo tiene una propiedad Modified
utilizada para indicar si el texto del memo ha cambiado (esta definido como True
en el controlador del evento OnChange del memo). Modified esta definida
como False en 10s metodos personalizados Save y Load y se verifica cuando
se cierra el formulario (sugiriendo que se guarde el archivo).
Como ya hemos dicho, el formulario principal del ejemplo esta basado en un
componente ActionList. Las acciones del ejemplo estan disponibles mediante algunos elementos de menu y en una barra de herramientas, como muestra la figura
8 . 3 . Para conocer 10s detalles del ActionList podemos estudiar el codigo fuente
del ejemplo; aqui nos centraremos en el codigo de las acciones.
o lnleresanle
lgo Inlelesante
lgo Intelesante
lgo lntelesanle
lyo ~nteleranle
h o ~nlererante
Figura 8.3. El prograrna MdiDemo hace uso de una serie de acciones Delphi
predefinidas conectadas a un menu y una barra de herrarnientas.
FontDialogl.Font;
end;
procedure TMainForm.ActionFontUpdate(Sender:
begin
ActionFont.Enabled : = MDIChildCount > 0;
end;
TObject);
TObject);
Figura 8.4. El resultado del ejemplo MdiMulti, con una ventana hijo que rnuestra
10s circulos.
El formulario principal
Tenemos que integrar ahora 10s dos formularios hijo en una aplicacion MDI.
El menu desplegable File tiene dos elementos de menu New aparte, que se usan
para crear una ventana hijo de cualquier tipo. El codigo usa un solo contador de
ventanas hijo. Como alternativa, podriamos usar dos tipos diferentes de contadores para 10s dos tipos de ventanas hijo. El menu Window usa las acciones MDI
predefinidas. Desde el momento en que un formulario de este tip0 aparece en
pantalla, su barra de menu se mezcla automaticamente con la barra de menu
principal. Cuando seleccionamos un formulario hijo de uno de 10s dos tipos, la
barra de menu cambia de acuerdo con ello. Cuando se hayan cerrado todas las
ventanas hijo, se configura de nuevo la barra de menu original del formulario
principal. A1 usar indices de menu adecuados, permitimos que Delphi lo haga
todo de forma automatica, como muestra la figura 8.5.
Se han aiiadido unos pocos elementos a1 menu del formulario principal para
cerrar todas las ventanas hijo y mostrar algunas estadisticas sobre ellas. El metodo relacionado con el comando C o u n t analiza la propiedad MDIChildren
para contar el numero de ventanas hijo de cada tipo (usando el operador RTTI
is):
for I : = 0 t o MDIChildCount - 1 do
if MDIChildren is TBounceChildForm then
Inc (NBounce);
else
Inc (NCircle) ;
,
:
,
,
,
,,A,C,:.-l,
,..,
.:.,,
,
,
I,,
,&+,A,
A,
....-...+-,l
A,
,.,.
UG I l l G l l D t l J G 3 ~ l G U G l L l l l U U ,r j U G 36 I G G I I V l 4 4 I V 3 IILGLUUUD U G b V U L I U I U G l l L G l l D 4 '
El formulario tiene tambien un metodo que usaremos como un nuevo procedimiento de ventana, con el codigo real usado para pintar en el fondo de la ventana.
Dado que este es un metodo y no un procedimiento de ventana normal, el programa ha de llamar a1 metodo MakeOb j ectInstance para aiiadir un prefijo al
metodo y dejar que el sistema lo use como si fuera una funcion. Toda esta descripcion se resume en dos sentencias complejas:
procedure TMainForm. Formcreate (Sender: TObject) ;
begin
NewWinProc : = MakeObjectInstance (NewWinProcedure);
OldWinProc : = Pointer (SetwindowLong (ClientHandle,
gwl-WndProc, Cardinal
(NewWinProc)) ) ;
Outcanvas := TCanvas .Create;
end:
formularios estandar). La principal ventaja de la herencia visual es que mas adelante puede modificarse el formulario original y actualizar automaticamente todos 10s formularios derivados. Esta es una de las ventajas de la herencia en 10s
lenguajes de programacion orientados a objctos. Pero existe un efecto colateral
muy beneficioso: el polimorfismo. Podemos aiiadir un metodo virtual en un formulario base y sobrecargarlo en un formulario heredado para despucs referirnos a
ambos formularios y poder llamar a ese metodo en cada uno de cllos.
NOTA: Delphi incluye otra funcion, 10s marcos, que imita la herencia de
formularios visuales. En arnbos casos, se puede trabajar en tiempo de diseiio en las dos versiones de un formulario/marco. Sin embargo, en la herencia de formularios visuales, definimos dos clases distintas (padre y derivada),
mientras que con 10s marcos, trabajamos en una clase y en una instancia.
Los marcos se trataran mas adelante en este capitulo.
a a
Form2
6 !nhrit
Figura 8.6. El cuadro de dialogo New Items permite crear un formulario heredado
Fijese en la presencia de la palabra clave inherited en la descripcion textual y tambien en que el formulario tiene algunos componentes, aunque estan
definidos en el formulario de clase basico. Si movemos el formulario y aAadimos
el titulo de uno de 10s botones, la descripcion textual cambia de acuerdo con
dichos cambios:
inherited Form2 : TForm2
Left = 313
Top = 202
Caption = 'Form2'
inherited Button2: TButton
Caption = ' B e e p . . . '
end
end
Solo se listan las propiedades con un valor diferente (por lo que si quitamos
estas propiedades de la descripcion textual del formulario heredado podemos dejarlas con 10s valores del formulario base). Como muestra la figura 8.7, hemos
cambiado el titulo de la mayoria de 10s botones.
Figura 8.7. Los dos formularios del ejemplo VFI en tiempo d e ejecucion
Cada uno de 10s botones del primer formulario tiene un controlador OnClic k .
El primer boton muestra el formulario heredado llamando a su metodo Show,el
segundo y tercer boton llaman a1 procedimiento Beep y el ultimo boton muestra
un mensaje sencillo.
En el formulario heredado, primer0 deberiamos eliminar el boton Show, porque el formulario secundario ya esta visible. Sin embargo; no podemos borrar un
componente de un formulario heredado. Podemos dejar el componente, per0 definir su propiedad Visible como False (el boton seguira estando ahi per0 no
sera visible). Los otros tres botones estaran visibles per0 con distintos
controladores. Esto es sencillo de conseguir. Si seleccionamos el evento OnClic k
de un boton en el formulario heredado (haciendo doble clic en el), obtendremos un
metodo ligeramente diferente a1 predefinido; porque incluye la palabra clave
inherited.Esta palabra clave representa m a llamada a1 controlador de eventos correspondiente. Esta palabra clave siempre la aiiade Delphi, incluso si el
controlador no se define en la clase padre o si el componente no esta presente en la
clase. Es sencillo ejecutar el codigo del formulario base y realizar algunas operaciones mas:
procedure TForm2.Button2Click(Sender:
begin
inherited;
ShowMessage ( 'Hi' ) ;
end;
TObject);
procedure TForm2.ButtonrlClick(Sender:
begin
inherited Button3Click ( S e n d e r ) ;
inherited;
end;
TObject);
,.:,.,
.,
A
:
,
.I
,
,
c,
,
I
L
,
,,&A
,
,
,A,:
I,,,,:,,
A, ,
.c
,
,:,,,I,..
ulla SUIUGIUIIa CSLC ~ I U U I C I CSW
I I ~ cu C V I F ~ Ila a a q p a w u u uc csrira GU~CGGIU-
TT-,
Formularios polimorficos
Si quercmos afiadir un controlador de eventos a1 formulario y despues convertirlo en formulario heredado, no hay forma de referirse a 10s dos metodos que
usan una variable comun de la clase basica; porque 10s controladores de eventos
usan por defect0 enlace estatico.
Veamos un ejemplo: queremos crear un formulario visor de mapas de bits y un
visor de texto en el mismo programa. Los dos formularios tienen elementos similarcs, una barra de herramientas similar, un menii similar, un componente
OpenDialog y componentes diferentes para ver 10s datos. Por lo tanto, decidimos
crear un formulario de clase basica que contenga 10s elementos comunes y herede
10s dos formularios de el. En la figura 8.8, podemos ver 10s tres formularios en
tiempo de diseiio.
Figura 8.8. El forrnulario d e clase basica y 10s dos forrnularios heredados del
ejernplo PoliForrn.
El formulario principal contiene un panel de barra de herramicntas con algunos botones (las barras de herramientas reales tienen problemas con la hcrcncia
de formularios visuales); un menu y un componente dc dialog0 abierto. Los dos
formularios heredados tienen solo diferencias mcnores, per0 presentan un nuevo
componente, o un visor de imageries ( T I m a g e ) o un visor de testo (TMemo).
Tambien modifican la configuration del componente O p e n D i a l o g , para referirse a diferentes tipos dc archivos.
El formulario principal incluye algo de codigo comun. El boton Close y la
orden File,>Close llaman a1 mdtodo c l o s e del formulario. La orden Help>About
mucstra un cuadro de mensa.je sencillo. El boton Load del formulario base tiene
unicamente una llamada a S h o w M e s s a g e para mostrar un mensa.je de error. La
ordcn File>Load en cambio llama a otro mttodo:
procedure
begin
TViewerForm.LoadlClick(Sender: T O b j e c t ) ;
LoadFile;
end;
1magel.Picture.LoadFromFile (0penDialogl.Filename);
end;
La otra clase heredada tiene un codigo similar, que carga el texto en el componente memo. El proyecto tiene un formulario mas, un formulario principal con
dos botones, utilizados para cargar de nuevo 10s archivos en cada uno de 10s
formularios de visor. El formulario principal es el unico formulario creado por el
proyecto a1 iniciar. El formulario de visor generic0 nunca se crea: solo es una
clase basica generica, que contiene codigo comun y componentes de dos subclases.
Los formularios de las dos subclases se crean en el controlador de eventos
oncre at e del formulario principal:
p r o c e d u r e TMainForm.FormCreate(Sender: T O b j e c t ) ;
var
I: Integer;
begin
FormList [ l ] : = TTextViewerForm.Create (Application);
FormList [2] : = T1mageViewerForm.Create (Application);
for I := 1 to 2 do
FormList [I] .Show;
end ;
(Self);
//ReloadButtonZClick
for I := 1 to 2 do
FormList [I] .LoadFile;
public
procedure LoadFile; virtual; abstract;
end;
type
TImageViewerForm = class (TViewerForm)
procedure ButtonLoadClick (Sender: TOb ject) ; override;
public
procedure LoadFile; override;
end;
original de la plantilla y ver el efecto en todos 10s sitios donde la usabamos. Con
marcos (y, de otra manera, con la herencia de formularios visuales), 10s cambios
de la version original (la clase) se reflejan en la copia (las instancias).
Podemos ver algunos elementos mas sobre 10s marcos con el ejemplo Frames2.
Este programa tiene un marco con un cuadro de lista, un cuadro de edicion y tres
botones con codigo sencillo que opera en 10s componentes. El marco tiene tambien un boton Bevel alineado con su zona de cliente, porque 10s marcos no tienen
borde. El marco tiene tambien una clase correspondiente que parece una clase de
formulario:
type
TFrameList = class (TFrame)
ListBox: TListBox;
Edit: TEdit;
btnAdd: TButton;
btnRemove: TButton;
btnclear: TButton;
Bevel: TBevel;
procedure btnAddClick (Sender: TObj ect) ;
procedure btnRemoveClick(Sender: TObject);
procedure btnClearClick (Sender: TObject) ;
private
{ Declaraciones privadas )
public
{ Declaraciones publicas
end ;
end
end
......
&d(~I-l Du I , .: .; .: .; .; .:
, ... ... ... ... ... ...
I
meted
Some !ex(
Figura 8.9. Un rnarco y dos instancias del rnisrno en tiempo de disetio, en el ejemplo
Frarnes2.
tos. Si hacemos doble clic en uno de 10s botones del marco mientras trabajamos en
cl formulario (no en el marco independiente), Delphi generara este codigo por
nosotros:
procedure TFormFrames.FrameList2btnClearClick
TObject) ;
begin
FrameList2.btnClearClick (Sender);
end;
(Sender:
...
Marcos y fichas
Cuando tenemos un cuadro de dialogo con muchas fichas llenas de controlesj
el codigo subyacente a1 formulario se vuelve muy complejo, porque todos 10s
controlcs y metodos se declaran en un ilnico formulario. Ademas, a1 crear todos
estos componentes (e iniciarlos) podriamos originar un retraso en la aparicion del
cuadro de dialogo. En realidad, 10s marcos no reducen el tiempo de construccion
e inicializacion dc 10s formularios cargados de forina equivalente. A1 contrario, es
mas complicado cargar 10s marcos para el sistema de slrenrning que cargar componentes simples. Sin embargo, a1 utilizar marcos se puedan cargar solo las fichas visibles de un cuadro de dialogo, reduciendo el tiempo de carga inicial, que
es el que percibe cl usuario.
Los marcos pueden resolver estos dos temas. En primer lugar, se puede dividir
facilmente el codigo de un formulario unico complejo en un marco por ficha. El
formulario albergara sencillamente todos 10s marcos en un PageControl. Esto
ayuda realmente a tener unidades mas sencillas y mas centradas, y hace que
resulte mas sencillo reutilizar una ficha concreta en un cuadro de dialogo diferente o una aplicacion. Reutilizar una unica ficha de un PageControl sin usar un
marco o un formulario incrustado es muy complicado (para conocer una tecnica
alternativa vease el apartado "Formularies en fichas").
Para ilustrar esto, hemos creado el e.jemplo FramePag,que tiene algunos marcos colocados en el interior de las tres fichas de un Pagecontrol, como muestra la
figura 8.10. Todos 10s marcos estan alineados con la zona del cliente, utilizando
toda la superficie de la hoja de solapa (la ficha) en la que se encuentran. En realidad, dos de las fichas tienen el mismo marco, per0 dos de las instancias del inarco
tienen algunas diferencias en tiempo de diseiio. El marco, llamado Frame3 en el
ejemplo, tiene un cuadro de lista que contiene un archivo de texto al arrancar y tiene
botones para modificar 10s elementos de la lista y guardarlos en un archivo. El
nombre de archivo se coloca en una etiqueta, para poder seleccionar con facilidad
un archivo en tiempo de diseiio cambiando el titulo de la etiqueta.
Figura 8.10. Cada ficha del ejemplo FramePag contiene un marco, separando el codigo
de este formulario complejo en trozos mas rnanejables.
marcos solo cuando aparece una ficha. Si tenemos marcos en varias fichas de un
Pagecontrol, las ventanas para 10s marcos se crean solo cuando se muestran por
primera vez, como podemos comprobar si colocamos un punto de parada en cl
codigo de creacion de ejemplo anterior.
Como tecnica mas drastica; podemos eliminar 10s controles de ficha y utilizar
TabControl. De este modo, la solapa no esta conectada a ninguna hoja (o ficha)
sino que simplemente muestra un conjunto de informacion cada vez. Por dicha
razon, sera necesario crear el marco actual y destruir el anterior o sencillamente
ocultarlo definiendo su propiedad V i s i b l e como F a l s e o llamando a
BringTo Front del nuevo marco. A pesar de que esto puede parecer muy trabajoso, en una aplicacion grandc esta tecnica puede merecer la pena por el reducido uso de recursos y memoria que se obtiene.
Para demostrar esta tecnica, hemos creado el ejemplo FrameTab, similar a1
antcrior, basado csta vez en un TabControl y hemos creado marcos de forma
dinamica. El formulario principal, visible en tiempo de ejecucion en la figura
8.1 1; solo tiene un TabControl con una ficha para cada marco:
o b j e c t Forml: TForml
Caption = ' F i c h a s d e M a r c o '
OnCreate = Formcreate
o b j e c t Buttonl: TButton...
o b j e c t Button2: TButton...
o b j e c t Tab: TTabControl
Anchors = [akLeft, akTop, akRight , akBottom]
Tabs .Strings = ( ' M a r c o 2 ' ' M a r c o 3 ' )
OnChange = Tabchange
end
end
Hemos dado un titulo para cada solapa que corresponde a1 nombre del marco,
porque vamos a usar esta informacion para crear nuevas fichas. Cuando creamos
el formulario y siempre que el usuario cambia la solapa activa, el programa
obtiene el titulo actual de la solapa y lo pasa a1 metodo Show Frame.El codigo
de este metodo, que aparece a continuacion, verifica si el marco solicitado ya
existe (10s nombres de 10s marcos de este ejemplo siguen el estandar de Delphi de
llevar un numero agregado a1 nombre de la clase) y, despues, hace que aparezca
en la parte de delante. Si el marco no existe, usa el nombre del marco para encontrar la clase de marco relacionada, crea un objeto de dicha clase y le asigna
algunas propiedades. El codigo utiliza de forma ampliada las referencias de clase
y las tecnicas de creacion dinamica:
type
TFrameClass
class of TFrame;
p r o c e d u r e TForml.ShowFrarne(FrameNarne: string);
var
Frame: TFrame;
FrameClass: TFrameClass;
begin
Frame : = Findcomponent (FrameName + '1 ' ) a s TFrame;
i f n o t Assigned (Frame) t h e n
begin
FrameClass : = TFrameClass ( Findclass ( 'T' + FrameName) ) ;
Frame : = FrameClass .Create (Self);
Frame.Parent : = Tab;
Frame.Visible : = True;
Frame.Name : = FrameName + '1';
end;
Frame.BringToFront;
end ;
formulario base que no tiene componentes adicionales podemos utilizar otra tecnica. Es preferible utilizar una clase de formulario personalizada, heredada de
T F o r m y, a continuacion, editar manualmente las declaraciones de clase del formulario para heredar de esa clase de formulario base personalizada en lugar de
heredar de la estandar. Si todo lo que hay que hacer es definir algunos metodos
compartidos o sobrescribir 10s metodos virtuales T F o r m de forma continuada,
puede ser buena idea definir clases de formulario personalizadas.
inherited;
end ;
Estc cs un ejemplo muy sencillo per0 podemos definir una clase compleja en su
interior. Para utilizar esta como clase base para 10s formularios que construyamos, debemos dejar que Delphi c:ee 10s formularios como siempre (sin herencia)
y, despues, actualizaremos la declaracion a algo parecido a este:
type
TFormBitmap = class (TSaveStatusForm)
Imagel: TImage;
OpenPictureDialogl: TOpenPictureDialog;
Aunque es tan sencilla como parece, esta tecnica es muy potente, porque lo
unico que tenemos que hacer es cambiar la definicibn de 10s formularios de nuestra aplicacion para referirnos a esta clase base. Incluso si este paso nos resulta
muy tedioso porque, quiza, queramos en algun momento cambiar esta clase en
nuestro programa, podemos usar un truco extra: las clases de interposicion
-
--
- - --
guardar'los datos de forma locd en la itpii~&i&n(en oposicibn a bacerlo de forma local para el usuario actual), deberiamos ofiecer una ruta
completa a1 constructor.
Los archivos IN1 se dividen en secciones, cada una de ellas indicada
mediante un nombre entre corchetes. Cada apartado puede contener
rlivarcnc alam~ntncAP tree tinnc nncihlac- rarlenac antarnc n hnnlaannc
.
c s p c c ~ ~ ~UIIc a
valor
~
p~cu~lmu
para
u uiuuar
SI lir
c o ~ ~ c s p o u u r c rcn~u
NOTA: Esta tecnica es mucho mas antigua que CLXNCL. Por ejemplo,
las unidades de servicio y panel de control definen su propio objetaI
TApplication. que no tiene nada que ver con el TApplication usa.. .
.
. -.-- . - . .
.. . do por las apllcaciones Vlsuales V L L y dekmldo en la unidad k-oms.
Existe una tecnica que hemos visto mencionada con el nombre de "clases de
interposicion", que sugeria que se sustituyesen 10s nombres de clase estandar de
Delphi por versiones propias, que tuviesen el mismo nombre de clase. Asi, podemos usar el diseiiador Delphi que se refiere a componentes estandar de Delphi en
tiempo de diseiio, per0 usando nuestras propias clases en tiempo de ejecucion.
La idea es sencilla. En la unidad SaveStatusForm, podriamos definir la nueva
clase de formulario asi:
type
TForm = class ( F o r m s . TForm)
protected
procedure D o c r e a t e ; override;
procedure D o D e s t r o y ; override;
end;
es dificil saber lo que ocurre. Y tienen razon: las clases de interposicion son
practicas a veces (mas para componentes que para formularios), per0 su uso hace
10s programas menos legibles y? en algunas ocasiones, mas dificil de depurar.
Uso de interfaces
Otra tecnica, que es ligeramente mas compleja per0 mas potente que la definicion de una clase de formulario comun consiste en tener formularios que
implementes interfaces especificas. Asi, podemos tener formularios que
implementen una o mas interfaces, consulten cada formulario para saber que
interfaz implementa y llamen a 10s metodos soportados.
Como ejemplo, hemos definido una interfaz simple para cargar y almacenar:
type
IFormOperations = i n t e r f a c e
[ ' {DAC,FDB76-0703-4A40-A951-1OD1 40B4AZAO)
p r o c e d u r e Load;
p r o c e d u r e Save;
end;
'1
Cada formulario puede implementar opcionalmente esta interfaz, como la siguiente clase T FormBitmap:
type
TFormBitmap = c l a s s (TForm, IFormOperations)
Imagel: T I m a g e ;
OpenPictureDialogl: TOpenPictureDialog;
SavePictureDialogl: TSavePictureDialog;
public
p r o c e d u r e Load;
p r o c e d u r e Save;
end;
El codigo de ejemplo incluye 10s metodos Load y Save, que utilizan 10s
cuadros de dialog0 estandar para cargar o guardar la imagen (en el ejemplo, el
formulario hereda tambien de la clase TSaveStatus Form). Cuando una aplicacion tiene uno o mas formularios que implementan interfaces, podemos aplicar
un metodo de interfaz concreto a todos 10s formularios que lo soportan, con codigo como este (extraido del formulario principal del ejemplo FormIntf):
p r o c e d u r e TFormMain.btnLoadClick (Sender: T O b j e c t ) ;
var
i: Integer;
iFormOp: IFormOperations;
begin
f o r i : = 0 t o Screen.FormCount - 1 d o
i f Supports (Screen.Forms [i],' IFormOperations, iFormOp) t h e n
iFormOp-Load;
end ;
initialization
finalization
if AllocMemCount > 0 then
begin
Str (AllocMemCount, msg) ;
Msg : = msg + ' b l o q u e s r e s t a n t e s e n l a p i l a ' ;
MessageBox ( 0 , PChar (msg), ' a g u j e r o d e memoria '
end ;
end.
MB-OK) ;
Creacion
de componentes
Delphi
La mayoria de 10s programadores en Delphi estaran, probablemente, acostumbrados a usarlos pero, a veces, puede resultar practico crear nuestros propios
componentes o personalizar 10s ya existentes. Uno de 10s aspectos mas interesantes de Delphi es que crear componentes no es mucho mas dificil que crear programas. Por esta razon, aunque este libro este dirigido a programadores de aplicaciones
Delphi en lugar de a creadores de herramientas, este capitulo tratara sobre la
creacion de componentes y presentara 10s afiadidos de Delphi, tales como 10s
componentes y editores de propiedades.
Este capitulo ofrece una introduccion a la creacion de componentes Delphi y
presenta algunos ejemplos. No hay espacio suficiente para presentar componentes
muy complejos per0 las ideas que hemos incluido cubren todos 10s fundamentos y
nos ofreceran un punto de partida.
Este capitulo trata estos temas:
Ampliacion de la biblioteca de Delphi.
Creacion de paquetes.
Componentes compuestos.
Uso de propiedades de interfaz.
Definicion de eventos personalizados.
paquete solo de disefio, suele estar enlazado estaticamente al archivo ejecutable, utilizando el codigo de 10s archivos DCU (Delphi Compiled Unit)
correspondientes. Sin embargo, hay que tener en cuenta que tambiln es
tecnicamente posible utilizar un paquete solo de diseiio como paquete de
tiempo de ejecucion.
Las aplicaciones de Delphi utilizan 10s paquetes de componentes solo de
cjccucion en tiempo de ejecucion. No sc pueden instalar en cl entorno Delphi,
per0 se afiaden autonxiticamente a la lista de paquetcs en tiempo de ejecucion cuando 10s necesita un paquetc solo de diseiio quc instalamos. Los
paquetes solo de ejecucion contienen normalmente el codigo de las clases
de componentes, per0 no poseen soporte en tiempo de diseiio (asi se minimiza el tamaiio de las bibliotecas de componentes incluidas con el archivo
ejecutablc). Los paquetes solo de ejecucion son importantes porque se pueden distribuir libremente junto con las aplicaciones, per0 no se pueden
instalar en cl entorno para crear programas nuevos.
Los paquetes normales de componentes (10s que no tienen ni la opcion de
solo disefio ni la dc solo ejecucion) no se pueden instalar y no se afiadiran
a la lista de paquctes en tiempo de ejecucion de forma automatica. Pueden
verse en paquetes de utilidades usados por otros paquetes, per0 no suelen
ser habituales.
Los paquetes que tengan ambos indicadores pueden instalarse y se afiaden
automaticamente a la lista de paquctcs en tiempo de ejecucion. Normalmente, dichos paquctes contienen componentes que necesitan poco o ningun soporte en tiempo de disefio (aparte del reducido codigo de registro del
componente)
I
/
---
~-
- -
--
- .-
--
comienzan con las letras DCL (por ejemplo, DCLSTDGO BPI,). Los nombres de archivo de 10s paquetes solo de ejecuci6n comienzan con las letras
VCL (por ejemplo, VCL60 .BPL). Podemos, si queremos, utilizar la mis. .
ma teenlea en nuestros paquetes.
Anteriormente, hemos tratado el efecto de 10s paquetes en el tamaiio del archivo ejecutable del programa. Ahora nos centraremos en la creacion de 10s paquetes, porque este es un paso necesario para la creacion o instalacion de componentes
en Delphi.
A1 compilar un paquetc de tiempo de ejecucion, se produce una biblioteca de
enlace dinamico con el codigo compilado (el archivo BPL) y un archivo solo con
una informacion de simbolo (un archivo DCP), que incluye el codigo maquina no
compilado. El ultimo archivo lo usa el compilador Delphi para reunir informacion
de simbolo sobre las unidades quc forman park del paquete sin tener acceso a 10s
...
&kUePapa
hid
A
J
Uno de 10s elementos clave de este listado es la definicion de clase, que comienza indicando la clase padre. La otra unica parte importante es el procedimiento R e g i s t e r . Como podemos ver, el asistente para componentes hace muy
poco trabajo.
ADVERTENC1A:El procedimiento Register debe escribirse con R mayliscula. Este requisito se impuso por razones de compatibilidad con C++
Builder (10s identificadores en C++ hacen distincion entre maylisculas y
minusculas).
r
r
r~ornvresue clase u~sr~rl~os.
ror esa razo11,la mayorla ue los uesarrollauores
type
TMdFontCombo = class (TComboBox)
private
FChangeFormFont: Boolean;
procedure SetChangeFormFont(c0nst Value: Boolean);
public
constructor Create (AOwner: TComponent); override;
procedure CreateWnd; override;
procedure Change; override;
published
property Style default csDropDownList;
property Items stored False ;
property ChangeFormFont: Boolean
read FChangeFormFont write SetChangeFormFont default True;
end;
Fijese en que ademas de dar un nuevo valor a la propiedad S t y l e del componente, en el metodo c r e a t e , hemos definido de nuevo dicha propiedad definiendo un valor con la palabra clave default. Tenemos que realizar ambas
operaciones, porque aiiadir la palabra clave default a una declaracion de propiedad no tiene un efecto direct0 en el valor inicial de dicha propiedad. Debemos
definir un valor predefinido porque las propiedades que tienen un valor igual a1
predefinido no se agrupan en el mismo stream que la definicion del formulario (y
no aparecen en la descripcion textual del formulario, el archivo DFM). La palabra clave d e f a u l t , informa a1 codigo de streaming, que el codigo de inicio del
componente definira el valor de dicha propiedad.
AP
UV
n~...nn..a..**
-..a
P"+~-A"
,...a..-rlfi
a1
ma..-
af
,.;a..*a
-a-n
~ 1 1 1 1 1 ~
~U
1 IILVUIIGIIL
U~U G G ~ L ~ I I ~
I U
I G
~ ~ U U kU1 I I I G I I U ~ ~ L I ~ I G I I C~G
YX"
ma..,.;
I 1 1V
1aa S G I I ~ I -
Boolean);
FChangeFormFont : = Value;
// refresca l a fuente
i f FChangeFormFont then
Change;
end ;
Creacion de un paquete
Ahora, tenemos que instalar el componente en el entorno, usando un paquctc.
Para este ejemplo; podemos crcar un nucvo paquctc o utilizar uno cxistcntc, como
el paquete predefinido del usuario.
En cada caso, hay que selcccionar la orden dcl menu Component>lnstall
Component. El cuadro dc dialog0 rcsultantc ticnc una ficha para instalar cl
componentc en un paquetc cxistentc y una ficha para crcar un nucvo paquctc. En
este ultimo caso? simplemente tecleainos un nombre dc archivo y una dcscripcion
dcl paquctc. A1 haccr clic sobrc cl boton OK sc abrc el Package Editor (veasc
la figura 9. l ) , que tiene dos partcs:
Compk
Add
T
I
Remove
Opl~ons
I
I
..
1.
3.
I
(
~ Q u hay
e detras de un paquete?
El Package Editor produce basicamente el codigo fuente del proyecto de
paquete: un tipo especial de DLL creada en Delphi. El proyecto de paquete se
guarda en un archivo con la extension DPK (de Delphi PacKage), mostrado si
pulsamos la tecla F12 en el editor de paquetes. Un proyecto de paquete normal es
como el siguiente:
package M d P a c k ;
...
{ $ D E S C R I P T I O N 'Mastering D e l p h i Package
{ $ IMPLICITBUILD ON)
I }
requires
vcl ;
contains
MdFontBox
end.
in
'MdFontBox.pa s ';
Como se puede ver, Delphi usa palabras clave del lenguaje especificas para
paquetes: la primera es la palabra clave package (similar a la palabra clave
library que se tratara mas adelante), que introduce un nuevo proyecto de
paquete.
A continuacion, hay una lista con todas las opciones del compilador, algunas
de las cuales han sido omitidas. Normalmente, las opciones de un proyecto Delphi
se guardan en un archivo a parte. Por el contrario, 10s paquetes incluyen todas las
opciones del compilador directamente en su codigo fuente. Entre las opciones de
compilador, esta la directiva de cornpilacion DESCRIPTION,utilizada para que
la descripcion del paquete este disponible en el entorno Delphi. De hecho, despues
de haber instalado un nuevo paquete, su descripcion aparecera en la ficha
Packages del cuadro de dialog0 Project Options, una ficha que se puede
Desciition
Campier
DrectarmlConddimls
,
Cornplerbtessages
Version Info
Luiker
Packages
Design packages
El proposito de este programa es solo probar el comportamiento del componente que acabamos dc crear. Aun asi, el componente no resulta muy util (podiamos haber aiiadido unas pocas lineas de codigo a un formulario para conseguir el
mismo efccto), per0 ver algunos componentes sencillos deberia servir de ayuda
para que nos hagamos una idea de lo que implica la construccion de un componente.
minacion no es solo una convencion, es un requisito para que el IDE pueda encontrar la imagen de una clase de componentes dada:
El nombre del recurso de mapa de bits habra de corresponder a1 nombre del
componente, incluida la letra inicial T. En este caso, el nombre del recurso
del mapa de bits deberia scr TMDFONTCOMBO. El nombre del recurso
de mapa dc bits habra de estar en mayuscula (esto es obligatorio).
Si queremos que el Package Editor reconozca e incluya el archivo de recurso, el nombre del archivo DCR habra de corresponder a1 nombre dc la
unidad compilada que define el componente. En este caso, el nombre de
archivo deberia ser M d C l o c k . DCR. Si incluimos el archivo de recurso
manualmente, mediante la directiva SR, podemos darle cl nombre que queramos asi como utilizar una extension RES y afiadir multiples mapas de
bits en dl.
Cuando esta listo cl mapa de bits para el componentc, podemos instalar el
componcnte en Dclphi. utilizando el boton InstaII Package de la barra de herramientas dcl Package Editor. Tras csta operacion, la scccion contains del
editor dcberia listar tanto el archivo PAS del componente como el correspondientc archivo DCR. En la figura 9.3 podcmos ver todos 10s archivos (tambien 10s
archivos DCR) de la vcrsion final del paquete MdPack.Si la instalacion DCR no
funciona correctamentc, se puede aiiadir manualmente la sentencia { $ R
unitname. d c r } en el codigo fuente del paqucte.
I.
_
.. ...
J Contains
.- -
--
. ....-.
.
-
.-.
3
b
Figura 9.3. La seccion Contains del Package Editor rnuestra tanto las unidades
incluidas en el paquete como 10s ficheros de recursos de componente.
Componentes internos
El componente en el que nos centraremos ahora es un reloj digital. Este ejemplo tiene algunas caracteristicas muy interesantes. Primero, tiene un componente
dentro de otro componente (un Temporizador). Segundo, muestra la tecnica de
datos en vivo: tendremos la posibilidad de ver un comportamiento dinamico (la
actualizacion del reloj) incluso en tiempo de diseiio, como ocurre, por ejemplo,
con componentes relacionados con datos.
--
_
I
(Value: Boolean);
Publicacion de subcomponentes
Ya desdc Delphi 6. podemos exponer simplemcnte el componente completo (cl
tcmporizador) en una sola propiedad, quc ampliara norn~allnenteel Object Inspector y pcrmitira a1 usuario definir cada una dc sus subpropiedades e incluso
controlar sus evcntos.
Vcamos la dcclaracion de tipo completa del componente T M d C l o c k , con cl
subconiponentc dcclarado en 10s datos privados y espucsto como una propiedad
publicada (en la ultima linea):
type
TMdClock = c l a s s (TCustomLabel)
private
FTimer : TTimer ;
protected
p r o c e d u r e Updateclock (Sender: TOblect);
public
constructor Create (AOwner: TComponent); override;
published
property Align;
property
property
property
property
property
property
property
property
property
property
property
end;
Alignment;
Color;
Font;
Parentcolor;
ParentFont;
ParentShowHint;
PopupMenu;
ShowHint ;
Transparent;
Visible;
Timer: TTimer read FTimer;
La propiedad T i m e r es solo de lectura, puesto que no queremos que 10s usuarios seleccionen otro valor para cste componente en el Object Inspector (ni que
desvinculen el componentc eliminando el valor de esta propiedad). Dcsarrollar
conjuntos de subcomponentes quc pueden ser usados alternativamente es posible,
per0 afiadir soporte de escritura para esta propiedad de un mod0 seguro no es
scncillo (considerando quc 10s usuarios de nuestros componentes puedcn no ser
programadores cspertos en Delphi). Por ello, es conveniente limitarse a propiedadcs de solo lectura para subcomponentes.
Para crcar cl tcmporizador. tenemos que sobrescribir el constructor del comp o n e n t ~rc10.j. El metodo c r e a t e llama a1 metodo correspondiente de la clase
basica y crea el objeto temporizador, instalando un controlador para su evento
OnTimer:
constructor T M d C l o c k - C r e a t e (AOwner: TComponent);
begin
i n h e r i t e d C r e a t e (AOwner);
// c r e a e l o b j e t o t e m p o r i z a d o r i n t e r n o
FTimer := TTimer .Create ( S e l f ) ;
FTimer .Name := ' C l o c k T i m e r ';
FTimer.OnTimer : = Updateclock;
FTimer. Enabled : = True;
FTimer . SetSubComponent ( T r u e );
end:
El codigo da un nombre a1 componente, para mostrarlo en el Object Inspector (vease la figura 9.4) y llama a1 metodo especifico S e t S u b C o m p o n e n t . No
necesitamos un destructor, sencillamente porque el objeto F T i m e r t i m e el
T M ~ ~ l o ccomo
k propietario (como indica el parametro de su constructor
C r e a t e ) , por lo tanto se destruira automaticamente cuando se destruya el componente reloj.
L.
Pfoyr~ks Events
~elc
Name
ParelllCobr
1- -
..
120
Mdnockl
True
-I- . ~ - * - - l - J - - 3 -
Componentes externos
Cuando un componente referencia un componentes externo, no crea este componente por si mismo (que es la razon por la que se le llama externo). Es el
programador que usa 10s componentes quien crea ambos separadamente (arrastrandolos desde la Paleta de Componentes a un formulario, por ejemplo) y
conecta 10s dos componentes usando una de sus propiedades. Por ello, podemos
decir que una propiedad de un componentes referencia a un componente enlazado
externamente. Esta propiedad debe ser de un tipo de clase que hereda de
TComponent.
class
(TComponent)
FirstName: s t r i n g r e a d F F i r s t N a m e w r i t e SetFirstName;
LastName: s t r i n g r e a d FLastName w r i t e SetLastName;
Age: Integer r e a d FAge w r i t e SetAge;
Description: string r e a d GetDescription;
OutLabel: TLabel r e a d FLabel w r i t e SetLabel;
Este metodo U p d a t e L a b e l es ejecutado cada vez que una de las otras propiedades cambia (como puede verse en tiempo de diseiio en la figura 9 . 5 ) , como
podemos ver aqui:
p r o c e d u r e TMdPersona1Data.SetFirstName
begin
i f FFirstName <> Value t h e n
begin
FFirstName : = Value;
UpdateLabel;
end ;
end;
(const Value:
string);
tYPe
IMdViewer
interface
['{97668600-8E4A-4254-9843-59B98FEE6C54}']
TMdIntfTest
var
icomp: I I n t e r f a c e C o r n p o n e n t R e f e r e n c e ;
begin
if FViewer <> Value t h e n
begin
FViewer : = Value;
FViewer.View(FText);
i f Supports (FViewer, IInterfaceComponentReference,
iComp) t h e n
iComp.GetComponent.FreeNotification(Se1f);
end;
end;
str: String);
I .nc m a r m c h
a r ~ rn l ~rll ~ c a r r n l l n
d~ mmnn-
TObject);
'=me
TMdArrowDir = (adup, adRight, adDown, adleft);
'=me
TMdArrow = c l a s s (TGraphicControl)
private
FDirection: TMdArrowDir;
FArrowHeight: Integer;
FFilled: Boolean;
procedure SetDirection (Value: TMd4ArrowDir);
procedure SetArrowHeight (Value: Integer) ;
procedure SetFilled (Value: Boolean) ;
published
property
property
property
read
property
read
property
d e f a u l t False;
W i d t h d e f a u l t 50;
Height d e f a u l t 20;
Direction: TMd4ArrowDir
FDirection w r i t e SetDirection d e f a u l t adRight;
ArrowHeight: Integer
FArrowHeight w r i t e SetArrowHeight d e f a u l t 10;
Filled: Boolean r e a d FFilled w r i t e SetFilled
(Value: TMdArrowDir);
flecha. De no ser asi, se pasa el codigo por alto y el metodo finaliza de forma
inmediata. Esta estructura de codigo resulta muy comun y la usaremos en el caso
de la mayoria de 10s procedimientos Set de propiedades.
Debemos recordar definir 10s valores predefinidos de las propiedades en el
constructor de componentes:
c o n s t r u c t o r T M d A r r o w - C r e a t e (AOwner: TComponent);
begin
/ / llama a 1 c o n s t r u c t o r padre
i n h e r i t e d Create
( A O w n e r );
Como hemos dicho anteriormente, el valor predefinido especificado en la declaracion de propiedad se usa solo para decidir si se guarda el valor de la propiedad en el disco.
El constructor create se define en la parte publica de la definicion de tip0
del nuevo componente y se indica mediante la palabra clave override,ya que
sustituye el constructor virtual Create de TComponent.Es fundamental recordar esta palabra clave, puesto que de no ser asi, cuando Delphi crea un componente nuevo de esta clase, llamara a1 constructor de la clase basica; en lugar de a1
que hemos escrito para la clase derivada.
o f TPoint;
// c a l c u l a 10s p u n t o s d e l a p u n t a d e f l e c h a
YCenter : = (Height - 1) div 2;
XCenter : = (Width - 1) div 2;
case FDirection of
adup: begin
FArrowPoints [0] := Point (0, FArrowHeight) ;
FArrowPoints [I] : = Point (XCenter, 0) ;
FArrowPoints [2] := Point (Width-1, FArrowHeight) ;
end;
// y a s i sucesivamente para o t r a s d i r e c c i o n e s
El codigo calcula el centro de la zona del componente (dividiendo sencillamente las propiedades H e i g h t y W i d t h entre dos) y, a continuation, lo usa para
establecer la posicion de la punta de flecha. Ademas de cambiar la direccion u
otras propiedades, es necesario refrescar la posicion de la punta de flecha cuando
cambia el tamaiio del componente. Lo que podemos hacer es sobrescribir el metodo S e t B o u n d s del componente, a1 que llama la VCL cada vez que cambian las
propiedades L e f t , Top, W i d t h y H e i g h t del componente:
procedure TMdArrow.SetBounds(ALeft, ATop, AWidth, AHeight:
Integer) ;
begin
inherited SetBounds
ComputePoints;
end;
// c a l c u l a e l c e n t r o
YCenter : = (Height - 1) div 2;
XCenter : = (Width - 1) div 2;
/ / d i b u j a l a lined d e l a f l e c h a
case FDirection of
adup: begin
Canvas .MoveTo (XCenter, Height-1) ;
Canvas-LineTo (XCenter, FArrowHeight);
end;
/ / y a s i s u c e s i v a m e n t e para l a s demds d i r e c c i o n e s
end ;
(FArrowPoints);
(TGraphicControl)
...
(AOwner: TCornponent);
...
/ / crea e l l d p i z y e l pincel
FPen : = TPen.Create;
/ / d e f i n e u n c o n t r o l a d o r p a r a e l e v e n t o OnChange
FPen.OnChange : = RepaintRequest;
end;
(Sender: TObject);
(Value: TPen) ;
Muchas clases TPe r s i s t e n t tienen un metodo A s s i g n que deberia utilizarse cuando hay que actualizar 10s datos de dichos objetos. Ahora, para utilizar
realmente el lapiz para dibujar, tenemos que modificar el metodo P a i n t , configurando las propiedades del componente c a n v a s como el valor de 10s objetos
internos antes de dibujar alguna linea (podemos ver un ejemplo del nuevo resultado del componente en la figura 9.7):
procedure TMdArrow.Paint;
begin
// usa e l l d p i z a c t u a l
Canvas.Pen : = FPen;
Figura 9.7. El resultado del cornponente Arrow con un lapiz grueso y una trarna
especial.
En este caso, hemos usado el tipo TNo t i fy E v e n t , que solo tiene un parametro
S e n d e r y Delphi lo utiliza con muchos eventos, comoOnClick y O n D b l C l i c k .
Usando este campo hemos definido una propiedad publicada muy simple, con
acceso direct0 al campo:
TRUCO: El uso de Self como ~a&nit& dkla inmcacibn dcl mttodo del
controhdor de eventos gmm&a$uq;4&
b a d 0 cl rnttodo, su parihctro
S e n d e r ae referira a1 objeto que 8cti~lSfdevento, @neralmcntc, un usuario d d agqmnentes.
,.
NOTA: Ona z- en eoestc contextoes una paflc &la pantalla mdeada por '
algGn tip0 de forma. Por ejdplo. pademos clear m a zona poligonal que
utilice 10s tres vtrtices del trislt$lo & la&qMil i(c flgcha. El rinico probbma es que para rellenar la superficie correctamegte, ,debemos definir una
matriz de T P o i n t s en Ia dirtccih deli. w,asde ~ l q[ iv k e la descijp ;
cion d e C r e a t e P o l y g o n a l R g n enli+ayud.azJcla q ~ ~ & x ~ i n & w s ~ a r 3
conocer 10s detalles de esta tknica). E h o es b gne hiein-bs en el m d t d o
'
ComputePoints.
Una vez definida la zona, podemos probar si el punto en cl que se hizo doble
clic csta dentro de la zona, utilizando la llamada Pt InRegion de la API. Podemos utilizar el codigo fuente completo de este procedimiento del siguiente listado:
procedure TMdArrow.WMLButtonDb1Clk (
var M s g : TWMLButtonDblClk) ; // mensa je wm-LBut tonDblClk;
var
HRegion: HRgn;
begin
// r e a l i z a e l c o n t r o l p r e d e f i n i d o
inherited;
// c a l c u l a l a zona d e l a p u n t a d e f l e c h a
HRegion : = CreatePolygonRgn (fArrowPoints, 3, WINDING);
try / / v e r i f i c a s i e l c l i c s e r e a l i z o e n l a z o n a
i f PtInRegion (HRegion, Msg.XPos, Msg.YPos) then
ArrowDblClick;
finally
DeleteOb ject (HRegion);
end ;
end :
c
dades.
o de S
ejemplo posterior tendremos que manejar mensajes Windows personalizados porque no hay un metodo correspondiente que sobrescribir.
(Value: Integer) ;
El metodo mas importante es el metodo redefinido Keypress, que filtra todos 10s caracteres no numericos y crea un evento especifico en caso de error:
p r o c e d u r e TMdNumEdit .WmChar ( v a r Msg : TWmChar) ;
begin
i f n o t (Key i n [ ' O r . . ' 9 ' ] ) a n d n o t (Key = # 6 ) t h e n
begin
K e y : = #O; / / s i m u l a r q u e n o s e h a p u l s a d o n a d a
begin
i f Assigned ( F I n p u t E r r o r ) t h e n
FInputError ( S e l f );
end
else
inherited;
end;
Este metodo verifica cada caracter cuando el usuario lo introduce, comprobando 10s numeros y la tecla Retroceso (que tiene un valor ASCII 8). El usuario
dcberia poder utilizar la tecla Retroceso ademas de las teclas de sistema (las
tcclas del cursor y Supr), por lo que es necesario verificar dicho valor.
Ahora, si colocamos este componente en un formulario, podemos escribir algo
en el cuadro de edicion y observar su comportamiento. Tambien podemos asociar
un metodo a1 evento OnInputError para ofrecer respuesta a1 usuario cuando
se pulsa una tecla incorrecta.
(+
o -)
El boton Sound
Nuestro proximo componente, TMdSoundBut ton,emite un sonido cuando
pulsamos el boton y otro cuando lo soltamos. El usuario especifica cada sonido
modificando dos propiedades String que denominen 10s archivos WAV corres-
pondientes a 10s respectivos sonidos. Una vez mas, es necesario interceptar algunos de 10s mensajes del sistema (wm LButtonDown y wm -LButtonUp), o
sobrescribir el controlador de segundonivel apropiado.
Veamos el codigo de la clase TMdSoundBut t o n , con 10s dos metodos protegidos y las dos propiedades de cadena que identifican 10s archivos de sonido,
proyectados sobre campos privados porque no necesitamos hacer nada especial
cuando el usuario cambia esas propiedades:
type
TMdSoundButton = class (TButton)
private
FSoundUp, FSoundDown: string;
protected
procedure MouseDown(Button: TMouseButton;
Shift: TShiftState; X, Y: Integer) ; override;
procedure MouseUp (Button: TMouseButton;
Shift: TShiftState; X, Y: Integer) ; override;
published
property SoundUp: string read FSoundUp write FSoundUp;
property SoundDown : string read FSoundDown write FSoundDown;
end:
El codigo que escribimos para estos dos metodos puede hacer lo que nosotros
queramos. Por ejemplo, hemos decidido que alternara el estilo negrita de la fuente
del propio boton. Podemos ver el efecto que se obtiene al mover el raton sobre uno
de esos componentes en la figura 9.9.
procedure TMdActiveButton.MouseEnter (var Msg: TMessage);
begin
Font.Style : = Font.Style + [fsBold];
end;
procedure TMdActiveButton.MouseLeave (var Msg: TMessage);
begin
Font .Style : = Font .Style - [fsBold];
end ;
Podemos aiiadir otros efectos, como agrandar el tip0 de letra, hacer el boton el
seleccionado por defect0 o cambiar el tamaiio del boton. Los meJores efectos
normalmente implican colores, pero debemos heredar de la clase T B i t B t n para
poder manipularlos (10s controles T B u t t on tienen un color predefinido).
Mensajes de componentes
Un componente de Delphi pasa mensajes de un componente a otros componentes, para indicar cualquier cambio en su estado que podria afectar a dichos componentes. La mayoria de estos mensajes comienzan como mensajes Windows,
c m-~
Los mensajes enviados a 10s componentes hijo cuando cambia una propiedad:
c m-BiDiModeChanged: c m-I c o n c h a n g e d
c m-B o r d e r C h a n g e d : c m-S h o w H i n t C h a n g e d
c m-C o l o r C h a n g e d : c m-S h o w i n g c h a n g e d
c m-C t l 3 D C h a n g e d : c m-S y s F o n t C h a n g e d
c m-C u r s o r C h a n g e d : c m-T a b s t o p c h a n g e d
c m-E n a b l e d C h a n g e d : cm-T e x t c h a n g e d
c m-F o n t C h a n g e d : c m-V i s i b l e C h a n g e d
Las notificaciones sobre 10s cambios en el sistema Windows: cmSysColorChange, cm-WinIniChange, cm-Timechange y
cm-Fontchange.Controlar estos mensajes resulta util en componentes
especiales que necesitan mantener un seguimiento de 10s colores o fuentes
del sistema.
Los mensajes del raton: cm-Drag se envia varias veces durante las operaciones de arrastre. cm MouseEnter y cm MouseLeave se envian a1
control cuando el cursor entra o sale de su<uperficie, per0 10s envia el
objeto Ap p 1 i c a t i o n como mensajes de poca prioridad. c m
Mouse Whee 1 corresponde a las operaciones basadas en la rueda del ra:
ton.
Mensajes de la aplicacion:
cm AppKeyDown: Se envia a1 objeto Application para dejarlo que
decida si una tecla corresponde a un menu de metodo abreviado.
cm-AppSysComrnand:Corresponde a1 mensaje wm-SysCommand.
cm DialogHandle:Se envia en una DLL para recuperar el valor
de la propiedad DialogHandle (utilizada por algunos cuadros de didogo no creados en Delphi).
cm Invo keHelp: Lo envia el codigo en una DLL para llamar a1
m&do InvokeHelp .
cm WindowHook: S e e n v i a e n u n a D L L p a r a l l a m a r a l o s m e t o d o s
~ o i k ~ a i n ~ i n dy oUnhookMainWindow
w
.
Es poco probable que necesitemos usar estos mensajes. Existe tambien un
mensaje cm-Hintshowpause, que nunca se maneja en VCL.
Mensajes internos de Delphi:
cm CancelMode:Termina operaciones especiales, como mostrar la
list; desplegable de un cuadro combinado.
cm Controlchange: Se envia a cada control antes de aiiadir o
elihinar un control hijo (controlado por controles comunes).
cm ControlLis tChange:Se envia a cada control antes de aiiadir
o eiminar un control hijo (controlado por el componente DBCtrlGrid).
cm DesignH itTest : Determina si una operacion de raton deberia
ir alcomponente o a1 diseiiador de formularios.
cm Hintshow:Se envia a un controljusto antes de mostrar su sugerencia (solo si la propiedad ShowHint est6 definida como rue).
cm Hit T est : Se envia a un control cuando un control padre intenta
loc&zar a un control hijo en una position de rat6n dada (si la hay).
cm-I s S h o r t C u t : No se usa actualmente (ya que la mayoria de codigo simplemente llama a Is S h o r t c u t ) , per0 esta pensado para ser
usado para identificar si un formulario da soporte a un acceso direct0 a
traves del evento OnS h o t c u t , un elemento del menu o una accion.
cm Want S p e c i a 1Key: Controlado por controles que interpretan
Mensajes de implementation de metodos, como cm R e c r e a t eW nd, lla: mado dentro del metodo R e c r e a t e W n d d e - ~ ~ o n t r o lcm
I n v a l i d a t e , llamado dentro de T C o n t r o l . I n v a 1 i d a t e ; cmC h a n g e d , llamado dentro de T C o n t r o l . C h a n g e d y cm
A l l C h i l d r e n F l i p p e d , llamado en 10s mktodos ~ o ~ l i ~ ~ h i l d r
de T w i n c o n t r o l y T S c r o l l i n g W i n C o n t r o l . En el grupo similar
estan dos mensajes relacionados con listas de acciones cm-A c t i o n U p d a t e y cm-A c t i o n E x e c u t e .
Notificaciones a componentes
Los mensajes de notificacion a componentes son 10s que envia un formulario o
componente padre a sus hijos. Estas notificaciones corresponden a 10s mensajes
enviados por Windows a la ventana del control padre, per0 logicamente destinados a1 control. Por ejemplo, la interaccion con controles como botones, cuadros
de edicion o cuadros de lista hace que Windows envie un mensaje wm Command
a1 padre del control. Cuando un programa de Delphi recibe estos mensajes, 10s
reenvia a1 mensaje del propio control, como una notificacion. El control de Delphi
puede controlar el mensaje y en ultimo termino producir un evento. Ocurren operaciones similares para muchos otros mensajes. La conexion entre 10s mensajes
Windows y 10s de notificacion a componentes es tan estrecha que, por lo general,
reconoceremos el nombre de 10s mensajes Windows por el nombre del mensaje de
notificacion, sencillamente sustituyendo la cn inicial por wm.Estos son algunos
de 10s grupos distintivos de mensajes de notificacion a componentes:
Mensajes generales del teclado: cn-Char,cn-Ke yUp, cn-Ke yDown,
cn-SysChary cn-SysKeyDown.
Mensajes especiales del teclado utilizados solo por 10s cuadros de lista con
el estilo lbs-WantKeyboardInput: cn-CharToItem y cnVKe yToItem.
Mensajes relacionados con la tecnica de dibujo personalizado: cnC o m p a r e I t e m , c n - D e l e t e I t e m , c n- D r a w I t e m y c n MeasureItem.
Mensajes para desplazamiento, utilizados solo por controles de la barra de
desplazamiento y de la barra de seguimiento: c n - H S c r o 1 1 y
cn-VScroll.
Mensajes de notificacion generales, utilizados por la mayoria de 10s controles: cn-Command, cn-Notify y cn-ParentNotify.
Mensajes de color de controles: cn CtlColorBtn,cn-CtlColorDlg,
cn-CtlColorcn-CtlColorEdit, cn-CtlColor~istbox,
Msgbox, cn-CtlColorScrollbar y cn-CtlColorStatic.
Hay mas notificaciones de controles definidas para soporte de controles comunes (en la unidad ComCtrls).
Selected: Es un entero que accede directamente a1 campo privado correspondiente. Almacena el elemento seleccionado de la lista de cadenas.
Title: Es una cadena utilizada para cambiar el titulo del cuadro de dialogo.
La propiedad publica es sel Itern, una propiedad de solo lectura que recupera automaticamente el elemento seleccionado de la lista de cadenas. Fijese en que
esta propiedad no almacena ni tiene datos: sencillamente accede a otras propiedades, ofreciendo una representacion virtual de 10s datos:
type
TMdListBoxDialog = class (TComponent)
private
FLines: TStrings;
FSelected: Integer;
FTitle: string;
function GetSelItem: string;
procedure SetLines (Value: TStrings) ;
function GetLines: TStrings;
public
constructor Create(A0wner: TComponent); override;
destructor Destroy; override;
function Execute: Boolean;
property SelItem: string read GetSelItem;
published
property Lines: TStrings read GetLines write SetLines;
property Selected: Integer read FSelected write FSelected;
property Title: string read FTitle w r i t e FTitle;
end;
string;
Por supuesto, dado que estamos escribiendo manualmente el codigo del componente y aiiadiendole codigo fuente a1 formulario original, tenemos que recordar
escribir el procedimiento R e g i s t e r .
Una vez escrito el procedimiento R e g i s t e r y preparado el componente, debemos crear un mapa de bits. En 10s componentes no visuales, 10s mapas de bits
son fundamentales ya que no solo se usan en la Component Palette, tambien se
utilizan a1 colocar el componente en un formulario.
Eso es todo lo necesario para ejecutar el cuadro de dialogo que hemos colocado en el componente, como se puede ver en la figura 9.10. Como hemos visto, esta
es una interesante tecnica para el desarrollo de algunos cuadros de dialogo comunes.
Propiedades de coleccion
De vez en cuando, necesitamos propiedades que contengan una lista de valores
en lugar de uno solo. A veces, podemos usar una propiedad basada en
T S t r i n g L i s t , per0 solo es valido con informacion textual (aunque puede asociarse un objeto a cada string). Cuando necesitemos una propiedad que almacene
una matriz de objetos, la solucion mas adecuada a VCL es usar una coleccion. El
papel de las colecciones es, por diseiio, el de crear propiedades que contengan una
one
three
public
constructor Create (CollOwner: TCornponent) ;
function GetOwner: TPersistent; override;
procedure Update(1tem: TCollectionItem); override;
end;
constructor TMdMyCollection.Create
begin
inherited Create (TMdMyItem);
FComp : = CollOwner;
end:
(CollOwner: TComponent);
function TMdMyCollection.Get0wner:
begin
Result : = FComp;
end;
TPersistent;
Value: TMyCollection);
begin
Result
:=
string;
FColl.FCollString;
end;
Los elementos de la coleccion se almacenan en ficheros DFM junto a1 componente que 10s contiene, usando las marcas especiales i tern y 10s simbolos mayor
y menor que, como en este ejemplo:
o b j e c t MdCollectionl:
TMdCollection
MoreData = <
item
Text = 'uno'
Code = 1
end
item
Text = 'dos'
Code = 2
end
item
Text
Code
end
=
=
'tres'
3
>
end
Para mostrar esta tecnica en la practica, hemos implementado las tres acciones, cortar, copiar y pegar, en un cuadro de lista, de un mod0 similar a1 de la VCL
para un cuadro de edicion (a pesar de que hemos simplificado un poco el codigo).
Hemos escrito una clase basica, que hereda de la clase generica TListControlAction de la nueva unidad ExtActns. Esta clase basica,
TMdCustomListAct ion,aiiade cierto codigo comun, compartido por todas
las acciones especificas y publica una serie de propiedades de accion. Las tres
clases derivadas tienen su propio codigo ExecuteTarget.Veamos las cuatro
clases:
type
TMdCustomListAction = class (TListControlAction)
protected
function TargetList (Target: TObject): TCustomListBox;
function Getcontrol (Target: TObject): TCustomListControl;
public
procedure UpdateTarget (Target: TObject) ; override;
published
property Caption;
property Enabled;
property Helpcontext;
property Hint;
property ImageIndex;
property Listcontrol;
property Shortcut;
property SecondaryShortCuts;
property Visible;
property OnHint;
end;
TMdListCutAction = class (TMdCustomListAction)
public
procedure ExecuteTarget (Target: TObj ect) ; override;
end;
TMdListCopyAction = class (TMdCustomListAction)
public
procedure ExecuteTarget (Target: TObject) ; override;
end;
TMdListPasteAction = class (TMdCustomListAction)
public
procedure UpdateTarget (Target: TObject); override;
procedure ExecuteTarget (Target: TObject); override;
end;
begin
- Result : = ( (ListControl <> nil) or
(ListControl = nil) and (Target is TCustomListControl))
and
TCustomListControl (Target).Focused;
end:
El metodo Upda teTa rget, en cambio, tiene dos implementaciones diferentes. La predefinida la ofrece la clase basica y la usan las acciones copiar y cortar.
Estas acciones se activan solo si el cuadro de lista objetivo tiene a1 menos un
elemento y si hay un elemento seleccionado en ese momento.
El estado de la accion pegar depende, sin embargo, del estado del portapapeles :
procedure TMdCustomListAction.UpdateTarget
(Target: TObject);
begin
Enabled : = (TargetList (Target). Items .Count > 0)
and (TargetList (Target). ItemIndex >= 0);
end:
function TMdCustomListAction.TargetList (Target: TObject) :
TCustomListBox;
begin
Result : = GetControl (Target) as TCustomListBox;
end;
function TMdCustomListAction.GetControl(Target: TObject) :
TCustomListControl;
begin
Result : = Target as TCustomListControl;
end;
procedure TMdListPasteAction.UpdateTarget (Target: TObject);
begin
Enabled : = C1ipboard.HasFormat (CF-TEXT);
end;
TObject);
begin
w i t h TargetList (Target) d o
begin
C l i p b o a r d - A s T e x t : = Items [ItemIndex];
Items .Delete (ItemIndex);
end ;
end ;
p r o c e d u r e TMdListPasteAction.ExecuteTarget(Target: TObject);
begin
(TargetList ( T a r g e t )) Items .Add (Clipboard.AsText) ;
end;
Una vez escrito este codigo en una unidad y afiadido a un paquete (el paquete
MdPack, en este caso), el paso final consiste en registrar las nuevas acciones
personalizadas en una categoria dada. Esta se indica como el primer parametro
del procedimiento RegisterActions,mientras que el segundo es la lista de
clases de acciones que se van a registrar:
procedure Register;
begin
RegisterActions ( 'List ',
[TMdListCutAction, TMdListCopyAction,
TMdListPasteAction] , nil) ;
end;
v . . .
,.
1
I
I
:)
.I
La funcion Ge tAt t ributes combina paValueLis t (para la lista desplegable) y 10s atributos paDialog (para el cuadro de edicion personalizado) y
tambidn clasifica las listas y permite la selection de la propiedad de diversos
componentes:
function TSoundProperty-GetAttributes: TPropertyAttributes;
begin
// e d i t o r , l i s t a ordenada, s e l e c c i d n m u l t i p l e
Result : = [paDialog, paMultiSelect, pavaluelist, paSortList];
end;
TGetStrProc);
Una tecnica mejor seria estraer estos valores del Registro de Windows, donde
todos estos nombres estan almacenados. El metodo Edit es sencillo: crea y muestra
un cuadro de dialogo. Podriamos haber mostrado el cuadro de dialogo Abrir
directamente, per0 hemos decidido afiadir un paso intermedio para permitir a1
usuario probar el sonido. Esto es parecido a lo que Delphi hace con las propiedades graficas: primer0 abrimos la vista previa y cargamos el fichero, solo despues
de confirmar que es correcto. El paso mas importante es cargar el fichero y probarlo antes de aplicarlo a la propiedad. Este es el codigo del metodo Edit:
p r o c e d u r e TSoundProperty.Edit;
begin
S o u n d F o r m : = TSoundForm.Create ( A p p l i c a t i o n ) ;
t rY
SoundForm.ComboBoxl.Text : = GetValue;
/ / m o s t r a r e l c u a d r o d e didlogo
i f SoundForm.ShowModal = mrOK t h e n
SetValue (SoundForm.ComboBoxl .Text) ;
finally
SoundForm.Free;
end;
end;
1 IAN shown
Figura 9.12. La lista de sonidos ofrece una pista al usuario, que tambien puede
escribir el valor de la propiedad o hacer doble clic para activar el editor (mostrado
despues, en la figura 9.13).
parametro indicando en cual de 10s componentes seleccionados estamos trabajando, 0 indica el primer componente). ~ u a n d oaccedemos a1 componente directamente, debemos tambien llamar a1 metodo Modified del objeto Designer
(una propiedad del editor de propiedades de la clase basica). No necesitamos esta
llamada a Modified en el ejemplo, porque el metodo Setvalue de la clase
base hace esto automaticamente por nosotros.
El metodo Edit anterior muestra un cuadro de dialogo (un formulario Delphi
estandar que se crea visualmente y que se aiiade a1 paquete que contiene 10s
componentes de tiempo de diseiio). El formulario es bastante simple; un cuadro
combinado muestra 10s valores devueltos por el metodo GetValues, y 10s cuatro botones nos permiten abrir un fichero, probar un sonido y cerrar el cuadro de
dialogo aceptando 10s valores o cancelandolos. Podemos ver un ejemplo del cuadro de dialogo en la figura 9.13. Proveer una lista desplegable de valores y un
cuadro de dialogo para editar una propiedad, hace que el Object Inspector
muestre solo el boton con flecha, que indica una lista desplegable, y omite el
boton eliptico, que indica que hay disponible un editor de cuadros de dialogo. En
este caso, como ocurre en el editor de propiedades por defect0 Color, el cuadro
de dialogo se obtiene haciendo doble clic sobre el valor actual o pulsando Control-Intro.
~p
1
--
IlmIx ~ d l
-
--
Figura 9.13. El forrnulario del editor de propiedades de sonido muestra una lista de
sonidos disponibles y nos perrnite cargar un fichero y escuchar el sonido
seleccionado.
Los dos primeros botones del formulario tienen un metodo asignado a su evento OnClick:
procedure TSoundForm.btnLoadClick(Sender:
begin
i f 0penDialogl.Execute then
ConboBoxl.Text
:=
TObject);
0penDialogl.FileName;
end ;
procedure
begin
TSoundForm.btnPlayClick(Sender:
TObject);
el sonido del sistema por defecto, que intenta reproducir cuando no encuentra el
sonido solicitado. Si el sonido solicitado no esta disponible, reproduce el sonido
del sistema por defecto y no devuelve el codigo de error. P l a y s o u n d busca
primero el sonido en el Registro y, si no lo encuentra ahi, comprueba si el fichero
de sonido especificado existe.
- -
...
'1
requires
vcl,
Mdpack ,
designide;
contains
PeSound i n ' PeSound. pas ',
PeFSound i n ' PePSound. pas '
(SoundPorm];
El efecto de este codigo es aiiadir 10s elementos del menu a1 menu local del
formulario, como se ve en la figura 9.14. A1 seleccionar cualquiera de dichos
elementos de menu, simplemente se activa el metodo Executeverb del editor
de componentes:
p r o c e d u r e TMdListCompEditor.ExecuteVerb (Index: Integer);
begin
c a s e Index o f
0: ; // nada que hacer
1: MessageDlg ('Este es un sencillo editor de
componentes '#I3 + 'creado por Marco Cantu1#13 + 'para el libro
"La biblia del Delphi " ' , mtInf ormation, [mbOK] , 0) ;
2: w i t h Component a s TMdListDialog d o
Execute;
end;
end;
Hemos decidido manejar 10s dos primeros elementos en una unica rama de la
sentencia case, a pesar de que podiamos habernos saltado el mensaje de copyright. El otro comando llama al metodo Execute del componente que estamos
editando, determinado usando la propiedad Component de la clase TComponent E d i t or. Conociendo el tipo del componente, podemos facilmente acceder
a sus metodos despues de una asignacion de tipos dinamica. El ultimo metodo se
refiere a la accion predefinida del componente y se activa haciendo doble clic
sobre el en el Form Designer:
procedure TMdListCompEditor.Edit;
begin
// p r o d u c e un s o n i d o y rnuestra e l c u a d r o A c e r c a d e
Beep;
Executeverb (0);
end:
Figura 9.14. Los elementos del menu personalizado atiadidos por el editor de
propiedades del componente ListDialog.
(TMdListDialog, TMdListCompEditor);
Hemos aiiadido esta unidad a1 paquete M d D e s P k, que incluye todas las extensiones en tiempo de diseiio del capitulo. Despues de instalarla y activar este paquete se puede crear un nuevo proyecto, colocar una componente de lista con
solapa en el y experimentar.
paquetes
El enlace dinamico
En primer lugar, es necesario conocer la diferencia entre el enlace dinamico y
el enlace estatico a funciones o procedimientos. Cuando una subrutina no esta
disponible directamente en un archivo fuente, el compilador aiiade la subrutina a
una tabla de simbolos interna. El compilador, debe haber visto, por supuesto, la
declaracion de la subrutina y conocer sus parametros y tip0 o producira un error.
Tras la compilacion de una subrutina normal (estatica), el editor de enlaces
obtiene el codigo compilado de la subrutina desde una unidad compilada de Delphi
(o biblioteca estatica) y lo aiiade a1 codigo del programa. El archivo ejecutable
resultante incluye todo el codigo del programa y de las unidades relacionadas. El
editor de enlaces de Delphi es lo suficientemente inteligente como para incluir la
cantidad minima de codigo de las unidades utilizadas por el programa y enlazar
so10 las funciones y mktodos que en realidad se utilizan.
,;&.,I,,
,
,
,
,
A
,,-,I,&&,
..,,l.:..,,
,.:,-.&,Ll,,
,
*
,
a
,
,A,
xrc:,,
L . X . -
.r
funciones virtuales.
En el caso del enlace dinamico, que tiene lugar cuando nuestro codigo llama a
una funcion basada en una DLL, el editor de enlaces sencillamente utiliza la
--
--
--
NOTA: El sistema operativo intentara cargar la DLE en la misma direccion de cada espacio de direcciones de la aplicacion (usando la direccion
bhica que se prefiera, especificada por la DLL). Si dicha direccion no e s d
cion, la imagen del codigo de la DLL de dicho proceso se tendra que volver
a colocar, una operacion costosa tanto en terminos de rendimiento como de
uso de memoria. La razon es que esa nueva asignacion se realiza en funci6n
de cada proceso y no de todo el sistema.
Otra interesante ventaja es que podemos ofrecer una version diferente de una
DLL. que sustituya a la actual. Si las subrutinas de la DLL tienen 10s mismos
parametros, podemos ejecutar el programa con la nueva version de la DLL sin
tener que volver a compilarlo. No importa en absoluto que la DLL tenga subrutinas
nucvas. Solo puede haber problemas si falta una rutina de la version antigua de la
DLL cn la nueva. Tambien puede haber dificultades si la nueva DLL no implementa
las funciones de una forma compatible con el funcionamiento de la antigua DLL.
Esta segunda ventaja es aplicable particularmente a aplicaciones complejas.
En caso de tener un programa muy grande que requiera actualizaciones y correcciones frecuentes, dividirlo en multiples archivos ejecutables y bibliotecas dinamicas nos permitira distribuir unicamente las partes modificadas en lugar de un
solo ejecutable de gran tamaiio. Hacer esto es especialmente importante con las
bibliotecas del sistema de Windows: generalmente, no es necesario recompilar
nuestro codigo si Microsoft publica una version actualizada de las bibliotecas del
sistema de Windows (en una nueva version del sistema operativo o en un paquete
de actualizacion).
Otra tecnica habitual es usar las bibliotecas dinamicas solo para almacenar
recursos. Podemos crear diferentes versiones de una DLL que contenga cadenas
de texto para diferentes idiomas y asi cambiar el idioma en tiempo de ejecucion, o
preparar una biblioteca de iconos e imagenes y utilizarlos en diferentes aplicaciones. El desarrollo de versiones de un programa adaptadas a varios idiomas es muy
importante y Delphi incluye soporte para esto mediante su entorno integrado de
traduccion, el Integrated 7'ranslntion Envrronment (ITE).
Otra ventaja clave es que las DLL son independientes del lenguaje de programacion. La mayoria de 10s entornos de programacion Windows, asi como la mayoria de 10s lenguajes de macro de aplicaciones para usuarios finales, permiten a1
programador llamar a una funcion almacenada en una DLL. Esta flexibilidad se
aplica solo para el uso de funciones. Para compartir ob-jetos en una DLL entre
lengua-jes de programacion, deberiamos cambiar a la infraestructura COM o a la
arquitectura .NET.
function
function
function
function
'gdi32.dl11;
-- -
--_-aparozca c~
-1 - I - L - ~ - n - 1 - L :
-quc
smuo~o
u e ~ p r uGUrrespondiente en el archivo de cabecera traducido en C*. Eso ayuda a
mantener en sincronia 10s identificadores Delphi y C++, de tal mod0 que
ambos lenguajes puedan compartir el c6digo.
c;a a
, Duuucr.
P ,
r)..:i~-LTT
-i-t-i-
-__:A-
JXCG S ~ D V I UWIISL
La definicion esterna de dichas funciones remite a la DLL que usan. El nombre de la DLL debera incluir la extension .DLL o el programa no funcionara bajo
Windows 2000 (aunque si funcione bajo 9x). El otro elemento es el nombre de la
propia funcion de la DLL. La directiva name no es necesaria si el nombre de la
funcion (o procedimiento) Delphi se corresponde con el nombre de la funcion
DLL (que distingue entre mayusculas y minusculas).
Para llamar a una funcion que reside en una DLL, podemos ofrecer su declaracion y definicion externa, como se muestra anteriormente, o podemos mezclar
ambas en una unica declaracion. Una vez que la funcion se ha definido correctamente, podemos llamarla en el codigo de la aplicacion Delphi, igual que con
cualquier otra funcion.
TRUCO:~elp;hiincluye la traduccih a1 lenguaje D e w de uaa $ran cantidad de las API de Windows, como podemas ver en bs ficheros de la
En el siguiente listado, podemos ver la declaracion de las funciones C++ utilizadas para crear el ejemplo de la biblioteca CppD11. El codigo fuente completo y
la version compilada de la DLL en C++ y el codigo fuente de la aplicacion en
Delphi que la usa estan en el directorio CppD11. Deberiamos poder compilar este
codigo con cualquier compilador C++. Veamos las declaraciones de las funciones
en C++:
extern "C"
declspec (dllexport)
int WINAPI ~ o u b l e (int n) ;
extern "C"
declspec(dl1export)
int WINAPI ~ r i ~ l(int
e
n) ;
--declspec (dllexport)
int WINAPI Add (int a, int b);
Las tres funciones realizan algunos calculos basicos sobre 10s parametros y
devuelven el resultado. Fijese en que todas las funciones se definen con el modificador W I NAP I,que define la convencion de llamada a parametros adecuada y
van precedidas de la declaracion
de c 1spec ( dl lexport ) , que hace que
las funciones estkn disponibles paraprogramas externos.
Dos de estas funciones C++ utilizan tambien la convencion de nombrado de C
(indicada por la sentencia extern " c " ) ,per0 la tercera, Add,no. Esto afecta
a1 mod0 en que llamamos a estas funciones desde Delphi. De hecho, 10s nombres
internos de las tres funciones corresponden a sus nombres en el archivo de codigo
fuente en C++, a excepcion de la funcion ~ d dDado
.
que no hemos utilizado la
clausula extern "c" para esta funcion, el compilador C++ ha utilizado la
tecnica name mangling o de manipulacion de nombres. Se trata de una tecnica
utilizada para incluir informacion sobre el numero y tip0 de parametros en el
nombre de la funcion, que necesita el lenguaje C++ para implementar la sobrecarga de funciones. El resultado a1 usar el compilador Borland C++ es un nombre de
funcion muy raro: @Add$qqsii.En realidad, este es el nombre que tenemos que
usar en nuestro ejemplo en Delphi para llamar a la funcion Add de la DLL (lo
cual explica por que tenemos que evitar normalmente la tecnica name mangling
de C++ en las funciones exportadas y por que las declaramos generalmente como
extern " c " ) .Lo que sigue son declaraciones de las tres funciones en el ejemplo Delphi CallCpp:
function Add (A, B: Integer) : Integer;
stdcall; external ' CPPDLL. DLL' name
function Double (N: Integer) : Integer;
stdcall ; external ' CPPDLL . DLL ' name
function Triple (N: Integer) : Integer;
stdcall; external ' C P P D L L . D L L ' ;
' @ ~ d d $ q q s i i' ;
Como podemos ver, se puede exponer u omitir el alias para la funcion externa.
Hemos ofrecido uno para la primera funcion (no habia otra alternativa, porque el
nombre de la funcion DLL exportada @Add$qqsii no es un identificador Delphi
valido) y para la segunda, aunque en el segundo caso no sea necesario. De hecho,
~-
.....-.
0-
..l
-~
-
---
.--1,.1.
Para usar esta DLL en C++, hemos creado un ejemplo en Delphi, denominado
CallCpp. Su sencillo formulario tiene botones para llamar a funciones de la DLL
y algunos componentes visuales para parametros de entrada y salida (vease la
figura 10.1). Fijese en que para ejecutar esta aplicacion, deberiamos tener la DLL
en el mismo directorio que el proyecto, en uno de 10s directorios de la ruta o en 10s
directorios Windows o System. Si movemos el archivo ejecutable a un nuevo
directorio e intentamos ejecutarlo, obtendremos un error en tiempo de ejecucion
indicando que no existe dicha DLL:
tan sencillo que podriamos hacer un uso excesivo de dicha funcion. En general,
convienc intentar crear componentes y paquctes en lugar de DLL. Como esplicarcmos mas adelante en este capitulo, 10s paquetes a menudo contienen componentes, pero tambien puede contener clases que no son de componentes. lo que nos
pcrmitira escribir codigo orientado a objetos y reutilizarlo de un mod0 efectivo.
Los paquetes, por supuesto. tambien pueden contener rutinas, constantes. variables, etc.
Figura 10.1. La salida del ejemplo CallCpp al hacer clic en cada uno de 10s botones.
Como ya hemos dicho, es util construir una DLL cuando hay una parte del
codigo del programa sometida a frecuentes cambios. En este caso, podemos sustituir frecuentemente la DLL y mantener el resto del programa igual. Asi, cuando
es necesario escribir un programa que ofrezca distintas funciones para distintos
grupos de usuarios, podemos distribuir versiones diferentes de una DLL a dichos
usuarios.
stdcall;
Result
end;
end;
:= N
f u n c t i o n Double
begin
Result := N
t rY
Result := N
except
Result := N
end;
end;
end;
exports
Triple,
1;
(N:
2;
2;
-1;
Integer) : Integer;
stdcall;
Double;
- -
// n o hay e s p a c i o s u f i c i e n t e
Result : = False;
except
Result
end ;
end;
:=
False;
Esta segunda version es mas compleja, per0 la primera solo se puede usar
desde Delphi. Ademas, en la primera version es necesario que incluyamos la
unidad ShareMem y que distribuyamos el archivo BorlndMM.DLL como ya se ha
explicado anteriormente.
Esta declaracion es similar a las usadas para llamar a la DLL en C++. Esta
vez, en cambio, no hay problemas con 10s nombres de las funciones. Despues de
haber vuelto a declararlas como e x t e r n a l , las funciones de la DLL se pueden
usar como si fueran funciones locales. Veamos un ejemplo, con llamadas a las
funciones relacionadas con cadenas (la figura 10.2 muestra un ejemplo del resultado) :
p r o c e d u r e TForml.BtnDoubleStringClick(Sender:
TObject);
begin
// l l a m a a l a f u n c i o n d e l a DLL d i r e c t a m e n t e
EditDouble.Text : = Doublestring (EditSource.Text, I ; ' ) ;
end ;
procedure
var
TForml.BtnDoublePCharClick(Sender: TObject);
Buffer: s t r i n g ;
begin
// h a c e e l b u f f e r l o s u f i c i e n t e r n e n t e a m p l i o
SetLength (Buffer, 1000) ;
/ / l l a m a a l a f u n c i o n d e l a DLL
if DoublePChar (PChar ( E d i t S o u r c e . T e x t ) , PChar
1000, I / ' ) t h e n
EditDouble.Text := B u f f e r ;
end;
(Buffer),
Figura 10.2. El resultado del ejemplo CallFrst, que llama a la DLL que hernos creado
en Delphi.
$LIBPREFIX: Se utiliza para aiiadir algo delante del nombre de la biblioteca. Imitando la tecnica Linux de aiiadir lib delante de 10s nombres de
biblioteca, esta directiva la usa Kylix para aiiadir bpl al principio de 10s
nombres de paquetes. Esto se debe al hecho de que Linux usa una unica
extension (.SO) para bibliotecas, mientras que en Windows podemos tener
extensiones diferentes, algo que Borland usa para 10s paquetes (.BPL).
$LIBSUFFIX: Se usa para aiiadir texto despues del nombre de la biblioteca y antes de la extension. Esto se puede emplear para especificar informacion sobre la version u otras variaciones del nombre de la biblioteca que
pucden ser utiles tambien en Windows.
$LIBVERSION: Se utiliza para aiiadir un numero de version tras la extension (algo muy comun en Linux, per0 que normalmente deberiamos
evitar en Windows).
Podemos fijar estas directivas en el entorno de desarrollo, en la pagina
A p p l i c a t i o n del cuadro de dialogo Project Options, como muestra la figu-
ra 10.3. Como ejemplo, consideremos las siguientes directivas, que crean una
biblioteca llamada MarcoNameTest60.dll:
library NameTest;
ISLIBPREFIX
{SLIBSUFFIX
'Mdrco I
60 ' 1
r pel&
[OK1
~ancel
~ c b
Figura 10.3. La pggina Application del cuadro de dialogo Project Options tiene ahora
una seccion llamada Library Name.
NOTA: Lon paquctes de Delphi 6 introdujeron el uso extensive de la directiva S L I B S U F F I X . Por esa r a d n , el paquete VCL genera 10s archivos
carnbiar las partes necesarias de nuestros paquetes para cada nueva versibn
de- Debhi. Por sunuesto. esto se transforma~~.
en alno
Dara
.
.-" muv
-., c6modo
.
actualizar proyectos de Delphi 6 a Delphi 7, porque las versiones anteriores
de Delphi no ofrecian esta caracteristica. Cumdo abrimos paquetes de Delphi
3 aun oeoemos actualizar su coalgo ruente, una operaclon que el ~ u ace
Delphi no realiza automhticamente por nosotros.
-
- --- - -
---
~~
codigo en cierto mod0 mas complejo. Sin embargo, la ventaja esta en que el
programa se ejecutara incluso sin la DLL. Ademas, si se aiiaden nuevas funciones
compatibles a la DLL, no tendremos que revisar el codigo fuente del programa ni
compilarlo de nuevo para acceder a dichas funciones nuevas. Esta es la parte
central del programa:
type
TIntFunction = function (I: Integer): Integer; stdcall;
cons t
DllName = ' F i r s t d l l . d l l '
tip0 de procedimiento, como en el listado anterior. Fijese en que el tip0 de procedimiento que definamos habra de ser compatible con la definicion del procedimiento en la DLL. Este es el talon de Aquiles de este metodo, no existe verification
de tipos de parametro.
La ventaja de esta tecnica es que, en teoria, podemos usarla para acceder a
cualquier funcion de cualquier DLL en cualquier momento. En la practica, resulta
util cuando tenemos diferentes DLL con funciones compatibles o una DLL con
diversas funciones compatibles, como en este caso. Lo que podemos hacer es
llamar a 10s metodos Double y Triple introduciendo sencillamente sus nombres en el cuadro de edicion. Ahora, si alguien nos proporciona una DLL con una
nueva funcion que reciba un entero como un parametro y devuelva un entero,
podemos llamarla simplemente introduciendo su nombre en el cuadro de edicion.
Ni siquiera necesitamos compilar la aplicacion de nuevo.
Con este codigo, el compilador y el editor de enlaces ignoran la existencia de la
DLL. Cuando se carga el programa, la DLL no se carga inmediatamente. Podriamos hacer que el programa fuese incluso mas flexible y permitir a1 usuario que
introduzca el nombre de la DLL que vamos a usar. En algunos casos, esta es una
gran ventaja. Un programa puede activar las DLL en tiempo de ejecucion, algo
que la tecnica directa no nos permite. Fijese en que esta tecnica para cargar
funciones DLL es comun en 10s lenguajes de macro y la usan muchos entornos de
programacion visual.
Solo un sistema basado en un compilador y en un editor de enlaces, como
Delphi, puede usar la tecnica directa, que por lo general es mas fiable y tambien
un poco mas rapida. La tecnica de carga indirecta del ejemplo DynaCall solo es
util en casos especiales, per0 puede resultar extremadamente potente. Por otro
lado, resulta muy valioso aprovechar la carga dinamica con paquetes que incluyan formularios, como veremos hacia el final de este capitulo.
var
FormScroll: TFormScroll;
begin
// v a l o r p r e d e f i n i d o
Result : = Col;
try
Formscroll : = TFormScroll.Create (Application);
try
// i n i c i a l i z a 1 0 s d a t o s
FormScroll.SelectedColor : = Col;
// m u e s t r a e l f o r m u l a r i o
i f FormScroll.ShowModal = mrOK then
Result : = FormScroll.SelectedColor;
finally
FormScroll.Free;
end ;
except
on E: Exception do
MessageDlg ( ' E r r o r e n l a b i b l i o t e c a : ' + E - M e s s a g e ,
mtError, [mbOK], 0) ;
end ;
end ;
Lo que hace que este codigo sea diferente del codigo que escribimos normalmente en un programa es el uso del control de excepciones:
Un bloque t r y / e x c e p t protege a toda la funcion, de mod0 que se atrapara cualquier excepcion creada por la funcion, mostrando un mensaje
adecuado. La razon para controlar toda excepcion posible es que la aplicacion que realiza la llamada podria estar escrita en cualquier lenguaje, sobre todo en uno que no sepa como controlar excepciones. Aun cuando el
que llama es un programa en Delphi, a veces, resulta util la misma tecnica
de proteccion.
Un bloque t r y / fi n a l l y protege las operaciones realizadas en el formulario, asegurando que el formulario se destruira correctamente, aunque
se produzca una excepcion.
A1 comprobar el valor de retorno del metodo ShowModal, el programa establece el resultado de la funcion. Hemos definido el valor predefinido antes de
introducir el bloque t r y para garantizar que siempre se va a ejecutar (y evitar
tambien la advertencia del compilador que indica que el resultado de la funcion
podria no estar definida).
Podemos encontrar este codigo en 10s proyectos FormDLL y UseCol de la
carpeta FormDLL. (Tambien encontraremos el archivo WORDCALL. TXT que
muestra como llamar a la rutina desde una macro de Word). El ejemplo muestra
tambien que podemos aiiadir un formulario no modal a la DLL, per0 esto causa
muchos problemas. El formulario no modal y el principal no se sincronizan ya
que la DLL tiene su propio objeto A p p l i c a t i o n global en su propia copia de
la VCL. Este problema puede ser parcialmente solventado copiando el H a n d l e
del o b j e t o A p p 1 i c a t i o n del programa a1 H a n d l e del o b j e t o A p p 1 i c a t i o n
bhmndl
MSCTF dl
TabHook dR
UxTherne dl1
MOUSEDLL dl1
-W m W
$74680000
$10000WO
$58150000
$00780000
P h
C \WlNDlOWS\syslern32\G
C \WlNDOWS\syslern32\A
C \WINDOWS\syslern32W
C \WINDOWS\syslem32\oI
C\WlNDOWS\syslern32\M
C \WINDOWS\syslern3nO
C \WINDOWS\sydern32\v
C.\WINDOWS\syslem32\c
-
- D:\md7de\lO\DMm\~
C \WINDOWS\syslern32\M
C \WINDOWS\Syslern32\I
C \WINDOWS\Syslern32\u
C W~chwosde progma\Br
Es importante saber que la direccion base de una DLL es algo que podemos
solicitar activando la opcion de direccion base. En Delphi esta direccion se determina mediante el valor Image Base de la pagina del editor de enlaces del cuadro
de dialog0 Project Options. En la biblioteca DllMem, por ejemplo, esta puesta
en $ 0 0 8 0 0 0 0 0 . Necesitamos tener un valor diferente para cada una de nuestras
bibliotecas, asegurandonos de que no entra en conflict0 con ninguna biblioteca
del sistema u otra biblioteca (paquete, control ActiveX y otros) usada por el
ejecutable. Esto tambien puede verificarse mediante la ventana Module del depurador .
A pesar de que esto no garantiza una ubicacion unica, fijar una direccion base
para una biblioteca es siempre mejor que no hacerlo; en este caso siempre ocurre
una reubicacion, pero la probabilidad de que dos ejecutables diferentes reubiquen
la misma biblioteca en la misma direccion no es muy alta.
h tt p : / f w w w
sys$nternals. corn para e x a d n a r walquier proceso eri cualquier
'
*ha.
DLL, pero podemos tambien utilizarla para compartir directamente 10s datos entre las aplicaciones.
Este ejemplo se llama DllMem e incluye el proyecto DllMem (la propia DLL)
y el proyecto UseMem (la aplicacion demo). El codigo DLL tiene un archivo de
proyecto que exporta cuatro subrutinas:
l i b r a r y dllmem;
uses
SysUtils,
DllMemU i n
exports
S e t D a t a , GetData,
GetShareData, S e t S h a r e D a t a ;
end.
( I : Integer);
p r o c e d u r e SetShareData
begin
ShareData" : = I ;
end;
stdcall;
( I: Integer) ; s t d c a l l ;
MapViewOfFile: Requiere como parametros el manejador del archivo proyectado en memoria, algunos atributos y desplazamientos y el tamaiio de
10s datos (de nuevo).
Aqui vemos el codigo de la parte de inicializacion, ejecutado cada vez que se
cargan las DLL en un nuevo espacio de procesado (es decir, una vez para cada
aplicacion que usa la DLL):
var
hMapFile: THandle;
cons t
VirtualFileName = ' S h a r e D 1 l D a t a l ;
DataSize = sizeof (Integer);
initialization
// crea un archivo proyectado e n memoria
hMapFile : = CreateFileMapping (SFFFFFFFF, nil,
Page-Readwrite, 0, DataSize, VirtualFileName);
if hMapFile = 0 then
raise E x c e p t i o n - C r e a t e ('Error creando archivo proyectado
e n memoria ' ) ;
( hMapFile,
~ile-Map-Write, 0,
0,
(UpDownl.Position);
Si hacemos clic sobre el segundo boton, el programa copia 10s datos de la DLL
en el segundo cuadro de edicion:
Edit2 .Text
:=
IntToStr (GetData);
El tercer boton se usa para mostrar las direcciones de memoria de una funcion,
con el codigo fuente que aparece a1 principio de este apartado. Los ultimos dos
botones tienen basicamente el mismo codigo que 10s dos primeros, per0 llaman a1
procedimiento Set ShareData y a la funcion Get ShareData. Si ejecutamos
dos copias de este programa, podemos ver que cada copia tiene su propio valor
para 10s datos globales normales de la DLL, mientras 10s valores de 10s datos
compartidos son comunes. Hay que definir valores diferentes en 10s dos programas y despues obtener ambos para ver el efecto. Esta situacion se muestra en la
figura 10.4.
ADVERTENCIA: Los archivos proyectados en memoria reservan un rango minim0 de 64 KB de direcciones virtuales y consumen memoria fisica en
paginas de 4 KB.El uso en el ejemplo de datos Integer de cuatro bytes en
memoria compartida resulta bastante costoso, sobre toda si usamos la misma t6cnica
compartir diversos valores. Si necesitamos compartir diversas variables, deberiamos colocarlas todas en una unica zona de memoria
n
d ;nduna \y
I.. annnnrtnr
n Ina A;ot;m+nr .rnm4nLlno mmonrrdn . . r . . m t n r n o
r\ n m n . r d n
b un lm
r .rry. x
a&
bububr a
laa u r a ~ r u ~ av aa 1 r a v 1 ~ aUJLLUUU ~ U U L G L U D v
W~LLUUU
Figura 10.4. Si ejecutarnos dos copias del prograrna UseMern, verernos que 10s
datos globales de su DLL no son cornpartidos.
cacion mas el tamaiio del paquete DLL requerido es siempre mucho mayor
que el tamaiio del programa enlazado estaticamente. El editor de enlaces
incluye solo el codigo realmente utilizado por el programa, mientras que
un paquete habra de enlazar todas las funciones y clases declaradas en las
partes de interfaz de todas las unidades que contiene el paquete.
Si distribuimos varias aplicaciones Delphi basadas en 10s mismos paquetes, podriamos acabar distribuyendo menos codigo, porque 10s paquetes en
tiempo de ejecucion son compartidos. En otras palabras, una vez que 10s
usuarios de la aplicacion tengan 10s paquetes de tiempo de ejecucion estandar
de Delphi, podremos proporcionarles programas muy pequeiios.
Si ejecutamos varias aplicaciones Delphi basadas en 10s mismos paquetes,
podemos ahorrar algun espacio en memoria en tiempo de ejecucion. El
codigo de 10s paquetes en tiempo de ejecucion se carga en memoria solo
una vez entre las diversas aplicaciones.
No conviene preocuparse demasiado por distribuir un archivo ejecutable
amplio. Tengamos en cuenta que cuando realizamos pequeiios cambios en
un programa, podemos utilizar las diversas herramientas para crear un
archivo parche, de mod0 que distribuimos solo un archivo que contenga las
diferencias, no una copia completa de 10s archivos.
Si colocamos algunos formularios de nuestro programa en un paquete en
tiempo de ejecucion, podemos compartirlos entre varios programas. Sin
embargo, cuando modificamos estos formularios, normalmente sera necesario volver a compilar tambien el programa principal y distribuir de nuevo ambos programas a 10s usuarios.
Un paquete es una coleccion de unidades compiladas (incluyendo clases,
tipos, variables, rutinas), que no difieren en absoluto de las unidades de
dentro del programa. La unica diferencia esta en el proceso de construccion. El codigo de las unidades del paquete y el de las unidades del programa principal que las usa es identico. Esta es una de las ventajas de 10s
paquetes respecto a las DLL.
Versiones de paquetes
Un elemento muy importante y normalmente incomprendido es la distribucion
de paquetes actualizados. Cuando actualizamos una DLL, podemos incluir la
nueva version y 10s programas ejecutables que necesiten dicha DLL todavia funcionaran (a menos que hayamos eliminado las funciones exportadas previamente
existentes o cambiado algunos de sus parametros).
Sin embargo, cuando distribuimos un paquete Delphi, si actualizamos el paquete y modificamos la parte de interfaz de cualquier unidad del paquete, podria
ser necesario compilar de nuevo todas las aplicaciones que usen el paquete. Esto
ADVERTENCIA: Esto se refiere la distribuci6n en programas cornpilados divididos en EXE y paquetes, no a la distribuci6n dr: componentes a
otros desarrolladores en Delphi. En este ultimo caso, las normas sobre
versiones son m b estrictas y deberiamos prestar una atencibn especial a
las versiones de 10s paquetes.
Dicho esto: es recomendable no cambiar nunca la interfaz de ninguna unidad
exportada por nuestros paquetes. Para ello, podemos aiiadir a nuestro paquete
una unidad con funciones de creacion de formularios (como en las DLL con
formularios presentadas previamente) y utilizarla para acceder a otra unidad que
defina 10s formularios. A pesar de que no hay manera de ocultar una unidad que
esta enlazada a un paquete, si nunca usamos directamente la clase definida en una
unidad, sin0 que la usamos a traves de otras rutinas, conseguiremos una mayor
flexibilidad para modificarla. Tambien podemos usar la herencia de formularios
para modificar un formulario que este dentro de un paquete sin afectar a la version original.
La regla mas restrictiva con respecto a 10s paquetes, usada por 10s autores de
componentes, es esta: para una distribucion y mantenimiento a largo plazo del
codigo de 10s paquetes, debemos planificar realizar una distribucion principal de
mayor entidad con distribuciones menores de mantenimiento. Una distribucion de
gran tamaiio de un paquete requerira recompilar todos 10s programas cliente; el
fichero del paquete debera entonces ser renombrado de acuerdo con un nuevo
numero de version, y las secciones de interfaz de las unidades podran ser modificadas. Las distribuciones de mantenimiento de ese paquete deberian limitarse a
cambios de implernentacion para mantener la compatibilidad total con 10s
ejecutables y las unidades existentes, tal y como hace Borland con sus Update
Packs.
// muestra el formulario
i f FormScroll.ShowModal = mrOK t h e n
C o l o r : = FormScroll.SelectedColor;
finally
FormScroll.Free;
end;
end ;
Una de las ventajas de esta tecnica esta en que podemos referirnos a un formulario compilado en un paquete con un codigo esactamente igual a1 que usaremos
para un formulario compilado en el programa. En realidad, si compilamos este
programa, la unidad del formulario se enlazara a el. Para guardarlo en el paquete,
tenemos que usar paquetes en tiempo de ejecucion para la aplicacion y aiiadir de
forma manual el paquete PackWithForm a la lista de paquetes en tiempo de ejecucion (esto no lo sugiere el IDE de Delphi porque no hemos instalado el paquete en
el entorno de desarrollo).
Despues de esto, compilamos el programa y se comportara como es habitual.
Pero ahora el formulario esta en un paquete DLL e incluso se puede modificar el
formulario en el paquete, compilarlo de nuevo y ejecutar sencillamente la aplicacion para ver 10s efectos. Sin embargo, fijese en que con la mayor parte de 10s
cambios que afectan a la parte de interfaz de las unidades del paquete, deberiamos
compilar tambien de nuevo el programa ejecutable que llama a1 paquete.
NOTA: Podemos encontrar el paquete y el programa de prueba en la carpet . PackForm en la que estA el d d i g o fuente relativo a este capitulo. El
_ J - 1 -:-..:--*L
. - 1coalgo
ael slgulenre ejemplo esra
en
la rmsma carpaa. FEIl paqueze -y 10s
proyectos son todos referenciados por el archivo & grupo de proyecfo (BPG)
de la carpeta.
-13:.
. I _
_:_...-I_
I -
I--
(TFormScroll);
Para resumir estos cambios, veamos el codigo utilizado por el programa principal (la aplicacion DynaPackForm) para mostrar el formulario modal desde el
paquete cargado dinamicamente:
procedure TForml.BtnChangeClick(Sender: TObject);
var
FormScroll: TForm;
FormClass: TFormClass;
HandlePack: HModule;
begin
// intenta cargar e l paquete
HandlePack := LoadPackage ( ' PackWi thForm. bpl ) ;
if HandlePack > 0 then
begin
FormClass := TFormClass (Getclass ( ' TFormScroll' ) )
Cabe destacar que el programa descarga el paquete tan pronto como ha acabado con el. Este paso no es obligatorio. Podiamos haber movido la llamada
U n l o a d P a c k a g e a1 manejador O n D e s t r o y del formulario para asi evitar la
recarga del paquete despues de la primera Ilamada.
Ahora podemos ejecutar el programa sin que el paquete este disponible y veremos que arranca de forma adecuada, solo indicara que no puede encontrar el
paquete cuando pulsemos el boton Change. En este programa no es necesario
utilizar paquetes en tiempo de ejecucion para mantener la unidad fuera del archivo ejecutable porque no se referencia a la unidad desde el codigo. Ademas, no es
necesario que el paquete PackWithForm este en la lista de paquetes en tiempo de
ejecucion.
De todos modos, debemos utilizar paquetes en tiempo de ejecucion o el programa incluira las variables globales VCL (como el objeto A p p l i c a t i o n ) y el
paquete cargado de forma dinamica contendra otra version, porque se referira de
todos modos a 10s paquetes VCL.
mente;se ciefrttpcklmoa sufrir violaciones de acceso. Frecuentemente, es.tas ocurren porqw y&. abjet:o cuya clase esta definida en el paquete se
mantiene en memoria incluso cuando el paquete es descargado de la memo'
-.,
&..-A*
-..--..l:1
*..1-.ria.
el nr r "o -e- -hv -se
muznr*l
UUGI
a 1-cac
~. Cuando
.
= ~~
...
.-- - uulcru
_ .., . . I I ~ I I U - c
- - -i I*.
~ . ~, uucuc
:
do a1 rnaodo D e s t r o y de una VMT inexistente, y causar por ello un
error. Dado que este tipi de errores son muy dificiles de detect& y corregir
. .
.
. roaos
-.
- mres ae aescargar
.t
aeoemos
asegurarnos
a e aesrrulr
ros oojnos
er
-L:-b-
:-&--*-*
- - -
---
- - - -~
----
-1.
- - - -
-1
(AClass: TClass);
var
ClassesColorSelect:
implementation
TClassList;
:=
~~1assList.Create;
finalization
ClassesColorSelect.Free;
end.
...
private
procedure SetSelColor (Col: TColor) ;
function GetSelColor: TColor;
public
function Display (Modal: Boolean = True) : Boolean;
Boolean): Boolean;
(TFormSimpleColor);
AComponent : TComponent ;
ColorSelect: IColorSelect;
begin
AComponent : = TComponentClass
(ClassesColorSelect[LbClasses.ItemIndex]) .Create
( A p p l i c a t i o n );
ColorSelect : = AComponent a s IColorSelect;
ColorSelect.SelColor : = Color;
ColorSelect.Display ( F a l s e ) ;
El programa usa en realidad la funcion supports para verificar que el formulario soporta la interfaz antes de usarla y tambien cuenta con la version modal
del formulario; per0 su esencia esta presente en las cuatro instrucciones anteriores. Ademas, hay que destacar que el codigo no necesita un formulario. Un buen
ejercicio podria consistir en aiiadir a la arquitectura un paquete con un componente que encapsulara o heredara del cuadro de dialog0 de selection de color
Estructura de un paquete
Podriamos preguntarnos si es posible saber si una unidad ha sido enlazada en
el fichero ejecutable o si forma parte de un paquete en tiempo de ejecucion. No
solo esto es posible en Delphi, sino que incluso se puede explorar la estructura
general de una aplicacion. Un componente puede utilizar la variable global
Module I s Pac kage, declarada en la unidad SysInit. No deberiamos necesitar
esta variable, per0 es tecnicamente posible tener un componente con codigos diferentes dependiendo de si esta empaquetado o no. El siguiente codigo extrae el
paquete en tiempo de ejecucion que alberga a1 componente, si lo hubiera:
var
fPackName: string;
begin
// o b t i e n e e l n o m b r e d e l p a q u e t e
SetLength (fPackName, 100);
i f ModuleIsPackage then
begin
GetModuleFileName (HInstance, PChar (fPackName), Length
(fP a c k N a m e ) ) ;
fPackName : = PChar (fPackName) // a j u s t a l a l o n g i t u d d e
l a cadena
end
else
fPackName : = ' N o t p a c k a g e d ' ;
D Contams
Packlnfo [ M a ~ nUnd )
PackFo~m
Sysln~t
Requ~res
C.\WINDOWS\System3Z\vcldb70 bpl
Contalns
.:
Syslnrl
VDQConsta
DBOleCll
DBCtrls
DBLogDlg
DBPclns
dbcgr~ds
DBGrlds
+ Flequurs
Deicr~p(~on
Borland Database Components
Fun Drily
- C \~dINDOWS\Syslem32\dbrII70 bpl
+ Conta~ns
+ Rcqu~res
Descnpt~or~
Borland Core Dslabase Components
Run Only
- r \ \ A I I N ~ ~ \ A I < \ < , , * I P ~M
~~\I~-~~
Figura 10.6. El resultado del ejernplo Packlnfo, con la lista de paquetes que usa.
/ / o b t i e n e l a description y a d a d e n o d o s a j u s t a d o s
Como podemos ver en el codigo anterior, la funcion F o r E a c h M o d u l e comienza aiiadiendo el nombre del modulo como nodo principal del arb01 (llamando
a1 metodo Add del objeto Treeview1 . I t e m s y pasando n i l como primer
parametro). A continuacion, aiiade dos nodos hijos fijos, que se guardan en las
variables C o n t N o d e y ReqNode declaradas en la parte de implementation de
esta unidad.
Como paso siguiente, el programa llama a la funcion G e t P a c k a g e I n f o y
le pasa otra funcion de retrollamada, Show 1n f o P r o c , para obtener una lista de
las unidades de la aplicacion o del paquete. A1 final d e l a funcion
F o r Ea c hModule, si el modulo es un paquete el programa aiiade mas informacion, como su descripcion y sus indicadores de compilation (el programa sabe
que es un paquete si su descripcion no es una cadena vacia).
Anteriormente, hemos mencionado el paso de una funcion de retrollamada (el
procedimiento S h o w I n f o P r o c ) a la funcion G e t P a c k a g e I n f o , que, a su
vez, llama a la funcion de retrollamada para cada paquete contenido o requerido
de un modulo. Este procedimiento crea una cadena de caracteres que describe el
paquete y sus indicadores principales (agregados entre parentesis), y despues
inserta esa cadena bajo uno de 10s dos nodos (ContNode y ReqNode), dependiendo del tipo de modulo. Podemos determinar el tipo de modulo examinando el
parametro NameType. Este es el codigo de la segunda funcion de retrollamada
a1 completo:
procedure ShowInfoProc (const Name: string; NameType:
TNameType;
Flags: Byte; Param: Pointer);
var
FlagStr: string;
begin
FlagStr : = ' ' ;
if Flags and ufMainUnit <> 0 then
FlagStr : = FlagStr + ' M a i n U n i t ' ;
Modelado
y programacion
orientada
a objetos (con
ModelMaker)
Cuando Borland decidio ofrecer una solucion de diseiio UML para las ediciones Enterprise y Architect de Delphi 7, escogio incluir ModelMaker, de
ModelMaker Tools ofHolland (www.modelmakertools.com).ModelMaker es una
herramienta de diseiio UML de alta calidad integrada en el IDE de Delphi. Pero a
medida que entremos en contact0 con ModelMaker, veremos que es mucho mas
que una herramienta para diagramas UML. Hablaremos, por supuesto, de las
facilidades que ofrece ModelMaker para realizar diagramas UML, per0 tambien
de otras caracteristicas de la herramienta asi como de una vision global conceptual del product0 que deberia permitir su maximo aprovechamiento.
ModelMaker ha existido desde 10s primeros dias de Delphi, y con el tiempo ha
acumulado opciones para soportar casi por completo el lenguaje Delphi a1 igual
que una gran cantidad de utiles recursos para programadores. El resultado es un
impresionante conjunto de caracteristicas atractivo a primera vista. La interfaz de
usuario de ModelMaker incluye mas de 100 formularios y, sin unos conocimientos adecuados, puede ser muy frustrante para el recien iniciado.
Aunque ModelMaker suele llamarse herramienta de diagramacion UML, es
preferible describirla como una herramienta CASE y de diagramas UML de ciclo
completo, extensible, personalizable y especifica para Delphi. Es especifica para
Delphi porque se diseiio para manejar codigo Delphi. Por ejemplo, cuando se
trabaja con una propiedad, 10s cuadros de dialog0 de ModelMaker presentan
opciones que son especificas a palabras y conceptos clave del lenguaje Delphi.
ModelMaker es personalizable porque cientos de opciones controlan el mod0 en
que se genera el codigo Delphi a partir del modelo de objetos. ModelMaker es
extensible porque incluye una API OpenTools muy robusta que permite la creacion de ampliaciones expertas para extender la funcionalidad del producto. Se
trata de una herramienta de ciclo completo porque ofrece caracteristicas que se
aplican a todas las fases de un proceso de desarrollo estandar. Por ultimo,
ModelMaker puede describirse como una herramienta CASE porque generara
automaticamente parte del codigo obvio y redundante necesario para las clases de
Delphi, dejando a1 programador con la tarea de proporcionar el codigo operativo
de las clases.
Este capitulo ha sido coescrito con Robert Leahey y se basa en gran medida en
su conocimiento e intensa experiencia con ModelMaker. En el mundo del software, Robert es un arquitecto, programador, autor y conferenciante. Como musico, ha tocado profesionalmente durante mas de 20 aiios y actualmente es un
estudiante graduado en la University of North Texas en el area de la teoria musical. A traves de su empresa, Thoughtsmithy (www.thoughtsmithy.com), Robert
ofrece servicios de consultoria e instruccion, software comercial, produccion de
sonido y esculturas LEG0 de gran tamaiio. Vive en el norte de Texas con su
mujer e hijas. Este capitulo trata 10s siguientes temas:
Conceptos de ModelMaker
Modelado y UML.
Caracteristicas de codification de ModelMaker.
Documentacion y macros.
Reingenieria de codigo.
Implementacion de patrones.
Modelado y UML
UML (Unified Modeling Language) es una noticacion grafica usada para expresar el analisis y diseiio de un proyecto software y comunicarselo a otras personas. UML es independiente del lenguaje, per0 esta pensado para la descripcion de
proyectos orientados a objetos. Como resaltan 10s creadores de UML, no se trata
de una metodologia, y puede usarse como una herramienta descriptiva sin importar cual sea el proceso de diseiio preferido.
Vamos a fijarnos en 10s diagramas UML desde el punto de vista de un programador de Delphi que use ModelMaker.
Diagramas de clase
Uno de 10s diagramas UML mas comunes soportados por ModelMaker es el
diagrama de clase. Los diagramas de clase pueden mostrar una amplia variedad
de relaciones de clase, pero, en su minima expresion, este tipo de diagrama muestra un conjunto de clases u objetos y las relaciones estaticas existentes entre ellos.
Por ejemplo, la figura 1 1.1 muestra un diagrama de clase que contiene las clases
del programa NewDate de un capitulo anterior. Si 10s resultados son distintos
cuando se importen estas clases en ModelMaker y se Cree un diagrama de clase
propio; hay que tener en cuenta las numerosas opciones comentadas anteriormente. Muchos parametros controlan el mod0 en que se muestran las clases
visualizadas. Se puede abrir el archivo de ModelMaker (MPB) usado para la
figura 1 1.1 desde la carpeta de codigo fuente del capitulo actual.
atlrlbr~CS
- IDBle. TDdeTne;
+
Day: Inleger;
Inleger.
Yaar M*p:
Mo*
0Wnions
- GetDay IMeger;
Assign(. )
- G e t ~ o m hhleger;
Denease(..)
Getyear, Imeger,
- SefDay( .)
- SelManlh(..)
- SelYear( )
+ Gesde
GelTeut. d r h g
+Increase(..)
+ Leapyea. BncFa~nc~a.
SelVW.I
r Setvalue( )
+
+Create( )
PUS htegsr,
check in
rmwt
Ncur~lssham
Diagramas de secuencia
Los diagramas de secuencia modelan la interaccion entre objetos representando 10s objetos y 10s mensajes transmitidos entre ellos a lo largo del tiempo. En un
diagrama de secuencia tipico, 10s objetos que interactuan dentro de un sistema se
organizan a lo largo del eje X y el tiempo se representa desde arriba hacia abajo,
a lo largo del eje Y. Los mensajes se representan como flechas entre objetos. Se
puede ver un ejemplo de un diagrama de secuencia bastante trivial en la figura
1 1.4. Los diagramas de secuencia pueden crearse a diversos niveles de abstraccion, con lo que se permite representar la interaccion del sistema a alto nivel,
con solo unos pocos mensajes, o una interaccion a bajo nivel, con muchos men-
IJumpei
= SetPosp'alue: Inlegerj;
= GelPor Inleper;
--
--
t h i i lrxepn
TAlh!.et~
Tkhlele
TAlhkle
TAlhkle
J w *;le
w a :rcr*
Podlan-InNpe,
Redundanl rnembar
+ C~ede.
+ Deshy.
+ IJumplrnpl TJurnperlrnpl.
+ Jurpcl TJurnperlrnpl
+ Wdkl: dnng:
TAthlele
TAIhlete
TAlhlele
TAthlele
TAtHele
TAlhlcte
Junto con 10s diagramas de clase, 10s diagramas de secuencia son unos de 10s
diagramas UML soportados por ModelMaker que se relacionan mas de cerca con
el modelo de codigo.
Se pueden crear varios diagramas en ModelMaker en 10s cuales 10s simbolos
usados no tienen una relacion directa con el modelo de codigo, per0 en 10s diagramas
de secuencia se puede afectar directamente a1 codigo de las clases del modelo y a
sus metodos. Por ejemplo, cuando se crea un simbolo para un mensaje entre
objetos. se puede escoger de entre una lista de metodos que corresponden a1 objeto
receptor, o se puede decidir afiadir un metodo nuevo a1 objeto, y el nuevo metodo
se aiiadira a1 modelo de codigo. Fijese en que tal y como se comento, ModelMaker
no creara automaticamente diagramas de secuencia para el codigo importado;
sera necesario crearlos de forma manual.
-13 s
+i
ru
Oeatcly rn b lntepn)
a PissglSwce 7 0 4 4
* 3 Dec~eeselNumbetOlDayr
+
, 3 Ge(Tex( simg
+,
iu
Increas$4mbnOiD~
Leapyea Bookan
!a(
1
ChFck h
Insert
Nmw~-nct-~.m
simbolo del caso de uso a un archivo externo que contenga el texto del caso
de uso. El resto de diagramas UML soportados por ModelMaker son 10s siguientes:
Diagramas de colaboraci6n: Diagramas de interaccion, muy parecidos a
diagramas de secuencia. Sin embargo, se diferencian en que el orden de 10s
mensajes se especifica mediante numeracion en lugar de emplear una escala de tiempo. Esto produce una disposicion de diagrama distinta en la que
las relaciones entre 10s objetos pueden verse en ocasiones de manera mas
clara.
Diagramas de estado: Diagramas que describen el comportamiento de un
sistema identificando todos 10s estados que puede asumir un objeto como
resultado de 10s mensajes que recibe. Un diagrama de estado deberia mostrar una lista de todas las transiciones de estado a que se encuentra sujeto
un objeto, indicando 10s estados inicial y final de cada transicion.
Diagramas de actividad: Diagramas que muestran el flujo de un sistema y
son particularmente utiles para visualizar procesamiento en paralelo.
Diagramas de componentes y despliegue: Tambien conocidos como
diagramas de implementacion. Diagramas que permiten modelar las relaciones entre 10s componentes (modulos, en realidad ejecutables, objetos
COM, DLL, etc...) o, en el caso de 10s diagramas de despliegue, 10s recursos fisicos (que suelen llamarse nodos).
Diagramas no UML
ModelMaker soporta tres diagramas que no son estandar de UML, per0 son
bastante utiles:
Diagramas de mapa mental (Mind-Map): Creados por Tony Buzan en la
decada de 1960. Un metodo excelente para tormentas de ideas, explorar
temas con ramificaciones o registrar rapidamente pensamientos encadenados. Suelen usarse para mostrar datos genericos durante presentaciones.
Diagramas de dependencia de unidades: Suelen usarse para mostrar 10s
resultados del potente Unit Dependency Analyzer de ModelMaker. Estos
diagramas pueden mostrar relaciones ramificadas de unidades dentro de un
proyecto Delphi.
Diagramas de robustez: Estos diagramas se han dejado fuera de la especificacion UML, tal vez sin mucho sentido. Ayudan a salvar el obstaculo
entre el modelado de casos de uso de solo interfaces y la implementacion de
diagramas de secuencia especificos. Los diagramas de robustez pueden
ayudar a un equipo de analisis a verificar sus casos de uso y orientarse
hacia 10s detalles de la implementacion.
M!;$
,T
SUML
almplemenlabonD~agram
Collaboral~onDlagram
m ~ l a s D~aglam
s
Use Case D~agram
.Yr
I.^
jd
R_UnModelMaker
16 4
Q, Add to Model
Sh~ft+Ctrl+H
Cmvert to Model
Cgnvert pro@ to Model
OpmModel
Inlegratan Opkm..
Version Control
R-ce
8s
-cut
.
b
S t r r q W~zard
Wizard.
r,
,,
Existe un cierto sentido de abstraccion en el nivel del desarrollador para desarrollar en ModelMaker. Libera a1 desarrollador de la necesidad de pensar en
detalles de implementacion durante la edicion de 10s miembros de clase; simplemente se necesita pensar en terminos de interfaz, mientras que ModelMaker se
encarga de la mayor parte de las tareas repetitivas de la implementacion del miembro. (No hay que confundir esta metafora con la escritura del codigo de la
implementacion de un metodo, que sigue resultando necesaria.)
MMW1N:CLASSIMPLEMENTATION
end.
TDateForm; ID=37;
de codigo. Cuando ModelMaker genera una unidad, comienza por la parte superior de este codigo y emite lineas de texto mientras busca una de estas tres cosas:
texto plano, macros o etiquetas de generacion de codigo.
En este ejemplo, el texto plano puede encontrarse en la primera linea: u n i t .
ModelMaker emitira este texto exactamente tal y como es. El siguiente elemento
de la linea es una macro, < !UnitName ! >. Ya las comentaremos en profundidad mas adelante, per0 baste ahora comprender que ModelMaker expandira la
macro en el sitio. En este caso, la macro representa el nombre la unidad, y sera ese
texto el que se emita.
Finalmente, un ejemplo de una etiqueta de generacion de codigo aparece directamente bajo la palabra clave t y p e :
MMW1N:CLASSINTERFACE
TDateForm; ID=37;
MMW1N:CLASSINTERFACE TDateForm;
var
DateForm: TDateForm;
ID=37;
implementation
MMW1N:CLASSIMPLEMENTATION
end.
TDateForm;
ID=37;
La vista de diferencias
Como ya se comento, es facil caer en una situacion en que el modelo y 10s
archivos fuente pierdan su sincronizacion. Si se han editado tanto el modelo como
10s archivos fuente, no se puede simpleinente volver a generar 10s archivos a
partir del modelo, porque se sobrescribirian 10s cambios hecho en 10s archivos.
Del mismo modo, no se pueden volver a importar las unidades, pues seguramente
se eliminarian 10s cambios del modelo. Por suerte. ModelMaker ofrece una de las
herramientas de analisis de diferencias mas robustas que esisten. Cuando el modelo haya perdido la sincronia, es hora de usar la pestaiia Difference (vease la
figura 1 1.8).
> laddE.lnkrr 6 - u a d
1
--
--
----
- 3.
4.7 * + , . C 1
Id 7
+.
SelC!dsrlnrcrurmnledaOos~IMM
.b M A o a 3 n g B
&a
FModeLoadmu Bodeen
1( ModePatComl lntw
FModtParICoun( I n l w ,
Id 9
1
I
GsrModelParlCwr lnteper,
MyNmPrqmty In(w
.A
*.
FMyPlswPrope~lyIntegef
GelMyNewPmpefly Inlepn
SetM:NcwPapetcomt aV&
.a PrcgfersVaClc I n t q i
I
w'
>
L-c 7
PLS
Irmt
--
indica que el miembro de clase solo existe en disco, no en el modelo. Con esta
informacion, se puede decidir como proceder en el proceso de volver a sincronizar
el modelo. Una caracteristica muy practica es la capacidad de volver a importar
un metodo seleccionado (un lugar de toda la unidad) desde la vista Difference.
4 . . h ~ p , < ' $ ~ . ; ~72: p
d 3 1&S
;j1134 k)P.~!!i:~,o
~~EGmbpmen~\~oddMaker
EX&SWMP bsk c~\Diivdopnrmwod~l~~
E~II\M~)
defat~t
v i r l b l l ~ t y : default
~rccejur.
t z n U I T e ~ r C l l c k ' 5 c n d e r : pzcceduzo btnrlITcsst1ic't:Sendar:
~~)C.\D~eb~ent\Mo&lMake~E~~lnW
MAPII
rLLe--r:= = zr,
a-z:=-=n=~z~z:.
rT.> C.\Devebpmenl\ModelMake~ExpertsWMAPII
;~,C,\Devebprnent\ModelMaker ExperlswMAP1l pzaccdure T ~ L S - W ~ ~ F I I X ~ ~ O : C E T Cp~rC wH ~~ d u r eTfzr-~PI1.xplo1CrTe.DX
Ljtuin
begin
h?r
C . \ D e v ~ n t \ M o d e l M a k e rExperlsWMAPIl
?la.~z*".e?2YL:
r?>C.\Dwebpnenl\ModelMakerEx~ert~WmP1l : r r ~ ~ ; ~ ~ i e 3 : - - . . ~ ~rrsize
ull
;pp.
cnx,
T~F~Y~;IzxFI~z~I~~F~!:I,.~.
I 0 C.\DevebprnenlWodelMaker ExpertsWtd4PII
' ::
Classes
;:t
:r?.?CXT~'it-"i-ll.L
1xp:ezer
'5
TlrrnMWlExpbrerTeslMam
I -
--
p7t<~_---
1,-
presente que aunque pueda existir un nuevo tipo de evento en el modelo de codigo
interno de ModelMaker, no existira en un archivo fuente hasta que se aiiada la
declaracion del tip0 de evento a una unidad. El mod0 mas facil de administrar este
proceso es arrastrar la declaracion de tip0 de evento desde la lista de la vista
Events a la lista Unit y dejarla caer sobre una unidad.
Documentacion y macros
ModelMaker puede ser muy practico para soportar 10s esfuerzos de documentacion de software. Necesitaremos dominar un concept0 muy importante antes de
continuar (aunque no es muy complejo): dentro de ModelMaker, la documentacion no equivale a 10s comentarios. No hay que tener miedo; se pueden hacer
cosas complicadas con 10s comentarios del codigo fuente, per0 hay que dar algunos pasos para que ModelMaker emita (o importe) esos comentarios. Casi cualquier elemento del modelo (clases, unidades, miembros, simbolos de diagrama y
demas) pueden tener documentacion, per0 introducir documentacion para un elemento no provocara que la documentacion aparezca automaticamente en el codigo fuente. El texto estara vinculado a1 elemento dentro del modelo, per0 hay que
provocar que ModelMaker genere un comentario en el codigo fuente que contenga
esa documentacion.
,.------Cbss s v m g
[Class
Qwhn Tcs:T';.FIIxplrze:
--
Reingenieria de codigo
Reingenieria es uno de esos terminos llamativos de la programacion de 10s que
constantemente se habla, per0 que significa cosas distintas para gente distinta. La
reingenieria es basicamente el proceso de mejorar el codigo existente in situ sin
alterar su comportamiento externo. No existe un unico proceso de reingenieria a1
que haya que acogerse, es simplemente el trabajo de tratar de mejorar el codigo in
situ sin provocar una gran perturbacion.
NOTA: Ademas, podemos decir que hay versiones beta (en el momento de
la elaboracion de este libro) de una version fbtura de ModelMaker que time
nuevas herramientas de reingenieria. La mayoria de ellas surgen &I libro
de Martin Fowler sobre la reingenieria, y son impresionantes.
Patrones de disefio
Mientras que 10s programadores se concentran en la irnplementacion de
clases especificas, 10s diseiiadores se centran m b en conseguir que clases y
objetos distintos hncionen juntos. Aunque es dificil ofrecer una definici6n
n r e ~ i c sAn1 A i c n i i ~AP c n f k w a r ~W
mm
m c ~ m ~C
iP
a t r s t a AP
Y W D W I I W I U UV % & U C U U W
yIWWIUCL U W I U a o W Y V Y W UVLCVIUIW,
11
nrrran;va&An
rle
11
I U V16LUILOWfiVY U W A U
Cl ,,A.L,,
GI UUUUIG
A-I ,
,
,
,
L
,
,
,
,
,
A
,
,
:
UCI pauuu ~3 U I I ~ VLCUILG,
I
a
,
.
.
pad
"
,
,.A,
L
.
. "
,
,
,,c
:
,
~ U 3~
G ~ U G WU ~ ~ IGG LI G I C L I L I ~
a
.
,
Plantillas de codigo
Otra potente prestacion mas en ModelMaker (que parece perdida, escondida
entre miles de otras caracteristicas) son las plantillas de codigo, una tecnica que
se puede usar para crear implementaciones propias de patrones de diseiio. Las
plantillas de codigo son como una diapositiva de parte de una clase que puede
aplicarse a otra clase. En otras palabras, se trata de un conjunto de miembros de
clase, guardados en una plantilla que puede aiiadirse a otra clase, de golpe. Aun
mejor, estas plantillas pueden parametrizarse (corno las macros) para que cuando
se apliquen a una clase aparezca un cuadro de dialogo solicitando rellenar ciertos
valores, que despues se aplican como parte de la plantilla. Un ejemplo es una
propiedad matriz. Declarar una propiedad matriz es sencillo en ModelMaker,
per0 implementar completamente una requiere varios pasos: no solo se necesita la
propiedad matriz en si, si no tambien una T L i s t o clase descendiente que contenga 10s elementos de la matriz y un sistema para proporcionar un recuento de
10s elementos almacenados. Incluso para este ejemplo tan sencillo, se requiere
algo de trabajo para conseguir que la propiedad de matriz quede lista y en funcionamiento. Aqui entra en accion la plantilla de la propiedad matriz. Abrimos un
modelo en ModelMaker (o creamos un nuevo modelo y aiiadimos un descendiente
de T O b j e c t ) y escogemos una clase a la que queramos aiiadir la nueva propiedad matriz. Hacemos clic con el boton derecho sobre la Member List y seleccionarnos Code Templates. Ahora deberia aparecer una barra de herramientas
flotante llamada Code Templates (fijese en que se trata de la misma barra de
herramientas disponible en la pestaiia Patterns). Hacemos clic sobre el boton
Apply Array Property Template para abrir el cuadro de dialogo Code Template
Parameters. Contiene una lista de elementos que se puede especificar para la
plantilla que se va a aplicar, como muestra la figura 11.10. Se puede resaltar
cualquier elemento de la columna de la izquierda y pulsar la tecla F2 para editar
el valor de ese parametro. Aceptamos 10s valores predeterminados y hacemos clic
sobre OK. La clase deberia contener ahora 10s siguientes miembros:
private
FItems : TList;
protected
function GetItemCount: Integer;
function GetItems (Index: Integer) : TObject;
public
property Itemcount: Integer read GetItemCount;
property Items [Index: Integer] : TObject read GetItems;
TOblrct
l terncount
Fltems
Para crcar una plantilla de codigo propia, comenzaremos con una clase existente que ya tenga 10s miembros que se desean convertir en una plantilla. Seleccionamos esa clase y, a continuacion, en la Member List, seleccionamos 10s
micmbros que deseamos usar (puede tratarse de cualquier tip0 de miembro). Hacemos clic con el boton derecho sobre la Member List y seleccionamos Create
Code Template. Aparecera el cuadro de dialogo Save Code Template. Es
muy parccido a1 cuadro de dialogo Guardar como estandar (y se especifica
donde guardar la plantilla), per0 se puede especificar el mod0 en que se desea que
aparezca la plantilla. Especificamos un nombre para la plantilla y la pagina de la
paleta dc plantillas en que se desea que aparezca. Prestamos atencion a1 mensaje
de confirmacion resultante; se puede modificar la imagen de la paleta si se desea.
Ahora la nueva plantilla estara disponible en la paleta de plantillas, de manera
que se pucde aiiadir esta plantilla a cualquier clase. Para parametrizar la plantilla, hay quc modificar el archivo PAS que se creo cuando se guard6 la plantilla.
Por ejemplo, esto es parte del archivo A r r a y P r o p -List .p a s usado por la
plantilla Array Property:
unit ArrayProp-List;
/ / D E F I N E M A C R O :I t e m s = n a m e o f a r r a y p r o p e r t y
//DEFINEMACRO: TObject=type of a r r a y p r o p e r t y
//DEFINEMACRO:IternCount=Method r e t u r n i n g # i t e m s
//DEFINEMACRO:FItems=TList F i e l d s t o r i n g items
Integer] : <!TObject!>
read
Cuando se aiiade esta propiedad a una clase como parte de la plantilla, las
macros (cosas como < ! ~te m s ! >) seran sustituidas por el valor del parametro
apropiado. De este modo, se pueden usar parametros para personalizar en profundidad las plantillas de codigo.
De COM
Implernentacion de IUnknow
Es importante que revisemos en primer lugar algunos conceptos basicos de
COM. Todo objeto COM debe implementar la interfaz IUnknown, tambien deno-
minada llnterface en Delphi, para usar interfaces que no sean de tipo COM. Esta
es la interfaz base de la que heredan todas las interfaces de Delphi, y Delphi
proporciona un par de clases diferentes con implementaciones de IUnknownl
I I n t e r f a c e listas para utilizar, como T I n t e r f a c e d O b j e c t y
TComOb j e c t . La primera sc puede utilizar para crear un objeto interno no
relacionado con COM, mientras que la segunda se utiliza para crear objetos que
pueden ser exportados por servidores. Como ya se vera, existen varias clases mas
que heredad de TComOb j e c t y proporcionan soporte para mas interfaces, que
son requeridas por servidores Automation o controles ActiveX.
La interfaz IUnknown tiene tres metodos: AddRe f , Re l e a s e y
Q u e r y 1n t e r f a c e . Aqui e s t i la definicibn de lainterfaz l ~ n k n o w n
(extraida
de la unidad System):
type
IUnknown
i n t e r ace
['{OOOOOOOO-0000-0000-COOO-000000000046}']
f u n c t i o n Q u e r y I n t e r f a c e ( c o n s t I I D : TGUID;
out O b j ) : Integer; stdcall;
f u n c t i o n -AddRef: I n t e g e r ; s t d c a l l ;
f u n c t i o n -Release: I n t e g e r ; s t d c a l l ;
end;
-1
,-1---,
-1
GU
Si la respuesta es si, Query Interface normalmente devuelve un punt e r ~a1 objeto, utilizando su parametro de referencia de salida (obj ) .
Para entender la funcion del metodo QueryInter face,es importante tener
en cuenta que un objeto COM puede implementar varias interfaces, a1 igual que la
clase TComObject.Cuando se llama aQueryInterface,se debe pedir uno
de las interfaces posibles del objeto, utilizando el parametro TGUID.
Ademas de la clase TComObje c t, Delphi incluye mas clases COM
predefinidas. Esta es una lista de las clases COM mas importantes de la VCL de
Delphi, que usaremos profusamente mas adelante:
TTypedComObject: Definida en la unidad C omOb j , hereda de
TComObject e implementa la interfaz IProvideClasslnfo (ademas de
las interfaces IUnknown e ISupportErrorlnfo ya implementadas por la
clase basica).
TAutoObject: Definida en la unidad ComObj,hereda de TTypedComOb ject y tambien implementa la interfaz IDispatch.
TActiveXControl: Definida en la unidad AxCt rls,hereda de TAutoOb j ect e implementa varias interfaces (IPer~i~tStream
Init, IPersistStorage, IOleObject e IOleControl, por citar unas cuantas).
cuyo caso el GUID se llama CLSID); interfaces (en cuyo caso se vera el tkrmino
IID): y otras entidad COM y del sistema. Cuando se quiere saber si un ob-jeto
soporta una interfaz especifica, se pregunta a1 ob-jeto si implementa la interfaz
que tiene un determinado identificador (que en el caso de las interfaces COM
prcdefinidas esta establecido por Microsoft). Para indicar una clase especifica, se
usa otro ID (o CLSID). El Registro de Windows guarda este identificador (CLSID),
con indicaciones sobre la DLL o el archivo e.jecutable relacionados. Los
desarrolladores de un servidor COM definen el identificador de clase.
Todos estos identificadores se conocen como 10s GUID, o identificadores
globalmente unicos. Si cada desarrollador usa un numero para indicar su propio
servidor COM, jcomo podemos estar seguros de quc estos valores no estan duplicados? La respucsta corta es que no podemos. La verdadera respuesta es que el
GUID cs un numero tan grande (con 16 bytcs, o 128 bits, jque implica un numero
con 38 digitos!) que es casi imposible conseguir dos niimeros aleatorios que tengan el mismo valor. Ademas, 10s programadores pueden usar la llamada especifica CoCreateGuid dc la API (directamente o a travds de su cntorno de desarrollo)
para conseguir un GUID valido que refle~ealguna informacion del sistema.
En partc, 10s GUID creados en equipos con tarjetas de rcd tienen la garantia de
ser unicos, porque las tarjetas de rcd conticnen numeros de serie unicos que forman una basc para la creacion dc GUID. Los GUID creados en equipos con
identificadores de la CPU (como las Pentium 111) tambiCn pueden tener la garantia dc scr unicos, incluso sin tar-jcta de red. Aunque no cxista un identificador de
hardware unico; es poco probable que 10s GUID sc repitan.
- -- . -- - ADVERTENCIA: Ademas de tener cuidado de no copiar el GUID del
programa de otra persona (que puede producir dos objetos COM totalmente
diferentes que usen el mismo GUID), nunca se debe inventar un identificador
propio introduciendo una secuencia casual de numeros. Para evitar cualquier problema, simplemente hay que pulsar Control-Mayiis-G en el editor
de Delphi y se obtendra un nuevo GUID definido correctamente y unico.
En Delphi, cl tip0 TGUID (definido en la unidad System) es una estructura de
registro, que es bastantc extraiia, per0 necesaria para Windows. Gracias a la
magia del compilador de Delphi, tipicamente preparado para simplificar las tareas mas tediosas o que requieren mas tiempo, se puede asignar un valor a un
GUID usando la notacion hexadecimal estandar guardada dentro de una cadena,
como en este fragment0 de codigo:
cons t
Class-ActiveForml:
TGUID
'
[1AFA6D61-7B89-llDO-98DO-
444555540000)';
Figura 12.1. Un ejemplo de 10s GUID generados por el ejemplo NewGuid. Los valores
dependen del ordenador en el que se ejecute el programa y del momento de
ejecucion.
Esta funcion de la API recibe como parametros la clase y la interfaz solicitadas, y devuelve un objeto en su parametro de referencia. El objeto devuelto por
esta funcion es una fabrica de clases.
Como su nombre sugiere, una fabrica de clases es un objeto capaz de crear
otros objetos. Cada servidor puede tener varios objetos. El servidor expone una
fabrica de clases para cada uno de 10s objetos COM que puede crear. Una de las
muchas ventanas del enfoque simplificado de Delphi a1 desarrollo COM es que el
sistema puede proporcionar una fabrica de clase en lugar del programador Por
este motivo, no fue necesario aiiadir una fabrica de clase personalizada a1 ejemplo. La llamada a1 metodo CreateComObject de la API no termina con la
creacion de la fabrica de clases. Tras recuperar l a fabrica de clases,
CreateComObject llama a1 metodo CreateInstance de la interfaz
IClassFactory. Este metodo crea el objeto solicitado y lo devuelve. Si no se
produce ningun error, este objeto se convierte en el valor de retorno de la API de
CreateComObject.
Mediante este mecanismo, (incluyendo fabricas de clases y la llamada a
DLLGetClassObject), resulta muy sencillo crear objetos COM. A1 mismo
tiempo, Crea teComObje ct es simplemente una llamada a una funcion que
tiene un comportamiento mas complejo que el que aparenta a simple vista. Lo
bueno de Delphi es que ese mecanismo complicado de COM lo lleva a cab0
automaticamente el sistema en tiempo de ejecucion (se encarga de ello la RTL).
Para cada clase COM basica de la VCL, Delphi define tambien una fabrica de
clase. Las clases de fabricas de clases forman una jerarquia e incluyen
TComObjectFactory,TTypedComObjectFactory,TAutoObjectFactory y TActiveXControlFactory.Las fabricas de clases son importantes y todo servidor COM las necesita. Normalmente, 10s programas de Delphi
utilizan fabricas de clases creando un objeto en la seccion de inicializacion de la
unidad que define la clase del objeto de servidor correspondiente
ComServ;
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
begin
end.
Las cuatro funciones que exporta la DLL son necesarias para la compatibilidad COM y el sistema las usa de la manera siguiente:
Para acceder a la biblioteca de clases (Dl 1 G e t C l a s sob j e c t ) .
Para comprobar si el servidor ha destruido todos sus objetos y se puede
descargar de la memoria (Dl ICanUnloadNow).
Para aiiadir o eliminar informacion sobre el servidor en el Registro de
Windows ( D l l R e g i s t e r S e r v e r y D l l U n r e g i s t e r S e r v e r ) .
Normalmente, no tendremos que implementar estas funciones, porque Delphi
nos ofrece una implementacion predefinida en la unidad c o m s e r v . Por esta razon, en el codigo de nuestro servidor, solo necesitamos exportarlas.
I1,d 1
, , , 1 8 1 , ~ -'
I 1 -.i
,,.:
Desc~ifliar:
I
Servidor COM: La Biblia de Delphi 7
end ;
Despues de esaminar el codigo fuente generado por el asistente, podemos completarlo aiiadiendole a la clase TNumber 10s metodos necesarios para implementar
la interfaz I N u m b e r y escribiendo su codigo, y tendremos un objcto COM funcional en nuestro servidor
Multiple:
Interna: lndica aue el obieto solo ~ u e d ecrearse dentro del servidor; las
aplicaciones cliente no pueden solicitar este tip0 de objeto (esta conf
guracion especifica afecta tambien a 10s servidores en proceso).
La segunaa aeclsion riene que ver con el sopone ae nuos oel oojeto LVIVI,
que solo es valido para 10s servidores en proceso (DLL). El modelo de hilos
(o threading) es una decision conjunta de las aplicaciones cliente y servidor: si arnbas partes acuerdan usar un modelo, este se usa para la conexion.
Si no se llega a un acuerdo, COM aun puede establecer una conexion meque
puede
ralentizar las operaciones.
diante intermediation (marshaling),
.
Tambien hay que tener presente que un senidor no solo debe publicar su
modelo de hilos en el Registro (como resultado de establecer la opcion en el
,,
codigo. Estos son 10s puntos clave de 10s diversos modelos de hilos:
Modelo unico: No se trata de un soporte real para hilos. Las solicitudes
. LUIW
n
que llegan a1 servlaor
se serializan para que el clienre pueaa
realizar una operacion cada vez.
I1
.-I'
I.
Modelo libre, o "apartamento multihilo": El cliente no tiene restricciones, lo que significa que mfiltiples hilos pueden usar el mismo objeto
dche
-a1- -minmn
-------- tiemnn
-- ----=-. Pnr
- -- ecte
---- mntivn
----.-,cnda
--- -"mhndn
----- Ae
-- cada nhictn
--J-----protegerse a si mismo y a 10s datos no locales que utiliza contra ~ a r i a s
llamadas simultheas. Este modelo de hilos es mas complejo de soportar para un servidor que 10s modelos unico y de apartamento, ya que
inchso el acceso a 10s datos de la propia instancia del objeto deben
llevarse a cab0 con atenci6n sobre la seguridad de hilos.
"
. . . ..
..-..
Como se puede ver, hemos sobrescrito tambien el destructor de la clase, porque queremos comprobar la destruccion automatica de 10s objetos COM provistos por Delphi.
TObject);
/ / crea e l segundo o b j e t o
Num2 : = CreateComObject (Class-Number) as INumber;
Label2. Caption : = ' Num2: ' + IntToStr (Num2.Getvalue) ;
Button3.Enabled
Button4.Enabled
: = True;
: = True;
end ;
..-aI I - - - L
A:---.-
- nm.A-.~..~n~--n
- em
, . . m ~ ~ eC-
-1
---- L
.-
TRUCO:La tabla de m h d o s virtuales (VMT)compatible con COM conlleva un efecto inesperado. Los nombres de 10s mitodos no son importantes,
siempre que su direccion estk en la posicion apropiada en la VMT. Este es
el motivo por el que se puede proyectar un mitodo de una interfaz sobre una
funcion real que la implemente.
Para concretar, COM proporciona un estandar binario independiente del lenguaje para 10s objetos. Los objetos que se cornparten entre 10s modulos se encuentran compilados y su VMT tiene una estructura particular determinada por COM
y no por el entorno de desarrollo que se haya utilizado.
Automatizacion
Hasta ahora hemos visto que se puede utilizar COM para permitir que un
archivo ejecutable y una biblioteca compartan objetos. Sin embargo, la mayoria
de las veces, 10s usuarios quieren aplicaciones que se comuniquen entre si. Uno de
10s enfoques que se pueden utilizar para obtener este objetivo es la Automatizacion
(Azrtomation, antes llamada Automatizacion OLE u OLE Azrtomatlon). A continuacion comentaremos el desarrollo de controladores Autornatizacion para Word
y Excel, mostrando como transferir inforrnacion de bases de datos a estas aplicaciones.
-- .-
---
NOTA: La documentacibn actual de Microsoft usa el termino Automatizacibn en lugar de Automatizacion OLE y usa 10s timinos documento
activo y documento compuesto en lugar de Documento OLE. Este libro
utiliza la nueva tenninologia, aunque la antigua tenninologia "OLE" sigue
estando indicada y probablemente resulte mhs clara.
En Windows, las aplicaciones no viven en mundos aparte, sino que 10s usuarios suelen querer que estas interactuen entre si, a1 igual que 10s usuarios pueden
copiar y pegar datos entre aplicaciones. Sin embargo, cada vez hay mas programas que ofrecen una interfaz de Automatizacion para que otros programas la
dirijan.
Mas alla de la gran ventaja de la automatizacion programada, en comparacion
con operaciones manuales del usuario, estas interfaces son completamente neutrales en cuanto a1 lenguaje, por lo que se puede usar Delphi, C++, Visual Basic
o un lenguaje de macros para controlar un servidor de Automatizacion, independientemente del lenguaje de programacion usado para escribirlo. La Automatizacion
tiene una implernentacion sencilla en Delphi gracias a la labor extendida del
compilador y la VCL para proteger a 10s desarrolladores de su complejidad. Para
soportar Automatizacion, Delphi proporciona un asistente y un potente editor de
bibliotecas de tipos y soporta interfaces dobles. Cuando se usa una DLL en proceso, la aplicacion cliente puede usar el servidor y llamar directamente a sus
metodos, porque estan en el mismo espacio de direccion. Cuando se usa
Automatizacion, la situacion es mas compleja. El cliente (llamado controlador) y
el servidor, son dos aplicaciones separadas que funcionan en distintos espacios de
direccion. Por esta razon, el sistema debe enviar las llamadas a metodos usando
un complejo mecanismo de paso de parametros llamado marshalling
(intermediacion).
Tecnicamente, soportar Automatizacion en COM implica implementar la
interfaz IDispatch, declarada en Delphi dentro de la unidad System como:
type
IDispatch = interface (IUnknown)
['{00020400-0000-0000-COOO-000000000046)"
El primer0 de 10s dos metodos devuelve la informacion de tipo; 10s dos ultimos
pueden usarse para invocar un metodo real del servidor de Automatizacion. En
realidad, la invocacion solo la realiza el ultimo metodo, I n v o k e , mientras que
G e t I D s O f N a m e s se usa para determinar el identificador de invocacion (necesario para I n v o k e ) a partir del nombre del metodo. Cuando se crea un servidor
de Automatizacion en Delphi, todo lo que se tiene que hacer es definir una biblioteca de tipos e implementar su interfaz. Delphi proporciona el resto de lo necesario mediante su compilador y el codigo de la VCL (en realidad una parte de la
VCL llamada originalmente marco de trabajo DAX).
El papel de IDispath resulta mas patente cuando se considera que hay tres
maneras de que un controlador llame a 10s metodos expuestos por un servidor de
Automatizacion:
Puede solicitar la ejecucion un metodo, pasando su nombre en una cadena,
de forma similar a la llamada dinamica a una DLL. Esto es lo que hace
Delphi cuando s e usa una variante para llamar a1 servidor de
Automatizacion. Esta tecnica es muy sencilla de usar, per0 es bastante
lenta y no ofrece una gran verificacion de tipos del compilador. Implica
una llamada a G e t I D s O f Names seguida de otra a I n v o k e .
Puede importar la definition de una interfaz de Delphi de invocacion
( d i s p i n t e r f a c e ) para el objeto en el servidor y llamar a sus mCtodos
de forma mas directa (simplemente enviando un numero, es decir, llamando directamente a I n v o k e ya quc el DispID de cada metodo se conoce
en tiempo de compilacion). Esta tkcnica, basada en interfaces, permite a1
compilador comprobar 10s tipos de 10s parametros y produce un codigo
mas rapido, per0 requiere un poco mas de esfuerzo por parte del programador (el uso de una biblioteca de tipos). Ademas. terminamos vinculando el
controlador de la aplicacion a una version especifica del servidor.
- .- _
.--.-
--
--
--- -- ..--
- - -- -
.,.
N O T---.
A r Sa niteAe
nnrn oi~nrAnr
rinn referencia
ietn
-.r..--- iicnr lrna
....-vnrinnte
. .----..
--. a rrn-- n hJ-"a
"
.
.
--a
-A
--
NOTA: Como s e vera mas adelante, las ultimas versiones de Word siguen
registrando la interfaz Word. Basic, que se corresponde con el lenguaje
interno de macros WordBasic, pero tambikn registran la nueva interfaz
Word .Application,que se corresponde con el Ienguaje de macros VBA.
Delphi ofrece componentes que simplifican la conexibn con las aplicaciones de Microsoft Offke.
Estas tres lineas de codigo arrancan Word (a no ser que ya se este ejecutando),
crean un nucvo documento y aiiaden algo de texto. Puede verse el resultado de
esta aplicacion en la figura 12.3. Lamentablemente, el compilador de Delphi no
tiene ningun mod0 de comprobar si existen 10s metodos. Realizar toda la comprobacion de tipos en tiempo de ejecucion es algo arriesgado, ya que si se comete el
mas minimo error ortografico en el nombre de una funcion no se vera ningtin
aviso sobre el error hasta que se ejecute el programa y se llegue a esa linea de
codigo. Por ejemplo, si se hubiera escrito VarW. Isnert,el compilador no se
quejaria del error, pero en tiernpo de ejecucion ese error saltaria, ya que no reconoce el nombre, Word asume que el metodo no existe.
Aunque la interfaz IDispatch soporta el enfoque que se acaba de ver, tambien
es posible (y mas seguro) que un servidor exporte la descripcion de sus interfaces
y objetos mediante una biblioteca de tipos. Esta biblioteca de tipos puede entonces convertirse mediante una herramienta especifica (como Delphi) en definiciones escritas en el lenguaje que se quiere usar para escribir el programa cliente o
controlador (como el lenguaje Delphi). Esto posibilita que un cornpilador cornpruebe que el codigo es correct0 y poder usar las caracteristicas Code Completion
y Code Parameters en el editor de Delphi.
Delphi.
Una vez que el compilador ha realizado sus pruebas, puede usar una de las dos
tecnicas distintas para enviar la pcticion a1 servidor. Puede usar una simple
V T a b l e (es decir, una entrada en una declaracion de tipo de interfaz) o usar una
d i s - p i n t e r f a c e (una interfaz de envio o dispatch).
Ya se ha utilizado una declaracion de tip0 de interfaz, asi que deberia resultar
familiar. Una d i s p i n t e r f ace es basicamente un mod0 de proyectar cada
entrada de un interfaz sobre un numero.
Las llamadas a1 servidor pueden enviarse entonces mediante llamadas de numeros a I D i s p a t c h .I n v o k e , sin el paso adicional de llamar a I D i s p a t c h .
G e t I D s O f N a m e s . Se puede considerar que esta es una tecnica intermedia, a
medio camino entre enviar el nombre de la funcion y usar una llamada directa de
la V T a b l e
wua c;lcrnc;nio, r e a o o n l y
Figura 12.4. El editor de la biblioteca de tipos con 10s detalles de una interfaz.
Hay dos recomendaciones que conviene seguir para trabajar mejor con el editor de biblioteca de tipos de Delphi. La primera y mas sencilla es que si se hace
clic con el boton derecho del raton sobre la barra de herramientas y se activa la
opcion Text Labels se vera en cada boton de la barra un texto comentando su
efecto, lo que simplificara el uso del editor.
La segunda y m b importante es acudir a la pagina Type Library del cuadro
de dialog0 Environment Options de Delphi y activar el boton de radio de lenguaje Pascal en lugar del lenguaje IDL. Esta configuration determina la notacion
usada por el editor de biblioteca de tipos para mostrar 10s metodos y parametros,
e incluso para editar 10s tipos de 10s parametros de un metodo o el tip0 de una
propiedad.
A no ser que se este acostumbrado a escribir codigo COM en C o C++, probablemente se prefiera pensar en tkrminos de Delphi que en terminos de IDL.
safecall;
- .
:--I:--
^--^-
~ I C U C L C I I I I I U ~ U quc
U
ILIUIC;~
CIIUI
---:A^
'P--L:*-
GXILU. ~ U I I U I C plcpala
~
--^-^-a
-1-
error ampliado de COM que contendra el mensaje de la excepcion, de manera que 10s clientes interesados (como 10s clientes de Delphi) puedan recrear la excepcion del servidor en el lado del cliente.
Ahora podemos aiiadir una propiedad a la interfaz, haciendo clic en el boton
Property de la barra del editor. De nuevo podemos escribir un nombre, como
Value, y seleccionar a continuacion un tip0 de datos en el cuadro combinado
Type. Ademas de seleccionar uno de 10s muchos tipos ya presentes en la lista,
tambien podemos escribir directamente otros tipos, especialmente interfaces de
otros objetos. La definicion de la propiedad Value del ejemplo se corresponde a
10s siguientes elementos de la interfaz Delphi:
function Get-Value: Integer; safecall;
procedure Set-Value(Va1ue: Integer); safecall;
property Value: Integer read Get-Value write Set-Value;
*. TLB]
A continuation, esta dispint erf ace, que asocia un numero con cada
elemento de la interfaz IFirs t Server:
type
IFirstServerDisp = dispinterface
['{89855B42-8EFE-11D0-98D0-444553540000]']
procedure Changecolor; dispid 1;
property Value: Integer dispid 2;
end;
La ultima parte del archivo incluye una clase creadora, que se utiliza para
crear un objeto en el servidor (y por ello se usa en la parte cliente de la aplicacion,
no el servidor):
type
CoFirstServer = class
class function Create: IFirstServer;
class function CreateRemote (const MachineName: string) :
IFirstServer;
end:
Esta unidad declara la clase del objeto servidor, que debera implementar la
interfaz que acabamos de definir:
tYPe
TFirstServer = class (TAutoObject, IFirstServer)
protected
function Get-Value: Integer; safecall;
procedure ChangeColor; safecall;
procedure Set-Value(Va1ue: Integer); safecall;
end :
Delphi ya nos ofrece el esquema del codigo de 10s metodos, por lo que solamente tenemos que completar las lineas intermedias. En este caso, 10s tres metodos se refieren a una propiedad y dos metodos que hemos aiiadido a1 formulario.
En general, no deberiamos aiiadir un codigo relativo a la interfaz de usuario
dentro de la clase del objeto servidor. Nosotros lo hemos hecho asi porque queremos cambiar la propiedad V a l u e y obtener un efecto colateral (mostrar el valor
en un cuadro de edicion). Este es el formulario en tiempo de disefio:
-.-.7,7
- .- .
--7
--
c l a s s f u n c t i o n CoFirstServer.Create: IFirstServer;
begin
Result : = CreateComObject (Class-Firstserver) a s
IFirstServer;
end;
names: TF~rstServer
Ud 6 name:
I~.~rchwo
desprograma\8orIand\D~h17\Impo
.. I
Despues puede usarse para llamar a 10s metodos como es habitual, despues de
haber asignado un objeto a la variable, convirtiendo el objeto devuelto por la
clase creadora:
DMyServer
: = CoFirstServer.Create
as IFirstServerDisp;
programs
E ...-
I - _3 -
n-i-~:
de referencias, por lo que si una variable que este relacionada con un objeto
interfaz se declara localmente en un metodo, el objeto se destruira al final del
metodo y el servidor puede cerrarse (si todos 10s objetos creados por el servidor se
han destruido). Por ejemplo, escribir un metodo con este codigo produce un efecto
minimo:
procedure TClientForm.ChangeColor;
var
IMyServer: IFirstServer;
begin
IMyServer : = CoFirstServer.Create;
1MyServer.ChangeColor;
end ;
A menos que el servidor ya se encuentre activo, se crea una copia del programa
y se modifica el color, pero entonces se cierra el servidor inmediatamente porque
el objeto de tip0 interfaz sale de su alcance. El metodo alternativo que hemos
utilizado en el ejemplo TlibCli es declarar el objeto como un campo del formulario y crear el objeto COM al arrancar, como en este procedimiento:
procedure TClientForm. Formcreate (Sender: TObject) ;
begin
I M y S e r v e r : = CoFirstServer.Create;
end ;
NOTA: Se inicia una variante como el tipo var Empty cuando se crea. Si
en cambio se asignara el valor nulo a la variante, su tip0 se convertiria en
varNull. Ambos tipos representan variantes sin valor asignado, per0 se
comportan de un mod0 diferente al evaluar la expresion. El valor varNull
.. , . .. . .
..
..
slempre se propaga a una expreslon (convlrtlendola en una expreslon nula),
mientras que el valor varEmpty desaparece sin hacerse notar.
El sewidor en un componente
A1 crear un programa cliente para este servidor u otro servidor de
Automatizacion, se puede utilizar una tecnica mejor: envolver el servidor COM
en un componente Delphi. Si se obsenla la parte final del archivo TlibdemoLib
TLB se v e r i la +declaracibn de una clase T Firstserver que hereda de
TOleServer.Se trata de un componente generado cuando se importa la biblioteca, que cl sistema registra en el procedimiento Register de la unidad.
Si se aAade esta unidad a un paquete, el nuevo componente servidor estara
disponible en la Component Palette de Delphi (en la pagina ActiveX, de manera predefinida). La generacion del codigo de este componente esta controlada por
una casilla de verification que se encuentra en la parte inferior del cuadro de
dialog0 Import Type Library, que mostraba la figura 12.5.
Se ha creado un nuevo paquete, PackAuto, que se encuentra disponible en un
directorio con el mismo nombre. En este paquete se ha aiiadido la directiva
LIVE SERVER-AT-DESIGN TIME en la pagina Directories/Conditionals del
cuadrode diilogo Project options del paquete. Esta directiva habilita una caracteristica adicional que no se obtiene por defecto: en tiempo de diseiio, el componente servidor tendra una propiedad adicional que lista como subelementos
todas las propiedades del servidor de Automatizacion:
debe
utilizarse con cuidado con 10s servidor& comple~osde ~utoma%zacion
(incluyendo programas como Word, Excel, PowerPoint y Visio). Ciertos ser.
.. .
vldores deben encontrarse en un tnOd0 especm antes cre pmer utlllzar sugunas
propiedades de sus interfaces de automatizacion.Ya que esta caracteristica
es problematica en tiempo de disefio para muchos servidores, no esta activada por defecto en Delphi.
..
. .
..a.
torio (tanto en tiempo de ejecucion como en tiempo de diseiio). Cuando la propiedad A u t oConne c t tiene el valor Fa 1se, el servidor de Automatizacion solo se
carga la primera vez que se llama a uno de sus metodos. Otra propiedad,
C o n n e c t K i n d , nos indica la manera de establecer la conexion con el servidor.
Siempre se puede iniciar una nueva instancia ( c k N e w I n s t a n c e ) , utilizar la
instancia en funcionamiento ( c k R u n n i n g I n s t a n c e , que muestra una violacion de acceso si el servidor no esta ya funcionando) o seleccionar la instancia
actual o iniciar una nueva si no hay ninguna disponible (ckRunningOrNew).
Por ultimo, se puede solicitar un servidor remoto utilizando c kRemo t e y anesar
directamente un servidor en el codigo despues de una conexion manual con
ckAttachToInterface.
NOTA: Para conectarse a un objeto ya existente, se necesita que estt registrado en la tabla de objetos en ejecucion (Running Object Table, ROT).El
A e h p r e ~ l ; ~ - r l-1
n e e r & A ~ r 1 1 a m - n A n Q 1- f i a n ~ ; A n am; C C ~ V n u e z o b a w uuvu a u u a t r u n a v u a o v a v a u v a ~ ~ r u u
~ lu
~ r~
u u u~a vu
u r\=
v y sa LG L
ren;c+m
particular, de un conjunto de clases interrelacionadas que son, con frecuencia, dificiles de entender.
Podriamos encontrarnos con un programa que solo funciona con una version especifica de la aplicacion del servidor, sobre todo si tratamos de
optimizar las llamadas usando interfaces en lugar de variantes. En concreto, Microsoft no intenta mantener la compatibilidad de guiones entre las
distintas versiones de Word u otras aplicaciones Office.
Delphi simplifica el uso de las aplicaciones Microsoft Office instalando de
antemano algunos componentes listos para usar que envuelven la interfaz de
Automatizacion de estos servidores. Estos componentes, disponibles en la ficha
Servers de la paleta, se han instalado usando la misma tecnica que mostramos en
el ultimo apartado. La ventaja real tiene que ver con la tecnica de crear componentes que recubran a 10s servidores de Automatizacion existentes, en lugar de la
disponibilidad de unos componentes servidores predefinidos. Hay que tener tambien en cuenta que 10s componentes de Office tienen distintas versiones segun la
version del paquete de Microsoft instalado: todos 10s componentes se instalaran,
per0 solo se registra un conjunto en tiempo de diseiio, de acuerdo con la eleccion
realizada en el programa de instalacion de Delphi. Se puede modificar esta configuracion mas tarde, eliminando el paquete del componente relacionado y aiiadiendo uno nuevo.
No vamos a ver ningun ejemplo real en esta seccion porque es dificil escribir
un programa que funcione con todas las distintas versiones de Microsoft Office.
El componente Container
Para crear una aplicacion contenedor COM en Delphi, hay que colocar un
componente Olecontainer en un formulario. A continuacion, hay que seleccionarlo y hacer clic con el boton derecho para activar su menu contextual, que
incluira una orden Insert Object. Al seleccionar esa orden, Delphi presenta el
cuadro de dialogo estandar Insert Object, que permite elegir entre una de las
aplicaciones servidor registradas en el ordenador.
Una vez insertado el objeto COM en el contenedor, el menu local del componente del contenedor mostrara varios elementos de menu personalizados que incluyen ordenes para cambiar las propiedades del objeto COM, insertar otro,
copiarlo o eliminarlo. La lista contiene tambien 10s verbos o acciones del objeto
(como Edit, Open o Play). Despues de insertar un objeto COM en el contenedor,
el servidor correspondiente se pondra en marcha para permitir la edicion del
nuevo objeto. En cuanto se cierre la aplicacion servidor, Delphi actualizara el
objeto en el contenedor y lo mostrara en tiempo de diseiio en el formulario de la
aplicacion Delphi en desarrollo.
Si observamos la descripcion textual de un formulario que contenga un componente con un objeto dentro, se puede ver una propiedad Data,que contiene 10s
datos reales del objeto COM. Aunque el programa cliente almacene 10s datos del
objeto, no sabe como controlarlo y mostrarlo sin la ayuda del servidor apropiado
(que debe estar disponible en el ordenador en que se ejecute el programa). Esto
indica que el objeto esta incrustado.
Para soportar totalmente documentos compuestos, el programa deberia proporcionar un menu y una barra de herramientas o un panel. Estos componentes
adicionales son importantes porque la edicion en el sitio supone una combinacion
de las interfaces del usuario del programa cliente y el programa servidor. Cuando
se active el objeto COM colocado, algunos de 10s menus desplegables pertenecientes a la barra de menu de la aplicacion servidor se incorporaran a la barra de
menu de la aplicacion contenedor.
La combinacion de menus es casi automatica en Delphi. Solamente hay que
definir 10s indices adecuados para 10s elementos de menu del contenedor, utilizando la propiedad GroupIndex.Cualquier elemento de menu con un numero de
indice impar se sustituira por el elemento correspondiente del objeto OLE activo.
Mas especificamente, 10s menus desplegables File (0)y Window (4) pertenecen
a la aplicacion contenedor. Los menus desplegables Edit (A), View (3) y Help
(5) (0 10s grupos de menus desplegables con esos indices) son capturados por el
servidor COM. Se puede utilizar un sexto grupo llamado Object (2) para mostrar
otro menu desplegable mas entre 10s grupos Edit y View, cuando el objeto COM
este activo. El programa de demostracion OleCont que hemos escrito para demostrar estas caracteristicas permite a1 usuario crear un nuevo objeto llamando a1
metodo InsertObjectDialog de la clase Tolecontainer.
Despues de haber creado el objeto, podemos ejecutar su verbo principal utilizando el metodo DoVerb.El programa muestra tambien una pequeiia barra de
herramientas con algunos botones de mapas de bits. Hemos colocado algunos
componentes Tw inContro 1 en el formulario para asi permitir a1 usuario seleccionarlos y desactivar el Olecontainer.
Para mantener esta barra de herramientas o panel visible durante la edicion in
situ, hay que definir su propiedad Locked como True,lo que obliga a1 panel a
estar presente en la aplicacion y a que no lo sustituya una barra de herramientas
del servidor.
Para mostrar lo quc sucede a1 utilizar este metodo, hemos aiiadido a1 programa
un segundo panel con algunos botones mas. Dado que no hemos definido su propicdad Locked,esta nueva barra de herramientas sera reemplazada por la del
servidor activo. Cuando la edicion in situ ponga en marcha una aplicacion servidor que muestre una barra de herramientas, la del servidor reemplazara a la del
contenedor, como mucstra la parte inferior de la figura 12.6.
lmagen Cclores A y d a
Otra forma de crear un objeto COM es usar el metodo PastespecialDialog. a1 que llama el controlador del evento PasteSpeciallClick del
ejemplo. Otro cuadro de dialog0 estandar COM, envuelto en una funcion Delphi,
es el que muestra las propiedades del objeto, que se activa con el elemento Object
Properties del menu desplegable Edit llamando a1 metodo Object PropertiesDialog del cornponente de OleContainer.
La illtima caracteristica del programa OleCont es el soporte para archivos.
Este es uno de 10s aiiadidos mas sencillos que se pueden realizar, porque el componente del contenedor OLE ya ofrece soporte para archivos.
var
Document, Paragraph: Variant;
begin
// a c t i v a si no e s t d funcionando
i f n o t (OleContainerl.State = osRunning)
OleContainer1.Run;
// o b t i e n e e l d o c u m e n t o
Document := OleContainer1.01eObject;
// a d a d e p d r r a f o s , o b t e n i e n d o e l u l t i m o
Document.Paragraphs.Add;
Paragraph : = Document.Paragraphs.Add;
// a d a d e t e x t o a 1 p d r r a f o , u s a n d o tamado
Paragraph-Range. Font .Size : = 10 + Random
Paragraph.Range.Text : = ' N e w t e x t ( ' +
IntToStr (Paragraph.Range. Font. Size)
end ;
then
de fuente aleatorio
(20);
+ ' ) '#13;
Controles ActiveX
Visual Basic de Microsoft fue el primer entorno de desarrollo de programas en
presentar la idea de ofrecer componentes software a1 gran mercado, incluso aun-
Figura 12.7. El ejernplo WordCont muestra como usar Autornatizacion con un objeto
incrustado.
__P
11-
compilaciones) del control ActiveX, pueden surgir algunos problemas de compatibilidad. Una ventaja de tener un archivo ejecutable autocontenido es que tambien ofrece menos problemas de instalacion.
La desventaja de usar componentes Delphi no es que haya menos componentes
Delphi que controles ActiveX, sino que si se compra un componente Delphi, solo
se puede usar en Delphi y Borland C++ Builder, por otra parte, si se compra un
control ActivcX. se puede usar en multiples entornos de desarrollo de muchos
fabricantes. Aim asi, si se desarrolla basicamente en Delphi y se encuentran dos
componentes similares basados en las dos tecnologias, lo mas recomendable es
adquirir el componente Delphi (se integrara mas con el entorno y sera por ello
mas facil de usar). Ademas, el componente Delphi nativo estara probablemente
mcjor documentado (desde el punto de vista de Delphi) y aprovechara Delphi y
sus caracteristicas del lenguaje que no estan disponiblcs en la interfaz general de
ActiveX, que tradicionalmente se basa en C y C++.
.-
P
1
_,
-*"-
<-
.-
-P
4. Hacemos clic sobre cl boton Install para afiadir esta nueva unidad a un
paquete Delphi y a la Component Palette.
Login
Code Central
Qual~tyCentral
The Coad Letter
Get Published
BOOKS
Developer Support
Shop
Chat
Downloads
Search
Logm
Soapbox
SIPfrom the Ftrehose
Dawd lnterslmone
Behtnd the Screen
-I
Figura 12.8. El programa WebDemo despues de escoger una pagina muy conocida
TForml.GotoPage(ReqUr1:
WebBrowserl.Navigate
EmptyParam,
EmptyParam);
(ReqUrl,
string);
EmptyParam,
EmptyParam,
end;
Empty Param es una OleVar iant predefinida que se puede utilizar siempre que haya que pasar un valor predefinido como parametro de referencia. Es un
metodo abreviado muy util que se puede emplear para evitar crear una variable
o l e v a r i a n t vacia cada vez que haga falta un p a r h e t r o similar. El programa
llama a1 metodo Goto Page cuando el usuario hace clic sobre el boton Open File,
o cuando pulsa la tecla Intro mientras que se encuentra en el cuadro combinado o
cuando hace clic sobre el boton Go, como puede verse en el codigo fuente del
ejemplo. El programa controla tambien cuatro eventos pertenecientes al control
WebBrowser. Cuando termina la operacion de descarga, el programa actualiza el
texto de la barra de estado y tambien la lista desplegable del cuadro combinado:
procedure TForml.WebBrowserlDown10adComp1ete(Sender: TObject);
var
NewUrl : string;
begin
StatusBarl. Panels [0] .Text : = ' D o n e ' ;
// a d a d e URL a 1 c u a d r o c o m b i n a d o
NewUrl : = WebBrowserl.LocationURL;
if (NewUrl <> ' ' ) and (ComboURL.Items. IndexOf (NewUrl) < 0 )
then
ComboURL.Items.Add (NewUrl);
end;
Otros dos eventos utiles son O n T i t l e c h a n g e , usado para actualizar el titulo de la ventana del programa con el del documento HTML y el evento
OnS t a t usTex t Change, utilizado para actualizar la segunda parte de la barra
de estado.
Este codigo basicamente duplica la informacion que muestran en la primera
parte de la barra de estado 10s dos controladores de evento anteriores:
2wlmpl2 pas
En este asistente simplemente hay que seleccionar la clase VCL que nos interesa, personalizar 10s nombres que aparecen en 10s cuadros de texto y hacer clic
&lax
Helper
OK
En el cuadro combinado hay que elegir entre una nueva propiedad, metodo o
evento. En el cuadro de edicion podremos escribir entonces la declaracion del
nuevo elemento de la interfaz. Si esta activada la casilla de verificacion Syntax
Helper, aparecera una sugerencia que describe lo que hay que escribir y resalta
10s errores. A1 definir un nuevo elemento de interfaz ActiveX, hay que tener en
cuenta que estamos limitados a 10s tipos de datos COM.
En el ejemplo XArrow hemos aiiadido dos propiedades a1 control ActiveX.
Dado que las propiedades P e n y B r u s h del componente original no son accesibles, hemos hecho accesible su color. Veamos 10s ejemplos que se pueden escribir
en el cuadro de edicion del cuadro de dialogo (ejecutandolo dos veces):
property F i l l c o l o r : I n t e g e r ;
property P e n c o l o r : I n t e g e r ;
. .
--
-.-
- 7
--2
-L
.--.-..-
---
.. -.a
,-."L.-2
..-
- - .
begin
ShapePen.Brush.Co1or : = Color;
Modified; / / a c t i v a el boton A p p l y
end ;
end;
end :
. . .
#. . . .
. . . . . . . . .'. . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . .
Direction. adR~ghf(3)
II
11
.....................................
......................................
.....................................
......................................
.......................................
......................................
.......................................
.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
Pencolor:
h o w point color:
New...
NW.
TRUCO: Otra sugerencia tiene que ver con el Capt ion del formulario de
la ficha de propiedades. Este se utiIizara en el cuadro de diiilogo de propiedades del entorno anfitrion como titulo de la solapa correspondiente a la
ficha de propiedades.
El siguiente paso es asociar 10s controles de la ficha de propiedades a las
propiedades reales del control ActiveX. La clase de la ficha de propiedades cumta automaticamente con dos metodos para ello: UpdateOleOb ject y
Updat ePropert y Page.Como sus nombres sugieren, ambos metodos copian
ActiveForms
Delphi proporciona una alternativa a1 uso del asistente ActiveX Control Wizard
para generar un control ActiveX. Podemos utilizar un ActiveForm que es un
control ActiveX basado en un formulario y que puede albergar uno o mas componentes de Delphi. Esta tecnica se usa en Visual Basic para crear nuevos controles
y tiene sentido si se quiere crear un componente compuesto.
En el ejemplo XClock, hemos colocado en un ActiveForm una etiqueta (un
control grafico que no puede usarse como punto de partida para un control ActiveX)
y un temporizador, y hemos conectado 10s dos con algo de codigo. El formulariol
control se convierte en un contenedor de otros controles, lo que facilita crear
componentes compuestos (es mas facil que para un componente compuesto VCL).
Para crear un control de este tipo, hay que seleccionar el icono ActiveForm de
la ficha ActiveX del cuadro de dialogo File>New. Delphi pedira informacion en
el cuadro de dialogo ActiveForm Wizard, que es similar a la asistente visto
anteriormente.
lnterioridades de ActiveForm
Antes de continuar con el ejemplo, examinaremos el codigo que genera el
ActiveForm Wizard. La principal diferencia con respecto a un simple formulario
de Delphi esta en la declaracion de la nueva clase formulario, que hereda de la
clase denominada TAct i v e Form e instala una interfaz ActiveForm especifica.
El codigo generado para la clase del formulario activo implementa unos cuantos
Cada evento se proyecta a si mismo sobre el evento del ActiveX externo, como
en el siguiente metodo:
procedure TAXForml.ActivateEvent(Sender: TObject);
begin
if FEvents <> nil then FEvents .OnActivate;
end;
3 . Compilaremos esta biblioteca, la registramos y la instalaremos en un paquete para probarla en el entorno Delphi.
Observese el efecto del borde en relieve. Esto esta controlado por la propiedad
AxBorderStyle del formulario activo, una de las pocas propiedades de 10s
- -
3 LKto
rrqJ M K
Figura 12.10. El control XClock en la pagina HTML de muestra.
Hay que fijarse en que en la parte del archivo HTML que se refiere a1 control
se puede usar la etiqueta especial param para personalizar las propiedades del
control. Por ejemplo, en el archivo HTML del control XArrow, hemos modificad o el a r c h i v o H T M L generado automaticamente (en el archivo
XArrowCus t .htm) con estas tres etiquetas param:
Ademas de crear servidores basicos COM, Delphi tambien permite crear objetos COM mejorados, incluyendo objetos sin estado y soporte de transacciones.
Este tipo de objeto COM fue presentado por Microsoft con las siglas MTS
(Microsoft Transaction Sewer) en Windows NT y 98 y renombrado posteriormente como COM+ en Windows 2000lXP. (Aqui hablaremos de COM+, per0 es
lo mismo.) Delphi soporta la creacion de objetos estandar sin estado y modulos de
datos remotos DataSnap basados en objetos sin estado. En ambos casos, empezaremos el desarrollo utilizando uno de 10s asistentes disponibles de Delphi, usando
para ello el cuadro de dialog0 New Items y seleccionando el icono Transactional
Object de la ficha ActiveX o el icono Transactional Data Module de la ficha
Multitier. Estos objetos se deben afiadir a un proyecto de biblioteca ActiveX, no a
una aplicacion base. El icono COM+ Event Object se utiliza para soportar 10s
eventos COM+. COM+ ofrece un entorno en tiempo de ejecucion que soporta
servicios de transaccion de bases de datos, seguridad, reserva de recursos y una
mejora global en la robustez de las aplicaciones DCOM. El entorno en tiempo de
ejecucion se encarga de manejar objetos llamados componentes COM+. Se trata
de objetos COM guardados en un servidor en proceso (es decir, una DLL). Mientras que otros objetos COM se ejecutan directamente en la aplicacion cliente, 10s
objetos COM+ se manejan en este entorno en tiempo de ejecucion, en el que se
instalan las bibliotecas COM+. Los objetos COM+ deben soportar interfaces
COM especificas, comenzando por IObjectControl, que es la interfaz base (como
IUnknown para un objeto COM).
Antes de entrar en detalles demasiado tecnicos y de bajo nivel, consideremos
COM+ desde una perspectiva distinta: las ventajas de este enfoque. COM+ proporciona unas cuantas caracteristicas interesantes:
Seguridad basada en funciones: La funcion asignada a un cliente determina si este tiene el derecho de acceso a una interfaz o a un modulo de
datos.
Recursos de bases de datos reducidos: Se puede reducir el numero de
conexiones a una base de datos, ya que 10s registros de la capa intermedia
se conectan a1 servidor y utilizan las mismas conexiones para varios clientes (aunque no es posible tener mas clientes conectados que licencias para
el servidor).
Transacciones de bases de datos: El soporte COM+ de transacciones
incluye operaciones en bases de datos multiples, aunque pocos servidores
SQL, ademas de 10s de Microsoft, soportan transacciones COM+.
Figura 12.11. El cuadro de dialogo New Transactional Object, utilizado para crear un
objeto COM+.
El cuadro de dialogo New Transactional Object permite escribir el nombre para la clase del objeto COM+, el modelo de hilos (ya que COM+
serializa todas las peticiones, Single o Apartment valdran perfectamente) y
un modelo transaccional:
Requires a Transaction: Indica que cada llamada del cliente a1 servidor es considerada como una transaccion (a menos que el remitente
proporcione un contexto existente de transaccion).
Requires a New Transaction: Indica que cada llamada es considerada
como una nueva transaccion.
Supports Transactions: Indica que el cliente debe proporcionar explicitamente un contexto de transaccion.
Does Not Support Transaction: (La seleccion por defecto, y la que
hemos utilizado). Indica que el modulo de datos remoto no participara
en ninguna transaccion. Esta opcion impide que el objeto se active si el
cliente que llama tiene una transaccion.
Ignores Transactions: Indica que el objeto no participa en transacciones, sin0 que puede usarse sin tener en cuenta si el cliente tiene una
transaccion.
3 . Cuando cerramos este dialogo, Delphi aiiade una biblioteca de tipos y una
unidad de irnplementacion a1 proyecto y abre el editor de la biblioteca de
tipos; donde se puede definir la interfaz del nuevo objeto COM. Para este
ejemplo, hemos aiiadido una propiedad entera Value, un metodo
Increase que tiene como parametro una cantidad y un metodo AsText
que devuelve un Widestring con el valor formateado.
4. Cuando aceptamos las ediciones en el editor de la biblioteca de tipos (haciendo clic sobre el boton Refresh o cerrando la ventana), Delphi muestra
el asistente Implementation File Update Wizard, per0 solo si se ha activado la opcion Display updates before refreshing de la pagina Type
Library en el cuadro de dialogo Environment Options. Este asistente
pedira confirrnacion antes de aiiadir cuatro metodos a la clase, incluyendo
10s metodos get y set de la propiedad. Ahora se puede escribir algo de
codigo para el objeto COM, que en el ejemplo es bastante trivial.
Una vez que se haya compilado una biblioteca ActiveX o COM, que alberga
un componente COM+, se puede usar la herramienta administrativa Servicios de
componentes (que se muestra en la Microsoft Management Console, o MMC)
para instalar y configurar el componente COM+. Aun mejor, se puede usar el IDE
de Delphi para instalar el componente COM+ mediante la opcion de menu
Run>lnstall COM+ Object. En el cuadro de dialogo que aparecera, se puede
seleccionar el componente a instalar (una biblioteca puede contener varios componentes) y seleccionar la aplicacion COM+ en que se desea instalar el componente.
Una aplicacion COM+ no es nada mas que una manera de agrupar componentes COM+; no se trata de un programa ni nada parecido (lo que hace que no quede
claro por que se le llama aplicacion). Por eso, en el cuadro de dialogo Install
COM+ Object, se puede escoger una aplicacion/grupo existente, seleccionar la
pagina Install Into New Application, y escribir un nombre y una descripcion.
I~
kch
?' 1 ~
vu
>:
Ventma
* : 1
-~~
+
"
,-
-*
~ ddepoqdma_.
.
.-
--
. CLSID- .... . .
--
.~-
-ompansnt
. . Transat& 1
Nose ad ...
...
md.L
5 y f . o ~ .~Moddo..,
Subpro. ..
Necesar~o
Apkadonc. COM+
l
i
&, .wrLnmie5
[i
i
3
COM+ Expbrcr
COM+ LnY~es
7 (ck~
+AcCorrponcr*r
:-.
a CmFlurl .ComFi
i
,
cTe5
*. 'J
--
a ~ n r n ~ l u sCmR
l . ... (3E93EF3-CE24-4
rnK
:+<
I
. 4-..
- -
ompansntompansnt~--.-
&I am
lo>=,my E
J
Interkc5
1susmpoone
--
. ---
7
-
Figura 12.12. El componente COM+ recien instalado en una aplicacion COM+ (tal y
como lo muestra la herramienta Servicios de componente de Microsoft).
I
I
-. -
-=
---- - -
Hemos creado un programa cliente para el objeto COM+, per0 es igual que
cualquier otro cliente COM de Delphi. Despues de importar la biblioteca de tipos,
que se registra automaticamente mientras se instala el componente, hemos creado
una variable de tip0 de interfaz que hace referencia a ella y llamada por sus
metodos como es habitual.
Eventos COM+
Las aplicaciones de cliente que utilizan objetos COM tradicionales y servidores de Automatizacion pueden llamar a 10s metodos de esos servidores, pero no es
una forma eficiente para comprobar si el servidor ha actualizado 10s datos para el
cliente. Debido a esto, un cliente puede definir un objeto COM que implemente
una interfaz de retrollamada, pasar este objeto a1 servidor y permitir que este lo
llame. Los eventos tradicionales de COM que utilizan la interfaz IConnectionPoint, son simplificados por Delphi para 10s objetos Automation, pero aun asi, su
manipulacion es bastante compleja.
COM+ introduce un modelo de evento simplificado en el cual 10s eventos son
componentes COM+ y el entorno COM+ gestiona las conexiones. En las
retrollamadas tradicionales de COM, el objeto de servidor tiene que hacer el
seguimiento de todos 10s clientes a 10s que se notifica, algo que Delphi no ofrece
de manera automatica (el codigo de evento de Delphi se encuentra limitado a un
unico cliente). Para soportar las retrollamadas de COM para multiples clientes es
necesario aiiadir el codigo para guardar las referencias a cada uno de 10s clientes.
En COM+, el servidor llama a una simple interfaz de evento y el entorno COM+
remite el evento a todos 10s clientes que hayan expresado interes en el. Asi el
cliente y el servidor estan menos acoplados, haciendo posible que un cliente reciba notificacion de diferentes servidores sin cambio alguno en su codigo.
-
--.
- - --- -
--
--
.-
NOTA: Algunos criticos dicen que Microsoft introdujo este modelo s6lo
porque era dificil para 10s desarrolladores de Visual Basic gestionar eventos COM del mod0 tradicional. Windows 2000 proporcionaba unas cuantas
caracteristicasnuevas especificamente pensadas para estos desarrolladores.
Para crear un evento COM+, deberiamos crear una biblioteca COM (o biblioteca ActiveX) y utilizar el asistente COM+ Event Object. El proyecto resultante
contendra una biblioteca de tipos con la definicion de la interfaz utilizada para
activar 10s eventos ademas de algo de codigo de implementacion falso. El servidor
que reciba la notificacion de 10s eventos proporcionara la implementacion de la
interfaz. El codigo falso se encuentra ahi solo para soportar el sistema de registro
COM de Delphi.
Mientras construiamos la biblioteca MdComEvents, hemos aiiadido a la biblioteca de tipos un metodo sencillo con dos parametros, que han dado lugar a1
siguiente codigo (en el archivo de definicion de la interfaz):
type
IMdInf orm = interface (IDispatch)
[ ' (202D2CC8-8E6C-4E96-9C14-1FAAE392OECC}' ]
p r o c e d u r e Informs(Code: Integer; const Message:
Widestring); safecall;
end;
2. En el asistente para instalacion de componentes COM+, hacemos clic sobre el boton Instalar nuevas clases de eventos y seleccionamos la biblioteca que acabamos de compilar. La definicion del evento COM+ se instalara
automaticamente.
Para comprobar si funciona, tendremos que crear una implementacion de esta
interfaz de evento y un cliente que la invoque. La implementacion puede aiiadirse
a otra biblioteca ActiveX, albergando un objeto COM basico. Dentro del COM
Object Wizard de Delphi, podemos seleccionar la interfaz a implementar, escogiendola de la lista que aparece a1 hacer clic sobre el boton List.
La biblioteca resultante, que en el ejemplo se llama EvtSubscriber, expone
un objeto de Autornatizacion a un objeto COM que implemente la interfaz
IDispatch (que es obligatorio para 10s eventos COM+). El objeto tiene la siguiente definicion y codigo:
type
TInformSubscriber = class (TAutoObject, IMdInf orm)
protected
procedure Informs (Code: Integer; const Message:
WideString) ; safecall;
end;
procedure TInformSubscriber.Informs(Code: Integer; const
Message: WideString);
begin
ShowMessage ('Mensaje <' + IntoToStr (Code) + I > : ' +
Message) ;
end;
kchm
+*
Prodn
AWa
\&a
Ver
-@-I
Bl5 5
J Ralr dr r ~ n s d a
- @j
Scrvms de componentes
- JEQUIWS
- gMPc
_1 A p k a c m z COM+
.+:
El UYes
6 COM+ Explaer
d @ COM+ QC Dead Lettn C
+
$ cm+
.
I Scmdu-
4&
&ma
r1ptk.a;
Mlke5
La M a & Debh 7 (der
O COmpanenteS
8-
a cmPCI51c a u
+
*I
- _.
Id dcntdaz
@$
'
JLnterfaer
- -- - - - - -
17
Finalmente, podemos centrarnos en la aplicacion que lanza el evento, que hemos llamado Publisher (ya que publica la informacion en la que estan interesados
otros objetos COM). Este es el paso mas simple del proceso, porque se trata de un
sencillo cliente COM que usa el servidor de eventos. Despues de importar la
biblioteca de tipos de eventos COM+, se puede aiiadir a1 codigo de publicacion de
esta manera:
var
Inform: IMdInform:
begin
Inform : = CoMdInform.Create;
Inform. Informs (20, Editl .Text) ;
Para hacer las cosas mas interesantes, se puede suscribir dos veces el mismo
servidor a la interfaz de eventos. El efecto global es que sin retocar el codigo del
cliente se conseguiran dos cuadros de mensaje, uno por cada servidor suscrito.
Obviamente este efecto pasa a ser interesante cuando se tiene multiples componentes COM distintos que pueden controlar el evento, ya que se pueden habilitar
1;
5
..
,(
NetLibrary
uses
NetNurnberClass in
'NetNumberClass.pasl;
begin
/ / c r e a u n o b j e t o p a r a e n l a z a r t o d o e l codigo
TNumber.Create;
end.
Created
Created
Created
Created
D:\rnd7code\l2\NetImport\mscorlib~TLBBdcr
D:\md7code\12\NetImport\mscorlib~TLBBpas
D:\md7code\12\NetImport\NetLibrary~TLBBdcr
D:\rnd7code\l2\NetImport\NetLibrary~TLBBpas
El efecto es crear una unidad para la biblioteca de tipos del proyecto y una
unidad para la Microsoft .NET Core Library importada ( m s c o r l i b .d l 1).Ahora
podemos crear una nueva aplicacion de Delphi 7 (un programa Win32 estandar) y
usar 10s objetos .NET como si fueran objetos COM. Este es el codigo del ejemplo
NetImport, que se muestra en la figura 12.14:
uses
NetLibrary-TLB;
procedure TForml.btnAddClick(Sender: TObject);
var
num: INumber;
begin
num : = CoTNumber .Create as INumber;
num.Increase;
ShowMessage (IntToStr (num.GetValue)) ;
end;
=
I
Figura 12.14. El programa Netlmport usa un objeto .NET para sumar nlimeros
Parte Ill
Arquitecturas
orientadas
a bases de
datos en Delphi
Arquitectura
de bases de
datos Delphi
El soporte de Delphi para aplicaciones de bases de datos es una de las caracteristicas clave del entorno de programacion. Muchos programadores pasan la mayor parte de su tiempo escribiendo codigo de acceso a 10s datos, que necesita ser
la parte mas robusta de una aplicacion de bases de datos. En este capitulo veremos como funciona el soporte que ofrece Delphi para la programacion con bases
de datos.
Lo que no se vera aqui es una explicacion sobre la teoria del diseiio de bases de
datos. Supondremos que ya conoce 10s fundamentos de este diseiio y que ya ha
diseiiado la estructura de una base datos. No entraremos en problemas especificos de bases de datos; el objetivo es ayudar a comprender como soporta Delphi el
acceso a bases de datos.
Comenzaremos con una explicacion de las distintas alternativas que Delphi
ofrece en cuanto a acceso a datos, y despues contemplaremos una vision global de
10s componentes de bases de datos disponibles en Delphi. Este capitulo se centra
en el uso del componente TC 1i e n t D a t S e t para acceder a 10s datos locales, sin
hacer caso a1 acceso clientelservidor (comentado en otro capitulo). Tambien hablaremos sobre la clase T D a t a S e t , analizaremos en profundidad los componentes T F i e l d y el uso de 10s controles data-aware.
Finalmente, hay que tener en cuenta que casi todo lo comentado en este capitulo se podra aplicar a diversas plataformas. En particular, 10s ejemplos pueden
Como una solucion mas, para las aplicaciones simples puede usarse el componente ClientDataSet de Delphi, que permite guardar tablas en archivos locales
(algo que Borland llama MyBase). Fijese en que una tipica aplicacion de Delphi
basada en tablas Paradox no puede adaptarse a Kylix, debido a su carencia de
BDE.
La biblioteca dbExpress
Una de las caracteristicas mas importantes nuevas en Delphi en 10s aiios mas
recientes es la introduccion de la biblioteca de la base de datos dbExpress (DBX),
disponible tanto para Linux como para Windows. Se trata de una biblioteca y no
de un motor de bases de datos porque, a1 contrario que otras soluciones, dbExpress
utiliza un enfoque ligero y basicamente no necesita ninguna configuration en las
maquinas de 10s usuarios finales.
Las caracteristicas claves de dbExpress y las razones por las que Borland la
ha introducido, junto con el desarrollo del proyecto Kylix son su ligereza y
portabilidad. En comparacion con otras bases de datos muy potentes, dbExpress
resulta algo limitada en cuanto a sus capacidades.
dbEspress solo puede acceder a servidores SQL (no a archivos locales); no
tiene capacidad para guardar copias temporales para acelerar 10s procesos y solo
proporciona un acceso a datos unidireccional. Solo puede trabajar originariamente con consultas SQL y no puede crear las sentencias de actualizacion SQL correspondientes.
La primera impresion que producen estas limitaciones es que la biblioteca
podria resultar inutil. Nada mas lejos de la realidad: se trata de caracteristicas
que la hacen interesante. Los con.juntos de datos unidireccionales sin actualizacion directa son lo normal si se necesita generar informes, incluyendo la generacion de paginas HTML que muestren el contenido de una base de datos. Sin
embargo, si se desea construir una interfaz de usuario para editar 10s datos, Delphi
incluye componentes especificos (ClientDataSet y Provider, para ser precisos)
que permiten la edicion en cache y la resolucion de consultas SQL.
Estos componentes permiten que la aplicacion basada en dbExpress tenga un
mayor control del que podemos tener con un motor de base de datos independiente
(monolitico), que realiza automaticamente acciones adicionales de un mod0 no
personalizable.
dbEspress permite escribir una aplicacion que, escepto 10s problemas derivados de 10s diversos dialectos SQL, puede acceder a muchos motores de bases de
datos distintos sin realizar mucha modification del codigo. Entre 10s servidores
SQL soportados en Delphi 7 se incluye la propia base de datos InterBase de
Borland, el servidor de bases de datos de Oracle, la base de datos MySQL (que es
muy popular en Linux), Informix, DB2 de IBM, y SQL Server de Microsoft. En
este capitulo vamos a centrarnos en las bases de la arquitectura de las bases de
datos.
Se puede considerar el uso de IBX (u otro conjunto de componentes comparable) si se esta seguro de que no se cambiara la base de datos y se quiere conseguir
el mejor rendimiento y control posible a costa de la flexibilidad y la transportabilidad. La parte negativa es que el rendimiento adicional y el control conseguidos pueden ser limitados. Tambien habra que aprender a utilizar otro conjunto de
componentes con un comportamiento especifico, en lugar de aprender a utilizar
un motor generic0 y aplicar ese conocimiento a distintas situaciones.
sentar un problema, puesto que el motor forma parte de las versiones recientes de
Windows. Sin embargo. la compatibilidad limitada entre versiones de ADO nos
obligara a 10s usuarios a actualizar sus ordenadores segun la version con la que se
creo el programa. El gran tamaiio de la instalacion de MDAC (M~crosoftData
Access Componentes), que actualiza grandes p a t e s del sistema operativo, hace
que esta tarea no resulte nada sencilla.
ADO ofrece ventajas concretas si queremos usar un servidor Access o SQL,
puesto que 10s controladores de Microsoft para sus propias bases de datos poseen
una calidad superior a 10s proveedores promedio de OLE DB. Para las bases de
datos Access. sobre todo, utilizar 10s componentes ADO de Delphi es una buena
solution. Pero si estamos pensando en usar otros servidores SQL, primer0 tendremos que asegurarnos de que haya disponibles controladores de buena calidad.
ADO es muy potente, pero hay que aprender a convivir con el, porque esta en
medio de nuestro programa y la base de datos, ofreciendo servicios pero tambiin
ocasionalmente dando ordenes diferentes a las esperadas. Por otra parte, tambien
hay algo negativo. no se puede ni siquiera pensar en usar ADO si planeamos
realizar un futuro desarrollo multiplataforma: esta tecnologia especifica de
Microsoft no esta disponible en Linux ni en otros sistemas operativos.
So10 cs recomendable usar ADO si solo te tiene pensado trabajar con Windows?
sc quierc usar Access LI otras bases de datos de Microsoft, o se encuentra un buen
provcedor de OLE DB para cada uno de 10s servidores de bases de datos con que
se tiene planeado trabajar (por el momento. esto escluye a InterBase y muchos
otros servidores SQL).
Los componcntes ADO (partc de un paquete Borland denominado dbGo) estan
agrupados en la ficha ADO de la Component Palette. Los tres componentes
principales son ADOConncction (para concxion a bases de datos), ADOCommand
(para ejccutar ordenes SQL) y ADODataSet (para ejecutar peticiones que devuelven un conjunto de resultado). Tambien hay tres componentes de compatibilidad
(ADOTable, ADOQuery y ADOStoredProc) que podemos usar para modificar
aplicaciones basadas en el BDE en ADO. Por tiltimo, esta el componente
RDSConncction, para acceder a datos en aplicaciones multicapa remotas.
ruts
.,mm
..
.-
sobre un archivo local. La proyeccion de este archivo local es distinta de la proyeccion de datos tradicionales sobre un archivo local. El enfoque tradicional consiste en leer desde el archivo un registro cada vez y posiblemente disponer de un
segundo archivo que almaccnc 10s indices. El client DataSet proyecta toda
una tabla (y posiblemente una estructura maestroldetalle) sobre el archivo por
completo: cuando se inicia un programa. sc carga en mcmoria el archivo completo
y se guarda todo al mismo tiempo.
'lr'
I 1
.?.
Activecontrol = DBGridl
Caption = ' M y B a s e l '
OnCreate = Formcreate
o b j e c t DBGrid: TDBGrid
DataSource = DataSourcel
end
o b j e c t DataSourcel: TDataSource
DataSet cds
end
o b j e c t cds: TClientDataSet
FileName = ' C: \Archives d e p r o g r a m \Archives
Comunes \ B o r l a n d
Shared\Da t a \ C u s t o m e r . c d s '
end
end
1510 Ocean Pi
1551 M m l D
1624 MakaiSC
1645 Action Ck
1651 Jamaica L
,,1680 l s l d Finders
1984 Adventure Undersea
2118 Blue Spalo Ckrb
2135 Frank's Divers Supply
,.,
d
l
Figura 13.1. Una tabla local de rnuestra activa en tiempo de disefio dentro del IDE de
Delphi.
(Sender: T O b j e c t ) ;
ADVERTENCIA: La bibliotecamidas .dl1 no tiene un numero de version en sn nambre. Por eso, si un ordenador tiene una version mhs antigua,
tarse correctamente.
- 7
La ultima sentencia incluye una llamada a S t r i n g R e-p l a c e como una especie de formateo XML para pobres: el c6digo aiiade una nueva linea a1 final.de
cada etiqueta XML afiadiendo una nueva liiea tras la marca de cierre. La figura
13.2 muestra la representacion XML con unos cuantos registros.
-
<PARAMS/)
</METADATA>
<ROWDATA>
<ROW om-"one" Iwo="lU/>
<ROW one.'koMIwo="2"/>
<ROWm="ne"lwa-"lV'l,
</ROWDATA)
t/DATAPACMT>
lndexado
Una vez que se ticne un C l i e n t D a t a S e t en mcmoria, se pueden realizar
muchas operaciones sobre el. Las mas simples son el indexado, filtrado y busqueda de registros; entre las operaciones mas complejas se incluyen la agrupacion, la
definicion de valores agregados y la gestion del registro de cambios. Dejaremos
10s temas mas compkjos para el final de capitulo.
Indexar un C l i e n t D a t a S e t es una cuestion de establecer la propiedad
I n d e x F i e l d N a m e s . Suele hacerse cuando el usuario hace clic sobre el campo
de titulo en un componente DBGrid (con lo que se lanza el evento O n T i t l e c l i c k ) , como en el ejemplo MyBase2:
procedure
begin
TForml.DbGridlTitleClic(Colurnn:
TColumn);
cds.IndexFieldNames : = Column.Field.FieldName;
end;
.r
v a a v u J
Filtrado
Al igual que con cualquier otro conjunto de datos, podemos usar la propiedad
Filter para especificar la inclusion en el conjunto de datos de partes de 10s
datos a las que esta ligado el componente. La operation de filtrado ocupa memoria despues de cargar todos 10s registros, asi que se trata de una manera de mostrar menos datos al usuario, no de limitar la ocupacion de memoria de un conjunto
de datos local grande.
Cuando queremos conseguir una gran cantidad de datos desde un servidor (en
una arquitectura clientelservidor) deberiamos usar una consulta adecuada de mod0
que no recuperemos un gran conjunto de datos de un servidor SQL. La mejor
opcion deberia ser normalmente el filtrado desde la salida del servidor. Con 10s
datos locales, se puede tener en cuenta el partir un gran numero de registros en un
conjunto de distintos archivos, para que se puedan cargar solo 10s necesarios y no
todos.
Sin embargo, el filtrado local en el ClientDataSet puede resultar muy
util, sobre todo porque las espresiones de filtro que podemos usar con este componente son mucho mas amplias que aquellas que podemos usar con otros conjuntos dc datos. En concreto, podemos usar lo siguiente:
La comparacion estandar y 10s operadores Iogicos ( ~ o p 1uation > 1000
and Area < 1000).
Operadores aritmeticos ( Population / Area < 10 ) .
Funciones de cadena (Substring(Last-Name, 1, 2 )
Ca ' )
Busqueda de registros
El filtrado permite limitar 10s registros que se muestran a1 usuario del programa, per0 muchas veces se querran mostrar todos 10s registros y acceder unicamente a uno especifico. El metodo Locate se encarga de esto. Si jamas se ha
usado Locate, un primer vistazo a1 archivo de ayuda no dejara las cosas muy
claras. La idea es que hay que proporcionar una lista de 10s campos que se quieren
buscar y una lista de valores, uno para cada campo. Si se desea buscar una
correspondencia con un unico campo, el valor se pasa directamente. como en este
caso en que la cadena de busqueda se encuentra en el componente EditName):
procedure TForml.btnLocateClick(Sender: TObject);
begin
i f not cds .Locate ( ' L a s t N a m e l, EditName. Text, [I ) then
MessageDlg ( ' " ' + EditName.Text + ' " not f o u n d ' , mtError,
[ r n b o k l , 0);
end;
Si se busca mediante varios campos, hay que pasar una matriz variante con la
lista de valores para 10s que se desea correspondencia. La matriz variante puede
crearse desde una matriz constante con la funcion VarArrayOf o a partir de la
nada mediante la llamada VarArra yCrea t e . Este es un fragment0 del codigo:
( ' LastName;FlrstName' , VarArrayOf
, [I )
cds. Locate
'Kevin' ]
( ['Cook',
Por ultimo, se puede usar el mismo metodo para buscar un registro incluso
aunque solo se conozca el principio del campo que se esta buscando. Todo lo que
hay que hacer es aiiadir el indicador l o p a r t ialKe y a1 parametro o p t i o n s
(el tercero) de la llamada a Locate.
NOTA: Usar Locate tiene sentido cuando se trabaja con una tabla local,
pero no se adapta bien a las aplicaciones cliente/servidor. En un servidor
SQL, tknicas sirnilares por parte del cliente implican llevar en primer lugar todos 10s datos a la aplicaci6n cliente (lo que es generalmente una rnala
idea) y buscar despues un registro especifico. Deberian localizarse 10s datos mediante sentencias SQL restringidas. A h se puede usar Locate despuis de obtener un conjunto de datos limitado. Por ejemplo, se puede buscar
Deshacer y Savepoint
Cuando un usuario modifica 10s datos de un componente C l i e n t Da t a Se t ,
las actualizaciones se almacenan en una zona de memoria llamada D e l t a . El
motivo de hacer un seguimiento de 10s cambios del usuario en lugar de conservar
la tabla resultante se debe a1 mod0 en que se manejan las actualizaciones en una
arquitectura clientelservidor.
En este caso, el programa no tiene que enviar toda la tabla de vuelta al servidor, sino solo una lista con 10s cambios del usuario (mediante sentencias SQL
especificas).
Ya que el componente C l i e n t D a t a S e t sigue la pista de 10s cambios, se
pueden rechazar esos cambios, eliminando entradas del delta. El componente posee un metodo U n d o L a s t C h a n g e especifico para ello. El parametro
F o l l o w c h a n g e de este metodo permite seguir la operacion deshacer (el conjunto de datos del cliente se movera a1 registro que se ha recuperado mediante la
operacion deshacer). Veamos el codigo usado para conectar un boton Undo:
procedure TForml.ButtonUndoClick(Sender: TObject);
begin
cds UndoLastChange (True);
end :
--- - .
-- -
--
'
-1
foco de entrada. Asi, se puede usar una sola barra de herrarnientas para
1
(
vafiogconjwtone datoa mostrados por un fomu1ario.16 pue pucde resultiu muy conh~lsopara el osuario si no se t i a d en cuenta;
1
1
DBText: Muestra el contenido de un campo que el usuario no puede modificar. Es un control grafico Label data-aware. Puede resultar muy util,
per0 10s usuarios pueden confundir este control con las etiquetas simples
que indican el contenido de cada control basado en campos.
DBEdit: Permite al usuario editar un campo (cambiar el valor actual)
usando un control E d i t . Ocasionalmente se podria desear inhabilitar la
edicion y utilizar un DBEdit como si se tratara de un componente DBText,
per0 resaltar el hecho de que se trata del dato procedente de la base de
datos.
DBMemo: Permite a1 usuario vcr y modificar un campo de texto amplio,
almacenado en ultimo termino en un campo de memo o BLOB (binary
large objet). Se parece al componente Memo y tiene capacidades totales de
cdicion, pero todo el texto se presenta en una unica fuente.
DBListBox: Permite la seleccion de elementos predefinidos (seleccion cerrada), pero no la entrada de texto y se puede usar para listar muchos
elementos. Por lo general, es mejor mostrar solo seis o siete elementos
aproximadamente, para evitar el uso de demasiado espacio en pantalla.
DBComboBox: Se puede usar tanto para una seleccion cerrada como para
permitir entradas del usuario. El estilo csDropDown del componente
DBComboBox permite que un usuario introduzca un nuevo valor, ademas
de seleccionar uno de 10s que hay disponibles. El componente usa tambih
una pequeiia zona del formulario porque la lista desplegable aparece solo
al solicitarla.
DBRadioGroup: Presenta botones de radio (lo que permite una unica seleccion), permite solo una seleccion cerrada y deberia de usarse solo para
El ejemplo DbAware
El ejemplo DbAware resalta el uso de un control D B R a d i o G r o u p con 10s
parametros comentados en la seccion anterior y un control D B C h e c k B o x . Este
ejemplo no es mucho mas complejo que 10s anteriores, pero tiene un formulario
con controles data-aware orientados a campo, en lugar de una cuadricula que 10s
englobe todos. La figura 13.3 muestra el formulario del ejemplo en tiempo de
diseiio.
A1 igual que en el programa MyBase2, la aplicacion define su propia estructura de tabla, mediante la propiedad de conjunto F i e l d D e f s del ClientDataSet.
La tabla 13.1 proporciona un breve resumen de 10s campos definidos.
AddRandwnOaa
-
--
*
-
41
Q'
\,
D d a S o w l -cdt
Management
-- -
-.
Figura 13.3. Los controles data-aware del ejemplo DbAware en tlempo de diseiio.
LastName
ft String
FirstName
ftString
Department
ftsmallint
Branch
ftString
Senior
ftBoolean
HireDate
ftDate
El programa tiene algo de codigo para rellenar la tabla con valores aleatorios.
Este codigo es aburrido y no demasiado complejo, por lo que no vamos a comentar 10s detalles, pero se puede analizar el codigo fuente de DbAware si se tiene
interes.
bajar con nombres de cliente. Sin embargo, en la base de datos, 10s nombres de
cliente se guardan en una tabla distinta, para evitar la duplicacion de 10s datos de
cliente por cada pedido realizado por el mismo cliente. Para solventar esta cuestion, con bases de datos locales o pequeiias tablas de busqueda, se puede usar un
control DBLookupComboBox. (Esta tecnica es dificil de adaptar bien a una
arquitectura cliente/servidor con tablas de busqueda grandes.)
El componente DBLookupComboBox se puede conectar a dos fuentes de datos
al mismo tiempo: una fuente que contenga 10s datos reales y una segunda que
contenga 10s datos que se muestran. Basicamente, hemos creado un formulario
estandar que usa el archivo o r d e r s . c d s de la carpeta de datos de muestra de
Delphi; el formulario i n c h ye varios controles DBEd i t .
Deberiamos eliminar el componente D B E d i t estandar conectado al numero
de cliente y sustituirlo por un componente DBLookupComboBox (y un componente DBText para entender lo que ocurre exactamente). El componente de busqueda (y DBText) esta conectado con el componente D a t a S o u r c e para el pedido
y con el campo CustNo. Para permitir que el componente de busqueda muestre la
informacion extraida de otro archivo ( c u s t o m e r . c d s ) , es necesario aiiadir
otro componente C l i e n t D a t a S e t que haga referencia a ese archivo, junto con
una nueva fuente de datos. Para que funcione el programa, es necesario configurar algunas propiedades del componente DBLoo kupComboBoxl. Veamos una
lista de valores necesarios:
object DBLookupComboBoxl: TDBLookupComboBox
DataField = ' C u stNo'
DataSource = Datasourceorders
KeyField = ' Cus tNo'
ListField = 'Company;CustNo'
Listsource = DataSourceCustomer
DropDownWidth = 300
end
Las primeras dos propiedades establecen la conexion principal, como es habitual. Las otras cuatro propiedades determinan el campo usado para la union de
10s datos (Key F i e l d ) , la informacion que se mostrara ( L i s t F i e l d ) y la fuente secundaria ( L i s t S o u r c e ) . Ademas de escribir el nombre en un unico campo,
se pueden proporcionar multiples campos, como en el ejemplo. El primer campo
se muestra como texto en un cuadro combinado, pero si se establece un valor
grande para la propiedad D r opDownWid t h, la lista desplegable del cuadro
combinado incluira varias columnas de datos. El resultado aparece en la figura
El componente DataSet
En lugar de pasar a analizar las prestaciones de un conjunto de datos especificos, hemos preferido dedicar algo de espacio a una introduccion general a las
caracteristicas de la clase T D a t a S e t , que comparten todas las clases heredadas
de acceso a datos. El componente DataSet es bastante complejo, por lo tanto, no
enumeraremos todas sus capacidades sin0 solo sus elementos principales.
La idea tras este componente es la de proporcionar acceso a una serie de
registros que se leen desde alguna fuente de datos, se guardan en buffers internos
(por razones de rendimiento) y que el usuario podria modificar, con la posibilidad
de volver a escribir 10s cambios almacenandolos de forma permanente. Este enfoque es lo suficientemente genkrico como para que se pueda aplicar a tipos diferen-
tes de datos (incluso datos que no esten en bases de datos) per0 hay que seguir
unas ciertas normas:
En primer lugar, solo puede haber un registro activo en cada momento, por
lo que si tenemos que acceder a datos que estan en diversos registros,
debemos movernos a cada uno de ellos, leer 10s datos, desplazarnos de
nuevo y asi sucesivamente.
En segundo lugar, solo podemos editar el registro activo: no podemos modificar un conjunto de registros a1 mismo tiempo, como hacemos en las
bases de datos relacionales.
Podemos modificar 10s datos del buffer activo solo despues de haber declarado explicitamente que deseamos hacerlo asi, dando la orden E d i t a1
conjunto de datos. Tambien podemos usar la orden I n s e r t para crear un
nuevo registro en blanco y cerrar ambas operaciones (de insercion o edicion) con la orden PO s t .
Otros elementos interesantes de un conjunto de datos que analizaremos mas
adelante son su estado (y 10s eventos de cambio de estado), las posiciones de
navegacion y de registros, y el papel de 10s objetos de campo. Como resumen de
las prestaciones del componente Da t aSe t , en el listado 13.2 hemos incluido 10s
metodos publicos de la clase D a t a s e t (donde se ha editado y comentado el
codigo por claridad). No todos estos metodos se utilizan directamente de manera
habitual, per0 aun asi, hemos preferido mostrarlos todos.
Listado 13.2. La interfaz publica de la clase TDataSet (extracto).
TDataSet =
class(TComponent,
IProviderSupport)
public
/ / c r e a y d e s t r u y e , a b r e y cierra
c o n s t r u c t o r Create (AOwner: TComponent) ; override;
d e s t r u c t o r Destroy; override;
p r o c e d u r e Open;
p r o c e d u r e Close;
p r o p e r t y Beforeopen: TDataSetNotifyEvent r e a d FBeforeOpen
w r i t e FBeforeOpen;
p r o p e r t y Afteropen: TDataSetNotifyEvent r e a d FAfterOpen
w r i t e FAfterOpen;
p r o p e r t y Beforeclose: TDataSetNotifyEvent
r e a d FBeforeClose w r i t e FBeforeClose;
p r o p e r t y Afterclose: TDataSetNotifyEvent r e a d FAfterClose
w r i t e FAfterClose;
// i n f o r m a c i o n s o b r e el estado
f u n c t i o n IsEmpty: Boolean;
p r o p e r t y Active: Boolean r e a d GetActive w r i t e SetActive
d e f a u l t False;
p r o p e r t y State: TDataSetState r e a d FState;
// f u e n t e d e d a t o s
property Datasource: TDataSource read GetDataSource;
procedure DisableControls;
procedure EnableCont rols ;
function ControlsDisabled: Boolean;
/ / c a m p o s , corno b l o b s , d e t a l l e s , c a l c u l a d o s y o t r o s
function FieldByName(c0nst FieldName: string): TField;
function FindField (const FieldName: string) : TField;
procedure GetFieldList (List: TList; const FieldNames:
string) ;
procedure Get FieldNames (List: TStrings) ; virtual; // v i r t u a l
// d e s d e D e l p h i 7
property Fieldcount: Integer read GetFieldCount;
property FieldDefs: TFieldDefs read FFieldDefs write
SetFieldDef s;
property FieldDefList: TFieldDefList read FFieldDefList;
property Fields : TFields read FFields;
property FieldList: TFieldList read FFieldList;
property FieldValues[const FieldName: string]: Variant
read GetFieldValue write SetFieldValue; default;
property AggFields: TFields read FAggFields;
property DataSetField: TDataSetField
read FDataSetField write SetDataSetField;
property DefaultFields: Boolean read FDefaultFields;
procedure ClearFields;
function GetBlobFieldData(FieldN0: Integer;
var Buffer: TBlobByteData): Integer; virtual;
function CreateBlobStream(Fie1d: TField;
Mode: TBlobStreamMode) : TStream; virtual;
function GetFieldData(Fie1d: TField;
Buffer : Pointer) : Boolean; overload; virtual;
procedure GetDetailDataSets(List: TList); virtual;
procedure GetDetailLinkFields(MasterFields, DetailFields:
TList) ; virtual;
function GetFieldData(FieldN0: Integer;
Buffer: Pointer) : Boolean; overload; virtual;
function GetFieldData(Fie1d: TField; Buffer: Pointer;
NativeFormat: Boolean):
Boolean; overload; virtual;
property AutoCalcFields: Boolean
// p o s i c i d n , m o v i m i e n t o
procedure CheckBrowseMode;
procedure First;
procedure Last;
procedure Next;
procedure Prior;
function MoveBy(Distance: Integer): Integer;
property RecNo: Integer read GetRecNo write SetRecNo;
property Bof: Boolean read FBOF;
property Eof: Boolean read FEOF;
procedure CursorPosChanged;
property BeforeScroll: TDataSetNotifyEvent
read FBeforeScroll write FBeforeScroll;
property Afterscroll: TDataSetNotifyEvent
read FAfterScroll write FAfterScroll;
// marcadores
procedure FreeBookmark(Bookmark: TBookmark); virtual;
function GetBookmark: TBookmark; virtual;
function BookmarkValid(Bookmark: TBookmark): Boolean;
virtual ;
procedure GotoBookmark (Bookmark: TBookmark) ;
function CompareBookmarks(Bookmark1, Bookmark2: TBookmark):
Integer; virtual;
property Bookmark: TBookmarkStr read GetBookmarkStr write
SetBookmarkStr;
// b u s c a , l o c a l i z a
function FindFirst: Boolean;
function FindLast: Boolean;
function FindNext: Boolean;
function Findprior: Boolean;
property Found: Boolean read GetFound;
function Locate(const KeyFields: string; const KeyValues:
Variant;
Options: TLocateOptions): Boolean; virtual;
function Lookup(const KeyFields: string; const KeyValues:
Variant;
const ResultFields: string) : Variant; virtual;
// f i l t r a d o
property Filter: string read FFilterText write
SetFilterText;
property Filtered: Boolean read FFiltered write SetFiltered
default False;
property FilterOptions: TFilterOptions
read FFilterOptions write SetFilterOptions default [ I ;
property OnFilterRecord: TFilterRecordEvent
read FOnFilterRecord write SetOnFilterRecord:
// r e f r e s c a , a c t u a l i z a
procedure Refresh;
property BeforeRefresh: TDataSetNotifyEvent
read FBeforeRefresh write FBeforeRefresh;
property AfterRefresh: TDataSetNotifyEvent
read FAfterRefresh w r i t e FAfterRefresh;
procedure UpdateCursorPos;
procedure UpdateRecord;
function GetCurrentRecord(Buffer: PChar): Boolean; v i r t u a l ;
procedure Res ync (Mode: TResyncMode) ; v i r t u a l ;
// e d i t a , i n s e r t a , e n v i a y b o r r a
property CanModify: Boolean read GetCanModify;
property Modified: Boolean read modified;
procedure Append;
procedure Edit;
procedure Insert ;
procedure Cancel; v i r t u a l ;
procedure Delete;
procedure Post ; v i r t u a l ;
procedure AppendRecord (const Values : a r r a y of const) ;
procedure InsertRecord (const Values : a r r a y of const) ;
procedure SetFields(const Values: a r r a y of c o n s t ) ;
// e v e n t o s r e l a c i o n a d o s c o n e d i t a r , i n s e r t a r , e n v i a r y
// b o r r a r
property BeforeInsert: TDataSetNotifyEvent
read FBeforeInsert w r i t e FBeforeInsert;
property AfterInsert: TDataSetNotifyEvent
read FAfterInsert w r i t e FAfterInsert;
property BeforeEdit: TDataSetNotifyEvent read FBeforeEdit
w r i t e FBeforeEdit;
property AfterEdit: TDataSetNotifyEvent read FAfterEdit
w r i t e FAfterEdit;
property BeforePost: TDataSetNotifyEvent read FBeforePost
w r i t e FBeforePost;
property AfterPost: TDataSetNotifyEvent read FAfterPost
w r i t e FAfterPost;
property Beforecancel: TDataSetNotifyEvent
read FBeforeCancel w r i t e FBeforeCancel;
property Aftercancel: TDataSetNotifyEvent
read FAfterCancel w r i t e FAfterCancel;
property BeforeDelete: TDataSetNotifyEvent
read FBeforeDelete w r i t e FBeforeDelete;
property AfterDelete: TDataSetNotifyEvent
read FAfterDelete w r i t e FAfterDelete;
property OnDeleteError: TDataSetErrorEvent
read FOnDeleteError w r i t e FOnDeleteError;
property OnEditError: TDataSetErrorEvent
read FOnEditError w r i t e FOnEditError;
property OnNewRecord: TDataSetNotifyEvent
read FOnNewRecord w r i t e FOnNewRecord;
property OnPostError: TDataSetErrorEvent
r e a d FOnPostError w r i t e FOnPostError;
/ / soporte, utilidades
f u n c t i o n Translate (Src, Dest: PChar;
ToOem: Boolean): Integer; virtual;
property Designer: TDataSetDesigner r e a d FDesigner;
p r o p e r t y BlockReadSize: Integer r e a d FBlockReadSize write
SetBlockReadSize;
property SparseArrays: Boolean r e a d FSparseArrays write
SetSparseArrays;
end;
El estado de un Dataset
Cuando se trabaja sobre un conjunto de datos en Delphi, podemos trabajar en
distintos estados que nos indicara la propiedad especifica St ate, a la que podemos dar diferentes valores:
lanza eventos antes y despues de cualquier carnbio de estado. Cuando un programa solicita una operacion Edit, el componente lanza el evento Be foreEdi t
justo antes de pasar a1 mod0 de edicion (una operacion que podemos detener
creando una excepcion). Inmediatamente despues de pasar a1 mod0 de edicion, el
conjunto de datos recibe el evento AfterEdit.Despues de que el usuario haya
terminado de editar y solicite guardar 10s datos, ejecutando la orden Post, el
conjunto de datos produce un evento Before Post, que se puede usar para
verificar la entrada antes de enviar 10s datos a la base de datos, y un evento
After Post despues de que se haya finalizado satisfactoriamente la operacion.
Otra tecnica de seguimiento de carnbio de estado mas general implica gestionar el evento Onstatechange del componente Datasource.Como ejemplo,
se puede mostrar el estado actual mediante un codigo como el siguiente:
p r o c e d u r e TForml.DataSourcelStateChange(Sender: T O b j e c t ) ;
var
strstatus: string;
begin
c a s e cds . S t a t e of
dsBrowse: s t r S t a t u s : = 'Browse' ;
d s E d i t : s t r S t a t u s := ' E d i t ' ;
dsInsert: s t r S t a t u s := ' I n s e r t ' ;
else
s t r s t a t u s := 'Other s t a t e ' ;
end;
S t a t u s B a r . Panels [ 0 ] .Text : = s t r S t a t u s ;
end:
Value es una propiedad de tipo variante, por lo que resulta en cierto mod0
mas eficiente usar propiedades de acceso especificas de tipo. El componente de
conjunto de datos tiene tambien una propiedad de metodo abreviado para acceder
a1 valor de tipo variante de un campo, la propiedad predefinida FieldValues.
A1 ser una propiedad predefinida significa que podemos omitirla en el codigo
aplicando directamente 10s corchetes a1 conjunto de datos:
strName
strName
Crear 10s componentes de campo cada vez que se abre un conjunto de datos es
solo un comportamiento predefinido. Como alternativa, podemos crear 10s componentes de campo en tiempo de disefio, usando el editor Fields (para ver este
editor en funcionamiento, hay que hacer doble clic sobre un conjunto de datos, o
activar su menu local o el de la vista Object TreeView y escoger la opcion
Fields Editor). Despues de crear un campo para la columna LastName de una
tabla, por ejemplo, podemos referirnos a su valor aplicando uno de 10s metodos
AsXxx a1 objeto de campo adecuado:
Ademas de ser utilizado para acceder a1 valor de un campo, cada objeto de
campo tiene tambien propiedades de visualizacion y edicion de su valor, como
rangos de valores, mascaras de edicion, formatos de presentacion, restricciones y
muchas otras. Por supuesto, dichas propiedades dependen del tip0 de campo, es
decir, de la clase especifica del objeto de campo. Si creamos campos permanentes
podemos definir algunas propiedades en tiempo de diseiio, en lugar de escribir
codigo en tiempo de ejecucion, tal vez en el evento A f teropen del conjunto de
datos.
Name
La orden Define del editor Fields permite definir un nuevo campo calculado, un campo de busqueda o un campo con un tipo modificado. En este cuadro de
dialogo, se puede escribir un nombre de campo descriptivo, que podria incluir
espacios en blanco.
Delphi genera un nombre interno (el nombre del componente de campo) que
ademas se puede personalizar. A continuacion, hay que seleccionar un tip0 de
datos para el campo. Si este se trata de un campo calculado o un campo de
busqueda y no solo una copia de un campo redefinido para usar un nuevo tipo de
datos, simplemente hay que activar el boton de radio apropiado.
NOTA: Un componente T F i e l d tiene una propiedad Name y una propiedad F i e l d N a m e . La propiedad Name es el nombre habitual del componente. La propiedad FieldName es el nombre de la colurnna de la tabla en
la base de-daios o el nombre que definamos para el campo calculado. Puede
ser mas descriptivo que Name y permite espacios en blanco. La propiedad
la propiedad D i s p l a y L a b e l , pero este nombre de carnpo puede cambiarse por cualquier text0 apropiado. Se usa, entre otras cosas, para buscar
-- -1 --l*-J> - 1- - I - - ,.._ _.
3un campo en
el m a w 0 r l e-,
l a a y m a r n e- ae
la clase xuar;aseL
y cuanao
se usa la notacih de matriz.
rrl
2-
...--
TRUCO: Tambien se pueden arrastrar 10s campos desde el editor a1 formulario para dejar que el IDE Cree 10s componentes visuales automiiticamcnte Sc tratn rlc Nina ~aracterictieamiiv nrictica niw n11~Ae
ahnrrar miiehn
TObject);
TFloatField) .DisplayFormat : =
'
###,###,###I
end;
Cuando accedemos a1 valor de un campo, podemos usar una serie de propiedades AS para controlar el valor de campo actual usando un tip0 de datos especifico
(si este esta disponible, si no, se crea una excepcion):
AsBoolean: Boolean;
AsDateTime: TDateTime;
AsFloat : Double;
A s Integer : LongInt;
A s s t r i n g : string;
Asvariant: Variant;
Estas propiedades se pueden usar para leer o cambiar el valor del campo. Para
cambiar el valor de un campo, el conjunto de datos habra de estar en mod0 de
edicion. Otra alternativa a1 uso de las propiedades As, es acceder a1 valor de un
campo usando su propiedad value, que se define como una variante.
La mayoria de las demas propiedades del componente TField, tales como
Alignment, DisplayLabel, Displaywidth y Visible, reflejan elementos de la interfaz de usuario del campo y las utilizan 10s distintos controles
data-aware, sobre todo DBGrid.
En el ejemplo FieldAcc, haciendo clic sobre el tercer boton de velocidad cambia la alineacion de cada campo:
p r o c e d u r e TForm2.SpeedButton3Click(Sender: TObject);
var
I: Integer;
begin
f o r I : = 0 t o cds.FieldCount - 1 d o
cds. Fields [I] .Alignment : = tacenter;
end;
Formal
ShowPop
Ccnln
t I . NdmO
.
- I ..
Cuba
- . .
I.
4
I-
NorthArnexa
Figura 13.6. El aspect0 del ejemplo F~eldAccdespues de haber pulsado 10s botones
Center y Format
TADTField
TObj ectField
TAggregateField
TField
TArrayField
T O b j ect Field
TAutoIncField
TIntegerField
Un ndrnero enter0 positivo conectad0 con un campo autoincremental de una tabla Paradox (un
campo especial al que se asigna
TField
Normalmente no utilizado de forma directa. Esta es la clase basics de las dos clases siguientes.
TField
TField
Un valor booleano.
TBinaryField
TFloatField
TDateField
TDateTimeField
Un valor de fecha.
TDateTimeField
TField
TFloatField
TNumericField
TNumericField
TStringField
TNumericField
TField
TLargeIntField
TIntegerField
TMemoField
TBlobField
TNumericField
TField
TObjectField
TField
TObjectField
TIntegerField
TField
TField
TDateTimeField
Un valor de hora.
TBytesField
TIntegerField
TStringField
TVariantField
TField
TWideStringField
TStringField
Un campo que representa una cadena Unicode (16 bits por caracter).
TWordField
TIntegerField
- . - ..
try
cdsPopulationDensity.Value : = c d s ~ o p u l a t i o n . V a l u e /
cdsArea.Value;
except
on Exception do
cdsPopulationDensity.Value : = 0;
end:
El codigo del metodo cdscalc Fields (en cada una de las tres versiones)
accede directamente a algunos campos. Se puede hacer asi porque se ha usado el
editor Fields y, automaticamente, se ha encargado de la creacion de las declaraciones de campo correspondientes, como se puede ver en este extract0 de la declaration de interfaz del formulario:
type
TCalcForm = c l a s s (TForm)
cds: TClientDataSet;
cdsPopulationDensity: TFloatField;
cdsArea: TFloatField;
cdspopulation: TFloatField;
cdsName: TStringField;
cdscapital: TStringField;
cdscontinent: TStringField;
procedure cdsCalcFields (Dataset: TDataSet) ;
Cada vez que aiiadimos o eliminamos campos en el editor Fields, podemos ver
el efecto inmediato en la cuadricula del formulario (a no ser que la cuadricula
tenga definidos sus propios objetos de columna, en cuyo caso normalmente no se
vera ningun cambio) .
Por supuesto, en tiempo de diseiio no se veran 10s valores de un campo calculado; solo se encuentran disponibles en tiempo de ejecucion, porque son el resultad0 de la ejecucion del codigo Delphi compilado.
Dado que hemos definido algunos componentes para 10s campos, podemos
usarlos para personalizar algunos elementos visuales de la cuadricula. Por ejemplo, para definir un formato de presentacion que aiiada un punto para separar 10s
miles, podemos usar el Object Inspector para cambiar l a propiedad
DisplayFormat de algunos componentes de campo a # # # , # # # , # # # . El
efecto de este cambio en la cuadricula es inmediato en tiempo de diseiio.
-1
..
l-:
3-
-_A-
3-
A_--*_
. -.
,.
Despues de trabajar con 10s componentes de tabla y 10s campos, hemos personalizado el componente DBGrid usando el editor de su propiedad C o l u m n s .
Hemos configurado la columna Popzrlation Density (densidad de poblacion) como
de solo lcctura y su propiedad But tonstyle como cbsEllipsis, para ofrecer
un editor personalizado. Cuando definimos este valor, aparece un pequeiio boton
con tres puntos si el usuario intenta editar la celda de la cuadricula. Al pulsar el
boton sc invoca a1 evento OnEditButtonClick de la DBGrid:
procedure T C a l c F o r m . D B G r i d l E d i t B u t t o n C l i c k ( S e n d e r :
begin
MessageDlg (Format (
' T h e p o p u l a t i o n d e n s i t y ( 8 . 2 n ) #I3 +
' i s t h e P o p u l a t i o n ( % . O n ) '#I3 +
' d i v i d e d by t h e A r e a (%.On). '#13#13 +
' E d i t these two f i e l d s t o c h a n g e i t . I ,
[cdsPopulationDensity.AsFloat,
cdsPopulation.AsFloat,
cdsArea.AsFloat] ) ,
mtInf ormation, [mbOK] , 0 ) ;
end;
TObject);
En rcalidad, no hemos proporcionado un editor real, sino un mensaje que describe la situation, como se ve en la figura 13.8, que muestra 10s valores de 10s
campos calculados. Para crear un editor, podriamos crear un formulario secundario para manejar las entradas de datos especiales.
A q n 1 ,m
Buenar P r ~ r
Sauth Ameuca
SoulhAmerce
SarlhAmnlcs
32 300 003
2 777 815
1163
Nolthh~ca
SouthAmerlce
Soulh Amctea
NmlhArnnca
Solnh America
Narthhaim
Campos de busqueda
Como alternativa a colocar un componente D B L o o k u p C o m b o B o x en un formulario (algo que ya hemos comentado antes), tambien podemos definir un campo de busqueda, que puede mostrarse con una lista de busqueda desplegable dentro
de un componente D B G r i d . Hemos visto que para aiiadir una selection fija a una
D B G r i d, podemos sencillamente editar la subpropiedad P i c k L i s t de la propiedad c o 1u m n s .
Para personalizar la cuadricula con una busqueda en directo, en cambio, tenemos que definir un campo de busqueda utilizando el editor Fields.
Como ejemplo, hemos construido el programa FieldLookup, que tiene una
cuadricula en la que aparecen pedidos con un campo de busqueda para mostrar el
nombre del empleado que anoto el pedido, en lugar del numero de codigo de dicho
empleado. Para ello, hemos aiiadido a1 modulo de datos un componente
C l i e n t D a t a S e t que se refiere a1 conjunto de datos e m p l o y e e . c d s . A continuacion, hemos abierto el editor Fields para el conjunto de datos o r d e r s . c d s
y hemos aiiadido todos 10s campos.
Hemos seleccionado el campo E m p N o y hemos definido su propiedad V i s i b l e como F a 1 s e para eliminarla de la cuadricula (no podemos eliminarla por
completo, porque se usa para crear la referencia cruzada a1 campo correspondiente del conjunto de datos de empleados).
Ahora, hay que definir el campo de busqueda. Si se han seguido 10s pasos
anteriores, se puede utilizar el editor Fields del conjunto de datos de pedidos
( o r d e r s .c d s ) y seleccionar la orden N e w F i e l d , con lo que aparecera el
cuadro de dialog0 New Field. Los valores que especificamos aqui afectaran a las
propiedades de un nuevo T F i e l d aiiadido a la tabla, tal como muestra la descripcion DFM del campo:
object cds2Employee: TStringField
FieldKind = f kLookup
FieldName = ' E r p l o y e e '
LookupDataSet = cds2
LookupKeyFields = ' E r p N o '
LookupResultField = ' L a s t N a r n e '
KeyFields = ' E r p N o '
Size = 3 0
Lookup = True
end
Esto es todo lo que se necesita para que la lista desplegable funcione (vease la
figura 13.9) y tambien para ver el valor del campo de la referencia cruzada en
tiempo de diseiio. Fijese en que no es necesario personalizar la propiedad C o l u m n s
de la cuadricula porque se usa el boton desplegable y el valor de siete filas viene
de manera predefinida. Eso no significa que no podamos usar esta propiedad para
personalizar aun mas estos y otros elementos visuales de la cuadricula a nuestro
gusto.
-- --
I ,
.^-^^-*-
-I- x x 7 : - 1 - - - .
C U I I G G ~ L VUG
w IIIUUWS
-1-
f-.
-.^-L^-^
. I
-..
II
^^--^-l^L--^-L^
---L-Ll-
^-L-^
UG UIM V G I I L ~ ~ (y
I I ~GS C ; U I I I ~ I G L S L I ~ I G I I~L G
U I L ~ U IGllLrt;
G
I.
. . I
G a y a u u i r o UG acc;r;ur;I
^^--^-^-*^^
a GUIII~UIIGIILG~
^
-1-
----
^*-^ r
UG ULIU
I--:-
LUIIIIUI~IIU
-LA-I-
u IIIUUUIU
-I- -I^*^-
us;
uarus
else
Text : = Sender .Asstring;
end;
procedure TForml.cdsShipDateSetText(Sender: TField;
String) ;
begin
if Text = " then
Sender.Clear
else
Sender.AsString : = Text;
end;
const T e x t :
OrdnNo
r-iiE
C&No
pi%r
Sald)ak
112/1995
ShpDae
Itundslned,
Emflo
ICU~NO
l~ak~ate
01brNo
U1298CN2315
U1300 CN 1384
Ill302 CN 1231
#13&5 CN 1356
W1309 CN 3615
9/1/1995
1011/1995
16/1/1995
M/1/1995
22/1/1995
I~hpDate
91111995
1W111995
16/1/1995
2Wlt1995
2211/I995
IE~~NO
EmpU 0011
EmpW OOB
ErnpU 0052
EmpU DO65
ErwW 0094
26/1/1995
EmpU 012'1
Emp# 0138
Empll0071
W1315CN1651
W1317CN1984
#1350 CN 3052
26/1/1995
1/2/1995
lm995
112/1995
W1355 CN 3 3 3
W1860 CN 3615
5/2/1 995
4/2/1996
5/2/1995
<un&lmeb
EmpW 0141
IEmp#0138
Figura 13.10. En el ejernplo NullDates se controlan 10s eventos OnGetText y
OnSetText de un carnpo de fecha.
- -- --- - - - - ADYERTENCIA: La gestion de valores nulos en Delphi 6 y 7 puede verse
-
registros. Ahora veremos como modificar 10s datos de la tabla mediante el codigo
del programa. El conjunto de datos de empleado que ya hemos utilizado tiene un
campo Salary, para que el administrador de la empresa pueda revisar la tabla y
modificar el sueldo de un unico empleado. Consideremos ahora lo que sucede con
el coste total en sueldos para la empresa y el caso de que el administrador quiera
aumcntar (o disminuir) en un 10 por ciento el sueldo de todos 10s empleados.
El programa, que tambien muestra el uso de una lista de acciones para las
acciones estandar de conjuntos de datos, tiene botones para calcular la suma de
10s sueldos actuales y modificarlos. La accion Total permite calcular la suma
de 10s sueldos de todos 10s empleados. Basicamente, se necesita analizar la tabla,
leyendo el valor del campo c d s s a l a r y para cada registro.
var
Total: Real;
begin
Total : = 0 ;
cds. First;
w h i l e n o t cds.EOF do
begin
Total : = Total + cdsSalary.Value;
cds .Next;
end;
MessageDlg ( ' S u m o f new s a l a r i e s is ' +
Format ( ' %m', [Total]) , mtInformation,
end
[rnbok] , 0 ) ;
Este codigo funciona, como muestra la figura 13.1 1. pero tiene algunos problemas. Uno de ellos es que el puntero de registro se desplaza a1 ultimo registro,
por lo que se pierde la posicion anterior en la tabla. Otro problema consiste en que
la interfaz de usuario se refresca muchas veces durante la operation.
Figura 13.11. La salida del programa Total, que muestra 10s salarios totales de 10s
empleados.
Uso de marcadores
Para evitar estos dos problemas, hay que desactivar las actualizaciones y guardar tambien la posicion actual del puntero de registro en una tabla y recuperarla
a1 final. Para ello, podemos usar un marcador de tabla, una variable especial que
guarda la posicion de un registro en una tabla de conjunto de datos. El enfoque
tradicional de Delphi consiste en declarar una variable del tipo de datos
T B o o kmar k e iniciarla mientras se obtiene la posicion actual de la tabla:
var
Bookmark: TBookmark;
begin
Bookmark : = cds.GetBookmark;
A1 final del metodo Act io nTot a lExe cut e, podemos recuperar la posicion y borrar el marcador con las dos sentencias siguientes (dentro de un bloque
final 1 y para asegurarnos de que se libera la memoria del puntero):
cds.GotoBookmark
cds.FreeBookmark
(Bookmark);
(Bookmark);
Bookmark: TBookmarkStr;
begin
Bookmark : = cds.Bookmark;
...
cds.Bookmark : = Bookmark:
Para evitar el otro efecto secundario del programa (vemos 10s registros desplazandose mientras la rutina recorre 10s datos), podemos desactivar temporalmente
10s controles visuales conectados con la tabla. La tabla tiene un metodo
Disablecontrols a1 que podemos llamar antes de que se inicie el bucle
while y un metodo Enablecontrols a1 que podemos llamar a1 final, despues de que se recupere el puntero de registro.
Por ultimo, nos enfrentamos a cuantos peligros de error a1 leer 10s datos de la
tabla, sobre todo si el programa esta leyendo 10s datos desde un servidor a traves
una red. Si surge un problema cualquiera mientras se consiguen 10s datos, se crea
una excepcion, 10s controles permanecen desactivados y el programa no puede
recuperar su comportamiento normal. Para evitar esta situacion, deberiamos utilizar un bloque t r y / f i n a l l y . En realidad, si queremos que el programa sea
fiable y a prueba de errores a1 cien por cien, deberiamos utilizar dos bloques
t r y / f i n a l l y anidados. Veamos el codigo en el que se han introducido estos
cambios:
procedure TSearchForm.ActionTotalExecute(Sender: TObject);
var
Bookmark: TBookmarkStr;
Total: Real;
begin
Bookmark : = cds.Bookmark;
t r~
cds.DisableContro1s;
Total : = 0;
try
cds. First;
while not cds.EOF do
begin
Total : = Total + cdsSalary.Value;
cds .Next;
end;
finally
cds.EnableControls;
end
finally
cds.Bookmark : = Bookmark;
end ;
MessageDlg ( 'Sum o f n e w s a l a r i e s is ' +
Format ( ' %ml , [Total]) , mt Information, [mbOK] , 0 ) ;
end;
NOTA: Hemos escrito este codigo para mostrar un ejemplo de bucle con el
que recorrer el contenido de una tabla, per0 conviene tener en cuenta que
existe una tecnica alternativa basada en el uso de una consulta SQL, que
devuelve la suma de valores de un campo. Cuando usamos un servidor
SQL, la ventaja de velocidad de una llakada SQL para calcular el total
puede ser significativa, dado que no es necesario lleiar todos 10s datos de
- . - . - - . - - - -.
-..cada camDo desde el S e ~ d 0 a1
r ordenador del cliente. El serv~dorsolo
envia al cliente el resultado final. Existe una alternativa mejor cuando se
usa un ClientDataSet, ya que aislar una columna es una de las caracteristicas
e ofrecen 10s aerenados. Lo
- - - - - -- - -- m
-- m
---e hemos comentado aaui
---- es
-- una
1--------- --- -=--=----.
solution generics, que deberia funcionar para cualquier conjunto de datos.
--------I--
plo mostrara como pintar en una cuadricula y el segundo como usar la caracteristica de selection multiple de la cuadricula.
// d i s e d o p r e d e f i n i d o
DBGridl.DefaultDrawDataCel1
end;
(OutRect, Column.Field, S t a t e ) ;
90020
90030
90050
90070
90080
90090
90100
901 10
90120
90130
90140
c&cP
Triggerfish
Wresse
Angelf~sh
Cod
Scorp~onfish
Bunefflylish
Shark
Ray
Eel
Cod
ICummnOmmMNama
Clown Triggerfish
~ e ~dm p e r o r
Z ~Glant
J Maor1 Wrasse
Blue Angell~sh
SW Lunarta~lRockcod
F~ref~sh
Ornate Bunefflylish
-Swell
Shark
6.1 Ray
" -7California Moray
4-- Lingmd
6-3
+=
15ceclcr ~
Ball~sloidesconspicillum
Lutjanus sebae
Che~l~nus
undulatus
Pomacanlhus nauarchus
Var~olalout1
Plerois vol~tans
,Chaetodon Ornatissimus
Cephaloscyllium ventriosum
Myhobatis californica
Gymnothorax mordax
Ophiodon elongatus
Figura 13.12. El programa DrawData muestra una cuadricula que incluye el texto de
un campo de memo y el omnipresente pez de Borland.
La Paz
Guyana
Jarna~ca
IGso~gelor
Kmgston
Memco Cty
Managua
Asuncion
b?
Sodh Arne
North Amer
NorlhAmer
NorlhAmer
South A m c d
Figura 13.13. El ejemplo MltGrid tiene un control DBGrid que permite la seleccibn de
varias filas.
BookmarkList : = DbGridl-SelectedRows;
for I : = 0 to BookmarkList.Count - 1 do
begin
// p a r a c a d a u n o , m u e v e l a t a b l a a e s e r e g i s t r o
cds.Bookmark : = BookmarkList[I];
// a i i a d e e l carnpo name d l c u a d r o d e l i s t a
ListBoxl. Items .Add (cds.FieldByName ( ' N a m e ' ) .Asstring);
end;
finally
// v u e l v e a 1 r e g i s t r o i n i c i a l
cds.Bookmark : = Bookmark;
end;
end;
class (TDbGrid)
:=
La primera operacion es decidir sobre que celda se solto el boton del raton.
Partiendo de las coordenadas X e Y del raton, podemos llamar a1 metodo protegido M o u s e C o o r d para acceder a la fila y a la columna de la celda. A menos que
el destino del arrastre sea la primera fila (normalmente la que contiene 10s titulos)
o la primera columna (normalmente la contiene el indicador), el programa mueve
el registro actual por la diferencia entre la fila solicitada (gc. y) y la fila activa
en ese momento (la propiedad protegida Row de la cuadricula). El siguiente paso
consiste en poner el conjunto de datos en mod0 de edicion, capturar el campo de la
columna de destino ( C o l u m n s . I terns [ g c .x - 1] . F i e l d ) y modificar su
texto.
El otro elemento del ejemplo NonAware es una lista de botones que corresponden a algunos de 10s que se encuentran en el control DBNavigator; estos
botones estan conectados a cinco acciones personalizadas. No podemos usar las
acciones de conjuntos de datos estandar en este ejemplo, sencillamente porque se
conectan automaticamente con la fuente de datos asociada con el control que tiene
el foco, un mecanismo que falla en 10s cuadros de edicion no data-aware de este
ejemplo. En general tambien se podria conectar una fuente de datos con la propiedad Datasource de cada una de las acciones, pero en este caso especifico no
tenemos una fuente de datos.
El programa tiene varios controladores de eventos que no se han usado en las
aplicaciones anteriores que utilizaban controles data-aware. En primer lugar, hay
que mostrar 10s datos del registro actual en 10s controles visuales (vease figura
13.14) controlando el evento O n A f terScro11 del componente de conjunto de
datos.
procedure TForml.cdsAfterScroll (Datasender: TDataSet);
begin
EditName.Text : = cdsName.AsString;
EditCapital.Text : = cdsCapital.AsString;
ComboContinent.Text : = cdsContinent.AsString;
EditArea.Text : = cdsArea.AsString;
EditPopu1ation.Text : = cdsPopu1ation.AsString;
end;
c d s .Edit;
end;
Una tecnica alternativa para comprobar el valor de un campo consiste en controlar el evento B e fo r e P o s t del conjunto de datos. Conviene tener en cuenta,
en este ejemplo, que la operacion de notificacion no se controla mediante un boton
especifico, sino que tiene lugar en cuanto el usuario se mueve a un nuevo registro
o inserta uno nuevo:
procedure TForml.cdsBeforePost(DataSet: T D a t a S e t ) ;
begin
i f cdsArea.Value < 100 then
r a i s e Exception.Create ( ' A r e a t o o small' ) ;
end ;
En cada uno de estos casos, existe una alternativa a la creacion de una excepcion que consiste en establecer un valor predefinido. Sin embargo, si un campo
tiene un valor predefinido, es mejor presentarlo, para que el usuario pueda ver
que valor se enviara a la base de datos. Para ello, podemos controlar el evento
que se haya creado un nuevo registro (podiamos haber usado tambien el evento
OnNewRecord ):
procedure TForml.cdsAfterInsert(DataSet:
begin
cdsContinent.Va1ue : = 'Asia';
end;
TDataSet);
Se llama a este metodo siempre que el usuario hace clic sobre el boton, selecciona un elemento del cuadro combinado o pulsa la tecla Intro mientras que se
encuentra en el cuadro de dialogo:
procedure TForml.ComboNameClick(Sender:
begin
GetData;
end;
TObject);
var Key:
Jamaica
&=
1756943
Por ultimo, el usuario puede modificar 10s valores de 10s controles y hacer clic
sobre el boton Send. El codigo que se va a ejecutar depende de si la operacion es
una actualizacion o una insercion. Podemos saberlo fijandonos en el nombre (aunque con este codigo, un nombre erroneo ya no podra modificarse):
p r o c e d u r e TForml.SendData;
begin
// crea una e x c e p c i d n , s i no hay nombre
i f ComboName.Text = " then
r a i s e Exception .Create ( ' I n s e r t t h e name' )
/ / v e r i f i e d s i e l r e g i s t r o ya e s t d e n l a t a b l a
i f cds .Locate ( ' N a m e ' , ComboName .Text, [loCaseInsensitive] )
then
begin
/ / modified e l r e g i s t r o encontrado
cds.Edit;
cdsCapita1.AsString : = E d i t c a p i t a l - T e x t ;
cdsContinent.AsString : = ComboContinent.Text;
cdsArea.AsString : = E d i t A r e a - T e x t ;
cdsPopu1ation.AsString : = EditPopulation.Text;
cds .Post;
end
else
begin
/ / inserta un nuevo registro
cds.InsertRecord ([ComboName.Text, EditCapital.Text,
ComboContinent.Text, EditArea.Text,
EditPopulation.Text1);
// a d a d e a la lista
ComboName Items .Add (ComboName.Text)
end;
Antes de enviar 10s datos a la tabla podemos realizar cualquier tipo de prueba
de validacion a 10s valores. En este caso, no tiene mucho sentido controlar 10s
eventos de 10s componentes de la base de datos, porque tenemos control total
sobre el momento en el que se realizan las operaciones de actualizacion o insercion.
Agrupacion y agregados
Ya hemos visto que un ClientDataSet puede tener un indice distinto a partir del
orden en que se guardan 10s datos en el archivo. Una vez que se define un indice,
se pueden agrupar 10s datos de acuerdo con ese indice. En la practica, un grupo
esta definido como una lista de registros consecutivos (segun su indice) para 10s
cuales no cambia el valor del campo indexado. Por ejemplo, si tenemos un indice
para la provincia, todas las direcciones de esa provincia entraran en el mismo
grupo.
Agrupacion
El ejemplo CdsCalcs tiene un componente C l i e n t D a t a S e t que extrae sus
datos del habitual conjunto de datos c o u n t r y . c d s .
El grupo se consigue, junto con la definicion de un indice, a1 especificar un
nivel de agrupacion para el indice:
object ClientDatasetl: TClientDataSet
IndexDefs = <
item
Name = ' Clientda taSetl Index1
Fields = 'Continent'
GroupingLeve = 1
en&
IndexName = 'ClientDataSetlIndexl'
Nicara~a
El S alvadar
Cuba
Jaaica
Un~tedSlates d Amarlca
Canada
S d h America
Washmglon
Ollawa
Amion
Monlevideo
Caracas
Paraguay
U~WW
Venezuela
Peru
hgmfina
Guyana
Ecuador
Lima
B uermr Aiies
Georgetown
Quito
Cob&
Bwta
'Sancbju
Brasika
Chi
8rd
- - - .-. -. - I
Figura 13.16. El ejemplo CdsCalcs rnuestra que escribiendo un poco de codigo, se
puede conseguir que un control DBGrid rnuestre visualrnente la agrupacion definida
en el ClientDataSet.
~
--
Definicion de agregados
Otra caracteristica del componente C1i e n t D a t a S e t es el soporte de agregados. Un agregado es un valor calculado basandose en varios registros, como la
&ma o el valorpromedio de un campo para toda la tabla o un gripe de registros
(definido mediante la logica de agrupacion ya comentada). Los agregados son
mantenidos, es decir, se vuelven a calcular inmediatamente si cambia uno de 10s
registros. Por ejemplo, se puede mantener automaticamente el total de una factura
mientras el usuario escribe 10s elementos de la factura.
lpd+ Es valores cada vez que &imbia an valor. La agregacih se aprove&a de 10s datos delta de1 Cll&ntDataSet.~btjempb,
para actualizar
un8 suma c u a n d ~cambi$-uij W p q el Clier)tQa,taSst resta e1 antiguo
v a h del agregadb y aiiade e1nqew valor. S&lo se neceskan dos cAlculos,
-90
aunque d a w m i l c s d i fileen esd
a g r e d p . . i o r cste motivo,
las actualizaciones de agrega& son instazit6vleas.
'
campos agregados mediante el editor Fields. En ambos casos, se define la expresion agregada, se le da un nombre y se conecta con un indice y un nivel de
agrupamiento (a no ser que se desee aplicar a toda la tabla). Este es el conjunto
A g g r e g a t e s del ejemplo CdsCalcs:
o b j e c t ClientDataSetl: TClientDataSet
Aggregates = <
item
Active = True
AggregateName = ' C o u n t '
Expression = 'COUNT (NAME)'
GroupingLevel = 1
IndexName = ' C l i e n t D a t a S e t l I n d e x 1 0
Visible = False
end
item
Active = True
AggregateName = ' T o t a l P o p u l a t i o n '
Expression = 'SUM (POPULATION) '
Visible = False
end>
AggregatesActive = T r u e
Fijese en que en la ultima linea del anterior fragment0 de codigo se debe activar el soporte para 10s agregados, ademas de activar especificamente cada agregad0 que se quiera usar. Es importante inhabilitar 10s agregados, porque tener
muchos de ellos puede ralentizar un programa.
El enfoque alternativo es usar el editor Fields, escoger la orden N e w F i e l d
desde su menu de metodo abreviado y seleccionar la opcion Aggregate (disponible, junto con la opcion InternalCalc, solo en un ClientDataSet). Esta es la
definition de un campo agregado:
object ClientDataSetl: TClientDataSet
object ClientDataSetlTotalArea: TAggregateField
FieldName = ' T o t a l A r e a '
ReadOnly = True
Visible = True
Active = True
DisplayFormat = ' # # # , # # # , # # # I
Expression = ' S U M (AREA) '
GroupingLevel = 1
IndexName = ' ClientDa taSetl Index1 '
end
Los campos agregados se muestran en el editor Fields en un grupo independiente, como muestra la figura 13.17. La ventaja de usar un campo agregado, en
comparacion con un simple agregado, es que se puede definir el formato de representacion y conectar directamente el campo con un control data-aware, como un
DBEd it en el ejemplo CdsCalcs. Ya que el agregado se encuentra conecta a un
grupo, en cuanto se selecciona un registro de un grupo distinto, se actualiza
automaticamente la salida. Ademas, se cambian 10s datos, el total mostrara inmediatamente el nuevo valor.
Figura 13.17. La parte inferior del editor Fields de un ClientDataSet muestra 10s
campos agregados.
Para usar agregados simples, hay que escribir algo de codigo, como en el
ejemplo siguiente (el V a l u e del agregado es una variante):
procedure TForml.ButtonlClick(Sender: TObject);
begin
Labell.Caption : =
'Area: ' + ClientDataSetlTotalArea.Disp1ayText +
#13'Population : ' +
FormatFloat ( ' # # # , # # # , # # # I
, ClientDataSet1.Aggregates
[l] . v a l u e ) +
# 1 3 ' N u m b e r : ' + IntToStr (ClientDataSetl.Aggregates
[0] . V a l u e ) ;
end ;
Estructuras maestroldetalles
Es habitual que se necesite relacionar tablas que tengan una relacion uno a
muchos. Esto significa que, para un unico registro de la tabla maestra existen
En la figura 13.18 se puede ver un ejemplo del formulario principal del programa MastDet en tiempo de ejecucion. Hemos colocado 10s controles data-aware
relacionados con la tabla maestra en la parte superior del formulario, y una cuadricula conectada con la tabla de detalle en la parte inferior. De esta manera, para
cada registro maestro, se puede ver inmediatamente la lista de 10s registros de
detalle conectados (en este caso, todos 10s pedidos vinculados con el cliente ac-
tual). Cada vez que se selecciona un nuevo cliente, la cuadricula que se encuentra
bajo el registro maestro muestra solo 10s pedidos que pertenezcan a ese cliente.
4/12/1989
1280
1059
1W
1305
45
65
;iLI
Figura 13.18. El ejernplo MastDet en tiernpo de ejecucion.
Como ejemplo, hemos creado un programa de bases de datos que muestra 10s
detalles de 10s errores en un componente de memo (10s errores se generan
automaticamente cuando el usuario hace clic sobre 10s botones del programa).
Para controlar todos 10s errores, el ejemplo DBError instala un controlador para
el evento OnExcept ion del objeto global Application.El controlador del
evento registra algo de informacion en un campo de memo que muestra 10s detalles del error de la base de datos si se trata de un EDBClient.
procedure TForml.ApplicationError (Sender: TObject; E: Exception);
begin
if E is EDBClient then
begin
Memol.Lines.Add('Error: ' + (E.Message));
Memol.Lines.Add('
Error Code: ' +
IntToStr (EDBClient (E).Errorcode)) ;
end
else
Memol.Lines.Add('Generic Error: ' + (E.Message));
end:
Clientel
sewidor
con dbExpress
En el capitulo anterior, hemos analizado el soporte de Delphi para la programacion de bases de datos, empleando archivos locales (en particular, usando el
componente C 1i e n tDat aSet , o MyBase) en la mayoria de 10s ejemplos, per0
sin centrarnos en ninguna tecnologia de bases de datos concreta. Este capitulo
pasa a comentar el uso de las bases de datos servidores SQL, centrandonos en el
desarrollo clientelservidor con el BDE y la nueva tecnologia dbExpress. Un unico
capitulo no puede agotar completamente un tema como este, asi que lo presentaremos desde la perspectiva del desarrollador en Delphi y aiiadiremos algunos trucos
y sugerencias. Para 10s ejemplos, hemos usado InterBase, ya que esta RDMBS
(sistema de administracion de bases de datos relacionales) de Borland, o este
servidor SQL, se incluye con las versiones Professional y superiores de Delphi;
ademas, se trata de un servidor gratuito y de codigo abierto (aunque no en todas
las versiones). Analizaremos InterBase desde el punto de vista de Delphi, sin
profundizar en su arquitectura interna. Gran parte de la infonnacion aqui presentada se aplica tambien a otros servidores SQL, por lo que, aunque decida no
utilizar InterBase, puede que siga siendo de interes.
En este capitulo trataremos 10s siguientes temas:
Vision global de la arquitectura clientelservidor.
Elementos del diseiio de bases de datos.
Presentacion de InterBase.
Programacion de servidor: vistas, procedimientos almacenados y
disparadores.
La biblioteca dbExpress.
Cache con el componente ClientDataSet.
Los componentes InterBase Express (IBX).
La arquitectura clientelservidor
Las aplicaciones de bases de datos de 10s capitulos anteriores utilizaban componentes nativos para acceder a 10s datos almacenados en archivos de un ordenador local y cargar todo el archivo en memoria. Se trata de un enfoque extremo. De
manera mas tradicional, el archivo se lee registro a registro de manera que varias
aplicaciones puedan acceder a el a1 mismo tiempo, usando algunos mecanismos
de sincronizacion en la escritura.
Cuando 10s datos se encuentran en un servidor remoto, copiar toda una tabla
en memoria para procesarla es una tarea que consume tiempo y ancho de banda, y
suele resultar inutil. Como ejemplo, supongamos que tenemos una tabla como
EMPLOYEE (parte de la base de datos de ejemplo de InterBase, incluida con
Delphi) y que le aiiadimos miles de registros y la colocamos en un ordenador en
red como un servidor de archivos.
Si queremos conocer el maximo salario que paga la empresa, podemos abrir un
componente de tabla de dbExpress ( E m p T a b l e ) o una consulta de seleccion de
todos 10s registros y ejecutar este codigo:
EmpTable.Open;
EmpTable-First;
MaxSalary := 0;
while not EmpTable.Eof d o
begin
i f EmpTable. FieldByName ( ' S a l a r y ' ) .Ascurrency > MaxSalary
then
MaxSalary : = EmpTable. FieldByName ( ' S a l a r y ' ) .AsCurrency;
EmpTable .Next;
end;
El efecto de este enfoque es que lleva todos 10s datos de la tabla desde el
ordenador en red a1 ordenador local, una operacion que puede tomar minutos. En
este caso, el enfoque correct0 seria permitir que el servidor SQL calcule directamente el resultado, devolviendo unicamente esta informacion. Puede hacerse esto
si se usa una sentencia SQL como:
select Max (Salary) from Employee
NOTA: Los dos fragmentos de cirdigo anteriores forman parte deI ejernplo
GetMax, que incluye cbdigo para cronometrar las dos tdcnicas. Para utiIizar
el componente Table de la pequefia tabla Employee se necesitan unas
diez veces m h de tiempo que para realizar la consulta, aunque el servidor
InterBase est6 instalado en el ordenador en que se ejecuta el programa.
Para almacenar una gran cantidad de datos en un ordenador central y no tener
que llevar 10s datos a 10s ordenadores cliente para procesarlos, la unica solucion
es permitir que el ordenador central manipule 10s datos y envie de vuelta a1 cliente
solo una cantidad limitada de informacion. Esta es la base de la programacion
clientelservidor.
Por lo general, utilizaremos un programa esistente en el servidor (un RDBMS)
y escribiremos una aplicacion de cliente personalizada que se conecte con el. Sin
embargo, algunas veces, podriamos querer escribir tanto un cliente personalizado
como un servidor personalizado, como en las aplicaciones de tres capas. El soporte de Delphi para este tipo de programa (que solia llamarse arquitectura MIDAS
IM~clclle-tierDistribz~tedApplicalion Services] y ahora se llama Datasnap) se
tratara mas adelante, en el capitulo 16.
El aumento de volumen de una aplicacion, es decir, la transferencia de datos
desde 10s archivos locales a un motor de base de datos de un servidor SQL, se
realiza normalmente por razones de rendimiento y para permitir el uso de grandes
cantidades de datos. Volviendo a1 ejemplo anterior, en un entorno clientelservidor, la consulta utilizada para calcular el salario masimo la calcularia el RDBMS,
que enviaria solamente el resultado de vuelta a1 ordenador cliente, un unico numero. Con un servidor potente (corno una estacion Sun SparcStation multiprocesador),
el tiempo total necesario para el calculo del resultado seria minimo. No obstante,
esisten otras razones para escoger una arquitectura clientelservidor:
Ayuda a gestionar una gran cantidad de datos, porque no es aconse.jable
guardar cientos de megabytes en un archivo local.
Soporta la necesidad de acceso concurrente a 10s datos varios usuarios a1
mismo tiempo. Las bases de datos de 10s servidores SQL usan normalmente el bloqueo optimista, una tecnica que permite que varios usuarios trabajen sobre 10s mismos datos y que retrasa el control de concurrencia hasta el
momento en que 10s usuarios envian de nuevo las actualizaciones.
Ofrece integridad de datos, control de transacciones, control de acceso,
soporte para copias de seguridad y otras prestaciones similares.
Soporta la programabilidad (la posibilidad de e.jecutar parte del codigo,
como procedimientos almacenados, disparadores, vistas de tablas y otras
tecnicas, en el servidor, reduciendo asi el trafico de red y la carga de
trabajo de 10s ordenadores cliente.
Entidades y relaciones
La tecnica de diseiio de bases de datos relacionales clasica, basada en el modelo entidad-relacion (E-R), implica tener una tabla para cada entidad que necesitemos representar en la base de datos, con un campo para cada elemento de datos
que necesitemos y un campo adicional para cada relacion uno a uno o uno a varios
con otra entidad (o tabla). En el caso de las relaciones varios a varios, sera
necesaria una tabla a parte.
Como ejemplo de relacion uno a uno, supongamos una tabla que represente un
curso universitario. Tendria un campo para cada elemento de datos importante
(nombre y descripcion, sala en la que se impartira, etc.) ademas de un campo
unico en el que se indique el profesor. Los datos del profesor, de hecho, no se
deberian almacenar junto con 10s datos del curso, sin0 en una tabla independiente,
puesto que podria hacerse referencia a ellos desde cualquier otra parte.
El horario de cada curso puede incluir un numero no definido de horas de dias
distintos, por lo que no pueden aiiadirse dentro de la misma tabla que describe el
curso. En su lugar, esta informacion habra de colocarse en una tabla aparte, que
contenga todos 10s horarios, con un campo que haga referencia a la clase corres-
pondiente a cada horario. En una relacion uno a muchos como esta, "muchos"
registros de la tabla de horarios apuntan de nuevo al mismo registro ("uno") de la
tabla de cursos.
Se necesita una situacion mas compleja para almacenar informacion sobre que
estudiante recibira que clase. No se pueden listar 10s estudiantes directamente en
la tabla de cursos, porque su numero no es fijo, y las clases no se pueden guardar
en 10s datos sobre estudiantes por la misma razon. Asi, en una relacion varios a
varios de este tipo, la unica tecnica consiste en tener una tabla adicional que
represente la relacion y liste las referencias a estudiantes y cursos.
Reglas de normalizacion
Los principios clasicos sobre diseiio incluyen una serie de reglas de normalizacion. El objetivo de estas reglas consiste en evitar la duplicacion de datos en las
bases de datos (no solo para ahorrar espacio, sino sobre todo para evitar producir
incoherencias de datos). Por ejemplo, no repetimos todos 10s datos de cliente en
cada pedido, sino que hacemos referencia a una entidad de cliente aparte. Asi,
ahorramos memoria y cuando cambien 10s datos del cliente (corno, por ejemplo,
un cambio de direccion), todos 10s pedidos de este cliente incluiran 10s nuevos
datos. Otras tablas que se relacionen con el mismo cliente tambien se actualizaran
automaticamente.
Las reglas de normalizacion implican el uso de codigos para valores repetidos
comunmente. Por ejemplo, si tenemos diferentes opciones de envio, no utilizaremos una descripcion basada en una cadena para dichas opciones en la tabla de
pedidos, sino un codigo numeric0 corto, proyectado sobre una descripcion en una
tabla de busqueda aparte. .
Esta ultima regla, que no deberia llevarse a1 extremo, ayuda a evitar tener que
agrupar un gran numero de tabla para cada consulta. Podemos tener en cuenta la
violacion de algunas de estas reglas de normalizacion en algunos casos (dejando
una breve descripcion del envio en la tabla de pedidos) o usar el programa cliente
para ofrecer la descripcion, con lo que acabariamos de nuevo con un diseiio de
base de datos formalmente incorrecto. Esta ultima opcion resulta practica solo
cuando usamos un entorno de desarrollo unico (corno Delphi) para acceder a la
base de datos.
NOTA: Muchos servidores de base de datos d a d e n identificadores de registro internos a las tablas, per0 solo lo hacen de cara a optimizaciones
internas v tiene ~ o c oaue ver con el diseiio 16nico de una base de datos
relational. Ademas, estos identificadores internos fincionan de un modo
diferente en servidores SQL diferentes y podrian incluso cambiar entre las
distintas versiones. una huena razcin Dara no fiarse de ellos.
meros unicos y consecutivos, sln dejar huecos en Ia secuencia de numeracion. Esta situacion resulta extremadamente compleja de controlar en un
programa, si tenemos en cuenta que solo la base de datos puede establece~
--A^-__---I- --_I---- -I-*-- 1la- -:esios
numerus C;unseCuiivos_'--:--unlws cuanuo
envlamos uaios nuevos a
rms-_'_-----
2 - -1
_.._A:L..A-_
I_:
--_-_-__--_L..___L__-I_
seguir usanao
claves susc~iulas
(SI nuttscra ttmpresa ttsla acosiumuraaa a
- _ A 1
ellas) junto con 10s OID, per0 todas las referencias externas a la tabla se
basarin en 10s OID.
Otra norma comun sugerida por 10s promotores de esta tecnica (que forma
parte de las teorias que apoyan la proyeccion relacional a objetos) es el uso
de identificadores unicos en todo e! sistema. Si tenemos una tabla de empreureguntarnos
uor auk
sas clientes v una tabla de em~leados.
, vodriamos
.
.
deberiamos usar un identificador unico para datos tan diversos. La razon es
que, si lo bacemos asi, podremos vender productos a un empleado sin tener
1 - :-r
:L- --L--a - - - I - - - I -- 1la- .raola
-LI1- -I:--&-L--:-que r e p e ~ ~
la
rmrorrnauon
swore GI
ernpleauo en
ue
cuenws, nawen-
----A:-
do sencillamente referencia at empleado en nuestro pedido o factura. AIguien identificado mediante un OID realiza un pedido y dicbo OID puede
remitir a varias tablas distintas. Usar identificadores OID y proyeccion
relacional a objetos es un elemento avanzado del disefio de aplicaciones de
bases de datos en Delphi. Es aconsejable investigar mhs acerca de este tema
antes de embarcarse en un proyecto medio o grande de Delphi porque 10s
beneficios pueden ser importantes (despues de una inversion en estudiar
este enfoque y crear un codigo bhico de soporte).
de relaciones como las ya mencionadas. Todos 10s servidores SQL pueden comprobar estas referencias externas, por lo que no podemos hacer referencia a un
registro no esistente de otra tabla. Estas restricciones de integridad referencial se
expresan cuando creamos una tabla.
Ademas de no poder aiiadir referencias a registros no esistentes, normalmente
se nos impide borrar un registro si existen referencias esternas a1 mismo. Algunos
servidores SQL van mas alla: cuando borramos un registro, en lugar de denegarnos la realization de dicha operacion, pueden borrar automaticamente todos 10s
registros de otras tablas que hagan referencia a el.
Mas restricciones
Ademas de la exclusividad de las claves primarias y de las restricciones
referenciales, generalmente podemos usar la base de datos para imponer mas
normas de validez para 10s datos. Podemos pedir que columnas concretas (como
las que se refieren a un identificador fiscal o a un numero de pedido de compra)
incluyan so10 valores unicos. Podemos imponer la exclusividad de 10s valores
para varias columnas, por e.jemplo, para indicar que no podemos dar clases en la
misma aula a la misma hora.
Por lo general, se pueden expresar normas sencillas para imponer restricciones
sobre una tabla, mientras que para las normas mas complejas, hay que ejecutar
procedimientos almacenados activados mediante disparadores (cada vez que 10s
datos cambian, por ejemplo, o que hay datos nuevos).
Una vez mas, hay muchos temas relacionados con un correct0 diseiio de bases
de datos. per0 10s elementos comentados en esta seccion serviran para proporcionar un bucn punto de partida.
Cursores unidireccionales
En bases de datos locales, las tablas son archivos secuenciales que tienen un
orden o bien fisico o definido por un indice. Por el contrario, 10s servidores SQL
funcionan en conjuntos de datos logicos, no relacionados mediante un orden fisico. Un servidor de bases de datos relacionales controla datos segun el modelo
relacional, un modelo matematico basado en una teoria fija.
Lo importante en este ambito cs que en una base de datos relacional, 10s registros (a veces llamados tuplas) de una tabla no se identifican por su posicion sino
exclusivamente mediante una clave primaria. basada en uno o mas canipos. Cuando
hemos obtenido un con.junto de registros, el servidor aiiade a cada uno de ellos
una referencia a1 siguiente, lo cual hace que sea mas rapido ir desde un registro a1
siguiente, per0 muy lento volver al registro anterior. Por esta razon, se suele decir
que un RDBMS usa un cursor unidireccional. Conectar una tabla o consulta de
este tipo a un control DBGrid resulta practicamente imposible, puesto que se
ralentizarian terriblemente las busquedas hacia atras en la cuadricula.
Algunos motores de bases de datos guardan en una cache 10s datos ya conseguidos, para soportal- una navegacion completamente bidireccional. En la arquitectura de Delphi. es el componente C 1i e n t D a t a S e t el que se encarga de esta
tarea, o algun otro conjunto dc datos con almacenamicnto local de datos. Veremos
este proccso mas detenidamente mas adelante. cuando nos centremos en dbEspress
y cl componente C l i e n t D a t a S e t .
Si retroceder pucdc originar problemas, tengamos cn cuenta que saltar a1 ultimo registro de una tabla puede resultar incluso peor. Normalmente esta operacion
conlleva la estraccion de todos 10s registros. En el caso de la propiedad
R e c o r d c o u n t dc 10s conjuntos de datos, tenemos una situation similar. Calcular el numero dc rcgistros implica normalmente llevarlos todos a1 ordenador clientc. Esta es la razon por la que el indicador de la barra dc desplazamiento vertical
del DBGrid funciona en el caso de una tabla local pero no de una remota. Si
necesitamos conocer el niimero de registros. hay que e.jecutar una consulta aparte
para permitir a1 servidor (y no a1 cliente) que la calcule. Por ejemplo, podemos
cuantos registros se seleccionaran de la tabla EMPLOYEE si estamos interesados
en que aquellos tengan un calnpo de salario (salary) mayor de 50.000:
select count ( * )
f r o m Employee
w h e r e Salary > 50000
Introduccion a InterBase
A pesar de su reducida cuota de mercado, InterBase es un potente RDBMS. En
esta seccion, presentaremos las principales caracteristicas tecnicas de InterBase
sin entrar en demasiado detalle (ya que este libro trata sobre Delphi). Lamentablemente, actualmente hay pocos titulos publicados sobre InterBase. La mayor
parte del material disponible es la documentacion que acompaiia a1 product0 o
que se encuentra en algunos sitios Web dedicados a ello (se puede comenzar la
busqueda en www.borland.com/interbase y www.ibphoenix.com).
InterBase se construyo desde el principio con una arquitectura moderna y
robusta. Su autor original, Jim Starkey, invent6 una arquitectura para manejar la
concurrencia y las transacciones sin imponer bloqueos fisicos sobre partes de las
tablas, alguno que otros servidores rnuy conocidos hoy en dia apenas hacen. La
arquitectura de InterBase se llama Multi-Generation Architecture (MGA); gestiona 10s accesos concurrentes a 10s mismos datos por parte de varios usuarios,
que pueden modificar 10s registros sin afectar a1 mod0 en que otros usuarios
concurrentes contemplan la base de datos.
Este enfoque se proyecta con naturalidad sobre el mod0 de aislamiento de
transacciones de lectura repetida, en el que el usuario que utiliza una transaccion
sigue viendo 10s mismos datos sin importar que se produzcan y confirmen cambios por parte de otros usuario. Tecnicamente, el servidor maneja la situacion
manteniendo una version diferente de cada registro a1 que se accede para cada
transaccion abierta. Incluso aunque este enfoque (que tambien se llama versionado)
puede llevar a un gran consumo de memoria, evita la mayoria de 10s bloqueos
fisicos sobre tablas y hace que el sistema resulte mucho mas robusto en caso de
problemas. MGA tambien empuja hacia un modelo de programacion rnuy claro,
la lectura repetible, que otros servidores SQL rnuy populares no soportan sin
perder la mayor parte de su rendimiento. Ademas de MGA como corazon de
InterBase, el servidor tiene muchas otras ventajas tecnicas:
Una ocupacion en memoria reducida: Hace que InterBase resulte el candidato ideal para ejecutarse directamente sobre ordenadores de cliente,
incluidos portatiles. El espacio de disco duro necesario para InterBase en
una instalacion minima esta por debajo de 10s 10 MB, y sus necesidades de
memoria son tambien rnuy reducidas.
Un buen rendimiento con grandes cantidades de datos.
Esta disponible en muchas plataformas distintas (como las versiones de
32 bits de Windows, Solaris y Linux), con versiones completamente compatibles. Por eso el servidor es escalable desde sistemas rnuy pequeiios a
sistemas gigantescos sin ninguna diferencia digna de mencion.
Un buen historial: Porque InterBase se esta usando desde hace 15 aiios,
con rnuy pocos problemas.
puiiado de empresas, InterBase ha sido escogido por varias empresas importantes, desde Ericsson a1 Departamento de Defensa de 10s Estados Unidnc
decde
mercndns
lnc
--.-,
--- -- -"------ de
-- cnmhin
-------- -a sistcmac de
-- hanca
---- rlnmkctica
------.-----. Entre- --.sucesos mas recientes se incluyen el anuncio de InterBase 6 como una base
de datos de codigo abierto (en diciembre de 1999), la publicacion efectiva
del codigo fuente para la comunidad (en julio de 2000) y la publicacion de
la version certificada oficial de LnterBase 6 por Borland (en marzo de 200 1).
Entre estos eventos se han producido anuncios de la derivacibn de una
emmesa inde~endienteDara "
nestionar 10s neaocios de consultoria v soDorte
ademas de la base de datos de codigo abierto. Un grupo de antiguos
desarrolladores y jefes de proyecto de InterBase (que dejaron Borland) for--------'I
I
-
. .
--
--:-- f
:L-L---:-- ---.\ - - - 1- :>->- -c----- ---maron rmn nr~n- o
e n ~ x1www.mpnoenlx.corn)
con la laea ue
orrecer soporre a-
10s usuarios de InterBase. A1 mismo tiempo, grupos independientes de expertos en InterBase comenzaron el proyecto de c6digo abierto Firebird para
extender mas alla InterBase. El proyecto se hospeda en SourceForge en la
direction sourceforge.net/projects/firebird/.Durante algun tiempo,
SourceForge tambien ha hospedado un proyecto de c6digo abierto de
Borland, pero mas tarde la empresa anuncio que continuaria soportando
unicamente la version propietaria, abandonando su esfuerzo de c6digo abierto. Asi, el escenario queda mas claro. Si se quiere una versi6n con una
1: ---- :- *-->:-:---1
f
----- una pequerra
x1- 1----- I-1lr;encla rraumonal [que
cuesm
parre ue
w que cues~anIUS
L-
--A-
.
A
A--
Uso de IBConsole
En las ultimas versiones de InterBase, se podian usar dos herramientas principales para interactuar directamente con el programa: la aplicacion Server Manager, que podia usarse para administrar tanto un servidor local como uno remoto,
y Windows Interactive SQL (WISQL). La version 6 incluye una aplicacion final
mucho mas potente, llamada IBConsole. Se trata de un programa para Window
muy completo (creado con Dclphi) que permite administrar, configurar. probar y
consultar a1 servidor InterBase, tanto en local como en remoto.
IBConsole es un sistema completo y sencillo para gestionar servidores InterBase
y sus bases de datos. Puede usarse para analizar 10s detalles de la estructura de la
base de datos, modificarla, consultar datos (lo que puede ser muy util para desarrollar las consultas que se quieran incluir en el programa), hacer copias de seguridad y recuperar la base de datos, y llevar a cabo otras tareas administrativas.
Como muestra la figura 14.1, IBConsole permite administrar varios servidores y bases de datos, todos ello en un simple arb01 de configuracion. Se puede
solicitar informacion general sobre la base de datos y mostrar sus entidades (tablas. dominios, procedimientos almacenados, disparadores y todo lo demas), accediendo a 10s detalles de cada una de ellas.
Tambien pueden crearse nuevas bases de datos y configurarlas, hacer copias
de respaldo de 10s archivos, actualizar las definiciones, consultar lo que sucedc,
quien se encuentra conectado, y cosas asi.
mole V w
Sewer
JP
839 ' * % I
3 InterBaseServels
-
"J LmalServer
- -- .-
--
--
_Descrptm
Drsconnect
Databases
Propeltles
'$
Database Stal~st~cs
D~splaydatabase std~st~cs
Shutdown
ShutdownIhe database
Sweep
38
-1
Sctmn
@ Domalns
Tables
V~ews
Stored Procedures
fx External Functions
Genelators
Except~ons
Blob Fltels
@ Roles
IBWintech
@ WlNTECH GDB
Backup
EB base
Sewer Log
@ USQS
wmlech-sewer
a
tb
%
0
8
Transact~on
Recovery Recovel lhmbo transactrons
V~ewMetadata
V~ewDatabase Metadata
Database Restart
Restart a database
Drop Database
DatabaseBackup
ConnectedUsers
RestoreDatabase
a
3
I
I
EMP-NO
FIRST-NAME
LAST NAME
[EMPNO] SMALLINT
[FIRSTNAME] VARCHAR(151
ILASTNAMEI VARCHARlZOl
HIRE-DATE
DEPT-NO
JOB CODE
JOB~GRADE
JOB-COUNTRY
SALARY
FULL-NAME
TIMESTAMP
[DEPTNO) CHAR01
IJOBCODEI VARCHARISI
~JOBGRADEJSMALLINT.
(COUNTRYNAME] VARCHARIlS]
[SALARY] NUMERIC[lS. 21
VARCHAR
DEFAULT 'NOW
DEFAULT 0
--
IC \
\exam&s\~atabese\e&
adb
--
Yes
No
No
No
No
No
No
Yes
---
Tables
Figura 14.2. IBConsole puede abrir ventanas independientes para rnostrar 10s
detalles de cada entidad (en este caso, una tabla).
.-
. . ..
l a 7 - @ - 8 1 B I h a I k % l f i=:. z .. .
1
k e l e c t last-name,
f r m employee
hire-date,
. . ..
.- .
--
salary
.!
A
Figura 14.3. La ventana Interactive SQL de IBConsole perrnite probar consultas que
se planee incluir en programas Delphi.
Procedimientos almacenados
Los procedimientos almacenados son como las funciones globales de una unidad Delphi, y deben llamarse explicitamente desde el lado del cliente. Los procedimientos almacenados suelen utilizarse para definir rutinas para el
mantenimiento de 10s datos, para agrupar secuencias de operaciones necesarias
en distintas situaciones, o para contener complejas sentencias select.
Al igual que 10s procedimientos de Delphi, 10s procedimientos almacenados
pueden tener uno o mas parametros con tipo. Por contra, pueden tener mas de un
valor de retorno. Como alternativa a devolver un valor, un procedimiento almacenado tambien puede devolver un conjunto de resultados (el resultado de una sentencia s e 1 e ct interna o un conjunto fabricado de forma personalizada).
El siguiente fragment0 de codigo es un procedimiento almacenado escrito para
InterBase: recibe una fecha como entrada y calcula el salario mas alto entre todos
10s empleados contratados en esa fecha:
create procedure MaxSalOfTheDay (ofday date)
returns (maxsal decimal ( 8 , 2 ) ) as
begin
s e l e c t max (salary)
from employee
where hiredate = :ofday
i n t o :maxsal;
end
Hay que prestar atencion a1 uso de la clausula into, que indica a1 servidor
que guarde el resultado de las sentencia select en el valor de retorno maxsal .
Para modificar o eliminar un procedimiento almacenado, se pueden usar mas
adelante 10s comandos alter procedure y drop procedure.
Si nos fijamos en este procedimiento almacenado, podriamos preguntarnos
cual es la ventaja en comparacion con la ejecucion de una consulta similar activada en el cliente.
La diferencia entre 10s dos enfoques no esta en el resultado conseguido, sin0 en
su velocidad. Un procedimiento almacenado se compila en el servidor en una
notacion intermedia mas rapida durante su creacion, y el servidor escoge en ese
momento la estrategia a utilizar para acceder a 10s datos. En cambio, una consulta se compila cada vez que se envia la peticion a1 servidor. Por este motivo, un
procedimiento almacenado puede sustituir a una consulta muy compleja, siempre
que no cambie muy a menudo.
Desde Delphi, se puede activar un procedimiento almacenado con el siguiente
codigo SQL:
select
from MaxSalOfTheDay
'Ol/Ol/ZOO3 ')
Disparadores (y generadores)
Los disparadores se comportan mas o menos como 10s eventos en Delphi, y se
activan automaticamente cuando se produce un evento determinado. Los
disparadores pueden tener codigo especifico o llamar a procedimientos almacenados; en ambos casos, la ejecucion se realiza completamente en el servidor. Los
disparadores se usan para mantener la consistencia de 10s datos, comprobar 10s
datos nuevos de un mod0 mas complejo que el que permite la verificacion de
restricciones, y para automatizar efectos secundarios de algunas operaciones de
entrada (como crear un registro de 10s cambios de sueldo anteriores cuando se
modifica el sueldo actual).
Los disparadores pueden activarse mediante tres operaciones basicas de actualizacion de datos: i n s e r t , u p d a t e y d e l e t e . Cuando se crea un disparador, se indica si se deberia lanzar antes o despues de una de estas tres acciones.
Como ejemplo de un disparador, podemos utilizar un generador para crear un
indice unico en una tabla. Muchas tablas utilizan un indice unico como clave
primaria. InterBase no tiene un campo AutoInc (de incremento automatico). Ya
que varios clientes no pueden generar identificadores unicos, se puede delegar en
el servidor para que haga esto. Casi todos 10s servidores SQL ofrecen un contador
a1 que se puede llamar para solicitar un nuevo identificador, que deberia usarse
mas tarde para la tabla. InterBase llama a estos contadores automaticos generadores, mientras que Oracle 10s llama secuencias: Este es el codigo de InterBase de
ejemplo:
c r e a t e generator cust-no-gen;
...
gen-id
(cust-no-gen,
1);
Este disparador se define para la tabla de clientes y se activa cada vez que se
inserta un nuevo registro. El simbolo new se refiere a1 nuevo registro que se
introduce. La opcion p o s i t i o n indica el orden de ejecucion de varios
disparadores conectados a1 mismo evento. (Los disparadores con 10s valores mas
bajos se ejecutan en primer lugar.)
Dentro de un disparador, se pueden escribir sentencias DML que actualicen
tambien otras tablas, per0 hay que prestar atencion a las actualizaciones que
acaban volviendo a activar el disparador y crean una recursion sin fin. Despues se
puede modificar o inhabilitar el disparador, mediante una llamada a las sentencias a l t e r t r i g g e r o d r o p t r i g g e r .
Los disparadores se activan automaticamente para 10s eventos especificados.
Si hay que hacer muchos cambios en la base de datos utilizando operaciones de
lotes, la presencia de un disparador puede ralentizar el proceso. Si 10s datos de
entrada ya se han comprobado en relacion a su coherencia, se puede desactivar
temporalmente el disparador. Estas operaciones de lotes suelen codificarse en
procedimientos almacenados, per0 en general estos procedimientos no envian sentencias DDL como las necesaria para desactivar y volver a activar el disparador.
En este caso, se puede definir una vista basada en un comando select * f r o m
table,creando asi un pseudonimo para la tabla. Despues se puede permitir que
el procedimiento almacenado realice el procesamiento de lotes sobre la tabla, y
aplique el disparador a la vista (que deberia utilizarse tambien por parte del
programa cliente).
La biblioteca dbExpress
Actualmente. el principal acceso a una base de datos de un servidor SQL en
Delphi lo proporciona la biblioteca dbEspress. Como mencionamos en el capitulo
13, no es la unica posibilidad, per0 es la mas usada. La biblioteca dbExpress se
present6 en Kylix y Delphi6, y permite acceder a varios servidores distintos
(IntcrBase. Oracle, DB2, MySql, Informix y ahora SQL Server de Microsoft).
Hemos ofrecido una vision general de dbExpress en comparacion con otras s o h ciones en cl capitulo anterior, asi que aqui nos saltaremos la presentation y nos
ccntraremos en elementos mas tecnicos.
NOTAt Es posible escribir controladores personalizad~spara la dkquitectura db&press. Esto estsl documentado con mayor detalle &el docmento
"dbExpms Draft Specification" publicado en el sitio Web dc Bodand
Community. Actualmente, este docurnento se encuentra en b&tp://'
c ~ . b o r l a n d . c o m / a r t i c l e / O14
, 1 O,224H,OO.htmI6~ & a b l k m ~ dpub
e
clan cncbntrarse controladores de terceros. Por ej&plo, c;uiste un ~ o n ~ o l p
dor ~ ~ iquet conecta
o
dbExpress y ODBC. Hay una Bsta cornpletar en el
articdd h ~ p : / / c m u n i t ~ . b o r l a n.comlarticle/0,14
d
f 0,2 b 7 T ~ ~ ~ .
/ / l e c t u r a de l a i n f o r m c i o n de v e r s i o n
Este fragment0 de codigo procede del ejemplo DbxMulti ya comentado. El programa lo utiliza para lanzar una escepcion si se trata de una version incompatible:
if GetDriverVersion ( 'dbexpint. d l 1 ' ) <> 7 then
raise Exception.Create (
'Incompatible version o f the dbExpress driver
"dbexpint.dlln found') ;
El componente SQLConnection
La clase TSQLConnecti o n hereda del componente TCustomConnection y
maneja conesiones a bases de datos, lo mismo que sus clases hermanas (10s componentes Database, ADOConnection e IBConnection).
,A
..,.Aa-*"
,",,,:c,..,
uarva, uw yvucauva G a p G u u L a I
AJ
,.-,
,.,A
U I I G ~ L Q I I I C ~ L GYUG
I...,,
A,
u a a UG
~
A,"
UULVB
.uam,
,,
"
:
,
a
SUIV
base de datos. Si se lee todo el archivo drivers.ini, veremos que 10s parametros
son en realidad especificos de la base de datos. Algunos de estos parametros no
tendran mucho sentido a1 nivel del controlador (como la base de datos a la que
conectar), per0 la lista incluye todos 10s parametros disponibles, sin importar su
USO.
Como podemos ver a1 comparar 10s dos listados, este es un subconjunto de 10s
parametros del controlador. Cuando creamos una nueva conexion, el sistema copiara 10s parametros predefinidos del controlador. A continuacion, podemos editarlos para la conexion especifica (proporcionando, por ejemplo, un nombre de
base de datos correcto). Cada conexion se relaciona con el controlador para cada
uno de sus atributos clave, como indica la propiedad DriverName. Hay que
tener en cuenta que la base de datos a que se hace referencia aqui es el resultado
de una edicion, de acuerdo con 10s parametros usados en la mayoria de 10s ejemplos. Lo importante es recordar que estos archivos de inicializacion se usan solo
en tiempo de diseiio. Cuando seleccionamos un controlador o una conexion en
tiempo de diseiio, 10s valores de dichos archivos se copian en las propiedades
correspondientes del componente SQLConnec t io n, como en este ejemplo:
object SQLConnectionl: TSQLConnection
ConnectionName = ' IBLocal '
DriverName = ' Interbase'
GetDriverFunc = 'getSQLDriverINTERBASET
LibraryName = ' dbexpint . dll'
Loginprompt = False
Params.Strings = (
' Blobsize=-1 '
'CodtRetain=Palse'
'Database=c:\Archives d e programa \Archives
comunes\Borland Shared\Data\employee.gdb'
' DriverName=Interbasel
end
automaticamente 10s valores de otros componentes SQLConnection que hagan referencia a la conexion mencionada: tenemos que volver a seleccionar la
conexion a la que se refieren dichos componentes.
Dliva Nanw
I Lb~wN
~ M
082
lnlelbase
DBEXPDBZ DLL
d b e ~dYl
dbrrpnys dl
O~acle
dbexpmd
I V&
lbra19
&2cb dl1
GDS32 DLL
LIBMYSOL dl1
0 0 DLL
Definirla como False permite proporcionar una contraseiia que se salte el cuadro de dialog0 de peticion de entrada a1 sistema, tanto en tiempo de diseiio como
de ejecucion. Aunque es algo practico para el desarrollo, puede reducir la seguridad del sistema. Por supuesto, deberia usarse tambien esta opcion para conexion
sin atencion, como las de un servidor Web.
enviar una orden. La propiedad CommandType establece uno de 10s tres modos
de acceso. Los posibles valores son ctQuery,ctStoredProc y ctTable,
que determinan el valor de la propiedad CommandText (y tambien el comportamiento del editor de la propiedad relacionada en el Object Inspector). En el caso
de una tabla o un procedimiento almacenado, la propiedad CommandText indica el nombre del elemento relacionado de la base de datos y el editor ofrece una
lista desplegable con 10s valores posibles. En el caso de una consulta, la propiedad CommandText almacena el testo de la orden SQL y el editor proporciona
algo de ayuda para crear la consulta SQL (en caso de que se trate de una sentencia
SELECT). Podemos ver el editor en la figura 14.5.
Add T d e lo SOL
---
--
FIRST-NAME
LAST-NAME
PHONE-EX1
HIRE-DATE
OEPT-NO
JOB CODE
~ d p
_I
con dos componentes compuestos (10s dos de dbExpress), ademas de un proveedor oculto. (El hecho de que el proveedor este oculto es extraiio, porque se crea
como un componente compuesto.)
El componente permite modificar las propiedades y eventos de 10s componentes compuestos (ademas del proveedor) y sustituir la conexion interna por una
externa, de manera que varios conjuntos de datos compartan la misma conexion a
la base de datos. Ademas de esto, el componente tiene otras limitaciones, como la
dificultad de manipulation de 10s campos del conjunto de datos de acceso a datos
(que es importante para configurar campos claw y puede afectar a1 mod0 en que
se generan las actualizaciones) y la ausencia dc algunos eventos de pro\feedor.
Por eso, aparte de para algunas aplicaciones sencillas, no resulta recornendable
usar el componente SimpleDataSet .
NOTA: Delphi 6 incluia un componente aun mas simple y limitado, llamado SQLClientDataSet. Existen componentes parecidos para las tecnologias
de acceso a datos BDE e IBX. ~ h o t aorl land indica &e todos estos componentes son obsoletos. Sin embargo, Demos\Db\SQLClientDataSet con. - .
. . - .
- - -.t~eneuna copla del componente ongmal, y se puede rnstalar en Delph17 por
motivos de compatibilidad. Pero se trata de un componente completamente
inutil.
El componente SQLMonitor
El ultimo componente del grupo dbExpress es SQLMonitor, utilizado para
registrar las solicitudes enviadas desde dbExpress al servidor de bases de datos.
Este componente de seguimiento permite ver las ordenes enviadas a la base de
datos y las respuestas recibidas a bajo nivel, haciendo un seguimiento del trafico
entre cliente y servidor a bajo nivel.
TSQLTimeStamp
padced record
Year : SmallInt;
Month : Word;
-.
--
day : Pidrd;
Hour : Word;
Minute : Word;
Second : Word;
Fractions : Longword;
end;
------^-:^-l-A
US~LUUU la yr u y ~ ~ u u u ud ~e I I I I E
1-
rn-,-.-L-",l--
AS
/-^-^-:-:A1\GIJ
uyusluuu
a la y ~ o -
piedad originaria A s SQLT imeSt amp). Tarnbien podemos realizar conversiones personalizadas y manipular aun mas las marcas de tiempo
utilizando las rutinas que ofrece la unidad SqlTirnSt, incluidas b c i o n e s
como DateTimeToSQLTimeStamp, S Q L T i m e S t a m p T o S t r y
VarSQLTimeStampCreate.
Como mencionamos antes, esta situacion se puede simplificar usando el componente SimpleDataSet, que sustituye 10s dos conjuntos de datos y el proveedor
(y posiblemente incluso la conexion). El componente SimpleDataSet combina la
mayoria de las propiedades de 10s componentes a 10s que sustituye.
Aplicacion de actualizaciones
En cada ejemplo basado en una cache local, a1 igual que el ofrecido por 10s componentes C 1 i e n t DataSe t y S i m p l e D a t a S e t , es importante escribir 10s cambios
Si queremos aplicar todas las actualizaciones en un unico lote, podemos hacerlo asi cuando se cierre el formulario o finalice el programa, o dejar que un usuario
realice la operacion de actualizacion seleccionando una orden concreta, posiblemente mediante la accion predefinida correspondiente que ofrece Delphi 7. Analizaremos este enfoque con mas detalle cuando comentemos el soporte de cache de
actualizacion del componente C 1i e n t D a t as e t mas adelante.
Seguimiento de la conexion
Otra funcion que hemos aiiadido a 10s ejemplos DbxSingle y DbxMulti, es la
capacidad de seguimiento ofrecida por el componente S Q L M o n i t o r . En el ejemplo, el componente se activa a1 iniciarse el programa. En el ejemplo DbxSingle,
ya que el S i m p 1e D a t a S e t incluye la conexion, el monitor no puede conectarse
a ella en tiempo de diseiio, sino solo cuando arranque el programa:
p r o c e d u r e TForml.FormCreate(Sender: TObject);
begin
SQLMonitor1.SQLConnection : = SimpleDataSet1.Connection;
SQLMonitorl.Active : = True;
S i m p l e D a t a S e t l - A c t i v e : = True;
end
Cada vez que hay una cadena de seguimiento disponible, el componente activa
el evento O n T r a c e para permitirnos decidir si incluir la cadena en el registro. Si
el parametro L o g T r a c e de dicho evento es T r u e (el valor predefinido), el componente registra el mensaje en la lista de cadenas T r a c e L i s t y activa el evento
O n L o g T r a c e para indicar que se ha aiiadido una nueva cadena a1 registro.
El componente tambien puede almacenar automaticamente el registro en el
archivo indicado por su propiedad F i l e N a m e , per0 no hemos usado esta funcion en el ejemplo. Todo lo que hemos hecho ha sido controlar el evento
O n L o g T r a c e , copiando todo el registro en el componente de memo mediante el
codigo siguiente (generando la salida que muestra la figura 14.6):
INTERBASE ~sc-cwnnwt-relammg
INTERBASE ~sc-dsql-free-statement
INTERBASE . ISC-start-lransact~on
INTERBASE - ISC-dsql-allocale-statement
#updaleEMPLOYEE set
PHONE-EXT = 7
where
EMP-NO = 9 and
FIRST-NAME = 7 and
LAST-NAME = 7 and
PHONE EXT = 7 and
HIRE-D~TE = ? a i d
DEPT-NO = ? a n d
JOB-CODE ? and
JOB-GRADE = ? and
JOB-COUNTRY - ? and
SALARY = 7 and
FULL-NAME = ?
INTERBASE .isc-dsql-prepare
INTERBASE .isc-dsql-sql-inlo
INTERBASE .sc-van-inlegel
INTERBASE - kc-ds@-describe-bimd
INTERBASE - SQLD~alect= 1
EMP-NO = ? and
PHONE-EXT = ?
update EMPLOYEE set
PHONE-EXT = ?
where
EMP-NO = ?
TRUCO: Este resultado es mejor que en Delphi 6 (sin aplicar 10s parches),
ya que esta operacibn causaba un error debido a que el campo clave no se
establecia correctamente.
Si queremos tener mas control sobre como se generan las sentencias de actualizacion, necesitamos trabajar con 10s campos del conjunto de datos subyacente,
que estan disponibles tambien cuando se usa el componente aglutinador
SimpleDataSet (que tiene dos editores de campos, uno para el componente basico
ClientDataSet del que hereda y otro para el componente SQLDataSet que
incluye).
Hemos corregido de un mod0 parecido el ejemplo DbxMulti, despues de aiiadir
campos permanente para el componente SQLDataSet y modificar las opciones
del proveedor para incluir algunos de 10s campos en la clave o escluirlos de las
actualizaciones.
TRUCO: El programa de ejemplo DbxExplorer incluido en Delphi muestra como acceder tanto a 10s archivos de administration de dbExpress como
a la informacion esquemitica. Tarnbih puede consultarse el archivo de
ayuda con la leyenda "The structure of metadatu datasets", en la seccion
"Developingdatabase applications".
REWO
I CATALOG-NAMEISMEMA-NME
( TABLE-NAME
1 <NIJLL>
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
SYSDBA
1TABLE-',
COUNTRY
CUSTOMER
DEPARTMENT
EMPLOYEE
EMPLOYEE_PROJECT
ITEMS
JOB
PHONE-LIST
PROJECT
PROJ-DEPT-BUDGET
SAIARY-HISTORY
SALES
Figura 14.7. El ejemplo SchemaTest permite ver las tablas de una base de datos y las
columnas de una tabla dada.
:country
'
PaamType
~tecision
ptlnpd
Io
'All shown
Despucs de activar esta consulta, el programa analiza el conjunto de resultados, estraycndo todos 10s valores y aiiadicndolos a1 cuadro de lista:
p r o c e d u r e TQueryForm.FormCreate(Sender: TObject);
begin
SqlDataSet2.0pen;
while n o t SqlDataSet2.EOF d o
begin
ComboBoxl.Items.Add (SqlDataSet2.Fields [O].AsString);
SqlDataSet2.Next;
end;
ComboBoxl .Text : = CombBoxl. Items [ O ] ;
end ;
Este codigo muestra 10s cmpleados del pais seleccionado cn la DBGrid, tal y
como muestra la figura 14.9.
Como alternativa al uso de 10s elementos de la matriz Params por posicion,
podria considerarse el uso del metodo ParamByName;para evitar cualquier tipo
dc problema en caso de que la consulta se acabe modificando y 10s parametros
adoptcn un orden distinto.
England
EMP-NO
IFIRST-NAMEIWT-NAME
28 Ann
36 Roger
37 W ~ l b
Eennel
Reeves
Stanshy
IPHONE-EXT HIRE-DAVE
5
6
7
2/1/1991
412511991
4/25/1931
IDEPT-NO(J~
120
120
Ad
120
En-
Sa
TRUCO:Las consultas parametricas suelen emplearse tambien para conseguir arquitecturas maestroldetalle con consultas SQL, a1 menos esto es lo
que suele hacerse en Delphi. La propiedad Datasource del componente
SQLDataSet, sustituye automaticamente 10s valores de 10s parametros con
10s campos del conjunto de datos maestro que tengan el mismo nombre que
el p a r h e t r o .
Para controlar la impresion, hemos escrito una rutina en cierto mod0 generica,
que requiere como parametros 10s datos que se van a imprimir, una barra de
progreso para la informacion de estado, la fuente de salida y el tamaiio de formato
maximo de cada campo. Toda la rutina usa el soporte de impresion de archivo y
da formato a cada campo con una cadena de tamaiio fijo, alineada a la izquierda
para producir un tipo de informe en columna. La llamada a la funcion Format
posee una cadena de formato parametrica creada de forma dinamica usando el
tamaiio del campo.
En el listado 14.1 se puede ver el codigo del metodo PrintOutDataSet
principal, que utiliza tres bloques try/ fina 11y anidados para liberar todos
10s recursos del mod0 correcto:
Listado 14.1. El rnetodo principal del ejemplo UniPrint.
procedure PrintOutDataSet (data: TDataSet;
progress: TProgressBar; Font: TFont; toFile: Boolean;
maxSize: Integer = 3 0 ) ;
var
PrintFile : TextFile;
I: Integer;
sizeStr: string;
oldFont: TFontRecall;
begin
// a s i g n a l a s a l i d a a l a i m p r e s o r a o a u n a r c h i v o
if toFile then
begin
SelectDirectory ( ' C h o o s e a f o l d e r ' , ' ', strDir) ;
AssignFile (PrintFile,
I n c l u d e T r a i l i n g ~ a t h D e l i m i t e r(strDir) + ' o u t p u t . t x t ' ) ;
end
else
AssignPrn (PrintFile);
// a s i g n a l a i m p r e s o r a a u n a r c h i v o
AssignPrn (PrintFile);
Rewrite (PrintFile);
// d e f i n e l a f u e n t e y m a n t i e n e l a o r i g i n a l
oldFont : = TFontRecall-Create (Printer.Canvas.Font);
try
Printer.Canvas.Font : = Font;
try
data.Open;
try
Writeln (PrintFile);
El programa recurre a esta rutina cuando se hace clic sobre el boton Print All.
El programa ejecuta una consulta independiente (select count ( * ) f r o m
EMPLOYEE),
que devuelve el numero de registros de la tabla de empleados. Esta
consulta es necesaria para preparar la barra de progreso (el conjunto de datos
unidireccional, en realidad, no tiene forma alguna de conocer el numero de registros que va a recuperar hasta que ha alcanzado el ultimo). A continuacion, define
la fuente de la salida, usando posiblemente una fuente con un ancho fijo, y llama
a la rutina PrintOutDataSet:
p r o c e d u r e TNavigator.PrintAllButtonClick(Sender: TObject);
var
Font: TFont;
begin
// d e f i n e e l r a n g o d e l a P r o g r e s s B a r
EmplCountData.Open;
try
ProgressBarl .Max : = EmplCountData. Fields [0] .AsInteger;
finally
Emp1CountData.Close;
end;
Font : = TFont .Create;
try
Font. Name : = ' C o u r i e r New' ;
Font.Size : = 9;
PrintOutDataSet (EmplData, ProgressBarl, Font) ;
finally
Font. Free;
end;
end:
..,.
Manipulacion de actualizaciones
Una de las ideas principales quc esta tras el componente C l i e n t D a t a S e t
es que se utiliza como una cache local para obtener la entrada de un usuario y. a
continuacion, enviar un lote de solicitudes de actualizacion a la base de datos. El
componente posee tanto una lista de 10s cambios que se van a aplicar al servidor
de la base de datos, almacenada en el mismo formato usado por el
C l i e n t D a t a S e t (accesiblc a travcs de la propiedad D e l t a ) , como un completo registro de actualizaciones que podemos manipular con algunos metodos
(incluyendo la capacidad de deshacer 10s cambios).
-
TDataSet);
U W ~ StxwDda
-.
.-
Data
1
. ....
--
_I
- -
- - ..
- ---
JDEPT-NO(EMP-NO
[FIRST-NAME
usUnmodlied
usUnmod111ed
usUnmod~fied
urUnmodilied
usModrfied
uslnserled
115
125
100
123
623
621
672
622
622
118 Takash
121 Roberto
127 Mihael
134 Jacques
136 colt
138 T.J
144 John
145 Mark
146 John
[LAST-NAME
Yamamlo
Ferran
Yanawski
Glon
Johnson
Green
Montgomery
Guckenhr
Rohd
IPHDWE-~~T~~ALWY
23
1
432
937
265
218
820
931
932
Acceso a Delta
Mas alla de examinar el estado de cada registro, el mejor mod0 de entender que
cambios han sucedido en un ClientDataSet dado (pero no se han cargado aun a1
scrvidor) consiste en fijarse en el delta, la lista de cambios que esperan ser aplicados a1 servidor. Esta propiedad se define como sigue:
property Delta: Olevariant;
En el ejemplo CdsDelta, hemos aiiadido un modulo de datos con 10s dos componentes C l i e n t Da t a S e t y una fuente de datos: un SQLDataSet proyectado
sobre la tabla EMPLOYEE de muestra de InterBase. Ambos con.juntos de datos
de cliente tienen el campo calculado adicional de estado (status), con una version
ligeramente mas gentrica que el codigo comentado antes, porque el controlador
de eventos es compartido entre ambos.
-
SWW
IDEPT-NOIEMP-NO
-) usllnrnnd~kd
- usMwhf~ed
600
- wModtfd
IFIRST-NAME
2 Robert
(LAST-NAME
uslnsertcd
- usUnrndhed
622
622
146 Jahn
145 Mak
Rdand
Guckenhec
usD&ed
- usUmd~hed
? 21
7 23
141 Pmre
134 Jacques
Osba~ne
-usModltnj
IPHONE-EXTISALARY
250
251
932
221
91
Nelson
105900
32Q30
3M(JO
llOOW
390W
Gbn
937
A
Figura 14.1 1. El ejemplo CdsDelta permite ver las solicitudes de actualizacion temporal
almacenadas en la propiedad Delta del ClientDataSet.
-
- - -
.-
-- - ~ .
El valor raSkip: Indica que el servidor deberia omitir el registro conflictivo, dejandolo en el delta (este es el valor predefinido).
El valor raAbort: Dice a1 servidor que interrumpa toda la operacion de
actualizacion y que ni siquiera intente aplicar 10s cambios que quedan en la
lista en delta.
El valor raMerge: Dice a1 servidor que mezcle 10s datos del cliente con
10s datos del servidor, aplicando solo 10s campos modificados de este cliente
(y manteniendo 10s otros campos modificados por otros clientes).
El valor racorrect: Dice a1 servidor que sustituya sus datos por 10s datos
actuales del cliente, sobrescribiendo todos 10s cambios de campo ya realizados por otros clientes.
El valor racancel: Cancela la solicitud de actualizacion, eliminado la
entrada del delta y recuperando 10s valores extraidos originalmente desde
la base de datos (ignorando asi 10s cambios realizados por otros clientes).
El valor raRefresh: Dice a1 servidor que deseche todas las actualizaciones del delta de cliente y las sustituya por 10s valores que estan actualmente en el servidor (guardando asi 10s cambios realizados por otros clientes).
Para verificar una colision podemos lanzar dos copias de la aplicacion cliente,
modificar el mismo registro en ambos clientes, y enviar despues las actualizaciones de ambos. Haremos esto mas adelante para generar un error, per0 por ahora
vamos a ver como controlar el evento O n R e c o n c i l e E r r o r .
Controlar este evento no es demasiado dificil, per0 solo porque se nos proporciona algo de ayuda. Ya que crear un formulario especifico para controlar el
evento OnReconci l e E r r o r es muy comun, Delphi ya proporciona dicho formulario en el Object Repository (disponible mediante las opciones de menu
File>New>Otherdel IDE de Delphi). Sencillamente hay que ir a la pagina Dialogs
y seleccionar el elemento Reconcile Error Dialog. Esta unidad exporta una
funcion que podemos usar directamente para inicializar y mostrar el cuadro de
dialogo, como hemos hecho en el ejemplo CdsDelta:
p r o c e d u r e TDmCds.cdsEmployeeReconcileError (DataSet:
TCustomClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind; v a r Action:
TReconcileAction) ;
begin
A c t i o n : = HandleReconcileError(DataSet, UpdateKind, E);
end:
-- - - --
--
Free;
end;
end;
-R
c Skip
Cbnd
C Coned
C Rdmh
C Mape
Fild Name
EMP-NO
PHONE-W
M d f i Vdue
I CUnchanaed,
Ih f l i c ! i n g ~ a b a
--
10iipnd ahr re
tunchanoed,
<Unchanged,
<Unchanged>
Robert
<Unchanged>
<Unchanged>
Nelson
tUnchmgecb
<Unchanged>
105900
251
333
250
Figura 14.12. El dialogo Reconcile Error que ofrece Delphi en el Object Repository y
que usa el ejemplo CdsDelta.
Uso de transacciones
Si trabajamos con un servidor SQL, deberiamos usar transacciones para que
nuestras aplicaciones fbese mas robustas. Podemos pensar en una transaccion
como una serie de operaciones que se consideraran como un todo unico, "atomicow,que no se puede dividir.
Un ejemplo puede ayudar a aclarar la idea. Supongamos que tenemos que
aumentar el sueldo de cada empleado de una empresa un tanto por ciento dado,
como en el ejemplo Total del capitulo 13. Un programa tipico ejecutaria una serie
de sentencias SQL en el servidor, una para cada registro que necesite ser actualizado. Si se produjera un error en esa operacion, podriamos deshacer 10s cambios
anteriores. Si se considera la operacion "aumentar el sueldo de cada empleado"
como una unica transaccion, deberia realizarse completamente o ignorarse completamente. 0, podemos considerar la analogia con las transacciones bancarias:
si un error provoca que solo se realice parte de la operacion, podriamos acabar
con dinero de menos o de mas.
Trabajar con operaciones de bases de datos como transacciones resulta muy
util. Se puede comenzar una transaccion y realizar varias operaciones que deberian considerarse todas ellas como parte de una operacion mayor. Entonces, a1
final, se pueden confirmar 10s cambios o deshacer la transaccion, descartando
todas las operaciones realizadas hasta el momento. Lo mas tipico es que se quiera
deshacer una transaccion si se produce un error en alguna de sus operaciones.
Existe otro punto que merece la pena resaltar: las transacciones sirven tambien
durante la lectura de datos. Hasta que 10s datos Sean confirmados por una transaccion, otras conexiones y/o transacciones no deberian verlos. Una vez que se
hayan confirmado 10s datos procedentes de una transaccion, otras deberian ver el
cambio cuando lean 10s datos, es decir, a menos que se necesite abrir una transaccion y volver a leer 10s mismos datos para realizar un analisis de 10s mismos o
complejas operaciones de generacion de informes. Distintos servidores SQL permiten leer datos en una transaccion de acuerdo con alguna de o todas estas alternativas, como veremos cuando analicemos 10s niveles de aislamiento de
transacciones.
Es muy sencillo controlar las transacciones en Delphi. Por defecto, cada operation de edicion y rnodificacion se considera como una transaccion implicita
unica, per0 podemos modificar este comportamiento controlando las operaciones
explicitamente. Simplemente usamos 10s siguientes tres metodos del componente
SQLConnection de dbExpress (otros componentes de conexion de bases de datos
tienen metodos similares):
saccion como ultima operacion del bloque t r y , que se ejecutara solo cuando no
se produzcan errores. El codigo podria tener este aspecto:
var
TD: TTransactionDesc;
begin
TD.TransactionID : = 1;
TD.IsolationLeve1 := xilREADCOMMITTED;
SQLConnectionl.StartTransaction(TD);
t rY
/ / - - l a s o p e r a c i o n e s d e la t r a n s a c c i o n v a n a q u i - - SQLConnectionl .Commit (TD);
except
SQLConnectionl .Rollback (TD);
end;
TRUCO:Comd sugcrencia general, por motivos de rendimicnto, lai tranwcciones deberlan irnplicar un numero minimo de actualizaciones (solo
quellas estrictamdte %dadonadasentre si y parte de una linica operacion
Btomica) y deberl'an romar poco tiempo. Deberian evitarse transacciones
&e apcren enfrada del uruario gara completane, ya que el usuario podria
..
rapidas, porque se puede abrir una transaccion para lectura, cerrarla, y desputs abrir una transaccion para escribir todo el lote de cambios.
El otro campo del registro TTransac t ionDesc contiene un identificador
dc transaccion. Solo es util junto con un servidor de bascs de datos que soporte
varias transacciones concurrentes sobre la misma conexion, como InterBase. Se
puede preguntar a1 componente de conexion si el servidor soporta varias transacciones o si no soporta las transacciones en absoluto, usando las propiedades
MultipleTransactionsSupported y Transactionssupported.
Cuando el servidor soporta transacciones multiples, hay que proporcionar a
cada transaccion un identificador unico cuando se llame a1 metodo S t a r t T r a n saction:
var
TD: TTransactionDesc;
begin
TD-TransactionID : = GetTickCount;
TD.IsolationLeve1 : = xilREADCOMMITTED;
SQLConnectionl.StartTransaction(TD);
SQLDataSet1.TransactionLevel : = TD.TransactionID;
IBSQL: Nos permite ejecutar sentencias SQL que no devuelven un conjunto de datos (por ejemplo, solicitudes DDL o sentencias u p d a t e y
d e l e t e ) sin la sobrecarga de un componente de conjunto de datos.
IBDatabaseInfo: Se usa para consultar la estructura y estado de la base
de datos.
IBSQLMonitor: Se utiliza para depurar el sistema, puesto que el depurador SQL Monitor que ofrece Delphi es una herramienta especifica de BDE.
IBEvents: Recibe eventos enviados por el servidor.
Este grupo de componentes ofrece mayor control sobre el servidor de bases de
datos del que podemos conseguir con dbExpress. Por ejemplo, tener un componente especifico de transaccion permite controlar varias transacciones concurrentes en una o varias bases de datos, asi como una transaccion unica que se realice
sobre varias bases de datos. El componente IBDatabase permite crear bases de
datos, comprobar la conexion y, normalmente, acceder a datos del sistema, algo
que 10s componentes Database y Session de BDE no permiten del todo.
TUCO:
un
FROM E M P L O Y E E ' )
end
o b j e c t IBDatabasel: TIBDatabase
DatabaseName = ' C : \Archives d e p r o g r a m \ InterBase ' +
'Corp\InterBase6\examples\Databd~e\employee.gdb'
Params .Strings = (
' user-name=SYSDBA1
' p a s s w o r d = m s t e r k e y ')
Loginprompt = False
IdleTimer = 0
SQLDialect = 1
TraceFlags = [ I
end
ADVERTENCIA: Fijese en que hemos incluido la contrasefia en el d i go, una ttcaica de'seguW bastante inocente. No r& @& ejecutar el
p r o p m a euaIqnier persma, sho que d m S s alguiea podria extraer hduso ?accantraseiia fijhdase e n d c w o hexadecimal dd rlrchivo ejeoutable.
us& esta tecnica para que no fuese necegario &crib@una y otra
vez q e s * contrasefia p1:pro'Liar 81p t ~ g mper6
, en una a @ l i c a d bred
deberiarnoi!pedir a h usuanos @e lo hickran asi, si Wremos gariintixar
la segutidad de nuestros datos.
'
'
'
'
'
'
end
ferencias entre usar 10s dos componentes o el componente unico son minimas.
Usar IBQuery e IBUpdateSQL es un enfoque mejor cuando se adapta una aplicacion ya existente basada en 10s dos componentes BDE equivalentes, aunque si se
adaptara el programa directamente a1 componente I BDataSet,no seria necesario un esfuerzo adicional muy grande.
En el ejemplo IbxUpdSql, hemos proporcionado ambas alternativas para que
puedan probarse directamente las posibles diferencias. Este es el esqueleto de la
definicion DFM del componente de conjunto de datos:
o b j e c t IBDataSetl: TIBDataSet
Database = IBDatabasel
Transaction = IBTransactionl
DeleteSQL-Strings = (
' d e l e t e f r o m EMPLOYEE'
' w h e r e EMP-NO = :OLD-EMP-NO')
InsertSQL-Strings = (
' i n s e r t i n t o EMPLOYEE'
' (FIRST-NAME, LAST-NAME, SALARY, DEPT-NO, JOB-CODE,
JOB-GRADE, ' +
' JOB-COUNTRY) '
'values'
' (:FIRST-NAME, :LAST-NAME, :SALARY, :DEPT-NO,
:JOB-CODE, ' +
' :JOB-GRADE, :JOB-COUNTRY) ' )
SelectSQL.Strings = ( . . )
UpdateRecordTypes = [cusunmodified, cusModified,
cusInserted]
ModifySQL.Strings = ( . . . )
end
Otra caracteristica de este ejemplo es la presencia de un componente de transaccion. Como ya hemos dicho, 10s componentes de InterBase Express utilizan un
componente de transaccion obligatorio, siguiendo de forma explicita un requisito
de InterBase. Bastaria con sencillamente aiiadir un par de botones a1 formulario
para confirmar o deshacer la transaccion, porque se inicia una transaccion de
forma automatica cuando editamos cualquier conjunto de datos conectados a la
misma. Tambien hemos mejorado el programa ligeramente aiiadiendole un componente ActionList. Este incluye todas las acciones estandar de bases de datos y
aiiade dos acciones personalizadas para soporte de transacciones: commit y
R o l l b a c k . Ambas acciones se habilitan cuando la transaccion esta activa:
procedure
begin
TForml.ActionUpdateT~ansactions(Sender: TObject);
acCommit.Enabled : = 1BTransactionl.InTransaction;
acRollback.Enabled : = acCommit.Enabled;
end;
TObject);
ADVERTENCIA: Debemd Wer en euenta que LntkrBase cierra cudquier cursor ihierto cnanilkifid h a una trans&i6n, lo coal significa que
tenemos
que reabrirlo
. - .' )y-v.oher a adquirir
- 10s datos,
. .aunque no hayarnos
-. hecho cas~blos.Bn cambib, cumdo continnirmos 10s datos podemos pedtrle
a Interwe que. Verigi el "amtptto de transa&ibp1?(que no rcierre 10s
conjut$& ck,
abiartoij;,&do uqa b r h ~ogmit~etaining,
cmo
ya meficioiamott. L& d ' d eeste winpottamiedto i28 InteiSw 6s que una
trmsaccih se coneqxmde con una instantheor i
k lox dams. thando la
tfansa&Bn iia termhtatlo.. se ~~e
que 18b r d m i & ndetro para
voher a ewaer 10s regigtros que pdedad habm si& mcrdificadcm*porotros
US~&&.
La v e ~ i S n60
. tie InftrBase lndh$e tambftn bnzt orden
~ o $ . & b akc~ e t ining,
a
que Bemqs.$eo;dndp nvusar, porque ep ma opera&n de retorno o rolback, &program& deberia refrescar 10s &tog del
conjunto de datos para mosq* loa valc&~ originales eq.pantalh n~ las
actualizaciones que hemos descartado,.
La ultima operacion se refiere a un conjunto de datos generic0 y no a uno
especifico porque vamos a aiiadir un segundo conjunto de datos alternativo a1
programa. Las acciones estan conectadas a una barra de herramientas de solo
texto, como muestra la figura 14.14. El programa abre el conjunto de datos a1
212850 0
De Sowa
15 Kalherme
Customer S ~ v c e o
Cuslomar Sewces
Customer Support
Cwtmsr Support
Cwlmer S ~ p o l t
Customr S u p 1 1
Customer Sqlpolt
Engneermg
Engneumg
Ewcpean Headquafiers
Euopean HsadqvaRers
EwopeanHeadquarters
Enpneer
Manager
Engnear
Enpneec
Engmeer
Manaper
T e c t w dW r m
Vm P~eodanl
Admrr&abve Assrdanl
Sdes h r d n a l a
Admrustrake Asrdanl
Eng~lee~
35000 6
56295 6
69482 63 6
56034 38 6
35630 667241 29 6
6W00 6
1E900 6
27000 6
33620 63 1
2335 1
3922406 1
2L
Figura 14.14. La salida del ejemplo IbxUpdSql.
Si ejecutamos 10s dos ejemplos a1 mismo tiempo, la salida del programa IbxMon
mostrara en una lista 10s detalles de la interaccion del programa IbxUpdSql con
InterBase, como muestra la figura 14.15.
_~ai:l
Slat~ibcsI S a w Froperk: I Utao 1
b 4753PM
[Appbcabon Ibxupdsql]
IBDalabasel [Connect]
6 47 53 PM
[Applrca~onIbxupdsql]
IBTransacllonl [Stad lransact~on]
6 47 53 PM
[Appi catton Ibxupdsql]
IBDataSell Prepae] SELECT Employee EMP-NO, Ewloyee FIRST-NAME. EmployeeLAST-NAME,
Department DEPARTMENT Job JOB-T ITLE, Employee SALARY Employee DEPT-NO Employee JOB-CODE
EmployeeJ 00-GRADE, Employee JOB-COUNTRY
FROM EMPLOYEE Employee
INNER JOlN DEPARTMENT Department
ON (Oepaltment DEPT-NO = EmployeeDEPT-NO)
INNER JOlN JOB Job
ON @&JOB-CODE EmployeeJOB-CODE]
AND [JobJOB-GRADE = EmployeeJOB-GRADE]
AND [JobJOB-COUNTRY = Employee JOB-COUNTRY]
ORDER BY Depa~tmentDEPARTMENT
Pbm PLAN SORT [JOIN [EMPLOYEE NATURALJOB INDEX [RDBSFRlMARY2),DEPARTMENT INDEX [RDB
PRIMARYS)]]
6 47 53 PM
[Appl~catlon Ibmpdsqfl
IBDataSetl [Prepare] delete lrom EMPLOYEE
whera
EMP-NO OLD-EMF-NO
11
IbxMon.
/ / o b t i e n e 10s d a t o s d e l u s u a r i o
1BSecurityServicel.DisplayUsers;
// muestra el nombre d e cada u s u a r i o
f o r i : = 0 t o IBSecurityServicel.UserInfoCount - 1 do
w i t h IBSecurityServicel.UserInfoli] do
RichEdit4 .Lines .Add (Format ( ' U s e r : % s , F u l l N a m e : % s ,
Id: %dl,
[UserName, FirstName + ' ' + LastName, UserId] ) ) ;
nArl;nn
V V U ~
mar-
net-
~
p V
a a a UJCU
.ran;t..ln
CP
m ~ r l ; n n tT~n t ~ r R o n mP
nncnl~
UVUPV~CI,
mmmnrl~o m - l ; v a r
u a p ~ b u n u .U C I Y
&aU
i fU
uaU
l C
l + I(
I ~ L U U I ~ ~ C CUCCIIYQJU
I
Generadores e identificadores
Como hemos dicho antes, somos partidarios del uso de identificadores para
identificar 10s registros de cada tabla de una base de datos.
-
--
--
) as
select gen-id (g-master, 1) f r o m rdbSdatabase
f r o m v-next-id;
Este metodo puede llamarse en el evento A f ter Insert de cualquier conjunto de datos, para rellenar el valor del identificador:
mydataset.FieldByName
('ID').AsInteger
:=
data-GetNewId;
Como he mencionado, 10s conjuntos de datos IBX se pueden enlazar directamente a un generador, simplificando asi el esquema global. Gracias a1 editor de
propiedad especifico (que muestra la figura 14.17), conectar un campo del conjunto de datos a1 generador resulta trivial.
r OnPost
.-
I
I
Fijese en que ambos enfoques son mucho mejores que el enfoque basado en un
disparador de servidor, comentado con anterioridad. En ese caso, la aplicacion
Delphi no conocia el identificador del registro enviado a la base de datos y no
podria refrescarlo. A1 no tener el identificador del registro (que es ademas el
unico campo clave) en Delphi, significa que resulta casi imposible insertar directamente un valor de este tip0 en una DBGrid. Si se intenta, se vera que se pierde
el valor que se inserta, y solo vuelve a aparecer en caso de realizar un refresco
total.
Usar tecnicas de cliente basadas en codigo manual o en la propiedad
G e n e r a t o r F i e 1d no provoca problemas. La aplicacion Delphi conoce el
identificador (la clave del registro) antes de enviarlo, por eso puede colocarlo
facilmente en una cuadricula y refrescarlo correctamente.
id
d-uid not n u l l ,
name
varchar ( 5 0 ) ,
tax-code
varchar(l6),
name-upper
varchar ( 5 0 ) ,
constraint companies-pk primary key
(id)
) ;
Para copiar el nombre en mayusculas de cada empresa en el campo relacionado, no podemos confiar en el codigo de cliente, ya que una inconsistencia de 10s
datos causaria problemas.
En un caso como este, es mejor usar un disparador en el servidor, de tal mod0
que cada vez que cambie el nombre de la empresa, su version en mayusculas se
actualice de manera apropiada. Para insertar una nueva empresa, se utiliza otro
disparador:
create t r i g g e r companies-bi f o r companies
a c t i v e before i n s e r t p o s i t i o n 0
as
begin
new. name-upper = upper (new.name) ;
end;
c r e a t e t r i g g e r companies-bu f o r companies
a c t i v e before update p o s i t i o n 0
as
begin
i f (new.name <> old.name) then
new. name-uppe r = upper (new.name) ;
end;
Por ultimo, hemos aiiadido un indice a la tabla con esta sentencia DDL:
create index i-companies-name-upper
on companies(name-upper) ;
Con esta estructura interna, podemos seleccionar todas las empresas que
comiencen con el texto del cuadro de edicion (edsearch) escribiendo el siguiente
codigo en una aplicacion Delphi:
dm.DataCompanies.Close;
dm.DataCompanies.Se1ectSQL.Text
:=
' s e l e c t c . i d , c.name, c. tax-code,'
' from companies c ' +
' w h e r e name-upper s t a r t i n g w i t h
Uppercase (edsearch.Text) + ' ' ' ' ;
dm.DataCompanies.0pen;
"'
Como alternativa, se podria crear un campo calculado de servidor en la definicion de tabla, per0 hacer esto impediria tener un indice en el campo, que acelera
las consultas en gran medida:
name-upper
(upper (name))
id
d-uid not null,
id-company
d-uid not null,
varchar ( 4 0 ) ,
address
town
varchar ( 3 0 ) ,
varchar ( 10 ) ,
zip
state
varchar (4) ,
phone
varchar ( 15 ) ,
varchar ( 15 ) ,
fax
constraint locations-pk primary key (id),
constraint locations-uc unique (id-company,
id)
) ;
datos, deberiamos aiiadir a esta tabla solo una referencia a la ubicacion, puesto
que cada ubicacion esta relacionada con una empresa. Sin embargo, para que
resulte mas sencillo modificar la ubicacion de una persona en una empresa y para
que las consultas sean mas eficientes (evitando un paso adicional), hemos aiiadido
a la tabla sobre personas una referencia a la ubicacion y una referencia a la
empresa. La tabla tambien tienen otra caracteristica no habitual: una de las personas que trabaja para una empresa se puede definir como el contacto clave. Para
ello se usa un campo booleano (definido con un dominio, dado que InterBase no
soporta el tip0 booleano) y aiiadiendo disparadores a la tabla para que solo un
empleado de cada empresa tenga activo dicho indicador:
c r e a t e domain d-boolean a s c h a r (1)
default ' P'
check (value i n ( ' T' , ' P ' ) ) n o t n u l l
c r e a t e t a b l e people
(
id
d-uid n o t n u l l ,
id-company
d-uid n o t n u l l ,
id-location
d-uid n o t n u l l ,
name
v a r c h a r (50) n o t n u l l ,
v a r c h a r ( 15 ) ,
phone
v a r c h a r ( 15 ) ,
fax
email
v a r c h a r (50) ,
key-contact
d-boolean,
c o n s t r a i n t people-pk primary key (id),
c o n s t r a i n t people-uc unique (id-company, name)
) ;
end;
c r e a t e t r i g g e r people-au f o r people
a c t i v e a f t e r update p o s i t i o n 0
as
begin
/ * si una persona es el contacto clave, elimina el
indicador de todas las d e d s (de la misma empresa) * /
if (new.key-contact = ' T' and old.key-contact = ' F' ) then
update people
set key-contact = ' F '
where id-company = new.id-company
and id <> new.id;
end;
end;
procedure TDmCompanies.DataPeopleAfterInsert(DataSet:
TDataSet) ;
begin
/ / inicializa 10s datos del registro detalle
// con una referencia a1 registro maestro
DataPeopleID-COMPANY.AsInteger
:= DataCornpaniesID.As1nteger;
/ / la ubicacion sugerida es la activa, si estd disponible
if not DataLocations.IsEmpty then
DataPeopleID-LOCATION-AsInteger : = DataLocationsID.As1nteger;
// la primera persona que se afiade se transform en el
contacto clave
// (verifica si el conjunto de datos de personas filtrado
// estd vacio)
DataPeopleKEY-C0NTACT.AsBoolean : = DataPeople.IsErnpty;
end;
ILI
I~D-COMP~ID-LOCPTIONIKEY_CONTACT~WE
ID
I
13
14
3
9
11 T
11 F
1 ~ ~ 6 9 1 ~
I.;
Chuck J
David l
rn
"
';
dm.DataPeople.0pen;
end;
end ;
Reserva de clases
Parte del programa y de la base de datos esta relacionada con la reserva de
clases y cursos de formacion (aunque este programa se creo como demostracion,
tambien resulta practico.) En la base de datos existe una tabla con clases que lista
todos 10s cursos de formacion, cada uno con un titulo y una fecha fijados. Otra
tabla contiene la matricula por empresa, incluyendo las clases matriculadas, el
identificador de la empresa y algunas anotaciones. Por ultimo, una tercera tabla
contiene una lista de personas que se han apuntado, cada una de ellas conectada a
una matricula para su empresa con la cantidad pagada.
El razonamiento que se esconde tras este proceso de matriculacion basado en
empresas es que las facturas se envian a las empresas, que reservan las clases
para sus programadores y pueden recibir descuentos especificos. En este caso, la
base de datos esta un poco mas normalizada, puesto que la matricula de personas
no se refiere directamente a una clase, sino solo a la matricula de la empresa para
dicha clase. Veamos la definicion de las tablas correspondientes (se omiten las
restricciones de claves externas y otros elementos):
create table classes
(
id
d-uid not null,
description varchar ( 5 0 ),
starts-on
timestamp not null,
constraint classes-pk primary key ( i d )
) :
create table classes-reg
I
id
d-uid not null,
id-company
d-uid not null,
id-class
d-uid not null,
notes
varchar ( 25 5 ) ,
constraint classes-reg-pk primary key (id),
constraint classes-reg-uc
unique (id-company,
) ;
id
d-uid
not null,
id-class)
id-classes-reg
d-uid n o t n u l l ,
id-person
d-uid n o t n u l l ,
amount
d-amount ,
c o n s t r a i n t people-reg-pk p r i m a r y k e y
( id)
) ;
El modulo de datos para este grupo de tablas utiliza una relacion maestro1
detalleldetalle, y posee codigo para establecer la conexion con el registro maestro
activo cuando se crea un nuevo registro de detalle. Cada conjunto de datos posee
un campo generador para su identificador y cada uno tiene las sentencias SQL
update e insert apropiadas.
Dichas sentencias se han generado mediante el editor de componente correspondiente usando solo el campo identificador para identificar 10s registros existentes y actualizar solo 10s campos de la tabla original. Cada uno de 10s dos
conjuntos de datos secundarios obtiene datos de una tabla de busqueda (ya sea la
lista de empresas o la lista de personas). Por ultimo, tuvimos que editar manualmente las sentencias Ref reshSQL para repetir la union interna adecuada. Veamos un ejemplo:
object
IBClassReg: TIBDataSet
Database = DmMain.IBDatabase1
Transaction = IBTransactionl
AfterInsert = IBClassRegAfterInsert
DeleteSQL-Strings = (
'delete from classes-reg'
'where id = :old-id')
InsertSQL. Strings = (
'insert into classes-reg (id, id-class, id-company, notes) '
' values (:id, :id-class, :id-company, :notes) ' )
RefreshSQL. Strings = (
' select reg. id, reg.id-class, reg.id-company, reg.notes,
c.name '
'from classes-reg reg'
'join companies c on reg.id-company = c.id'
'where id = :id' )
SelectSQL. Strings = (
'select reg. id, reg.id-class, reg.id-company, reg.notes,
c.name '
' from classes-reg reg'
'join companies c on reg.id-company = c.id'
'where id-class = :id1)
ModifySQL-Strings = (
' update classes-reg'
'set'
' id = :id,'
' id-class = :id-class,'
' id-company = :id-company,'
' notes = :notes1
'where id = :old-id' )
GeneratorField.Field = 'id'
GeneratorFie1d.Generator = 'g-master'
Datasource = dsclasses
end
TDataSet);
El conjunto de datos I B P e o p l e R e g posee parametros similares, pero el conjunto de datos I B C l a s s e s es mas sencillo en tiempo de diseiio. En tiempo de
ejecucion, el codigo SQL de este con.junto de datos se modifica de forma dinamica, usando tres alternativas para mostrar las clases programadas (siempre que la
fecha sea posterior a la actual), las clases que ya han comenzado o finalizado en
el presente aiio, y las clases de aiios anteriores. Un usuario escoge uno de 10s tres
grupos de registros para la tabla con un control de solapa, que alberga la DBGrid
de la tabla principal (vease la figura 14.19).
dd
19 Bc~lmdCorp
23 Wlr$echItaha Srl
L ID
21 NAME
Davd l
24 Chuck J
AMOUNT300
3W
--
// p r e p a r a e l SOL p a r a l a s t r e s s o l a p a s
SqlComrnands : = TStringList.Create;
SqlCommands .Add ( ' w h e r e S t a r t s - O n > ' ' n o w " ' ) ;
SqlCommands.Add ( ' w h e r e S t a r t s - O n <= " n o w "
and ' +
' e x t r a c t ( y e a r f r o m S t a r t s- O n ) >= e x t r a c t ( y e a r f r o m
current- t i m e stamp) ' ) ;
SqlCommands.Add ( ' w h e r e e x t r a c t ( y e a r f r o m S t a r t s - O n ) < '
' e x t r a c t ( y e a r from current-times tamp) ' ) ;
end;
p r o c e d u r e TFormClasses.TabChange(Sender: TObject);
begin
dm.IBC1asses.Active : = False;
dm.IBClasses.Se1ectSQL [I] : = SqlCommands [Tab.TabIndex];
dm.IBC1asses.Active : = True;
end;
diseiio y permite que el usuario seleccione una empresa a la que hacer referencia
desde otra tabla. Para simplificar el uso de esta busqueda, que puede darse varias
veces en un programa grande, hemos aiiadido a1 formulario de empresas una
funcion de clase, que tiene como parametros de salida el nombre e identificador de
la empresa seleccionada. Se puede pasar un identificador inicial a la funcion para
determinar su seleccion inicial. Veamos el codigo completo de esta funcion de
clase, que crea un objeto de su clase, selecciona el registro inicial si asi se solicita,
muestra el cuadro de dialog0 y, por ultimo, extrae 10s valores de retorno:
class function TFormCompanies.SelectCompany (
var CompanyName: string; var CompanyId: Integer): Boolean;
var
FormComp : TFormCompanies ;
begin
Result : = False;
FormComp : = TFormCompanies-Create (Application);
FormComp.Caption : = ' S e l e c t Company1;
try
// a c t i v a 10s botones d e didlogo
FormComp.btnCancel.Visib1e : = True;
FormComp.btn0K.Visible : = True;
// s e l e c c i o n a empresa
if CompanyId > 0 then
FormComp.dm.DataCompanies.SelectSQL.Text
:=
' s e l e c t c . i d , c . name, c . t a x - c o d e ' +
' from companies c ' +
'
w h e r e c . i d = ' + IntToStr (CompanyId)
else
FormComp.dm.DataCompanies.Se~ectSQL.Text : =
' s e l e c t c . i d , c . name, c . t a x - c o d e ' +
' from companies c ' +
' w h e r e name-upper s t a r t i n g w i t h ' ' a " ' ;
FormComp.dm.DataCompanies.Open;
FormComp.dm.DataLocations.0pen;
FormComp.dm.DataPeople.Open;
if FormComp. ShowModal = mrOK then
begin
Result : = True;
CompanyId : = FormComp.dm.DataCompanies.Fie1dByName
( ' i d ' ) .AsInteger;
CompanyName : = FormComp.dm.DataCompanies.Fie1dByName
( ' n a m e ' ) .Asstring;
end ;
finally
FormComp-Free;
end ;
end;
Otra funcion de clase ligeramente mas compleja (disponible en el codigo fuente del ejemplo, per0 que no aparece listada aqui) permite seleccionar a una perso-
na de una empresa dada para matricular a personas en las clases. En ese caso, el
formulario se muestra despues de desactivar la busqueda de otra empresa o la
modificacion de 10s datos de esa empresa.
En ambos casos, la busqueda se desencadena aiiadiendo un boton de puntos
suspensivos a la columna de la DBGrid (por ejemplo, la columna de la cuadricula
que lista 10s nombres de las empresas matriculadas para las clases). Cuando se
pulsa este boton, el programa llama a la funcion de clase para mostrar el cuadro
de dialog0 y utiliza su resultado para actualizar el campo identificador oculto y el
campo de nombre visible:
p r o c e d u r e TFormClasses.DBGridClassRegEditButtonClick(Sender:
TObject) ;
var
CompanyName : string;
CompanyId: Integer;
begin
CompanyId : = dm.IBClassReg.Fie1dByName
( ' id- Company') .AsInteger;
i f TFormCompanies.SelectCompany (CompanyName, CompanyId)
then
begin
dm.1BClassReg.Edit;
dm.IBClassReg.Fie1dByName ('Name').Asstring : = CompanyName;
dm. IBClassReg. FieldByName ( ' id-Company' ) .AsInteger :=
CompanyId;
end;
end ;
A1 seleccionar un elemento del cuadro combinado, se genera una sencilla consulta SQL:
MemoSql.Lines.Text
:=
'select
* f r o m ' + ComboTables.Text;
El usuario, si es un experto, puede editar entonces la sentencia SQL, introduciendo posiblemente clausulas restrictivas y ejecutando, a continuacion, la consulta:
p r o c e d u r e TFormFreeQuery.ButtonRunClick(Sender: TObject);
begin
QueryFree .Close;
QueryFree.SQL : = MemoSql.Lines;
QueryFree.Open;
end;
Podemos ver este tercer formulario del programa RWBlocks en la figura 14.20.
Por supuesto que no sugerimos que se afiada edicion SQL a 10s programas pensados para todos 10s usuarios. Esta caracteristica basicamente se destina a usuarios
avanzados o programadores.
.....".__-... ,
-,
--
~ID
~DESCRIPTION
18 Lea~mcqXML
I STARTS-ON
10/l0/2002
Figura 14.20. El formulario de consulta libre del ejernplo RWBlocks esta pensado
para usuarios avanzados.
Trabajo
con ADO
Desde mediados de 10s aiios 80, 10s programadores de bases de datos han
buscado el "santo grial" de la independencia de las bases de datos. La idea es
utilizar una API unica que puedan usar las aplicaciones para interactuar con
muchas fuentes de datos distintas. El uso de una API de este tip0 liberaria a 10s
desarrolladores de la dependencia con respecto a un unico motor de bases de
datos y les permitiria adaptarse a las necesidades en constante carnbio de todo el
mundo.
Los fabricantes han producido muchas soluciones para este objetivo, siendo
las dos mas notables Open Database Connectivity (ODBC) de Microsoft y la
Integrated Database Application Programming Interface (IDAPI) de Borland,
que es mas conocida como Borland Database Engine (BDE).
Microsoft comenzo a sustituir ODBC con OLE DB a mediados de 10s aiios 90,
con el exito de COM. Sin embargo, OLE DB es lo que lo que Microsoft clasificaria como una interfaz a nivel de sistema y esta pensada para 10s programadores a
nivel de sistema. Es una solucion muy grande, compleja y exquisita. Requiere un
programador habil y capaz y un alto grado de conocimiento, a carnbio de una
productividad muy baja. ActiveX Data Objects (ADO) es una capa que recubre a
OLE DB y suele definirse como una interfaz a nivel de aplicacion. Es considerablemente mas simple que OLE DB y mas relajada. En pocas palabras, esta diseiiada para 10s programadores de aplicaciones.
Como se trato en el capitulo 14; Borland tambien ha sustituido a BDE por una
tecnologia mas reciente. llamada dbExpress. ADO tiene mas parecidos con BDE
que con la tecnologia ligera de dbExpress. BDE y ADO soportan la navegacion y
manipulacion de conjuntos de datos, el procesamiento de transacciones y las actualizaciones en cache (llamadas actualizaciones por lotes en ADO). de manera
que 10s conceptos relacionados con el uso de ADO son similares a 10s de BDE.
The Delphi Magazine y para otros temas no relacionados con Delphi. Tambien ha participado en numerosas conferencias en Norte America y en Europa. Guy vive en Inglaterra con su mujer, su hijo y su gato.
En este capitulo prestaremos nuestra atencion a ADO. Tambien analizaremos
dbGo, un conjunto de componentes Delphi llamado inicialmente ADOEspress,
per0 que se renombro en Delphi 6 ya que Microsoft se opone a1 uso del termino
ADO en nombres de productos de terceros. Es posible usar ADO en Delphi sin
necesidad de recurrir a dbGo. A1 importar la biblioteca de tipos de ADO, se
consigue acceso direct0 a las interfaces de ADO; es asi como 10s programadores
en Delphi usaban ADO antes de la version 5 de Delphi. Sin embargo, de esta
manera nos saltamos la infraestructura de bases de datos de Delphi y garantizamos la imposibilidad de usar otras tecnologias de Delphi como 10s controlesdataaware o Datasnap. Este capitulo utiliza dbGo para todos sus ejemplos, no solo su
inmediata disponibilidad y soporte, sino tambien porque se trata de una solucion
muy viable. Sin importar cual sea la opcion final tomada, esta informacion resultara muy util.
Procesamiento de transacciones.
Conjuntos de registros desconectados y persistentes.
El modelo de maletin y el despliegue de MDAC
Proveedores de OLE DB
Los proveedores de OLE DB permiten acceder a una fuente de datos. Se trata
de 10s homologos de ADO a 10s controladores dbExpress y 10s SQL Links de
MSDASQL
Controladores ODBC
Microsoft.Jet.OLEDB.3.5
Jet 3.5
Microsoft.Jet.OLEDB.4.0
Jet 4.0
SQLOLEDB
SQL Server
MSDAORA
Oracle
MSOLAP
Servicios OLAP
SarnpProv
Proveedor de muestra
Ejernplo d e un proveedor
OLE DB para archivos CSV
MSDAOSP
Proveedor sencillo
Ademas de estos proveedores OLE DB de MDAC, Microsoft ofrece otros proveedores con otros productos o con equipos de desarrollo que pueden descargarse:
El proveedor OLE DB deActive Directory Services se incluye con el SDK
de ADSI: el proveedor OLE DB para AS1400 y VSAM se incluye con el
servidor SNA Server; y el proveedor OLE DB para Exchange se incluye
con Microsoft Exchange 2000.
El proveedor OLE DB para Indexing Service es parte del Microsoft Indexing
Service, un mecanismo de Windows que acelera las busquedas de archivos
a1 crear catalogos de informacion sobre archivos. Indexing Service esta
intcgrado con IIS y, en consecuencia, suele usarse para crear indices de
sitios Web.
El proveedor OLE DB para Internet Publishing permite que 10s
desarrolladores manipules directorios y archivos mediante HTTP.
Hay aim mas proveedores OLE DB en forma de proveedores de servicios.
Como implica su nombrc, 10s proveedores de servicio OLE DB ofrecen un
servicio a otros proveedores OLE DB y suelen invocarse automaticamente
cuando se necesita; sin la intervention del programador. El Cursor Service,
por ejemplo, se invoca cuando se crea un cursor de cliente, y el proveedor
Persisted Recordset se invoca para guardar localmente datos.
MDAC incluye muchos proveedores que analizaremos con detalle, pero hay
muchos mas disponibles gracias a Microsoft y a un importante mercado de terceros. Es imposible proporcionar una lista precisa de todos 10s proveedores OLE
DB disponibles, ya que esta lista es muy grande y cambia constantemente. Ademas de terceros independientes, deberian tenerse en cuenta a la mayoria de 10s
dcsarrolladorcs de bases de datos. ya que suelen proporcionar sus propios proveedores OLE DB. Por ejcmplo, Oracle ofrece el proveedor ORAOLEDB.
ADOConnection
Database
ADOCornrnand
Ejecuta un comando
SQL de accion
Sin equivalente
ADODataSet
Descendiente de
TDataSet de proposito
general
Sin equivalente
ADOTable
Encapsulacion de una
tabla
Table
ADOQuery
Encapsulacion de la
sentencia SELECT de
SQL
Query
ADOStoredProIC
Encapsulacion de un
procedirniento alrnacenado StoredProc
RDSConnection
Sin equivalente
Un ejemplo practico
Ya basta de teoria: veamos como funcionan las cosas. Colocamos un ADOTable
en un formulario. Para indicar la base de datos a la que conectarse, ADO usa
cadenas de conexion. Se puede escribir una cadena de conexion manualmente si
Este editor aiiade poca cosa al proceso de escribir una cadena de conexion, por
lo que se puede hacer clic sobre el boton Build para usar directamente el editor de
cadenas de conesion de Microsoft, que se muestra en la figura 15.2.
Se trata de una herramienta que es importante conocer. La primera pestaiia
muestra 10s proveedores OLE DB y de servicio instalados en el ordenador. La
lista variara segun la version de MDAC y de otro software instalado. En este
ejemplo, seleccionaremos el proveedor OLE DB Jet 4.0. Hacemos doble clic sobre Jet 4.0 OLE Dl3 Provider y aparecera la pestafia Connection. Esta pagina
varia segun el proveedor seleccionado; para Jet, solicita el nombre de la base de
datos y 10s detalles del usuario de conexion. Se puede acceder a un archivo MDB
de Access instalado por Borland junto con Delphi 7: el archivo dbdemos.mdb
disponible en la carpeta compartida de datos (de manera predefinida, C:\Archivos
de programa\Archivos comunes\Borland Shared\Data\dbdemos.mdb). Haremos
clic sobre el boton Probar Conexi6n para dar validez a las opciones seleccionadas. La pestafia Avanzadas maneja el control de acceso a la base de datos; aqui
se especifica el acceso exclusivo o de solo lectura a la base de datos. La pestaiia
Todas muestra una lista de todos 10s parametros de la cadena de conexion. La
lista es especifica del proveedor OLE DB que se haya escogido al principio.
(Deberiamos fijarnos con atencion en esta pagina, ya que contiene muchos
parametros que son la respuesta a muchos problemas.) Despues de cerrar el editor
de cadenas de conexion de Microsoft, ser vera en el editor de Borland para la
propiedad C o n n e c t i o n s t r i n g el valor devuelto a esta propiedad (que aqui
se muestra en varias lineas por comodidad):
Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=C:\Archivos d e programa\Archivos comunes\Borland
Shared\Data\dbdemos.mdb;
Persist Security Info=False
Las cadenas de conexion son simples cadenas con muchos parametros separados por punto y coma. Para aiiadir, modificar o eliminar cualquiera de estos
parametros mediante programacion, hay que escribir rutinas propias para encontrar el parametro en la lista y modificarlo como corresponda. Un enfoque mas
simple es copiar la cadena en una lista de cadenas de Delphi y usar su prestacion
de pares nombrelvalor: esta tecnica se mostrara en el ejemplo JetText que veremos mas adelante.
-_
Prw&e&OLE
DB .Med~aCsIslogDBOLE DB Prowdm
MedtaLataloaMeroedDB OLE DB Prowder
~ e d l a ~ d a l o g OLE
~ e bDB
~ Plov~der
~
M~crosoltISAM 1 1 OLE DB Prov~der
M~uosolrJet4 0 OLE DB Prov~der
M~c~osolt
OLE DB Prov~dmFa D a t a M m g S m c r
Mtc~osoltOLE DB Provtder lo1 lndevmg Suvlce
htslnel Pubhshtr~~
M~nosoltOLE 00 Plowdm b~
M~crosoltOLE DB Provder lor OLAP Serv~ces
M~crosoltOLE DB PrtJndel for OLAP Servlces 8 0
Mtcrosolt OLE D8 Prov~de~
for O~acle
Mlcrosolt OLE D8 Prowder for Outlook Search
Mlcrosolt OLE DB Pronder for SOL Serva
Mlcrosoft OLE DB S~mpleP ~ o w d e ~
MSDataShape
I*,
--._--
El componente ADOConnection
Cuando se usa de este mod0 un componente ADOTable, crea su propio componente interno de conexion. No es necesario aceptar la conexion predeterminada
que crea. En general, deberia crearse una conexion propia mediante el component e ADOConnection, que tiene el mismo proposito que el componente
SQLConnection de dbExpress y el componente Database de DBE. Permite personalizar el procedimiento de entrada en el sistema, controlar transacciones, ejecutar directamente comandos de accion y reducir el numero de conexion de una
aplicacion.
Usar un ADOConnection es sencillo. Hay que colocar uno en un formulario y
fijar su propiedad C o n n e c t i o n s t r i n g del mismo mod0 que se haria para el
componente ADOTable. Alternativamente, se puede hacer doble clic sobre un
componente ADOConnection (o usar un elemento especifico de su Componente
Editor, en su menu local) para llamar directamente a1 editor de la cadena de
conexion. Con C o n n e c t i o n s t r i n g apuntando a la base de datos correcta, se
puede inhabilitar el cuadro de dialog0 de entrada fijando como F a l s e la propiedad L o g i n P r o m p t . Para usar una nueva conexion en el ejemplo anterior, hay
que establecer la propiedadconnection deADoTable1 aADOConnection1.
Se vera que la propiedad C o n n e c t i onS t r i ng de A D O T a b l e 1 recupera su
valor original.
Esto se debe a que las propiedades c o n n e c t i o n y C o n n e c t i o n s t r i n g
son mutuamente excluyentes. Una de las ventajas de usar un ADOConnection es
que la cadena de conexion esta centralizada en lugar de repartida a lo largo de
muchos componentes.
Otra ventaja, mas importante, es que todos 10s componentes que comparten el
componente ADOConnection comparten una conexion unica con el servidor de la
base de datos. Sin su propia ADOConnection, cada conjunto de datos ADO tendria una conexion independiente.
[oledb]
Everything after this line is an OLE DB initstring
Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=C:\Archivos d e programa\Archivos comunes\Borland
Shared\Data\dbdemos.mdb
;
Propiedades dinamicas
Imaginemos que somos 10s responsables del diseiio de una nueva arquitectura
de software intermedio (middleware) de bases de datos. Tenemos que reconciliar
10s dos objetivos antagonicos de conseguir una sola API para todas las bases de
datos y acceder a funciones especificas de todas las bases de datos. ADO tiene
que resolver estos objetivos que a1 parecer se excluyen mutuamente, y lo hace
utilizando propiedades dinamicas. Casi todas las interfaces ADO, y sus correspondientes componentes dbGo, tienen una propiedad denominada p r o p e r t i e s
que es un conjunto de propiedades especificas de cada base de datos. Se puede
acceder a estas propiedades mediante su posicion ordinal, del siguiente modo:
ShowMessage (ADOTable1.Properties [I] .Value) ;
Las propiedades dinamicas dependen del tip0 de objeto y tambien de 10s proveedores OLE DB. Para hacernos una idea de su importancia, una conexion ADO
tipica o un conjunto de registros tiene aproximadamente 100 propiedades dinamicas. Como se vera a lo largo de este capitulo, las respuestas a muchas preguntas
sobre ADO tienen que ver con las propiedades dinamicas.
TRUCO: Un evento importante relacionado con el uso de propiedades din M c a s es OnRecordsetCreate, que se introdujo en una actualizacion de Delphi 6 y est&disponible en Delphi7. OnRecordse tcreate se
usa inmediatamente despuCs de que se haya creado un conjunto de registros, pero antes de que se haya abierto. Esto resulta util para definir algunas propiedades d i n h i c a s puesto que algunas de ellas s6lo se pueden definir
cuando el conjunto de registro estA cerrado.
tran aquellos necesarios para recuperar una lista de tablas, indices, columnas, vistas y procedimientos almacenados.
Un filtro que se va a colocar en 10s datos antes de que estos se devuelvan. Se vera un ejemplo de este parametro en breve.
Un GUID para una consulta especifica de proveedor. Solo se usa si el
primer parametro es siProviderSpecif ic.
Un ADODataSet en el que se devuelven 10s datos. Este ultimo parametro
muestra un tema comun en ADO: cualquier metodo que tenga que devolver
una gran cantidad de datos 10s devolvera en forma de Recordset (conjunto
de registros), o, en terminos de Delphi, un ADODataSet.
Para usar OpenSchema,es necesario abrir una ADOConnection. El ejemplo
siguiente (parte del ejemplo OpenSchema) recupera una lista de claves primarias
para cada tabla de un ADODataSet:
ADOConnectionl.OpenSchema(siPrimaryKeys,
EmptyParam, ADODataSetl) ;
EmptyParam,
Cada campo de una clave primaria posee una fila unica en el conjunto de
resultados. Asi, una tabla con una clave compuesta por dos campos tiene dos
filas. Los dos valores EmptyParam indican que dichos parametros estan vacios
y se ignoran. El resultado de este codigo se muestra en la figura 15.3, despues de
ajustar el tamaiio de la cuadricula con algo de codigo personalizado.
Cuando se pasa EmptyParam como segundo parametro, el conjunto de resultados incluye toda la informacion del tipo solicitado para toda la base de datos.
country
customer
Name
CustNo
emdo~ee
tlems
EmpNo
ItemNo
01derNo
ems
MSysAccessOblecls 10
orders
OrhNo
parts
Pa~tNo
vendas
VendorNo
Figura 15.3. El ejemplo Openschema obtiene las claves primarias de las tablas de la
base de datos.
-- -- --
universal como OpenScherna, asi que hay muchos huews sin cubrir. Pafa
simplemente obtener la information esquemhtica y no actualizarla,
Openschema suele ser una opcion mejor.
Sin embargo, el motor Jet incluye controladores para Paradox, dBase, Excel,
texto y HTML.
- - --
False
Fal,e
1
Fdse
Fdse
0
1
+-.I-.
Por desgracia para 10s usuarios de Paradox, en algunos casos tendremos que
instalar el BDE ademas del motor Jet. Jet 4.0 necesita el BDE para poder actuali-
zar tablas Paradox, per0 no para solo leerlas. Lo mismo sucede en el caso de la
mayoria de las versiones de Paradox ODBC Driver. Microsoft ha recibido criticas muy justificadas por este tema y ha creado un nuevo Paradox IISAM que no
necesita el BDE. Se pueden conseguir estos controladores actualizados gracias al
Servicio Tecnico de Microsoft.
-
NOTA: A medida que se conozca ADO en mayor profundidad, se descubrira c u h t o depende del proveedor OLE DB y del RDBMS (sistema de
administration de bases de datos relacionales) de que se trate. Aunque puede usarse ADO con un fonnato de archivo local. como se mostrara en 10s
ejemplos siguientes, la idea general es instalar un motor SQL local siempre
que sea posible. Access y MSDE son buenas elecciones si se tiene que usar
ADO; de no ser asi, podrian tenerse en cuenta alternativas como InterBase
o Firebird, como se comenttr en el capitulo 14.
Ford
Marvin
Dent
+ 55 41
Prelecl
Tr~ll~an
BecMebrm
273-3522
Robot
338.5031
A1
Figura 15.5. ABCCompany.xls en Delphi.
,HomeTown
,Cincinnati
, London
,Milan
TObject);
sl: TStringList;
begin
s l : = TStringList.Create;
sl.Text : = StringReplace (ADOTablel.ConnectionString,
I ,
. I ,
sLineBreak, [rfReplaceAll]);
s l .Values [ 'Data Source '1 : = ExtractFilePath
(App1ication.ExeName);
ADOTable1.ConnectionString : = StringReplace (sl.Text,
sLineBreak, '; ', [rfReplaceAll] ) ;
ADOTablel.Open;
sl. Free;
end;
ICincinnati
l London
l Milan
Importation y exportacion
El motor Jet es especialmente aficionado a la importacion y exportacion de
datos. El proceso de exportacion de datos es el mismo para cada formato de
exportacion y consisten en la ejecucion de una sentencia SELECT con una sintaxis especial. Comencemos con un ejemplo de exportacion de datos desde la
version de Access de la base de datos DBDemos de vuelta a una tabla Paradox.
Necesitaremos una ADOConnection activa, denominada ADOConnect ion1 en
el ejemplo JetlmportExport, que usa el motor Jet para abrir la base de datos. El
siguiente codigo exporta la tabla Customer a un archivo customers. db de
Paradox:
SELECT
Fijemonos en las partes de esta sentencia SELECT.La clausula INTO especifica la nueva tabla que creara la sentencia SELECT.Dicha tabla no puede existir
previamente. La clausula IN especifica la base de datos a la que se aiiadira la
nueva tabla; en Paradox, se trata de un directorio que ya existe. La clausula
inmediatamente siguiente a la base de datos es el nombre del controlador IISAM
que se utilizara para la exportacion. Es necesario incluir el punto y la coma
posterior a1 final del nombre del controlador. La clausula FROM es una parte
habitual de cualquier sentencia SELECT.En el programa de muestra, la operacion se ejecuta mediante el componente ADOConnection y utiliza la carpeta del
programa en lugar de una fija:
ADOConnectionl .Execute ( ' S E L E C T * I N T O C u s t o m e r I N " ' +
CurrentFolder + ' " " P a r a d o x 7 . x ; " PROM CUSTOMER' ) ;
Todas las sentencias de exportacion siguen estas mismas normas basicas, aunque algunos controladores IISAM tienen distintas interpretaciones de lo que es
una base de datos. En este caso, exportaremos 10s mismos datos a Excel:
ADOConnectionl .Execute ( S E L E C T * I N T O C u s t o m e r I N " ' +
CurrentFolder + ' d b d e m o s . x l s " " E x c e l 8 . 0;" PROM CUSTOMER' )
Se crea un nuevo archivo Excel llamado dbdemos . xls en el directorio actual de la aplicacion. Se aiiade un libro llamado Customer, que contiene todos 10s
datos de la tabla Customer de dbdemos .mdb.
Este ultimo ejemplo exporta 10s mismos datos a un archivo HTML:
ADOConnectionl .Execute ( ' S E L E C T * I N T O [ C u s t o m e r . h t m ] I N " ' +
CurrentFolder + ' " "HTML E x p o r t ; " PROM C U S T O M E R ' ) ;
portar a HTML. El ultimo controlador IISAM que veremos en este analisis del
motor Jet es el hermano de HTML Export: HTML Import. Aiiadimos una
ADOTable a un formulario, definimos su c o n n e c t i o n s t r i n g para utilizar el
proveedor Jet 4.0 OLE DB y tambien definimos Extended Properties como HTML
Import. Establecemos el nombre de la base de datos del archivo HTML creado
mediante exportacion hace un momento, a saber, c u s t o m e r .h t m . Ahora definimos la propiedad T a b l e N a m e como C u s t o m e r . Abrimos la tabla e inmediatamente habremos importado el archivo HTML. Conviene recordar, sin embargo,
que si intentamos actualizar 10s datos, recibiremos un error, puesto que este controlador solo esta pensado para la importacion. Por ultimo, si creamos nuestros
archivos HTML que contengan tablas y queremos abrir dichas tablas utilizando
este controlador, hay que recordar que el nombre de la tabla es el valor de la
etiqueta c a p t i o n de la tabla (etiqueta t a b l e ) HTML.
Ubicacion de cu.rsor
La propiedad c u r s o r l o c a t i o n permite especificar quien controla la recuperacion y actualizacion de datos. Existen dos opciones: el cliente ( c l u s e c l i e n t )
o el servidor ( c l u s e se r v e r ) . La decision tomada afectara a la funcionalidad
del conjunto de datos, asi como a su rendimiento y escalabilidad.
Un cursor de cliente lo administra el motor ADO Cursor Engine. Este motor es
un excelente ejemplo de un proveedor de servicio OLE DB: Presta servicio a otros
proveedores OLE DB. El motor ADO Cursor Engine administra 10s datos del
cliente de la aplicacion. Todos 10s datos del conjunto de resultado se consiguen
del servidor cuando se abre el conjunto de datos. Por tanto, 10s datos se mantienen
en memoria y el motor ADO Cursor Engine se encarga de las actualizaciones y la
manipulacion. es algo parecido a usar el componente ClientDataSet en una aplicacion dbExpress. Una ventaja de esta manipulacion de 10s datos, despues de la
recuperacion inicial, consiste en su considerable velocidad incrementada. Aun
mas, dado que la manipulacion se realiza en memoria, el motor ADO Cursor
Engine resulta mas versatil que la mayoria de 10s cursores de servidor y ofrece
servicios adicionales. Mas adelante analizaremos estas ventajas, a1 igual que otras
tecnologias de cursores de cliente (corno 10s conjuntos de registros desconectados
y permanentes).
Tipo de cursor
La eleccion de la ubicacion del cursor afecta directamente a la eleccion dcl tip0
de cursor. Existen cuatro tipos de cursores que podemos usar para cualquier fin o
proposito, per0 hay un valor que no se puede usar, porque es un valor "sin especificar". Muchos valores en ADO implican un valor sin especificar, y 10s analizaremos y explicaremos porque no hay que utilizarlos. Existen en Delphi solo porque
existen en ADO. ADO se diseiio basicamente para programadores en Visual Basic
y C. En estos lenguajes, se pueden usar directamente objetos sin la ayuda que
proporciona dbGo. De ese modo, se pueden crear y abrir conjuntos de registros,
tal y como se llaman en terminos de ADO, sin tener que especificar cada valor
para cada propiedad. Las propiedades para las que no se ha especificado un
valor, tienen un valor no especificado. Sin embargo, en dbGo se utilizan componentes. Estos componentes tienen constructores, y esos constructores dan un valor inicial a cada propiedad de 10s componentes. De ese modo, desde el momento
Pedir y no recibir
Tras haberlo explicado todo sobre las ubicaciones y tipos del cursor, debemos
de hacer una advertencia: no todas las combinaciones de ubicacion y tipo de
cursor son posibles. Normalmente, esta es una limitacion impuesta por el RDBMS
o el proveedor OLE DB como resultado de la funcionalidad y arquitectura de la
base de datos. Por ejemplo, 10s cursores de cliente siempre condicionan que el
tip0 de cursor sea estatico. Podemos comprobarlo por nosotros mismos. Aiiadimos un componente ADODataSet a un formulario, definimos su propiedad
Connectionstring como cualquier base de datos, definimos ClientLocation como clUseCursor y CursorType como ctDynamic.Ahora definimos Active como True y vigilamos el CursorType:cambia a ctstatic.
A partir de este ejemplo, sacamos la siguiente conclusion:
lo que pedimos no es necesariamente lo mismo que recibimos. Hay que comprobar siempre las propiedades despues de abrir un conjunto de datos para ver el
efecto real de las solicitudes. Cada proveedor OLE DB realizara distintos cambios de acuerdo con distintas solicitudes y distintas circunstancias, per0 para
tener una idea de lo que podemos esperar, pondremos algunos ejemplos:
El proveedor Jet 4.0 OLE DB cambia la mayoria de 10s tipos de cursor a
conjunto de claves.
El proveedor SQL Server OLE DB cambia normalmente el conjunto de
claves y estatico a dinamico.
Indices de cliente
Una de las muchas ventajas de 10s cursores de cliente es la capacidad de crear
indices locales o de cliente. Para verlo, podemos suponer que tenemos un conjunto de datos de cliente ADO para la tabla Customer de DBDemos, que tiene una
cuadricula conectada, y definir la propiedad Index Fie ldNames del conjunto
de datos como CompanyName.La cuadricula mostrara inmediatamente que 10s
datos estan ordenados por nombre de empresa (CompanyName). Debemos aclarar algo importante: para indexar 10s datos, ADO no vuelve a leer 10s datos desde
su fuente. El indice se creo a partir de 10s datos en memoria. Esto significa que no
solo la creacion del indice es tan rapida como seria posible, sin0 que la red y el
RDBMS no se sobrecargan debido a la transferencia de 10s mismos datos una y
otra vez con distintas ordenaciones.
La propiedad IndexFieldNames posee mas potencial. Si la definimos como
Country; CompanyName, veremos 10s datos se ordenan primero por pais y
despues, dentro de cada pais, por nombre de empresa. Ahora, definimos
IndexFieldNames como CompanyName DESC.Debemos asegurarnos que
ADODataSet1.IndexFieldNames
: = Column.Field.FieldName
+ '
DESC'
else
ADODataSetl.IndexFie1dNames
end;
: = Column.Field.Fie1dName
Replicacion
ADO tiene una gran cantidad de potentes caracteristicas. Se puede pensar que
eso supone un gran consumo de recursos, per0 tambien se traduce en aplicaciones
Figura 15.6. El forrnulario del ejernplo DataClone, con dos copias de un conjunto de
datos (el original y el clon).
Esta linea dona ADOTable 1 y asigna el don a A D O T a b l e 2 . En el programa veremos una segunda vista de 10s datos. Los dos conjuntos de datos poseen
sus propios punteros de registro y otra information de estado, por lo que el clon
no interfiere con su copia original. Este comportamiento hace que 10s clones
resulten ideales para la trabajar con un conjunto de datos sin afectar a 10s datos
originales. Otra caracteristica interesante es que se pueden tener varios registros
activos distintos, uno para cada uno de 10s clones, una prestacion que no puede
conseguirse en Delphi con un unico conjunto de datos.
Procesamiento de transacciones
Como ya vimos en el capitulo 14, el procesamiento de transacciones permite a
10s desarrolladorcs agrupar actualizaciones individuales de una base de datos en
una unidad logica de trabajo unica.
El soporte de procesamiento de transacciones de ADO se controla con el componente ADOConnection, empleando 10s metodos B e g i n T r a n s , C o r n r n i t T r a n s
y R o l l b a c k T r a n s , que tienen efectos parecidos a 10s de 10s nietodos BDE y
dbExpress correspondientes. Para investigar el soporte del procesamiento de transacciones de ADO. crearemos un programa de prueba sencillo, llamado
TransProcessing. El programa ticne un componente ADOConnection con su propiedad c o n n e c t i o n s t r i n g configurada para el proveedor Jet 4.0 OLE DB y
el archivo dbdemos.mdb. Tiene un componente A D O T a b l e conectado a la tabla
Customer y un Datasource y una DBGrid para mostrar 10s datos. Por ultimo,
tiene tres botones para ejecutar cada una de las siguientes ordenes:
bios, debido a limitaciones del proveedor OLE DB. Otra diferencia algo extraiia
resulta evidente cuando se trabaja con Access: si se usa el proveedor ODBC OLE
DB, se podran usar transacciones, per0 no transacciones anidadas. Abrir una
transaccion cuando otra esta activa producira un error. Sin embargo, mediante el
motor Jet, Access si soporta transacciones anidadas.
Transacciones anidadas
Mediante el programa TransProcessing vamos a hacer esta prueba:
El efecto global es que so10 es permanente el cambio del registro Around The
Horn. Sin embargo, si la transaccion interna se habia confirmado y la transaccion
externa se deshace, el efecto global seria que ninguno de 10s cambios seria permanente (ni siquiera 10s cambios de la transaccion interna). Esto es lo esperado,
siendo el unico limite que Access solo soporta cinco niveles de transacciones
anidadas.
ODBC ni siquiera soporta transacciones anidadas, el proveedor de Jet OLE
DB solo soporta hasta cinco niveles de transacciones anidadas y el proveedor
SQL Server OLE DB no soporta en absoluto la anidacion. Podriamos obtener un
resultado distinto segun la version de SQL Server o del controlador, per0 la documcntacion y nuestros experimentos con 10s servidores indican que asi sucede.
Aparentemente, solo la transaccion mas externa decide si el trabajo se confirma o
se deshace.
Atributos de ADOConnection
Existe otro tema que deberiamos considerar si estamos pensando en utilizar
transacciones anidadas. El componente ADOConnection posee una propiedad llamada Attributes que determina el mod0 en que se deberia comportar una
transaccion cuando se confirma o se deshace. Se trata de un conjunto de
TXActAtt ributes que, por defecto, esta vacio. Solo hay dos valores en
TXActAttribuf:es:xaCommitRetainingyxaAbortRetaining(este
valor se suele escribir, incorrectamente, como xaRol lbac kRetaining por-
Tipos de bloqueo
ADO soporta cuatro tecnicas diferentes para bloquear 10s datos frente a actualizaciones. Las cuatro tecnicas se pueden utilizar mediante la propiedad LockType
del conjunto de datos, con 10s valores: 1tReadOnl y, 1tPessimist ic,
1tOptimistic o ItBatchOptimistic. (Existe ademas una opcion
itunspecified,pero, como ya se comento, ignoraremos 10s valores no especificados.) En esta seccion ofreceremos una vision global de estos cuatro enfoques. El valor 1tReadOnly especifica que 10s datos son solo de lectura y no
pueden actualizarse. Asi, no se necesita ningun control de bloqueo porque 10s
datos no se pueden actualizar.
Los valores 1tPessimistic y 1topt imistic ofrecen el mismo control
de bloqueo "pesimista" y "optimista" que ofrece el BDE. Una ventaja importante
que ofrece ADO en oposicion a BDE a este respecto es que la opcion del control
de bloqueo es nuestra, en lugar de del controlador BDE. Si se usa una base de
datos de escritorio como dBase o Paradox, el controlador BDE usara un bloqueo
pesimista; si se usa una base de datos clientelservidor como InterBase, SQL Server
u Oracle, el controlador usara el bloqueo optimista.
El bloqueo pesimista
Las palabras pesimista y optimista en este context0 se refieren a lo que espera
el desarrollador de cara a1 conflicto entre actualizaciones de usuario. El bloqueo
pesimista supone que existe una alta probabilidad de que 10s usuarios intenten
actualizar 10s mismos registros a1 mismo tiempo y por lo tanto el conflicto es
probable. Para evitar dicho conflicto, se bloquea el registro cuando comienza la
edicion. El bloqueo se mantiene hasta que se ha finalizado o cancelado la actualizacion. Un segundo usuario que intente editar el mismo registro a1 mismo tiempo
no podra colocar su bloqueo de registro y recibira una excepcion "Could not
update; currently locked" ("No se puedo actualizar, en este momento esta cerrado"). Esta tecnica de bloqueo resultara familiar a 10s desarrolladores que hayan
trabajado con bases de datos como dBase y Paradox. La ventaja es que si el
usuario sabe que si puede comenzar a editar un registro, podra guardar su actualizacion con exito. El inconveniente del bloqueo pesimista es que el usuario controla cuando se coloca y se elimina el bloqueo. Si el usuario domina la aplicacion,
el bloqueo podria durar meros segundos. Sin embargo, de cara a una base de
datos, un par de segundos puede ser una eternidad. Por otra parte, el usuario
podria comenzar la edicion e irse a almorzar, y el registro permaneceria bloqueado todo ese tiempo, hasta su vuelta. Como consecuencia, la mayoria de 10s defensores del bloqueo pesimista se protegen contra este caso usando un temporizador
u otro dispositivo para provocar la caducidad de 10s bloqueos despues de un
cierto plazo de inactividad a la entrada.
Otro problema del bloqueo pesimista es que necesita un cursor de servidor.
Antes mencionamos que las ubicaciones del cursor influian sobre la disponibilidad de 10s diferentes tipos de cursor. Ahora podemos ver que las ubicaciones del
cursor tambien influyen sobre 10s tipos de bloqueo. Mas adelante, analizaremos
las ventajas de 10s cursores de cliente, si se decide aprovechar las ventajas de este
tipo de cursores, no se podra utilizar el bloqueo pesimista.
El bloqueo pesimista es un area de dbGo que ha cambiado en Delphi 6 (respecto a Delphi 5). En este apartado se describe como funciona el bloqueo pesimista
de las versiones 6 y 7. Para remarcar este mod0 de funcionamiento, hemos creado
el ejemplo PessimisticLocking. Es parecido a otros ejemplos de este capitulo,
per0 la propiedad CursorLocation se configura como cluseserver y la
propiedad Lo ckType como 1tP e ssimistic.Para usarlo, hay que ejecutar
dos copias desde el Explorador de Windows y tratar de editar el mismo registro en
ambas instancias en ejecucion del programa: no se podra, porque el registro estara bloqueado por otro usuario.
puede hacer que una union SQL sea actualizable. Consideremos la siguiente equiunion SQL:
SELECT * FROM Orders, Customer
WHERE Customer.CustNo=Orders.CustNo
Esta sentencia proporciona una lista de pedidos y 10s datos de 10s clientes que
han realizado dichos pedidos. El BDE considera que cualquier union SQL es de
solo lectura porque insertar, actualizar y eliminar filas en una union resulta ambiguo. Por ejemplo, podriamos plantearnos si la insercion de una fila en la union
anterior originaria un nuevo pedido y tambien un nuevo cliente o solo un nuevo
pedido. La arquitectura ClientDataSet/Provider permite especificar una tabla de
actualizacion principal (y otras caracteristicas avanzadas que no vamos a comentar), y tambien personalizar el codigo SQL de las actualizaciones, como se vio en
parte en el capitulo 14 y se comentara aun mas en el capitulo 16.
ADO soporta un equivalente a las actualizaciones mediante cache, denominado actualizaciones por lotes, que son muy similares a1 enfoque del BDE. En el
proximo apartado hablaremos sobre estas actualizaciones por lotes, lo que puede
ofrecer y por que son tan importantes. Sin embargo, en esta seccion no las necesitaremos para resolver el problema de la actualizacion de una union porque, en
ADO, las uniones son intrinsecamente actualizables.
El ejemplo JoinData se basa en un componente A D O D a t a S e t que usa la
union SQL anterior. Si se ejecuta, se puede editar uno de 10s campos y guardar 10s
cambios (saliendo del registro). No se produce ningun error, porque la actualizacion se habra aplicado con exito. ADO, en comparacion con el BDE, ha adoptado
una tecnica mas practica para el problema. En una union ADO, cada objeto de
campo sabe a que tabla subyacente pertenece. Si actualizamos un campo de la
tabla Orders y enviamos el cambio, se crea entonces una sentencia SQL UPDATE
para actualizar el campo de la tabla O r d e r s . Si cambiamos un campo de la tabla
Orders y un campo de la tabla Customer, se crean dos sentencias SQL UPDATE,
una para cada tabla.
La insercion de una fila en una union sigue un comportamiento similar. Si
insertamos una fila e escribimos valores solo para la tabla O r d e r s , se crea una
sentencia SQL I N S E R T para la tabla O r d e r s . Si escribimos valores para ambas tablas, se crean dos sentencias SQL I N S E R T , una por tabla. El orden en el
que se ejecutan las sentencias es importante, porque el nuevo pedido podria estar
relacionado con el nuevo cliente, por lo que el nuevo cliente se inserta en primer
lugar .
El mayor problema de la solucion de ADO se puede ver cuando se elimina una
fila de una union. El intento de eliminacion parece no tener exito. El mensaje
exacto que veamos dependera de la version de ADO que estemos usando y de la
base de datos, per0 se nos comunicara que no podemos eliminar la fila porque
otros registros estan relacionados con ella. El mensaje de error puede resultar
confuso. En este caso, el mensaje de error implica que un pedido no puede elimi-
narse porque esisten registros que estan relacionados con el pedido, per0 el error
ocurre tanto si el pedido tiene asociados otros registros como si no. La explicacion puede obtenerse siguiendo la misma logica para las eliminaciones que para
las inserciones. Se crean dos sentencias SQL DELETE:una para la tabla Customer
y, a continuacion, otra para la tabla Orders. En contra de lo que pudiera parecer,
la sentencia DELETE de la tabla Orders tiene esito. Es la sentencia DELETE de
la tabla Customer la que no funciona, porque no se puede eliminar el cliente
mientras registros dependientes.
:=
' Products'
TForml.FormCloseQuery(
Sender: TObject; var CanClose: Boolean) ;
begin
CanClose
:=
True;
i f ADODataSet1.UpdatesPending then
CanClose : = (MessageDlg ( ' U p d a t e s a r e s t i l l p e n d i n g ' $ 1 3
' C l o s e a n y w a y ? ' , mtconfirmation, [&Yes,
&No] , 0 ) =
mrYes) ;
end;
AD0DataSetl.FilterGroup : = fgPendingRecords;
ADODataSetl.Filtered : = True;
Bloqueo optimista
Ya hablamos anteriormente de la propiedad Loc kType y vimos como funciona el bloqueo pesimista. A continuacion hablaremos del bloqueo optimista, no
solo por tratarse del tip0 de bloqueo preferido para transacciones con un trafico
medio o alto, sino tambien por ser el esquema de bloqueo empleado para las
actualizaciones por lotes.
El bloqueo optimista supone que existe una probabilidad muy reducida de que
10s usuarios intenten actualizar 10s mismos registros a1 mismo tiempo y, por ello,
no es probable que ocurra un conflicto. Asi, la actitud es que todos 10s usuarios
pueden editar cualquier registro en cualquier momento y tratamos con las consecuencias de conflictos entre actualizaciones de diferentes usuarios sobre 10s mismos registros cuando se guardan 10s cambios. De este modo, 10s conflictos se
consideran una excepcion a la norma. Esto significa que no existen controles que
eviten que dos usuarios editen el mismo registro a1 mismo tiempo. El primer
usuario en guardar 10s cambios tendra exito. El intento del segundo usuario de
actualizar el mismo registro puede que no lo tenga. Este comportamiento es esencia1 para las aplicaciones de maletin y aplicaciones Web, en las que no existe una
conexion permanente con la base de datos y, por lo tanto, no hay forma de
implementar un bloqueo pesimista. En oposicion a1 bloqueo pesimista, el bloqueo
optimista posee la considerable ventaja adicional de que 10s recursos solo se
consumen momentaneamente y, por lo tanto, el uso medio de 10s recursos es
mucho menor, haciendo que la base de datos resulte mas escalable.
Vearnos un ejemplo. Supongamos que tenemos un ADODataSet conectado a la
tabla Customer de la base de datos dbdemos .mdb, con LockType definido
como 1tBat chOpt imi s t i c y que el contenido se muestra en una cuadricula.
Supongamos que tambien tenemos un boton para llamar a UpdateBat ch.Ejecutamos el programa dos veces (es el ejemplo BatchUpdates) y empezamos a
editar un registro en la primera copia del programa. Aunque por motivos de
sencillez, mostraremos un conflicto empleando un unico equipo, la situacion y
eventos subsecuentes no cambian cuando se usan varios equipos:
1. Escogemos la empresa Bottom-Dollar Markets de Canada y cambiamos el
nombre por Bottom-Franc Markets.
2. Guardamos el cambio, salimos del registro para enviarlo y hacemos clic
sobre el boton para actualizar el lote.
A1 igual que con muchos otros mensajes de error de ADO, el mensaje exacto
que se reciba dependera no solo de la version de ADO sin0 tambien de la precision
con que se siga el ejemplo. En ADO 2.6, el mensaje de error es "Row cannot be
located for updating. Some values may have been changed since it was last
react' ("No se puede encontrar la fila para su actualizacion. Puede que algunos
valores hayan cambiado desde su ultima lectura"). Este es el comportamiento del
bloqueo optimista. La actualizacion del registro se realiza ejecutando la siguiente
sentencia SQL:
UPDATE CUSTOMER SET CompanyName="Bottom-Pound Markets"
WHERE CustomerID="BOTTM" AND CompanyName="Bottom-Dollar
Markets"
adCriteriaKey
adCriteriaAllCols
adCriteriaUpdCols
adCriteriaTimeStamp
Otra cuestion que hay que tener en cuenta es el mod0 en que ADO trata con
errores durante la actualizacion de varios registros. Usando las actualizaciones
mediante cache del BDE y ClientDataSet, podemos usar el evento OnUpdateError para controlar cada error de actualizacion cuando sucede el error y
resolver el problema antes de pasar a1 registro siguiente. En ADO, no podemos
establecer dicho dialogo. Podemos realizar un seguimiento del progreso y del
exito o fracas0 de la actualizacion del lote usando OnWillChangeRecord y
OnRecordChangeComplet e del conjunto de datos, per0 no podemos revisar
el registro y reenviarlo durante dicho proceso como podemos con el BDE y
ClientDataSet. Ademas, si durante el proceso de actualizacion ocurre un error, la
actualizacion no se detiene, sin0 que continua hasta el final, hasta que se hayan
aplicado o hayan fallado todas las actualizaciones. Esto puede ocasionar un mensaje de error incorrect0 e inutil. Si no se puede actualizar mas de un registro o el
unico registro que ha fallado es distinto del ultimo registro, el mensaje de error en
ADO 2.6 es "Multiple-step OLE DB operation generated errors. Check each
OLE DB status value, if available. No work was done" ("La operacion OLE DB
en varios pasos genero errores. Verifique cada valor de estado OLE DB, si estan
disponibles. No se ha realizado ninguna tarea"). El problema esta en la ultima
frase, declara que "No se ha realizado ninguna tarea", per0 eso no es correcto. Es
verdad que no se realizo ninguna tarea para el registro que fallo, per0 en 10s
demas registros se aplicaron correctamente las actualizacionesy Qtas se mantienen.
Newvalue
curvalue
I OldValue
II
:=
nil;
Esta funcion tambien esta disponible para el BDE y otras tecnologias de bases
de datos cambiando a1 uso de componentes Client Dataset, per0 la belleza
de la solucion de ADO esta en que podemos crear toda la aplicacion utilizando
componentes de conjuntos de datos dbGo y no percatarnos de 10s conjuntos de
registros desconectados. En el momento en que descubramos esta caracteristica y
deseemos beneficiarnos de ella, podemos continuar utilizando 10s mismos componentes que hemos usado siempre. Existen dos razones por las que podriamos
querer desconectar 10s conjuntos de registros:
Para que el numero total de conexiones sea reducido.
Para crear una aplicacion de maletin.
La mayoria de las aplicaciones empresariales clientetservidor abren tablas y
mantienen una conexion permanente con su base de datos mientras la tabla esta
abierta. Sin embargo, normalmente solo existen dos razones por las que queramos
estar conectados a la base de datos: recuperar datos y actualizarlos. Supongamos
que queremos cambiar la tipica aplicacion clientetservidor para que una vez que
se abra la tabla y se consigan 10s datos, se desconecte el conjunto de datos de la
conexion y se rompa esta. El usuario no tiene porque saberlo y la aplicacion no
necesitara mantener una conexion abierta con la base de datos. El siguiente codigo muestra 10s dos pasos:
ADODataSetl.Connection : = nil;
ADOConnection1.Connected : = False;
El unico momento en el que se necesita una conexion es aquel en el que es necesario aplicar el lote de actualizaciones, por lo que el codigo de actualizacion seria:
ADOConnection1.Connected : = True;
AD0DataSetl.Connection : = ADOConnectionl;
t rY
ADODataSetl.UpdateBatch;
finally
AD0DataSetl.Connection : = nil;
ADOConnectionl.Connected : = False;
end;
Pooling de conexiones
Toda esta explicacion sobre el cierre de conexiones y su reapertura nos acerca
el tema delpooling de conexiones. Elpooling o reserva de conexiones, que no se
mascara de bits que permite desactivar varios servicios OLE DB, como elpooling
de conexion, el listado de transacciones y el motor de cursor. Para desactivar el
pooling de conexiones usando la cadena de conexion incluimos ";OLE DB
Services= - 2 " a1 final de la cadena de conexion. Para activar el pooling de
;O L E D B
conexion para el proveedor Jet OLE DB, podemos incluir
services=-1 " a1 final de la cadena de conexion, lo cual activa todos 10s
servicios OLE DB.
If
Este guarda 10s datos y su delta en un archivo del disco duro. Podemos volver
a cargar dicho archivo utilizando el metodo LoadFromFile,que acepta un
solo parametro que indica el archivo que se ha de cargar. El formato del archivo
es Advanced Data Table Gram (ADTG), que es un formato propietario de
Microsoft. Sin embargo, tiene la ventaja de ser muy eficiente. Si lo preferimos,
podemos guardar el archivo como XML pasando un segundo parametro a
SaveToFile:
Sin embargo, ADO no posee su propio analizador sintactico XML incorporado (igual que ClientDataSet), por lo que debemos usar el analizador sintactico
MSXML. Nuestro usuario debe instalar Internet Explorer 5 o una version posterior, o descargar el analizador sintactico MSXML de la propia pagina Web de
Microsoft. Si pretendemos que 10s archivos Sean permanentes de forma local en
formato XML, debemos tener en cuenta una serie de inconvenientes:
En primer lugar, es mas lento guardar y cargar archivos XML que guardar
y cargar archivos ADTG.
En segundo lugar, 10s archivos XML de ADO (y 10s archivos XML en
general) son bastante mas grandes que sus homologos ADTG (el tamaiio
de 10s archivos XML es normalmente el doble que el de sus homologos
ADTG).
El modelo de maletin
Nuestro recientemente adquirido conocimiento sobre las actualizaciones por
lotes, conjuntos de registros desconectados y conjuntos de registros permanentes
nos permiten sacar partido del "modelo de maletin". La idea que se esconde detras
este modelo es que 10s usuarios quieren ser capaces de usar nuestra aplicacion
mientras e s t h de viaje, quieren llevarse la misma aplicacion que utilizan en 10s
escritorios de su oficina y utilizarla en sus portatiles mientras estan en las instalaciones de sus clientes. El problema de dichas circunstancias es que normalmente
cuando nuestros usuarios se encuentran en las instalaciones de sus clientes, no
estan conectados a su servidor de base de datos, porque el servidor de base de
datos esta en funcionamiento en la red interna de su propia oficina. En consecuencia, no hay datos en el portatil y de todas formas no se pueden actualizar 10s
datos.
Es aqui donde entra en juego lo aprendido. Supongamos que la aplicacion ya
esta escrita. El usuario ha solicitado esta nueva ampliacion de maletin y tenemos
que adaptar la aplicacion existente. Es necesario afiadir una nueva opcion para
permitir que 10s usuarios que se "preparen" para la aplicacion de maletin ejecutando sencillamente S a v e T o F i l e para cada tabla de la base de datos. El resultad0 es una coleccion de archivos ADTG o XML en la que se refleja el contenido
de la base de datos. Estos archivos se copian entonces en el portatil en el que se ha
instalado previamente una copia de la aplicacion.
La aplicacion debera poder discernir si se esta ejecutando de forma local o
conectada a la red. Podemos determinarlo intentando conectar con la base de
datos y comprobando si no se nos permite hacerlo, detectando la presencia de un
archivo local de "maletin" o creando algun indicador de diseiio propio. Si la
aplicacion se esta ejecutando en mod0 de maletin, es necesario utilizar
L o a d F r o m F i l e para cada tabla en lugar de definir C o n n e c t e d como T r u e
para las conexiones ADOConnections y Act ive como True para 10s conjuntos
de datos ADO. Ademas, la aplicacion en mod0 de maletin necesita utilizar
SaveTo Fi le en lugar de UpdateBa t c h siempre que se guarden 10s datos. A1
volver a la oficina, el usuario necesita que haya un proceso de actualizacion que
cargue cada tabla del archivo local, se conecte el conjunto de datos a la base de
datos y aplique 10s cambios usando UpdateBatch.
Aplicacio'nes
DataSnap
multicapa
Las grandes empresas suelen tener necesidades que son mucho mas amplias de
lo que pueden cubrir aplicaciones que usen bases de datos locales y servidores
SQL. En 10s ultimos aiios, Borland Software Corporation se ha enfrentado a las
necesidades de las grandes empresas e incluso carnbio temporalmente su nombre
por Inprise para subrayar su enfoque orientado a la empresa. Finalmente el nombre volvio a cambiarse por Borland, pero el tener como objetivo el desarrollo de la
empresa sigue permaneciendo.
Delphi se dirige a muchas tecnologias diferentes: arquitecturas de tres capas
basadas en Windows NT y DCOM, aplicaciones TCPIIP y de sockets y, sobre
todo, servicios Web basados en XML y SOAP. Este capitulo se centra en las
arquitecturas multicapa orientadas a bases de datos, mientras que del resto de las
tecnologias se hablara mas adelante.
Antes de continuar, deberiamos resaltar dos elementos importantes. En primer
lugar, las herramientas para soportar este tip0 de desarrollo solo se encuentran
disponibles en la version Enterprise de Delphi; y, en segundo lugar, con Delphi 7
no hay que pagar derechos de desarrollo para aplicaciones DataSnap. Se adquiere
el entorno de desarrollo y despues se despliegan 10s programas en tantos servidores como se quiera, sin deber dinero a Borland. Se trata de un carnbio muy significativo (el mas significativo en Delphi 7) de la politica de distribucion de DataSnap,
que solia requerir el pago de derechos por servidor (una cantidad inicialmente
el lenguaje que mas suele utilizarse para efectuar consultas sobre 10s datos. Tambien pueden denominarse RDBMS (sistema de administracion de bases de datos
relacionales), recalcando de esta forma que el servidor proporciona una serie de
herramientas para la administracion de 10s datos, como el soporte de la seguridad
y las tareas de replicacion de la informacion.
Por supuesto, alguna de las aplicaciones creadas puede no necesitar las ventajas que ofrece un RDBMS completo, con lo que tal vez baste con una solucion
orientada unicamente a1 cliente. Por otra parte, podria darse el caso en el que se
necesitase parte de la robustez de un RDBMS, per0 en un unico ordenador aislado. Ante una situacion como esa, puede emplearse una version local de un semidor SQL, como InterBase. El desarrollo clientelservidor tradicional suele realizarse
bajo una arquitectura de dos capas. Sin embargo, si el RDBMS realiza principalmente tareas de almacenamiento de datos mas que de calculo de datos y numeros,
el cliente podria contener tanto codigo para la interfaz de usuario (formateando la
salida y la entrada mediante informes personalizados, formularios de entrada de
datos, pantallas de consulta, etc.) como codigo relacionado con la gestion de 10s
datos (tambien llamado reglas de negocio). En este caso, suele ser buena idea
tratar de separar estas dos secciones del programa y crear una arquitectura logica
de tres capas. El termino "logica" implica que seguimos contando con dos ordenadores (es decir, dos capas fisicas), per0 ahora la aplicacion se ha partido en tres
elementos diferentes. Delphi 2 proporcionaba soporte para arquitecturas logicas
de tres capas mediante modulos de datos. Como recordara, un modulo de datos
consiste en un contenedor no visual para 10s componentes de acceso a datos de
una aplicacion (u otras componentes no visuales), si bien a menudo incluye varios
controladores para eventos relacionados con bases de datos. Puede compartirse
un unico modulo de datos entre diferentes formularios y ofrecer diferentes interfaces
de usuario para 10s mismos datos. Podrian existir uno o mas formularios de entrada de datos, informes, formularios maestroldetalle y diversos formularios de salida dinamica o en diagrama.
El enfoque logico de tres capas representa la solucion a multiples problemas,
per0 tambien tiene varios inconvenientes. En primer lugar, es precis0 reproducir
la parte del programa dedicada a la administracion de datos en 10s ordenadores de
diferentes clientes, lo cual podria afectar a1 rendimiento, aunque el aspect0 mas
importante es la complejidad que esto aiiade a1 mantenimiento del codigo. En
segundo lugar, dado que 10s mismos datos estan siendo actualizados por diversos
usuarios, no existe un mod0 simple de gestionar 10s conflictos de actualizacion
resultantes. Por ultimo, las aplicaciones logicas de tres capas de Delphi requieren
la instalacion y configuracion del motor de la base de datos (si existe) y de la
biblioteca de cliente del servidor SQL en cada ordenador cliente.
El siguiente paso logico desde el escenario clientelservidor consiste en trasladar la parte de la aplicacion integrada por el modulo de datos a un servidor
independiente y diseiiar todos 10s programas cliente de tal forma que Sean capaces
de interactuar con dicho servidor. ~ s t era
e precisamente el objetivo de 10s m6du-
La interfaz AppServer
Las dos partes de una aplicacion DataSnap se comunican mediante la interfaz
IAppServer.La definicion de esta interfaz se muestra en el listado 16.1. Es
extraiio que se necesite llamar directamente a 10s metodos de la interfaz
' /lAEPCC20-
7 A 2 4 - 1 lD2-9SBO-C69BEB+'B5B6D/
']
--
Protocolo de conexion
DataSnap define unicamente la arquitectura de nivel superior y puede emplear
diferentes tecnologias para transferir datos desde la capa intermedia a1 entorno
del cliente. Soporta muchos protocolos distintos, entre 10s que destacan:
NOTA: El transporte HTTP de DataSnap puede utilizar XML como formato de 10s paquetes de datos, permitiendo que cualquier plataforma o
herramienta capaz de leer dicho formato pueda formar parte de una arquitectura DataSnap. Se trata de una extension del formato original de paquetes de datos de DataSnap, que tampoco estaba supeditado a ningun tip0 de
plataforma en particular: La utilization de XML sobre HTTP es tarnbikn la
base de SOAP.
Hasta Delphi 6, tambien se podia usar CORBA (Common Object Request
Broker Architecture) como mecanismo de transporte para aplicaciones DataSnap.
utiliza Delphi para las actualizaciones almacenadas. El C l i e n t D a t a S e t gestiona 10s datos en una cache de memoria y solo suele leer un subconjunto de 10s
datos disponibles en el servidor, cargando mas elementos a medida que 10s neccsita. Cuando el cliente actualiza registros o inserta unos nuevos, almacena estos
cambios pendientes en otra cache local en el cliente, la cache delta.
El cliente tambien puede guardar 10s paquetes de datos en un disco y trabajar
con ellos desconectado, gracias a1 soporte de MyBase ya comentado. El protocolo
de paquetes de datos mueve incluso la informacion sobre errores y otros datos,
por eso se trata de uno de 10s elementos clave de esta arquitectura.
-
NOTA: En la ficha WebServices, tambien se: puede encontrar el componente SoapConnection, que exige un tipo especiifico de servidor.
El componente Webconnection: Se utiliza para manejar conexiones HTTP
que pueden atravesar facilmente un cortafuego. Deberia indicarse el URL
en la que se encuentra la copia de h t tpsrvr . d l 1 y el nombre o GUID
del objeto remoto en el servidor.
En Delphi 6 se aiiadieron unos cuantos compones de cliente mas a la arquitectura de DataSnap, la mayoria destinados a la administracion de las conesiones.
El componente ConnectionBroker: Puede usarse para sustituir a un componente de conesion real, lo cual resulta bastante util cuando se tiene una
unica aplicacion con multitud de conjuntos de datos de cliente. Para modificar la conexion fisica de cada conjunto de datos, bastara con modificar la
propiedad c o n n e c t i o n del ConnectionBroker. Asimismo, se podran usar
10s eventos de este componente virtual de conesion en lugar de 10s de las
conesiones reales, lo cual evitara tener que modificar codigo alguno cuando se cambie la tecnologia de transporte de datos. Por esta misma razon,
tambien sera posible referirse al objeto AppServer del ConnectionBroker
en lugar de a la propiedad correspondiente de una conexion fisica.
ficha DataSnap presenta otros componentes relacionados con la transformacion del paquete de datos de DataSnap en formatos XML personalizados. Estos
componentes (XMLTransform, XMLTransformProvider y XMLTransformClient)
se trataran mas adelante.
type
TAppServerOne = class(TRemoteDataModule, IAppServerOne)
private
( Private declarations )
protected
c l a s s p r o c e d u r e UpdateRegistry(Register: Boolean;
c o n s t ClassID, ProgID: string); override;
public
{ Public declarations )
end;
!nrlanci-g
I ~ u l i i l eInstance
AI
n**.,P.-
Como cabe esperar, el siguiente paso consiste en afiadir a1 formulario un componente ClientDataSet. El ClientDataSet debe conectarse a1 componente
D C O M C o n n e c t i o n 1 mediante la propiedad R e m o t e s e r v e r , y a traves de
esta a uno de 10s proveedores que esporta. La lista de proveedores disponibles
puede consultarse en la propiedad P r o v i d e r N a m e , a traves del habitual cuadro
combinado. En este ejemplo, se podra seleccionar unicamente D a t a S e t P r o v i d e r l , puesto que se trata del unico proveedor disponible en el servidor que
acabamos de crear. Con esta operacion, el conjunto de datos que figura en la
memoria del cliente queda conectado con el conjunto de datos dbEspress del
servidor. Si se activa el conjunto de datos del cliente y se afiaden una serie de
controles data-aware (o un control DBGrid), apareceran inmediatamente en ellos
10s datos dcl servidor, tal y como muestra la figura 16.2.
OEPT-NO~EMP-NO FIRST-NAME
) SUO
,
2'R0beit
IFULL-WE
m
1
1
1
IHIRE~-
Pidsan, Robnt
28/12
-E 2 b ~ ~ ~ ~ m w e c & o n ~
Yang, B~uce
Lambert, Kim
28/12
6/2/1
5/4/1
5 Kim
130
Johnson, Lesb
- 622 Clia;DaaSdl
9 Phil
130
11 K. J.
000
i +
12Te1ri
149emt
1
9
~
~
623 DataSou~cel
15 Kalheme
671
M Chr~s
671
24 Pete
120
28 Ann
623
29 Roger
110
34 Janel
"
Forest, Phil
Wedm, K. J
Len. Terri
Hall. Stewart
YKatherine
Papmkrpoulos. Chris
F~her,Pete
Bemet. Ann
De Swza. R o w
B a l k . Jan&
17/11
17tlt1/5/1
4/6/1
1219/
1/2/
18/21
-OOOOOlOOA,?7B)
'
o b j e c t DataSourcel: TDataSource
DataSet = ClientDataSetl
end
end
Los programas de esta primera aplicacion de tres capas son obviamente muy
sencillos, aunque sirven de ejemplo sobre como crear un visualizador de conjuntos de datos capaz de repartir el trabajo entre dos ficheros ejecutables diferentes.
A estas alturas, nuestro cliente solo desempeiia funciones de visualizacion. Si 10s
datos se editan en el cliente, estos no se actualizaran en el servidor. Para llevar a
acabo esta operacion, sera precis0 aiiadir a1 programa algo de codigo adicional.
No obstante, antes de proceder con ello, aiiadiremos algunas caracteristicas mas
a1 servidor.
datos no son validos. Otra de las ventajas que ofrece la codificacion de las restricciones en el lado del servidor consiste en el hecho de que si cambian las reglas de
negocio, bastara con actualizar el unico servidor de aplicacion y no todos y cada
uno de 10s diversos clientes instalados en diferentes ordenadores.
Para definir las restricciones, se pueden utilizar varias propiedades:
Los conjuntos de datos BDE tienen una propiedad constraints, que
consiste en un conjunto de objetos TChec kCons traint. Cada objeto
tiene unas pocas propiedades, entre las que se incluyen la expresion y el
mensaje de error.
Cada objeto de campo define las propiedades c u s t omcons t raint y
C o n s t r a i n t E r r o r M e s s a g e . Tambien existe una propiedad
Import edCo ns t r a i nt para las restricciones importadas desde el servidor SQL.
Cada objeto de campo tiene una propiedad Def aultExpres s ion que
puede utilizarse localmente o que puede transferirse al ClientDataSet. No
se trata de una restriccion real, es tan solo una sugerencia para el usuario
final.
El ejemplo siguiente, AppServ2, aiiade unas cuantas restricciones a un modulo
de datos remoto conectado a la base de datos de muestra EMPLOYEE de InterBase.
Despues de conectar la tabla a la base de datos y tras haber creado 10s objetos de
campo pertinentes, pueden establecerse las siguientes propiedades especiales:
o b j e c t SQLDataSetl:
TSQLDataSet
.. .
o b j e c t SQLDataSetlEMP-NO: TSmallintField
Customconstraint = ' x > 0 a n d x < 1 0 0 0 0 '
ConstraintErrorMessage =
'Employee number m u s t b e a p o s i t i v e i n t e g e r b e l o w 10000'
FieldName = 'EMP-NO'
end
o b j e c t SQLDataSetlFIRST-NAME: TStringField
CustomConstraint = ' x <> ' # 3 9 # 3 9
ConstraintErrorMessage = ' T h e f i r s t n a m e i s r e q u i r e d '
FieldName = 'FIRST-NAME'
Size = 15
end
o b j e c t SQLDataSetlLAST-NAME: TStringField
CustomConstraint = ' n o t x i s n u l l '
ConstraintErrorMessage = ' T h e l a s t n a m e i s r e q u i r e d '
FieldName = ' LAST-NAME'
end
end
tamente igual que cuando un usuario edita, inserta, o borra directamente campos
de manera local.
Esto proceso de actualizacion se solicita estableciendo la propiedad
ResolveToDataSet del componente TDat a s e t p r o v i d e r , conectando de
nuevo el conjunto de datos utilizado para la entrada o un segundo conjunto usado
para actualizaciones. Este enfoque es posible con conjuntos de datos aptos para
operaciones de edicion, como 10s conjuntos de datos de BDE, ADO, e InterBase
Express, per0 no 10s pertenecientes a la nueva arquitectura dbExpress.
Con esta tecnica, es el propio conjunto de datos el que realiza las actualizaciones, lo que implica un alto grado de control (se lanzan 10s eventos estandar)
asociado generalmente a una disminucion del rendimiento. El grado de flexibilidad es mucho mayor, ya que se puede utilizar incluso metodos estandar de programacion. Ademas, adaptar las aplicaciones de bases de datos locales o clientel
servidor existentes, que utilizan eventos de conjuntos de datos y campos, es mucho mas directo de acuerdo con este modelo. Sin embargo, hay que tener en
cuenta que 10s usuarios del programa cliente recibiran 10s mensajes de error emitidos solo cuando la cache local (10s paquetes delta) se envien de vuelta a la capa
intermedia. Decir a1 usuario que algunos datos preparados hace media hora ya no
son validos, podria parecer algo raro. Si se adopta este enfoque, probablemente
sea precis0 aplicar las actualizaciones en la cache cada vez que se produzca un
evento A f t e r P o s t en el entorno del cliente.
Por ultimo, si se permite que sea el conjunto de datos y no el proveedor el
encargado de realizar las actualizaciones, Delphi resulta de gran utilidad para
tratar posibles excepciones. Cualquier excepcion lanzada por 10s eventos de actualizacion de la capa intermedia (por ejemplo OnBef o r e P o s t ) , Delphi la transformara automaticamente en un error de actualizacion que activara el evento
OnRe c o n c i l e E r r o r en el cliente. En la capa intermedia no se muestra excepcion alguna, sino que el error vuelve a1 cliente.
Update
1Status
usunmodified
Snapshot.
Reload.
( FIRST-NAME
(HIRE-DATE
12/ 7/1993
121 Roberto
125
).
Show Delta
...............................,
,
L"*
,~!s.Jus.s-w
kodified
---
621
99912
,-
-. -. .--.
Bruce
28/12/1988
-
Secuencia de actualizacion
Este programa cliente incluye tambidn un boton que sirve para aplicar las
actualizaciones a1 servidor y un dialog0 estandar de reconciliation. A continuacion. se ofrece un resumen de la secuencia completa de operaciones relacionadas
con una solicitud de actualizacion y 10s posibles eventos de error:
1. El programa cliente llama el metodo Applyupdates de un ClientDataSet.
2. El delta se envia a1 proveedor de la capa intermedia. El proveedor lanza el
evento OnUpdat e Dat a en el que se podran examinar las modificaciones
solicitadas antes de que dstas lleguen a1 senidor de la base de datos. En
este punto sera posible modificar el delta, que se transmite en un formato
compatible con 10s datos de un ClientDataSet.
3 . El proveedor (tecnicamente, una parte del proveedor llamada "resolver" o
resolutor) aplica cada una de las filas del delta a1 servidor de la base de
datos. Antes de aplicar cada actualizacion, el proveedor recibe un evento
Bef oreUpdateRecord. Si se ha activado el indicador ResolveToDataSet, esta actualizacion acabara lanzando eventos locales del conjunto de datos de la capa intermedia.
Refresco de datos
El metodo R e f r e s h del ClientDataSet permite obtener una version actualizada de 10s datos que podrian haber sido modificados por otros usuarios. Sin
embargo, esta operacion solo podra realizarse si en la cache no figura ninguna
tarea de actualizacion pendiente, puesto que si se llama a1 metodo y no esta vacio
el registro de modificaciones, R e f r e s h lanzara una excepcion:
if cds .ChangeCount
cds.Refresh;
then
Si solo se han modificado algunos registros, se puede refrescar el resto llamando a1 metodo R e f r e s hRe c o r d s . ~ s t solo
e refrescara el registro actual, per0
solo deberia utilizarse cuando el usuario no haya modificado el registro actual.
En este caso, el metodo R e f r e s hRe c o r d s no hace mas que apuntar 10s cambios no aplicados en el registro de modificaciones. Como ejemplo, se puede refrescar un registro cada vez que pasa a ser el activo, a menos que ya haya sido
modificado y las modificaciones aun no se hayan enviado a1 servidor:
procedure TForml.cdsAfterScrol1(DataSet: TDataSet);
begin
if cds .Updatestatus = usUnModified then
cds.RefreshRecord;
end;
registros visibles en una cuadricula. Este codigo forma parte del ejemplo
ClientRefresh, conectado al servidor AppServ2. A efectos de depuracion, el programa tambien registra el campo EMP-NO para cada registro que refresca, como
muestra la figura 16.4.
RJresh Lop.
DEPT-NO~EMP-NO
- 600
IFIRST-NAMEIHIREDATE
2 Robe11
4 Bruce
621
5 Kun
8 Leslie
9 PhJ
11 K.J
12 Terri
28112/1988
2811211988
6/2/1989
5/4/1989
171411989
17/1/1990
1/St1 990
I~q.1
vPJ
En
ill
1
Figura 16.4. El formulario del ejemplo ClientRefresh refresca automaticamente el
registro activo y permite actualizaciones mas extensas haciendo clic sobre 10s
botones.
class (TDBGrid)
procedure TFOrrnl.Button2Click(Sender: T O b j e c t ) ;
var
i: Integer;
bm: TBookmarkStr;
begin
/ / refresca las filas visibles
cds.DisableControls;
/ / comienza con la fila actual
i := TMyGrid (DbGridl).ROW;
b m : = cds.Bookmark;
try
/ / vuelve a1 primer registro visible
while i > 1 do
begin
cds.Prior;
Dec (i);
end ;
/ / v u e l v e a 1 regis t r o a c t u a l
i : = TMyGrid (DbGridl).Row;
c d s - B o o k m a r k : = bm;
// s i g u e a d e l a n t e h a s t a q u e l a c u a d r i c u l a e s t a c o m p l e t a
w h i l e i < TMyGrid (DbGridl).Rowcount do
begin
cds .Next;
Inc (i);
end;
finally
// d e f i n e t o d o d e n u e v o y r e f r e s c a
c d s - B o o k m a r k : = bm;
cds.EnableControls;
end;
end;
Este enfoquc genera un trafico de red niuy denso, por lo que podria desearse
desencadenar actualizaciones unicamente cuando se produzcan modificaciones
rcales. Esta operacion se puede implcmentar aiiadiendo tecnologia de retrollamadas
a1 servidor, para que este pueda informar a todos 10s clientes conectados de que se
ha modificado un determinado registro. El cliente podra determinar si la modification es de su interes y, en caso oportuno, lanzar la solicitud de rnodificacion.
ADVERTENCIA: El ejemplo ThinPlus requiere que se ejecute el semidor de sockets de Delphi (que se encuentra en la carpeta bin de Delphi). Sin
este programa, se vera un error de socket cuando el cliente intente conectarse con el servidor. La parte positiva es que se puede desplegar el programa
facilmente sobre una red modificando la direccion IP del semidor en el
programa cliente.
Ademas de las caracteristicas que trataremos en las secciones siguientes, 10s
ejemplos AppSPlus y ThinPlus muestran la utilizacion de una conexion de socket,
el registro limitado de eventos y actualizaciones en el servidor y la captura directa
Esta caracteristica permitira obtener mas registros que 10s requeridos por la
interfaz de usuario del cliente (la DBGrid). En otras palabras, se pueden conseguir directamente 10s registros sin esperar a que el usuario se desplace por toda la
cuadricula. Es aconsejable estudiar 10s detalles de estos ejemplos complejos despues de la lectura del resto de esta seccion.
* f r o m customer w h e r e Country
:Country
biblioteca de tipos del servidor y utilizarlo igual que con cualquier otro servidor
COM. En el ejemplo AppSPlus, hemos aiiadido un metodo L o g i n personalizado
con la siguiente implementation:
procedure TAppServerPlus.Login(const Name, Password:
Widestring) ;
begin
// TODO: a d a d i r c o d i g o d e l o g i n r e a l . . .
i f Password <> Name t h e n
raise Exception .Create ( ' W r o n g n a r n e / p a s s w o r d c o m b i n a t i o n
received' )
else
Query .Active := True;
S e r v e r F o r m - A d d ( ' L o g i n : ' + Name + ' / ' + Password) ;
end;
IMl
6582 N&'a
er SCUBA Company
SCURA L i i e d
FO Box Sn 91
PO Box 6834
Figura 16.5. Formulario secundario del ejemplo ThinPlus, que muestra 10s datos de
una consulta por parametros.
El programa realiza una comprobacion sencilla, en lugar de contrastar la combinacion de nombre y contraseiia de acceso con una lista de autorizaciones tal y
como deberia hacer una aplicacion real. Ademas la inhabilitacion de Q u e r y no
funciona realmente, ya puede activarla el proveedor. La inhabilitacion del
DataSetProvider resulta realmente mas adecuada. El cliente tiene una manera
sencilla de acceder al servidor: la propiedad A p p S e r v e r del componente de
conexion remota. Esta es una llamada de muestra del ejemplo ThinPlus que tiene
lugar en el evento A f t e r c o n n e c t del componente de conexion:
procedure TClientForm.ConnectionAfterConnect(Sender: TObject);
begin
Connection.AppServer.Login (Edit2.Text, Edit3.Text);
end;
Hay que tener en cuenta que tambien se puede llamar a metodos adicionales de
la interfaz COM a traves de DCOM, asi como utilizando una conexion de socket
o HTTP. Dado que el programa utiliza la convencion de llamada s a f e c a l l , la
Relaciones maestroldetalle
Si la aplicacion de la capa intermedia exporta diversos conjuntos de datos,
estos se pueden extraer por medio de diversos componentes ClientDataSet en la
parte del cliente y conectarlos localmente formando una estructura maestroldetaIle. Esto ocasionara ciertos problemas para el conjunto de datos de detalle. a
menos que se estraigan todos 10s registros de forma local.
Esta solucion tambien dificulta la aplicacion de actualizaciones: normalmente,
un registro maestro no se puede cancelar hasta que se han eliminado todos 10s
rcgistros detallados relacionados, del mismo mod0 que tampoco es posible la
adicion de registros detallados hasta que el nuevo registro macstro esta ubicado
adecuadamente. (Distintos servidores se enfrentan a esta situacion de formas distintas, pero en la mayoria de 10s casos en 10s que se utilizan claves externas: este
es el procedimiento estandar.) Para solucionar este problema, se puede escribir
codigo complejo en el cliente para actualizar 10s registros de las dos tablas segun
las reglas especificas.
Un enfoque completamente diferente consiste en obtener un unico conjunto de
datos que ya incluya el detalle como un campo de conjunto de datos; un campo de
tipo T D a t a s e t F i e l d . Para ello es necesario preparar la relacion maestro/
detalle en la aplicacion de servidor:
o b j e c t Tablecustomer: TTable
DatabaseName = ' DBDEMOS'
TableName = ' cus torner. db'
end
o b j e c t Tableorders : TTable
DatabaseName = ' DBDEMOS'
end
o b j e c t c d s : TClientDataSet
ConnectionBroker = ConnectionBrokerl
end
// e n el formulario secundario
o b j e c t cdsQuery: TClientDataSet
ConnectionBroker = C1ientForm.ConnectionBrokerl
end
Basicamente no hay que hacer nada mas. Para modificar la conexion fisica,
elegimos un nuevo componente de conexion DataSnap para el formulario principal y establecemos la propiedad Connection del agente para que utilice esa
conexion.
Pooling de objetos
Cuando varios clientes se conectan a1 servidor a1 mismo tiempo, existen dos
opciones: en primer lugar, se puede crear un objeto de modulo de datos remoto
para cada uno y permitir que cada solicitud sea procesada secuencialmente (el
comportamiento estandar de un servidor COM con el estilo ciMultiInstance). Por
otra parte, se puede dejar que el sistema Cree una instancia diferente de la aplicacion para cada cliente (ciSingleInstance). Este enfoque necesita mas recursos y
mas conexiones (y posiblemente licencias) del servidor SQL.
El soporte de DataSnap para el pooling de objetos ofrece un enfoque alternativo. Todo lo que se necesita hacer para solicitar esta caracteristica es aiiadir una
llamada a Registerpooled en el metodo UpdateRegistry sobrescrito.
En combinacion con el soporte sin estado integrado en esta arquitectura, la capacidad de pooling permite compartir ciertos objetos de la capa intermedia entre un
numero mucho mayor de clientes. En COM+ se incluye un mecanismo de pooling,
per0 DataSnap permite que este disponible tambien para conexiones basadas en
sockets y HTTP.
Los usuarios de 10s ordenadores cliente invertiran la mayor parte de su tiempo
leyendo y registrando actualizaciones, y en general no continuan solicitando datos
ni enviando actualizaciones. Cuando el cliente no llama a un metodo del objeto de
la capa intermedia, este modulo de datos remoto puede ser utilizado por otro
cliente. A1 carecer de estado, cada una de las solicitudes llega a la capa intermedia
como si fuese una operacion nueva, aun cuando un servidor este dedicado a un
cliente especifico.
TAppServerPlus.ProviderQueryGetDataSetProperties(
Creacion de
componentes
de bases
de datos
Ya hemos comentado la creacion de componentes Delphi en profundidad en un
capitulo anterior. Ahora que ya hemos comentado la programacion de bases de
datos, podemos volver a1 tema anterior y centrarnos en el desarrollo de componentes relacionados con bases de datos.
Basicamente existen dos familias de este tip0 de componentes: controles dataaware que pueden usarse para presentar 10s datos de un campo o un registro a1
completo a 10s usuarios de un programa y componentes de conjuntos de datos que
se pueden definir para proporcionar datos a controles data-aware ya existentes,
leer datos desde una base de datos o cualquier otra fuente de datos. Este capitulo
trata 10s siguientes temas:
Componentes data-aware: el enlace de datos.
Controles data-aware orientados a campos.
TrackBar y ProgressBar data-aware.
Controles data-aware orientados a registros.
Un visualizador de registros .
Creacion de conjuntos de datos personalizados.
Guardar un conjunto de datos a un flujo local
El enlace de datos
Cuando escribimos un programa de base de datos en Delphi, solemos conectar
algunos controles data-aware (controles "conscientes de 10s datos") con un componente DataSource y, a continuacion, conectar este ultimo a un conjunto de
datos.
La conexion entre 10s controles data-aware y el DataSource se denomina enlace de datos y se representa mediante un objeto de la clase T D a t a L i n k . El
control data-aware crea y gestiona este objeto, a la vez que constituye su unica
conexion con 10s datos. Desde un punto de vista mas practico, para elaborar un
componente data-aware, hay que acoplarle un enlace de datos y exteriorizar algunas de las propiedades del objeto interno, como las propiedades D a t a S o u r c e y
DataField.
La clase TDataLink
En gran parte de este capitulo trabajaremos con T D a t a L i n k y sus clases
derivadas, que estan definidas en la unidad DB. Este tipo cuenta con un grupo de
metodos virtuales protegidos, 10s cuales tienen una funcion muy similar a la de
10s eventos.
Se trata de metodos que "no hacen casi nada" y podemos sobrescribir en una
subclase especifica para interceptar operaciones del usuario y otros eventos de la
fuente de datos. A continuacion, aparece un listado extraida del codigo fuente de
esta clase:
type
TDataLink = class (TPersistent)
protected
procedure ActiveChanged; virtual;
procedure CheckBrowseMode; virtual;
procedure DataSetChanged; virtual;
procedure DataSetScrolled(Distance: Integer); virtual;
procedure
procedure
procedure
procedure
procedure
A1 igual que cualquier otro componente data-aware conectado a un solo campo, este control permite disponer de las propiedades D a t a s o u r c e y D a t a F i e l d .
Hay que escribir un codigo muy breve, que simplemente consiste en exportar las
propiedades del objeto de enlace de datos interno de la siguiente manera:
function TMdDbProgress.GetDataFie1d:
begin
Result : = FDataLink.FieldName;
end;
string;
procedure TMdDbProgress-SetDataField
begin
FDataLink-FieldName : = Value;
end ;
(Value: string);
function TMdDbProgress.GetDataSource:
begin
Result : = FDataLink.DataSource;
end;
TDataSource;
procedure TMdDbProgress.SetDataSource
begin
FDataLink.DataSource : = Value;
end;
(Value: TDataSource);
Resulta evidente que para que funcione este componente, hay que crear y destruir el enlace de datos despues de haber creado o destruido el propio componente:
constructor TMdDbProgress-Create (AOwner: TComponent);
begin
inherited Create (AOwner);
FDataLink : = TFieldDataLink.Create;
FDataLink-Control : = Self;
FDataLink-OnDataChange : = Datachange;
end ;
destructor TMdDbProgress.Destroy;
begin
FDataLink-Free;
FDataLink : = nil;
inherited Destroy;
end ;
NOTA: Si recordarnos el analisis realizado sobre el mCtodo Not i fi ca tion, podemos preguntarnos quC ocurriria si se destruye la fuente de da-
--- - ---
- ----
--
--
- . -----------
---I
I
-
--'----
3-
- 1 1
----A-
h'..-.L
-_-
.--a
-1
- -
- -
INmTickelt l ~ m l _ ~ a i dI~y-~e(hod(Card-No
l ~ v e n l ~ o CUSINO
15
7
B
6
3
10
C52 50 DINERS
E40 00 DINERS
C45.00 DINERS
E37.50 DINERS
E50 00 DINERS
A
:
.
.
1-1
256335017856420371
6146617034656232
481853612351817
2513715852358158
0521773736155304 - 1
En comparacion con el control data-aware construido anteriormente, esta clase solo es mas compleja porque tiene tres controladores de mensajes, entre 10s que
se incluyen 10s controladores de notification de componentes, y dos nuevos
Este codigo comprueba tres condiciones: el enlace de datos deberia estar activo, el enlace deberia hacer referencia a un campo real y el campo no deberia ser
de solo lectura. Cuando el usuario modifica el campo, el componente deberia
tener en cuenta que el nombre del campo podria no ser valido. Para comprobar
esta condicion, el componente usa un bloque try/finally:
procedure TMdDbTrack-SetDataField (Value: string);
begin
try
FDataLink.Fie1dName : = Value;
finally
Enabled : = FDataLink.Active and (FDataLink.Field <> nil)
and
not FDataLink.Field.Read0nly;
end ;
end ;
Cuando el conjunto de datos necesita datos nuevos, por ejemplo para realizar
una nueva operacion Post, lo unico que hace es solicitarlos a1 componente mediante el evento OnUpdateData de la clase TFieldDataLink:
p r o c e d u r e TMdDbTrack.UpdateData (Sender: TObject);
begin
if F D a t a L i n k - F i e l d i s TNumericField t h e n
FDataLink.Field.As1nteger : = Position;
end ;
Message:
TCrnExit);
FDataLink-UpdateRecord;
except
SetFocus;
raise;
end ;
inherited;
end;
Una vez mas, existe un programa de muestra para probar este componente,
podemos observar su salida en la figura 17.2. El programa DbTrack contiene una
casilla de verificacion que activa o desactiva la tabla, 10s componentes visuales y
un par de botones que podemos utilizar para separar el componente T r a c k B a r
vertical del campo a1 que esta relacionado. Se han colocado en el formulario para
comprobar la habilitacion e inhabilitacion de la barra de seguimiento.
Figura 17.2. Las barras de seguirniento del ejernplo DbTrack perrniten introducir
datos en una tabla de la base de datos. La casilla de verificacion y 10s botones
comprueban el estado de activacion de 10s cornponentes.
// l o p i n t a t o d o d e n u e v o . .
RView.Invalidate;
end :
procedure TMdRecordLink.RecordChanged;
begin
inherited;
// l o p i n t a t o d o d e nuevo.. .
RView.Invalidate;
end ;
El codigo del enlace de registros es muy sencillo. La mayor parte de las dificultades de la construccion de este ejemplo se deben a la utilizacion de la cuadricula. Para evitar las propiedades innecesarias, hemos derivado la cuadricula del
visualizador de registros de la clase TCustomGrid.Esta clase incluye gran
parte del codigo para las cuadriculas, per0 la mayoria de sus propiedades, eventos y metodos estan protegidos. Esta es la razon por la que la especificacion de la
clase resulta bastante larga, ya que es necesario publicar muchas de las propiedades existentes. Este es un fragment0 de codigo (en el que se excluyen las propiedades de la clase basica):
type
TMdRecordView = class (TCustomGrid)
private
// s o p o r t a data-aware
FDataLink: TDataLink;
function GetDataSource: TDataSource;
procedure SetDataSource (Value: TDataSource) ;
protected
// rnetodos r e d e f i n i d o s TCustomGrid
procedure Drawcell (ACol, ARow: Longint ; ARect : TRect;
AState: TGridDrawState); override;
procedure ColWidthsChanged; override;
procedure RowHeightsChanged; override;
public
constructor Create (AOwner: TComponent) ; override;
destructor Destroy; override;
procedure SetBounds (ALeft, ATop, AWidth, AHeight:
Integer) ; override;
procedure Defineproperties (Filer: TFiler) ; override;
// p r o p i e d a d e s p a d r e p d b l i c a s ( o m i t i d a s . . . )
published
// p r o p i e d a d e s d a t a - a w a r e
property DataSource: TDataSource read GetDataSource write
SetDataSource;
/ / p r o p i e d a d e s p a d r e p u b l i c a d a s ( o m it i d a s . . . )
end ;
Ademas de volver a especificar las propiedades para publicarlas, el componente define un objeto de enlace de datos y la propiedad Datasource.No existe
la propiedad Data Field para este componente, ya que hace referencia a todo
La cuadricula tiene dos columnas (una de ellas fija) y ninguna fila fija. La
columna fija se usa para modificar el tamaiio de cada fila de la cuadricula. Por
desgracia. el usuario no puede utilizar la fila fija para ajustar el tamaiio de las
columnas; ya que no es posible modificar el tamaiio de elementos fijos y la cuadricula ya cuenta con una colurnna fija.
Esta modification del tamaiio tiene lugar cuando cambia el tamaiio del componente y cambia alguna de las columnas. Con este codigo, la propiedad
DefaultColWidth del componente se convierte en el ancho fijo de la primera
columna. Despues de haberlo preparado todo, el metodo clave del componente es
el metodo DrawCell sobrescrito, que se detalla en el listado 17.1. En este meto-
do, el control muestra la informacion sobre 10s campos y sus valores. Tiene que
representar tres cosas. Si el enlace de datos no esta conectado a una fuente de
datos, la cuadricula muestra un simbolo de "elemento vacio" ([]).Cuando se representa la primera colurnna, el visualizador de registros muestra la propiedad
DisplayName del campo, que es el mismo valor que el utilizado por la DBGrid
para el encabezamiento. A1 pintar la segunda colurnna, el componente accede a la
representacion textual del valor del campo, extraida de la propiedad
DisplayTex t (o con la propiedad Ass t r i n g para 10s campos de memo).
Listado 17.1. El rnetodo Drawcell del cornponente Recordview personalizado
procedure TMdRecordVie~.DrawCe11(ACol, ARow: Longint; ARect:
TRect;
AState: TGridDrawState) ;
var
Text: string;
CurrFleld: TField;
Bmp : TBitmap ;
begin
CurrField : = nil;
Text : = ' [ I ' ; / / p o r d e f e c t 0
// p i n t a e l fondo
if (ACol = 0 ) then
Canvas.Brush.Color : = Fixedcolor
else
Canvas.Brush.Color : = Color;
Canvas. FillRect (ARect);
// d e j a u n p e q u e d o b o r d e
InflateRect (ARect, -2, -2) ;
if (FDataLink.DataSource <> nil) and FDataLink.Active then
begin
CurrField : = FDataLink.DataSet.Fields[ARow];
if ACol = 0 then
Text : = CurrField.DisplayName
else if CurrField is TMemoField then
Text : = TMemoField (CurrField).AsString
else
Text : = CurrField.Disp1ayText;
end;
if (ACol = 1) and (CurrField is TGraphicField) then
begin
Bmp : = TBitmap.Create;
try
Bmp-Assign (CurrField);
Canvas.StretchDraw (ARect, Bmp) ;
finally
Bmp .Free ;
end;
end
else if (ACol = 1) and (CurrField is TMemoField) then
begin
En la ultima parte del metodo, el componente tiene en cuenta 10s campos graficos y de memo. Si el campo es un TMemoField, la llamada a la funcion
DrawText no especifica el indicador dt SingleLine, sino que utiliza el
indicador dt WordBreak para partir las palabras cuando no hay espacio suficiente. ~ l a r o & i que para un campo grafico, el componente utiliza un enfoque
totalmente distinto, que consiste en asignar a la imagen de campo un mapa de bits
temporal y despues ampliarlo hasta que cubra la superficie de la celda.
Observe ademas que el componente establece como False la propiedad
DefaultDrawing, de tal forma que tambien es responsable de pintar el fondo
y el rectangulo de foco, igual que ocurre en el metodo Drawcell. El componente tambien llama a la funcion In f 1 a t eRe ct de la API para dejar un pequeiio
espacio entre el borde de celda y el texto de salida. La salida real se obtiene a1
llamar a otra funcion de la API de Windows, DrawText, la cual centra el texto
verticalmente dentro de su celda.
Este codigo de representacion funciona tanto en tiempo de ejecucion, como
podemos comprobar en la figura 17.3, como en tiempo de diseiio. La salida puede
que no sea perfecta, per0 este componente puede ser util en muchos casos. Si
queremos mostrar 10s datos de un solo registro, en lugar de construir un formulario personalizado con etiquetas y controles data-aware, podemos utilizar de forma sencilla esta cuadricula de visualizacion de registros. No obstante, es importante
tener presente que el visualizador de registros es un componente de solo lectura.
Es posible ampliarlo para que adquiera capacidades de edicion (ya forman parte
de la clase TCustomGrid), pero, de todas formas, en lugar de aiiadir estc soporte, hemos decidido hacer que el componente sea mas completo afiadiendo soporte para mostrar campos BLOB.
Para mejorar la salida grafica, el control traza las lineas de 10s campos BLOB
el doble de altas que 10s campos de texto simple. Esta operacion se realiza una vez
que se activa el conjunto de datos conectado a1 control data-aware. El metodo
Activechanged del enlace de datos tambien se activa mediante 10s metodos
RowHeightsChanged conectados a la propiedad Def aultRowHeight de
la clase basica:
p r o c e d u r e TMdRecordLink.ActiveChanged;
var
I: Integer;
begin
// d e f i n e e l numero d e f i l a s
RView-RowCount : = DataSet.FieldCount;
[I] : = RView.DefaultRowHeight
* 2;
RView-Invalidate;
end ;
-I
Blue Angelhsh
Species Name
Panafanltusnararchus
11 81 1 a236220472
Habrtal 1s around bouldels. caves.
coral ledges and crevices m shallow
waters. Swims alone or in groups.
Bdrtudes
Snappei
Red Emperor
conwch
. ggerl~sh lnhab~ts
&ter lee! areas and feeds
upon crudaceam and
rnollusks bv c~ush~na
them
Called seaperch In
Austraha lnhablls h e
Lut~anussebae
W~asse
Giant Maor1W~asse
Che~l~nur
undulatus
hwptlsh
Bbe Angdlish
Pmacanlhus nauarchus
Mientras que para crear la salida tan solo hub0 que adaptar el codigo utilizado
cn el componente visualizador de registros, para establecer la altura de las celdas
de la cuadricula se planteo un problema de dificil solucion. iLas lineas del codigo
para esa operacion puede quc sean pocas, per0 cost6 horas llegar a esta solucion!
-
El constructor establece el valor predeterminado para el campo F L i n e s PerRow. A continuacion, se muestra el metodo de establecimiento de la propiedad:
procedure TMdDbGrid.SetLinesPerRow(Va1ue:
begin
if Value <> FLinesPerRow then
begin
FLinesPerRow : = Value;
LayoutChanged;
end ;
end ;
Integer);
3;
+ 4;
// d e f i n e e l n u r n e r o d e f i l a s
RowCount : = 1 + (Height - PixelsTitle) div (PixelsPerRow
FLinesPerRow) ;
// d e f i n e l a a l t u r a d e cada f i l a
DefaultRowHeight : = PixelsPerRow * FLinesPerRow;
RowHeights [0] : = PixelsTitle;
f o r I : = 1 to RowCount - 1 do
RowHeights [I] : = PixelsPerRow * FLinesPerRow;
// e n v i a u n m e n s a j e -SIZE
para p e r m i t i r q u e e l cornponente
// b a s e v u e l v a a c a l c u l a r l a s f i l a s v i s i b l e s e n e l metodo
/ / p r i v a d o Upda t e R o w C o u n t
PostMessage (Handle, WM-SIZE, 0 , MakeLong(Width, Height));
end;
Lo mas dificil de conseguir en este metodo fue que las cuatro ultimas lineas
fueran correctas. Podemos definir la propiedad D e f a u l t R o w H e i g h t , per0 es
probable que en ese caso el titulo de fila sea demasiado alto. En un principio se
intento establecer la propiedad DefaultRowHeight y, despues, la altura de la
primera fila, per0 este enfoque complicaba el codigo usado para calcular el numero de filas visibles en la cuadricula (la propiedad de solo lectura V i s i b l e R o w C o u n t ) . Si especificamos el numero de filas, para que las filas no queden
escondidas debajo del extremo inferior de la cuadricula, la clase basica continua
sigue calculandolo. Este es el codigo usado para representar 10s datos, tomado del
componente R e c o r d v i e w y ligeramente adaptado para la cuadricula:
p r o c e d u r e TMdDbGrid.DrawColumnCel1 (const Rect: TRect; DataCol:
Integer;
Column: TColumn; State: TGridDrawState);
var
Bmp: TBitmap;
OutRect : TRect;
begin
i f FLinesPerRow = 1 t h e n
inherited DrawColurnnCell (Rect, DataCol, Column, State)
else
begin
// lirnpia l a zona
Canvas. FillRect (Rect);
// copia e l r e c t d n g u l o
OutRect := Rect;
// r e s t r i n g e l a s a l i d a
InflateRect (OutRect, - 2 , - 2 ) ;
// s a l i d a d e d a t o s d e l carnpo
Bmp : = TBitmap.Create;
try
Bmp.Assign (Column.Field) ;
Canvas .StretchDraw (OutRect, Bmp) ;
finally
Bmp.Free;
end ;
end
else i f Column.Field i s TMemoField then
begin
En este codigo, se puede comprobar que si el usuario muestra una unica linea,
la cuadricula utiliza la tecnica estandar de representacion, sin salidas para campos graficos ni de memo. No obstante, en cuanto aumenta el numero de lineas se
podra comprobar como mejora la salida.
Para ver como funciona este codigo, hay que ejecutar el ejemplo GridDemo.
Este programa cuenta con dos botones que podemos utilizar para aumentar o
disminuir la altura de las filas de la cuadricula y otros dos mas para cambiar la
fuente. Se trata de una comprobacion importante, ya que la altura de cada celda
en pixeles es la altura de la fuente multiplicada por el numero de lineas.
millones de punteros) del libro. Aun mas, Borland aun no ha publicado ninguna
documentacion oficial sobre la creacion de conjuntos de datos personalizados. Si
se trata de las primeras experiencias con Delphi, podria ser aconsejable saltarse el
resto de este capitulo y volver a este punto mas adelante.
La clase TData Set es una clase abstracta que declara varios metodos abstractos virtuales (23 metodos en Delphi, ahora solo unos cuantos, ya que la mayoria han sido sustituidos por metodos virtuales vacios que habra que sobrescribir).
Cada subclase de TDataSet debe sobrescribir todos esos metodos.
Antes de analizar el desarrollo de un conjunto de datos personalizado, debemos estudiar algunos elementos tecnicos de la clase TDataSet,en concreto 10s
buffers de 10s registros. La clase mantiene una lista de buffers que almacenan 10s
valores de 10s diversos registros. Estos buffers almacenan 10s datos, per0 tambien
suelen almacenar informacion adicional para que la utilice el conjunto de datos
cuando este trabajando con 10s registros. Estos buffers no tienen una estructura
predefinida y cada conjunto de datos personalizado debe ubicarlos, rellenarlos y
destruirlos. El conjunto de datos personalizado tambien debe copiar 10s datos
desde 10s buffers de registro a 10s distintos campos del conjunto de datos, y
viceversa. En otras palabras, el conjunto de datos personalizado es completamente responsable de la gestion de estos buffers.
Ademas de la gestion de 10s buffers de datos, el componente tambien es responsable de la navegacion entre 10s registros, la gestion de 10s marcadores, la
definicion de la estructura del conjunto de datos y la creacion de 10s campos de
datos adecuados. La clase TDataSet no es mas que un entorno de trabajo que
hay que rellenar con el codigo apropiado. Afortunadamente, la mayor parte del
codigo sigue una estructura estandar que utilizan las clases derivadas de
TDataset de la VCL. Una vez que se hayan comprendido las ideas clave, se
podran crear multiples conjuntos de datos personalizados tomando prestada una
gran cantidad de codigo.
Para simplificar este tip0 de reciclaje, hemos agrupado las caracteristicas comunes que necesita todo conjunto de datos personalizado de la clase
TMDCustomDataSet.Sin embargo, no vamos a comentar en primer lugar la
clase base y despues la implernentacion especifica, porque resultaria algo complicado. En su lugar, detallaremos el codigo necesario para un conjunto de datos,
presentando 10s metodos de las clases genericas y especificas a1 mismo tiempo,
segun un esquema logico.
// e n l a u n i d a d M d D s C u s t o m
type
EMdDataSetError = class (Exception);
TMdRecInfo = record
Bookmark: Longint;
BookmarkFlag: TBookmarkFlag;
end ;
PMdRecInfo = "TMdRecInfo;
TMdCustomDataSet = class (TDataSet)
protected
// e s t a d o
FIsTableOpen: Boolean;
// d a t o s d e l r e g i s t r o
FRecordSize,
// e l t a m a d o d e 1 0 s d a t o s r e a l e s
FRecordBufferSize,
// d a t o s + t a r e a s d e m a n t e n i m i e n t o
// ( T R e c I n f o )
FCurrentRecord,
// r e g i s t r o a c t u a l ( 0 a F R e c o r d C o u n t - 1 )
BofCrack,
// a n t e s d e l p r i m e r r e g i s t r o ( c r a c k )
EofCrack: Integer; // d e s p u e s d e l u l t i m o r e g i s t r o
// ( c r a c k )
// c r e a , c i e r r a , e t c .
procedure Internalopen; override;
procedure Internalclose; override;
function IsCursorOpen: Boolean; override;
// f u n c i o n e s p e r s o n a l i z a d a s
function InternalRecordCount: Integer; virtual; abstract;
procedure Internalpreopen; virtual;
procedure InternalAfterOpen; virtual;
procedure InternalLoadCurrentRecord(Buffer: PChar);
virtual; abstract;
// a d m i n i s t r a c i d n d e m e m o r i a
function AllocRecordBuffer: PChar; override;
procedure InternalInitRecord(Buffer: PChar); override;
procedure FreeRecordBuffer(var Buffer: PChar); override;
function GetRecordSize: Word; override;
// m o v i m i e n t o y n a v e g a c i o n o p c i o n a l ( u t i l i z a d a p o r l a s
// c u a d r i c u l a s )
function GetRecord(Buffer: PChar; GetMode: TGetMode;
DoCheck: Boolean) :
TGetResult; override;
procedure InternalFirst; override;
procedure InternalLast; override;
// e n l a u n i d a d M d D s S t r e a r n
type
TMdDataFileHeader = record
VersionNumber: Integer;
Recordsize: Integer;
Recordcount: Integer;
end :
TMdDataSetStream = class (TMdCustomDataSet)
private
procedure SetTableName(const Value: string);
protected
FDataFileHeader: TMdDataFileHeader;
FDataFileHeaderSize,
// tannfio d e cabecera d e a r c h i v o
// o p c i o n a l
FRecordCount: Integer; // n u m e r o a c t u a l d e r e g i s t r o s
FStream: TStream;
// l a t a b l a f i s i c a
FTableName: string;
// nombre d e a r c h i v o y r u t a d e t a b l a
FFieldOffset: TList; // d e s p l a z a m i e n t o s d e campo e n e l
// b u f f e r
protected
// a b r e y c i e r r a
procedure Internalpreopen; override;
procedure InternalAfterOpen; override;
procedure Internalclose; override;
procedure InternalInitFieldDefs; override;
// s o p o r t e d e e d i c i d n
procedure InternalAddRecord(Buffer: Pointer; Append:
Boolean) ; override;
procedure InternalPost; override;
procedure InternalInsert; override;
// c a m p o s
procedure SetFieldData(Fie1d: TField; Buffer: Pointer);
override;
// z k t o d o s v i r t u a l e s d e l c o n j u n t o d e d a t o s
// p e r s o n a l i z a d o
function InternalRecordCount: Integer; override;
procedure InternalLoadCurrentRecord(Buffer: PChar);
override;
public
procedure CreateTable;
function GetFieldData(Fie1d: TField; Buffer: Pointer):
Boolean; override;
published
property TableName: string read FTableName write
SetTableName;
end :
subclases
// i n i c i a l a s d e f i n i c i o n e s d e campos
InternalInitFieldDefs:
// s i n o h a y o b j e t o s d e campo p e r m a n e n t e s , c r e a 1 0 s c a m p o s d e
/ / forma d i n d m i c a
if DefaultFields then
CreateFields;
// c o n e c t a 1 0 s o b j e t o s T F i e l d c o n 1 0 s c a m p o s r e a l e s
BindFields (True);
InternalAfterOpen; / / m e t o d o p e r s o n a l i z a d o p a r a
subclases
// d e f i n e 1 0 s c r a c k s y l a p o s i c i o n y tamado d e l r e g i s t r o
BofCrack : = - 1 ;
EofCrack : = InternalRecordCount;
FCurrentRecord : = BofCrack;
FRecordBufferSize : = FRecordSize + sizeof (TMdRecInfo);
Bookmarksize : = sizeof (Integer);
// t o d o O K : a h o r a l a t a b l a e s t d a b i e r t a
FIsTableOpen : = True;
end;
Podemos ver que el metodo define gran parte de 10s campos locales de la clase,
asi como el campo Boo kmar kSi ze de la clase base TDa taSet . En este metodo, se ha llamado a dos metodos personalizados que se han introducido en la
jerarquia del conjunto de datos personalizado: I n t e r n a 1 P r e o p e n e
InternalAfterOpen.
El primero, Internal PreOpen, se utiliza para operaciones que son nece-
creo la tabla por primera vez. A1 crear una excepcion en este metodo, podemos
detener en ultimo termino la operacion de apertura.
A continuacion, aparece el codigo de 10s dos metodos del conjunto de datos
derivado basado en streams:
cons t
HeaderVersion
10;
procedure TMdDataSetStream-InternalPreOpen;
begin
// e l tamafio d e l a c a b e c e r a
FDataFileHeaderSize : = sizeof (TMdDataFileHeader);
// v e r i f i c a s i e x i s t e e l a r c h i v o
i f n o t FileExists (FTableName) then
r a i s e EMdDataSetError .Create ( ' O p e n : T a b l e f i l e
found' ) ;
not
// c r e a u n s t r e a m p a r a e l a r c h i v o
F S t r e a m : = T F i l e S t r e a m - C r e a t e (FTableName, fmOpenReadWrite);
// i n i c i a d a t o s l o c a l e s
(cargando el t i t u l o )
F S t r e a m - R e a d B u f f e r (FDataFileHeader, FDataFileHeaderSize);
i f FDataFileHeader-VersionNumber <> HeaderVersion then
r a i s e EMdDataSetError. Create ( ' I l l e g a l P i l e V e r s i o n ' ) ;
// v a m o s a l e e r e s t o , v e r i f i c a r d e n u e v o m d s a d e l a n t e
FRecordCount : = FDataFi1eHeader.RecordCount;
end;
procedure TMdDataSetStream.InternalAfter0pen;
begin
// v e r i f i c a e l t a m a f i o d e l r e g i s t r o
i f FDataFi1eHeader.RecordSize <> FRecordSize then
r a i s e EMdDataSetError-Create ( ' F i l e r e c o r d s i z e
mismatch' ) ;
// v e r i f i c a e l n u m e r o d e r e g i s t r o s en o p o s i c i o n a 1 t a m a f i o d e l
// a r c h i v o
i f (FDataFileHeaderSize + FRecordCount * FRecordSize) <>
FStream-Size then
r a i s e EMdDataSetError .Create ( ' I n t e r n a l o p e n : I n v a l i d
Record S i z e ' ) ;
end ;
Este archivo, o uno similar, debe tener el mismo nombre que el archivo de
tabla y debe ubicarse en el mismo directorio.
El metodo InternalInit FieldDef s (que se muestra en el listado 17.4)
lo leera utilizando 10s valores que encuentra para establecer las definiciones de
campo y determinar el tamaiio de cada registro.
El metodo tarnbien inicia un objeto TLis t interno que almacena el desplazamiento de cada campo dentro del registro. Se utiliza esta TLis t para acceder a
10s datos de 10s campos en el buffer de registro, como se puede comprobar en el
fragment0 de codigo.
1niFile.Free;
end ;
FRecordSize : = TrnpFieldOffset;
end ;
Para cerrar la tabla, solo hay que desconectar 10s campos utilizando llamadas
estandar. Cada clase debe encargarse de 10s datos que asigno y actualizar la
cabecera del archivo, la primera vez que se aiiaden 10s registros y cada vez que se
modifique el numero de registros:
procedure
begin
TMDCustomDataSet.InternalClose;
// d e s c o n e c t a r o b j e t o s d e campo
BindFields (False);
// d e s t r u y e e l o b j e t o d e campo ( s i n o e s p e r m n e n t e )
i f DefaultFields then
DestroyFields;
// c i e r r a e l a r c h i v o
FIsTableOpen : = False;
end ;
procedure
begin
TMdDataSetStream.InternalClose;
// s i e s n e c e s a r i o , g u a r d a l a c a b e c e r a a c t u a l i z a d a
(FDataFi1eHeader.RecordCount <> FRecordCount) or
(FDataFi1eHeader.RecordSize = 0) then
begin
FDataFi1eHeader.RecordSize : = FRecordSize;
FDataFi1eHeader.RecordCount : = FRecordCount;
i f Assigned (FStrearn) then
begin
FStream. Seek (0, soFromBeginning) ;
FStrearn.WriteBuffer (FDataFileHeader,
FDataFileHeaderSize);
end ;
end ;
// l i b e r a 1 0 s d e s p l a z a m i e n t o s d e campo d e l a l i s t a i n t e r n a y
el stream
FField0ffset.Free;
FStream. Free;
i n h e r i t e d Internalclose;
end ;
if
Boolean;
Estos son 10s metodos de apertura y cierre que debemos implementar en cualquier conjunto de datos personalizados. No obstante, en la mayoria de 10s casos,
tambien tendremos que aiiadir un metodo para crear la tabla. En este ejemplo, el
metodo CreateTable crea un archivo vacio e inserta informacion en la cabecera: un numero fijo de version, un tamaiio de registro ficticio (no se conoce el
tamaiio real hasta que se inicien 10s campos) y el recuento de registros (que a1
empezar sera cero):
procedure TMdDataSetStream-CreateTable;
begin
CheckInactive;
InternalInitFieldDefs;
// c r e a e l a r c h i v o n u e v o
if FileExists (FTableName) then
raise EMdDataSetError.Create ( ' P i l e ' + FTableName + '
already e x i s t s ' );
FStream : = TFileStream.Create (FTableName, fmCreate or
fmShareExclusive);
try
// guarda l a c a b e c e r a
FDataFi1eHeader.VersionNumber : = Headerversion;
FDataFileHeader .Recordsize : = 0;
/ / s e u t i l i z a mds t a r d e
FDataFileHeader .Recordcount : = 0;
// v a c i o
FStream-WriteBuffer (FDataFileHeader, FDataFileHeaderSize);
finally
// c i e r r a e l a r c h i v o
FStream. Free;
end ;
end :
de datos almacena 10s marcadores para el registro en el buffer, asi como algunos
indicadores de marcadores que se definen como sigue:
type
TBookmarkFlag = (bfcurrent, bfBOF, bfEOF, bf Inserted) ;
FRecordSize
Bookmark
BookmarkFlag
Figura 17.5. La estructura de cada buffer del conjunto de datos personalizado, junto
con 10s diversos campos locales que hacen referencia a sus partes.
Para acceder a 10s marcadores y 10s indicadores, se puede utilizar como desplazamiento el tamaiio de 10s datos reales, convirtiendo el valor a1 tip0 de puntero
P M d R e c Inf o. Despues habra que acceder a1 campo apropiado de la estructura
T M d R e c Inf o mediante el puntero.
Los dos metodos que se utilizan para establecer y obtener 10s indicadores de
marcador muestran esta tecnica:
procedure TMDCustomDataSet.SetBookmarkF1ag (Buffer: PChar;
Value: TBookmarkFlag);
begin
PMdRecInfo(Buffer + FRecordSize).BookmarkFlag : = Value;
end;
function TMDCustomDataSet.GetBookmarkF1ag (Buffer: PChar):
TBookmarkFlag;
begin
Result := PMdRecInf o (Buffer + FRecordSize) .BookmarkFlag;
end;
Longint;
else
Result
end;
:=
FCurrentRecord
1;
p r o c e d u r e TMDCustomDataSet.SetRecNo(Value: Integer);
begin
CheckBrowseMode;
i f (Value > 1 ) and (Value <= FRecordCount) t h e n
begin
FCurrentRecord : = Value - 1;
Resync ( [ I ) ;
end ;
end;
PChar;
(var Buffer:
La razon para reservar memoria de este mod0 es que el conjunto de datos suele
aiiadir mas informacion a1 buffer del registro, por lo tanto el sistema no puede
saber cuanta memoria debe asignar. Hay que fijarse en que, en el metodo
A1 1ocRecordBuf f er, el componente reserva la memoria para el buffer de
registro, en el que se incluyen tanto 10s datos de la base de datos como la informacion de registro. En el metodo Internalopen se escribio:
FRecordBufferSize : = InternalRecordSize
sizeof
(TMdRecInfo);
La comprobacion de si existe un registro determinado puede diferir ligeramente de lo que se podria pensar. No es necesario determinar si el registro actual esta
en el rango adecuado, solo si lo esta ese registro determinado. Por ejemplo, en la
bifurcacion gmcur rent de la sentencia case,se utiliza la expresion estandar
CurrentRecord>=InternalRecourdCount.Para comprender plenamente
10s diversos casos, seria aconsejable leer el codigo un par de veces.
Hay que tener cuidado con la modificacion de este codigo, pues se llego a el
mediante el procedimiento de prueba y error (y una p a n cantidad de bloqueos en
la maquina, debido a las llamadas recursivas). Para comprobarlo, hay que tener
en cuenta que si utilizamos un componente DBGrid, el sistema realizara una
serie de llamadas a GetRecord,hasta que la cuadricula este llena o GetRecord
devuelva grEOF. A continuacion se muestra el codigo completo del metodo
GetRecord:
(Buffer:
PChar) ;
begin
BookmarkFlag := bfcurrent;
Bookmark : = FCurrentRecord;
end ;
end ;
Ptr : = ActiveBuffer;
Inc (Ptr, FieldOffset) ;
i f Assigned (Buffer) then
Move (Ptr", BufferA, Field.DataSize);
Result : = True;
i f (Field is TDateTimeField) and (PInteger (Ptr)A = 0)
then
Result : = False;
end ;
end ;
procedure TMdDataSetOne.SetFieldData(Fie1d: TField; Buffer:
Pointer) ;
var
FieldOffset: Integer;
Ptr: PChar;
begin
i f Field.FieldNo >= 0 then
begin
FieldOffset : = Integer (FFieldOffset [Field.FieldNo - I ] ) ;
Ptr : = ActiveBuffer;
Inc (Ptr, FieldOffset) ;
i f Assigned (Buffer) then
Move (BufferA, PtrA, Field.Datasize)
else
raise Exception.Create ('Very bad error in
TMdDataSetStream. SetField data') ;
DataEvent (deFieldChange, Longint (Field)) ;
end;
end;
[-
--.
Existe un metodo final que no entra en ninguna categoria: I n t e r n a l H a n d l e E x c e p t i o n . En general, este metodo silencia la excepcion, ya que solo se activa en tiempo de diseiio.
ad
) Marco
salsald
-Paul
1 9/8/1965
2 3/12/1990
A-
E50 W lohn@ma~lcorn
F=?
~&m&l
ADVERTENCIA: El programa dc prueba de& ldrufa completa del archive de la tabla en tiempo de diseiio, por lo que sera necesario corregirla si
copiamos 10s ejemplos a un directorio o disco distinto. En el ejemplo, la
propiedad TableName solo se utiliza en tiempo de disefio. En tiempo de
ejecucion, el programa busca la tabla en el directorio actual.
El codigo del ejemplo es bastante sencillo, sobre todo si se compara con el
codigo del conjunto de datos personalizado. Si la tabla todavia no esiste, se puede
hacer clic sobre el boton Create New Table:
procedure T F o r m l . B u t t o n l C l i c k ( S e n d e r : TObject);
begin
MdDataSetStreaml.CreateTab1e;
MdDataSetStreaml.0pen;
CheckBoxl.Checked : = MdDataSetStream1.Active;
end ;
procedure TMdListDataSet-InternalPreOpen;
begin
FList : = T0bjectList.Create (True); // posee 10s objetos
FRecordSize : = 4 ; // un entero, el identificador d e elemento
de la lista
end ;
~antiene..msdatos.enpwmoria. Sbr embargo, mediante tecnicas inteligentes tambiCn se puede w a r bna,lista iTe bbjetos falsos y cargar desputs 'los
objeto$ kales cuando pe acceda a ellos.
El cierre es una simple cuestion de liberacion de la lista, que tiene un numero
de registros que se corresponde con el tamaiio de la lista:
function TMdListDataSet.Interna1RecordCount:
begin
Result : = fList.Count;
end;
Integer;
Solo existe otro metodo que se utiliza para guardar 10s datos del registro actual en el buffer del registro, incluyendo la informacion sobre 10s marcadores.
Los datos centrales se reducen a la posicion del registro actual, que se corresponde con el indice de la lista (y tambien con el marcador):
procedure TMdListDataSet.Interna1LoadCurrentRecord
PChar) ;
begin
PInteger (Buffer)* := fCurrentRecord;
with PMdRecInfo (Buffer + FRecordSize) " do
begin
BookmarkFlag : = bfcurrent;
Bookmark : = fCurrentRecord;
end;
end ;
(Buffer:
type
TMdDirDataset = class(TMdListDataSet)
private
FDirectory: string;
procedure SetDirectory (const NewDirectory: string) ;
protected
// r n e t o d o s v i r t u a l e s d e T D a t a S e t
procedure InternalInitFieldDefs; override;
procedure SetFieldData (Field: TField; Buffer: Pointer) ;
override;
function GetCanModify: Boolean; override;
// r n e t o d o s v i r t u a l e s d e l c o n j u n t o d e d a t o s p e r s o n a l i z a d o
procedure InternalAfterOpen; override;
public
function GetFieldData (Field: TField; Buffer: Pointer) :
Boolean; override;
published
property Directory: stringread FDirectorywrite SetDirectory;
end;
Para cada carpeta se llama a este constructor durante la apertura del conjunto
de datos:
procedure TMdDirDataset.InternalAfter0pen;
var
Attr: Integer;
FileInfo: TSearchRec;
FileData: TFileData;
begin
// define todos 10s archivos
Attr : = faAnyFile;
FList .Clear;
if SysUtils.FindFirst(fDirectory, Attr, FileInfo) = 0 then
repeat
FileData : = TFileData. Create (FileInfo);
FList .Add (FileData);
until SysUtils. FindNext (FileInfo) <> 0;
SysUtils.FindClose(FileInfo);
end;
El siguiente paso es definir 10s campos del conjunto de datos que; en este caso,
son fijos y dependen de 10s datos disponibles del directorio:
procedure TMdDirDataset.InternalInitFie1dDefs;
begin
i f fDirectory = " then
raise EMdDataSetError.Create ('Missing directory');
// definiciones de campos
FieldDefs.Clear;
FieldDefs .Add ( 'FileNamel, ftstring, 40, True) ;
FieldDefs .Add ( ' Timestamp', ftDateTime) ;
FieldDefs .Add ( ' Size1, ftInteger) ;
FieldDefs .Add ( 'Attributes', ftstring, 3) ;
FieldDefs.Add ('Folder', ftBoolean);
end;
Por ultimo, el componente tiene que mover 10s datos desde el objeto de la lista
a1 que hace referencia el buffer del registro actual (el valor A c t i v e B u f f e r )
hasta cada uno de 10s campos del conjunto de datos, tal como lo solicita el metodo
G e t F i e l d D a t a . Esta funcion utiliza Move o S t r C o p y , segun el tipo de datos, y realiza algunas conversiones para 10s codigos de atributos (H para 10s
ocultos, R para 10s de solo lectura y s para 10s de sistema) que sc extraen de 10s
indicadores correspondientes y que tambien se utilizan para determinar si un archive es realmente una carpeta. A continuacion, se muestra el codigo:
function TMdDirDataset.GetFie1dData (Field: TField; Buffer:
Pointer) : Boolean;
var
FileData: TFileData;
Booll: WordBool;
strAttr: string;
t : TDateTimeRec;
begin
FileData : = List [PInteger (ActiveBuffer)" 1 as TFileData;
case Field. Index of
0 : // nombre de archivo
StrCopy (Buffer, pchar(FileData.ShortFi1eName));
1: // s e l l o d e ultima m o d i f i c a c i d n
begin
t := DateTimeToNative (ftdatetime, FileData.Time) ;
Move (t, Buffer", sizeof (TDateTime)) ;
end ;
2: / / tarnafio
Move (FileData.Size, Buffer", sizeof (Integer));
3: / / a t t r i b u t o s
begin
strAttr : = '
if (FileData.Attr and SysUtils.faReadOnly) > 0 then
strAttr [I] : = ' R ' ;
i f (FileData.Attr and SysUtils. faSysFile) > 0 then
strAttr [2] : = ' S t ;
i f (FileData.Attr and SysUtils .faHidden) > 0 then
strAttr [3] : = 'HI;
StrCopy (Buffer, pchar (strAttr)) ;
end;
4: / / d i r e c t o r i o
begin
Booll : = FileData.Attr and SysUtils.faDirectory > 0;
Move (Booll, Buff er", sizeof (WordBool)) ;
end ;
end; // d e c a s e
Result : = True;
end;
1 .
var
Timestamp: TTimeStamp;
begin
Timestamp : = DateTimeToTimeStamp(Data);
case DataType of
ftDate: Result.Date : = TimeStamp.Date;
ftTime: Result.Time : = TimeStamp.Time;
else
Result.DateTime : = TimeStampToMSecs(TimeStamp);
end ;
end ;
TTreeNode) ;
begin
MdDirDatasetl.Close;
end;
13/9/2002 10:49:10 AN
Tru.
13/9/2002 10:49:1O M
Tru.
9/8/2002 2:OO:OO PI
64.272
9/8/2002 2:00:00 PI
645.792
False
9/8/2002 2:00:00 PH
6.646
False
3.712
?.Is@
9/8/2002 4:OO:OO
Pn
Pn
9/8/2002 4:00:00
14/9/2002 6:28:32
13
Altova
70.bpl
HTHLZ-strict.dcd
Falsa
66.560
Pals*
PH
160.256
Ialsm
9/812002 4:OO:OO
PI
4.449
hlsc
9/8/2002 4:OO:OO
PI
3.120
?aha
9/8/2002 4:OO:OO
PI
18.796
False
3/8/2002 4:OO:OO
PI
18.110
Fmlse
9/8/2002 4:OO:OO
PI
21.875
Ialse
9/8/2002 4:OO:OO
PI
11.956
False
9/8/2002 4:OO:OO
PII
4.114
9/9/2002 4:DO:dD
PR
14 -447
Ids*
Fmlr.
Figura 17.7. La salida del ejernplo DirDemo, que usa un conjunto de datos algo
inusual para rnostrar datos de directorio.
.- -- - ADVERTENCIA: Si la v e m h & Win&m que ~sewtiILa h n e problemas con los cdroles de la shell & e-b
disponibles a Dew,sq p d e
usar k versih DirDemoNoShell del ejempio, qae asa 1- v i e s d o l e s
de arcihivos de Delphi ~ornpatiblescon Windows 3.1.
// d e f i n i c i o n e s d e c a m p o s
FieldDefs-Clear;
nProps : = G e t T y p e D a t a ( f O b j C l a s s . C l a s s I n f ~ ) ~ . P r o p C o u n t ;
GetMem(PropList, nProps * SizeOf (Pointer)) ;
GetPropInfos (fObjClass.ClassInfo, PropList);
for i : = 0 to nProps - 1 do
case PropList [i] .PropTypeA .Kind of
tkInteger, tkEnumeration, tkset:
FieldDefs.Add (PropList [i].Name, ftInteger, 0);
tkChar: FieldDefs.Add (PropList [i].Name, ftFixedChar, 0) ;
tkFloat: FieldDefs.Add (PropList [i].Name, ftFloat, 0);
tkstring, tkLString:
FieldDefs.Add (PropList [i].Name, ftstring, 50);
// TODO: a r r e g l a r t a m a d o
tkwstring: FieldDefs.Add (PropList [i].Name,
ftWideString, 50) ;
/ / TODO: a r r e g l a r t a m a f i o
end ;
end ;
En 10s metodos G e t F i e l d D a t a y S e t F i e l d D a t a se usa un codigo basado en la RTTI similar para acceder a las propiedades del objeto actual cuando se
solicita una operacion de acceso a un campo del conjunto de datos. La gran
ventaja de usar propiedades para acceder a 10s datos del conjunto de datos es que
las operaciones de lectura y escritura se pueden proyectar directamente sobre
datos per0 tambien usan el metodo correspondiente. De esta manera, se pueden
escribir las reglas de negocio de la aplicacion en cuestion implementando reglas
en 10s metodos de lectura y escritura de las propiedades (un enfoque definitivamente mas orientado a objetos que enganchar codigo a objetos de cambio y validarlos).
Esta es una version ligeramente simplificada de G e t F i e l d D a t a (el otro
metodo resulta simetrico):
function TObjDataSet.GetFiledData (
Field: TField; Buffer: Pointer) : Boolean;
var
Obj: TPersistent;
TypeInfo: PTypeInfo;
IntValue: Integer;
FlValue: Double;
begin
if FList .Count = 0 then
begin
Result : = False;
exit;
end;
Obj : = fList [Integer (ActiveBuffer") ] as TPersistent;
TypeInfo : = PropList [Field.FieldNo-l]".PropTypeA;
c a s e TypeInfo.Kind of
tkInteger, tkChar, tkWChar, tkClass, tkEnumeration, tkSet:
begin
IntValue : = GetOrdProp(Obj, PropList [Field-FieldNo-
11 )
Este codigo basado en punteros tiene un aspect0 horrible, per0 si se ha aguantad0 hasta aqui el analisis de 10s detalles tecnicos del desarrollo de un conjunto de
datos personalizado, no aiiadira mucha complejidad mas a1 esquema global. Usa
algunas de las estructuras de datos definidas (y brevemente comentadas) en la
unidad TypInfo, que deberia ser la referencia a usar para cualquier pregunta
sobre el codigo anterior. A1 usar este enfoque tan ingenuo de editar directamente
10s datos del objeto, podriamos preguntarnos por lo que sucederia si el usuario
cancelara la operacion de edicion (algo de lo que suele preocuparse Delphi). Este
conjunto de datos ofrece dos enfoques alternativos, controlados por la propiedad
ChangeToClone y basados en la idea de clonar objetos mediante la copia de
sus propiedades publicadas. El procedimiento basico DoClone codigo RTTI
similar a1 que ya se ha visto para copiar todos 10s datos publicados de un objeto
en otro objeto, creando una copia efectiva (o un clon).
Este proceso de clonacion tiene lugar en ambos casos. Segun el valor de la
propiedad ChangeToClone, o las operaciones de edicion se realizan sobre el
objeto clonado (que despues vuelve a copiarse sobre el objeto real durante la
operacion de aceptacion de 10s cambios, Post) o se realizan sobre el objeto real, y
el clon se utiliza para recuperar 10s valores originales si la edicion termina con
una solicitud de cancelacion, Cancel. Este es el codigo de 10s tres metodos
involucrados :
procedure TObjDataSet.Interna1Edit;
begin
DoClone (fList [FCurrentRecord] a s TDbPers, ObjClone) ;
end ;
procedure TObjDataSet.Interna1Post;
begin
i f FChangeToClone and Assigned (Obj Clone) then
(ObjClone) then
(List [fCurrentRecord])
) ;
end :
En el metodo S e t F i e l d D a t a hay que modificar el objeto clonado o la instancia original. Para complicar las cosas algo mas, debemos tener tambien en
cuenta esta diferencia en el metodo G e t F i e l d D a t a : si se van a leer campos del
objeto actual, podriamos tener que usar su clon modificado (de otro modo, 10s
cambios del usuario sobre 10s otros campos desaparecerian).
Como muestra el listado 17.5, la clase tambien tiene una matriz Ob j e c t s que
accede a 10s datos de un mod0 orientado a objetos y un metodo A d d que es similar
a1 metodo ~ d de
d un conjunto. A1 llamar a ~ d del, codigo crea un nuevo objeto
vacio de la clase objetivo y lo aiiade a la lista interna:
f u n c t i o n TMdObjDataSet.Add:
begin
i f n o t Active then
TPersistent;
Open;
Result : = fObjClass.Create;
List .Add (Result);
end ;
21
3242.43
33
6716,54
28
John
12
3722,38
24
4747.94
23
62
597,24
14
Generacion
de informes
con Rave
Las aplicaciones de bases de datos permiten ver y editar datos, per0 normalmente 10s datos deberian imprimirse fisicamente en papel. Tecnicamente, Delphi
soporta la impresion de muchas maneras distintas, desde la salida directa de texto
a1 uso de la impresora C a n v a s , desde sofisticados informes de bases de datos a
la generacion de documentos en varios formatos (como Microsoft Word u
Openoffice de Sun). En este capitulo vamos a centrarnos en 10s informes y en
particular en el uso del motor de informes Rave, una herramienta de terceras
partes incluida en Delphi 7. Si se tiene interes en otras tecnicas de Delphi para
controlar la impresora, puede estudiarse el material relacionado con este tema en
el sitio Web del autor.
Las herramientas de generacion de informes son importantes porque pueden
llevar a cab0 procesos complejos por si mismas. El subsistema de informes puede
convertirse en una aplicacion independiente. Aunque este capitulo se centra en el
mod0 de producir un informe a partir de un conjunto de datos desde 10s programas Delphi, siempre deberia tenerse en cuenta la naturaleza autonoma de 10s
informes a1 evaluar una herramienta de este tipo.
Hay que crear 10s informes con cuidado, ya que representan una interfaz de
usuario para la aplicacion que transciende, y que a veces es mas importante que el
propio software. Es normal que haya mas gente que se fije en 10s informes impresos que usuarios que generen 10s informes mediante 10s programas. Es por esto
Presentacion de Rave
Los informes son uno de 10s principales medios de adquisicion de informacion
a partir de 10s datos que se manejan en una aplicacion. Para resolver 10s problemas vinculados con la presentacion de un informe visual de datos que resulte
claro y lleno de significado, las aplicaciones tradicionales de generacion de informes visuales han proporcionado herramientas de disposicion en bandas con la
intencion de proporcionar listas de datos con el aspect0 de tablas. Sin embargo,
hoy en dia esisten informes con necesidades mas completas que no se manejan
con facilidad mediante estas herramientas.
Rave Reports es un entorno de diseiio de informes visuales que ofrece muchas
prestaciones unicas que ayudan a simplificar, acelerar y volver mas eficaz el
proceso de generacion de informes. Rave puede manejar una gran variedad de
formatos de informes e incluye tecnologias avanzadas como las copias reflejo
para fomentar la reutilizacion del contenido de 10s informes con el fin de conseguir un mantenimiento mas sencillo y cambios mas rapidos.
Este capitulo sirve como breve presentacion de las caracteristicas de Rave. Se
puede encontrar informacion adicional sobre Rave en 10s archivos de ayuda, en la
documentacion PDF que se encuentra en el CD de Delphi, en varios proyectos de
muestra; y en el sitio Web del fabricante del software, www.nevrona.com.
--.NOTA: Una caracteristica clave de Rave (y una de las razones por las que
Borland ha escogido este product0 sobre otras soluciones) es que es una
solucion completamentemultiplatafonna que puede usarse tanto en Windows
como en Linux. No solo 10s componentes de Rave se integran tanto con la
..
JI
NOTA: S i e m p ~ - q w , squiera
e
yer el resultado dcl trabajo, habra que pulsar la tech F9 c% e l k w e Designer para tener una vista previa del infor-
Hay que tener presente que Rave permite que 10s usuarios finales puedan crear
o modificar sus propios informes. Se puede configurar Rave Designer a nivel
El panel Property
El panel Property que se encuentra a la izquierda en el Rave Designer
ayuda a personalizar el aspecto o comportamiento de 10s componentes. Este panel
tiene una funcion parecida a la del Object Inspector de Delphi: cuando se selecciona un componente en la pagina, el panel Property refleja la seleccion realizada mostrando las distintas propiedades asociadas con ese componente. Si no hay
seleccionado ningun componente, el panel esta en blanco.
A1 igual que en el IDE de Delphi, se puede modificar el valor de una propiedad
manipulando el contenido del cuadro de edicion, seleccionando una opcion de una
lista desplegable o haciendo que aparezca un cuadro de dialogo de edicion. Se
puede hacer doble clic sobre cualquier propiedad que tenga una lista de opciones
(en lugar de hacer clic sobre el boton Flecha abajo y seleccionar a continuacion
la opcion) para pasar a1 siguiente elemento de la lista.
Report Library: Contiene todos 10s informes del proyecto. Cada informe
tiene una o mas paginas. Cada una de estas paginas incluira normalmente
uno o mas componentes.
Global Page Catalog: Gestiona las plantillas de informes. Las plantillas
de informes pueden tener uno o mas componentes y reutilizarse mediante la
tecnologia de espejo de Rave. Pueden incluir elementos como cabeceras y
pies de carta, formularios preimpresos, diseiios de marcas de agua o definiciones completas de pagina que puedan servir de base a otros informes.
Se puede pensar en el Global Page Catalog como en un repositorio, una
ubicacion centralizada para almacenar 10s elementos de informes que se
quieran tener a mano para multiples informes.
Data View Dictionary: Define todas las vistas de datos y otros objetos
relacionados con datos para informes.
Fonts Toolbar: Puede usarse para cambiar 10s atributos de la fuente como
el nombre, el tamafio, el estilo y la alineacion.
Fills Toolbar: Proporciona el estilo de relleno para formas como recthngulos y circulos.
Lines Toolbar: Permite modificar el ancho del borde y 10s estilos de lineas
y borde.
Colors Toolbar: Determina 10s colores primario y secundario (normalmente el color frontal y el de fondo, respectivamente). El boton izquierdo
del raton sirve para seleccionar el color primario y el boton derecho el
secundario. Hay disponibles ocho cuadros de color personalizados para
10s colores escogidos usados mas habitualmente. Si se hace doble clic sobre el cuadro de color primario o secundario que se encuentra a la derecha
de la barra de herramientas, se abrira el dialogo Color Editor, que proporciona herramientas adicionales para la seleccion de color.
Zoom Toolbar: Proporciona muchas herramientas que permiten ampliar o
reducir la imagen del Page Designer para facilitar la edicion.
Designer Toolbar: Permite personalizar el Page Designer y el Rave
Designer mediante el dialogo de preferencias.
La barra de estado
En la parte inferior del Rave Designer se encuentra la barra de estado. Esta
barra de estado proporciona informacion sobre el estado de la conexion de la vista
de datos directa y la posicion y tamafio del raton. El color de 10s indicadores de la
conexion de datos permiten conocer el estado del sistema de datos de Rave
(DirectDataView): gris y verde indican una conexion inactiva o activa, respectivamente; amarillo y rojo indican situaciones especificas del acceso a datos (respectivamente, espera de una respuesta o fuera de plazo).
Los valores X e Y son las coordenadas del cursor del raton en las unidades de
pagina. Cuando se deja caer un componente sobre la pagina, si no se suelta el
boton del raton, se mostrara el tamafio del componente mediante 10s valores dX y
dY (con d de delta, incremento).
Ahora dispondremos de una aplicacion (el ejemplo Raveprint que se encuentra entre 10s archivos de codigo fuente) que puede imprimir un informe y que
incluye la posibilidad de ver una vista previa del resultado impreso, como muestra la figura 18.2. El archivo al que se hace referencia deberia distribuirse de
forma independiente, y puede modificarse sin necesidad de modificar el programa
Delphi. Como alternativa, tambien se puede incrustar el archivo .rav en el archivo
ejecutable de Delphi cargandolo en el archivo DFM. Para realizar esto, hay que
utilizar la propiedad S t o r e R A V del componente de proyecto Rave.
And more text And more text. And more text. And more text. And more
text And more text And more text. And more text.
And more text. And more text. And more text. And more l e x t And more
text. And more text And more text. And more text. And more text And
.- .
.----
-- -
And more text And more text. And more text. And more text. And more
text And more text. And more text. And more text. And more text. And
more text And more text. And more text. And more text. And more text
And more text And more text.
"I
- - .- -
- --
A\
camp-
ha comentado, el
todos 10s cornponentes disponibles en ~ a v Reports
e
pue.de usarst en h a
nnliracilin VCl . n rl .X
El
Para controlar 10s parametros mas importantes del informe y la vista previa,
puede conectarse un componente RvNDRWriter o RvSystem con la propiedad
E n g i n e del componente RvProject:
Componente RvNDRWriter: Genera un archivo con formato NDR cuando se ejecuta el informe. Los archivos con formato NDR son archivos con
un formato binario propietario y almacenan toda la informacion que necesitan 10s componentes de representacion para reproducir el informe en una
gran variedad de formatos.
componente RvSystem: Combina el componente RvNDRWr iter con
una interfaz de usuario estandar de representacion, vista previa e impresion. Esisten muchas propiedades para personalizar la interfaz de usuario,
y se pueden definir eventos sobrescritos para sustituir 10s dialogos estandar
con versiones particularizadas.
Formatos de representacion
El motor Rave produce un archivo o flujo NDR, generado por el
RvNDRWr iter.El motor de representacion de Rave puede convertir esta representacion interna en una amplia gama de formatos. Para permitir a un usuario
escoger entre uno de 10s formatos para el archivo final, hay que colocar 10s componentes de representacion deseados dentro de un formulario del programa Delphi.
Cuando se ejecuta el metodo Execute del componente RvProject (como en
el ejemplo Raveprint), se puede escoger uno de 10s formatos de archivo en el
cuadro de dialog0 que muestra Rave y que se puede ver en la figura 18.3.
Conexiones de datos
Los componentes de conexion de datos proporcionan un enlace entre 10s datos
contenidos en una aplicacion Delphi y las DirectDataView disponibles en el Rave
Designer. Fijese en que el valor definido en la propiedad N a m e de cada componente de conexion de datos se utiliza para proporcionar el enlace con el informe de
Rave. Es por este motivo por el que hay que tener cuidado en evitar modificar 10s
nombres de componentes despues de que se creen las DirectDataViews en Rave.
Los componentes de conexion de datos son 10s siguientes:
RvCustomConnection: Proporciona datos a 10s informes de Rave mediante eventos programados y puede usarse para enviar datos que no formen parte de una base de datos a un informe visual.
RvDataSetConnection: Conecta cualquier componente descendiente de la
clase T D a t a S e t con una DirectDataView de Rave. Mediante la propiedad F i e l d A l i a s L i s t tambien se pueden modificar 10s nombres de 10s
campos del conjunto de datos, sustituyendolos por nombres mas expresivos para desarrolladores o usuarios finales que vayan a crear el informe.
Si se necesita ordenacion o filtrado para busquedas o relaciones maestro1
detalle en 10s informes de Rave, se pueden controlar 10s eventos
OnSetSort y OnSetFilter.
RvTableConnection y RvQueryConnection: Conecta 10s componentes
BDE Table y Query con una DirectDataView de Rave. Rave proporciona
de mod0 nativo soporte de ordenacion y filtrado para conexiones de datos.
Como primer ejemplo de creacion de un informe relacionado con una base de
datos, hemos creado el ejemplo RaveSingle, que es una actualizacion del programa DbxSingle del capitulo 14, con un proyecto Rave y una conexion:
o b j e c t RvDataSetConnectionl: TRvDataSetConnection
Runtimevisibility = rtDeveloper
DataSet = SimpleDataSetl
end
o b j e c t RvProjectl: TRvProject
ProjectFile = ' R a v e S i n g l e . rav'
end
se a 10s datos que expone el programa escrito en Delphi, es necesario aiiadir una
vista de datos haciendo clic sobre el boton New Data Object que se encuentra en
la barra de herramientas Project, seleccionar la opcion Direct Data View y
escoger una conexion disponible. La lista que se presenta depende de las conexiones disponibles en el proyecto que se encuentra activo en ese momento en el IDE
de Delphi.
Ahora se puede crear un informe con la ayuda de un asistente. En el menu del
Rave Designer habra que escoger la opcion de menu Tools> Report Wizards>
Simple Table. Despues se selecciona la vista de datos (deberia existir una si se
han seguido 10s pasos), y en la siguiente ventana hay que escoger 10s campos del
conjunto de datos que se desean incluir en el informe. Deberia verse un informe
como el que muestra la figura 18.4.
Tal y como se puede ver en el Project Tree, el asistente ha generado una
pagina de informe con una zona (Region) que contiene tres componentes de banda
(Band): una para el titulo, una para la cabecera de la tabla y una banda dataaware para 10s elementos. Hablaremos de 10s detalles del uso de estos componen-
tes en la seccion sobre Region y Band que se encontrara mas adelante. Por el
momento basta con experimentar con un ejemplo funcional.
Cada proyecto puede contener varios informes, representados por el componente Report. Un componente Report contiene las paginas de un informe. Pueden
existir varios componentes Report en un unico proyecto, y cada Report puede
tener varias paginas (componentes Page). El componente Page es el componente
visual basico sobre el que se pueden colocar 10s componentes visuales del informe. Es aqui donde se completa el diseiio y disposicion de un informe.
Ademas de la lista de informes disponible bajo el nodo Report Library, un
proyecto Rave tiene un Global Page Catalog, del que ya hemos hablado, y un
Data View Dictionary. El Data View Dictionary es una lista detallada de las
conexiones de datos que ofrece la aplicacion Delphi anfitriona (mediante el uso de
10s componentes de conexion Rave de Delphi, de 10s que ya hemos hablado) o que
se activan directamente desde el informe a una base de datos.
Para diseiiar el informe hay que colocar directarnente 10s componentes visuales sobre la pagina o en otro contenedor como un componente Band (una banda) o
Region (una zona). Algunos de estos componentes no estan conectados con 10s
datos de la base de datos (por ejemplo, 10s componentes Text, Memo y Bitmap en
la barra de herramientas estandar del diseiiador). Otros componentes pueden estar
conectados con un campo en una tabla de una base de datos (o ser data-aware,
por usar el termino habitual de Delphi), como 10s componentes DataText y
DataMemo de la barra de herramientas Report.
Componentes basicos
La barra de herramientas Standard tiene siete componentes: Text, Memo,
Section, Bitmap, Metafile, FontMaster y PageNumInit. Muchos de 10s componentes estandar se suelen utilizar cuando se diseiian informes.
El componente Section
El componente Section se utiliza para agrupar componentes, como un Panel en
Delphi. Ofrece ventajas como permitir el desplazamiento de todos 10s componen-
tes que forman parte de la seccion con un solo clic, en lugar de tener que mover
cada componente de forma individual o seleccionar todos 10s componentes antes
del desplazamiento.
El Project Tree resulta util cuando se maneja el componente Section. A partir
de un nodo expandido resulta sencillo comprobar quC componentes se encuentran
en que seccion, ya que 10s componentes compuestos pueden formar un arbol con
relaciones padre-hijo.
-
Componentes graficos
Los componentes Bitmap y Metafile permiten disponer imagenes en un informe. Bitmap soporta archivos de mapas de bits con la extension .bmp, y Metafile
soporta archivos de imagenes vectoriales con las estensiones .w m f y . emf.
Cuando se utiliza Rave en una aplicacion CLX no se soportan 10s componentes
Metafile, porque estan basados en una tecnologia especifica de Windows.
El componente FontMaster
Cada componente Text de un informe tiene un propiedad F o n t . Al establecer
esta propiedad, se puede asignar una fuente especifica al componente. En muchos
casos puede ser util y necesario establecer las mismas propiedades de fuente para
mas de un objeto. Aunque se puede hacer esto si se selecciona mas de un componente a1 mismo tiempo, este metodo tiene un inconveniente: hay que hacer un
seguimiento de que fuentes deben tener el mismo tipo, tamaiio y estilo, lo que no
resulta sencillo para mas de un informe. El componente FontMaster permite definir fuentes estandar para distintas partes del informe, como las cabeceras, el
cuerpo y 10s pies de pagina. El FontMaster es un componente no visual (que se
indica mediante el color verde del boton), por lo que no se tendra ninguna referencia visual sobre el en la pagina (no como en Delphi). Al igual que otros componentes no visuales, solo puede accederse a el mediante el Project Tree.
Una vez que se haya establecido la propiedad F o n t del componente FontMaster,
es sencillo vincularlo con un conjunto de texto. Hay que seleccionar un componente Test o Memo en el informe y utilizar a continuacion el boton Flecha abajo
que se encuentra junto a la propiedad F o n t M i r r o r en el panel Property para
escoger un enlace con un FontMaster. Cualquier componente cuya propiedad
Fo n t M i r r o r se haya vinculado con el FontMaster se vera afectada por la propiedad F o n t del FontMaster.
Cuando se fija la propiedad F o n t M i r r o r de un componente, se reemplazara
la propiedad F o n t del componente por la propiedad F o n t del FontMaster. Otro
efecto secundario del uso del FontMaster es que se inhabilita la barra de herramientas de fuentes cuando se fija la propiedad FontMirror para ese componente.
Puede existir mas de un FontMaster por pagina; sin embargo, es muy aconsejable renombrar 10s componentes FontMaster para describir su funcion. Tambien
deberian colocarse en una pagina global, para que puedan utilizarse en todos 10s
informes que formen parte de un proyecto y se consiga asi una estructura tipografica mas consistente.
Nlimeros de pagina
PageNumInit es un componente no visual que permite reiniciar la numeracion
de paginas dentro de un informe. Se utiliza de un mod0 similar a otros componentes no visuales. Lo mas normal es utilizar este componente cuando Sean necesarios formatos mas avanzados .
Por ejemplo, podemos tener en cuenta un informe de estado de cliente para una
cuenta bancaria. Los balances de estado que reciban 10s clientes cada mes pueden
variar en el numero de paginas. Supongamos que la primera pagina define la
estructura de la pagina de resumen de la cuenta, la segunda define 10s creditos o
depositos del cliente, y la tercera define las deudas y las retiradas de fondos.
Puede que 10s dos primeros informes necesiten una sola pagina; per0 si la actividad de la cuenta de un cliente es muy alta, entonces la seccion de retiradas puede
ocupar varias paginas. Si el usuario que genera el informe quiere numerar individualmente las paginas de cada seccion, las paginas de resumen y de depositos
deberian marcarse como "1 de 1". Si la cuenta de un cliente activo tiene tres
paginas de retiradas y deudas, esta seccion del balance deberia numerarse como
" 1 de 3", "2 de 3" y "3 de 3 ". PageNumInit es un componente muy practico para
este tipo de numeracion de paginas.
Componentes de dibujo
A1 igual que 10s componentes estandar, 10s componentes de dibujo no tienen
que ver con 10s datos. Los informes de Rave pueden incluir tres tipos de componentes para lineas: Las lineas genericas se dibujan en cualquier direccion e inchyen lineas oblicuas; las lineas horizontales y verticales tienen una direccion fija.
Entre las formas geometricas disponibles hay cuadrados, rectangulos, circunferencias y elipses.
Se puede dejar caer una forma sobre un informe y despues esconderla tras otro
elemento. Por ejemplo, puede colocarse un rectangulo alrededor de un componente DataBand, ajustar su tamaiio para que ocupe completamente la banda y situarlo despuis tras el resto de 10s componentes de la banda.
WdObjscl fW
D d a Lookup Secwly Conlrdln
@ Database Cmnedion
Direct D d a Vlew
a Driver D d a V ~ e w
fl Simple Securny Cordroller
Regiones y bandas
Un componente Region es un contenedor de componentes Band. En su forma
mas simple, la region podria ser todo el componente Page. Esto seria cierto para
un informe que sea un tipo lista. Muchos informes maestro-detalle podrian encajar en un diseiio de una unica region. Sin embargo, no hay que limitarse a pensar
en una region como en toda la pagina; las propiedades de una region tienen que
ver con su tamaiio y posicion en la pagina. El uso creativo de las regiones ofrece
mas flexibilidad cuando se diseiian informes complejos. Se pueden colocar varias
regiones en una unica pagina; pueden estar adosados, apilados, o distribuidos por
la pagina.
TRUCO: N o hay que confundir una region (Region) con una seccion
(Section). Los componentes Region solo pueden contener componentes Band.
Un componente Section puede contener cualquier grupo de componentes,
como cimponentes ~ e ~ i oper0
n , no directameite componentes ~ & d .
Cuando se trabaja con bandas hay que seguir una regla muy sencilla: Las
bandas tienen que estar en una region. Hay que tener en cuenta que no hay limite
a1 numero de regiones en una pagina ni a1 numero de bandas en una region.
Mientras se puede ver mentalmente el informe, se puede usar una combinacion de
regiones y bandas para resolver cualquier dificultad que surja a la hora de plasmar esas ideas en forma de diseiio.
Esisten dos tipos de banda:
DataBand: Se usan para mostrar informacion repetitiva procedente de una
vista de datos. En general, un componente DataBand contendra varios componentes DataTest. La propiedad D a t a V i e w de un DataBand debe fijarse a1 componente DataView (la vista de datos) sobre la que habra que
realizar las repeticiones, y tipicamente contendra otros componentes dataaware que trabajaran con la misma D a t a V i e w .
Band: Se usa para mostrar bandas de cabecera y pie de pagina en una
region. Entre 10s tipos soportados estan Body, Group y Row: se escogen
mediante la propiedad B a n d S t y l e . No se necesitan que las cabeceras o
pies de pagina esten en una banda porque se pueden dejar caer directamente sobre la pagina, fuera del componente Region.
Una propiedad importante del componente Band es C o n t r o 1l e r B a n d . Esta
propiedad determina a que DataBand pertenece un componente Band (o por que
componente esta controlado).
Cuando se establece la DataBand de control, hay que fijarse en que el simbolo
grafico de la banda apunta en la direccion de su controlador y que 10s colores de
10s simbolos se corresponden. A continuacion explicaremos 10s codigos de letras
que se muestran en la banda.
El area de muestra del Band Style Editor esta pensada para representar el flujo
de un informe en una especie de estructura aprosimada. Los componentes DataBand
se rcpiten tres veccs para mostrar que son iterativos. La banda que se esta editando aparecc resaltada. Aunque se pueden ver otras bandas en el editor, solo se
pueden modificar 10s parametros de la actual.
El Band Style Editor utiliza simbolos y letras en el area de representacion y en
el area Page Layout (como muestra la figura 18.6) para informar del comportsmiento de cada banda. La principal diferencia entre estas dos representaciones es
que el Band Style Editor organiza las bandas con una estructura falsa segun la
definicion de cada banda. En el Band Style Editor se organizan las bandas de
acuerdo con el flujo logico y el orden en que se colocan en el informe en tiempo de
diseiio. La secuencia de las bandas en el resultado del informe tiene que ver
basicamente con este orden. Las cabeceras (letras mayusculas BGR, que significan Body. Group y Row, respectivamente) se imprimiran en primer lugar, seguidas por las bandas de datos (DataBand) y 10s pies de pagina (letras minusculas
bgr) para cada nivel. No obstante, si se define mas de una cabecera para un nivel
particular, entonces las bandas de cabecera se procesan en el orden en que se
organizan en la region. Por ello es posible colocar todas las cabeceras en la parte
superior, todas las bandas de datos en medio y todos 10s pies de pagina en la parte
inferior de una region para todos 10s niveles de un informe maestro-detalle. Ademas, se puede agrupar cada nivel con las cabeceras, pies de pagina y bandas de
datos apropiados juntos para cada nivel. Rave permite usar la estructura de la
region de tal manera que tengan el mayor sentido posible para el flujo de diseiio.
Hay que recordar que el orden de precedencia para las bandas del mismo nivel
esta controlado por su orden dentro de la region.
Dos simbolos distintos muestran las relaciones padre-hijo o maestro-detalle de
las distintas bandas:
Cornponentes data-aware
Se pueden colocar distintos componentes data-aware de Rave en un DataBand.
La opcion mas comun es el componente DataText, utilizado para mostrar un
campo de texto procedente de un conjunto de datos como se muestra en el ejemplo
Ravesingle.
Hay disponibles dos opciones para introducir datos en una propiedad
D a t a F i e l d . La primera es seleccionar un unico campo mediante la lista desplegable; este enfoque esta bien para informes normales en que solo se necesite un
campo de datos para cada elemento DataText. La segunda es utilizar el Data Text
Editor.
' + Zip
LastName
I W a Ted --
--
--
FIRST-NAME
De Text a Memo
El componente DataMemo muestra un campo de memo procedente de una
Dataview. La diferencia principal entre el componente DataMemo y el componente DataTest es que el primer0 se utiliza para mostrar texto que necesita mas de
una linea y necesita adaptar su forma. Por ejemplo, podria utilizarse para imprimir comentarios sobre un cliente en la parte inferior de cada pagina de una factura.
Un posible uso para el componente DataMemo son las funciones de fusion de
correos. El mod0 mas sencillo de realizar esto es establecer las propiedades
DataView y DataField como la fuente del campo Memo. Despues, se arranca el Mail Merge Editor haciendo clic sobre el boton de puntos suspensivos que se
encuentra junto a la propiedad Mai 1MergeI tems.Este editor permite determinar 10s elementos del Memo que se modificaran.
Para usar el Mail Merge Editor, hay que hacer clic sobre el boton Add. En la
ventana Search Token, se escribe el elemento que esta en el Memo y que sera
sustituido. Despues, se escribe la cadena de sustitucion en la ventana Replacement
o se hace clic sobre el boton Edit para arrancar el Data Text Editor que ayudara
a seleccionar las distintas vistas de datos y campos.
Calculo de totales
El componente CalcText es un componente de recuentodata-aware. La mayor
diferencia entre el componente DataText y el componente CalcText es que
este ultimo esta diseiiado especialmente para realizar calculos y mostrar 10s resultados. La propiedad CalcType determina el tipo de calculo que se va a realizar,
como promedio (Average), recuento (Count), maximo (Maximum), minimo
(Minimum) y sumatorio (Sum). Por ejemplo, se puede utilizar este componente
para imprimir 10s totales de una factura en la parte superior de cada pagina.
La propiedad CountBlanks determina si 10s valores de 10s campos vacios
se incluyen en 10s metodos de calculo de recuento y promedio. Si RunningTotal
es True,entonces no se volvera a poner a 0 el valor del calculo cada vez que se
imprima.
Rave avanzado
En esta extensa introduccion de Rave hemos visto que este sistema de generacion de informes es tan complejo que podria dedicarsele todo un libro. Ya hemos
creado algunos ejemplos, y podriamos continuar mostrando una relacion maestrodetalle u otros informes con una estructura compleja. Sin embargo, con 10s asistentes y la informacion disponible hasta ahora, deberian poderse hacer ejemplos
similares sin gran dificultad. Por esto, en esta seccion solo vamos a crear un unico
ejemplo de este tipo, y despues ofreceremos informacion sobre unos cuantos aspectos importantes de Rave que no son faciles de entender mediante el procedimiento de prueba y error.
lnformes maestro-detalle
Para crear un informe maestro-detalle en Rave, se necesitan dos conjuntos de
datos en la correspondiente aplicacion Delphi, per0 no es necesario que estos
conjuntos de datos tengan definida una relacion maestro-detalle en el programa,
ya que el propio informe puede definir una relacion de este tipo. En el programa
de muestra RaveDetails, se expone cada uno de 10s conjuntos de datos a traves de
una conexion Rave:
o b j e c t dsDepartments: TSimpleDataSet
Connection = SQLConnectionl
DataSet. CommandText = ' select * from DEPARTMENT'
end
o b j e c t dsEmployee: TSimpleDataSet
Connection = SQLConnectionl
DataSet.CommandText = 'select * from EMPLOYEE'
end
o b j e c t RvConnectionDepartments: TRvDataSetConnection
DataSet = dsDepartments
end
o b j e c t RvConnectionEmployee: TRvDataSetConnection
DataSet = dsEmployee
end
v oasv*lrRegm -W1-
Simple T a b d l
Figura 18.6. El informe maestro-detalle. El Band Style Editor aparece por delante.
ADVERTENCIA: Para crear un infoxme maestro-detalle se podria utilizar el asistente correspondiente disponible en Rave. En la version que se
incluye con Delphi 7 no funciona este asistente. Aun no hay disponible una
actualizacih para sotucionar este y otros problemas.
Guiones de informes
A1 comienzo de este capitulo hablamos de la ventana Event Editor del Rave
Designer, per0 aun no lo hemos usado. Esta herramienta se utiliza para escribir
codigo (guiones o scripts) en un informe, que responda a eventos de 10s diversos
componentes, como se haria en Delphi. La escritura de guiones en el Rave
self.FontMirror
:=
fmPlainFont;
end if;
Espejos
Las plantillas de informes pueden tener uno o mas componentes y reutilizarse
mediante la tecnologia de espejo de Rave. El componente DataMirrorSection refleja otras secciones de acuerdo con 10s contenidos de un DataField. El uso de
secciones espejo permite que la DataMirrorSection sea muy flexible. Conviene
recordar que las secciones pueden contener cualquier otro componente como graficos, regiones, texto y demas.
Por ejemplo, podria usarse un componente DataMirrorSection para que un
unico informe genere distintos formatos de sobre para direcciones internacionales
DEPARTMENT
BudgetCjalary
1 000.000.00
Corporate Headquarters
Lee, Tern
Bender, Ollver H.
212.850.00
2.000 000,OO
44.WOPO
Engineering
1.100.000.(
Nelson, Robelt
Brown, Kelly
105.900.0
age 1 o f 3
San Franc~sco
111.262.50
MacDond. Mary S
Yanowski, Mchael
Monterey
53 793.00
LOCATION
270M,C
-
Lo mas normal es definir una de las dos configuraciones como predeterminada. Si no se define una opcion predeterminada y el valor de campo no encaja con
ninguna de las configuraciones, entonces el formato usado sera el contenido normal del componente DataMirrorSection.
Calculos a tope
Ademas del sencillo componente CalcTest ya comentado, el Rave Designer
incluye tres componentes para manejar situaciones mas complejas: CalcTotal,
CalcController y CalcOp.
CalcTotal
El componente CalcTot a1 es una version no visual del componente
CalcText.Cuando se imprime este componente, lo m h habitual es guardar su
valor en un parametro de proyecto (definido por la propiedad ~estParam)
y darle
formato de acuerdo con la propiedad DisplayFormat.Puede resultar util cuando
se realicen calculos totales que se utilizaran en otros calculos antes de presentarse.
CalcController
C a 1cCont ro 11e r es un componente no visual que actua como un controlador para 10s componentes CalcText y CalcTo tal mediante sus propiedades contro 1ler. Cuando se imprime el componente controlador, indica a todos
10s componentes de calculo que controla que realicen sus operaciones. Este proceso permite que un informe vuelva a calcular 10s totales de de bandas de grupo, de
detalle o de paginas completas segun cual sea la situacion del componente
CalcController.
El componente CalcController tambien puede iniciar un componente CalcText
o CalcTotal con un valor especifico (mediante las propiedades Initcalcvar,
InitDataField e Initvalue). El componente CalcController solo
iniciara 10s valores si se usa en la propiedad Init ia1izer de 10s componentes
CalcTexto CalcTotal.
CalcOp
CalcOp es un componente no visual que permite realizar una operacion (definida por la propiedad Ope rator) sobre valores de distintas fuentes de datos. El
resultado puede guardarse despuQ de un parametro de proyecto, como CalcTotal,
tal y como indiquen las propiedades De stParam y DisplayFormat.
Por ejemplo, supongamos que necesitamos aiiadir dos componentes DataText,
como en A + B = C (donde A y B representan 10s valores de dos componentes
DataText y c representa el resultado que se almacena en un parametro de proyecto). Los tres tipos de fuentes tienen asociados muchos valores distintos.
El calculo puede comenzar con distintos tipos de fuentes de datos:
Una fuente Data Fie ld es un campo en una tabla, o un DataView en
terminos de Rave. Por ello, para escoger un campo hay que seleccionar
antes un Dataview.
Para una fuente Value,se rellenara la propiedad con un valor numerico.
Una fuente Calcvar representa otra variable de calculo; se puede escoger una del menu desplegable que muestra la lista de variables de calculo
disponibles en la pagina. Este valor puede proceder de otro componente
CalcOp o de algun otro componente de calculo.
Despues de escoger las fuentes de datos, hay que seleccionar la operacion que
se realizara con ellos. La propiedad operator tiene un menu desplegable que
puede usarse para realizar la eleccion adecuada. En el ejemplo A + B = C, el
operador es c o ~ d d .
En ocasiones se necesita realizar una funcion con un valor antes de procesarlo
junto con el segundo valor. En este caso es practica la propiedad Function de
la fuente. Con una funcion se puede convertir un valor (corno de horas a minutos),
calcular una funcion trigonometrica (corno el sen0 de un valor) o realizar muchos
otros calculos (corno una raiz cuadrada o el valor absoluto).
Es tan importante asegurarse de que 10s componente estan ordenados en el
Project T r e e para realizar 10s calculos en orden. Un informe ejecuta 10s componentes en sentido descendente en el Project Tree. Para 10s componentes CalcOp
o cualquier otro componente de calculo, esto significa que deben seguir el orden
correcto. Tambien es importante tener en cuenta que si un valor de fuente depende
de otro componente (corno de otros componentes C a l c O p o componentes
DataText), esos componentes deben encontrarse antes en el Project Tree.
Parte IV
Delphi e Internet
Programacion
para Internet:
sockets e lndy
La API WinInet.
Protocolos de correo (SMTP y POP3).
El protocolo HTTP.
Generacion de HTML.
De bases de datos a HTML.
~~
~~
- -
-~
-~
3-
. .
. .
2.
A-
- - - A - - ~ - -
A--
Puertos TCP
Cada conexion TCP se realiza a traves de un puerto, que se representa como
un numero de 16 bits. La direccion IP y el puerto TCP especifican juntos una
conexion de Internet o un socket. Distintos procesos activos en la misma maquina
no pueden utilizar el mismo socket (el mismo puerto).
Algunos puertos TCP tienen un uso estandar para protocolos y servicios de
alto nivel especificos. En otras palabras, deberian utilizarse esos numeros de
80
21
25
110
Telnet
23
Conexiones de socket
Para iniciar la comunicacion a traves de un socket, el programa servidor debe
comenzar a ejecutarse en primer lugar; per0 simplemente esperara una peticion
procedente de un cliente. El programa cliente solicita una conexion indicando el
servidor a1 que desea conectarse. Cuando el cliente envia la peticion, el servidor
puede aceptar la conexion, iniciando un socket de servidor especifico que se co-
necte a1 socket del cliente. Para soportar este modelo esisten tres tipos de conexiones de socket:
Las conexiones d e cliente: Son iniciadas por el cliente y conectan un
socket del cliente local con un socket del servidor remoto. Los sockets de
clientc deben describir el servidor a1 que se quieren conectar, proporcionando su nombre (o su direccion IP) y su puerto.
L a s conexiones d e escucha: Son sockets pasivos de servidor que espcran
una peticion de un cliente. Una vez que un clientc efectua una nueva peticion, cl servidor crea un nuevo socket dedicado a esa conesion especifica y
dcspuks vuelve a1 estado de escucha. Los sockets de escucha de scrvidor
deben indicar el pucrto quc reprcscnta el servicio quc proporcionan. (El
cliente sc conectara mediante esc puerto.)
Las conexiones d e servidor: Son activadas por 10s servidores; aceptan
una peticion procedente de un cliente.
Los distintos tipos de conesiones son solo importantcs en el cstablecimiento
dcl enlace entre el clientc y el servidor. Una vez que se ha establecido el enlace,
ambos lados pueden realizar peticiones y enviar datos a1 otro lado.
procedure
begin
Ahora que se ha establecido una conexion, se necesita hacer que 10s dos programas se comuniquen. Tanto 10s sockets de cliente como de servidor tienen metodos de lectura y escritura que pueden utilizar para enviar datos, per0 escribir un
servidor multihilo que pueda recibir muchas ordenes distintas (normalmente basadas en cadenas) y trabajar de distinto mod0 con cada una de ellas no es algo
trivial. Sin embargo, Indy simplifica el desarrollo de un servidor mediante su
arquitectura de comandos. En un servidor se puede definir un cierto numero de
comandos, que se guardan en la coleccion CommandHandlers de IdTCPSewer.
En el ejemplo IndySockl el servidor tiene tres controladores, todos ellos
implementados de distinta manera para mostrar algunas de las posibles alternativas. La primera orden de servidor, llamada test,es la mas simple, porque esta
completamente definida en sus propiedades. Hemos preparado la cadena de la
orden, un codigo numeric0 y una cadena resultante en la propiedad ReplyNorma1
del controlador de ordenes:
object IdTCPServerl: TIdTCPServer
CommandHandlers
<
i tern
end
TFormClient.btnTestClick(Sender: TObject);
IdTCPClientl.SendCmd
' t e s t ')
ShowMessage (1dTCPClientl.LastCmdResult.TextCode
1dTCPClientl.LastCmdResult.Text.Text);
end;
+ '
' +
El codigo de cliente correspondiente escribe el nombre de la orden en la conexion del socket y despues lee una unica linea de respuesta, mediante metodos
distintos a1 primero:
procedure TFormClient.btnExecuteClick(Sender:
begin
IdTCPClientl .WriteLn ( 'execute ' ) ;
ShowMessage ( IdTCPClient 1. ReadLn) ;
end ;
TObject);
del senidor, que a su vez finalizara la conexion (no se trata de una solucion muy
realista, per0 es un enfoque mas seguro y facil de implementar):
procedure TFormServer.IdTCPServerlTIdCommandHandler2Command(
ASender: TIdCommand);
var
filename: string;
fstream: TFileStream;
begin
i f Assigned (ASender Params) then
filename := HttpDecode (ASender.Params [O] ) ;
i f not FileExists (filename) then
begin
ASender .Response.Text : = 'File not found';
1bLog.Items.Add ('File not found: ' + filename);
r a i s e E1dTCPServerError.Create ('File not found: ' +
filename) ;
end
else
begin
fstream : = TFileStream.Create (filename, fmOpenRead);
try
ASender.Thread.Connection.WriteStream(fstream,
True,
True) ;
1bLog.Items.Add ('File returned: ' + filename +
' ( ' + IntToStr (fStream.Size) + ' ) ' ) ;
finally
fstream-Free;
end;
end ;
end;
La llamada a la funcion auxiliar H t t p D e c o d e sobre el p a r h e t r o es necesaria para codificar un nombre de ruta que incluya espacios como p a r h e t r o unico,
a1 contrario del programa cliente que llama a H t t p E n c o d e . Como puede verse,
el sewidor tambien registra 10s archivos devueltos y sus tamaiios, o un mensaje de
error.
El programa cliente lee el flujo y lo copia en un componente Image, para
mostrarlo directamente (vease figura 19.1):
procedure TFormClient.btnGetFileClick(Sender: TObject);
var
stream: TStream;
begin
IdTCPClientl.WriteLn('getfi1e ' + HttpEncode
(edFileName.Text) ) ;
stream : = TMemoryStream.Create;
try
IdTCPClientl .ReadStream(stream);
stream.Position : = 0;
Imagel.Picture.Bitmap.LoadFromStream
(stream);
finally
stream. Free;
end;
end ;
NOTA ~lAr
registmr & una base de da&s a tn& de un socket es
exactamente b qye re pus& hacer con Datasnap y un componente de conexi6n de sockets (& en el capitulo 16) o con sopoite SOAP (como en el
capftulo 23).
El programa cliente que hemos creado trabaja con un ClientDataSet con esta
estructura guardada en el directorio actual. (Se puede ver el codigo pertinente en
el controlador del evento oncreate.) El metodo principal en el cliente es el
controlador del evento OnClic k del boton Send All, que envia todos 10s nuevos
Synchronize (DoLog);
Data.Clear;
// crea cadenas con la estructura
"FieldName=Value "
for I : = 0 to fDataSet.Fie1dCount - 1 do
Data.Values [fDataSet.Fields[I].FieldName] : =
fDataSet .Fields [I] .Asstring;
// envia el registro
fIdTcpClient .Writeln ( 'senddata ' ) ;
fIdTcpC1ient.WriteStrings (Data, True);
// espera una respuesta
Buf : = fIdTcpC1ient.ReadLn;
fDataSet.Edit;
fDataSet .FieldByName( ' C o m p I D f ).Asstring : =
Buf ;
fDataSet .Post;
FLogMsg :=
fDataSet.FieldByName('Companyr).AsString +
logged a s ' +
Dataset. FieldByName ( 'CompID') .Asstring;
Synchronize (DoLog);
end;
Dataset.Next;
end ;
finally
fIdTcpC1ient.Disconnect;
fIdTcpClient.Free;
Data.Free;
end ;
except
/ / atrapa expeciones en caso de errores del conjunto d e
// da tos (edicidn concurrente, etc. . )
end;
end;
cds.Insert;
// d a v a l o r a 1 0 s c a m p o s c o n l a s c a d e n a s
f o r I : = 0 t o cds.FieldCount - 1 d o
cds.Fields [I] .Asstring : =
Data.Values [cds.Fields[I] .FieldName];
/ / c o m p l e t a c o n I D , remitente y f e c h a
Inc ( I D ) ;
cdsCompID.AsInteger : = ID;
cdsLoggedBy.AsString : =
ASender.Thread.Connection.Socket.Binding.Peer1P;
cdsLogged0n.AsDateTime : = Date;
c d s . Post;
// d e v u e l v e e l ID
ASender.Thread.Connection.WriteLn(cdsComp1D.AsString);
finally
Data.Free;
end;
end ;
Excepto por el hecho de que podrian perderse algunos datos, no hay ningun
problema cuando 10s campos tienen un orden distinto y si no se corresponden, ya
que 10s datos se guardan con la estructura FieldName=FieldValue. Despues de recibir todos 10s datos y enviarlos a la tabla local, el servidor envia de
vuelta el identificador de la empresa a1 cliente. Cuando se recibe esta respuesta, el
programa cliente guarda el identificador de la empresa, con lo que el registro se
marca como enviado. Si el usuario modifica este registro, no existe ningun mod0
de enviar una actualizacion a1 servidor. Para conseguir esto, podria aiiadirse un
campo modificado a la tabla de la base de datos de cliente y hacer que el servidor
comprobase si esta recibiendo un campo nuevo o un campo modificado. Con un
campo modificado, el servidor no deberia aiiadir un nuevo registro, sin0 actualizar el existente.
Como muestra la figura 19.2, el programa servidor tiene dos paginas: una con
un registro y otra como un DBGrid que muestra 10s datos actuales en la tabla de
la base de datos del servidor. El programa cliente es una entrada de datos basada
en formularios, con botones adicionales para enviar 10s datos y eliminar 10s registros ya enviados (y para 10s cuales se haya recibido un identificador).
podemos hacer muchas cosas con componentes y protocolos de correo electronico. Hemos agrupado estas posibilidades en dos areas:
Addless
l~lmanza
Stale
cuml~aunlly
Emd
plaly
rnarco@marcocanluc a n
Canla3
Marco Canlu
Figura 19.2. Los programas cliente y servidor del ejemplo de sockets de base de
datos (IndyDbSock).
Uso d e protocolos de correo electr6nico para la comunicaci6n con usuario que s61o se conectan ocasionalmente: Cuando haya que llevar datos
entre usuarios que no siempre esten conectados, se puede escribir una aplicacion en un servidor para sincronizar entre ellos, y se puede ofrecer a
nItem: Integer;
Res : Word;
begin
Res : = MessageDlg ( ' S t a r t s e n d i n g f r o m i t e m ' +
IntToStr (ListAddr.ItemIndex) + ' ( ' +
ListAddr Items [ListAddr.ItemIndex] + ') ? ' $ 1 3 +
' (No s t a r t s f r o m 0 ) ', mtconfirmation, [ d y e s , mbNo,
mbcancel] , 0) ;
if Res = mrCancel then
Exit;
if Res = mrYes then
nItem : = ListAddr.ItemIndex
else
nItem : = 0;
// c o n e c t a
Mail.Host : = eServer.Text;
Mail.UserName : = eUserName.Text;
if ePassword.Text <> " then
begin
Mail-Password : = ePassword.Text;
Mai1.AuthenticationType : = atlogin;
end;
Mail-Connect;
// e n v i a 1 0 s m e n s a j e s , u n o a u n o , p r e c e d i d o s d e u n m e n s a j e
// p e r s o n a l i z a d o
try
/ / e s t a b l e c e l a p a r t e f i j a de l a cabecera
MailMessage.From.Name : = eFrom.Text;
MailMessage-Subject : = eSubject.Text;
M a i l M e s s a g e - B o d y - S e t T e x t (reMessageText.Lines.GetText);
MailMessage.Body.Insert ( 0 , ' H e l l o ' ) ;
while nItem < ListAddr.1tems.Count do
begin
// m u e s t r a l a selection a c t u a l
Application.ProcessMessages;
ListAddr-ItemIndex : = nItem;
MailMessage.Body [O] := ' H e l l o ' + ListAddr.Items [nItem];
MailMessage.Recipients.EMai1Addresse.s : =
ListAddr. Items [nItem];
Mail. Send (MailMessage);
Inc (nItem);
end;
finally // ya e s t d
Mail.Disconnect;
end;
end:
Otro interesante ejemplo del uso del correo es informar a 10s desarrolladores
de problemas de las aplicaciones (una tecnica que podria desearse utilizar en una
aplicacion interna en lugar de en una distribuida para el publico). Se puede conseguir este efecto modificando el ejemplo ErrorLog del capitulo 2 para enviar correo cuando se produzca una excepcion (o solo una de un tip0 dado).
(Handle,
'open
',
FileName,
sw-ShowNormal)
efecto). Se puede utilizar una llamada similar para visitar un sitio Web, utilizando una cadena como 'http://www.ejemplo.com'en lugar de un nombre de archivo.
En este caso, el sistema reconoce la seccion http de la solicitud como la necesidad
de un navegador Web, y lo arrancara.
En el lado del servidor, se generan y se ofrecen las paginas HTML. De vez en
cuando, puede que baste con tener un mod0 de producir paginas estaticas, extrayendo de vez en cuando nuevos datos a partir de una base de datos para actualizar
10s archivos HTML cuando se necesite. En otros casos, se requerira generar
paginas dinamicas basadas en una peticion de un usuario.
Como punto de partida, hablaremos de HTTP para crear un servidor y un
cliente sencillos per0 completos. Despues analizaremos 10s componentes que producen HTML. En el capitulo siguiente pasaremos de este nivel basico de la tecnologia a1 estilo desarrollo RAD para la Web soportado por Delphi, comentando las
tecnologias de extension del servidor Web (CGI, ISAP y modulos de Apache) y
las arquitecturas de WebBroker y WebSnap.
' h t t p : / / w w w . g o o g l e . corn/search?as-q=
procedure TForml.BtnFindClick(Sender:
TObject);
';
var
FindThread: TFindWebThread;
begin
// c r e a r s u s p e n d i d o , f i j a r v a l o r e s i n i c i a l e s y a r r a n c a r
FindThread : = TFindWebThread.Create (True);
FindThread.Free0nTerminate : = True;
// o b t e n e r l a s primeras 100 e n t r a d a s
FindThread. strUrl : = strsearch + EditSearch.Text + ' & n ~ m = 1 0 0';
FindThread.Resume;
end ;
ADVERTENCIA: El programa WebFind funciona con el servidor del sitio Web de Google en el momento de la elaboracibn y comprobacibn de este
l!B---a --n
..-L!-~-l-~
3-1
--...
.-..
11or0. IYO oosranre, el
sonware particular
ael
sirlo pueae cammar,
con
lo
que se podria impedir que WebFind funcionara correctamente. Este programa tambikn estaba en la edicion anterior de este libro; sin embargo carecia
de la cabecera HTTP de agente usuario, y despues de un tiempo Google
modifico el software de su servidor y bloqueo las peticiones. Aaadir un
valor cualquiera como agente usuario servia para solucionar el problema.
XT-
-l--*-~-A-
-?A:-
L!-.
1-
procedure AddToList;
procedure ShowStatus;
procedure GrabHtml;
procedure HtmlToList;
procedure HttpWork (Sender: TObject; AWorkMode:
TWorkMode;
const AWorkCount: Integer);
public
strUrl: string;
strRead: string;
end;
implementation
uses
WebFindF;
procedure TFindWebThread.AddToList;
begin
if Forml. ListBoxl. Items.Indexof (Addr) < 0 then
begin
Forml.ListBoxl.1tems.Add
(Addr);
Forml.DetailsList.Add (Text);
end;
end;
procedure TFindWebThread-Execute;
begin
GrabHtml;
HtmlToList;
Status : = 'Done with ' + StrUrl;
Synchronize (ShowStatus);
end ;
procedure TFindWebThread-GrabHtml;
var
Httpl: TIdHTTP;
begin
Status := 'Sending query: ' + StrUrl;
Synchronize (ShowStatus);
Httpl : = TIdHTTP-Create (nil);
try
Http1.Request.UserAgent : = 'User-Agent: NULL';
Httpl.OnWork : = HttpWork;
strRead : = Httpl .Get (StrUrl);
finally
Httpl.Free;
end;
end;
procedure TFindWebThread.Htm1ToList;
var
strAddr, strText: string;
nText : integer;
nBegin, nEnd: Integer;
begin
Status := ' E x t r a c t i n g d a t a f o r : ' + StrUrl;
Synchronize (ShowStatus);
strRead : = Lowercase (strRead);
repeat
// e n c u e n t r a l a p a r t e i n i c i a l d e l a r e f e r e n c i a HTTP
nBegin : = Pos ( ' h r e f = h t t p r , strRead) ;
if nBegin <> 0 then
begin
// o b t i e n e l a p a r t e r e s t a n t e d e l a c a d e n a , d e s d e h t t p
strRead : = Copy (strRead, nBegin + 5, 1000000);
/ / e n c u e n t r a e l f i n a l d e l a r e f e r e n c i a HTTP
nEnd : = Pos ( ' > I , strRead) ;
strAddr : = Copy (strRead, 1, nEnd - 1);
// c o n t i n u a
strRead : = Copy (strRead, nEnd + 1, 1000000) ;
/ / a f i a d e e l URL s i n o s e e n c u e n t r a g o o g l e
if Pos ( ' g o o g l e ' , strAddr) = 0 then
begin
nText : = Pos ( ' < / a > ', strRead) ;
strText : = copy (strRead, 1, nText - 1);
// e l e m i n a l a s r e f e r e n c i a s y d u p l i c a d o s d e l a c a c h e
if (Pos ( ' c a c h e d ' , strText) = 0) then
begin
Addr : = strAddr;
Text : = strText;
AddToList;
end;
end;
end;
until nBegin = 0;
end;
procedure TFindWebThread.HttpWork(Sender: TObject; AWorkMode:
TWorkMode;
const AWorkCount: Integer);
begin
Status : = ' R e c e i v e d ' + IntToStr (AWorkCount) + ' f o r ' +
strUrl;
Synchronize (ShowStatus);
end;
procedure TFindWebThread.AddToList;
begin
Forml.StatusBarl.SimpleText : = Status;
end;
end.
El programa busca apariciones consecutivas de la cadena href=http, copiando el testo que sigue hasta el caracter de cierre >. Si la cadena encontrada
contienc la palabra google o el testo incluye la palabra cached,se ignora. La
figura 19.4 muestra el efecto de este codigo. Se pueden iniciar varias busquedas
a1 mismo tiempo, per0 hay que tener en cuenta que 10s resultados se aiiadiran a1
mismo componente de memo
Borland
=I
Figura 19.4. La aplicacion WebFind puede utilizarse para buscar una lista de sitios
en el motor de busqueda de Google.
La API Winlnet
Cuando se necesita utilizar 10s protocolos FTP y HTTP, como alternativas a1
uso de componentes particulares de la VCl, se puede usar una API especifica de
Microsoft, que se ofrece en la DLL WinInet. Esta biblioteca forma parte del
nucleo del sistema operativo e implementa 10s protocolos FTP y HTTP sobre la
API de sockets de Windows.
Con solo tres llamadas (Internetopen, InternetOperURL e
InternetRead Fi le) se puede conseguir un archivo que se corresponda con
cualquier URL y guardarlo como una copia local o analizarlo. Se pueden utilizar
otros metodos simples para FTP; es aconsejable analizar el codigo fuente de la
unidad Delphi win1 net .pas,que ofrece una lista de todas las funciones.
Un navegador propio
Aunque no es probable que interese escribir un nuevo navegador Web, podria
resultar interesante comprobar como se puede conseguir un archivo HTML de
Internet y mostrarlo localmente, empleando el visor HTML disponible en CLX (el
control TextBrowser). Si se conecta este control a un cliente HTTP de Indy, se
puede conseguir rapidamente un navegador Web de texto con unas capacidades
de navegacion limitadas. La base es:
TextBrowserl.Text
:=
1dHttpl.Get
(NewUrl);
que hace el seguimiento de las ultimas peticiones. El efecto de una llamada de este
tipo es devolver la parte de texto de una pagina Web (vease la figura 1 9 . 9 , ya que
recuperar el contenido grafico requiere una codificacion mucho mas compleja. El
control TextBrowser se define realmente mejor como un visor de archivos locales
que como navegador.
h~tp//www m.3rcocantucorn
II
Books
GO
1~~4.1
D c v ~ m t
&
h M L
!5-6June 2002
11
I. . . :.....
l-t
8lh. 2
m Ths sde has been moved to a Liproblems here and there w t h case inc~sislencies.If you
w c l c a r e or lowncasc the lir* IeUer, a email me with the problem..
..
.T,.
--
...
.,.
Goto:
GoToUrl (NewRequest )
e l s e if NewRequest [I] = ' / ' t h e n
GoToUrl ( ' h t t p : / / I + Uri .Host + NewRequest)
else
GoToUrl ( ' h t t p : / / ' + Uri.Host + Uri.Path + NewRequest);
end;
end :
Una vez mas, este ejemplo es trivial y poco funcional, per0 construir un
navegador implica poco mas que la capacidad de conectarse mediante HTTP y
mostrar archivos HTML.
' < p > R e q u e s t : ' + Request Inf o .Document + ' < / p > ' +
' < p > H o s t : ' + RequestInfo.Host + ' < / p > ' +
'<p>Pararns: ' + RequestInfo .UnparsedParams + ' < / p > ' +
' < p > T h e h e a d e r s o f t h e r e q u e s t f o l l o w : <br>' +
Request1nfo.RawHeaders.Text + ' < / p > ' ;
ResponseInfo.ContentText : = HtmlResult;
end;
'
['d
I
I HttpSen-Demo
Recargar
http ,,localhost
8080/1e.
Inca
Request /test
Si este ejemplo parece demasiado trivial, se podra ver una version algo mas
interesante en la prosima seccion, en la que hablaremos de la generacion de codigo HTML con 10s componentes productores de Delphi.
..
NOTA: Si se tiene planeado crear un servidor Web avanzado u otros servidores de Internet con Delphi, entonces, como alternativa a 10s componentes
Indy, deberian consultarse 10s componentes DXSock de Brain Patchwork
DX (www.dxsock.com).
Generacion de HTML
El lenguaje de marcas de hipertexto, HTML, es el formato mas extendido para
distribuir contenido en la Web. HTML es el formato que tipicamente leen 10s
navegadores Web; es un estandar definido por el W3C (World Wide Web
Consortium, www.w3.org), que es uno de 10s organismos que controlan Internet.
El documento del HTML estandar esta disponible en www.w3 .org/markUp. junto
con algunos enlaces bastante interesantes.
piada, para que pueda verse el aspect0 que tendra la salida HTML en un
navegador, directamente en tiempo de diseiio.
Los componentes QueryTableProducer y SQLQueryTableProducer:
Son parccidos a1 DataSetTableProducer, pero estan especificamente adaptados para construir consultas con parametros (para BDE o dbEspress,
respectivamente) basadas en la entrada de un formulario HTML de busqueda. Este componente tiene poco sentido en un programa independiente,
y por ello no lo trataremos hasta el proximo capitulo.
~ ~ A U A I I W ~ I V J
AD
UW
1-n
1-3
nt;n**atmn
t-nmn an
b b ~ y u \ r b a a , WAUV
C~AI
A a. r e = rr 9 1 11
uaya-
L L
, y a yuu
-7-
n n a n nn + r a t - An1
.YU b s c z u z uw
El boton Demo Page copia la salida del componente PageProducer en la propiedad Text de un componente de memo. Cuando se llama a la funci6n Content
del componente PageProducer, lee el codigo HTML de entrada, lo procesa y lanza
el controlador del evento OnTag para cada etiqueta especial. En el controlador
para este evento, el programa comprueba el valor de la etiqueta (que se pasa en el
thlmb
t head,
<l~lle>P~oducer
DunocJt~llel
</head>
<body,
thl>Producer Democ/hl>
<p>Thisis a demo d the page producedby the
tb>HtmlP~odexe</b> apphcation on <b>12/4/2002t/b>.</p>
<hr,
tp>The prices in lhis catalog are valrd unld
tb>12/25RWZclb>.<lp>
</body>
*
-
Figura 19.7. El resultado del ejemplo HtmlProd, una sencilla demostracion del
componente PageProducer, cuando el usuario hace clic sobre el boton Demo Page.
procedure TFormProd.PageProducerlHTMLTag(Sender: TObject;
Tag: TTag; c o n s t TagString: String; TagParams: TStrings;
var ReplaceText : String) ;
var
nDays: Integer;
begin
if TagString = ' d a t e ' then
ReplaceText : = DateToStr ( N o w )
else i f TagString = ' a p p n a m e ' then
ReplaceText : = ~ x t r a c t ~ i l e n a m e
(Forms.App1ication.Exename)
e l s e i f TagString = ' e x p i r a t i o n ' then
begin
nDays : = StrToIntDef (TagParams .Values [ ' d a y s ' 1 , 0 ) ;
i f nDays <> 0 then
ReplaceText : = DateToStr ( N o w + nDays)
else
ReplaceText := '<i> [ e x p i r a t i o n t a g e r r o r ] < / i > ';
end;
end ;
texto del parametro de la etiqueta (en este caso, days=2 1)en una cadena que
forma parte de la lista TagParams. Para extraer la parte del valor de esta
cadena (la partc que se encuentra tras el signo de igual), se puede usar la propiedad values de la lista de cadena TagParams y buscar la entrada apropiada a1
mismo tiempo. Si no se pucdc encontrar el parametro o si el valor del parametro
no cs un entero. cl programa mostrara un mensa.jc de error
-
A1 usar etiquetas con 10s nombres de 10s campos del conjunto de datos conectado (la tipica tabla de la base de datos COUNTRY.DB), el programa obtendra
automaticamente el wlor de 10s campos del registro actual y 10s sustituira instantaneamente. Esto produce la salida quc muestra la figura 19.8; el navegador esta
concctado a1 ejemplo HtmlProd que funciona como un servidor HTTP, como
veremos mas adelante. En el codigo fuente del programa relacionado con este
componente, no esiste ninguna referencia a 10s datos de la base de datos:
p r o c e d u r e TFormProd.DataSetPageProducerlHTMLTag(Sender:
TObject; Tag: TTag;
c o n s t TagString: String; TagParams : TStrings; v a r
ReplaceText: String);
begin
i f TagString = 'program' then
ReplaceText : = ExtractFilename (Forms.Application.Exename)
Figura 19.8. El resultado del ejemplo HtmlProd para el boton Print Line.
., . .
..
I 1
Para que la salida de este componente productor sea mas completa, se pueden
realizar dos operaciones. La primera es proporcionar alguna information Header
y Footer (para generar 10s elementos HTML de apertura y cierre) y aiiadir un
Caption a la tabla HTML. La segunda es personalizar la tabla mediante 10s
valores especificados por las propiedades Ro w A t t r ibu t e s , Tab 1e Attributes y Columns.El editor de propiedades para las columnas, que es
tambien el editor de componentes predeterminado, permite establecer la mayoria
de estas propiedades, ofreciendo a1 mismo tiempo una vista previa del resultado,
como se puede ver en la figura 19.9. Antes de utilizar este editor, se pueden
preparar las propiedades para 10s campos del conjunto de datos mediante el editor
Fields. Asi, por ejemplo, se puede dar formato a la salida de 10s campos de
poblacion y area para que utilicen separadores de miles.
DataSetTableProducer Demo
II
American Clountries
El resto del codigo esta resumido en 10s parametros del componente productor
de la tabla, incluidos su cabecera y pie, como puede verse si se abre el codigo
fuente del ejemplo HtmlProd.
verse algun documento de referencia sobre HTML para ver 10s detalles. Se puede
actualizar la generacion de una tabla en el ejemplo HtmlProd para incluir hojas de
estilo proporcionando un enlace con la hoja de estilo en la propiedad H e a d e r de
un segundo componente DataSetTableProducer:
Por ultimo, si el programa devuelve una tabla que utiliza CSS, el navegador
solicitara el archivo CSS a1 servidor; por eso hemos afiadido algo de codigo
especifico para devolverlo. Con las apropiadas generalizaciones, este codigo muestra como puede responder un servidor devolviendo archivos, y tambien como
indicar el tipo MIME de la respuesta ( C o n t e n t T y p e ) :
if Pos ( ' t e s t . c s s f , Req) > 0 then
begin
CssTest : = TStringList.Create;
try
CssTest.LoadFromFile(ExtractFi~ePath(Applicat~on.ExeName)
+ 'test.cssl);
Response1nfo.ContentText : = CssTest.Text;
ResponseInfo.ContentType : = 'text/css';
finally
CssTest.Free;
end;
Exit;
end ;
Programacion
Web con
WebBroker y
Internet ticne cada vcz un papel mas importantc cn el mundo, y gran parte de el
depende dcl Cxito de la World Wide Web, la telaraiia mundial basada en el protocolo HTTP. En el capitulo anterior hemos comentado cl protocolo HTTP y el
desarrollo de aplicaciones de cliente y de servidor basadas en el. Con la disponibilidad de varios servidores Web de alto rendimiento, escalables y flexibles, sera
algo muy raro que se desee crear uno propio. Las aplicaciones Web dinarnicas se
construyen en general integrando guiones o programas compilados con servidores
Wcb, en lugar de sustituirlos por un software personalizado.
Este capitulo se centra completamente en el desarrollo de aplicaciones de servidor, que amplien 10s servidores Web ya esistentes. Antes hemos comentado la
generacion dinamica de paginas HTML. Ahora aprenderemos a integrar csta generacion dinimica con un servidor. Este capitulo supone la continuacion Iogica
del anterior, pero con el no se completa la esplicacion acerca de la programacion
orientada a Internet. El proximo capitulo se dedicara a la tecnologia IntraWeb
disponible con Delphi 7, y el 22 vuelve a tratar la programacion para Internet
desde el punto de vista de XML.
-- -
..
---.
----
--
--
Un resumen de CGI
CGI es un protocolo estandar de comunicaciones entre el navegador cliente y
el servidor Web. No se trata de un protocolo particularmente eficaz, per0 se usa
mucho y no es especifico de ninguna plataforma. Este protocolo permite a1
navegador tanto solicitar como enviar datos y se basa en la entradalsalida de linea
de comandos estandar de una aplicacion (normalmente una aplicacion de consola). Cuando el servidor detecta la solicitud de una pagina para la aplicacion CGI,
pone la aplicacion en marcha, pasa 10s datos de linea de comandos desde la solicitud de pagina a la aplicacion y despues envia la salida estandar de la aplicacion
a1 ordenador cliente.
Se pueden usar muchas herramientas y lenguajes para escribir aplicaciones
CGI y, Delphi es solo una de ellas. A pesar de la limitacion obvia de que el
servidor Web debe ser un sistema Windows o Linux basado en Intel, se pueden
crear programas CGI bastante complicados en Delphi y Kylix. CGI es una tecnica de bajo nivel, ya que utiliza la entrada y salida estandar de la linea de comandos junto con variables de entorno para recibir information desde el servidor Web
y enviarla de vuelta.
Para crear un programa CGI sin utilizar clases de soporte, podemos generar
simplemente una aplicacion de consola de Delphi, eliminar el codigo fuente tipico
del proyecto y reemplazarlo por las instrucciones siguientes:
program CgiDate;
{ S A P P T Y P E CONSOLE}
u s e s SysUtils;
begin
writeln
writeln;
writeln
writeln
writeln
writeln
writeln
writeln
writeln
( ' c o n t e n t - t y p e : t e x t / h t m l l ):
('<html><head>' ) ;
( ' <title>Time a t t h i s s i t e < / t i t l e > ' );
( ' < / h e a d > < b o d y > ' );
('<hl>Time a t t h i s s i t e < / h l > ' ) ;
( ' <hr>' ) ;
( ' <h3>' ) ;
(FormatDateTime ( ' " T o d a y i s
d d d d , mmmm d , y y y y , '
"'<br> a n d t h e t i m e i s r 1 hh:mrn:ss A M / P M 1 , Now) ) ;
writeln ( ' < / h 3 > ' );
writeln
writeln
writeln
end.
<hr>');
('<i>Page generated by CgiDate.exe</i>');
('</body></html>');
( I
0--, -t4)
Rhm
R y p a C
Ad
@ IU
r
Mtp {lladhost/cg~-bnlcgsddeexe
lmo
--
yi
---
---
. -
IS
PathName, sizeof
dentro del proceso principal, en lugar de lanzar un nuevo ejecutable para cada
solicitud (corno sucede en las aplicaciones CGI).
Cuando el servidor recibe una solicitud de pagina, carga la DLL (si no lo ha
hecho ya) y ejecuta el codigo adecuado, que puede poner en marcha un nuevo hilo
o thread, o utilizar uno existente para procesar la solicitud. La biblioteca envia
entonces 10s datos HTTP correspondientes a1 cliente que ha solicitado la pagina.
Dado que esta comunicacion se suele producir en memoria, este tipo de aplicacion
es mucho mas rapida que el enfoque CGI.
Este nombre de ruta es una parte de la URL de la aplicacion CGI o ISAPI, que
viene detras del nombre del programa y antes de 10s parametros, como path1 en
la siguiente URL:
Al proporcionar acciones diferentes, la aplicacion puede responder con facilidad a solicitudes con diferentes nombres de ruta y podemos asignar un componente productor distinto o llamar un controlador del evento OnAction diferente
para cada nombre de ruta posible. Desde luego, podemos omitir el nombre de la
ruta para manejar una solicitud generica.
Tambien hay que considerar que en vez de basar la aplicacion en un WebModule,
podemos utilizar un simple modulo de datos y aiiadirle un componente
WebDispatcher. Es un buen metodo para convertir una aplicacion Delphi ya existente en una extension de servidor Web.
.- - - . - - - e bhsica WebDispatcher
ate. Los programas de
w. CUDIUKCI
uu
uucucu
CCUCI
V~LI
,US
U
C
S
U
~
L
U
~
L
U
U
I
C
S
o- varios
- .
.
.
.. .
.. . . .. - .- .
..
.. -. .- mbdulos
. .. - - Web.
. . .
Tambitn hay que tener en cuenta que todas las acciones del WebDispatcher
no tienen nada que ver con las acciones almacenadas en un componente
.
~ * lI
..
-. - I S - -
- -
- -
-7-
-- -
- -
--
---
-.
.
Cuando definimos las paginas HTML adjuntas que ponen en marcha la aplicacion, 10s vinculos haran solicitudes de paginas a las URL para cada una de esas
rutas. Tener una sola biblioteca que pueda llevar a cab0 diferentes operaciones en
funcion de un parametro (en este caso el nombre de la ruta), permite que el servidor pueda guardar una copia de esta biblioteca en memoria y responder mucho
mas rapidamente a las solicitudes del usuario. En parte, sucede lo mismo para una
aplicacion CGI: el servidor tiene que ejecutar varias instancias per0 puede guardar en cache el archivo y hacerlo que este disponible mas rapidamente.
En el evento OnAction es donde escribimos el codigo para especificar la
respuesta a una consulta dada, 10s dos parametros principales pasados al controlador del evento. Veamos un ejemplo:
procedure TWebModulel.WebModulelWebActionItemlAction(Sender:
TObj ect;
Request: TWebRequest; Response: TWebResponse; var Handled:
Boolean) ;
begin
Response.Content
:=
' <h tml><head><title>Hello Page</ti tle></head><body>' +
' <hl>Hello</hl>' +
' <hr><p><i>Page genera ted by Marco</i></p></body></html>' ;
end;
'
C 1o s edGr a ce fu 11y .
El Web App Debugger utiliza esta inforrnacion para conseguir una lista de 10s
programas disponibles. Hace esto cuando se utiliza el URL predeterminado para
el depurador, indicado en el formulario como un enlace, tal y como muestra, por
ejemplo, la figura 20.2. La lista incluye todos 10s servidores registrados, no solo
aquellos en ejecucion, y puede usarse para activar un programa. Sin embargo, no
se trata de una buena idea porque hay que ejecutar el programa dentro del IDE de
Delphi para poder depurarlo. (Fijese en que puede expandirse la lista a1 hacer clic
sobre View Details; esta vista incluye una lista de 10s archivos ejecutables y
muchos otros detalles.) El modulo de datos para este tipo de proyecto incluye
codigo de inicializacion:
uses W e b R e q ;
initialization
: = TWebModule2;
Registered Servers
View Llst 1 V ~ e wD e t d s
G
J
o
sewer~nfoSewerlnfo
WSnapl WSnapl
WSnap2 WSnap2
WSnapMD WSnapMD
WSnapSess~onWSnapSession
WSnapTable.WSnapTable
Figura 20.2. Se muestra una lista de aplicaciones registradas con el Web App
Debugger cuando se conecta con su pagina principal.
El Web App Debugger deberia utilizarse solo para depuracion. Para desplegar
la aplicacion, deberia utilizarse alguna de las otras opciones. Pueden crearse 10s
archivos de proyecto para otro tip0 de programa servidor Web y aiiadir al proyecto el mismo modulo Web que a la aplicacion de depuracion.
El proceso inverso es ligeramente mas complejo. Para depurar una aplicacion
ya existente hay que crear un programa de este tipo, eliminar el modulo Web,
aiiadir el ya existente y parchearlo aiiadiendole una linea para establecer la variable W e b M o d u l e C l a s s del W e b R e q u e s t H a n d l e r , como en el fragment0 de
codigo anterior.
do de una aplicacion como CGI, se puede comenzar a experimentar inmediatamente con una arquitectura multihilo, sin tener que enfrentarse con la carga y
descarga de bibliotecas (que suele implicar el apagado del servidor Web y posiblemente incluso del ordenador).
Otra accion del ejemplo Bro kDemo . proporciona a 10s usuarios una lista de
10s parametros del sistema relacionados con la solicitud, algo que es bastante util
para la depuracion. Tambien es instructivo aprender cuhnta informacion (y exactamente que informacion) transfiere el protocolo HTTP desde un navegador a un
servidor Web y viceversa. Para generar esta lista, el programa busca el valor de
cada propiedad de la clase TWebRequest, como muestra este fragment0 de
codigo:
procedure TWebModulel.StatusAction(Sender: TObject; Request:
TWebRequest ;
Response: TWebResponse; var Handled: Boolean) ;
var
I: Integer;
begin
La figura 20.3 muestra el resultado de esta funcion. Cuando el usuario selecciona uno de 10s vinculos, se llama de nuevo a1 programa y puede comprobar la
lista de cadena Q u e r y F i e l d y estraer 10s parametros desde la URL. Es entonces cuando utiliza 10s valores correspondientes a 10s campos de la tabla utilizados
para la busqueda del registro (basada en la llamada a F i n d N e a r e s t ) .
[I
Last Name
First Name
Phone Ext.
Hire Date
Salary
Nelson
Robert
250
12/28/1988
105900
young
Bruce
233
12/28/1988
97500
Lambcrt
Kun
22
2/6/1989
102750
2-
Johnson
Leshe
410
41511989
64635
Forest
Phil
229
4/17/1989
j i 0 W D '
--
75060
=='
f
Figura 20.3. La sahda correspondiente a la ruta table del ejemplo BrokDemo, que
genera una tabla HTML con h~pervinculosinternos.
procedure TWebModulel.RecordAction(Sender: TObject; Request:
TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
dataEmployee.0pen;
// va a 1 registro solicitado
dataEmployee .Locate ( ' L A S T N A M E ; F I R S T N A M E r,
VarArrayOf([Request.QueryFields.Values['LastName'],
Request .QueryFields .Values [ 'FirstNdrnel] ] ) , [ ] ) ;
/ / obtiene l a salida
Response.Content : = Response.Content + DataSetPage.Content;
end;
Consultas y formularios
El ejemplo anterior utilizaba algunos componentes productores de HTML presentados con anterioridad, per0 hay otro componente de este grupo que no hemos
utilizado aun: el QueryTableProducer (para BDE) y su hermano SQLQueryTableProducer (para dbEspress). Como veremos, este componente hace que la creacion
de programas complejos de bases de datos sea algo muy sencillo. Supongamos
que queremos buscar algunos clientes en una base de datos. Para ello, podriamos
crear el siguiente formulario HTML (incrustado en una tabla HTML para que
tenga un formato mejor):
<hl>Customer QueryProducer Search Form</h4>
<form action="/scripts/CustQueP.dll/search"
method="POSTW>
<table>
<trXtd>State:</td>
<td><input type="textn n a m e = " S t a t e W X / t d X / t r >
<trXtd>Country:</td>
< t d X i n p u t type="textn n a m e = " C o u n t r y " > < / t d X / t r >
< t r X t d X / td>
< t d X c e n t e r X i n p u t type="Submit"></center></td></tr>
</table></f o m >
3.
. . ! - l . ..tl.
.I.
State-Province
State-Province,
Country
<trXtd>Country:</td>
<tdXselect name="Country"><option> </option><#Country></
selectX/tdX/tr>
<trXtdX/td>
<tdXcenterXinput t y p e = " S u b m i t " X / c e n t e r X / t d X / t r >
</ tableX/f o m >
Observara que las etiquetas tienen el mismo nombre que algunos campos de la
tabla. Cuando el PageProducer se encuentra con una de estas etiquetas, aiiade una
etiqneta HTML < o p t ion> para cada valor del campo correspondiente. Veamos
el codigo del controlador del evento OnTag, que es bastante generic0 y reutilizable:
procedure TWebModulel.PageProducerlHTMLTag(Sender: TObject;
Tag: TTag;
const TagString: String; Tagparams: TStrings; var
ReplaceText : String) ;
begin
ReplaceText : = " ;
SQLQuery2.SQL.Clear;
SQLQuery2.SQL.Add ('select distinct ' + TagString + ' from
customer') ;
try
Query2.0pen;
try
SQLQuery2.First;
while not Query2.EOF do
begin
ReplaceText : = ReplaceText +
' <option>' + Query2. Fields [ O ] .Asstring + ' </
option>'#13;
SQLQuery2.Next;
end;
finally
SQLQuery2.Close;
end;
except
ReplaceText
end;
end;
:=
I ) ' ;
State
Canada
England
FijI
France
Hang Kong
Italy
Japan
Netherlands
Switzerland
USA
Esta extension del servidor Web, como muchas otras que hemos creado, permite a1 usuario ver 10s detalles de un registro especifico. Igual que en el ejemplo
anterior, esto se puede llevar cab0 personalizando la salida de la primera columna
(la columna cero), que es generada por el componente QueryTableProducer:
procedure TWebModulel.QueryTableProducerlFormatCell(
Sender: TObject; CellRow, CellColumn: Integer:
var BgColor: THTMLBgColor; var Align: THTMLAlign;
var VAlign: THTMLVAlign; var CustornAttrs, CellData: String);
begin
i f (CellColumn = 0 ) and (CellRow <> 0 ) then
CellData : = ' < a href="' + Request.ScriptName + ' /
record?Company=' + CellData +
r 11, I
+ CellData + ' < / a > ' # 1 3 ;
i f CellData = " then
CellData : = ' & n b s p ; ' ;
end;
TRUCO: Cuando hay una celda vacia en una tabla HTML,la mayoria de
10s navegadores la representan sin borde. Es por esto que hemos aaadido un
caracter de espacio ( ) en cada celda vacia. Es necesario hacer esto
en cada tabla HTML generada por 10s productores de tablas de Delphi.
La accion para este vinculo es / r e c o r d y hay que pasar un elemento especifico despues del parametro ? (sin el nombre del parametro; que no es estandar).
El codigo que utilizamos para producir las tablas HTML para 10s registros no
utiliza componentes productores como hemos venido haciendo hasta ahora, sino
que muestra 10s datos de cada campo en una tabla personalizada:
p r o c e d u r e TWebModulel.RecordAction(Sender: TObject; Request:
TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
I: Integer;
begin
i f Request. QueryFields .Count = 0 then
Response.Content : = ' R e c o r d not found'
else
begin
Query2.SQL.Clear;
Query2.SQL.Add ( ' s e l e c t * f r o m customer ' +
' w h e r e Company="' +
Request .QueryFields .values [ ' C o m p a n y ' ] + ' "' ) :
Query2.0pen;
Response. Content : =
'<htn~l><head><title>CustomeR
r ecord</title></
h e a d > < b o d y > ' # 13 +
' <hl>Customer Record: ' + Request .QueryFields [O] + ' </
hl>'#13 +
' <table border,' #l3;
f o r I : = 1 t o Query2.FieldCount - 1 d o
Response.Content : = Response-Content +
' < t r > < t d > ' + Query2. Fields [I].FieldName + ' </
td>'#13'<td>' +
Query2.Fields [I].AsString + ' < / t d > < / t r > ' # 1 3 ;
Response .Content := Response .Content + ' </table><hr>'#13 +
/ / e n l a c e a 1 formulario d e consulta
' <a href=lV' + Request. ScriptName + ' / f o r m n > ' +
' Next Query < / a > '# I 3 + ' < / b o d y > < / h t m l > '# l 3 ;
end;
end:
aplicaciones en casi cualquier servidor Web. Sin embargo, esta opcion significa
una reduccion en la velocidad y algunos problemas a la hora de manejar informacion de estado (ya que no podemos guardar ningun dato en memoria). Esta es una
buena razon para escribir una aplicacion ISAPI o un modulo dinamico de Apache. Utilizando la tecnologia WebBroker de Delphi, podemos compilar facilmente
el mismo codigo para ambas aplicaciones de forma que sea mucho mas sencillo
Ilevar el programa a una plataforma Web diferente. Tambien podemos recompilar
un programa CGI o un modulo dinamico de Apache con Kylix y utilizarlo en un
servidor Linus.
Como ya hemos comentado, Apache ejecuta aplicaciones CGI tradicionales y
tambien utiliza una tecnologia cspecifica para guardar el programa de estension
del servidor cargado sicmpre en memoria para conseguir una respuesta mas rapida. Para crear este programa en Delphi, simplemente hay que utilizar la opcion
Apache Shared Module del cuadro de dialogo New Server Application: hay
quc escoger entre Apache 1 o Apache 2, segun la version del servidor que vaya a
utilizarse.
.. .-.
,.
..-a
a
,
.
Si se decide crear un modulo de Apache, se obtendra una biblioteca que contiene el siguiente tip0 de codigo fuente para este proyecto:
library Apachel;
uses
WebBroker,
ApacheApp,
ApacheWm i n 'ApacheWm.pas' (WebModulel: TWebModule);
exports
apache-module
name
'apachel-module';
begin
Application.Initialize;
Application.CreateForm(TWebModulel, WebModulel);
Application.Run;
end.
Preste particular atencion a la clausula exports, que indica el nombre utilizado por 10s archivos de configuracion de Apache para hacer referencia a1 modulo dinamico. En el codigo fuente del proyecto, podemos aiiadir dos definiciones
mas: el nombre del modulo y el tip0 de contenido, de la siguiente manera:
ModuleName : =
ContentType: =
' Apachel-module'
'Apachel-handler1;
apachel-module
modules/apachel.dll
Ejemplos practicos
Despues de esta presentacion general del desarrollo de aplicaciones de semidor con WebBroker, completaremos esta parte del capitulo con dos ejemplos
practicos. El primer0 es un clasico contador Web. El segundo es una ampliacion
del programa WebFind del capitulo 19, que genera una pagina dinamica en lugar
de rellenar un cuadro de lista.
--
- -- - --
- - ---
-- -
ADVERTENCIA: Este manejo simple de archivo no escala. Cuando vanos visitantes accedan a la pirgina a la vez, el c6digo puede devolver resultados falsos o fallar con un error de entraddsalida a1 archivo debido a qne
'
Bitmap. Free;
end;
Las tres sentencias responsables de devolver la imagen JPEG son las dos que
fijan las propiedades C o n t e n t s t r e a m y C o n t e n t T y p e de R e s p o n s e y la
llamada final a S e n d R e s p o n s e . El tipo de contenido debe corresponderse con
uno de 10s posibles tipos MIME aceptados por el navegador, y el orden de estas
tres sentencias es importante. El objeto R e s p o n s e tambidn time un metodo
S e n d s t r e a m , pero solo deberia llamarse despues de enviar el tipo de 10s datos
con una llamada independiente. Este es el efecto de este programa:
Para incrustar el programa en una pagina, hay que aiiadir el siguiente codigo
a1 codigo HTML:
begin
i f not cds .Active then
cds.CreateDataSet
else
cds.EmptyDataSet;
for i : = 0 to 5 do / / n u r n e r o d e p d g i n a s
begin
// c o n s i g u e e l f o r r n u l a r i o d e d a t o s d e l s i t i o d e b u s q u e d a
GrabHtml (strSearch + ' & s t a r t = ' + IntToStr (i*100));
// l o a n a l i z a p a r a r e l l e n a r e l c d s
HtmlStringToCds;
end;
cds .First;
// d e v u e l v e e l c o n t e n i d o d e l p r o d u c t o r
Response.Content : = DataSetTableProducerl-Content;
end:
d e ~ p h amp. I+
jhimr owe
cod
WebSnap
Ahora que hemos presentado 10s elementos mas importantes del desarrollo de
10s clementos dc aplicaciones de servidores Web con Delphi, podemos centrarnos
en una arquitcctura mas comple.ja disponible desde Delphi 6: WebSnap. Habia
dos buenas razones para no abordar este tema desde el principio. Una de cllas es
que WcbSnap se base en 10s conceptos que proporciona WebBroker, por lo que si
no conocemos las caracteristicas subyacentes, no podremos comprender las mas
nuevas. Por e.jemplo, una aplicacion WebSnap tecnicamentc es un programa CGI;
o un modulo ISAPI o de Apache. La segunda razon es que desde WebSnap solo sc
incluye en la version Enterprise Studio de Delphi, y no todos 10s programadores
de Delphi tienen la posibilidad de utilizarla.
WebSnap tiene unas cuantas ventajas sobre WebBroker, ya que pcrmite utilizar multiples modulos Web, cada uno para una pagina, la integracion de guiones
de servidor, XSL, y la tecnologia Internet Express (de estos dos ultimos temas
hablaremos mas adelante, en el capitulo 22). Aun mas, dispone de muchos componentes listos para ser usados para mane.jar tareas comunes tales como el registro de un usuario, gestion de sesiones, etc. En lugar de dar una lista con todas las
caracteristicas de WebSnap, vamos a analizarlas con una serie de sencillas aplicaciones centradas en las mismas. Por motivos de comprobacion, todas estas
aplicaciones han sido creadas utilizando eI Web App Debugger, pero no sera
dificil desplegarlas usando cualquier otra tecnologia.
El punto de partida del desarrollo de una aplicacion WebSnap es un cuadro de
dialogo que podemos invocar bien desde la pagina WebSnap del cuadro de dia-
Figura 20.6. Las opciones ofrecidas por el cuadro de dialogo New WebSnap
Application incluyen el tip0 de servidor y un boton que permite seleccionar 10s
cornponentes basicos de la aplicacion.
---
..
--
..~
-
L - , x # -
aplicacion Web con otro editor mas sofisticado, podemos determinar esta eleccion en la ficha Internet del cuadro de dialog0 Environment Options. A1 hacer
clic sobre el boton Edit para la extension HTML, podremos escoger un editor
externo para el menu de metodo abreviado del editor o un boton especifico de la
barra de herramientas de Internet de Delphi.
La plantilla HTML estandar utilizada por WebSnap aiiade a cualquier pagina
del programa su titulo y el titulo de la aplicacion, utilizando lineas de guion como
las siguientes:
<hl><%= Application.Title %></hl>
<h2><%= Page .Title %></h2>
Mas adelante hablaremos sobre 10s guiones. Por ahora vamos a comenzar el
desarrollo del ejemplo WSnapl creando un programa con varias paginas. Pero
antes, vamos a completar este repaso mostrando el codigo fuente de un modulo de
una pagina Web de muestra:
type
Thome = class (TWebAppPageModule)
end;
function home : Thome;
implementation
{SR . d f m )
{.htzd)
Variants;
initialization
if WebRequestHandler <> nil then
WebRequestHandler.AddWebModu1eFactory
(TWebAppPageModuleFactory.Create(Thome, TWebPageInfo.
Create([wpPublished { , wpLoginRequired)], ' . h t z d l ) ,
caCache) ) ;
end.
- --
E
m
Las paginas estaran casi vacias, per0 a1 menos tendremos la estructura adecuada. Para completar la pagina de inicio, simplemente editaremos directamente
el archivo HTML vinculado. Para la pagina date, hemos empleado el mismo
metodo que para una aplicacion de WebBroker, aiiadiendo a1 texto HTML unas
etiquetas personalizadas, como la siguiente:
<p>The t i m e at this site is <#time>.</p>
Tambien hemos aiiadido algo de codigo a1 controlador del evento OnTag del
componente productor para reemplazar esta etiqueta con la hora actual.
Para la segunda pagina, country, hemos modificado el codigo HTML incluyendo etiquetas para 10s diversos campos de la tabla de pais, como en:
TDataSetPageProducer
o b j e c t cdscountry: TClientDataSet
FileName = 'C:\Archivos de programa\Archivos comunes\Borland
Shared\Data\country.cdsl
end
Para abrir este conjunto de datos cuando la pagina se crea por primera vez, y
devolverla a1 primer registro en futuras llamadas, hemos manipulado el evento
OnBe f o r e D i s p a t c h P a g e del modulo de la pagina Web, aiiadiendole este
codigo:
Es bastante importante el hecho de que una pagina WebSnap sea muy similar
a una parte de una aplicacion WebBroker (basicamente una accion ligada a un
productor), ya que nos permite adaptar componentes del codigo WebBroker a esta
nueva arquitectura. Incluso se pueden adaptar 10s componentes DataSetTableProducer a esta arquitectura. Tecnicamente, podemos generar una pagina nueva,
quitar su componente productor, sustituirlo por un DataSetTableProducer y conectar este componente a la propiedad P a g e P r o d u c e r del modulo de la pagina
Web. En la practica, este metodo recortara el archivo HTML de la pagina y sus
guiones. En el programa WSnap1 hemos utilizado una tecnica mejor a1 aiiadir a1
archivo HTML una etiqueta personalizada (< # h tml t a b 1 e >). Tambien hemos
utilizado el evento OnTag del productor de la pagina para aiiadir el resultado de
tabla del conjunto de datos a1 HTML:
if Tagstring = ' h t r d t a b l e ' then
ReplaceText : = DataSetTableProducer1.Content;
Guiones de servidor
Cuando tenemos varias paginas en un programa de servidor (cada una de ellas
asociada a un modulo de pagina diferente), la forma de escribir un programa
cambia. Tener a mano 10s guiones de servidor nos permite utilizar un enfoque aun
mas potente. Por ejemplo, 10s guiones estandar del ejemplo WSnap 1 se encargan
de la aplicacion, 10s titulos de las paginas y el indice de las mismas. Este indice lo
genera un cnumerador, es decir, la misma tecnica que se utiliza para recorrer una
lista dentro del codigo de un guion WebSnap:
< t a b l e cellspacing="O" cellpadding="O"><td>
<X
e = n e w Enumerator(Pages)
s = "
c = o
for ( ; ! e. atEnd ( ) ; e .moveNext
(
if (e.item() .Published)
(
C++
1
1
if ( c > l ) Response .Write (s)
'
>
< / td></table>
.
%>
</tr>
< % for (i=l;i<=5;i++)
( %>
< tr>
< td>Line <%=i %></td>
< % for (j=l;j<=5;j++)
( %>
< td>Value= <%=i * j %></td>
<% } B>
</tr>
<B } %>
</table>
<hr>
<hl><%= Page. Title %></hl>
<P>
Este guion para el menu utiliza la lista Pages y 10s objetos globales de guion
Page y Application. WebSnap permite disponer de otros objetos globales
tales como EndUser y Session (en caso de que se aiiadan 10s correspondientes adaptadores a la aplicacion), Modules y Producer, que permite acceder a1
componente Producer del modulo de la pagina Web. El guion tambien permite
usar 10s objetos Response y Request del modulo Web.
Adaptadores
Ademas de a estos objetos globales, dentro de un guion tambien podemos
acceder a todos 10s adaptadores que esten disponibles en el modulo de la pagina
Web correspondiente. (Adaptadores de otros modulos, como 10s modulos de datos
Web compartidos, deben de ser referenciados prefijando su nombre con el objeto
Modules y el correspondiente modulo.) La finalidad de 10s adaptadores es pasar
informacion del codigo cornpilado de Delphi al guion interpretado, proporcionando una interfaz que puedan utilizar guiones para la aplicacion Delphi. Los
adaptadores contienen campos que representan datos y albergan funciones que
representan ordenes. Los guiones de servidor pueden acceder a estos valores y
lanzar estas ordenes, pasandoles parametros especificos.
Campos de adaptadores
Para realizar personalizaciones sencillas, simplemente bastara con aiiadir nuevos campos a 10s adaptadores especificos. En el ejemplo WSnap2, hemos aiiadido
un campo personalizado al adaptador de la aplicacion. Despues de seleccionar
este componente, podemos optar por abrir su editor Fields (accesible a traves de
su menu local) o trabajar dentro del Object Treeview. Despues de aiiadir un
campo nuevo (Ilamado AppHitCount en el ejemplo), podemos asignarle un
valor en su evento OnGe tvalue. Si queremos contar las solicitudes de cada
pagina de la aplicacion Web, tambien podemos controlar el evento OnBef orePageDispat ch del componente PageDispatcher global para incrementar el valor
de un campo local, HitCount. Este es el codigo para 10s dos metodos:
procedure Thome.PageDispatcherBeforeDispatchPage(Sender:
TObject;
const PageName: String; var Handled: Boolean);
begin
Inc (HitCount);
end;
procedure Thome.CountGetValue(Sender:
Variant) ;
begin
Value : = Hitcount;
end;
Tambien podriamos usar el nombre de la pagina para el recuento de 10s accesos a cada una de ellas (y podriamos aiiadir soporte para permanencia, ya que el
contador se pone a cero cada vez que ejecutamos una nueva instancia de la aplicacion). Ahora que hemos aiiadido un campo personalizado a un adaptador ya existente, (correspondiente a1 objeto de guion Application), podemos acceder a1
mismo desde cualquier guion de la siguiente manera:
<p>Application hits since last activation:
< % = Application. C o u n t . V a l u e % X / p >
Componentes de adaptadores
Del mismo modo, podemos aiiadir tambien adaptadores personalizados a paginas especificas. Si lo que necesitamos es pasar solamente unos cuantos campos,
es mejor utilizar el componente Adapter generico. Otros adaptadores personalizados
(ademas del ApplicationAdapter global que ya hemos utilizado) son:
o b j e c t TAdapterActions
o b j e c t AddPlus: TAdapterAction
OnExecute = AddPlusExecute
end
o b j e c t Post: TAdapterAction
OnExecute = PostExecute
end
end
o b j e c t TAdapterFields
o b j e c t Text: TAdapterField
OnGetValue = TextGetValue
end
object Auto: TAdapterBooleanField
OnGetValue = AutoGetValue
end
end
end
Displaycomponent = AdapterFieldGroupl
object CmdPost: TAdapterActionButton
ActionName = ' P o s t '
end
object CmdAddPlus: TAdapterActionButton
Act ionName = ' A d d P l u s '
end
end
end
end
Figura 20.9. El Web Surface Designer para la pagina inout del ejernplo WSnap2, en
tiernpo de disefio.
Ahora que tenemos una pagina HTML con algunos guiones para mover 10s
datos a nuestro antojo y enviar ordenes, v e a m s el codigo fuente necesario para
que funcione este ejemplo. En primer lugar, tenemos que aiiadir a la clase dos
campos locales para almacenar 10s campos del adaptador y poder manipularlos.
Tambien necesitamos implementar el evento OnGetValue para ambos, devolviendo 10s valores de campo. Cada vez que se hace clic sobre uno de 10s botones,
tenemos que conseguir el texto que se ha pasado a1 usuario, que no se copia
automaticamente en el campo correspondiente del adaptador. Podemos conseguir
este efecto si nos fijamos en la propiedad Actionvalue de estos campos, la
cual solamente se establece si escribimos algo (es por esto que, si no lo hacemos,
tenemos que establecer el campo booleano como False). Para evitar la repeticion de este codigo para ambas acciones, lo colocaremos en el evento
OnBeforeExecuteAction del modulo de la pagina Web.
procedure Tinout.AdapterlBeforeExecuteAction(Sender,
TOb j ect ;
Params: TStrings; var Handled: Boolean);
Action:
begin
i f Assigned (Text.ActionValue) then
fText : = Text.ActionValue.Va1ues [O];
fAuto : = Assigned (Auto-Actionvalue);
end;
Fijese en que cada accion puede tener varios valores (en caso de que 10s componentes permitan selection multiple); aunque como no es este el caso, simplemente
tomaremos el primer elemento. Por ultimo, hemos escrito el siguiente codigo para
10s eventos O n E x e c u t e de las dos acciones:
procedure Tinout.AddPlusExecute(Sender:
TStrings) ;
begin
fText : = Text + ' + ' ;
end:
procedure Tinout.PostExecute(Sender:
TStrings) ;
begin
i f Auto then
AddPlusExecute (Self, nil) ;
end;
TObject;
TObject;
Params:
Params:
remos mezclando el guion con el codigo, de manera que 10s cambios en la interfaz
de usuario requeriran actualizar el programa. Se pierde el reparto de responsabilidades entre el desarrollador de la aplicacion Delphi y el diseiiador de HTML y
guiones. E, ironicamente, se acabara necesitando la ejecucion de un guion para
realizar algo que el programa Delphi podria haber hecho de manera correcta y
posiblemente a mayor velocidad.
WebSnap es una arquitectura potente y un gran paso adelante con respecto a
WebBroker, per0 deberia utilizarse con cuidado para evitar el ma1 uso de algunas
de estas tecnologias ya que son simples y potentes. Por ejemplo, podria merecer la
pena utilizar el diseiiador Adapterpageproducer para generar la primera version
de una pagina, y despues coger el guion generado y copiarlo en el codigo HTML
de un simple Pageproducer, de manera que un diseiiador Web pueda modificar el
guion con una herramienta especifica.
Para aplicaciones mas complicadas, es preferible usar las posibilidades que
ofrecen XML y XSL, que se encuentran disponibles en esta arquitectura aunque
no tengan un papel central. En el capitulo 22 hablaremos mas sobre este tema.
Encontrar archivos
Cuando se ha escrito una aplicacion como la que acabamos de describir, hay
que desplegarla como un CGI o como una biblioteca dinamica. En lugar de colocar las plantillas en 10s archivos incluidos en la misma carpeta que el archivo
ejecutable, puede dedicarse una subcarpeta o carpeta personalizada para albergar
todos 10s archivos. El componente LocateFileService se encarga de esta tarea.
El componente no resulta intuitivo. En lugar de tener que especificar una carpeta destino como una propiedad, el sistema lanza uno de 10s eventos de este
componente cada vez que tiene que encontrar un archivo. (Este enfoque es mucho
mas potente.) Existen tres eventos: OnFindIncludeFile,OnFindStream
y OnFindTempla te File.El primer y el ultimo evento devuelven el nombre
del archivo a utilizar en un parametro var.El evento On Findst ream permite
incluso proporcionar directamente un flujo, empleando uno de 10s que ya se encuentran en memoria o que se ha creado a1 vuelo, extraido de una base de datos,
conseguido mediante una conexion HTTP o de cualquier otra manera que pueda
pensarse.
En el caso mas simple del evento OnFind Include File,se puede escribir
un codigo como el siguiente:
procedure TPageProducerPage2.LocateFileServicelFindIncludeFile(
ASender: TObject; AComponent: TComponent; const AFileName:
String;
var AFoundFile: String; var AHandled: Boolean);
begin
AFoundFile : = DefaultFolder + AFileName;
AHandled := True;
end ;
CITY,
El DataSetAdapter
Ahora que tenemos disponible un conjunto de datos, podemos aiiadir a la primera pagina un DataSetAdapter y conectarlo a1 ClientDataSet del modulo Web.
Automaticamente, el adaptador hace que todos 10s campos del conjunto de datos y
diversas acciones predetenninadas para operar con el conjunto (corno Delete,
Edit y Apply) esten disponibles. Podemos aiiadirlos explicitamente a las colecciones A c t i o n s y F i e l d s para excluir algunos y personalizar su comportamiento, per0 no siempre es necesario. A1 igual que el PagedAdapter, el DataSetAdapter
tiene una propiedad P a g e s i z e que podemos utilizar para indicar el numero de
elementos que queremos mostrar en una pagina. El componente tambien dispone
de metodos que podemos utilizar para recorrer las paginas. Este enfoque es muy
aconsejable para visualizar un gran conjunto de datos dentro de una cuadricula. A
continuacion veremos 10s valores del adaptador para la pagina principal del ejemplo WSnapTable:
object DataSetAdapterl: TDataSetAdapter
DataSet = WebDataModulel.ClientDataSet1
Pagesize = 6
end
...
\\'SnapTa ble
table
Prenous Page 1 2
3 Ntxt Pme
En este listado hay algunas cosas que debemos de tener en cuenta. La primera
es que la cuadricula tiene la propiedad AdapterMode establecida como Browse.
Otras posibilidades podrian ser Edit, Insert y Query. Este mod0 de representacion del conjunto de datos para adaptadores determina el tipo de interfaz de
usuario (texto, cuadros de dialog0 y otros controles de entrada) y la visibilidad de
otros botones (como por ejemplo 10s botones Apply y Cancel que solo estan
presentes en la vista de edicion; lo contrario sucede con la orden Edit).
-. ---NOTA: ~ambi6npucdc modificarse el m o d ~del adaptador mediante gguio-
o ode .
-iJp1
--
--
- ---.
- -- .--
- --
--
AdaplmPagcRoduar AdaptetForrnl
AdaplerFolrnl
AdaplerCornmar
AdapIetFreldGro
E
I
Apply
Cancel
I Delete (
CUST-NO
pE7S~gnatureDes~gn
CXTsTOhE.R
~c
Bivd
ADDRESS-LINE1 15500 P a c ~ f He~ghts
San D~ego
CFY
STATE_PRO\ WCE CA
USA
COUNTRY
L
A
Q
2l
En el listado, puede verse que todas las operaciones envian el usuario de vuelta
a la pagina principal y que el AdapterMode se establece como Edit, a menos
que haya conflictos o errores en la actualizacion. En este caso se vuelve a mostrar
' BgColor="Silver"
align="center"'
...
' +
First
Revtous
Nexl
Lns!
Tm
Lee
000
' A h
USA
53733
Bender
000
CEO
/USA
,212850
1&3'sf0
-d'.
q
.
Uso de sesiones
Para subrayar la importancia de este tip0 de soporte, hemos creado una aplicacion de WebSnap con una sola pagina que muestra tanto el numero total de visitas
como el numero total de visitas para cada sesion. El programa tiene un componente Sessionservice con valores predeterminados para sus propiedades
MaxSess ions y Def aultTimeout . Para cada nueva peticion, el programa
incrementa tanto su campo privado nHits del modulo de pagina como el valor
SessionHits de la sesion actual:
procedure
TSessionDemo.WebAppPageModuleBeforeDispatchPage(Sender:
TOb j ect;
const PageName: String; var Handled: Boolean) ;
begin
// incrementa las visitas d e aplicacion y sesion
Inc (nHits);
WebContext.Session.Values ['SessionHitsl] : =
Integer ( ~ e b ~ ~ n t.Session.Values
ext
[ 'SessionHits '1 ) + 1 ;
end:
begin
Value : = I n t e g e r
[ 'SessionHit s ' 1 ) ;
end;
(WebContext.Session.Va1ues
El efecto de este programa se muestra en la figura 20.13, donde hemos activado dos sesiones en dos navegadores distintos.
- .
TRUCO:En este ejemplo, hemos usado la sustitucih de etiquetas tradicional de WebBroker y 10s nuevos campos de adaptador y guiones de
WebSnap, para que se puedan comparar 10s dos enfoques. Hay que tener
presente que arnbos se encuentran disponibles en una aplicacion de WebSnap.
P I a h TRy
Scrr:an td CY[ZuGcM3zRx85F
Scmon h r 6
Script
Snnonhts (ma npphcannn) 6
Appbcabon hllr 12
Figura 20.13. Dos instancias del navegador funcionan con dos sesiones distintas de
la misrna aplicacion de WebSnap.
--
se incluyen en la
fabric:a en lugar de en el WebPageModule ya que el programa puede cornu 10s derechos de acceso y listar las paginas sin cargar siquiera el
I-
Cuando un usuario trata de ver una pagina que requiera la identificacion del
usuario. aparecera la pagina de entrada en el sistema indicada en el componente
EndUserSessionAdapter. Se puede crear una pagina de este tip0 facilmente, creando
un nuevo modulo de pagina Web basado en un Adapterpageproducer y aiiadiendole el LoginFormAdapter.
En el editor de la pagina, se aiiade un grupo de campos dentro de un formulario, se conecta el grupo de campos a1 LoginFormAdapter, y se aiiade un grupo de
comandos con el boton predeterminado Login. El formulario de entrada resultante tendra campos para el nombre de usuario y su contraseiia, y tambien para la
pagina solicitada.
Este ultimo valor se rellena automaticamente con la pagina solicitada, en caso
de que la pagina necesitara autorizacion y el usuario no haya entrado en el sistema aun. De este modo, un usuario puede alcanzar inmediatamente la pagina solicitada sin tener que volver a1 menu general.
El formulario de entrada tipicamente es no publicado, porque la orden Login
correspondiente ya esta disponible cuando el usuario no se encuentra dentro del
sistema; cuando entra el usuario, se sustituye con un comando Logout (de salida del sistema). Este comando lo obtiene el guion estandar del modulo de la
pagina Web, en particular con el siguiente codigo:
i f
( E n d U s e r . L o g o u t != n u l l ) ( %>
i f ( E n d U s e r . D i s p l a y N a m e != ' ') ( %>
<hl>Welcome <%=EndUser.DisplayName
%>
i f ( E n d U s e r . L o g o ~ r tE n a b l e d )
:></hl>
( %>
< a href="<%=EndUser.Logout.AsHREF%>">Logout</a>
I
i f
%>
( E n d U s e r . LoginPorrn. E n a b l e d )
( %>
< a href="<%=EndUser.LoginForm.As~R~FG>">Login</a>
}
} %>
%>
No hay mucho mas que decir sobre la aplicacion WSnapUsers, ya que casi no
tiene codigo ni valores personalizados. Este guion para la plantilla esthdar muestra
como se realiza el acceso a 10s datos de usuario.
Programacion
Web con
'
I
I
I)
lntroduccion a IntraWeb
lntraWeb es una biblioteca de componentes creada por Atozed Software
(www.atozedsoftware.com).En las ediciones Professional y Enterprise de Delphi
7, sc puede encontrar la version correspondiente de IntraWeb. La version
Professional solo puede usarse en mod0 de pagina, como veremos mas adelante.
Aunque Delphi 7 es la primera version del IDE de Borland en incluir este con-junto de componentes, IntraWeb lleva existiendo ya varios aiios, ha sido muy bien
recibida y apoyada, con la disponibilidad afiadida de varios componentes de terceras partes.
siones para C++ Builder y Java. Se esta trabajando en una version .NET y
probablemente estara disponible junto con una futura version de Delphi
TObject);
Esto es todo lo que se necesita para crear una aplicacion basada en Web capaz
de aiiadir texto a un cuadro de lista, como muestra la figura 2 1.1 (que muestra la
version final del programa, con un par mas de botones). Lo que es importante
tener en cuenta es cuando se ejecuta este programa es que cada vez que se hace
clic sobre el boton, el navegador envia una peticion nueva a la aplicacion, que
ejecuta el controlador de evento de Delphi y produce una nueva pagina HTML
basada en el nuevo estado de 10s componentes del formulario.
Cuando se ejecute la aplicacion, no se vera la salida del programa en el
navegador, sino en el formulario del controlador de IntraWeb que muestra la
figura 21.2. Una aplicacion IntraWeb independiente es un servidor HTTP completo, como se vera a continuacion. El formulario que se puede ver esta controlado por la llamada IWRun en el archivo de proyecto creado de manera predefinida
en cada aplicacion IntraWeb independiente. El formulario de depuracion permite
escoger un navegador y ejecutar la aplicacion a traves de el o copiar el URL de la
aplicacion en el Portapapeles, para que se pueda pegar despues dentro del
navegador. Es importante saber que la aplicacion utilizara de manera predetermi-
nada un numero de puerto aleatorio, que es distinto para cada ejecucion. Por eso
habra que utiliza un URL distinta cada vez. Se puede modificar este comportamiento si se selecciona el diseiiador del controlador del servidor (que es parecido
a un modulo de datos) y se fija la propiedad port. En el ejemplo hemos usado
8080 (uno de 10s puertos HTTP habituales), per0 otros valores tambien pueden
funcionar.
one
hvo
lhree
lour
four
Fie
Run I.ldp
3wmi- 1 -Ia
lnlraweb Version. 5 0 43
HTTP Pmt.8080
Packaged Enterprise
L~censeNumber 0
.-.
.-.
...............
--
II
,,,
aqui, porque ocuparia un espacio excesivo), se podra ver que esta dividido en tres
secciones principales.
La primera es una lista de estilos (basados en la etiqueta HTTP style) con
lineas como la siguiente:
Este controlador llama a1 correspondiente codigo de servidor. Si se ha proporcionado directamente el codigo JavaScript en la aplicacion IntraWeb, como ya
hemos comentado, se vera este codigo:
f u n c t i o n IWBUTTON3_onClick(ASender)
- .
- . --
--
..-
- ..
por la version 5.1 de IntraWeb. Hay que tener presente que un navegador
puede simular su identidad: Por ejemplo, es habitual que Opera este configurado para identificarse como Internet Explorer, lo que irnpedira una identificacion correcta para posibilitar el uso de sitios restringidos a otros
navegadores, pero posiblemente llevara a errores en tiempo de ejecucion o
inconsistencias.
La tercera parte del HTML generado es la definition de la estructura de pagina. Dentro de la etiqueta body se encuentra una etiqueta f o r m (en la misma
linea) con la siguiente accion de ejecucion:
Arquitecturas IntraWeb
Antes de escribir mas ejemplos para mostrar el uso de otros componentes
IntraWeb disponibles en Delphi 7, vamos a analizar otro elemento clave de
IntraWeb: las distintas arquitecturas que pueden usarse para crear y desplegar
aplicaciones basadas en esta biblioteca. Se pueden crear proyectos IntraWeb en el
mod0 Application (donde son aplicables todas las caracteristicas de IntraWeb o
en el mod0 Page (que es una version simplificada que puede aiiadirse a aplicaciones WebBroker o WebSnap ya existentes). Las aplicaciones que utilizan el mod0
Application pueden desplegarse como bibliotecas ISAPI, modulos de Apache o
utilizando el mod0 IntraWeb Standalone (una variante de la arquitectura del mod0
Application).
Los programas en mod0 Page pueden desplegarse como cualquier otra aplicacion WebBroker (ISAPI, modulo de Apache, CGI, etc.. .). IntraWeb usa tres arquitecturas distintas que se solapan en parte:
En 10s ejemplos que apareceran en el resto del capitulo utilizaremos por simplicidad y un proceso de depuracion mas sencillo el mod0 Standalone, per0 tambien hablaremos del soporte del mod0 Page.
elementos del menu permiten que el programa trabaje con el menu expandiendo o
colapsando 10s nodos y modificando la fuente. Este es el codigo para un par de
controladores de eventos:
procedure TformTree.ExpandAlllClick(Sender: TObject);
var
i: Integer;
begin
for i : = 0 to 1WTreeViewl.Items.Count - 1 do
1WTreeViewl.Item.s [i].Expanded : = True;
end;
procedure TformTree.EnlargeFontlClick(Sender: TObject);
begin
1WTreeViewl.Font.Size : = 1WTreeViewl.Font.Size + 2;
end;
Figura 21.3. El ejemplo IWTree utiliza un menti, una vista en arbol y la creacion
dinamica de un componente de memo.
El menu tiene dos submenus, que son bastante mas complejos. El primer0
muestra el identificador de la aplicacion, que es un identificador de la ejecucion y
sesion de la aplicacion. Este identificador esta disponible mediante la propiedad
Contents, muestra una lista de 10s tres primeros nodos del nivel principal junto
con el numero de subnodos directos. Aim asi, lo que es interesante es que la
informacion se muestra en un componente de memo creado en tiempo de ejecucion
(como muestra la anterior figura 2 1.3.), esactamente del mismo mod0 que en una
aplicacion VCL:
procedure TformTree.TreeContentslClick(Sender: TObject);
var
i: Integer;
begin
w i t h T I W M e m o - C r e a t e (Self) d o
begin
Parent : = Self;
A l i g n : = alBottom;
f o r i : = 0 t o 1WTreeViewl.Items.Count - 1 d o
Lines .Add ( IWTreeViewl. Items [i].Caption + ' ( ' +
IntToStr ( IWTreeViewl. Items [i] .SubItems .Count)
')
'1;
end;
end;
--
Click here</a>
< / td>
< /tr>
kowl ~ W ~ ' W O by
FM
O a~ m Canhl
blick here
bow
\
Flick here
> - A _ -
1-
3-
3-
La caracteristica principal del programa es su capacidad de mostrar una segunda pagina. Para realizar esto, en primer lugar se necesita aiiadir una nueva
pagina IntraWeb a la aplicacion, mediante la opcion ApplicationForm de la
pagina IntraWeb del cuadro de dialogo New Items de Delphi (File>New>Other).
Aiiadimos a esta pagina algunos componentes IntraWeb, como siempre, y despues la aiiadiremos un boton u otro control a1 formulario principal que podamos
usar para mostrar el formulario secundario (con la referencia a n o t h e r f orm
almacenada en un campo del formulario principal):
procedure TformMain.btnShowGraphicClick(Sender: TObject);
begin
anotherform : = TAnotherForm.Create(WebApp1ication);
anotherform.Show;
end;
-. - --
.I
'
Ahora que se ha visto como crear una aplicacion IntraWeb con dos formularios, vamos a analizar brevemente el mod0 que se IntraWeb crea el formulario
principal. El codigo relevante, generado por el asistente de IntraWeb cuando se
crea un programa nuevo, esta en el archivo de proyecto:
begin
IWRun(TFormMain,
TIWServerController);
TObject;
10) ;
rnapa de bits. No hay que intentar utilizar el lienzo &age (como se hacia
con el companente TImage de la VCL) y no hay que tratar de war un JPEG
en primer Ingar, o no se verh nin& efecto o aparecera un error en tiempo
de e j m c i h .
Gestion de sesiones
Si se ha realizado algo de programacion Web, ya se sabe que la gestion de
sesiones es un asunto bastante complejo. IntraWeb proporciona un sistema de
gestion de sesiones predefinido y simplifica el mod0 en que se trabaja con sesio-
nes. Si se necesita una sesion de datos para un formulario especifico, todo lo que
hay que hacer es afiadir un campo a ese formulario. Los formularies IntraWeb y
sus componentes tienen una instancia para cada sesion de usuario. Por ejemplo,
en IWSession hemos aiiadido al formulario un campo llamado Formcount.Por
contra, tambitn hemos declarado una variable de unidad global llamada
Globalcount, que comparten todas las instancias (o sesiones) de la aplicacion.
Para aumentar el control sobre 10s datos de sesion y permitir que varios formularios la compartan, se puede personalizar la clase TUserSession que coloca
el IntraWeb Wizard en la unidad ServerController. En el ejemplo WSession,
hemos particularizado la clase de esta forma:
type
TUserSession = class
public
Usercount : Integer;
end ;
IntraWeb crea una instancia de este objeto para cada nueva sesion, como puede verse en el metodo IWServerControllerBaseNewSession de la clase TIWServerController en la unidad ServerController predefinida.
procedure TIWServerController.IWServerControllerBaseNewSession(
ASession: TIWApplication; v a r VMainForm: TIWAppForm);
begin
ASession.Data := TUserSession-Create;
end;
Ya que la mayor parte del codigo se genera automaticamente, despues de aiiadir datos a la clase TUser ses s ion simplemente hay que usarla mediante la
funcion User ses s ion, como en el codigo siguiente, extraido del ejemplo
IWSession. Cuando se hace clic sobre un boton, el programa incrementa varios
contadores (uno global y dos especificos de sesion) y muestra sus valores en
etiquetas:
procedure TformMain.IWButtonlClick(Sender: TObject);
begin
InterlockedIncrement (GlobalCount);
Inc (FormCount);
Inc
(UserSession.UserCount);
procedure TWebModulel.IWPageProducerlGetForm(ASender:
TIWPageProducer;
AWebApplication: TIWApplication; var VForm: TIWPageForm);
begin
VForm : = TformMain.Create(AWebApplication);
end;
Global 24
Form 14
User. 14
- -
ADVERTENCIA: La versibn que se incluye con Delphi 7 tiene un problema con el Web App Debugger de Delphi y el componente IWModuleController. Ya se ha solucionado este problema y existe una actualizacion
gratuita.
Este es un resumen del archivo DFM del mbdulo Web del programa de ejemplo:
object WebModulel: TWebModulel
Actions = <
item
Default = True
Name = ' W e b A c t i o n I t e m l r
PathInfo = ' / s h o w '
OnAction = WebModulelWebActionItemlAction
end
item
Name = ' W e b A c t i o n I t e m . 2 '
PathInfo = ' / i w d e m o r
Producer = IWPageProducerl
end>
object IWModuleControllerl: TIWModuleController
object IWPageProducerl: TIWPageProducer
OnGetForm = IWPageProducerlGetForm
end
end
Ya que esta es una aplicacion CGI en mod0 Page, no hay ninguna gestion de
sesiones. Aun mas, el estado de 10s componentes de una pagina no se actualiza
automaticamente escribiendo controladores de eventos, como en un programa
IntraWeb estandar. Para conseguir el mismo efecto se necesita escribir codigo
especifico para manejar mas parametros de la peticion HTTP. Deberia quedar
claro incluso mediante este ejemplo tan sencillo que el mod0 Page hace menos
cosas de mod0 automatic0 que el mod0 Application, pero que es mas flexible. En
particular, el mod0 Page de IntraWeb permite aiiadir prestaciones de diseiio RAD
visual a las aplicaciones WebBroker y WebSnap.
Control de la estructura
El programa CgiIntra utiliza otra interesante tecnologia disponible en IntraWeb:
la definition de una estructura personalizada basada en HTML. (Este tema no
tiene realmente relacion, ya que las estructuras HTML tambien funcionan en
mod0 Application, pero, simplemente, se han usado estas dos tecnicas en un unico
ejemplo.) En 10s programas creados h a s h este momento, la pagina resultante es la
proyeccion de una serie de componentes colocados en tiempo de diseiio en un
formulario, en el que se pueden usar propiedades para modificar el codigo HTML
resultante. i Q u l es lo que sucederia si se deseara incrustar un formulario de
1-
Figura 21.6. El HTML Layout Editor de IntraWeb es un cornpleto editor HTML visual.
En el HTML generado, el HTML define la estructura de Ia pagina. Los componentes solo se marcan con una etiqueta especial basada en Ilaves, como en el
ejemplo siguiente:
No hace falta decir que el HTML que se ve en el diseiiador visual del HTML
Layout Editor se corresponde de manera casi perfecta con el HTML que se
puede ver a1 ejecutar el programa en un navegador.
La unidad del modulo de datos no tiene asignada una variable global; si fuera
asi, todos 10s datos se compartirian entre todas las sesiones, con una gran posibilidad de problemas en caso de peticiones concurrentes desde varios hilos. Sin
embargo, el modulo de datos ya expone una funcion global que tiene el mismo
nombre que la variable global que utilizaria Delphi, y que accede a1 modulo de
datos de la sesion actual:
function DataModulel: TDataModulel;
begin
Result : = TUserSession(RWebApp1ication.Data) .Datamodulel;
end;
TObject);
El codigo relevante del ejemplo esta en el codigo del boton, que puede usarse
para recorrer 10s datos mostrando la pagina siguiente o volviendo a la anterior.
Este cs el codigo para uno de 10s dos metodos (el otro no se presenta porque es
muy parecido):
procedure TformMain.btnNextClick(Sender: TObject);
var
i: Integer;
begin
n P o s : = n P o s + 10;
if n P o s > DataSourcel.DataSet.RecordCount - 10 then
n P o s : = DataSourcel.DataSet.RecordCount - 10;
DataSourcel.DataSet.First;
for i : = 0 to nPos do
DataSource1.DataSet.Next;
end;
Figura 21.8. El formulario principal del ejemplo IWGridDemo utiliza una cuadricula
con marco con hipervinculos hacia el formulario secundario.
UseFrame = True
Usewidth = True
Columns = <
item
Alignment = taLeftJustify
BGColor = clNone
DoSubmitValidation = True
Font-Color = clNone
Font-Enabled = True
Font.Size = 10
Font-Style = [ I
Header = False
Height = '0'
VAlign = vaMiddle
Visible = True
Width = ' 0 '
Wrap = False
BlobCharLimit = 0
CompareHighlight = hcNone
DataField = ' F I R S T - N A M E '
Title.Alignment = taCenter
Title.BGColor = clNone
Title.DoSubmitVa1idation = True
Title.Font.Color = clNone
Tit1e.Font.Enabled = True
Title.Font.Size = 10
Title.Font.Style = [ I
Title.Header = False
Title-Height = '0'
Title.Text = ' F I R S T - N A M E '
Title.VAlign = vaMiddle
Title.Visible = True
Title.Width = '0'
Title.Wrap = False
end
item
DataField = 'LAST-NAME'
LinkField = 'EMP-NO '
OnClick = IWDBGridlColumnslClick
end
item
DataField =
' JOB-CODE'
end
item
DataField =
' JOB-COUNTRY'
end
item
DataField =
end
item
' JOB-GRADE'
DataField = 'PHONE-EXT'
end>
Datasource = DataSourcel
Options = [dgShowTitles]
end
A1 establecer la propiedad Start I D del segundo formulario, se puede encontrar el registro apropiado:
procedure TRecordForm.SetStartID(const Value: string);
begin
FStartID : = Value;
Value, [ I ) ;
DataSourcel .Dataset .Locate ( 'EMP-NO',
end;
maLast Name
Hire
estan
lC'
LI
TJ
Green
Tcm
Luke
Carol
Mary
Leshe
K J
Randy
Mchael
Lcc
Leung
Nordskom
pas
Phong
Wcrton
Wdhams
Yanowslu
4
4
4
4
4
4
4
4
4
USA
USA
USA
USA
USA
USA
USA
USA
USA
El motivo para utilizar este enfoque basado en JavaScript en lugar de un enfoque basado en XML como el utilizado en otras tecnologias parecidas, es que solo
Internet Explorer ofrecer soporte para islas de datos XML. Mozilla y Netscape
carecen de esta caracteristica y tienen un soporte muy limitado de XML.
tecnolog~as
XML
Internet Express.
Uso de XSLT.
XSL en WebSnap.
Presentacion de XML
El lenguaje extensible de marcas (extens~bleMarkup Language, XML) es una
version simplificada de SGML y recibe mucha atencion en el mundo de las tecnologias de la informacion. XML es un lenguaje de marcas, que quiere decir que
utiliza simbolos para describir su propio contenido (en este caso, etiquetas que
consistente en un texto definido de manera especial, encerrado entre 10s caracteres < y >). Es extensible porque permite usar marcas libres (en contraste con, por
ejemplo, HTML, que tiene marcas predefinidas). El lenguaje XML es un estandar
promocionado por el World Wide Web Consortium (W3C). La recomendacion
XML puede encontrarse en www.w3.org/TR/REC-xml.
Se ha llamado a XML el ASCI del aiio 2000, para indicar que es una tecnologia simple y muy extendida y tambien que un documento XML es un archivo de
texto plano (de manera opcional con caracteres Unicode en lugar de simple texto
ASCII). La caracteristica mas importante de XML es que es descriptivo, ya que
cada etiqueta tiene un nombre casi legible para un humano. Este es un ejemplo, en
caso de que jamas se haya visto un documento XML:
<book>
<title>La biblia de Delphi 7</title>
<author>Cantu</author>
<publisher>Anaya</publisher>
</book>
XML presenta unas cuantas desventajas que estaria bien resaltar desde el
principio. La mas importante es que sin una descripcion formal, un documento
vale de poco. Si se quiere intercambiar documentos con otra empresa, hay que
llegar a un acuerdo sobre lo que significa cada etiqueta y tambien sobre el significado semantic0 del contenido. (Por ejemplo, cuando se tiene una cantidad, hay
que acordar el sistema de medida o incluirlo en el documento.) Otra desventaja es
que 10s documentos XML son mucho mayores que otros formatos. Por ejemplo,
usar cadenas para 10s numeros no es nada eficiente, y las etiquetas de apertura y
cierre ocupan mucho espacio. Lo bueno es que XML se comprime muy bien, por
el mismo motivo.
/ / correct0
/ / erroneo
Cualquier nodo XML puede tener varios atributos, varias etiquetas incrustadas y un unico bloque de texto que representa el valor del nodo. Es
habitual que 10s nodos XML tengan un valor textual o etiquetas incrustadas, y no ambas variantes. Este es un ejemplo de la sintaxis completa de un
nodo:
Un nodo puede tener varios nodos hijo con la misma etiqueta (las etiquetas
no tiene por que ser unicas). Los nombres de atributos son unicos para
cada nodo.
TObject);
XmlDoc.Active : = True;
xmlBar. Panels [l] .Text := 'OK';
xmlBar Panels [2] .Text : = ' ';
(XmlDoc as IXMLDocumentAccess) .DOMPersist loadxml (MemoXml .Text) ;
eParse := (XmlDoc.DOMDocument as IDOMParseError) ;
i f eParse. errorcode <> 0 then
with eParse do
begin
xmlBar. Panels [1] .Text : = 'Error in: ' + IntToStr
(Line) + '. ' + IntToStr (LinePos);
xmlBar. Panels [2] .Text : = SrcText + ': ' + Reason;
end;
end;
La figura 22.1 muestra un ejemplo de la salida del programa, junto con la vista
en arb01 XML que ofrece la tercera pagina (para un documento correcto). La
tercera pagina del programa se construyo mediante el componente WebBrowser,
que incluye un control ActiveX de Internet Explorer. Lamentablemente, no esiste
un mod0 direct0 de asignar una cadena con testo XML a este control, por lo
quc habra que guardar el archivo en primer lugar para luego pasar a esta pagina
para iniciar la carga del XML en el navegador (despues de hacer clic a mano
sobre el boton Refresh a1 menos una vez).
. -
--
.-
NOTA: Hemos utilizado este codigo como punto de partida para crear un
editor XML cornpleto llamado XrnlTypist. Incluye resaltado de sintaxis,
soporte XSLT y unas cuantas caracteristicas adicionales. En el apCndice A
se puede consultar la disponibilidad de este editor XML gratuito.
programas en general, ya que algunas de las tecnicas que vamos a ver van mas
alla del lenguaje que se utilice). Hay dos tecnicas basicas para manipular documentos XML: utilizar una interfaz de modelo de objeto de documento (Document
Object Model, DOM) o utilizar una API para XML sencilla (Simple API for
XML, SAX). Los dos enfoques son bastante distintos:
es lo que hace DOM. DOM es una interfaz estandar, por lo que cuando se ha
escrito codigo que utiliza un arb01 DOM, podemos cambiar de implementacion de
DOM sin alterar el codigo fuente (a1 menos si no hemos utilizado extensiones
personalizadas).
En Delphi se pueden instalar varias implementaciones de DOM, disponibles
como servidores COM, y utilizar sus interfaces. Uno de 10s motores DOM mas
utilizados en Windows es el que proporciona Microsoft como parte del MSXML
SDK, per0 que tambien instala Internet Explorer (y por ello todas las versiones
recientcs de Windows) y muchas otras aplicaciones de Microsoft. (Con el MSXML
SDK cornpleto tambien se incluye documentacion y ejemplos bastante detallados
que no se conseguiran en otras instalaciones de la misma biblioteca incluidas con
otras aplicaciones.) Otros motores DOM disponibles directamente en Delphi 7
son Xerces, de la fundacion Apache y OpenXML, de codigo abierto.
TRUCO: OpenXML es un motor DOM nativo en Object Pascal disponible
en www.philo.de/xml. Otro motor DOM nativo en Delphi lo ofrece
Turbopower. Estas soluciones tienen dos ventajas. No necesitan una bi.. .
.
.
a
ouoteca externa para que se ejecure el programs, ya que el componente
DOM se compila con la aplicacion; y son multiplataforma.
Delphi incluye las implementaciones DOM en un componente envoltorio Ilamado XMLDocument. Hemos usado este componente en el ejemplo anterior, per0
esaminaremos su papel en un aspect0 mas general. La idea de usar este componente en lugar de la interfaz DOM es permanecer independientes de las
implementaciones y poder trabajar con metodos simplificados, o auxiliares.
El uso de la interfaz DOM es bastante complejo. Un documento es un conjunto
de nodos, cada uno con un nombre, un elemento de texto, un conjunto de atributos
y un conjunto de nodos hijo. Cada conjunto de nodos permite el acceso a 10s
elementos a traves de su posicion o buscandolos por nombre.
Observese que el texto que se encuentra dentro de las etiquetas de un nodo, si
hay alguno, se representa como un hijo como del nodo y se listara en el conjunto
de nodos hijo. El nodo raiz tiene algunos metodos adicionales para crear nuevos
nodos, valores o atributos. Con el XMLDocument de Delphi podemos trabajar a
dos niveles:
A un nivel inferior, podemos utilizar la propiedad DOMDocument (del
tip0 de interfaz ~DOMDocument)para acceder a la interfaz estandar
W3C Document Object Model. La interfaz DOM oficial se define en la
unidad xmldom e incluye interfaces como IDOMNode, IDOMNodeList,
IDOMAttr, IDOMElement e IDOMText. Con las interfaces DOM oficiales, Delphi soporta un modelo de programacion estandar per0 de bajo nivel. La implementacion de DOM la indica el componente XMLDocument
en la propiedad DOMVendor.
// afiade e l p r o p i o nodo
NodeText : = XmlNode.NodeName;
i f XmlNode.1sTextElement then
NodeText : = NodeText + ' = ' + XmlNode.NodeValue;
NewTreeNode : = TreeViewl.Items.AddChild(TreeNode, NodeText);
// a t i a d e s u s a t r i b u t o s
f o r I := 0 t o xmlNode.AttributeNodes.Count - 1 d o
begin
AttrNode : = xmlNode.AttributeNodes.Nodes[I];
TreeViewl.Items.AddChild(NewTreeNode,
' I ' + AttrNode.NodeName + ' = " ' + AttrNode.Text + " ' 1 ' ) ;
end;
// afiade cada nodo h i j o
i f XmlNode.HasChildNodes then
f o r I : = 0 t o xmlNode.ChildNodes.Count - 1 d o
DomToTree (xmlNode.Chi1dNodes.Nodes [I], NewTreeNode);
end;
a u h r = Canlu
i
3 book
title = Delphi Devdoper'sHandbook
aulhol = Canlu
aulhor = Gaoch
r3boak
litle = MarletingDelph~6
aulho~= Canlu
@i:book
E book
El ebwk
l i b = EssenlidPascd
utl = hllp:Nwww.marwcantucorn
wthm = Canlu
8 ebmk
title = Thinking in Java
url = hllp:llwww,mindview.com
aulhu = Eckel
['text']
Hay que tener en cuenta que si no hay ningun atributo llamado t e x t , la llamada
fallara con un mensaje de error generico: "Invalid variant type conversion" (conversion de tip0 variante invalida). Si necesitamos acceder a1 primer atributo de la
raiz y no conocemos su nombre, podemos utilizar el siguiente codigo:
Este codigo consigue el (primer) autor del segundo libro. No podemos utilizar
la expresion Chi 1 dVa lues [ ' book ' ] , ya que hay varios nodos con el mismo
nombre bajo el nodo raiz.
Listado 22.1. El docurnento XML de muestra utilizado en 10s ejernplos de este
capitulo.
<?xml version="l.OW encoding="UTF-8"?>
<books t e x t = " B o o k s W >
<book>
<title>La biblia de Delphi 7</title>
<author>Cantu</author>
</book>
<book>
<title>Delphi Developer's Handbook</title>
<author>Cantu</author>
<author>Gooch</author>
</book>
<book>
<title>Delphi COM Programming</title>
<author>Harmon</author>
</book>
<book>
<title>Thinking i n C++</title>
<author>Eckel</author>
</book>
para mejorar la salida al memo del texto XML, fo&tehdolo m$or. Podemos escoger el
sangrado estqbleciendo
la
propie* ..$ Node- tip0- de
.--.
nco, tamb16 podemos
.blecidos dos espacios
,no hay forma alguna
d
El primer boton del formulario, Simple, crea un texto XML sencillo utilizando
las interfaces oficiales de bajo nivel de DOM. El programa llama a1 metodo
creat eElement del documento para cada nodo, aiiadiendolos como hijos de
otros nodos:
procedure TForml.btnSimpleClick(Sender: TObject);
var
iXml: IDOMDocument;
iRoot, iNode, iNode2, iChild, iAttribute: IDOMNode;
begin
/ / v a c i a e l docurnento
XMLDoc.Active : = False;
XMLDoc. XML-Text := ";
XMLDoc.Active : = True;
/ / raiz
iXml : = XmlDoc.DOMDocument;
iRoot : = iXml.appendChild (iXml.createElement ( ' x m l ' ) ) ;
/ / nodo "test"
iNode : = iRoot.appendChild (iXml.createElement ('test'));
iNode.appendChild (iXml.createElement ('test2'));
iChild : = iNode.appendChild (iXml.createElement ('test3'));
iChild. appendchild (iXml.createTextNode ( 'simple value ' ) ) ;
iNode.insertBefore (iXml.createElement ('testd'), iChild);
/ / replica de nodo
iNode2 : = iNode. cloneNode (True);
iRoot.appendChild (iNode2);
/ / adade un atrlbuto
iAttribute .nodevalue : = 'red ';
iNode2.attributes.setNamedItem
(iAttribute);
(XMLDoc.XML.Text);
end;
Fijese en que 10s textos de 10s nodos se aiiaden explicitamente, que 10s atributos se crean con una llamada de creacion especifica y que el codigo utiliza
cloneNode para hacer una replica de una rama entera del arbol. Globalmente,
la escritura del codigo es un poco engorrosa, per0 se acostumbrara a1 estilo. El
efecto del programa se muestra en la figura 22.3 (con formato en el memo y en el
arbol).
9x d
E test
test2
test4
test3 = s~mplevalue
E test
[color ="red"]
test2
test4
lest3 = s~molevalue
('customers',
SQLDataSetl);
El procedimiento Da t aSet ToDOM crea el nodo raiz con el texto del primer
parametro, coge cada registro del conjunto de datos, define un nodo con el segundo parametro y agrega un subnodo para cada campo del registro utilizando un
codigo extremadamente generico:
p r o c e d u r e DataSetToDOM (RootName, RecordName:
TXMLDocument;
DataSet: TDataSet) ;
var
iNode, iChild: IXMLNode;
i: Integer;
begin
DataSet.Open;
Dataset-First;
string; XMLDoc:
// r a i z
XMLDoc.DocumentElement
:=
XMLDoc.CreateNode
(RootName);
// afiade d a t o s d e t a b l a
w h i l e n o t DataSet .EOF d o
begin
// a d a d e u n n o d o p a r a c a d a r e g i s t r o
iNode : = XMLDoc.DocumentElement.AddChild (RecordName);
f o r I : = 0 t o DataSet.FieldCount - 1 d o
begin
// a f i a d e u n e l e m e n t o p a r a c a d a c a m p o
iChild : = iNode.AddChild (DataSet.Fields[i].FieldName);
iChild.Text : = DataSet.Fields[i].AsString;
end;
DataSet.Next;
end;
DataSet.Close;
end;
describir algunas propiedades de un objeto usando, una vez mas, el DOM de bajo
nivel :
procedure AddAttr (iNode: IDOMNode; Name, Value : string) ;
var
iAttr: IDOMNode;
begin
iAttr : = iNode.ownerDocument.createAttribute (name);
iAttr.nodeValue : = Value;
iNode.attributes.setNamed1tem
(iAttr);
end;
procedure TForml.btnObjectClick(Sender:
var
iXml: IDOMDocument ;
iRoot: IDOMNode;
begin
// v a c i a e l documento
XMLDoc.Active : = False;
XMLDoc. XML.Text : = ' ';
XMLDoc-Active : = True;
TObject);
// r a i z
iXml := XmlDoc. DOMDocument ;
iRoot : = iXml.appendChild (iXm1-createElement ( ' B u t t o n l ' ) ) ;
/ / a l g u n a s p r o p i e d a d e s como a t r i b u t o s ( t a m b i e n p o d r i a n s e r
// n o d o s )
AddAttr (iRoot, ' N a m e ' , Buttonl-Name);
AddAttr (iRoot, ' C a p t i o n ' , Buttonl .Caption);
AddAttr (iRoot, ' F o n t .Name ', Buttonl.Font.Name) ;
AddAttr (iRoot, ' L e f t ', IntToStr (Buttonl.Left)) ;
AddAttr (iRoot, ' H i n t ', Buttonl .Hint);
/ / m u e s t r a XML e n u n memo
Memol.Lines : = XmlDoc.XML;
end;
Desde luego, seria mas interesante disponer de una tecnica generica capaz de
guardar las propiedades de cada componente de Delphi (u objeto permanente,
para ser mas precisos), recorriendo de manera recursiva 10s subobjetos permanentes e indicando 10s nombres de 10s componentes a 10s que se hace referencia.
Esto es lo que hace el procedimiento C o m p o n e n t T o D O M , que utiliza la informacion RTTI bajo nivel proporcionada por la unidad TypInfo e incluye la extraccion
de la lista de propiedades de componentes. Una vez mas, el programa utiliza las
interfaces XML simplificadas de Delphi:
procedure ComponentToDOM
var
nProps, i: Integer;
PropList: PPropList;
Value : Variant ;
newNode: IXmlNode;
begin
// o b t i e n e l a lista d e p r o p i e d a d e s
nProps : = GetTypeData ( C ~ m p . C l a s s I n f o ) ~ . P r o p C o u n t ;
GetMem (PropList, nProps * SizeOf (Pointer)) ;
try
GetPropInfos (Comp.ClassInfo, PropList) ;
for i : = 0 to nProps - 1 do
begin
Value : = GetPropValue (Comp, PropList [i] .Name) ;
NewNode := iNode .Addchild (PropList [i] .Name) ;
NewNode.Text : = Value;
i f (PropList [i] . PropTypeA .Kind = tkclass) and (Value
<> 0 ) then
i f TObject (Integer (Value)) is TComponent then
NewNode .Text : = TComponent (Integer (Value)) .Name
else
/ / TPersistent p e r 0 n o TComponent: recursive
ComponentToDOM (newNode, TOb ject
(Integer(Value)) as TPersistent) ;
end ;
finally
FreeMem (PropList);
end;
end;
Las siguientes dos lineas de codigo, disparan la creacion del documento XML
(que se muestra en la figura 22.4):
XMLDoc.DocumentE1ement : = XMLDoc.CreateNode(SelffC1assName);
ComponentToDOM (XMLDoc.DocumentElement, Self) ;
cuanto a 10s tipos de documentos que podemos manipular con ellas (y esto es mas
positivo de lo que podria parecer en primera instancia).
1 TFolml
Name = Fmml
Lell = 192
Top = 107
Wdh=
He~gh!= 412
HorzScrollBar
Range 97
VertScrollBar = 20260720
Act~veConlrol=btnRTTl
B~DlMode= bdLellToR~ghl
Capl~on= DomCrealc
= 385
Cl~enlHerghl
CfienlWdh = 563
Color = -16777201
Cnnslranls = 20255360
5n
FOIM
Charset = 1
Color = 16777208
He~ght= 11
., .... ,,"".-.*-A
'1
Figura 22.4. El XML generado para describir el formulario del programa DomCreate.
Fijese en que las propiedades de 10s tipos de clase estan mas expandidas.
El asistente XML Data Binding Wizard se activa utilizando el icono correspondiente de la primera pagina del cuadro de dialogo New Items del IDE, o
haciendo doble clic directamente sobre el componente XMLDocument. (Es extrafio que el comando correspondiente no este en el menu local del componente).
Despues de una pagina en la que seleccionaremos un archivo de entrada, este
asistente muestra graficamente la estructura del documento, como se puede ver en
la figura 22.5 para el archivo XML de muestra del listado 22.1. En esta pagina es
donde nombramos cada entidad de las interfaces generadas, en caso de que no nos
gusten 10s que el asistente proporciona de manera predeterminada. Incluso podemos cambiar las reglas utilizadas por el asistente para generar 10s nombres (una
flexibilidad especial que no estaria ma1 en otras partes del IDE de Delphi). La
pagina final nos ofrece una vista previa de las interfaces generadas y ofrece opciones para generar 10s esquemas y otros archivos de definicion.
Para el archivo XML de muestra con 10s nombres de autores, el XML Data
Binding Wizard genera una interfaz para el nodo raiz, dos interfaces para las
listas de elementos de 10s dos tipos distintos de nodos (libros y libros electronicos), y dos interfaces mas para 10s elementos de cada uno de estos tipos.
6~
booksType
texi
U Q book
O tale
-.
P Generate B
--
i i
Figura 22.5. t l aslstente XML Data Blndlng Wlzard de Delphi puede anallzar la estructura
de un documento o un esquema (u otra definicion de documento) para crear un conjunto
de interfaces para un acceso mas simple y direct0 a 10s datos DOM.
( Property Accessors
( Property Accessors
'J
f u n c t i o n Get-Author: IXMLString-List;
p r o c e d u r e Set-Title (Value: WideString) ;
{ Methods & Properties }
p r o p e r t y Title: WideString r e a d Get-Title w r i t e Set-Title;
property Author: IXMLString-List r e a d G e t A u t h o r ;
end:
Para cada interfaz, el XML Data Binding Wizard genera tambien una clase de
implementacion que proporciona el codigo para 10s metodos de la interfaz, convirtiendo 10s consultas en llamadas DOM. La unidad incluye tres funciones de
inicializacion, que pueden devolver la interfaz del nodo raiz desde un documento
cargado en un componente XMLDocument (o un componente que proporcione
una interfaz IXMLDocument generica), o devolverla desde un archivo, o crear
un DOM completamente nuevo:
f u n c t i o n Getbooks(Doc: IXMLDocument) : IXMLBooksType;
f u n c t i o n Loadbooks(const FileName: WideString): IXMLBooksType;
f u n c t i o n Newbooks: IXMLBooksType;
Puede ser incluso mas sencillo si recuerda que mientras se escribe este codigo,
la funcion Code Insight de Delphi puede ayudar listando las propiedades disponibles de cada nodo, gracias a que el analizador sintactico puede leer las definiciones de la interfaz (aunque no entienda el formato de un documento XML generico).
Para acceder a un nodo de una de estas sublistas, escribiremos una de las siguientes sentencias (posiblemente la segunda, con la propiedad de matriz predeterminada) :
Books.Book. Items [I] .Title
Books .Book [l] .Title
// completo
// mds simple
Podemos utilizar un codigo igualmente simplificado para generar nuevos documentos o aiiadir elementos nuevos, gracias a1 metodo personalizado ~ d dque
,
esta disponible en cada interfaz basada en una lista. Si no disponemos de una
estructura predefinida para el documento XML, como en 10s ejemplos basados en
un conjunto de datos y RTTI de la demostracion anterior, no podremos utilizar
este enfoque.
Validation y esquemas
El asistente XML Data Binding Wizard puede trabajar a partir de esquemas ya
existentes o generar un esquema para un documento XML (e incluso guardarlo en
un archivo con la extension .XDB). Un documento XML describe algunos datos,
per0 para compartir estos datos entre empresas, tiene que adherirse a alguna
estructura previamente acordada. Un esquema es una definicion de documento
contra la que se puede comprobar la correccion de un documento, una operacion
que suele llamarse validacion.
El primer (y mas difundido) tipo de validacion disponible para XML usaba las
definiciones de tipo de documento (Document Type Definitions, DTD). Estos
documentos describen la estructura del XML per0 no pueden definir 10s posibles
contenidos de cada nodo. Ademas, 10s DTD no son documentos XML ellos mismo, sino que usan una notacion diferente y algo extraiia.
A finales del aiio 2000, el W3c aprobo el primer borrador oficial de 10s esquemas XML ya disponibles en una version incompatible llamada XML-Data dentro
del DOM de Microsoft). Un esquema XML es un documento XML que puede
validar tanto la estructura del arb01 XML como el contenido de 10s nodos. Un
esquema se basa en el uso y la definicion de tipos de datos simples y complejos, de
un mod0 parecido a un lenguaje orientado a objetos.
Un esquema define tipos complejos, indicando cada uno de 10s nodos posibles,
su secuencia opcional ( s e q u e n c e , a l l ) , el numero de ocurrencias de cada
subnodo ( m i n o c c u r s , m a x 0 c c u r s ) y el tipo de datos de cada elemento especifico. Este es el esquema definido por el XML Data Binding Wizard para el
archivo de libros de muestra:
Los motores DOM de Microsoft y Apache tienen un buen soporte para 10s
esquemas. Otra herramienta que hemos usado para la validation es XML Schema
Validator (XSV), un intento de codigo abierto de conseguir un procesador conforme con 10s esquemas, que puede usarse bien directamente a traves de la Web o
despues de descargar un ejecutable en linea de comandos (en las paginas sobre
XML Schema del W3C se encuentra el enlace a1 sitio Web actual de esta herra-
Es bastante comun utilizar una pila para manejar la ruta actual dentro del
arbol de nodos, y meter y sacar elementos enly desde la misma para cada evento
I
-
IVBSAXXMLReader;
protected
stack: TStringList;
public
constructor Create;
destructor Destroy; override;
/ / IDispa tch
function GetTypeInfoCount(out Count: Integer): HResult;
stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo):
HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
Namecount, LocaleID: Integer; DispIDs: Pointer) :
HResult; stdcall;
function Invoke(Disp1D: Integer; const IID: TGUID;
LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr:
Pointer) :
HResult; stdcall;
/ / IVBSAXContentHandler
procedure S e t ~ d o c u m e n t L o c a t o r ( c o n s tParaml: IVBSAXLocator);
virtual; safecall;
procedure startDocument; virtual; safecall;
procedure endDocument; virtual; safecall;
procedure startPrefixMapping(var strprefix: WideString;
var strURI : Widestring) ; virtual; safecall;
procedure endPrefixMapping(var strprefix: Widestring);
virtual; safecall;
procedure startElement(var strNamespaceUR1: WideString;
var strLocalName: WideString; var strQName: WideString;
const ~Attributes: IVBSAXAttributes); virtual; safecall;
procedure endElement(var strNamespaceUR1: WideString;
var strLocalName: WideString; var strQName: WideString);
virtual; safecall;
procedure characters(var strchars: WideString); virtual;
safecall ;
procedure ignorableWhitespace(var strchars: WideString);
virtual; safecall;
procedure processingInstruction(var strTarget: WideString;
var strData: WideString); virtual; safecall;
procedure skippedEntity(var strName: WideString);
virtual; safecall;
end :
La parte mas interesante es la lista final de 10s eventos SAX. Todo lo que hace
esta clase basica es enviar la informacion a un registro cuando el analizador
sintactico empieza (startDocument) y finaliza (endDocument) y hace el
seguimiento de 10s nodos actuales y 10s nodos padre con una pila:
/ / TMySaxHandler. s tartElement
stack .Add (strLocalName);
// TMySaxHandler. endEl ement
stack .Delete (stack.Count - 1) ;
(strLocalName +
') ') ;
List
(
'
Paw TkJw
pam~w
'
strchars:
I-
s ~ a l ~ ~ o c l m--ent
bwkslbooksl
book(books.book]
lkle(books.book.tille]
Texl: La B~bhade Delphi 7
author~books.book.auIhar1
booklbooks,book]
blle(books,book,liUe]
Text DelphlDeveloper's Handbook
author(books,bo&.author]
Ten!: Canlu
au(hor(books.book.au(hor1
Text: Gooch
bwk[books.book]
!~tle[books,book,l~Ile]
Texl. La Bibl~ade Delphi 6
aulhor(books,book,aulhor]
Text. Canlu
bmk[books,bwk]
Mle(bwks.book,lillej
Ted: DelphiCOM Programrring
aulho@ooks.book,auIhor]
Texl Herrnon
bwk(books,book]
title(books.book.lille)
Text Thlnkmg in Ctt
aulhor[bwks.bodLw(hal
Text: Edtel
Figura 22.6. El registro generado por la lectura de un documento XML con SAX en el
ejernplo SaxDemol.
Tmrh-mmon E w ~ ~ o ~ m d c v ~ u ~ l - ~ B d r ( , d a a
lrri~d
Figura 22.7. El XML Mapper muestra 10s dos extremos de una transforrnac~onpara
definir la proyeccion entre ellos (con las reglas indicadas en la parte central).
o b j e c t ClientDataSet2: TClientDataSet
DataSetField = ClientDataSetlbook
end
o b j e c t ClientDataSet3: TClientDataSet
DataSetField = ClientDataSetlebook
end
Harmon
Thinkinp m C t t
La Bbl~ade Delph 7
Btuce
Can(u
hltg//ww.ma~cocantu.~m Cantu
II
Este programa no solo permite editar 10s datos de las diferentes sublistas de
nodos dentro de las cuadriculas, sin0 tambien modificarlos, borrarlos o aiiadir
nuevos registros. Cuando aplicamos 10s cambios al conjunto de datos (haciendo
clic sobre el boton Save, que llama a ~pplyupdates),el proveedor de transformaciones guarda una version actualizada del archivo en el disco. Como metodo alternativo, tambien podemos crear transformaciones que proyecten solo
determinadas partes del documento XML sobre un conjunto de datos. Como ejemplo, puede consultarse el archivo Booksonly .x t r que se encuentra en la carpeta del ejemplo XmlMapping. El documento XML modificado que generara tendra
una estructura y contenido distintos del original, incluyendo solo la parte que se
ha seleccionado. Por eso, puede ser util para ver 10s datos per0 no para editarlos.
Una transformacion puede utilizarse para coger una tabla de una base de datos
o el resultado de una consulta y producir un archivo XML con un formato mas
En el siguiente cuadro de dialogo, aceptamos las proyecciones predeterminadas de 10s nombres para 10s campos y solo cambiamos el nombre sugerido para
10s nodos de registro (ROW) por algo mas legible (Customer). Si probamos ahora
la transformacion, el XML Mapper mostrara el contenido del documento XML
resultante en una vista de arb01 personalizada
Una vez que hemos guardado el archivo de transformacion, podemos reanudar
el desarrollo del programa, eliminando el ClientDataSet y aiiadiendo un Datasource
y una DBGrid (para que un usuario pueda editar 10s datos en una DBGrid antes
de transformarlos), y un componente XMLTransformClient. Este componente
tiene conectado el archivo de transformacion, per0 no un archivo XML. En lugar
de eso, hace referencia a 10s datos a traves del proveedor. A1 hacer clic sobre el
boton, veremos el documento XML dentro de un campo de memo (despues de
darle formato) en lugar de guardarlo en un archivo, algo que podemos hacer
llamando a1 metodo GetDataAsXml (aunque el archivo de ayuda no resulta
muy claro sobre el uso de este metodo):
procedure TForrnl.btnMapClick(Sender: TObject);
begin
Mernol.Lines.Text : =
ForrnatXmlData(XMLTransf0rmC1ientl.GetDataAsXml("));
end;
Este es el unico codigo del programa que podemos ver en tiempo de ejecucion
en la figura 22.9. El conjunto de datos original puede verse en la DBGrid, y el
documento XML resultante en el control de memo que se encuentra bajo la cuadricula. La aplicacion dispone de un codigo mucho mas sencillo que el que hemos
utilizado en el ejemplo DomCreate para generar un documento XML parecido,
per0 requiere la definition de la transformacion en tiempo de diseiio. El ejemplo
DomCreate podria trabajar en tiempo de ejecucion sobre cualquier conjunto de
datos, sin necesidad de una conexion a una tabla especifica ya es un codigo
bastante generico.
En teoria, podemos producir proyecciones dinamicas similares utilizando 10s
eventos del componente generico XMLTransform, per0 parece mas sencillo usar
el enfoque basado en DOM ya comentado. Ademas, la llamada a FormatXmlData produce una salida mas agradable per0 ralentiza el programa, ya que implica la carga del XML en un DOM.
1231 Unirco
1351 S~ghlD i w
1354 Cayman Diverr World Llnhded
1356 Tom Swyer D i n g Cedre
1 3 0 Blw Jack Aqua Center
1384 VIP Divers Club
..............
.........
Suhe 310
..
PO BoxZ-547
1 Neptune Lane
PO Box 541
632.1 T hid Frydenhq
23-73 PaddnglonL a m
32 Main St.
.-...
.......
........,...Map
...
-,
..........
.-
....
..
- -:
O x m l vefs~on="l0"b
< Docwnent~
<Cuttomef>
~Custl~lo>t221
;ICustNo)
<Compary)Kaua~D~veShoppe</Cornpany>
cAddrl>4.976 Sugarloaf Hwyc/Addrl>
cAd&2>Smte 103</Addr2>
Figura 22.9. El ejemplo MapTable genera un documento XML a partir de una tabla de
base de datos rnediante un archivo de transforrnacion personalizado.
El componente XMLBroker
Internet Express utiliza diversas tecnologias para conseguir este resultado.
Convierte 10s paquetes de datos DataSnap a1 formato XML para que el programa
pueda insertar estos datos en la pagina HTML para su manipulation Web en el
cliente. Realmente, el paquete de datos delta tambien se representa como XML.
El componente XMLBroker lleva a cabo estas operaciones, maneja el XML y
proporciona 10s datos a 10s nuevos componentes JavaScript. A1 igual que el
ClientDataSet, el XMLBroker tiene:
Una propiedad MaxRecords: Sirve para indicar el numero de registros
que se aiiaden a una sola pagina.
Una propiedad Params: Se utiliza para albergar 10s parametros que 10s
componentes reenviaran a la consulta remota a traves del proveedor.
Una propiedad WebDispatch: Sirve para indicar la consulta actualizada
a la que responde el broker.
--
-,
-.
~ s t o cornponent&~tienen
s
las propiedyad8- st yi&-e$
~ i i para
e
definir el CSS y cada elemento visual time una propiedad
3
t
y
l
e
Rule
que puede usarse para seleccioaar el nombre del &lo.
-
'
Soporte de JavaScript
Para producir potentes operaciones de edicion en el lado del cliente, el
InetXPageProducer utiliza un codigo y unos componentes JavaScript especiales.
Delphi incluye una biblioteca bastante extensa de JavaScript, que el navegador
tiene que descargar. Es un proceso algo fastidioso, per0 es la unica manera de que
la interfaz del navegador (que se basa en codigo HTML dinamico), sea lo suficientemente buena como para soportar restricciones de campos y otras reglas de
negocio similares.
Esto seria totalmente imposible con el HTML simple. Los archivos de JavaScript
proporcionados por Borland, que deberian hacerse disponibles en la pagina Web
que albergue la aplicacion, son 10s siguientes:
Xmldom. j s
I x m l d b . js
Xm1disp.j~
Xrnlerrdisp js
Xrn1Show.j~
Funciones JavaScript para mostrar datos y paquetes delta (cuya finalidad es la depuraci6n).
..."
Creacion de un ejemplo
Para entender mejor de que hablamos, y como forma de comentar algunos detalles mas tecnicos, vamos a probar sencillo ejemplo llamado IeFirst. Para evitar
problemas de configuracion, esta es una aplicacion CGI que accede directamente a
un conjunto de datos (en este caso a una tabla local conseguida mediante un componente C l i e n t D a t a S e t ) . Mas tarde veremos como convertir un cliente DataSnap
de Windows ya existente en una interfaz que basada en un navegador. Para crear
IeFirst, hemos creado una nueva aplicacion CGI y afiadido a su modulo de datos un
componente C l i e n t Dataset conectado con un archivo .CDS local y un componente D a t a S e t P r o v i d e r conectado con el conjunto de datos. El paso siguiente
es aiiadir un componente XMLBroker y conectarlo con el proveedor:
object ClientDataSetl: TClientDataSet
FileName = 'C: \Archives d e programa \Archives comunes \Borland
Shared\Data \employee.cds '
end
o b j e c t DataSetProviderl: TDataSetProvider
DataSet = ClientDataSetl
end
o b j e c t XMLBrokerl: TXMLBroker
<#INCLUDES>
<#STYLES>
<#WARNINGS>
<#FORMS>
---
.
.
A
- - - -
--
.I
I.
--
.
,
1
'
*
.-
Para personalizar el HTML resultante dcl InetXPageProducer, podemos utilizar su editor. que vuelve a scr parecido a1 editor de guiones de servidor de WebSnap.
Haciendo dobIe clic sobre el componente InetXPageProducer,Delphi abre
una x n t a n a como la que muestra la figura 22.10 (con la configuracion final del
ejemplo).
En este editor podemos crear estructuras complejas partiendo de un formulario
de consulta. un formulario de datos o un grupo generic0 de estructura. En el
formulario de datos de nuestro ejemplo, hemos aiiadido dos componentes
DataGr id y DataNavigator sin personalizarlos (operacion que se puede
llevar a cabo aiiadiendo botones hijo, columnas y otros objetos que sustituyan
completamente a 10s predeterminados).
at?,*
-
InelXPagePraducerl ErnpFlo
DataForrnl
LaslNarne
DalaNavqalor F1r~tNarne
E:
Salary
SlalusColurnnl
object
end
o b j e c t DataGridl: TDataGrid
XMLBroker = XMLBrokerl
DisplayRows = 5
TableAttributes.BgCo1or = 'Silver'
TableAttributes.CellSpacing = 0
TableAttributes.Cel1Padding = 2
HeadingAttributes.BgCo1or = 'Aqua'
o b j e c t EmpNo: TTextColumn...
o b j e c t LastName: TTextColumn. . .
o b j e c t FirstName: TTextColumn...
o b j e c t PhoneExt : TTextColumn. .
o b j e c t HireDate: TTextColumn. ..
o b j e c t Salary: TTextColumn.. .
o b j e c t StatusColumnl: TStatusColumn...
end
end
end
...
/ / titulo de cuadricula de datos
<tr>
/ / u n a celda d e datos
<td><div>
<input type="textW name="EmpNo" size="lO"
onfocus='if(xml~ready)DataGridlonfocus='ifoDataGridl_Disp.xfoDi~p.~focus(this);'
onkeydown='if (xml-ready) D
a
tG
r
i
d
l
o
n
k
e
y
d
o
w
n
=
'
i
f
o
D
a
tG
r
i
d
l
_
Ds
p
.
k
D
i
~
p
.
keys (this); I >
</div></td>
...
programs
lelson
IRoberio
.
'oung
--
l~ruce
:uao
A1 mismo tiempo, las clases JavaScript del sistema permiten que el usuario
introduzca datos nuevos, siguiendo las reglas impuestas por el codigo JavaScript
que conectado a 10s eventos HTML dinamicos. De manera predeterminada, la
cuadricula tiene una columna adicional con un asterisco que indica que registros
se han modificado. Los datos de actualizacion se agrupan en un paquete de datos
XML en el navegador y se envian de vuelta al sewidor cuando el usuario hace clic
sobre el boton Apply Updates. A partir de aqui, el navegador activa la accion
especificada por la propiedad WebDispat h . Pat hInf o del XMLBroker. No
hay necesidad de exportar esta accion desde el modulo de datos Web, ya que es
una operacion automatica (aunque podemos desactivarla si establecemos
WebDispath. Enable como False).
El XMLBroker aplica 10s cambios al servidor, devolviendo el contenido a1
proveedor conectado a la propiedad Re conc i 1e Provider (o lanzando una
excepcion si esta propiedad no esta definida). Si todo funciona bien, el XMLBroker
redirige el control a la pagina principal que contiene 10s datos. Sin embargo,
hemos experimentado algunos problemas con esta tecnica, por lo que el ejemplo
IeFirst controla el evento OnGetResponse, indicando que se trata de una vista
actualizada:
procedure TWebModulel.XMLBrokerlGetResponse(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; v a r Handled:
Boolean) ;
begin
Response .Content : = ' < h l > U p d a t e d < / h l > < p >' +
1netXPageProducerl.Content;
Handled : = True;
end;
Uso de XSLT
Otra posibilidad para generar un codigo HTML partiendo de un documento
XML es usar el lenguaje de hojas de estilo extensible (Extensible Stylesheet
Language, XSL) o, para ser mas precisos, su subconjunto XSL Transformations
(XSLT).
El objetivo de de XSLT es transformar un documento XML en otro documento, generalmente un documento XML. Uno de 10s usos mas frecuentes de la tecnologia es convertir un documento XML en un documento XHTML para enviarlo a
un navegador desde un servidor Web. Otra interesante tecnologia relacionada es
XSL-FO (XSL Formatting Objects), que puede usarse para convertir un documento XML en un documento PDF u otro tipo de documento con formato.
Un documento XSLT es un documento XML bien formado. La estructura de
un archivo XSLT necesita un nodo raiz como el siguiente:
El punto de partida para esta operacion es una plantilla que procesa el nodo
raiz, que puede ser la unica plantilla del archivo XSLT. En las plantillas, se puede
encontrar cualquier otro comando, como la extraccion de un valor desde un documento XML (xsl :value-of select), sentencias de bucle (xsl : for-each),
expresiones condicionales (xs1 : if,xs 1 :choose), peticiones de ordenacion
(xs1 : sort) y peticiones de numeracion (xsl : number) por mencionar solo
algunos comandos XSLT comunes.
Uso de XPath
XSLT usa otras tecnologias XML, sobre todo XPath para identificar partes de
documentos. XPath define un conjunto de reglas para encontrar uno o mas nodos
dentro de un documento. Estas reglas se basan en una estructura de lineas de ruta
del nodo dentro del arb01 XML. De esta manera, la /books/book identifica
cualquier nodo book bajo la raiz de documento books.XPath utiliza unos simbo10s especiales para identificar a 10s nodos:
Un asterisco (*) significa cualquier nodo; por ejemplo book/* indica
cualquier subnodo bajo el nodo book.
Un punto ( .) significa el nodo actual.
El simbolo de barra
XML origen y que genera como salida otro documento XML, como un documento
XHTML que podemos ver en un navegador Web.
XSLT en la practica
Vamos a analizar un par de ejemplos. Como punto de partida, deberiamos
estudiar el propio estandar XSL y centrarnos en su activacion desde una aplicacion Delphi.
Como prueba inicial, podemos conectar directamente un archivo XSL con un
a r c h i ~ ~XML.
o
Cuando cargamos en archivo XML en lnternet Explorer veremos
la transformacion en XHTML resultante. La conexion se indica en la cabecera del
documento XML con un comando como el siguiente:
Esto es lo que hemos hecho en el archivo samplelembedded . xml disponible en la carpeta XslEmbed.El XSL relacionado incluye varios fragmentos de
XSL que no tenemos espacio para comentar en detalle. Por ejemplo, coge la lista
completa de autores o filtra un grupo especifico de ellos con el siguiente codigo:
Se usa codigo mas complejo para extraer nodos solo cuando se encuentra
presente un valor especifico en un subnodo o atributo, sin tener en cuenta 10s
nodos de mayor nivel. El siguiente fragment0 de XSL tambien tiene una sentencia
i f y produce un atributo en el nodo resultante, como un mod0 de crear un
hipervinculo href en el codigo HTML:
< h 3 > M a r c o 1 s works
<ul>
(books
+ ebooks)</h3>
txs1:template match="ROWDATA/ROW">
txs1:variable name="fieldDefs" select="//METADATA/FIELDS"/>
<xsl:variable name="currentRow" select="current()"/>
<tr>
<xsl:for-each select="$fieldDefs/FIELD">
<td>
<xsl:value-of select="$currentRow/
@ *[ n a m e ( ) =current ( ) /@attrname] " / > < b r / >
</td>
</xsl:for-each>
</tr>
</xsl:template>
--.
- -
GXLGLISIULI a 1
c;uu~gu
A
tabla, con una celda < t h > para cada entrada de una sola fila. Los datos de fila se
usan para rellenar el resto de las filas de la tabla. No basta con tomar el valor de
cada atributo (select=" @ * "); ya que 10s atributos pueden no existir. Por este
motivo, la lista de campos y la fila actual se guardan en dos variables: despues,
para cada campo, el codigo XSL extrae el valor de un elemento de fila que tenga
un nombre de atributo (@* [name( ) = . .) que se corresponda con el nombre
del campo actual guardado en su atributo attrnnme (eattrname). Este codigo
no resulta nada sencillo, per0 es un mod0 compacto y adaptable para analizar
divcrsas partes de un documento XML a1 mismo tiempo.
j Procedures
.JUser
Tom
--
39 1
Insert
~ H T M ~ ~ P ~ Tree,&~~
~ ~ ~Tree/
~ , $ M. L
procedure
TOb j ect;
TWebModulel.WebModulelWebActionItemlAction(Sender:
ClientDataSetl.0pen;
XmlDom.Xml.Text : = ClientDataSetl.XMLData;
XmlDom.Active := True;
// carga el archivo xsl solicitado
xslfile : = Request.QueryFields.Va1ues ['style'];
i f xslfile = " then
xslfile : = 'customer.xsl ';
xslfolder : = ExtractFilePath (ParamStr (0)) + 'xsl\';
i f FileExists (xslfolder + xslfile) then
xslDom.LoadFromFile (xslfolder + xslfile)
else
r a i s e Exception.Create('Missing
file: '
xslfolder +
xslfile) ;
XSLDom.Active : = True;
i f xslfile = 'single.xsl ' then
begin
attr : = xslDom.DOMDocument.createAttribute('select');
attr.value : = '//ROW[@CustNo="'
+
Request. QueryFields .Values [ 'id'] + ' " I ';
xslDom.DOMDocument.getElementsByTagName ('xs1:applytemplates ' ) .
item[O].attributes.setNamedItem(attr);
end;
/ / redliza la transformation
HTMLDom.Active : = True;
xmlDom.DocumentElement.transformNode
(xslDom.DocumentElement, HTMLDom);
Response.Content
:=
HTMLDom.XML.Text;
end:
ADVERTENCIA: Para ejecutar este programa, hay que colocar 10s archivos XSL en una carpeta llamada XSL dentro de aquella en que se encuentre
la Carpeta de 10s guiones de este capitdo. Para desplegar e m s archivos en
g que'e&e
l .
el dombn
' I .
for i : = 0 to ClientDataSetl.Fie1dCount - 1 do
s : = s + MakeXmlstr
(ClientDataSetl.Fields [i] . FieldName,
ClientDataSetl.Fields[i].AsString);
s : = MakeXmlStr ( 'employeeData ', s) ;
str-Write(s[1] , length (s)) ;
ClientDataSet1.Next
end;
s : = '</employee>';
str.Write (s[1] , length (s)) ;
finally
str . Free;
end;
end;
Este codigo utiliza una funcion auxiliar simple per0 eficaz para crear 10s nodos
XML:
function MakeXmlstr (node, value: string) : string;
begin
Result : = ' < I + node + ' > I + value + I < / ' + node +
end :
' > I ;
Si se ejecuta el programa, se podra ver el tiempo que se tarda en cada operacion, como lo muestra la figura 22.13. Guardar 10s datos del ClientDataSet es el
enfoque mas rapido, per0 probablemente no se consiga el efecto deseado. El
streaming personalizado es solo ligeramente mas lento, per0 deberia considerarse
que este codigo no necesita que 10s datos se lleven en primer lugar a un
ClientDataSet, porque se puede aplicar directamente, incluso en un conjunto de
datos unidireccional de dbExpress. Deberiamos olvidarnos de utilizar el codigo
313 Robert
--
4747 LesLe
332
G Ion
937
6716
161 7 Janet
'
Nelson
!-
5 -
Bddwn
s-'
,7
-
__ 1410 -
Johnson
h",bM
'2-L.
+I
aqui solo vamos a mostrar el codigo de 10s controladores que se utilizan. Como
puede verse, a1 comienzo de un ejemplo employeeData se inserta un nuevo registro, que se envia cuando se cierra el mismo nodo. Los nodos de menor nivel se
aiiaden como campos al registro actual. Este es el codigo:
procedure TMyDataSaxHandler.startElement(var strNamespaceUR1,
strLocalName,
strQName: WideString; const ~Attributes: IVBSAXAttributes);
begin
inherited;
i f strLocalName = 'employeeDatat then
Forml.clientdataset2.Insert;
strcurrent : = ' I ;
end;
procedure TMyDataSaxHandler.characters(var strchars:
WideString) ;
begin
inherited;
strcurrent : = strcurrent + Removewhites (strchars);
end;
procedure TMyDataSaxHandler.endElement(var strNamespaceUR1,
strLocalName,
strQName : WideString) ;
begin
i f strLocalName = employeeData ' then
Forml.clientdataset2.Post;
if stack.Count > 2 then
Forml.ClientDataSet2.FieldByName (strLocalName).Asstring
.= strcurrent;
inherited;
end;
El codigo para 10s controladores de eventos en la version OpenXml es parecido. Todo lo que cambia son las interfaces de 10s metodos y 10s nombres de 10s
parametros:
type
TDataSaxHandler = class (TXmlStandardHandler)
protected
stack: TStringList;
strcurrent: string;
public
constructor Create(aowner: TComponent); override;
function endElement(const sender: TXmlCustomProcessorAgent;
const locator: TdomStandardLocator;
namespaceUR1, tagName: widestring): TXmlParserError;
override;
function PCDATA(const sender: TXmlCustomProcessorAgent;
const locator: TdomStandardLocator; data: widestring):
TXmlParserError; override;
f u n c t i o n s t a r t E l e m e n t ( c o n s t sender: TXmlCustomProcessorAgent;
const locator: TdomStandardLocator; namespaceURI,
t a g N a m e : widestring;
attributes: TdomNameValueList) : TXmlParserError; override;
d e s t r u c t o r Destroy; override;
end;
Servicios
Web y SOAP
De entre todas las caracteristicas mas recientes de Delphi, una sobresale por
encima de todas: el soporte para servicios Web incluido en el producto. El hecho
de que lo tratemos hacia el final del libro no tiene nada que ver con su importancia, sino solamente con el logico desarrollo del texto y con el hecho de que no es el
mejor punto de partida para comprender la programacion en Delphi.
El tema de 10s servicios Web es muy amplio e implica varias tecnologias y
estindares de negocio. Como siempre, nos centraremos en la implementacion subyacente de Delphi y en la parte tecnica de 10s servicios Web, en lugar de analizar
el panorama global y las implicaciones empresariales.
Este capitulo tambien es importante porque Delphi 7 aiiade una gran cantidad
de potencia a la implementacion de servicios Web que ofrecia Delphi 6, incluyendo soporte para adjuntos, cabeceras personalizadas y muchas cosas mas. Veremos como crear un cliente y un servidor de servicios Web, y tambien como
transportar datos de una base de datos sobre SOAP empleando la tecnologia
DataSnap. Este capitulo trata 10s siguientes temas:
Servicios Web.
SOAPyWSDL.
DataSnap sobre SOAP
Manejo de adjuntos.
UDDI.
Servicios Web
Esta tecnologia en rapida expansion tiene el potencial de cambiar la forma en
la que Internet funciona para las empresas. Explorar una pagina Web para hacer
un pedido esta bien para un unico usuario (las conocidas aplicaciones B2C o
aplicaciones de empresa a consumidor), pero no para una empresa (las aplicaciones B2B de empresa a empresa). Si queremos comprar unos cuantos libros, visitar el sitio Web de un vendedor de libros y escribir nuestros pedidos estara bien,
pero si nuestro negocio es una libreria y queremos hacer cientos de pedidos a1 dia,
este metodo esta lejos de ser el mas adecuado, en particular si tenemos un programa que nos ayude a hacer el seguimiento de ventas y determinar nuevos pedidos.
Seria ridiculo tomar la salida de este programa y volver a escribirla en otra aplicacion.
La idea de 10s s e ~ i c i o Web
s
es solventar este problema: el programa utilizado
para el seguimiento de ventas, puede crear automaticamente una consulta y enviarla a un servicio Web, el cual, devuelve inmediatamente la informacion sobre
el pedido. El siguiente paso podria ser una consulta sobre el numero de envio. A
continuacion, el programa puede utilizar otro servicio Web para hacer el seguimiento del envio hasta que llegue a su destino y asi poder decirle a1 cliente cuanto
tardara en llegar. Cuando llegue el envio, el programa puede enviar una notificacion SMS o mediante un busca a las personas que tengan solicitudes pendientes,
emitir una orden de pago con el servicio Web de un banco, ... y podriamos continuar pero creemos que ya se ha captado la idea. Los servicios Web estan pensados para la interoperabilidad informatics a1 igual que la Web y el correo electronico,
estan pensados para la comunicacion entre personas.
SOAP y WSDL
Los servicios Web son posibles gracias a1 Simple Object Access Protocol
(SOAP). Creado sobre el protocolo HTTP estandar para que un servidor Web
pueda manejar las consultas SOAP y que 10s paquetes de datos puedan pasar a
traves de cortafuegos. SOAP define una notacion basada en XML para solicitar
la ejecucion de un metodo por parte de un objeto en el servidor, pasandole 10s
parametros. Mediante otra notacion se define el formato de la respuesta.
ROTA: SOAP h e desarrollado originalmente por DevelopMentor (la cornpafiia de entrenamiento de Don Box, el experto en COM)y Microsofi, para
superar la debilidad del uso de DCOM en s e ~ d o r e Web.
s
Sometido a1
W para
f su es
C
n
i
m u c h &qxis&~
lo .Mgieron. co.aparticular empujc de IBM. Es muy pronto pQa aaber si se producid una
eatandarizacih real para que 10s programas de software de Miqrosoft, IBM,
Sun, Oracle y muchos otros interaden realmeate o si algunas de cstas
marcas tratarh de promover una version privada del esthdar. En cualquier caso, SOAP es s61o una de Ias piedras angulares de la arquiWhm
.NETde Microsoft, asi como de las platafonnas aduales de Sun y Omclc.
Traducciones BabelFish
Como primer ejemplo del uso de un servicio Web, vamos a crear un cliente
para el servicio de traduccion BabelFish ofrecido por AltaVista. Se puede encontrar este y muchos otros servicios para su experimentacion en el sitio Web de
XMethods (~vw.xmethods.com).
Despues de descargar la descripcion WSDL de este servicio del sitio de
XMethods (disponible tambien entre 10s archivos de codigo fuente para este capitulo), hemos invocado a1 Web Services Importer de Delphi en la pagina Web
Services del cuadro de dialog0 New Items y hemos seleccionado el archivo. El
asistente permite acceder a una vista previa de la estructura del servicio (vCase la
figura 23.1) y generar las interfaces en lenguaje Delphi apropiadas en una unidad
como la siguiente (con muchos de 10s comentarios eliminados):
klsDL Conocrmnlx
E8
. Bab$F~shSewm
I'(DZDB671Z-BBBO-lDA6-8DBC-8A445595ABOC)'l
function BabelF~sh(const trmslatlonrodo: Wldel
end;
f m c c x o n CetBabelFlsMortType(UseWSDL- Boolean-Systs
- - .- - -
--
lil
--An-
--
// omitido
initialization
InvRegistry.RegisterInterface(TypeInfo(BabelFishPortType),
' urn:xmethodsBableFish ', ' ') ;
1nvRegistry.RegisterDefaultSOAPAction
(TypeInfo (BabelFishPortType),
' urn:xmethodsBableFish#Babe1Fish ') ;
end.
sino que se compila con el indicador utilizado para establecer la generacion RTTI,
( $M+}, como la clase T P e r s i s t e n t . En la seccion de inicializacion puede
verse tambien que la interfaz se registra en el registro de invocacion global (o
I nvReg i s t r y), pasando la referencia de informacion de tipo del tipo de interfaz.
NOTA: Dis-r
de info@n
pan$ mtmfaccs q m&mpte d
avance tecd&gicq &I imparfslnte relacionado-conla i ~ c a c i l i t SOAP.
l
No es que proyea$h de ~ C l h .PGcal
6
no'sea M p b m e (es v
i
a paf'd
simplificar el prowso) sino que' d$sponex d.e. i n t k m n a d b RTfl para ma
interfaz es lo que realmente hace t p e la arquite&ra sea pdente .j;r~pusta.
El tercer elemento de la unidad generada por el WDSL Import Wizard es una
funcion global que toma su nombre del servicio, introducida en Delphi 7. Esta
funcion ayuda a simplificar el codigo utilizado para llamar a1 servicio Web. La
funcion G e t B a b e l F i s h P o r t T y p e devuelve una interfaz del tipo apropiado,
que puede usarse para lanzar directamente una Ilamada. Por ejemplo, el siguiente
codigo traduce una breve frase de ingles a italiano (corno indica el valor de su.
primer parametro, e n -i t ) y la muestra en pantalla:
ShowMessage
w o r l d ! ' )) ;
'Hello,
Llegados a este punto, poco mas hay que hacer ya. Tenemos informacion sobre
el servicio que podemos usar para invocarlo y conocemos 10s tipos de 10s parametros
requeridos por el unico metodo disponible tal y como se indican en la interfaz.
Los dos elementos se unen extrayendo la interfaz a que queremos llamar directamente desde el componente HTTPRIO, con una expresion como HTTPRIOl a s
Babe 1Fi s hPor tTy p e . Puede parecer sorprendente, per0 es increiblemente
simple.
Esta es la llamada a1 servicio Web realizada por el ejemplo:
EditTarget.Text : = (HTTPRIO1 as BabelFishPortType).
BabelFish(ComboBoxType.Text, Editsource-Text);
La salida del programa, como muestra la figura 23.2, permite aprender idiomas (aunque, en este caso, el profesor tiene algunas limitaciones, claro). No hemos repetido este mismo ejemplo con opciones de compra, divisas, pronosticos
del tiempo y muchos otros servicios disponibles, porque tendrian todos un aspect0
muy parecido.
pzq
len_de
.--.-.__I
Dud
las pruebas iniciales. Despues de completar este paso, Delphi aiiadira tres componentes a1 modulo Web resultante, que no es mas que un modulo Web basico sin
adiciones especiales:
El componente HTTPSoapDispatcher recibe la consulta Web como
lo haria cualquier otro repartidor HTTP.
El componente HTT PSoapPasca 1Invo ker realiza la operacion inversa a la del componente HTTPRIO: es capaz de traducir consultas SOAP en
llamadas para interfaces Pascal (en lugar de convertir las llamadas a metodos de la interfaz en consultas SOAP).
El componente WS DLHTMLPub1ish puede usarse para extraer la definicion WSDL del servicio a partir de las interfaces que soporta, y realiza el
papel contrario a1 del Web Services Importer Wizard. Tecnicamente, se
trata de otro repartidor HTTP.
InvokeRegistry;
initialization
InvRegistry.RegisterInterface(TypeInfo(1Convert));
Ahora que disponemos de una interfaz que podemos mostrar a1 publico, tenemos que proporcionarle una implementacion. Para ello utilizaremos, una vez mas,
el codigo estandar de Delphi (con la ayuda de la clase TInvo kableclass
predefinida):
type
TConvert = c l a s s (TInvokableClass, IConvert)
protected
f u n c t i o n ConvertCurrency (Source, Dest: string; Amount:
Double) : Double;
stdcall;
f u n c t i o n T o E u r o (Source : string; Amount: Double) : Double;
s tdcall;
f u n c t i o n F r o m E u r o (Dest: string; Amount: Double) : Double;
stdcall;
f u n c t i o n TypesList: string; stdcall;
end;
(TConvert);
PortTypes:
Iconvert [WSDL]
0
Convertcurrency
ToEuro
FromEuro
TypesLtst
IWSDLPublish [.=I
LI-t; ,711 !h* P 0 r t P p ~ 5p ~ ~ b l ~ b
~ yhl eb dl Servlre
~
WSIL:
GetPortTypeLirt
GetWSDLForPortType
GetTypeSystemsList
GetXSDForTypeSystem
Request,
Response,
Esto es todo lo que hay que hacer para proporcionar esta caracteristica a un
servicio Web de Delphi ya esistente que carezca de ella. Para conseguir de forma
manual una prestacion similar, hay que llamar a1 registro de invocacion (el objeto
global I n v R e g i s t r y ) , con llamadas comoGet I n t e r f a c e E x t e r n a l N a m e
y G e t M e t h E x t e r n a l N a m e . Lo que es importante es la capacidad del servicio
Web de autodocumentarse para cualquier otro programador o herramienta de
programacion, presentando el WSDL.
Como una alternativa al uso del un archivo WSDL, el componente que invoca
a SOAP puede asociarse con un URL. Una vez que se ha realizado esta asociacion y la interfaz necesaria se ha extraido del componente, podemos empezar a
escribir el codigo Pascal para invocar a1 servicio, como hemos visto anteriormente. Un usuario puede rellenar 10s dos cuadros combinados, llamando a1 metodo
TypesList,que devuelve una lista de las monedas disponibles dentro de una
cadena (separada por puntos y coma). Extraeremos esta lista sustituyendo cada
punto y coma por un caracter de nueva linea y asignando despues directamente la
cadena multilinea a 10s elementos combinados:
procedure TForml.Button2Click(Sender: TObject);
var
TypeNames: string;
begin
TypeNames : = ConvIntf.TypesList;
ComboBoxFrom.1tems.Text : = StringReplace (TypeNames,
sLineBreak, [rfReplaceAll]) ;
ComboBoxTo. Items : = ComboBoxFrom. Items;
end :
I ; ' ,
-[
"z
Fill List
DEM
I
=I
Figura 23.4. El cliente Convertcaller del servicio Web Convertservice rnuestra 10s
rnarcos alemanes necesarios para conseguir muchisimas liras italianas, antes
de que el euro lo cambiara todo.
POCOS
El primer metodo devuelve una lista de 10s nombres de todos 10s empleados de
la empresa, y el segundo devuelve 10s detalles de un empleado determinado. La
implementacion de esta interfaz se proporciona en la unidad SoapEmployeeImpl
con la clase siguiente:
type
TSoapEmployee = class(TInvokableClass, ISoapEmployee )
public
function GetEmployeeNames: string; stdcall;
function GetEmployeeData (EmpID: string) : string; s tdcall;
end;
from
SQLConnection = SQLConnection
o b j e c t dsEmplListEMP-NO: TStringField
o b j e c t dsEmplListLAST-NAME: TStringField
o b j e c t dsEmplListFIRST-NAME: TStringField
end
o b j e c t dsEmpData: TSQLDataSet
ComrnandText = 'select * f r o m E M P L O Y E E where E m p N o = : i d f
Params = <
item
DataType = ftFixedChar
Name = 'id'
ParamType = ptInput
end>
SQLConnection = SQLConnection
end
end
Como puede verse, el modulo de datos tiene dos sentencias SQL en sendos
componentes SQLDataSet. La primera se utiliza para obtener el nombre e
identificador de cada empleado, y la segunda devuelve el conjunto de datos completo para un empleado dado.
dm.dsEmplListFIRSTNME.AsString,
MakeXmlAttribute ( 'id',
dm.dsEmplListEMPN0.AsString)) + sLineBreak;
dm.dsEmplList.Next;
end;
Result : = Result + ' < / e m p l o y e e L i s t > ';
finally
dm. Free;
end;
end;
En lugar de emplear la generacion manual de XML, podriamos haber empleado el XML Mapper o alguna otra tecnologia, per0 hemos preferido crear directamente XML en forma de cadenas. Usaremos el XML Mapper para procesar 10s
datos recibidos en el cliente
--
- --
- -
--
- -.- -
--
NOTA: Puede que se pregunte por qui crea el programa una nueva instancia del mMulo de datos cada vez. La parte negativa de este enfoaue es oue
6:1 programa establece cada vez un;a nueva conexion con I
:una operation bastante lenta); perco la pmte positiva es qu
-'----=- - - - --I--:
- - - el
-1
1
mln
nenwn
relmmnaan con
ilnn
ilna
anlicacihn milltihun -a r -s e eiecil------ ne
---- -r------tan concurrentemente dos peticiones a1 servicio Web, se puede utilizar una
conexiirn carnpartida a la base de datos, per0 hay que usar componentes de
. .
.
. .- .
conjunto ae aatos cusumos para a acceso a aatos. rwrmws aespmar ms
conjuntos be dams en el d&go & la fuudb y mantener &lo la conexi6n en
el modulo de datos, o tener un mbdulo de datos cornpartido global para la
conexion (usado por varias hebras) y una instancia especifica de un segundo modulo de datos albergado por 10s Gaqjuntos be datos para cada llamada
a m&odo.
2
--.d-
..--
---------a
.. .
. . - .,
-J---
. .
dm.dsEmpData.Open;
Result : = FieldsToXml
finally
dm. Free;
end;
end;
strXml: string;
begin
strXml : = Get1SoapEmployee.GetEmployeeNames;
strXML : =
XMLTransformProvider1.T~an~f0rmRead.Transfor~ML(strXml);
ClientDataSetl.XmlData : = strXml;
ClientDataSetl.0pen;
end ;
Nelson Robert
Young B w e
Jdnson Leslie
Forest Ph1
Weslon K. J.
Lee Te~ri
Hall Stewart
Young Katherine
Papadopoules Chris
Fisher Pete
De Sarra Ropr
El Web App Dcbugger podria no cstar siempre disponiblc, por eso otra tecnica
habitual es controlar 10s eventos del componente HTTPRIO, como hace el ejemplo BabelFishDebug. El formulario del programa time dos componentes de memo
en 10s que puede verse la peticion SOAP y la respuesta:
p r o c e d u r e TForml.HTTPRIO1BeforeExecute(const
String;
v a r SOAPRequest: W i d e s t r i n g ) ;
begin
MemoRequest.Text : = SoapRequest;
end:
MethodName:
p r o c e d u r e TForml.HTTPRIO1AfterExecute(const MethodName:
SOAPResponse: T S t r e a m ) ;
begin
S0APResponse.Position : = 0;
MemoResponse.Lines.LoadFromStream(S0APResponse);
end ;
String;
thd vewon="1.0"?)
t SOAP-ENV:Envelopemlns:S~P~ENV="hll~//schemas.mlsoap.ug/w~en~ebpe~~
m l n s xsd="hltp~//www w3.org12001 M L S c h e m a "
mlns HSI-'lafp //www w3 org/2001 /XMLSchemamstance"
xmlns SO~.ENC="htfp.//schemas.xmlsoap.o~g/soap/enc~n'~cSOAP~ENV:Body
SOAP-ENV encodiylStyle="http://a:hemas.Mnlmap org/soap/encod1ng/"~tNS1:GetErnployeeData
xmhs NS 1="run SoapEmployeelnlf.ISoapEmployee"~
tEmplD
wxtype-"xsd slrmg">ll c/EmplD>c/NSl GelEmployeeDala>t/SOAP.ENV Body)c/SOAP.ENV Envelope,
Figura 23.6. El registro HTTP del Web App Debugger incluye la peticion SOAP
a bajo nivel.
1. Crear una aplicacion de servicio Web o aiiadir 10s componentes relacionados a un proyecto WebBroker ya existente.
2. Definir una interfaz que herede de llnvokable y aiiadirle 10s metodos que
se desean hacer accesibles en el servicio Web (mediante la convencion de
llamada s tdcall). Los metodos seran parecidos a 10s de las clase que se
quiera hacer accesible.
3. Definir una nueva clase que herede de la clase que se desea exponer e
implementar la interfaz. Los metodos se implementaran llamando a 10s
metodos correspondientes de la clase base.
4. Escribir una metodo de creacion de un objeto de la clase de implernentacion
para cada vez que lo necesite una peticion SOAP.
Este ultimo paso es el mas complejo. Podria definirse una fabrica y registrarla
de esta manera:
procedure MyObjFactory
begin
Obj : = TMyImplClass.Create;
end ;
initialization
InvRegistry.RegisterInvokableClass(TMyImplClass, MyObjFactory);
Sin embargo, este codigo crea un objeto nuevo para cada llamada. Utilizar un
unico objeto global seria igual de malo: varios usuarios podrian tratar de usarlo,
y si el objeto tiene un estado o sus metodos no son concurrentes, podrian darse
problemas. Queda la necesidad de implementar algun tipo de control de sesion,
que es una variante del prob!,cma que teniamos con el primer servicio Web que se
conectaba a la base de datos.
DataSetProviderl: TDataSetProvider;
SQLConnectionl: TSQLConnection;
SQLDataSetl: TSQLDataSet;
public
f u n c t i o n GetRecordCount: Integer; stdcall;
end;
obj:
TObject);
initialization
InvRegistry.RegisterInvokab1eC1ass(
TSampleDataModule, TSampleDataModuleCreateInstance);
InvRegistry.RegisterInterface(TypeInfo(ISampleDataModu1e));
- -
- - -- -
Fijese en la ultima propiedad, Use SOAPAdap t er, que indica que trabajamos contra un servidor creado con Delphi 7. Como comparacion, el ejemplo
SoapDataClient (de Delphi 6), que utiliza un servidor creado con Delphi 6 y se ha
vuelto a compilar con Delphi 7, debe tener establecida esta propiedad como True.
Este valor obliga a1 programa a usar la interfaz IAppServer simple en lugar de
la nueva interfaz IAppServerSOAP.
Desde aqui, todo es como siempre: aiiadir un componente Cl ient Dat aSe t,
un Datasource y una DBGrid a1 programa, escoger el unico proveedor disponible
para el conjunto de datos cliente y conectar el resto. No es sorprendente que para
este ejemplo tan simple la aplicacion cliente tenga poco codigo personalizado:
una unica llamada para abrir la conexion cuando se hace clic sobre un boton
(para evitar errores de arranque) y una llamada Appl yUpdat e s para enviar 10s
cambios de vuelta a la base de datos.
per0 informar a1 usuario del numero de registros que aun no se han descargado
desde el servidor. El codigo del cliente para llamar a1 metodo se basa en un
componente HTTPRIO adicional:
p r o c e d u r e TFormSDC.Button3Click(Sender: TObject);
var
SoapData: ISampleDataModule;
begin
SoapData : = HttpRiol a s ISampleDataModule;
ShowMessage (IntToStr (SoapData.GetRecordCount));
end;
Manejo de adjuntos
Una de las caracteristicas mas importantes que Borland ha afiadido a Delphi 7
es el completo soporte de adjuntos SOAP. Los adjuntos en SOAP permiten enviar
datos que no Sean texto XML, como archivos binarios o imagenes. En Delphi, 10s
adjuntos se gestionan a traves de flujos. Se puede leer o indicar el tip0 de codification del adjunto, per0 la transformacion de un flujo bruto de bytes hacia y
desde una codificacion dada depende del codigo. Aun asi, este proceso no es
demasiado complejo, si se tiene en cuenta que Indy incluye unos cuantos componentes de codificacion.
Como ejemplo del uso de adjuntos, hemos escrito un programa que reenvia el
contenido binario de un ClientDataSet (que tambien alberga imagenes) o una sola
de las imagenes.
El servidor tiene esta interfaz:
tn?e
ISoapFish = i n t e r f a c e ( IInvokable)
[ ' {4E4C57BF-4AC9-41C2-BB2A-64BCE4 7OD4SO} ' 1
f u n c t i o n GetCds: TSoapAttachment; stdcall;
f u n c t i o n GetImage(fishName: s t r i n g ) : TSoapAttachment;
s tdcall;
end;
La implernentacion del metodo G e t C d s usa un ClientDataSet que hace referencia a la clasica tabla BIOLIFE, crea un flujo en memoria, copia en el 10s datos,
y despues adjunta el flujo a1 resultado T S o a p A t t a c h m e n t :
function TSoapFish.GetCds: TSoapAttachment; stdcall;
var
memStr: TMemoryStream;
begin
Result : = TSoapAttachment-Create;
memStr : = TMemoryStream-Create;
WebModule2.cdsFish.SaveToStream(MemStr);
// binary
Result.SetSourceStream (memStr, soReference);
end;
que hacer es conseguir el adjunto SOAP, guardarlo en un flujo temporal en memoria y despues copiar 10s datos desde el flujo de memoria al ClientDataSet local.
p r o c e d u r e TForml.btnGetCdsClick(Sender: TObject);
var
sAtt: TSoapAttachment;
memStr: TMemoryStream;
begin
nRead : = 0;
sAtt : = (HttpRiol a s ISoapFish) .GetCds;
try
memStr : = TMemoryStream.Create;
tr Y
sAtt SaveToStream(memStr) ;
memStr.Position : = 0;
ClientDataSetl.LoadFromStream(MemStr);
finally
memStr. Free;
end;
finally
DeleteFile (sAtt .CacheFile) ;
sAtt.Free;
end;
end ;
ADVERTENCIA: De manera predeterminada, 10s adjuntos de SOAP recibidos por un cliente se guardan en un archivo temporal, a1 que hace referencia la propiedad CacheFile del objeto TSOAPAttachment. Si no se
borra este archivo, permanecera en una carpeta que albergue archims temporales.
Este codigo produce el mismo efecto visual que una aplicacion cliente que
cargue un archivo local en un ClientDataSet, como muestra la figura 23.7. En
este cliente SOAP hemos usado un componente HTTPRIO de manera explicita
para poder inspeccionar 10s datos entrantes (que posiblemente seran muy grandes
y lentos). Por este motivo, hemos puesto a cero una variable nRead global antes
de invocar a1 metodo remoto. En el evento OnReceivingData de la propiedad
HTTPWebNode del objeto HTTPRIO, aiiadiremos 10s datos recibidos a la variable nRead. Los parametros Read y T o t a l que se pasan a1 evento se refieren a1
bloque de datos especifico que se envia a traves de un socket, por lo que resultan
casi inutiles por si solos para inspeccionar el progreso:
p r o c e d u r e TForml.HTTPRIO1HTTPWebNodelReceivingData(
Read, Total: Integer);
begin
Inc (nRead, Read) ;
StatusBarl.SimpleText : = IntToStr ( n R e a d ) ;
App1ication.ProcessMessages;
end ;
90020 Triggerfishy
90030 Snapper
90050 Wrasse
90070 Angelfish
9M80 Cod
90090 Scorpionlish
Clown Triggerf'ish
Red Emperor
G~anlM m r ~Wrasse
Blue Angellish
Lunartail Rockead
Fnefish
Soporte de UDDI
La gran popularidad de XML y SOAP abre nuevas vias para que las aplicaciones de comunicacion B2B interacthen. XML y SOAP proporcionan una base,
per0 no bastan (la estandarizacion en 10s formatos XML, en el proceso de comunicacion y en la disponibilidad de la informacion sobre un negocio son todos ellos
elementos claves de una solucion real). Entre 10s estandares propuestos para superar esta situacion 10s mas notables son Universal Description, Discovery, and
Integration (UDDI, www.uddi.org) y Electronic Business using extensible Markup
Language (ebXML, www.ebxml.org). Estas dos soluciones se solapan y difieren
en parte y ahora se trabaja con mas empeiio en ellas por parte del consorcio
OASIS (Organizationjor the Advancement of Structured Information Standards,
www.oasis-open.org). No vamos a entrar en 10s problemas de 10s procesos de
negocio; en lugar de eso, solo vamos a comentar 10s elementos tecnicos de UDDI,
ya que, de manera especial, Delphi 7 soporta este estandar.
~ Q u es
e UDDI?
La especificacion Universal Description, Discovery, and Integration (UDDI)
es un esfuerzo para crear un catalog0 de servicios Web ofrecidos por empresas de
IBM tiene un conjunto de herramientas Java Open Source para UDDI. Las API de
UDDi incluyen consultas ( f i n d x x y g e t x x ) y publicaciones ( s a v e x x y
d e l e t e x x ) para cada una d e las cuatro estructuras de datos principales
( b u s i n e s s ~ n t i tb~u ,s i n e s s s e r v i c e , b i n d i n g T e r n p l a t e y t M o d e l ) .
UDDI en Delphi 7
Delphi 7 incluye un navegador UDDI que puede usarse para encontrar un
servicio Web cuando se importa un archivo WSDL. El navegador UDDI, que
muestra la figura 23.8, lo activa el WSDL Import Wizard. Este navegador solo
utiliza la version 1 de servidores UDDI (hay disponible una interfaz mas nueva,
pero no esta soportada) y tiene unos cuantos registros UDDI predefinidos). Se
pueden aiiadir configuraciones predefinidas en el archivo UDDIBrow.ini que se
encuentra en la carpeta bin de Delphi.
Este es un mod0 muy practico de acceder a informacion sobre servicios Web,
pero no es todo lo que permite Delphi. Aunque el UDDI Browser no este disponible como una aplicacion independiente, las unidades de interfaz UDDI estan disponibles (y no es trivial importarlas). Por ello, se puede escribir un navegador
UDDI propio.
Vamos a bosquejar una solucion sencilla, que es un buen punto de partida para
un navegador UDDI mas complete. El ejemplo UddiInquiry, que se muestra en la
figura 23.9, tiene un gran numero de caracteristicas, pero no todas ellas funcionara correctamente (en particular las caracteristicas de busqueda por categoria). El
motivo es que usar UDDI implica recorrer estructuras de datos muy complejas,
que no siempre se proyectan del mod0 mas obvio mediante el importador WSDL.
Esto complica bastante el codigo del ejemplo; por eso solo vamos a mostrar el
codigo de una busqueda sencilla, y no todo (otro motivo es que algunos lectores
pueden no tener particular interes por UDDI).
Search by Name
Search t o r lmicro
] Description
Micro C, Inc.
Micro Focus
Micro Focus
Micro lntormalica LLC
MICRO MACHINES
Micro Motion Inc.
MicroApplications. Inc.
rnrcrobizl
Search by Category
1 BusinessKay
516ab96a-50f5-48b4-9fO4- ...
7e76378cfa28-47a2-b8a...
9566~530-7d59-11d6-8c3...
dce959d-200d-4d9e-bee ...
ca2551 cc088f-46b7-9cl ...
d4e4b830-fl9e-4edI-9144...
a23~901e-834~-4b8~bf3...
87f5ta08-508e-4065-b379 ...
4
_1
r ? dv e ~ s ~ o n = " l .encoding=*utf-8"
~l'
1,
s o a p : Envelope .<rnlns.io~p="htt~~://sct~emas.xmlsoap.org/soap/envelope/'
umlnr-.: ~ ~ 1 = " h t t p : / / w ~ ~ . w 3 . ~ rl/XMLSchema-instance'
g/200
.dns: v s d = " h t t p : / / c c ~ v ~ ~ ~ . ~ ~ ~ 3 . o r ~ / 2 0 O 1 / X k 1 L S c h ~ n ~ a ' ~
- <soap:Bodyr
- <businessDeta~lgeneric="l.O uperator="Microsoft Corporation" truncated="false"
:ri~ln~="urn:urldi-org:clpin:
- <bus~nessEnt~ty
bus1nesst~ey="59593094-dfld-4f53-9a2c-BbffcBc93513'
operator="Microroft Corporation" author~zedName="ScottWitkin">
- .rd~scove~-vURLC>
procedure TForml.btnSearchClick(Sender:
var
findBusinessData: Findbusiness;
businessListData: businessList2;
begin
httpriol.Ur1 : = cornboRegistry.Text;
TObject);
businessListData : =
inftInquire.find-business(findBusindBusinessData);
BusinessListToListView (businessListData);
findBusinessData.Free;
end ;
A1 hacer doble clic sobre el elemento de vista de lista, se pueden explorar aun
mas sus detalles, aunque el programa muestra la informacion XML resultante en
un formato de texto plano (o en una vista XML basada en TWebBrowser) y no
lo procesa mas. Como se ha mencionado, no queremos entrar en detalles tecnicos;
si se siente interes, se puede analizar con mas detalle el codigo hente.
Parte V
Apendices
Herramientas
Delphi del autor
Durante 10s ultimos aiios el autor de este libro ha desarrollado algunos pequeiios componentes y herramientas complementarias de Delphi. Algunas de estas
herramientas fueron creadas para libros o como resultado de la ampliacion de
ejemplos de libros. Otras fueron escritas como ayuda para tareas repetitivas.
Todas estas herramientas estan disponibles gratuitamente y algunas incluyen el
codigo fuente. Este aphdice proporciona una lista, incluyendo especialmente las
mencionadas en este libro. En el for0 de discusion del autor se ofrece soporte para
todas estas herramientas (vease www.marcocantu.com para obtener las direcciones).
CanTools Wizards
Este es un conjunto de asistentes que podemos instalar en Delphi, en un menu
desplegable extra o como un submenu del menu Tools. Los asistentes (disponibles gratuitamente en www.marcocantu.com/cantoolsw)no estan relacionados
entre si y tienen caracteristicas diferentes:
List Template Wizard: Racionaliza el desarrollo de clases similares basadas en listas, cada una con su propio tip0 de objetos. Este asistente se
sbookshd7codeU4RunPropR~~1Prop
dpr
e bmkshd7codeU4\ICompess~Compress
Tambien podemos compilar automaticamente un proyecto concreto o comenzar una (lenta) creacion de multiples proyectos: en el cuadro de resultados del compilador, haremos clic sobre un boton para proceder solo si la
opcion del entorno correspondiente esta fijada. Si esta opcion del entorno
no esta fijada, no veremos 10s errores del compilador, porque 10s mensajes
del compilador son reemplazados en cada cornpilacion.
Clip History Viewer: Mantiene una lista de elementos de texto que hemos
copiado a1 Portapapeles. Un campo de la ventana del visor muestra las
ultimas 100 lineas copiadas. Editar ese campo (y hacer clic sobre S a v e )
modifica este historic0 del Portapapeles. Si mantenemos abierto Delphi, el
Portapapeles recogera texto de otros programas (pero solo texto, por su-
E !bwks\rnd7code\08\VI1\VI1dpr
E-\books\md7code\OB\Pol1Fo1m\PoliForrn dp
E:\books\rnd7code\OBF1arnes2\F1arnes2
dp
Do RepbDsI ROld
QForms
--
-1%
-_-_I
Fums
Object Debugger
En tiempo de diseiio, podemos usar el Object Inspector para fijar las propiedades de 10s componentes de nuestros formularios y otros modulos. En Delphi 4,
DlapMode
Enabbd
M d
TIW
EutsmkdSckcl
Trw
+Font
[ O W 01343DF8)
Charsc(
Cdn
He*
Nams
Pilch
S k
SM
1
&ridowTed
,;,.
T m t Nau Roman
IpDeld
0
-.
Memory Snap
Existen multiples herramientas para analizar el estado de la memoria de una
aplicacion Delphi.
Este es un gestor de memoria personalizado que se conecta con el gestor de
memoria por defecto de Delphi, analizando todas las asignaciones y liberaciones
de memoria. Ademas de informar del numero total (algo que ahora Delphi hace
por defecto), puede guardar una descripcion detallada del estado de la memoria en
un archivo. Memory Snap mantiene en memoria una lista de bloques asignados
(hasta una cantidad maxima, facilmente modificable), de mod0 que puede volcar
el contenido de la pila a un fichero con una perspectiva de bajo nivel. Esta lista se
genera examinando cada bloque de memoria y determinando su naturaleza con
tecnicas empiricas que podemos ver en el codigo fuente (aunque no son faciles de
comprender). La salida se guarda en un archivo, porque esta es la unica actividad
que no requiere una asignacion de memoria que pueda afectar a 10s resultados.
Este es un fragment0 de un fichero de ejemplo:
00C035CC:
00C035EO:
00C03730:
00C03744:
00C03968:
OOCO3990:
00C039B4:
00C039F4:
OOCO3B34:
OOCO3B48:
00C03B58:
El programa puede ampliarse para que analice el uso de la memoria por tipos
(cadenas, objetos, otros bloques), vigile 10s bloques no liberados y mantenga las
asignaciones de memoria bajo control.
El codigo fuente de este componente tambien esta disponible gratuitamente en
la carpeta Tools del codigo del libro.
Licencias y contribuciones
Como hemos dicho, algunas de estas herramientas estan disponibles con su
codigo fuente completo. Estan protegidas bajo licencia LGPL (Lesser General
Public License, www.gnu.org/copyleft/lesser.htm),lo que significa que pueden
ser usadas gratuitamente y redistribuidas de cualquier modo, incluyendo modificaciones, mientras el autor retiene el copyright. La LGPL no permite cerrar el
codigo fuente de nuestras extensiones, per0 podemos usar este codigo de biblioteca en programas comerciales, independientemente de la disponibilidad del codigo
fuente. En caso de ampliar estas herramientas corrigiendo errores o aiiadiendo
nuevas caracteristicas, el autor solicita que se le envien las actualizaciones de
mod0 que pueda distribuirlas y evitar multiplicar el codigo en diferentes versiones, aunque la licencia no nos obliga a ello.
Contenido
del CD-ROM
Este libro se basa en ejemplos. Tras la presentacion de cada concept0 o componente Delphi, encontrara un programa de ejemplo (a veces mas de uno) que
demuestra como se puede usar dicha caracteristica. En total, en el libro se presentan mas de 300 ejemplos. La mayoria de 10s ejemplos son bastante sencillos y se
centran en una unica caracteristica. Los ejemplos mas complejos se elaboran
normalmente paso a paso, con pasos intermedios como soluciones parciales y
mejoras.
<
Tambien hay una version HTML del codigo fuente, en la que la sintaxis aparece resaltada, junto con un indice completo de las palabras claves y 10s
identificadores (clase, funcion, metodo y nombres de propiedades, entre otros). El
archivo del indice es un archivo HTML, por lo que podra utilizar su explorador
facilmente para encontrar todos 10s programas que usen la palabra clave o el
alfabetico
aaManua1, 633
abstract, 1 18
Abstractos, metodos, 1 18
Access, 81 5
Acciones, 251, 631
Accion, destino de la, 3 16
Aceleradoras, teclas, 261
Acerca de, 396
ActionManager, 25 1
ActiveForm, 638
ActiveForms, 644
ActiveRecord, 9 14
ActiveX, 598, 633-634, 648, 819, 1037
Control Wizard, 638
controles, 977
Data Objects (ADO), 803
uso de controles, 636
y componentes Delphi, 635
AdapterGrid, 1037
AdapterMode, 1039
Adapterpageproducer, 1042
adCriteriaAIICols, 838
adCriteriaKey, 838
adCriteriaTimeStamp, 838
adCriteriaUpdCols, 838
Add, 194, 276
To Repository, 85
addAffectGroup, 840
(a),
BabelFish, 1 131
Bands, 3 19
Barra de herramientas, 316
Barras de desplazamiento, 248
BaseCLX, 170, 172, 21 5
BDE, 173, 804
BeforeEdit, 687
BeforePost, 687
BeforeUpdateRecord, 873
BeginThread, 140
BeginTrans, 829
Beveled, 259
Beyond Compare, 46
Biblioteca
de clases estandar, 169
de tipos, 6 18
dinamica, 527
en tiempo de ejecucion (RTL), 135
Bibliotecas, 527
del sistema de Windows, 530
BindingTemplates, 11 52
Bit menos significative, 304
bkcancel, 392
bkOK, 392
BLOB, 202, 872-873, 892
campos, 206
Bloqueo
optimists, 836
pesimista, 832
recursive, 142
tipos de, 83 1
BMP, 681
extension, 77
body, 1056
Bookmarksize, 902
BoolToStr, 142
Bordes, 232
Borland
Memory Manager. 538
Registry Cleanup Utility, 75
BorlndMM.DLL, 154, 538-539
BPG, 69
extension, 77
BPL, extension, 77
BRC32.exe, 75
BRCC32.exe, 75
BringToFront, 412
Businessservice, 1 152
Buttonstyle, 676
Blisqueda, dialog0 de, 798
C#, 45
CAB
archivo comprimido, 646
extension, 77
CacheFile, 1 150
Cachesize, 825
Cadenas
de conexion, editor de, 809, 8 16
exportar, 537
caFree, 4 10
Caja negra, 95
Calculado, campo, 695, 790
Callback. 563
Callbacks, 22 1
Campos, 687
del formulario. elimination, 185
Canal Alpha, 366
CancelBatch, 835
Cancelupdate, 83 5
Caption, 133, 164, 261
CaretPos, 304
Cascade, 424
Cascading Style Sheets (CSS), 993
CASE, 567
Casilla de ver~ficacion,24 1
Casos de uso, 572
Catalogo, 8 14
CD-ROM, 1 165
CDATA, 1088
cdecl, 53 1
cdPreventFullOpen, 394
CDS, 669
CellData, 993
CFG
archivo, 70
extension, 77
Changed, 5 10
Characters, 1099
CheckBox, 241
Checked, 245
CheckListBox, 244,272
ChildValues, I089
ciMultiInstance, 874
ckAttachToInterface, 627
ckNewlnstance, 627
ckRemote, 627
ckRunningInstance, 627
ckRunningOrNew, 627
clActiveCaption, 234
clActiveForeground, 234
Clases de escucha, 189
Class Completion, 49-50, 92
ClassPDColorPropPage, 642
Class-DFontPropPage, 642
Cla~s~DPicturePropPage,
642
Class-DStringPropPage, 642
Classes, 169, 180, 21 5
CLassInfo, 165
Classparent, 164
ClassType, 164
Clave de registro, 842
Clave externa, 790
clBase, 233-234
clBtnFace, 234
clCream. 233
clDisabledBase, 234
clGreen, 233
ClientDataSet, 173, 670, 696, 727, 822, 854,
871, 1085, 1105
Clientelservidor, 848-849
Clientelservidor, arquitectura, 728
ClientToScreen, 252,263
Clip History Viewer, 1160
clMedGray, 233
clMoneyGreen, 233
clNone, 1071
Clone, 828
cloneNode, 1091
clRed, 233
clsilver, 233
clSkyBlue, 233
clUseClient, 822, 834, 840
clUseCursor, 825
clUseServer, 822
clWhite, 233
clwindow, 234
CLX, 38, 170, 172, 220, 222
cm-Activate, 500
cm-BiDiModeChanged, 500
cm-BorderChanged, 500
cm-Changed, 643
cm-ColorChanged, 500
cm-Ctl3DChanged, 500
cm-CursorChanged, 500
cm-Deactivate, 500
cm-EnabledChanged, 500
cm-Enter, 500
cm-Exit, 500
cm-FocusChanged, 500
cm-FontChanged, 500
cm_GotFocus, 500
cm-LostFocus, 500
cm-MouseEnter, 498
cm-MouseExit, 498
cmdFile, 844
CmdGotoPage, 1037
CmdLine, 140
CmdNextPage, 1037
CmdPrevPage, 1037
coBookMark, 829
Code, 177, 189
Completion, 50-5 1, 102
Explorer, 46-48
Insight. 49
Parameters, 52
Templates, 52
Codificaciones, 1082
Colecciones, 196
Color, 233
Key, 366
ColorBox, 245
ColorDialog, 394
Colores, 233
ColorRef, 537
ColorToString, 269
ColumnLayout, 243
Columns, 243, 676, 992
COM+, 154, 599, 648-650, 845, 851, 874, 1148
eventos, 653
COM
aplicacion contenedor, 630
objetos locales, 1148
Comandos, 250
ComboBox, 243
ComboBoxEx, 245
ComConst, 154
ComCtrls, 276
CommandText, 817, 844
CommandType, 844
Commit, 78 1
CommitRetaining, 782
CommitTrans, 829
ComObj, 154
Comparevalue, 146
Compartido, controlador de eventos, 30 1
Compatibilidad, 1 13
Compatible en tipo, 124
Compilador
advertencias de, 73
mensajes de, 73
Compilar, 7 1-72
Complejos, ntimeros, 152
Component Palette, 38, 63, 66, 171
Componentcount, 18 1
ComponentIndex, 180
Components, 181-182
Components, matriz, 181
Componentstate, 41 1
Compuestos, documentos, 629
ComServ, 154
Concurrencia, 65 1
Conexion
agente de, 871
cadenas de, 809-81 1
Conjuntos de registros
desconectados, 840-841
permancntes, 843-844
Connected, 858
Connection, 8 1 1, 872
ConnectionBroker, 855, 871
Connectionstring, 809, 81 1-8 12, 8 17, 822
ConnectKind, 627
Conscientes de 10s datos, controles, 878
ConstraintErrorMessage, 861
Constraints, 258, 260, 861
Constructor virtual, 133
Constructores, 103-104
Consulta
en vivo, 779
libre, 800
Container, 630
Contenedor, 242
Contenedores, 193, 196
ContentType, 995
Contnrs, 196
Contribuciones, 1 164
Controlador, 61 3
ControlBar, 31 8, 320
Controls, 182, 393
Convert, 76, 155
CONVERT.EXE, 208
ConvUtils, 148, 154
Cookies, 1056
CoolBar, 3 18-3 19CORBA, 852
Correo electronico, 973
protocolos de, 974
enviado, 975
recibido, 975
Cracker, 112
Create, 103
CreateComObject, 623
createElement, 1090
CreateFileMapping, 548
CreateForm, 380
CreateGUID, 143
CreateHandle, 235
Createoleobject, 623
Createparams, 235
CreateTable, 91 8
CreateWindowEx, 354
CreateWindowHandle, 235
Creational Wizard, 595
Cross-platform Form Modules (XFM). 224
csDestroying, 4 1 1
csDropDown, 243
csDropDownList, 243
csExecute, 978
CSS, 993
archivo, 995
cssimple, 243
ctAnchor, 1039
ctDynamic, 825
ctstatic, 825
Cuadricula, 676
Cuadricula, HTML, 1061
Cubos, 198
CUR, extension, 77
Currency, 862
Cursores, 822
conjunto de claves, 824
dinamico, 824
estatico, 824
solo avance. 824
Data Link, 81 1
Data, 177
Data-aware, 237, 675, 877-879, 882, 897, 91 1
controles orientados a campos, 880
Database Explorer, 75
Datachange, 885
DataCLX, 170, 173
DataEvent, 879
DataField, 675, 878, 881
DataGrid, 1 1 13
DataLinkDir, 812
DataNavigador, 1 1 13
DataRelation, 845
DataSet, 845, 861, 991
DataSetAdapter, 1037
DataSetField, 871
DataSetPageProducer, 987
Datasetprovider, 862
DataSetReader, 845
DataSetTableProducer, 987, 991
Datasnap, 850
Datasource, 675, 818, 878, 881, 1077
DateUtils, 99, 136, 148, 217
DAX, 613
DBCheckBox, 675
DBCtrlGrid, 882, 888
DBEdit, 675
dbExpress, 727-728, 804
dbGo, 173, 807, 828
DBGrid, 112-1 13, 675-676, 788, 793. 818. 859,
871, 888, 917, 1142
personalization, 893-897
DBI, extension, 82
DBNavigator, 675
DCI, extension, 82
DCOM, 652, 85 1, 869
DCOMConnection, 854, 858
DCP, extension, 77
DCT, extension, 82
DCU
archivo, 552
extension, 7 8
DDE, 598
DDL, 789
DDP, extension, 78
Debugger Optiones, 129
dsCurValue, 686
dsEdit, 686
dsFilter, 686
dshactive, 686
dsInsert, 686
DSM, extension, 80
dsNewValue, 686
dsOIdValue, 686
DST
archivos, 39
extension, 82
dt-Singleline, 892
dt-WordBreak, 892
DTD, 1098
Dual, soporte, 222
DXSock, 986
dynamic, 117
ebXML, 1 15 1
EchoMode, 238
Edit, 522, 1037, 1039
componente, 237-238
EditFormat, 862
EditMask, 239, 862
Editor Properties, 5 1
EDivByZero, 127
EFileStreamError, 206
ElnvalidCast, 120
Emptyparam, 81 3
Enablecommit, 652
Enabled, 232, 3 14
Encapsulacion. reglas de la, 186
Encapsulado, 95, 10 1
con propiedades, 97
campos protegidos y, 1 11
EndDocument, 1099, 1101
EndElement, 1099-1 100
EndThread, 140
EndUserSessionAdapter, 1045
Enlace
de datos, 878
archivos de, 81 1-812
dinamico, 528
posterior, 114
Enlazador inteligente, 186
Ensamblaje, 656
EnsureRange, 145
Enterprise Studio, 36
Entrada de teclado, 356
EnumModules, 139, 562-563
Environment Options, 40-4 1, 53
Environment Variables, pagina, 40
Envoltorio, 200-20 1
EqualsValue, 146
Esquematica, informacion. 8 13
Estilo, 304
Estilos de ventana, 354
Estatica, sobrescritura, 2000
Euro, 158
Event Types View, 584
Evento, 191
Eventos, 188, 190
COM+, 653
programacion guiada por, 4 12
Events, 236
Excel, 815, 817, 821
Excepciones, 124-125
clases de, 127
depuracion y, 128
soporte, 124
except, 125-126
Exception, 127
EXE, 553
extension, 8 0
EXEC, 1056
Execute, 507
ExecuteTarget, 5 12
Executeverb, 522-523
ExpandFileName, 144
Experts, 87
exports, 53 1 , 537
Extended Database Forms Wizard, 1161
Extendedselect, 243
extern "C", 533
External Translation Manager, 75
external, 539
ExtractStr~ngs,2 16
FalseBoolStrs, 142
fdApplyButton, 394
FetchDetails, 873
FetchDetailsOnDemand. 873
FetchOnDemand, 873
Fetchparams, 868
fgConflictingRecords, 839
fgPendingRecords, 835
FieldAddress, I76
FieldByName, 687-890
FieldDataLink. 884
Fields, 1037
FieldsDef, 902
FieldValues, 688
FIFO, 197
Filecreate, 144
Filter, 673, 814
Filtered, 8 14
Handle, 235
HandlesPackages, 560
HandlesTarget, 5 12
Hash, 198
Header, 992
Hebras, 1141
Herencia, 109, 1 13, 200, 432
de un formulario base, 433
Hide, 232
HideOptions, 1047
Hilos, 4 12
HInstance, 563
Hint, 303
Hintcolor, 262
HintHidePause, 262
Hintpause, 262
Hintshortpause, 262
Hojas de estilo, 993
HorzScrollBar, 249
HTML, 987
extension, 80
HTML 4 , 9 8 8
archivo, 82 1
estructuras, 1068
generation de paginas, 988
HTTP, 852,977, 982
servidor, 9 8 5
socket, 869
HttpDecode, 969
HttpEncode, 969
HTTPRIO, 1 134, 1 137, 1 144
HTTPSoapDispatcher, 1135
HTTPSoapPascalInvoker, 1135
httpsrvr.dll, 852, 855
HTTP We bNode. 1 1SO
IConnectionPoint, 653
lconview, 246
IconView, 27 1
IDAPI, 803
Identificador de sesion, 1056
Identificador global, 790
IdHTTPServer, 985, 994
IDispatch, 139, 614-6 15, 623
IDL, lenguaje, 6 18
IdMessage, 975
IDOMAttr, 1086
IDOMElement, 1086
IDOMNode, 1086
IDOMNodeList, 1086
IDOMParseError, 1083
IDOMText, 1086
IdPop3,975
IdSMTP, 975
IdURL, 984
IFDEF, 227
Iflhen, 145, 149
ignorablewarning, 1100
IInterface, 122-124, 139
IInvokable, 139, 1132, 1145
IISAM, 815-816, 818
ImageIndex, 252
ImageList, 59
Images, 252
Import Type Library, 622
ImportedConstraint, 86 1
IN, clausula, 821
IncludeInDelta, 874
IncludeTrailingBackslash, 144
IncludingTrailingPathDelimiter,144
Increase, 9 7
Indexado, 671
IndexFieldNames, 826
IndexOf, 149
IndexOfName, 194
InetXCustom, 1 1 10-1 11 1
InetXPageProducer, 1108-1 109, 11 13
Infinity, 145
InflateRect, 892
Information de clase, 167
inherited, 48, 116, 434, 893
InheritsFrom, 1 19, 164
INI, archivos, 39
Inicial, pantalla, 397
InputBox, 396
InputQuery, 396
Inquiresoap, 1 154InRange, 145
Insert, 194, 1039
Insertcomponent, 180, 182
InsertObjectDialog, 6 3 1
Installable Indexed Sequential Access Method
(IISAM), 81 5
InstanceS~ze,164
Instancias de formulario, 793
Integrated Translation Environment (ITE), 530
InterBase Admin, 777
InterBase Express, 173, 783-785
InterceptGUID, 854
Interfaces, 12 1
Interfaz
de envio. 616
de usuario, 283
InterLockedIncrement, 1066
Intermediacion, 6 13
InternalInitFieldDefs, 904
Internet Express, 1 108
Internet, pagina, 40
InternetCloseHandle, 983
Internetopen, 982-983
InternetOpenURL, 982-983
InternetReadFile, 982-983
INTO, clausula, 82 1
IntraWeb, 173, 1049-1050
arquitecturas, 1057
Introduced, 4 8
IntToStr, 141
Invalidate, 365-366
InvalidateRegion, 366
IObjectContext, 652
is, 119, 164
ISAPI, bibliotecas, 1057
IsConsole, 139
IsEqualGUID, 143
IsInfinite, 145
IslnTransaction, 652
IWClientSideDataSet, 1076
IWClientSideDataSetDBLink, 1076
IWCSLabel, 1076
IWCSNavigator, 1076
IWDataModulePool, 105 1
IWDBGrid, 1071, 1075-1076
IWDialogs, 105 1
IWDynamicChart, 1076
IWDynGrid, 1076
IWEdit, 1052
IWGranPrimo, 105 1
IWGrid, 1061
IWImagen, 1063
IWLayoutMgrForm, 1069
IWLayoutMgrHTML, 1069
IWListBox, 1052
IWModuleController, 1067
IWOpenSource, 1051
IWPageProducer, 1066
IWRun, 1052
IWServerControllerBaseNewSession,1065
IWTemplateProcessorHTML, 1069
IWTranslator. 105 1
IWURL, 1061
IXMLDocument, 1087
IXMLDocumentAccess, 1083
IXMLNode, 1087
IXMLNodeCollection, 1087
IXMLNodeList, 1087
maAutomatic, 26 1
Macros, 587
Madre, clase, 163
Maestroldetalle, 792, 870, 104 1
MainForm, 380
MainMenu, 59
MakeObjectInstance, 431
Maletin, 844
malloc, 154
maManual, 261
Manejadores de mensajes, 1 17
MapViewOfFile, 549
Marshalling, 599, 61 3
MaskEdit, componente, 238
MasterAdapter. 1041
Math, 145, 217
Matrices, propiedades basadas en, 100
MaxRecords, 1 109
MaxSessions, 1043
MaxValue, 862
Mayusculas, 789
mbMenuBarBreak, 252
mbRight, 253
MDAC, 805, 807, 842, 850
MDI, 423
aplicaciones, 422
cliente, 423
Memo, componente, 239
Memory Snap, 1 163-1 164
Mensajes de correo, generacion automatica de, 974
Menu Des~gner,2 5 1
Menu, 261
MergeChangesLog, 675
message, 1 17
MessageBox, 396, 536
MessageDlg, 395
MessageDlgPos, 395
Messages, 230
Method Implementation Code Editor, 582
MethodAddress. 176
MethodName, 176
MidasLib, 669Middleware, 812
MilliSecondOfMonth, 148
MIME, tipo, 995
MinSize, 259
MinValue, 862
MmSystem, 497
ModalResult, 390, 505
ModelMaker, 41, 47, 567-569, 592
ModelMaker, integracion de Delphi con, 576-575
Modelo
de hilos, 649
de referencia a objetos, 104
transaccional, 649
Modified, 643
ModifyAccess, 1047
ModuleIsPackage, 56 1
Move, 921
Mozilla, 1055
MPB, extension, 570
mrOk, 505
mscorlib.dll, 658
MSXML SDK, l o 8 6
MTS, 648, 851-852
Multicapa, aplicaciones Datasnap. 847
Multiline Palette Manager, 1161
Multiplataforma, 932
MultiSelect, 242, 280
MultiSelectStyle, 280
MyBase, 173, 1085
Metodo, punteros a, 189
Modelo de codigo, 578
Modulo de datos transaccionales, 65 1
OASIS, 1 15 1
OBJ, extension, 80
ObjAuto, 2 17
Object
Browser, 74
Debugger, 1162-1 163
Inspector Font Wizard, 1160
Inspector, 58. 59, 191
Pascal, 89
Repository, 84-85, 535
Treeview, 54, 59, 61-62
ObjectBinaryToText, 208
ObjectPropertiesDialog, 632
Objects, 268
Ob.jeto
COM, 599
interno, 633
Objetos
de automatization, alcance de, 624
y memoria, 107
OCX, 636, 646
extension, 80
ODBC, 803, 805
of object, 189
ofAllowMultiSelect, 394
ofExtensionDifferent, 394
Office, aplicaciones, 629
OID global, 786
OID, 786
OiFontPk, 60
OLAP. 173
Oldcreateorder, 225
OldValue, 839
OLE Automation, 97, 612
OLE DB, 803, 805, 809
proveedores, 805
OLE, 598, 629
Olecontainer, 630
Olevariant, 637-638
OnActionUpdate, 404
OnActivate, 404
OnCalcFields, 696
OnCanViewPage, 1047
Onchange, 191, 236, 924
OnChar, 493
Onclick, 166, 184, 190-192, 236
Onclose, 783
OnCloseUp, 244
OnColumnClick, 273
OnCommandGet, 994
Oncompare, 273
OnContextMenu, 253-254
Oncreate, 379
OnCreateNodesClass, 280
OnDataChange, 879
OnDestroy, 557
OnDoubleClick, 39 1
OnDragDrop, 157, 277
Page, 1057
Pagecontrol, 181, 284, 1083
PageControls, 285
PagedAdapter, 1037
Pageproducer, 987
PageScroller, 249
Pagesize, 1037
PakageInfo, 139
Panel, 181
Paquetes, 527, 553
interfaces en, 558
versiones de, 55 1
de datos, 853
delta, 853, 865
Paradox, 8 16, 82 1
param, 647
Paramcount. 140
Params, 868, 1 109
ParamStr, 137, 140
Parcheo, 546
Parent, 133, 23 1
Parentcolor, 233, 635
ParentFont, 233, 635
Parser, 1083
Parametros
consulta preparada con, 790
consultas por, 868
PAS. extension, 80
Pascal orientado a objetos, 89
PasteSpecialDialog, 632
Patrones de diseiio, 590-592
PChar, 537-538
Permanencia, 207
Permisos, 1043
Personalizada
clase stream, 210
variante, 153
Pes~mista,bloqueo, 832
Peticion de entrada, 1045
PickList, 676
PixelsPerInch, 378-379
Plantilla, 66
Plantillas
de componentes. 65
de codigo, 45, 593
modificar, 52
platform, 90
Playsound, 497
poAllowCommandText, 873
poCascadeDeletes, 873
poDefault, 368
poDefaultPosOnly, 368
Preferences, pagina, 4 0
Principal, formulario, 429
Privada, parte de la declaration, 186
Privado, 96
private. 96, 176
procedure, 92
ProcessMessages, 365, 4 12
Professional Studio, 36
Propiedades
ficha de, 642
por su nombre, 177
Propietario
cambio de, 182
componente, 180
protected, 96, 176
Protegido, 96
Proveedores, 806
ProviderFlags, 862
ProviderName, 859
Proyecto
en blanco, plantilla de. 85
opciones de, 69
Proyectos, gestionar, 67-68
public, 96, 100, 176
published, 100, 174, 176
Paginas
amarillas, 1 152
blancas, 1 152
verdes, 1 152
P~iblico,96
QDialogs, 224
QForms, 224
QGraphics, 224
QPainterH, 222
QStdCtrls, 223
Qt, 2 2 0 , 2 2 2
Qt/C++, 236
QtFreeEdition, 222
Query, 1039
QueryInterface, 600, 610
QueryTableProducer, 988
RadioButton, 24 1
RadioGroup. 241
raise, 125
Random, 145
RandomFrom, 145
RandomRange, 145
Rangos, 248
Rave Reports, 932
RAVE, 173
Rave, 93 1
RC, extension, 81
RDBMS, 849
RDS, 805
RDSConnection, 808
Read, 202, 1 150
read, 98
ReadBuffer, 203
Readcomponent, 203
ReadComponentRes, 208
Rebuild Wizard, 1 1GO
ReconcileProducer, 1 1 12
Recordcount, 826
Recordsize, 9 12
Redondeo, 147
Referencias de clase, 130-132
Refresco, 865
Refresh, 865
RefreshRecords, 865-866
RefreshSQL, 796
REG, extension, 81, 609
regasm, 657
Register, 524, 53 1
Registerclass, 186, 558
RegisterClasses, 186
RegisterConversionType, 16 1
RegisterPooled, 874
Registro de Windows, 78 1, 1 148
Registro, 64
Registros, conjunto de, 81 3
Reglas de negocio, 926
Reingenieria, 587-588
Release, 124, 1063
Remoteserver, 859
Remove, 194
RemoveComponent, 182
Repaint, 365
Replace, 395
Replicacion, 827-829
Required, 1062
Reserva, de conexiones, 84 1
Resizestyle, 260
Resolutor, 864
Resolver, 864
ResolveToDataSet, 863-864
Resource Explorer, 76
Resource Workshop, 76
resourcestring, 14 1
Restricciones, 860
Resync, 840
ResyncValues, 840
Rethink Orthogonal, 595
RethinkHotKeys, 261
RethinkLines, 26 1
Retrollamada, 563
Retrollamadas, 221
Reutilizacion, 1 15
RGB, 234
RichEdit, 239
Rollback, 781
RollbackRetaining, 782
RollbackTrans, 829
Root, 924
ROT, 627
RoundTo, 146
Row, 243
RowAttributes, 992
RowCurrentColor. 107 1
RowLayout, 243
RowLimit, 107 1
RPS, extension, 8 1
rsline, 260
rsPattern, 260
rsupdate, 560
RTL, 123, 127, 152, 170, 220
unidades de la, 136
VCL y, 138
RTTI, 119, 121, 133, 164, 556, 558, 1093
operadores, 12 1
RunOnly, 562
RWebApplication, 1065
safecall, 869
SafeLoadLibrary, 542, 556
Samevalue, 146
save-xx, 1 15 3
SaveDialog, 394
SavePictureDialog, 394
Savepoint, 674
SaveToFile, 193, 843
SaveToStream, 204
SAX, 1085, 1099
ScaleBy, 377
SCHEMA.IN1, 8 19-820
ScktSrvr.exe, 852
Screen, 378-379, 409
Screensnap, 368
ScrollBar, 248
ScrollBox, 250
SecondOtWeek, 148
Seguimiento activo, 274
Seguridad de tipos, 199
SelAttributes, 239
SelCount, 243
Selected, 243
Selections, 280
SelectNextPage, 287-288
Self, 93-94, 104, 123, 189
Sender, 157
ServerGUID, 854
ServerName, 854
Servicios de componentes de Microsoft, consola
de, 654
Sesiones, 1043
gestion de, 1064-1066
Sessionservice, 1045
SetAbort, 652
SetBounds, 232
SetComplete, 652
SetDataField, 882
SetFieldData, 92 1
SetForegroundWindow, 421
SetMemoryManager, 139
SetOleFont, 627
Setolestrings, 628
SetShareData, 549
SGML, 1080
SharedConnection, 855
ShareMem, 154
ShellApi, 977
ShellExecute, 977
ShortMonthNames, 110
Show, 559
ShowColumnHeaders, 273
Showing, 233
ShowMessage, 178, 396, 536
ShowMessagePos, 396
ShowModal, 545, 559, 1063
Showsender, 166
Signals, 236
Signatures, 207
SimpleObjectBroker, 873
SimplePanel, 255, 301
SimpleRoundTo, 147
SimpleText, 30 1
Sincronizacion, problemas, 3 16
Sincronizador multilectura, 142
Singleton, patron, 590
siProviderSpecific, 813
Size, 202, 8 18
SizeGrip, 302
SizeOf, 164
Skin, 304
SmallImages, 270
SMTP, 975
SNA Server, 807
Snap To Grid, 56
SOAP Server Application, 1 134
SOAP, 652, 852, 1129, 1131, 1140, 1145
DataSnap sobre, 1 145
depuracion de cabeceras, 1143
proyeccion sobre Pascal, 1 133
SOAPConnection, 1148
SOAPMidas, 1 147
SOAPServerIID, 1147
Sobrecargadas, funciones, 537
Sobrescribir, 1 15
Socket, conexion de, 970
Socketconnection, 854
SortType, 273
Soundex, 149
Source Options, 45, 52
SpeedButton, 2 3 1
Splitter, 258, 260
SQL Monitor, 75
SQL Server Profiler, 834
SQL Server, 834
SQL
edicion, 801
insert, 796
sentencia, 728, 779, 800, 792
servidor, 672
servidores, 648, 727
update, 796
SQLDataSet, 857, 1140
SQLQueryTableProducer, 988
Standalone, 1057
StartDocument, 1099, 1101
StartElement, 1099-1 100, 1102-1 103
starting with, 788
State, 245, 914
State-setters, 250
StateImages, 270
Stateless COM (MTS), 85 1
Statics, 4 8
StatusBar, 102
stBoth, 273
stData, 273
stdcall, 531, 534, 1145
StdConvs, 148, 155, 159, 161
StdCtrls, 223
Stones, 148
stored, 100
StoreDefs, 670
StrCopy, 92 1
Stream, 2 14
Streaming, 174-175, 202, 206
sistema de, 186
Streams, 205, 209
compresion de, 2 13
conjunto de datos basado en, 9 17-9 19
StringReplace, 67 1
StringToColor, 269
StringToFloat, 495
StripParamQuotes, 988
StrUtils, 149, 2 17
style, 1055
SubMenuImages, 252
Sugerencias, personalization, 263
SupportCallbacks, 854
SupportedBrowsers, 1055
Supports, 561, 829
Sustitucion de etiquetas, 1045
Synchronize, 97 1
SyncObjs, 217
SysConst, 141
SysInit, 139-140, 561
System, 139-140
SysUtils, 136, 141-142, 162,217
TabbedNotebook, 285
Tabcontrol, 284
Tabla de metodos virtuales (VMT), 123, 552
TableAttributes, 992
Terminate, 1063
Text IISAM, 819-820
Text, 133, 244
TextBrowser, 983-984
TextHeight, 268
Texto, archivos de, 8 19
Textviewer, 240
TField, 992
TFieldDataLink, 879, 886, 888
TFields, 902
TFileData, 92 1, 924
TFileRec, 14 1
TFileStream, 204
TFloatField, 690
TFMTBCDField, 693
TForm, 645
TFormatSettings, 144
TFormClass, 132
tgcustom, 990
tglmage, 990
tgLink. 990
TGraphicControl, 230-231, 639
TGraphicField, 693
tgTable, 990
THandleStream, 204
THashedStringList, 199
THeapStatus, 139
THintInfo, 264
Threads, 142, 412
threadvar, 1065
THTTPRIO, 1 133
TIcon, 204
TidThreadSafeInteger, 1066
Tiempo de ejecucion
verificaciones en, 200
Tile, 424
TileMode, 424
TMenuItem, 261
TMethod, 177, 189
tModel, 1 152
TMREWSync, 142
TMultiReadExclusiveWriteSynchronizer,142
TNotifyEvent, 2 15
TO-DOList, 41
TObject, 121, 139, 163-164, 165-167
TObjectBucketList, 198
TObjectList, 197, 919
TObjectQuery, 197
Tocommon, 16 1
TODO
comentarios, 42
extension, 81
TOleContainer, 63 1
TOleControl, 640
TOleServer, 626
TOleStream. 205
ToolBar, 30 1, 3 13
Tooltip Expression Evaluation, 53
Tooltip Symbol Insight, 4 8
Top, 232
Total, 1 150
TOwnedCollection, 2 15
Transaccion. 65 1. 783
componente de, 78 1
context0 de, 782
Transaction DDL, propiedad, 829
TransformNode, 11 19
Transparentcolor, 366
TransparentColorValue, 366
TReader, 206-207
TRecall, 2 16
TReconcileErrorForm, 840
TRect, 15 1
Treeview, 270, 275-276, 1087
Tres capas, arquitectura logica de, 849
TResourceStream, 2 0 5 , 2 1 0
Trolltech, 220
TrueBoolStrs, 142
try, 125-126
trylexcept, 129
TryEncodeData, 143
TryEncodeTime, 143
TryStrToCurr, 143
TryStrToDate, 143
TryStrToFloat, 143
TSchemaInfo, 8 13
TSearchRec, 92 1
TSmallPoint, 15 1
TSOAPAttachment, 1050
TSoapAttachment, 1149
TSoapDataModule. 1 147
TSQLTimeStampField, 694
TStack. 197
TStream, 202-203
TStringList. 193, 204, 215, 217, 508
TStrings, 149, 193, 204, 2 15
TStringStream, 204
TTextRec, 141
TThreadList, 2 15
TTimeField, 694
TTimeStamp, 923
TToolButton, 250
TTreeItems, 280
TTreeNode, 278
Tuberia (I), 303
Turbo
Grep, 76
Pascal, 4 6
TextFile, 130
Register Server, 76
TUserSession, 1065, 1066, 1070
TVarBytesField, 694
TVarData, 139
TVariantField, 695
TVariantManager, 139
TWebAppPageModuleFactory, 1046
Type Library Editor, 1135
Type Library Importer, 657
TypeInfo. 179
Types, 15 1.230
TypInfo, 178, 217, 1093
Validacion, 1098
ValueFromIndex, 194
ValueListEditor, 246
Values, 194
var, 101, 107, 118
VarArrayCreate, 673
VarArrayOf, 673
VarCmply 153
VarComplexCreate, 153
Variant, 136
Variantes personalizadas, 152
WideSameText, 143
Widestring, 650
WideUpperCase, 143
Widgets, 220
Width, 259
WINAPI, 53 3
WindowMenu, 423
WindowPRoc, 235
Windows 2000, 353
Windows, 230, 536, 932
Windowstate, 368
WinInet, 982
Winsight, 75
WndProc, 235
Word, 629
Wordstar. 46
Worl Wide Web Consortium (W3C), 987
wpLoginRequired, 1046
Write, 202
xaAbortRetaining, 830-83 1
xaCommitRetaining, 830-83 1
XDB, extension, 1098
Xerces, 1086
XFM, 174,224
XHTML, 988
XLSReadWrite, 8 17
XMethods, 1 13 1
XML Mapper, 75, 1103-1 104, 1141-1 145
XML Schema Validator (XSV), 1099
XML Schema, 1099
XML, 669, 671, 844, 1079-1080
analizador sintactico, 843
bien formado, 1082
con transformaciones, 1 103
datos, 1139
esquemas, 1098
interfaces de enlace de datos, 1094-1099
paso de documentos, 1140
sintaxis, 1080-1082
XMLBroker, 1108-1 109, 11 15
XMLData, 669
XMLDocument, 1083
xmldom, 1086
XMLTransform, 855, 1 105
XMLTransformClient, 855, 1 105
XMLTransformProvider, 855, 1105, 1 142