Professional Documents
Culture Documents
. .
6 7
Siete versiones y contando ..................................................................................................... La estructura del libro ............................................................................................................ Normas usadas en este libro ............................................................................................
Parte I Bases
29 31 32
Ediciones de Delphi ................................................................................................................ 36 Una vision global del IDE ..................................................................................................... 37 Un IDE para dos bibliotecas ............................................................................................ 38 . Configuration del escritorio ..................................................................................... 38 Environment Options ....................................................................................................... 40 40 Sobre 10s menus ................................................................................................................. El cuadro de dialog0 Environment Options .............................................................. 41 TO-DO List .......................................................................................................................... 41 Mensajes ampliados del compilador y resultados de busqueda en Delphi 7 .............. 43 44 El editor de Delphi ................................................................................................................. El Code Explorer ............................................................................................................... 46 . Exploracion en el editor ................................................................................................... 48 Class Completion .............................................................................................................. 49 Code Insight ...................................................................................................................... 50
Code Completion ......................................................................................................... 50 Code Templates ............................................................................................................ 52 Code Parameters .......................................................................................................... 52 Tooltip Expression Evaluation ................................................................................... 53 Mas teclas de metodo abreviado del editor .................................................................... 53 Vistas que se pueden cargar ............................................................................................. 54 Diagram View .............................................................................................................. 54 Form Designer ......................................................................................................................... 56 Object Inspector ................................................................................................................ 58 Categorias de propiedades .......................................................................................... 60 Object TreeView ................................................................................................................ 61 Secretos de la Component Palette ......................................................................................... 63 Copiar y pegar componentes ............................................................................................ 64 De las plantillas de componentes a 10s marcos ............................................................. 65 Gestionar proyectos ................................................................................................................ 67 Opciones de proyecto ........................................................................................................ 69 Compilar y crear proyectos .............................................................................................. 71 Ayudante para mensajes del compilador y advertencias ......................................... 73 Exploracion de las clases de un proyecto ....................................................................... 74 Herramientas Delphi adicionales y externas .............................. ........................................ 75 Los archivos creados por el sistema ..................................................................................... 76 Un vistazo a 10s archivos de codigo fuente .................................................................... 82 El Object Repository ............................................................................................................... 84 Actualizaciones del depurador en Delphi 7 ......................................................................... 87
2 El lenguaje de programaci6n Delphi
..........................................................................89
Caracteristicas centrales del lenguaje .................................................................................. 90 Clases y objetos ....................................................................................................................... 91 Mas sobre metodos ............................................................................................................ 93 Creacion de componentes de forma dinamica ............................................................ 94 Encapsulado ............................................................................................................................ 95 Privado, protegido y public0 ............................................................................................ 96 Encapsulado con propiedades ......................................................................................... 97 Propiedades de la clase TDate ................................................................................... 99 Caracteristicas avanzadas de las propiedades ........................................................ 100 Encapsulado y formularios ............................................................................................. 101 Aiiadir propiedades a formularios ........................................................................... 101 Constructores ......................................................................................................................... 103 Destructores y el metodo Free ....................................................................................... 104 El modelo de referencia a objetos de Delphi ..................................................................... 104 Asignacion de objetos ..................................................................................................... 105 Objetos y memoria ........................................................................................................... 107 Destruir objetos una sola vez ................................................................................... 108 Herencia de 10s tipos existentes .......................................................................................... 109 Campos protegidos y encapsulado ................................................................................ 111 Herencia y compatibilidad de tipos ............................................................................... 113 Enlace posterior y polimorfismo ......................................................................................... 114
Sobrescribir y redefinir metodos ................................................................................... 115 Metodos virtuales frente a metodos dinamicos ............................................................ 117 Manejadores de mensajes ......................................................................................... 117 Metodos abstractos .......................................................................................................... 118 Conversion descendiente con seguridad de tipos .............................................................. 119 Uso de interfaces ................................................................................................................... 121 Trabajar con excepciones ..................................................................................................... 124 Flujo de programa y el bloque finally ........................................................................... 125 Clases de excepciones ..................................................................................................... 127 Registro de errores .......................................................................................................... 129 Referencias de clase .............................................................................................................. 130 Crear. componentes usando referencias de clase ....................................................... 132 3 La biblioteca en tiempo de ejecuc~on
.. .........................................................................
135
Las unidades de la RTL ........................................................................................................ 136 . Las unidades System y SysInit ...................................................................................... 139 Cambios recientes en la unidad System .................................................................. 140 Las unidades SysUtils y SysConst ................................................................................. 141 Nuevas hnciones de SysUtils .................................................................................. 142 Rutinas extendidas de formato de cadenas en Delphi 7 ........................................ 144 La unidad Math ............................................................................................................... 145 Nuevas funciones matematicas ................................................................................ 145 Redondeo y dolores de cabeza .................................................................................. 147 Las unidades ConvUtils y StdConvs .............................................................................148 La unidad DateUtils ........................................................................................................ 148 La unidad StrUtils ........................................................................................................... 149 De Pos a PosEx .......................................................................................................... 150 La unidad Types .............................................................................................................. 151 La unidad Variants y VarUtils ....................................................................................... 151 Variantes personalizadas y numeros complejos ..................................................... 152 Las unidades DelphiMM y ShareMem ......................................................................... 154 Unidades relacionadas con COM .................................................................................. 154 Convertir datos ...................................................................................................................... 154 iConversiones de divisas? ................................................................................................... 158 Gestion de archivos con SysUtils ........................................................................................162 La clase TObject ................................................................................................................... 163 Mostrar information de clase ........................................................................................ 167
4 La biblioteca de clases principales
.............................................................................169
El paquete RTL. VCL y CLX ..............................................................................................170 171 Partes tradicionales de la VCL ...................................................................................... La estructura de CLX ..................................................................................................... 172 Partes especificas de VCL de la biblioteca ................................................................... 173 La clase TPersistent .............................................................................................................. 173 La palabra clave published .............................................................................................176 Acceso a las propiedades por su nombre ...................................................................... 177 La clase TComponent ...........................................................................................................180
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
5 Controles visuales
...........................................................................................................
VCL frente a VisualCLX ...................................................................................................... Soporte dual de bibliotecas en Delphi .......................................................................... Clases iguales, unidades diferentes ......................................................................... DFM y XFM ............................................................................................................... Sentencias uses .......................................................................................................... Inhabilitar el soporte de ayuda a la biblioteca dual ............................................... Eleccion de una biblioteca visual .................................................................................. Ejecucion en Linux ................................................................................................... Compilacion condicional de las bibliotecas ........................................................... Conversion de aplicaciones existentes .......................................................................... Las clases TControl y derivadas ......................................................................................... Parent y Controls ............................................................................................................ Propiedades relacionadas con el tamafio y la posicion del control ........................... Propiedades de activation y visibilidad ........................................................................ Fuentes .............................................................................................................................
220 222 223 224 226 226 226 227 228 229 230 231 232 232 233
Colores ............................................................................................................................. 233 La clase TWinControl (VCL) ........................................................................................ 235 La clase TWidgetControl (CLX) ................................................................................... 236 Abrir la caja de herramientas de componentes ................................................................. 236 Los componentes de entrada de texto ........................................................................... 237 El componente Edit ................................................................................................... 237 El control LabeledEdit ............................................................................................. 238 El componente MaskEdit .......................................................................................... 238 Los componentes Memo y RichEdit ........................................................................ 239 El control CLX Textviewer ...................................................................................... 240 Seleccion de opciones ..................................................................................................... 240 Los componentes CheckBox y RadioButton ........................................................... 241 Los componentes GroupBox ..................................................................................... 241 El componente RadioGroup ..................................................................................... 241 Listas .................'............................................................................................................... 242 El componente ListBox ............................................................................................. 242 El componente ComboBox ....................................................................................... 243 El componente CheckListBox .................................................................................. 244 Los cuadros combinados extendidos: ComboBoxEx y ColorBox ......................... 245 Los componentes Listview y TreeView .................................................................. 246 El componente ValueListEditor ............................................................................... 246 Rangos .............................................................................................................................. 248 El componente ScrollBar .......................................................................................... 248 Los componentes TrackBar y ProgressBar ............................................................. 249 El componente UpDown ........................................................................................... 249 El componente PageScroller .................................................................................... 249 El componente ScrollBox ......................................................................................... 250 Comandos ......................................................................................................................... 250 Comandos y acciones ................................................................................................ 251 Menu Designer .......................................................................................................... 251 Menus contextuales y el evento OncontextPopup .............................................. 252 Tecnicas relacionadas con 10s controles ............................................................................ 254 Gestion del foco de entrada ............................................................................................ 254 Anclajes de control ......................................................................................................... 257 Uso del componente Splitter .......................................................................................... 258 Division en sentido horizontal ................................................................................. 260 Teclas aceleradoras ......................................................................................................... 261 Sugerencias flotantes ...................................................................................................... 262 Personalizacion de las sugerencias .......................................................................... 263 Estilos y controles dibujados por el propietario .......................................................... 264 Elementos del menu dibujados por el usuario ........................................................ 265 Una ListBox de colores ............................................................................................. 267 Controles ListView y TreeView ........................................................................................... 270 Una lista de referencias grafica ..................................................................................... 270 Un arb01 de datos ............................................................................................................ 275 La version adaptada de DragTree ............................................................................ 278 Nodos de arb01 personalizados ...................................................................................... 280
..
..............................................................................283
Formularios de varias paginas ............................................................................................ 284 Pagecontrols y Tabsheets .............................................................................................. 285 Un visor de imagenes con solapas dibujadas por el propietario ................................ 290 La interfaz de usuario de un asistente .......................................................................... 294 El control ToolBar ................................................................................................................ 297 El ejemplo RichBar ......................................................................................................... 298 Un menu y un cuadro combinado en una barra de herramientas .............................. 300 Una barra de estado simple ............................................................................................ 301 Temas y estilos ...................................................................................................................... 304 Estilos CLX ..................................................................................................................... 305 Temas de Windows XP ................................................................................................... 305 El Componente ActionList .................................................................................................. 308 Acciones predefinidas en Delphi ................................................................................... 310 Las acciones en la practica ............................................................................................ 312 La barra de herramientas y la lista de acciones de un editor ..................................... 316 Los contenedores de barra de herramientas .......................................................................318 ControlBar ....................................................................................................................... 320 Un menu en una barra de control ............................................................................323 Soporte de anclaje en Delphi ......................................................................................... 323 Anclaje de barras de herramientas en barras de control ............................................ 324 Control de las operaciones de anclaje ..................................................................... 325 Anclaje a un Pagecontrol ..............................................................................................329 La arquitectura de ActionManager ..................................................................................... 331 Construir una sencilla demostracion ............................................................................ 332 Objetos del menu utilizados con menos frecuencia .....................................................336 Modificar un programa existente .................................................................................. 339 Emplear las acciones de las listas ................................................................................. 340
7 Trabajo con formularios
................................................................................................ 345
La clase TForm ..................................................................................................................... 346 Usar formularios normales ............................................................................................. 346 El estilo del formulario .................................................................................................. 348 El estilo del borde ........................................................................................................... 349 Los iconos del borde .......................................................................................................352 Definicion de mas estilos de ventana ............................................................................ 354 Entrada directa en un formulario ........................................................................................ 356 Supervision de la entrada del teclado ........................................................................... 356 Obtener una entrada de raton ........................................................................................ 358 Los parametros de 10s eventos de raton ............................................................... 359 Arrastrar y dibujar con el raton ..................................................................................... 359 Pintar sobre formularios ...................................................................................................... 364 Tecnicas inusuales: Canal Alpha, Color Key y la API Animate ..................................... 366 Posicion, tamaiio, desplazamiento y ajuste de escala ....................................................... 367 .. La posicion del formulario ............................................................................................. 368 Ajuste a la ventana (en Delphi 7) ................................................................................. 368 El tamafio de un formulario y su zona de cliente ........................................................ 369
Restricciones del formulario .......................................................................................... 370 Desplazar un formulario ................................................................................................ 370 Un ejemplo de prueba de desplazamiento ............................................................... 371 Desplazamiento automatico ..................................................................................... 373 Desplazamiento y coordenadas del formulario ...................................................... 374 Escalado de formularios ................................................................................................. 376 Escalado manual del formulario .............................................................................. 377 Ajuste automatic0 de la escala del formulario ............................................................. 378 Crear y cerrar formularios ................................................................................................... 379 Eventos de creacion de formularios .............................................................................. 381 Cerrar un formulario ...................................................................................................... 382 Cuadros de dialog0 y otros formularios secundarios ........................................................ 383 Afiadir un formulario secundario a un programa ........................................................ 384 Crear formularios secundarios en tiempo de ejecucion .............................................. 385 Crear un unica instancia de formularios secundarios ........................................... 386 . Creacion de un cuadro de d~alogo ....................................................................................... 387 El cuadro de dialogo del ejemplo RefList .................................................................... 388 Un cuadro de dialog0 no modal ..................................................................................... 390 . Cuadros de dialog0 predefinidos ......................................................................................... 393 Dialogos comunes de Windows ................................................................................... 394 Un desfile de cuadros de mensaje ................................................................................. 395 Cuadros "Acerca den y pantallas iniciales ......................................................................... 396 ., Creacion de una pantalla inicial ................................................................................... 397
El objeto Application ............................................................................................................ 404 . Mostrar la ventana de la aplicacion .............................................................................. 406 Activacion de aplicaciones y formularios .................................................................... 407 Seguimiento de formularios con el objeto Screen ..................................................... 407 De eventos a hilos ................................................................................................................. 412 Programacion guiada por eventos ................................................................................. 412 Entrega de mensajes Windows ...................................................................................... 414 Proceso secundario y multitarea .................................................................................... 414 Multihilo en Delphi ........................................................................................................ 415 Un ejemplo con hilos ................................................................................................ 416 Verificando si existe una instancia previa de una aplicacion .......................................... 418 Buscando una copia de la ventana principal ................................................................ 418 Uso de un mutex .............................................................................................................. 419 Buscar en una lista de ventanas .................................................................................... 420 Controlar mensajes de ventana definidos por el usuario ............................................ 421 Creacion de aplicaciones MDI ............................................................................................ 422 MDI en Windows: resumen tecnico ............................................................................. 422 Ventanas marco y ventanas hijo en Delphi ........................................................................ 423 Crear un menu Window completo ................................................................................. 424 El ejemplo MdiDemo ...................................................................................................... 426 Aplicaciones MDI con distintas ventanas hijo .................................................................. 428
Formularios hijo y mezcla de menus ............................................................................ El formulario principal ................................................................................................... Subclasificacion de la ventana MdiClient .................................................................... Herencia de formularios visuales ........................................................................................ Herencia de un formulario base .................................................................................... Formularios polimorficos ............................................................................................... Entender 10s marcos ............................................................................................................. Marcos y fichas ............................................................................................................... Varios marcos sin fichas ................................................................................................. Formularios base e interfaces .............................................................................................. Uso de una clase de formulario base ............................................................................. Un truco adicional: clases de interposition ............................................................ Uso de interfaces ............................................................................................................. El gestor de memoria de Delphi ..........................................................................................
9 Creac~on componentes Delphi de
..
428 429 430 432 433 436 439 442 444 446 447 450 451 452
455
.................................................................................
Ampliacion de la biblioteca de Delphi ............................................................................... 456 Paquetes de componentes .............................................................................................. 4 5 6 Normas para escribir componentes ............................................................................... 458 Las clases basicas de componentes ............................................................................... 459 .. Creacion de nuestro primer componente ........................................................................... 460 El cuadro combinado Fonts ............................................................................................ 460 Creacion de un paquete .................................................................................................. 465 ~ Q u C detras de un paquete? ............................................................................... 466 hay Uso del cuadro combinado Fonts ................................................................................... 469 Los mapas de bits de la Component Palette ................................................................. 469 Creacion de componentes compuestos ............................................................................... 471 Componentes internos .................................................................................................... 471 Publicacion de subcomponentes .................................................................................... 472 Componentes externos .................................................................................................... 475 Referencias a componentes mediante interfaces .......................................................... 477 Un componente grafico complejo ........................................................................................ 481 Definition de una propiedad enumerada ...................................................................... 482 Escritura del metodo Paint ............................................................................................. 484 Adicion de las propiedades TPersistent ........................................................................ 486 Definition de un nuevo evento personalizado ............................................................. 488 Uso de llamadas de bajo nivel a la API de Windows ..... ....................................... 489 La version CLX: Llamadas a funciones Qt nativas ............................................... 490 Registro de las categorias de propiedades .................................................................... 490 Personalizacion de 10s controles de Windows ................................................................... 492 El cuadro de edicion numeric0 ...................................................................................... 494 Un editor numeric0 con separador de millares ...................................................... 495 El boton Sound ................................................................................................................ 496 Control de mensaje internos: El boton Active ........................................................... 498 Mensajes de componente y notificaciones .................................................................... 499 Mensajes de componentes ........................................................................................ 499 Notificaciones a componentes .................................................................................. 503 Un ejemplo de mensajes de componente ................................................................. 503
Un cuadro de dialog0 en un componente ........................................................................... Uso del componente no visual ....................................................................................... Propiedades de coleccion ............................................................................................... Definicion de acciones personalizadas ........................................................................ Escritura de editores de propiedades .................................................................................. Un editor para las propiedades de sonido ................................................................ Instalacion del editor de propiedades ..................................................................... Creacion de un editor de componentes ............................................................................... Subclasificacion de la clase TComponentEditor ......................................................... Un editor de componentes para ListDialog .................................................................. Registro del editor de componentes ........................................................................
10 Bibliotecas y paquetes
504 508 508 512 516 517 520 521 522 522 524
................................................................................................. 527
528 528 529 530 531 532 534 535 537 537 539 540 540 542 544 546 548 550 551 553 555 558 561
La funcion de las DLL en Windows ............................................................................ El enlace dinamico .......................................................................................................... Uso de las DLL ................................................................................................................ Normas de creacion de DLL en Delphi ....................................................................... Uso de las DLL existentes .................................................................................................... Usar una DLL de C++ .................................................................................................... Creacion de una DLL en Delphi ......................................................................................... La primera DLL en Delphi ...................................................................................... Funciones sobrecargadas en las DLL de Delphi ................................................... Exportar cadenas de una DLL ................................................................................. Llamada a la DLL de Delphi ...................................................................................... Caracteristicas avanzadas de las DLL en Delphi ............................................................ Cambiar nombres de proyecto y de biblioteca ............................................................. Llamada a una funcion DLL en tiempo de ejecucion .................................................. Un formulario de Delphi en una DLL .......................................................................... Bibliotecas en memoria: codigo y datos ............................................................................. Compartir datos con archivos proyectados en memoria ............................................. Uso de paquetes Delphi ........................................................................................................ Versiones de paquetes .................................................................................................... Formularios dentro de paquetes .......................................................................................... Carga de paquetes en tiempo de ejecucion ................................................................ Uso de interfaces en paquetes .................................................................................. Estructura de un paquete ..................................................................................................... 11 Modelado y programacih orientada a objetos (con ModelMaker)
...................567
568 569 569 571 572 574 575 576 576
Comprension del modelo interno de ModelMaker ............................................................ Modelado y UML .................................................................................................................. Diagramas de clase ........................................................................................................ Diagramas de secuencia ............................................................................................. Casos de uso y otros diagramas ..................................................................................... Diagramas no W ........................................................................................................ Elementos comunes de 10s diagramas ........................................................................... Caracteristicas de codification de ModelMaker .......................................................... Integracion Delphi / ModelMaker .................................................................................
Gestion del modelo de codigo ........................................................................................ El editor Unit Code Editor ............................................................................................. El editor Method Implementation Code Editor ........................................................... La vista de diferencias .................................................................................................... La vista Event Types View ............................................................................................. Documentacion y macros ..................................................................................................... Documentacion frente a comentarios ............................................................................ Trabajo con macros ......................................................................................................... Reingenieria de codigo ......................................................................................................... .. Aplicacion de patrones de diseiio .................................................................................. Plantillas de codigo ......................................................................................................... Detallitos poco conocidos ...................................................................................................
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
Una breve historia de OLE y COM ..................................................................................... Implementacion de IUnknow .............................................................................................. Identificadores globalmente unicos ............................................................................... El papel de las fabricas de clases .................................................................................. Un primer sewidor COM ..................................................................................................... Interfaces y objetos COM ............................................................................................... Inicializacion del objeto COM ....................................................................................... Prueba del sewidor COM ............................................................................................... Uso de las propiedades de la interfaz ........................................................................... Llamada a metodos virtuales ......................................................................................... Automatization ..................................................................................................................... Envio de una llamada Automatizacion ......................................................................... Creacion de un sewidor de Automatizacion ...................................................................... El editor de bibliotecas de tipos .................................................................................... El codigo del sewidor ..................................................................................................... Registro del sewidor de autornatizacion ...................................................................... Creacion de un cliente para el sewidor ........................................................................ El alcance de 10s objetos de automatizacion ................................................................ El senidor en un componente ...................................................................................... Tipos de datos COM ....................................................................................................... Exponer listas de cadenas y fuentes ....................................................................... Us0 de programas Office ................................................................................................ Uso de documentos compuestos .......................................................................................... El componente Container ............................................................................................... Uso del objeto interno .................................................................................................... Controles ActiveX ................................................................................................................. Controles ActiveX frente a componentes Delphi ........................................................ Uso de controles ActiveX en Delphi ............................................................................. Uso del control WebBrowser .................................................................................... Creacion de controles ActiveX ............................................................................................ Creacion de una flecha ActiveX .................................................................................... Afiadir Nuevas Propiedades ........................................................................................... Adicibn de una ficha de propiedades ............................................................................
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 ....................................................................................................
Parte I11 Arquitecturas orientadas a bases de datos en Delphi
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
Edicion de una columna de tabla ................................................................................. 707 Personalizacion de la cuadricula de una base de datos .................................................... 707 Pintar una DBGrid ...................................................................................................... 708 Una cuadricula que permite la seleccion multiple ................................................. 710 Arrastre sobre una cuadricula ........................................................................................ 712 Aplicaciones de bases de datos con controles estandar .................................................... 713 Imitacion de 10s controles data-aware de Delphi ....................................................... 713 Envio de solicitudes a la base de datos ......................................................................... 716 Agrupacion y agregados ....................................................................................................... 718 Agrupacion ...................................................................................................................... 718 Definicion de agregados ................................................................................................. 719 Estructuras maestroldetalles ................................................................................................ 721 Maestro/detalle con 10s ClientDataSet ..................................................................... 722 Control de errores de la base de datos ............................................................................ 723
14 Clientelsemidor con dbExpress
............................................................................
727
La arquitectura clientelservidor .......................................................................................... 728 Elementos del disefio de bases de datos .......................................................................... 730 Entidades y relaciones .................................................................................................... 730 Reglas de normalizacion ........................................................................................ 731 De las claves primarias a 10s OID ................................................................................. 731 Claves externas e integridad referencial ................................................................. 733 . . Mas restricciones ............................................................................................................ 734 Cursores unidireccianales ....................................................................................... 734 Introduccion a InterBase ...................................................................................................... 736 Uso de IBConsole ............................................................................................................ 738 Programacion de servidor en InterBase ........................................................................ 740 Procedimientos almacenados ................................................................................. 740 Disparadores (y generadores) ................................................................................... 741 La biblioteca dbExpress ....................................................................................................... 743 Trabajo con cursores unidireccionales ..................................................................... 743 Plataformas y bases de datos ........................................................................................ 744 Problemas con las versiones de controladores e inclusion de unidades .................... 745 Los componentes dbExpress ................................................................................................ 746 El componente SQLConnection .................................................................................... 747 Los componentes de conjuntos de datos de dbExpress ............................................... 751 El componente SimpleDataSet de Delphi 7 ........................................................... 752 El componente SQLMonitor .......................................................................................... 753 Algunos ejemplos de dbExpress .................................................................................... 754 Uso de un componente unico o de varios ..................................................................... 755 Aplicacion de actualizaciones .................................................................................. 755 . . Seguimiento de la conexion ..................................................................................... 756 Control del codigo SQL de actualizacion ............................................................... 757 Acceso a metadatos de la base de datos con SetSchemaInfo ................................ 758 Una consulta parametrica ............................................................................................... 760 Cuando basta una sola direccion: imprimir datas ....................................................... 762 Los paquetes y la cache ........................................................................................................ 765
Manipulacion de actualizaciones .................................................................................. 766 El estado de 10s registros ....................................................................................... 766 Acceso a Delta ........................................................................................................... 767 Actualizar 10s datos .................................................................................................... 768 Uso de transacciones ....................................................................................................... 771 Uso de InterBase Express ............................................................................................... 774 Componentes de conjunto de datos IBX ..................................................................... 776 Componentes administrativos IBX .......................................................................... 777 Creacion de un ejemplo IBX ....................................................................................... 777 Creacion de una consulta en vivo .................................................................................. 779 Control en InterBase Express ........................................................................................ 783 Obtencion de mas datos de sistema ............................................................................... 784 Bloques del mundo real ....................................................................................................... 785 Generadores e identificadores ........................................................................................ 786 Busquedas sin distincion entre mayusculas y minusculas .......................................... 788 Manejo de ubicaciones y personas ........................................................................... 790 Creacion de una interfaz de usuario .......................................................................... 792 Reserva de clases ............................................................................................................. 795 Creacion de un dialogo de busqueda ............................................................................. 798 Adicion de un formulario de consulta libre ................................................................ 800 15 Trabajo con ADO
.......................................................................................................... 803
Microsoft Data Access Componentes (MDAC) ............................................................. 805 Proveedores de OLE DB .............................................................................................805 Uso de componentes dbGo ................................................................................................... 807 Un ejemplo practico ................................................................................................... 808 El componente ADOConnection ............................................................................. 811 Archivos de enlace de datos ......................................................................................... 811 Propiedades dinamicas ....................................................................................................... 812 Obtencion de information esquematica ............................................................................ 813 Uso del motor Jet ............................................................................................................. 815 Paradox a traves de Jet ................................................................................................... 816 Excel a traves de Jet ....................................................................................................... 817 Archivos de texto a traves de Jet ............................................................................. 819 ., Importaclon y exportation ............................................................................................ 821 Trabajo con cursores ............................................................................................................. 822 . Ubicacion de cursor ................................................................................................... 822 Tipo de cursor .................................................................................................................. 823 Pedir y no recibir ............................................................................................................. 825 Sin recuento de registros ................................................................................................ 826 Indices de cliente ............................................................................................................. 826 . . . Repllcaclon ...................................................................................................................... 827 Procesamiento de transacciones .................................................................................... 829 Transacciones anidadas ........................................................................................... 830 Atributos de ADOConnection ................................................................................... 830 Tipos de bloqueo ............................................................................................................. 831 . . El bloqueo peslmlsta ............................................................................................. 832
Actualizacion de 10s datos ................................................................................................... Actualizaciones por lotes ............................................................................................... Bloqueo optimists ........................................................................................................... Resolution de conflictos de actualizacion .................................................................... Conjuntos de registros desconectados ................................................................................ Pooling de conexiones .......................................................................................................... Conjuntos de registros permanentes ............................................................................. El modelo de maletin ...................................................................................................... Unas palabras sobre ALIO.NET...........................................................................................
16 Aplicaciones DataSnap multicapa
832 834 836 839 840 841 843 844 845 847
.............................................................................
Niveles uno. dos y tres en la historia de Delphi ................................................................ 848 Fundamento tecnico de DataSnap ................................................................................. 850 La interfaz AppSener .................................................................................................... 850 Protocolo de conexion ..................................................................................................... 851 Proporcionar paquetes de datos ..................................................................................... 853 Componentes de soporte Delphi (entorno cliente) .................................................... 854 Componentes de soporte Delphi (entorno senidor) .................................................... 856 Construction de una aplicacion de ejemplo ...................................................................... 856 El primer senidor de aplicacion ................................................................................... 856 El primer cliente ligero .................................................................................................. 858 Adicion de restricciones a1 senidor .................................................................................... 860 Restricciones de campo y conjuntos de datos .............................................................. 860 Inclusion de propiedades de campo .............................................................................. 862 Eventos de campo y tabla ............................................................................................... 862 Adicion de caracteristicas a1 cliente ................................................................................... 863 Secuencia de actualization ............................................................................................ 864 Refresco de datos ............................................................................................................. 865 Caracteristicas avanzadas de DataSnap ............................................................................. 867 Consultas por parametros ............................................................................................... 868 Llamadas a metodos personalizados ............................................................................. 868 Relaciones maestroldetalle ............................................................................................. 870 Uso del agente de conexion ............................................................................................ 871 Mas opciones de proveedor ............................................................................................ 872 Agente simple de objetos ................................................................................................ 873 Pooling de objetos ........................................................................................................... 874 Personalizacion de paquetes de datos ...........................................................................874 17. CreacMn de componentes de bases de datos
...........................................................
877
El enlace de datos ................................................................................................................. 878 La clase TDataLink ......................................................................................................... 878 Clases de enlaces de datos derivadas ............................................................................ 879 Creacion de controles data-aware orientados a campos ..................................................880 Una ProgressBar de solo lectura ...................................................................................880 Una TrackBar de lectura y escritura .............................................................................884 Creacion de enlaces de datos personalizados ....................................................................887 Un componente visualizador de registros ....................................................................888
Personalizacion del componente DBGrid .......................................................................... Construir conjuntos de datos personalizados .................................................................... La definicion de las clases ............................................................................................. Apartado I: Inicio. apertura y cierre ............................................................................. Apartado 11: Movimiento y gestion de marcadores ..................................................... Apartado 111: Buffers de registro y gestion de campos ............................................... Apartado IV: De buffers a campos ................................................................................ Comprobacion el conjunto de datos basado en streams .............................................. Un directorio en un conjunto de datos ............................................................................... Una lista como conjunto de datos .................................................................................. Datos del directorio ......................................................................................................... Un conjunto de datos de objetos ..........................................................................................
18 Generation de informes con Rave
893 897 898 902 907 911 915 917 918 919 920 924
931
..
.............................................................................
Presentation de Rave ............................................................................................................ Rave: el entorno visual de creacion de informes ......................................................... El Page Designer y el Event Editor ..................................................................... El panel Property .................................................................................................. El panel Project Tree ................................................................................................. Barras de herramientas y la Toolbar Palette .......................................................... La barra de estado ..................................................................................................... Uso del componente RvProject ...................................................................................... Formatos de representacion ........................................................................................... Conexiones de datos ....................................................................................................... Componentes del Rave Designer ........................................................................................ Componentes basicos ...................................................................................................... Componentes Text y Memo ...................................................................................... El componente Section ............................................................................................. Componentes grhficos ............................................................................................... El componente FontMaster ...................................................................................... Numeros de pagina .................................................................................................... Componentes de dibujo ............................................................................................. Componentes de codigo de barras ........................................................................... Objetos de acceso a datos ............................................................................................... Regiones y bandas ........................................................................................................... El Band Style Editor ................................................................................................. Componentes data-aware ............................................................................................... El Data Text Editor ................................................................................................... De Text a Memo ........................................................................................................ Calculo de totales ...................................................................................................... Repeticion de datos en paginas ................................................................................ Rave avanzado ....................................................................................................................... Informes maestro-detalle ................................................................................................ Guiones de informes ....................................................................................................... Espejos .............................................................................................................................. Calculos a tope ................................................................................................................ CalcTotal ....................................................................................................................
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
Creacion de aplicaciones con sockets ................................................................................. 962 Bases de la programacion de sockets ............................................................................ 963 Configuracion de una red local: direcciones IP ................................................ 964 Nombres de dominio local ........................................................................................ 964 Puertos TCP ............................................................................................................... 964 Protocolos de alto nivel ............................................................................................ 965 Conexiones de socket ................................................................................................ 965 Uso de componentes TCP de Indy ................................................................................. 966 Envio de datos de una base de datos a traves de una conexion de socket ................ 970 Envio y recepcion de correo electronic0 ....................................................................... 973 Correo recibido y enviado .............................................................................................. 975 Trabajo con HTTP ................................................................................................................ 977 Obtencion de contenido HTTP ................................................................................. 978 La M I WinInet .......................................................................................................... 982 Un navegador propio ...................................................................................................... 983 Un sencillo servidor HTTP ............................................................................................ 985 Generacion de HTML ........................................................................................................... 987 Los componentes productores de codigo HTML de Delphi ........................................ 987 Generacion de paginas HTML ....................................................................................... 988 Creacion de paginas de datos ................................................................................. 990 Produccion de tablas HTML .......................................................................................... 991 Uso de hojas de estilo ..................................................................................................... 993 Paginas dinamicas de un servidor personalizado ........................................................ 994
20 Programacidn Web con WebBroker y WebSnap
....................................................997
Paginas Web dinarnicas .................................................................................................. 998 Un resumen de CGI ........................................................................................................ 999 Uso de bibliotecas dinamicas ....................................................................................... 1000 Tecnologia WebBroker de Delphi ..................................................................................... 1001 Depuracion con Web App Debugger ...................................................................... 1004 Creacion de un WebModule multiproposito ............................................................... 1007 Informes dinamicos de base de datos .......................................................................... 1009 Consultas y formularios ................................................................................................ 1010 Trabajo con Apache ...................................................................................................... 1014 Ejemplos practices .............................................................................................................. 1016 Un contador Web grafico de visitas ............................................................................ 1017 Busquedas con un motor Web de busquedas .............................................................. 1019 1021 WebSnap ............................................................................................................................ ., , . Gestion de varias paglnas ........................................................................................ 1025 Guiones de servidor ...................................................................................................... 1027 Adaptadores ................................................................................................................... 1030
Campos de adaptadores .......................................................................................... Componentes de adaptadores ................................................................................. Uso del Adapterpageproducer ........................................................................... Guiones en lugar de codigo .................................................................................... Encontrar archivos ........................................................................................................ WebSnap y bases de datos .................................................................................................. Un modulo de datos WebSnap ..................................................................................... El DataSetAdapter ........................................................................................................ Edicion de 10s datos en un formulario ........................................................................ Maestro/Detalle en WebSnap ................................................................................... Sesiones, usuarios y permisos ........................................................................................... Uso de sesiones .............................................................................................................. Peticion de entrada en el sistema ............................................................................ Derechos de acceso a una unica pagina ..............................................................
1030 1031 1031 1034 1035 1036 1036 1036 1039 1041 1043 1043 1045 1047
...........................................................................1049
Introduccion a IntraWeb ............................................................................................... 1050 De sitios Web a aplicaciones Web ........................................................................... 1051 Un primer vistazo interior ...................................................................................... 1054 Arquitecturas IntraWeb .......................................................................................... 1057 Creacion del aplicaciones IntraWeb ............................................................................ 1058 Escritura de aplicaciones de varias paginas .......................................................... 1060 Gestion de sesiones ................................................................................................. 1064 Integracion con WebBroker (y WebSnap) .............................................................. 1066 Control de la estructura ................................................................................................ 1068 Aplicaciones Web de bases de datos ................................................................................. 1070 Enlaces con detalles ...................................................................................................... 1072 Transporte de datos a1 cliente ...................................................................................... 1076
............................................................................................ 1079
1080 1080 1082 1083 1084 1085 1087 1090 1094 1098 1099 1103 1108 1109 1110 1111 1116
Presentacion de XML ......................................................................................................... Sintaxis XML basica .................................................................................................. XML bien formado ........................................................................................................ Trabajo con XML .......................................................................................................... Manejo de documentos XML en Delphi .............................................................. Programacion con DOM .................................................................................................... Un documento XML en una TreeView ................................................................... Creacion de documentos utilizando DOM ................................................................. Interfaces de enlace de datos XML ......................................................................... Validacion y esquemas ............................................................................................ Uso de la API de SAX .................................................................................................. Proyeccion de XML con transformaciones ................................................................. XML e Internet Express ..................................................................................................... El componente XMLBroker ......................................................................................... Soporte de JavaScript ................................................................................................... Creacion de un ejemplo ........................................................................................... Uso de XSLT .......................................................................................................................
Uso de XPath ................................................................................................................. XSLT en la practica ...................................................................................................... XSLT con WebSnap ...................................................................................................... Transformaciones XSL directas con DOM ................................................................. Procesamiento de grandes documentos XML ........................................................... De un ClientDataSet a un documento XML ............................................................ De un documento XML a un ClientDataSet ............................................................
23 Semicios Web y SOAP
...............................................................................................
Servicios Web ................................................................................................................... SOAP y WSDL .............................................................................................................. Traducciones BabelFish ........................................................................................ Creacion de un servicio Web ....................................................................................... Un servicio Web de conversion de divisas ............................................................... Publicacion del WSDL ............................................................................................ Creacion de un cliente personalizado ............................................................... Peticion de datos de una base de datos ................................................................... Acceso a 10s datos ................................................................................................... Paso de documentos XML ...................................................................................... El programa cliente (con proyeccion XML) ......................................................... Depuracion de las cabeceras SOAP ............................................................................ Exponer una clase ya existente como un servicio Web ............................................. DataSnap sobre SOAP ........................................................................................................ Creacion del semidor SOAP DataSnap ...................................................................... Creacion del cliente SOAP DataSnap ......................................................................... SOAP frente a otras conexion con DataSnap ............................................................. Manejo de adjuntos ............................................................................................................. Soporte de UDDI ................................................................................................................. ~ Q u C UDDI? .............................................................................................................. es UDDI en Delphi 7 .........................................................................................................
Parte V ApCndices
1130 1130 1131 1134 1135 1136 1137 1139 1139 1140 1142 1143 1144 1145 1145 1148 1148 1149 1151 1151 1153
1157 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? ~BUSC~ una 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-
rablemente el estar familiarizado con el lenguaje Delphi, sobre todo despues de 10s capitulos introductorios. El libro comienza a tratar 10s temas con detenimiento de forma inmediata; se ha eliminado gran parte del material introductorio incluido en otros textos.
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
En una herramienta de programacion visual como Delphi, el papel del Entorno de Desarrollo Integrado (IDE, Integrated Development Environment) resulta a veces mas importante que el lenguaje de programacion. Delphi 7 ofrece algunas nuevas caracteristicas muy interesantes sobre el maravilloso IDE de Delphi 6. En este capitulo examinaremos estas nuevas caracteristicas, a1 igual que las caracteristicas aiiadidas en otras versiones recientes de Delphi. Tambien comentaremos unas cuantas caracteristicas tradicionales de Delphi que no son bien conocidas u obvias a 10s recien llegados. Este capitulo no es un tutorial completo sobre el IDE, que necesitaria mucho mas espacio; principalmente es un conjunto de consejos y sugerencias dirigidas a1 usuario medio de Delphi. Si se trata de un programador novato, no se preocupe. El IDE de Delphi es bastante intuitivo. El propio Delphi incluye un manual (disponible en formato Acrobat en el CD Delphi Companion Tools) con un tutorial que presenta el desarrollo de aplicaciones en Delphi. Puede encontrar una introduccion mas sencilla a Delphi y su IDE en otros textos. Pero en este libro asumiremos que ya sabe como llevar a cab0 las operaciones basicas del IDE; todos 10s capitulos despues de este se centraran en cuestiones y tecnicas de programacion. Este capitulo trata 10s siguientes temas: Navegacion del IDE. El editor.
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
El editor de codigo es donde se escribe el codigo. El mod0 mas obvio de escribir codigo en un entorno visual implica responder a eventos, comenzando por 10s eventos enlazados con las operaciones realizadas por 10s usuarios del programa, como hacer clic sobre un boton o escoger un elemento de un cuadro de lista. Puede usarse el mismo enfoque para manejar eventos internos, como 10s eventos que implican cambios en bases de datos o notificaciones del sistema operativo. A medida que 10s programadores adquieren un mayor conocimiento sobre Delphi, suelen comenzar escribiendo basicamente codigo gestor de eventos y despues escriben sus propias clases y componentes y, normalmente, acaban invirtiendo la mayor parte de su tiempo en el editor. Ya que este libro trata mas conceptos que la programacion visual e intenta ayudar a dominar toda la potencia de Delphi, a medida que el testo avance se vera mas codigo y menos formularios.
blpbi w-pefliiik,~mmpi&pcbdi& + G& 91 g para, qv fwnicqk bajc Fioux. R e ipte~eaante:vt&w CLS D& 7, ya que ip versi6n para e lenguaje lM@i K y b se dkstribuyejunto con el pv&tr;topam ~ind'ms, dc
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.
po de diseiio y un conjunto distinto en tiempo de depuracion. Del mismo modo, podria necesitarse una disposicion cuando se trabaje con formularios y otra completamente diferente cuando se escriban componentes o codigo de bajo nivel mediante el unico uso del editor. Reorganizar el IDE para cada una de estas necesidades es una tarea tediosa. Por este motivo, Delphi permite almacenar una determinada disposicion de las ventanas del IDE (llamada escritorio o escritorio global (Global Desktop) para distinguirlo de un escritorio de proyecto (Project Desktop) con un nombre y recuperarla rapidamente. Tambien se puede convertir a una de estas agrupaciones en la configuracion predeterminada para la depuracion, de manera que se recuperara automaticamente cuando se inicie el depurador. Todas estas caracteristicas estan disponibles en la barra de herramientas Desktops. Tambien puede trabajar con las configuraciones de escritorio mediante el menu View>Desktops. La informacion de configuracion de escritorio se guarda en archivos DST (dentro del directorio b i n de Delphi), que en realidad son archivos INI. Los parametros guardados incluyen la posicion de la ventana principal, el Project Manager, la Alignment Palette, el Object Inspector (incluida su configuracion de categorias de propiedades), el editor de ventanas (con el estado del Code Explorer y la Message View) y muchos otros, ademb del estado de anclaje de las diversas ventanas. Este es un pequeiio extract0 de un archivo DST, que deberia resultar facil de leer:
[Main Window] Create=l Visible=l State=O Left=O Top=O Width=1024 Height=105 ClientWidth=1016 ClientHeight=78
Las configuraciones de escritorio tienen mas fierza que las configuraciones de proyecto, que se guardan en un archivo DSK con una estructura similar. Las configuraciones de escritorio ayudan a eliminar problemas que pueden suceder
cuando se traslada un proyecto de una maquina a otra (o de un desarrollador a otro) y es necesario reorganizar las ventanas a1 gusto. Delphi separa las configuraciones de escritorio globales por usuario y las configuraciones de escritorio por proyecto, para ofrecer un mejor soporte a equipos de desarrollo.
-
----
----
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.
mcnte la mayoria de las tareas se realizaran mediante atajos de teclado y de menu. La barra de menu no cambia demasiado como reaccion a la operacion actual: se necesita hacer clic con el boton derecho del raton para conseguir una lista de las operaciones que se pueden realizar en la ventana o componente actual. La barra de menu cambia en gran medida segun las herramientas y asistentes de terceras partes que se hayan instalado. En Delphi 7, ModelMaker dispone de su propio menu. Si se instalan modulos adicionales como GExperts se pueden contemplar otros menus. Un importante menu afiadido a Delphi en las versiones mas recientes es el menu Window del IDE. Este menu muestra la lista de las ventanas abiertas; antes, se podia obtener esta lista mediante la combinacion de teclas Alt-0 o la opcion de menu View>Window List. El menu Window resulta realmente practico, ya que las ventanas suelen acabar detras de otras y son dificiles de encontrar. Puede controlarse el orden alfabetico de este menu mediante un parametro del Registro de Windows: hay que encontrar la subclave Main Window de Delphi (dentro de HKEY CURRENT USER\Software\Borland\Delphi\7.0). Esta c l a w del ~ e g i s t r o utiliz; una cadena (en lugar de valores booleanos), donde -1 y True indican verdadero y 0 y False indican falso.
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;
El mismo elemento puede editarse visualmente en la ventana que muestra la figura 1.3, dentro de la ventana To-Do List. La excepcion a esta regla de las dos vias es la definition de elementos pendientes en el ambito del proyecto. Debe aiiadir directamente estos elementos a la lista. Para hacer esto, puede utilizar la combinacion de teclas Control-A dentro de la ventana To-Do List o hacer clic con el boton derecho sobre la ventana y seleccionar la opcion Add en el menu desplegable. Estos elementos se guardan en un archivo especial con el mismo nombre raiz que el archivo del proyecto y una extension .TODO. Pueden utilizarse diversas opciones con un comentario TODO.Puede usarse -0 (como en el ejemplo anterior) para indicar el propietario (el programador que escribio el comentario), la opcion -c para indicar una categoria, o simplemente un numero de 1 a 5 para indicar la prioridad ( 0 , o ningun numero, indica que no se establece ningun nivel de prioridad). Por ejemplo, a1 usar el comando A d d
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
.I ~ o a ~ e
10Marco
--'
IWWY
-A
7 Check comp~lerfelhngs
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-DO List, 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
siguiente pulsando Control-Tab (o Mayus-Control-Tab para movernos en la direccion opuesta). Podemos pasar de una ficha del editor a la siguiente pulsando Control-Tab (o Mayus-Control-Tab para movernos en la direccion opuesta). Se puede arrastrar y soltar las solapas con 10s nombres de unidad situadas en la parte superior del editor para cambiar su orden, para que se pueda usar un simple Control-Tab para moverse entre las unidades en que se trabaje en un momento dado. El menu local del editor posee tambien un comando Pages, que lista todas las fichas disponibles en un submenu, muy util cuando se cargan muchas unidades. Tambien se pueden abrir varias ventanas del editor, cada una de ellas con multiples fichas o pestaiias. Hacer esto es la unica manera de inspeccionar el codigo fuente de dos unidas a la vez. (Realmente, cuando es necesario comparar dos unidades de Delphi, tambien se puede utilizar Beyond Compare, una herramienta de comparacion de archivos muy barata y maravillosa escrita en Delphi y disponible a traves de www.scootersoftware.com.) En el cuadro de dialogo Editor Properties, hay algunas opciones que afectan a1 editor. Sin embargo, para definir la propiedad AutoSave del editor, que guarda 10s archivos del codigo fuente cada vez que se ejecuta el programa (y evita que se pierdan 10s datos en caso de que el programa sufra daiios importantes en el depurador), tenemos que ir a la ficha Preferences del cuadro de dialogo Environment Options. El editor de Delphi proporciona muchos comandos, incluyendo algunos que se remontan a sus ancestros de emulacion de WordStar (de 10s primeros compiladores Turbo Pascal). N o vamos a comentar 10s distintos parametros del editor, ya que son bastante intuitivos y estan bien descritos en la ayuda disponible. Aun asi, fijese en que la pagina de ayuda que describe 10s atajos de teclado es accesible de una sola vez solo si se busca el elemento del indice shortcuts.
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.
Podemos usar el Code Explorer para desplazarnos por el editor. A1 hacer doble clic sobre una de las entradas del Code Explorer, el editor pasa a la declaracion correspondiente. Tambien podemos modificar nombres de variables, propiedades y m6todos directamente en el Code Explorer. Sin embargo, si se desea utilizar una herramienta visual para trabajar con las clases, ModelMaker ofrece muchas mas caracteristicas. Aunque todo esto resulta bastante obvio a 10s cinco minutos de comenzar a usar Delphi, algunas caracteristicas del Code Explorer no se pueden utilizar de una forma tan intuitiva. Lo importante es que el usuario tiene control total sobre el mod0 en que aparece dispuesta la informacion y que se puede reducir la profundidad del arbol que aparece en esta ventana cuando se personaliza el Code Explorer. Si reducimos el arbol, podremos realizar las elecciones con mayor rapidez. Podemos configurar el Code Explorer mediante la pagina de Environment Options correspondiente, como se muestra en la figura 1.5.
Figura 1.5. Se puede configurar el Code Explorer mediante el cuadro de dialogo Environment Options.
Fijese en que a1 eliminar la seleccion de uno de 10s elementos de Explorer Categories situados en la parte derecha de esta pagina del cuadro de dialogo, el Explorer no elimina 10s elementos correspondientes, simplemente aiiade el nodo a1 arbol. Por ejemplo, si se elimina la seleccion de la casilla Uses, Delphi no oculta la lista de unidades usadas; a1 contrario, las unidad usadas aparecen en la lista como nodos principales en lugar de permanecer en la carpeta Uses. Es una buena idea eliminar la seleccion de Types, Classes y VariablesIConstants. Dado que cada elemento del arbol Code Explorer tiene un icono que indica su tipo, la organizacion por campo y metodo parece menos importante que la organi-
zacion por especificador de acceso. Es preferible mostrar todos 10s elementos en un grupo unico, puesto asi no es necesario pulsar el raton tantas veces para llegar a cada uno de 10s elementos. En realidad, la posibilidad de seleccionar elementos en el Code Explorer supone una forma muy comoda de desplazarnos por el codigo fuente de una unidad amplia. Cuando hacemos doble clic sobre un metodo en el Code Explorer, el foco se desplaza a la definicion de la declaracion de clase (en la parte de interfaz de la unidad). Se puede usar la combinacion Control-Mayus junto con las teclas de cursor arriba y abajo para saltar de la definicion de un metodo o procedimiento en la parte de interfaz de una unidad a su definicion completa en la parte de implernentacion o volver hacia atras (es lo que se llaman Module Navigation).
.-
NOTA: Algunas de las categorias del explorador que aparecen en la figura 1.5 son mas utilizadas por el Project Explorer que por el Code Explorer. Entre estas se encuentran las opciones de agrupamiento vi r t ua 1s,
!d e Introduced.
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.
Podemos, por ejemplo, pulsar Control y hacer clic sobre el identificador TLabel para abrir su definition en el codigo de la VCL. Cuando seleccionamos referencias, el editor conserva la pista de las diversas posiciones a las que se ha movido y gracias a ella podemos pasar de una referencia a otra (de nuevo como en un explorador Web mediante 10s botones Browse Back y Browse Forward que se encuentran en la esquina superior derecha de las ventanas o mediante las combinaciones Alt-Flecha izda. o Alt-Flecha dcha.). Tambien podemos hacer clic sobre las flechas desplegables proximas a los botones Back y Forward para ver una lista pormenorizada de las lineas de 10s archivos de codigo fuente a las que ya hemos accedido, para tener mayor control sobre 10s movimientos adelante y atras. Ahora cabe preguntarse como podemos saltar directamente al codigo fuente de la VCL si no forma parte de nuestro proyecto. El editor no solo puede encontrar las unidades de la ruta de busqueda (Search, que se compila como parte del proyecto), sino tambien aquellas que estan en las rutas Debug Source, Browsing y Library de Delphi. La busqueda se realiza en estos directorios en el mismo orden en que aparecen aqui enumerados y podemos definirlos en la ficha Directories/Conditionals del cuadro de dialogo Project Options y en la ficha Library del cuadro de dialogo Environment Options. Por defecto, Delphi aiiade 10s directorios de codigo fuente de la VCL a la ruta Browsing del entorno.
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;
Delphi aiiadira tambien el metodo setvalue a la declaracion de clase y proporcionara una implementacion predefinida para ese metodo.
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
Code Insight del cuadro de dialogo Editor Properties. Otra caracteristica en el caso de funciones con parametros es la inclusion de parentesis en el codigo creado y la aparicion inmediata de la ventana de sugerencia de la lista de parametros. Cuando se escribe := despues de una variable o propiedad, Delphi listara todas las demas variables u objetos del mismo tipo, ademas de 10s objetos que tengan propiedades de ese tipo. Mientras la lista permanece visible, podemos hacer clic con el boton derecho del raton sobre ella para modificar el orden de 10s elementos, clasificandolos por alcance o por nombre y tambien podemos adaptar el tamaiio de la ventana. Desde Delphi 6, Code Completion funciona ademas en la parte de interfaz de una unidad. Si pulsamos Control-Barra espaciadora mientras el cursor esta dentro de la definition de clase, obtendremos una lista de 10s metodos virtuales que se pueden sobrescribir (como por ejemplo, 10s metodos abstractos), 10s metodos de las interfaces implementadas, las propiedades de clase basica y, por ultimo, 10s mensajes del sistema que se pueden controlar. A1 seleccionar uno de ellos, aiiadiremos sencillamente el metodo adecuado a la declaracion de clase. En este caso concreto, la lista Code Completion permite la seleccion multiple.
..
Hay algunas caracteristicas avanzadas de la funcion Code Completion que no resultan faciles de ver. Una particularmente util esta relacionada con el descubrimiento de 10s simbolos en unidades no utilizadas por nuestro proyecto. Cuando recurrimos a ella (con Control-Barra espaciadora) sobre una linea en blanco, la lista incluye tambien simbolos de unidades comunes (como Math, StrUtils y DateUtils) todavia no incluidas en la sentencia uses de la unidad en uso. A1 seleccionar uno de estos simbolos externos, Delphi aiiade la unidad a la sentencia uses de forma automatics. Esta caracteristica (que no funciona dentro de expresiones) esta dirigida por una lista de unidades adicionales que puede ser personalizada, almacenada en la clave de registro \Delphi\7.0\CodeCompletion\ExtraUnits.
L.
b capaoidad de explorar la d e c h r w i h de elementos de la fista.de campletitud de codigo a1 mantener pul'sadd la teela Control y'hacer chc sohre cualquier identificador de la Ikta.
TRgC(r:,l&fpG 7
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, es la unica propiedad de la etiqueque ta 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.
--
ti
h e
Q+fcrolim
J
Ths IS a simple version
Figura 1.6. La vista Diagram rnuestra relaciones entre cornponentes (e incluso perrnite establecer esas relaciones).
Aunque se pueda emplear la vista en diagrama para establecer relaciones, su funcion principal es documentar nuestro diseiio. Por esa razon: es importante que se pueda imprimir el contenido de dicha vista. Al usar la orden estandar File>Print mientras este activada la vista en diagrama, Delphi nos indica que seleccionemos las opciones para personalizar la impresion, como se puede ver en la figura 1.7.
rM base
La informacion de la vista Diagram se guarda en un archivo separado, no como parte del archivo DFM. Delphi 5 empleaba 10s archivos de informacion en tiempo de diseiio (DTI), que tenian una estructura similar a 10s archivos INI. Delphi 6 y 7 todavia pueden leer el antiguo formato .DTI, per0 usan el nuevo formato Delphi Diagram Portfolio (.DDP). Estos archivos utilizan aparentemente el formato binario DFM (o uno similar), por lo que se pueden editar como texto. Obviamente todos estos archivos son inutiles en tiempo de ejecucion (no tiene sentido incluirlos en la compilacion del archivo ejecutable).
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 . - C - I 1, , L 1 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 l l I l g U l i l l .V i
. ,
,. .,
..,-.I,.., . Y -,
U l l U 1IIUC;llU IllilJ
,A,
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).
Cuando este terminado el diseiio de un formulario, podemos emplear la orden L o c k C o n t r o l s del menu Edit para evitar cambiar por equivocacion la posicion de una componente en un formulario. Esto resulta util, sobre todo teniendo en cuenta que las operaciones Undo en 10s formularios son limitadas (solo se puede recuperar elementos eliminados), per0 la definicion no es permanente. Entre otras de sus caracteristicas, el Form Designer ofrece diversas ventanas de sugerencia: A1 mover el punter0 sobre un componente, en la sugerencia aparece el nombre y el tipo del componente. Desde la version 6, Delphi ofrece sugerencias extendidas, con datos sobre la posicion del control, el tamaiio, el orden de tabulacion y mas. Esta es una mejora de la configuracion del entorno Show Component Captions que se puede mantener activada. Cuando adaptamos el tamaiio de un control, en la sugerencia aparece el tamaiio actual (las propiedades W i d t h y H e i g h t ) . Por supuesto, estas caracteristicas estan disponibles solo para controles, no para componentes no visuales (que estan indicados en el Form Designer mediante iconos). A1 mover un componente, la sugerencia indica la posicion actual (las propiedades L e f t y Top). Por ultimo, se pueden guardar 10s archivos DFM (Delphi Form Module, Modulo de Formulario Delphi) en el formato de recurso binario tradicional, en lugar de hacerlo como texto normal que es el comportamiento predeterminado. Esta opcion se puede modificar en el caso de un formulario individual, con el menu de metodo abreviado del Form Designer o establecer un valor predefinido para 10s formularios nuevos que creemos en la ficha Designer del cuadro de dialogo Environment Options. En la misma ficha, podemos especificar tambien si 10s formularios secundarios de un programa se crearan automaticamente a1 arrancar, una decision que siempre se podra modificar en el caso de cada formulario individual (usando la ficha Forms del cuadro de dialogo Project Options). Disponer de archivos DFM almacenados como texto permite trabajar de manera mas eficaz con sistemas de control de versiones. Los programadores no se aprovecharan mucho de esta caracteristica, ya que se podria simplemente abrir el archivo DFM binario en el editor de Delphi con un comando especifico desde el menu de metodo abreviado del diseiiador. Por otra parte, 10s sistemas de control de versiones necesitan guardar la version textual de 10s archivos DFM para ser capaz de compararlos y extraer las diferencias entre dos versiones del mismo archivo. En cualquier caso, si se utilizan archivos DFM como texto, Delphi 10s convertira a un formato de recurso binario antes de incluirlos en el archivo ejecutable del programa. Los archivos DFM estan enlazados a su ejecutable en formato binario para reducir el tamaiio del archivo ejecutable (aunque no esten comprimidos) y para mejorar el rendimiento en tiempo de ejecucion (se pueden cargar mas rapido).
NOTA: Los archivos de texto DPM resultan m b faciles de tramportar de una version a otra de Delphi que sus versiones binarias. Aunque una version mas antigua de Delphi puede no acevtar una nueva propiedad de un control en un archivo DFM ireado por u veni6n posterior be Delphi, la & version anterior si sera capaz de leer el resto del archivo de texto DFM. Sin a , . t , :. . , . A, -Z.-.Ar. .... IIUGVU c~yu A,+,. A, GuIualgu, A I, LYGIJIUU ,A- IGUGULG UG r\-1-L:L U m a u ~u ....-.., , , :+ UG uacua, SI la 111aa c~.-.:~,+, UGI~ u la version anterior no podra leer en absoluto 10s archivos binarios DFM mas recientes. hcluso aunque no suene probable, recuerde que 10s sistemas que funcionan con 64 bits e s t b a la vuelta de la esquina. Si tiene dudas, guarde 10s archivos en formato texto DFM. Fijese tarnbien en que todas las versiones de Delphi soportan DFM en formato texto, usando la herrarnienta en linea de comandos convert que se encuentra en el directorio bin. Por ultimo, tenga presente que la biblioteca CLS utiliza la extension XFM en lugar de la extension DFM, tanto en Delphi como en Kylix.
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 .
po de diseKo utilimdo d meet ImpMor. Este n s a s l linodeloInterfaced por Component Reference ('Rdeienciasde G~m~pobetrte hterfaz) presentado en KyIix/Delpbi 6, en &qxk IT& c?mp&&nk$ pueden implemmtar y mantener referenciae* b interfaces siempre qae I ihterfaces eat& a w 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.
I
j
1
B F d
Charsel
ITFant)
'DEFAULT-CHARSET
Cob
' W cWindowText
Existe una segunda forma, miis compteja, de personalizar el Object Ins. . pector: una fuente personalizada para todo el Object Inspector, para que su texto resulte mhs visible. Esta caracteristica resulta especialmente util en el caso de presentaciones publicas.
P
Categorias
propiedades
Delphi incluye tambien el concept0 de categorias de propiedades, activadas mediante la opcion Arrange del mcnu local del Object Inspector. Si se activa
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:
Fijcse en que la vista en arbol esta sincronizada con el Object Inspector y con el Form Designer, de tal mod0 que cuando escogemos un elemento y cambiamos el foco en una de estas tres herramientas, tambien cambiara en las otras dos . Ademas de la relacion padrelhijo, la Object TreeView muestra tambien otras relaciones, como la de propietariolposeido, componentelsubobjeto, coleccion/elemento, y otras especificas como conjunto de datos/conexion y fuente de datosl relaciones del conjunto de datos. A continuacion, se puede ver un ejemplo de la estructura de un menu en forma de arbol:
L d 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
editores personalizados de componentes). Podemos incluso eliminar elementos del arbol. La vista en arbol sirve tambien como editor de colecciones, como podemos ver a continuacion en el caso de la propiedad C o l u m n s de un control Listview. En esta ocasion, no solo podemos reorganizar y eliminar 10s elementos existentes, sin0 tambien aiiadir elementos nuevos a la coleccion.
1Folrnl
[-I I J
-
0 Bullon2
, :
Columns
4 o .r L ~ r c o l w m L: 1 TL~~tCalurnn
2 . TL~stCalumn
4 3 - T L~slCd.mm
subrncnu para modificar la pagina activa. en particular cuando la pagina que se neccsita no esta visible en pantalla.
L
TRUCO: Se puede establecer el orden de las entradas en el submenu Tabs para que tengan el misrno orden que la propia paleta, en lugas de un orden alfabetico. Para hacer esto, hay que ir a la seccion Main Window del Registroi para Delphi (dentro de \Sof tware\Borland\Delphi\7 .0 para el usuario actual) y dar a la clave Sort Palette Tabs Menu el valor de 0 (falso).
Una importante caracteristica no documentada de la Component Palette es la posibilidad de activar un "seguimiento directo". Si configuramos claves especiales del Registro. podemos seleccionar una ficha de la paleta a1 movernos sobre la solapa, sin tener que hacer clic con el raton. Se puede aplicar esta misma caracteristica a las barras de desplazamiento de 10s componentes situadas a ambos lados de la paleta, que aparecen cuando hay demasiados componentes en la ficha. Para activar csta caracteristica oculta, hay que aiiadir una clave Extras dentro de la seccion \ H K E Y C U R R E N T USER\Software\Borland\Delphi\7.0. Bajo esta clave ST introducen-dos valores de cadena, Auto Palet teselect y Auto Palette S c r o 11 y definimos cl valor de cada cadcna como '1'
Ahora, si se modifica el nombre del objeto, su etiqueta o su posicion, por ejemplo, o se aiiade una nueva propiedad, estos cambios pueden copiarse y volver a pegarse en un formulario. Estos son algunos cambios de muestra:
object B u t t o n l : T B u t t o n L e f t = 152 Top = 1 0 4 Width = 75 H e i g h t = 25 Caption = ' M i Boton' TabOrder = 0 F o n t .Name = ' Arial' end
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.
De manera predeterminada, el nombre de la plantilla es el nombre del primer componente seleccionado, seguido de la palabra Template. El icono predefinido de la plantilla es tambien el icono del primer componente seleccionado, per0 se puede sustituir con un archivo de icono. El nombre que se de a la plantilla del componente sera el utilizado para describirlo en la Component Palette (cuando Delphi muestre la sugerencia contextual). Toda la informacion sobre las plantillas de componentes se almacena en un unico archivo, D E L P H I 3 2 .DCT, per0 aparentemente no hay forma de recuperar dicha informacion y editar una plantilla. Sin embargo, lo que si se puede hacer es colocar la plantilla de componentes en un formulario completamente nuevo, editarlo e instalarlo de nuevo como una plantilla de componentes utilizando el mismo nombre. De este mod0 se podra sobrescribir la definicion anterior.
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.
Podemos ver un ejemplo sencillo, llamado Framesl, en la figura 1.8. Una imagen realmente no significa mucho, deberia abrir el programa o reconstruir uno similar si desea comenzar a utilizar frames.
T
Framel
Framel
All
Figura 1.8. El ejemplo Framesl demuestra el uso de marcos El marco (a la izquierda) y su instancia en un formulario (a la derecha) permanecen en sincronia.
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
Entre las caracteristicas avanzadas del Project Manager, se encuentra la funcion de arrastre de archivos de codigo fuente desde carpetas de Windows o desde el Windows Explorer a un proyecto de la ventana del Project Manager para aiiadirlos a un proyecto (tambien se soporta este comportamiento para abrir archivos en el editor de codigo). Podemos ver facilmente que proyecto esta seleccionado y cambiarlo utilizando el cuadro combinado de la parte superior de la ventana o utilizando la flecha hacia abajo que se encuentra junto a1 boton Run en la barra de herramientas de Delphi. Ademas de aiiadir archivos y proyectos de Pascal, se pueden aiiadir archivos de recurso de Windows a1 Project Manager; estos se compilan junto con el proyecto. Sencillamente, hay que desplazarse a un proyecto, seleccionar Add en el menu de metodo abreviado y escoger Resource File (*.rc) como tipo de archivo. Este archivo de recurso se unira automaticamente a1 proyecto, incluso aunque no haya una directiva $R correspondiente. Delphi guarda 10s grupos de proyectos con la extension .BPG (Borland Project Group). Esta caracteristica procede del C++ Builder y de 10s antiguos compiladores Borland C++, un historial que resulta claramente visible a1 abrir el codigo fuente de un grupo de proyectos, que basicamente corresponde a1 de un archivo makefile de un entorno de desarrollo C/C++. Veamos un ejemplo:
#---------------------------------------------------------M A K E = $ (ROOT)\bin\make.exe - $ (MAKEFLAGS) -f$** DCC = $ (ROOT)\bin\dcc32. exe $ * * BRCC = $ (ROOT)\bin\brcc32. exe $ * * #---------------------------------------------------------PROJECTS
=
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
escoger su icono. Otras posibilidades de Project Options estan relacionadas con el compilador y el editor de enlaces de Delphi, la informacion sobre la version y el uso de paquetes en tiempo de ejecucion. Existen dos formas de configurar las opciones del compilador. Una es utilizar la ficha Compiler del dialogo Project Options. La otra es definir o eliminar las opciones individuales del codigo fuente con las ordenes { $x+} o { $x-} , en las que se reemplazaria la X por la opcion que queramos definir. Esta segunda tecnica resulta mas flexible, puesto que permite modificar una opcion solo para un archivo de codigo fuente concreto o incluso solarnente para unas cuantas lineas de codigo. Las opciones del nivel de fuente sobrescriben las opciones del nivel de compilacion. Todas las opciones de un proyecto se guardan automaticamente con el, per0 en un archivo a parte con una extension .DOF. Este es un archivo de texto que se puede editar facilmente. No se deberia eliminar dicho archivo si se ha modificado alguna de las opciones predefinidas. Delphi tambien guarda las opciones del compilador en otro formato, en un archivo CFG, para la compilacion desde la linea de comandos. Los dos archivos poseen un contenido similar per0 tienen un formato distinto: el compilador de la linea de comandos dcc no puede usar archivos .DOF, sino que necesita el formato .CFG. Tambien se pueden guardar las opciones del compilador pulsando Control-00 (pulsar la tecla 0 dos veces mientras se mantiene pulsada la tecla Control). Esto inserta, en la parte superior de la unidad actual, directivas de compilador que corresponden a las opciones de proyecto en uso, como en el siguiente listado:
I$A+,B-, C+,D+, E - , F- ,G+,Ht, I t , J t , K - , L t , M - , N t , O+, P t , Q-,R-, ,U-,vt, W-,X+,Yt,Zl) {$MINSTACKSIZE $ 0 0 0 0 4 0 0 0 ) {$MAYSTACKSIZE $OOIOOOOO) {$IMAGEBASE $ 0 0 4 0 0 0 0 0 ) {SAPPTYPE G U I ) ISWARN SYMBOL-DEPRECATED O N ) {$WARN S Y M B O L - L I B R A R Y ON) {$WARN SYMBOL-PLATFORM ON) {$WARN U N I T - L I B R A R Y O N ) {$WARN UNIT-PLATFORM ON) {$WARN UNIT-DEPRECATED O N ) {$WARN HRESULT-COMPAT ON) {$WARN HIDING-MEMBER O N ) {$WARN HIDDEN-VIRTUAL ON) {$WARN GARBAGE O N ) {$WARN BOUNDS-ERROR O N ) {$WARN ZERO-NIL-COMPAT ON) {$WARN STRING-CONST- TRUNCED O N ) {$WARN FOR-LOOP-VAR-VARPAR ON) {$WARN TYPED-CONS T-VARPAR O N ) {$WARN ASG- TO- TYPED-CONST O N ) {$WARN CASE-LABEL-RANGE ON) {$WARN FOR-VARIABLE ON) 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
CONS TRUCTING-ABS TRACT ON) {$WARN COMPARISON-FALSE COMPARISON- TRUE ON) COMPARING- S IGNED- UNSIGNED ON) COMBINING- S I G N E D UNSIGNED ON) UNSUPPORTED-CONS TRUCT ON) FILE-OPEN ON) FILE-OPEN-UNITSRC ON) BAD-GLOBAL-SYWBOL ON) DUPLICATE-CTOR-DTOR ON) INVALID-DIRECTIVE ON) PACKAGE-NO- L I N K O N ) PACKAGED- THREADVAR ON) I M P L I C I T - IMPORT ON) HPPEMI T- IGNORED ON) NO-RETVAL ON) USE-BEFORE-DEF ON) FOR-LOOP-VAR-UNDEF ON) UNIT--MISMATCH ON) NO-CFG-FILE-FOUND ON) MESSAGE-DIRECTIVE ON) IMPLICIT-VARIANTS ON) UNICODE- TO-LOCALE ON) LOCALE- TO- UNICODE ON) IMAGEBASE-MULTIPLE ON) SUSPICIOUS-TYPECAST ON) PRIVATE-PROPACCESSOR ON) UNSAFE- T Y P E O F F ) UNSAFE-CODE OFF) UNSAFE-CAST O F F )
ON)
archivo ejecutable cualquier archivo de recurso opcional, como el archivo RES del proyecto, que contiene su icono principal y 10s archivos DFM de 10s formularios. Se pueden entender mejor 10s pasos de la compilacion y seguir el hilo de lo que ocurre durante dicha operacion si activamos la opcion Show Compiler Progress (en la pagina Preferences del cuadro de dialog0 Environment Options).
ADVERTENCIA: Del@
-
no siempre tiene claro cuhijo feconstruir las unidadcs basadas en ah.bs unidades que se han m o d i f i d . Esto s s sobre todo verdad en 10s casos (y son muchos) en que la i n t e r v W 4 n -deIusuario confunde la logica del wmpilador. Por ejemplo, r m m b n u adiivas, modi#3catAcodigo.fuentedesde el exterior del IDE. copiar archiui,s t &.&go k h ae ~n&uos Q wchivos DCU a1 disco, o t m e r multiples ~ o p i a de un et s archiyo fuentc d c ' o n i ~ la ruta dc busqucda puede e s t r o p k el proceso en dc compiIaclln. Ctqh vez que el compilador muestra &6n naensaje de error estraiio, 1~primer0 que deberiamos hacer es utilizarla oFden B u i l d XLl pard sincrohizar de nuevo l a caracteristica make (dq @mdmcci6n) .corilos uchivas acfuafes del disco.
La orden Compile se puede usar solo cuando se ha cargado un proyccto en el cditor. Si no hay ningun proyecto activo y cargamos un archivo fuente Pascal, no se puede compilar. Sin embargo, si cargamos el archivo fuente como si fuera un proyecto, podremos compilar el archivo. Para ello, simplemente seleccionamos el boton de la barra de herramientas Open Project y cargamos un archivo PAS. Ahora podemos verificar su sintaxis o compilarlo, creando un DCU. Ya mencionamos que Delphi permite el uso de paquetes en tiempo de ejecucion, lo que afecta a la distribucion del programa mas que a1 proceso de compilacion. Los paquetes Delphi son bibliotecas de cnlace dinamico (DLL) que contienen componentes Delphi. A1 emplear paquetes, se consigue que un archivo ejecutable sea mucho mas pequeiio. Sin embargo, el programa no se ejecutara a no ser que este disponible la biblioteca dinamica apropiada (corno vcl50. bpl, que es bastante amplia) en el ordenador en el que desea ejecutar el programa. Si sumamos el tamafio de esta biblioteca dinamica a1 del pequeiio archivo ejecutable, la cantidad total de espacio en disco necesaria por el aparentemente pcqueiio programa, que hemos creado con 10s paquetes en tiempo de ejecucion, es mucho mayor que el espacio necesario por el supuestamente gran archivo ejecutable por si solo. Por supuesto, si tenemos diversas aplicaciones en un unico sistema, ahorraremos mucho, tanto en espacio de disco como en consumo de memoria en tiempo de ejecucion. El uso de paquetes suele ser recomendable, pero no siempre. En ambos casos, 10s ejecutables Delphi resultan extremadamente rapidos de compilar y la velocidad de la aplicacion que obtenemos es comparable a la de un
programa en C o C++. El codigo compilado en Delphi se ejecuta al menos cinco vcccs mas rapido que el codigo equivalente de herramientas interpretadas o "scmicompiladas".
yarn-
- -
-. -
1
-
i
1
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.
Al contrario que el Code Explorer, el Project Browser so10 se actualiza cuando se vuelve a compilar el proyecto. Este explorador permite ver listas de clases, unidades y globales, y tambien escoger si buscar so10 simbolos definidos en el proyecto o tanto del proyecto como de la VCL. Se puede cambiar la configuration del Project Browser y la del Code Explorer en la pagina Explorer de Environment Options o mediante el comando P r o p e r t i e s del menu desplegable del Project Browser. Algunas de las categorias que se pueden ver en esta ventana son especificas del Project Browser; otras estan relacionadas con ambas herramientas.
rio b i n . Ya que esta destinada a BDE, no se utiliza mucho actualmente. OpenHelp (oh. e x e ) : Es la herramienta que podemos emplear para administrar la estructura de 10s propios archivos de ayuda de Delphi, integrando archivos de otras personas en el sistema de ayuda. Convert ( C o n v e r t .e xe): Es una herramienta de linea de comandos que podemos usar para convertir 10s archivos DFM en su descripcion textual equivalente y viceversa. Turbo Grep ( G r e p . e x e ) : Es una utilidad de busqueda de lineas de ordenes, mucho mas rapida que el arraigado mecanismo Find In Files, per0 no es facil de usar. Turbo Register Server (TRegSvr .e x e ) : Es una herramienta que podemos emplear para registrar bibliotecas ActiveX y servidores COM. El codig0 fuente de esta herramienta esta disponible bajo \Demos \ A c t i v e X \ TRegSvr. Resource Explorer: Es un poderoso visor de recursos (pero no un editor de recursos propiamente dicho) que se puede encontrar bajo \Demos \ ResXplor. Resource Workshop: Es un editor de recursos de 16 bits que puede controlar archivos de recursos de Win32. El CD de instalacion de Delphi incluye una instalacion independiente para esta herramienta. Se ofrecia con 10s compiladores de Borland para C++ y Pascal para Windows y era mucho mejor que 10s editores de recursos de Microsoft disponibles entonces. Aunque su interfaz de usuario no se ha actualizado y no trabaja con nombres de archivos largos, esta herramienta todavia resulta muy util para construir recursos especiales o personalizados. Tambien le permite explorar 10s recursos de 10s archivos ejecutables existentes.
BMP, ICO, Archivos de mapas de bits, Desarrollo: iconos y cursores: archivos Image Editor CUR estandar de Windows usados para almacenar imagenes de mapas. BPG Desarrollo Borland Project Group (Grupo de proyectos Borland): archivos que usa el nuevo Project Manager. Es una especie de makefile.
Normalmente no, per0 pueden ser necesarios en tiempo de ejecucion y para una posterior modificacion. Necesario para compilar de nuevo todos 10s proyectos del grupo a la vez.
BPL
Borland Package Library Compilacion: (Biblioteca de paquetes Enlace Borland): una DLL que contiene, entre otros, componentes VCL que usa el entorno Delphi en tiempo de disefio o las aplicaciones en tiempo de ejecucion. (Estos archivos usaban una extension .DPL en Delphi 3.)
Formato de archivo comprimido Microsoft Cabinet usado por Delphi para el despliegue Web. Un archivo CAB puede contener diversos archivos comprimidos. Archivo de configuracion con las opciones de proyecto. Similar a 10s archivos DOF. Compilacion
CAB
Distribuido a usuarios
.CFG
Desarrollo
Necesario han definido opciones especiales de compilacion. Necesario cuando usamos paquetes. Solo se distribuira a otros desarrolladores
.DCP
Delphi Component Packa- Compilacion ge (Paquete de componentes de Delphi): un archivo con informacion de simbo-
lo para el codigo compilado en el paquete. No incluye codigo compilador, que se guarda en archivos DCU.
junto con 10s archivos BDPL. Se puede compilar una aplicacion con las unidades de un paquete simplemente con el archivo DCP y el BPL (sin archivos 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. No. Este archivo almacena informacion "solo en tiempo de disefio" no necesaria para el programa resultante per0 muy importante para el programador. Si. Todos 10s formularios se almacenan tanto en un archivo PAS como en un DFM.
.DCU
Delphi Compiled Unit (U ni- Com pilacion dad compilada Delphi): resultado de la compilacion de un archivo en Pascal.
. DDP
El n uevo Delphi Diagram Desarrollo Portfolio (Cartera de diagrama Delphi) usado por la vista en diagrarna del editor (era DTI en Delphi
5).
.DFM
Delphi Form File (Archivo de formulario de Delphi): un archivo binario con la descripcion de las propiedades de un formulario (o un modulo de datos) y de 10s componentes que contiene. Copia de seguridad de Delphi Form File (DFM)
Desarrc
.-DF
Desarrollo
No. Este archivo se crea al guardar una version de la unidad relacionada con el formulario y el archivo del formulario junto con ella.
DFN
Archivo de soporte para Desarrollo (ITE) Integrated Translation Environment (hay un archivo DFN para cada formulario y cada lenguaje o bjetivo). Dinamic Link Library (Biblioteca de enlace dinamico): otra version de un archivo ejecutable. Cornpilacion Enlace
Si (para el ITE). Estos archivos contienen cadenas traducidas para cada formulario que editarnos en el Translation Manager Vease .EXE
DLL
DOF
Delphi Option File: archivo Desarrollo de texto con la configuracion actual de las opciones actuales para las opciones del proyecto. Delphi Package: el archivo de codigo fuente del proyecto de un paquete (o un archivo de proyecto especifico para Windows o Linux). Archivo Delphi Project. (Este archivo contiene en realidad codigo fuente Pascal.) Copia de seguridad del archivo Delphi Project (.DPR). Desarrollo
Si.
DPR
Desarrollo
-DP
Desarrollo
No. Este archivo se crea automaticarnente al guardar una nueva version de un archivo de proyecto. No. En realidad deberian eliminarse si se copia el proyecto en un nuevo directorio.
DSK
Desktop file (Archivo de es- Desarrollo critorio): contiene informacion sobre la posicion de las ventanas Delphi, 10s archivos que se abren en el editor y otras configuraciones del escritorio.
DSM
Delphi Symbol Module (Modulo de simbolos Delphi): Alrnacena toda la informacion de sirnbolo del explorador.
No. El Object Browser usa este archivo, en lugar de 10s datos en memoria, cuando no puede volver a compilar un proyecto. No. Este archivo que se distribuira incluye todas las unidades compiladas, forrnularios y recursos.
EXE
Executable file (Archivo eje- Compilacion: cutable): la aplicacion Enlace Windows creada.
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 Object file (Archivo objeto) (cornpilado), tipico del rnundo C/C++. Paso intermedio de compilacion, generalmente no se usa en Delphi.
No. Es necesario usar el control en otro entorno de desarrollo. Podria ser necesario para rnezclar Delphi con codigo cornpilado C++ en un tinico proyecto. Vease .EXE.
OBJ
. OCX
OLE Control Extension (Ex- Compilacion: tension de control OLE): Enlace una version especial de una DLL, que contiene controles ActiveX o forrnularios.
.PAS
Pascal file (Archivo de Desarrollo Pascal): El codigo fuente de una unidad Pascal, una unidad relacionada con un forrnulario o una unidad independiente.
-PA
Desarrollo
No. Este archivo lo crea Delphi autornaticamente al guardar una nueva version del codigo fuente. Si. Delphi crea de nuevo el archivo RES principal de una aplicacion en funcion de la inforrnacion de la ficha Application del cuadro de dialogo Project Options.
RES, .RC
Resource file (Archivo de recurso): el archivo binario asociado con el proyecto de una aplicacion y que normalmente contiene su icono. Podernos afiadir otros archivos de este tip0 a un proyecto. Cuando creamos archivos de recurso personalizados podernos usar tambien el forrnato textual .RC. Translation Repository (parte de Integrated Translation Environment).
Cuadro de dialogo Development Options. El ITE (Integrated Translation Environment) crea archivos de recurso con comentarios especiales.
.RPS
Desarrollo (ITE)
No. Necesario para adrninistracion de las traducciones. Este es un archivo que pueden necesitar otros prograrnas OLE.
.TLB
Type Library (Biblioteca de Desarrollo tipos): un archivo creado de forrna autornatica o por el Type Library Editor para aplicaciones del servidor OLE. Archivo de lista To-do en el que se guardan elernentos relacionados con el proyecto entero. Desarrollo
TODO
.UDL
Usado por ADO para referirse a un proveedor de datos. Similar a un alias en el entorno BDE.
Ademas de 10s archivos creados durante el desarrollo de un proyecto en Delphi, existen muchos otros creados y usados por el propio IDE. En la tabla 1.2, se
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
Delphi Code Templates (Plantillas de codigo Delphi). Delphi Object Repository (Object Repository d e Delphi) (Deberia modificarse el Repository con la orden Tools> Repository.) Delphi Menu Templates (Plantillas d e menu d e Delphi). Database Explorer Information (Informacion del explorador d e bases d e datos). Delphi Edit Mask (Mascara d e edicion d e Delphi) (Archivos con formatos especificos seglin paises para mascaras d e edicion). Delphi Component Template (Plantillas d e componentes d e Delphi). Desktop Settings File (Archivo d e configuracion del escritorio): uno para cada configuracion d e escritorio definida.
.DRO
.DMT
.DBI .DEM
.DCT
.DST
-- - - -
- .
- -
-----
vo a parte con la descripcion de formulario (con extension .DFM). La unica excepcion es la propiedad Name,que se usa en la declaracion de formulario para hacer referencia a 10s componentes del formulario. El archivo DFM es de manera predeterminada una representacion textual del formulario, pero se puede guardar en cl formato binario tradicional Resource de Windows. Podemos establecer el formato predefinido que queremos usar para proyectos nuevos en la ficha Preferences del cuadro de dialog0 Environment Options y cambiar el formato de formularios individuales con la orden Tcxt DFM del menu de metodo abreviado de un formulario. Un editor de texto normal puede leer solo la version de texto. Sin embargo, se pueden cargar 10s archivos DFM de ambos tipos en el editor Delphi, que 10s convertira primero, si cs neccsarioj en una descripcion textual. La forma mas sencilla de abrir la descripcion textual dc un formulario (sea en el formato que sea) es seleccionar la orden View A s Text del menu de metodo abreviado del Form Designer. Esta orden cierra el formulario, lo guarda si es necesario y abre el archivo DFM en el editor. Mas tarde sc puede volver a1 formulario usando la orden View A s Form del menu dc metodo abreviado de la ventana del editor. En realidad, se puede editar la descripcion textual de un formulario, aunquc esto deberia hacerse con extremo cuidado. Desde el momento en que se guarde el archivo, se convertira de nuevo en un archivo binario. Si se han hecho cambios incorrectos, se detendra la compilacion con un mensaje de error y habra que corregir el contenido del archivo DFM antes de volver a abrir el formulario. Por esa razonj no se deberia intentar cambiar la descripcion textual de un formulario manualmentc hasta disponcr de un solido conocimiento de programacion en Delphi.
-
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~iente Pascal, en 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
Los archivos de escritorio (DSK) utilizan la misma estructura, que contiene el estado del IDE de Delphi para un proyecto especifico, listando la posicion de cada ventana.
[Mainwindow] Create=l Visible=l State=O Lef t = 2 Top=O Width=800 Height=97
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.
por techa o por descnpcl6n) y mostrat dttim%&s ViMw (idol3 giqhks, iconos pequefios, listas y detalles). La vista Msib propo*ei& & hes-
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*
Console Applcat~on
DCC W ~ z a d
Fam
T I
Flame
1 -
Package
Ploiect GI-
Service
Figura 1.12. La primera pagina del cuadro de dialogo New Items, conocida generalmente corno Object Repository.
El mod0 mas sencillo de personalizar el Object Repository es aiiadir nuevos proyectos, formularios y modulos de datos como plantillas. Tambien podemos aiiadir fichas nuevas y organizar 10s elementos de algunos de ellas (sin incluir las fichas New ni la del proyecto en uso). Cuando se tiene una aplicacion en funcionamiento que se quiere emplear como punto de arranque para el desarrollo de programas similares, se puede guardar el estado actual en una plantilla para usarla mas tarde. Simplemente se usa la orden Project>Add To Repository y se cubre su cuadro de dialogo. Tambien se pueden aiiadir nuwas plantillas de formulario. Sencillamente desd plazamos el formulario que se quiere aiiadir y seleccionamos la orden ~ d T o R e p o s i t o r y del menu de metodo abreviado. A continuacion, indicamos el titulo, la descripcion, el autor, la ficha y el icono en su cuadro de dialogo. Hay que tener en cuenta que si se copia un proyecto o una plantilla de formulario a1 Repository y se vuelve a copiar a otro directorio, simplemente se realiza una operacion de copiar y pegar; no es muy distinto de copiar 10s archivos manualmente.
hay que seguir para ello son: 1. Crear un nuevo proyecto como de costumbre.
como funciona cl Repository, porque si queremos modificar un proyecto o un objeto guardados en 61; la mejor tecnica es trabajar con 10s archivos originalcs, sin copiar 10s datos una y otra vez en el Repository.
.
__:_*_-A_
__LZ__
I
rn rn
Wc_hName ' V W Controls ' ' expected but end of lde found by TRad~oButton Symbol was el~mtnated l~nker
_ -_
. .
El entorno de desarrollo para Delphi se basa en una extension orientada a objetos del lenguaje de programacion Pascal conocida como Object Pascal o Pascal orientado a objetos. Recientemente, Borland declaro su intencion de referirse a1 lenguaje como "el lenguaje Delphi", probablemente porque la empresa deseaba ser capaz de decir que Kylix usa el lenguaje Delphi y porque Borland ofrecera el lenguaje Delphi sobre la plataforma .NET de Microsoft. Debido a la costumbre de 10s aiios, es comun utilizar ambos nombres por igual. La mayoria de 10s lenguajes de programacion modernos soportan programacion orientada a objetos (OOP). Los lenguajes OOP se basan en tres conceptos fundamentales: la encapsulacion (normalmente implementada mediante clases), la herencia y el polimorfismo (o enlace tardio). Aunque se puede escribir codigo Delphi sin comprender las caracteristicas principales del lenguaje, no es posible dominar este entorno hasta que se comprende totalmente el lenguaje de programacion. Este capitulo trata 10s siguientes temas: Clases y objetos. Encapsulacion: p r i v a t e y pub1 ic. Uso de propiedades. Constructores.
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.
NOTA: Los tkrminos clase y objeto se utdizan con mucha fiecuencia y a rnenudo se confunden, ssi que asegurbmonos de estar de acuerdo sobre sus definiciones. Una clase es un tip0 de dabs definido por el usuario, que posee un estado (su representacibn o sus datos internos) y algunas operaciones (su comportamiento o sus mbtodos). Un objeto es una instancia de una clase o una variable del tipo de datos demdo por la clase. Los objetos son entidades rcales. ~ u a n d ~programa se ejeiuta, los objetos ocipan el park de la memoria para su repreaentacilln intern. La relacih en- objeto y clase es la misma que entre variable y tipo.
Como en la mayor parte del resto de 10s lenguajes orientados a objetos (como Java y C#),en Delphi una variable de tipo clase no proporciona el almacenamiento para el objeto, sino solo un punter0 o referencia al objeto en la memoria. Antes de utilizar el objeto, se debe reservar memoria para 61 mediante la creacion de una nueva instancia o asignando una instancia ya existente a la variable:
var
Obj 1, Obj2 : TMyClass; begin // a s i g n a r un o b j e t o r e c i e n c r e a d o Objl : = TMyClass.Create; // a s i g n a r un o b j e t o e x i s t e n t e Obi2 : = ExistingObject;
La llamada a create invoca un constructor predefinido disponible para cada clase, a no ser que la clase lo vuelva a definir (como ya veremos). Para declarar un nuevo tipo de datos de clase en Delphi, con algunos campos de datos locales y algunos metodos, se puede utilizar la siguicnte sintaxis:
type TDate = class Month, Day, Year: Integer; procedure SetValue (m, d, y: Integer); function Leapyear: Boolean; end;
qOTA: La convenci6n en Delphi es usar la letra T mmo prefijo para el lombre de cada clase que se escribe y cualquier otro tipo (T significa Tipo). , 'I, , , , ., , I, -l -1 ,,,:t,A,, ,, C ,, . , , h~GE ,JUlU . W W-,V G U b :*, l l I,,,, GI W l l l ~ l i W l 'Pa,,'I ,, I1d ,L, 1 4 CiVUlV U l U WWir , ES &UlW 1 cualquier otra), per0 es tan frecuente que respetarla harfr' que el d g o resulte intis facil de entender.
? , A ,
Un metodo se define con la palabra clave f u n c t i o n o p r o c e d u r e , segun si dispone de un valor de retorno o no. Dentro de la definicion de clase, solo se pueden definir 10s metodos; despues deben definirse en la seccion de implernentacion de la misma unidad. En este caso, se antepone a1 nombre de cada metodo el nombre de la clase a la que pertenece, mediante una notacion de puntos:
procedure TDate.SetValue begin M o n t h : = m; Day : = d; Y e a r : = y; end;
(m, d, y: Integer) ;
TRUCO:Si se pulsa Controi-Maylis-C mientras que el cursor se m'cwtra sobre la definicion de clase, la ~aracteristiea Class 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 ) ) ;
Fijese en que ADa y .L e a p Y e a r es una expresion similar a ADa y .Year, sin embargo, la primera es una llamada a una funcion y la segunda es un acceso direct0 a datos. Opcionalmente se pueden aiiadir parentesis tras la llamada a la funcion sin parametros. Se pueden encontrar 10s fragmentos de codigo anteriores en el codigo fuente del ejemplo Date 1, la unica diferencia es que el programa crea una fecha basada en el aiio que se introduce en un cuadro de edicion.
Se pueden definir metodos de clase, indicados por la palabra clave c l a s s . Un metodo de clase no tiene una instancia de objeto sobre la que actuar, ya que puede aplicarse a un objeto de la clase o a la clase en su totalidad. Actualmente Delphi no tiene un mod0 de definir datos de clase, per0 puede simularse esta prestacion aiiadiendo datos globales en la porcion de implementacion de la unidad en que se defina a la clase. De manera predeterminada, 10s metodos usan la convencion de llamada r e g i s t e r : 10s parametros (simples) y 10s valores de retorno se pasan del codigo de llamada a la funcion y de vuelta mediante registros de la CPU,
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.
Cuando se escribe un procedimiento como el codigo que acabamos de ver, podriamos sentirnos tentados a utilizar la variable F o r m l en lugar de S e l f . En este ejemplo concreto, el cambio no tendria ninguna diferencia practica, per0 si existen diversas instancias de un formulario, usar F o r m l seria un error. De hecho, si la variable F o r m l se refiere a1 primer formulario de ese tipo que se ha creado, a1 pinchar sobre otro formulario del mismo tipo, el nuevo boton siempre aparecera en el primer formulario. Sus O w n e r y P a r e n t seran el F o r m l y no el formulario que ha pinchado el usuario. Por lo general, no conviene referirse a una instancia concreta de una clase cuando se necesita el objeto actual.
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.
Se podria pensar en aiiadir otras funciones, como G e t D a y , G e t M o n t h y G e t Y ear, que simplemente devuelvan 10s datos privados correspondientes, per0 no siempre se necesitan funciones similares directas de acceso a datos. Si se conceden funciones de acceso para cada uno de 10s campos, se podria reducir el encapsulado y dificultar la modificacion de la implementacion interna de una clase. Las funciones de acceso deberian de definirse unicamente si forman parte de la interfaz logica de la clase que esta implementando. Otro nuevo metodo es el procedimiento Increase, que suma un dia a la fecha. Este calculo no es nada sencillo, porque hay que considerar las distintas longitudes de 10s meses asi como 10s 6 0 s bisiestos o no bisiestos. Lo que haremos, para que resulte mas sencillo escribir el codigo, sera cambiar la implementacion interna de la clase a1 tipo T D a t e T i m e de Delphi para la implernentacion interna. La definicion de clase cambiara a lo siguiente (el codigo completo aparece en el proximo ejemplo, D a t e p r o p ) :
type TDate = class private fDate: TDateTime; public procedure SetValue (y, m, d: Integer); overload; procedure SetValue (NewDate: TDateTime); overload; function Leapyear: Boolean; function GetText: string; procedure Increase; end ;
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 read FMonth write SetMonth;
Para acceder a1 valor de la propiedad Month, el programa lee el valor del campo privado FMonth, mientras que para cambiar el valor de la propiedad llama a1 metodo S e t M o n t h (que ha de estar definido dentro de la clase, por supuesto). Son posibles diversas combinaciones (por ejemplo, un metodo para leer el valor o cambiar directamente un campo en la directiva w r i t e ) , per0 el uso de un metodo para cambiar el valor de una propiedad es muy comun. Estas son dos definiciones alternativas para la propiedad, proyectada sobre dos metodos de acceso o directamente sobre 10s datos en ambas direcciones:
property Month: property Month: I n t e g e r read GetMonth write SetMonth; I n t e g e r read m o n t h write m o n t h ;
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.
TRUCO: Cuando se definen propiedades, se puede aprovechar la funcion Class C o m p l e t i o n del editor de Delphi. que se activa con la combina- .
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 palade bta clave read tambitn se conseguira un m e t o d ~ lectura sin apenas escribir.
Cada uno de estos metodos se implementa facilmente utilizando funciones disponibles en la nueva unidad DateUtils. Veamos el codigo de dos de ellos (10s otros son muy similares):
f u n c t i o n TDate.GetYear: I n t e g e r ; begin R e s u l t := YearOf ( f D a t e ) ; end; p r o c e d u r e T D a t e . S e t Y e a r ( c o n s t Value: I n t e g e r ) ; begin f D a t e : = Recodeyear ( f D a t e , Value) ; end ;
El codigo de esta clase esta disponible en el ejemplo Dateprop. El programa utiliza una unidad secundaria para que la definicion de la clase T D a t e active el encapsulado y Cree un objeto de fecha simple guardado en una variable de formulario y almacenado en memoria durante toda la ejecucion del programa. Si se usa una tecnica estandar, el objeto se crea en el controlador de eventos oncreate del formulario y se destruye en el controlador de eventos O n D e s t r o y del formulario. El formulario del programa (vease figura 2.2) tiene tres cuadros de edicion y botones para copiar 10s valores de estos cuadros de edicion en las propiedades del objeto de fecha:
ADVERTENCIA: Cuando se escribefi 10s v a l m ; d $pn>&'ama utiliza el metodo setvalue en lugar & &inkcada una de las pfopiedades. De hecho, asignar e1 mes y el &a por separado puede cam& pLoblemati cuando por el mes no es valid0 para el dia en uso. Pongam~s &&plo que la fecha actual es el 3 1 de enero jr qgxemos twignark el 20 de fe'bkro. Si asignamos primero el mes, esa pa& darsr error, puesto que el 3 1 d;e febrero no existe. Si asignamos primero el &a, el problem slirgilpr al hacer 1a asignstci6n inversa. Debido a las rm&m de validmikt para fixkttd, ss enejor.wigm.r todo a1mismo ~~.
Caracteristicas avanzadas de las propiedades
Las propiedades tienen varias caracteristicas avanzadas. Este es un breve resumen de ellas: La directiva write de una propiedad se puede omitir, convirtiendola asi en una propiedad de solo lectura. El compilador dara error si intentamos cambiarla. Tambien se puede omitir la directiva read y definir una propiedad de solo escritura, per0 ese enfoque no tiene mucho sentido y no se suele emplear. El IDE de Delphi da un trato especial a las propiedades en tiempo de diseiio, que se declaran con el especificador de acceso pub1 ished y que por lo general aparecen en el Object Inspector para el componente seleccionado. Las otras propiedades, normalmente denominadas propiedades solo de tiempo de ejecucion, son las declaradas con el especificador de acceso public. Dichas propiedades pueden usarse en el codigo del programa. Se pueden definir propiedades basadas en matrices, que usan la notacion tipica con corchetes para acceder a un elemento de la lista. Las propiedades basadas en la lista de cadenas, como Lines en un cuadro de lista, son un ejemplo tipico de este grupo. Las propiedades tienen directivas especiales, como stored y default, que controlan el sistema de streaming de componentes.
incluso se pueden usar propiedades en expresiones, pero no siempre se puede pasar una propiedad como parimetro a un procedimiento o metodo. Esto se debe a que una propiedad no es una posicion de memoria, por lo que no se puede utilizar como parimetro var u o u t : no se puede pasar por referencia.
.'
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.
a'la M a de ~
NOTA:~ i ~ cegequo el a & d ~ p & i ~ d d a dpun f o n dario, no 3. s ~ del f a h~d ! del Qbject B p e c t o r . e s
Mia&
-~4'
Conviene usar las propiedades tambien en las clases de formulario para encapsular el acceso a 10s componentes de un formulario. Por ejemplo, si hay un formulario principal con una barra de estado en la que se muestre cierta informacion (y con la propiedad s i m p l e p a n e l definida como T r u e ) y hay que modificar el texto de un formulario secundario, podriamos sentir la tentacion de escribir:
Form1 .StatusBarl .SimpleText : = ' n u e v o texto' ;
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;
siendo G e t T e x t y S e t T e x t metodos que leen de y escriben en la propiedad S i m p l e T e x t de la barra de estado (o la etiqueta de uno de sus paneles). En 10s
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 ) ;
Las normas de escritura de constructores para componentes personalizados son diferentes. L a razon es que en este caso hay que sobrescribir un constructor virtual. La sobrecarga resulta particularmente importante para 10s constructores ya que se pueden aiiadir multiples constructores a una clase y llamarlos a todos
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 ;
modelo de referencia a objetos. La idea es que una variable de un tipo de clase, como la variable TheDay en el ejemplo anterior ViewDate, no mantiene el valor del objeto. En lugar de eso, contiene una referencia, o un puntero, para indicar la posicion de mernoria en la que se ha almacenado el objeto. Se puede ver la estructura en la figura 2.4.
TheDay objeto TDay
Figura 2.4. Una representacion de la estructura de un objeto en memoria, con una variable que se refiere a el.
El unico problema de esta tecnica es que cuando se declara una variable, no se crea un objeto en memoria (lo que es inconsistente con el resto de variables, confundiendo a 10s nuevos usuarios de Delphi); solo se reserva la posicion de memoria para una referencia al objeto. Las instancias de objetos h a b r h de crearse manualmente, a1 menos para 10s objetos de las clases que se definan. Las instancias de 10s componentes que se coloquen en un formulario son creadas automaticamente por la biblioteca de Delphi. Hemos visto como crear una instancia de un objeto, aplicando un constructor a su clase. Cuando hayamos creado un objeto y hayamos terminado de usarlo, es necesario eliminarlo (para evitar llenar la rnemoria que ya no necesita, lo cual origina lo que se conoce como "goteo de memoria"). Esto se puede hacer mediante una llamada a1 metodo Free.Siernpre que se creen objetos cuando Sean necesarios y se liberen cuando ya no lo Sean, el modelo de referencia a objetos funcionara perfectamente. El modelo de referencia a objetos tiene una gran influencia en la asignacion de objetos y en la administracion de memoria.
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);
Este codigo copia la direccion de memoria del objeto NewDay a la variable TheDay (corno muestra la figura 2.5); no copia 10s datos de un objeto en el otro. En esta circunstancia concreta, esta tecnica no es muy adecuada, puesto que cada vez que se pulsa el boton, se asigna memoria para un nuevo objeto y nunca se libera la memoria del objeto a la que anteriormente apuntaba la variable TheDay.
NewDay objeto TDate
TheDay
Figura 2.5. Una representacion de la operacion de asignacion de una referencia de objeto a otro objeto. Esto es distinto de copiar el contenido real de un objeto en otro.
Esta cuestion especifica puede resolverse liberando el objeto antiguo, como en el siguiente codigo (que tambien esta simplificado, sin el uso de una variable explicita para el objeto de nueva creacion):
procedure TDateForm.BtnTodayClick(Sender: TObject); begin TheDay-Free; TheDay : = TDate.Create;
Lo importante es que cuando se asigna un objeto a otro objeto, Delphi copia la referencia a1 objeto en memoria en la nueva referencia a objeto. No deberia considerarse esto como algo negativo: en muchos casos, ser capaz de definir una variable que se refiera a un objeto ya existente puede ser una ventaja. Por ejemplo, se puede almacenar el objeto devuelto a1 acceder a una propiedad y usarla en las sentencias siguientes, como se indica en este fragment0 de codigo:
var
ADay: TDate; begin ADay: User1nformation.GetBirthDate; / / usar u n A D a y
Lo mismo ocurre si se pasa un objeto como parametro a una funcion: no se crea un nuevo objeto, sino que se hace referencia a1 mismo en dos lugares diferentes del codigo. Por ejemplo, a1 escribir este procedimiento y llamarlo como se indica a continuacion, se modificara la propiedad C a p t i o n del objeto B u t t o n l , no de una copia de sus datos en memoria (algo que seria completamente inutil):
procedure Captionplus begin
(Button: TButton);
Button.Caption end;
: = Button.Caption
' + I ;
/ / llamar..
CaptionPlus
.
(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.
Delphi soporta tres tipos de administration de memoria para elementos dinamicos: Cada vez que creamos un objeto explicitamente en el codigo de una aplicacion, tambien debemos liberarlo (con la sola excepcion de un puiiado de objetos del sistema y de objetos que se utilizan a traves de referencias de interfaz). Si no se hace asi, la memoria utilizada por dicho objeto no se libera hasta que finaliza el programa. Cuando creamos un componente, podemos especificar un componente propietario, pasando el propietario a1 constructor del componente. El componente propietario (normalmente un formulario) se transforma en el responsable de destruir todos 10s objetos que posee. Asi, si creamos un componente y le damos un propietario, no es necesario que nos acordemos dc destruirlo. Este es el comportamiento estandar de 10s componentes que se crean en tiempo de diseiio a1 colocarlos sobre un formulario o modulo de datos. Sin embargo, es imperativo que se escoja un propietario cuya destruccion quede garantizada; por ejemplo, 10s formularios suelen pertenecer a 10s objetos globales A p p l i c a t i o n , que son destruidos por la biblioteca cuando acaba el programa. Cuando la RTL de Delphi reserva memoria para las cadenas y matrices dinamicas, libera automaticamente la memoria cuando la referencia resulta inalcanzable. No es necesario liberar una cadena: cuando resulta inaccesible, se libera su memoria.
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.
En general, tambien se puede verificar si un objeto es nil usando la funcion signed.Por lo que las dos sentencias siguientes son equivalentes, a1 menos la mayor parte de 10s casos:
i f Assigned (ADate) then i f ADate <> nil then . . .
.. .
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 :
Para implementar la nueva version de la funcion GetText, utilizamos la funcion Format DateTime, que emplea (entre otras caracteristicas) 10s nombres de mes predefinidos disponibles en Windows, estos nombres dependen de la configuracion regional del usuario y de la configuracion del lenguaje. Muchas de estas configuraciones las copia Delphi en constantes definidas en la biblioteca, como LongMonthNames, ShortMonthNames y muchas otras que puede encontrar bajo el tema "Currencyand datehime formatting variables" (Variables para formatear la moneda y la fecha/hora) en el archivo de ayuda de Delphi. Veamos el metodo GetText,en el que 'dddddd' corresponde a1 formato de fecha largo:
function TNewDate.GetText: string; begin GetText : = FormatDateTime ( ' d d d d d d ' , f D a t e ) ; end :
TRUCO: Cuando usamos la information regional, el programa NewDate de se adapta automaticamente%las diferentes codi@r&i~nes & u~arici Windows. Si ejecuta este mismo programa en un ordenador con una configuracion regi~nalen ,espaiiol, 10s nombres de 10s m e s a agsuecerh automaticamenteen eqpat(o1
Cuando tengamos la definicion de la nueva clase, hay que usar este nuevo tipo de datos en el codigo del formulario del ejemplo NewDate. Simplemente hay que definir el objeto TheDay de tipo TNewDate y crear un objeto de la nueva clase mediante en el metodo Formcreate.No es necesario modificar el codigo con llamadas de metodo, ya que 10s metodos heredados seguiran funcionando del mismo modo; sin embargo, se modifica su efecto, como muestra la nueva salida (vease figura 2.6)
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
Acceder a datos protegidos de ottas clases Hemos vim que en Delphi, 10s datos privados y p r i P t e ~ 1de~ 3 una elase son accesibles para cualquier funcib o rnM~ aparezca ed fa rnismti que unidad qae l a dase. Por ejemplo. ~ ~ ~ i d e r e esta'clase f ~ &del ejem rnij~ plo Protec&~):
tYP9 TTest = ex-s ptotec ted ~ r o t e k t e d ~ I k BInteger; ? d;
Cued0 hayarnos'co~ocado clase qn Ia unidad, no se podra accedet absu esta park protegida directamente desde otras unidades. Segiin esto, si escribimm el kipiente cbdigo.
p~ocecbre TForrnl. B u t C o n l C & i c R (Sender: T O b j e c t )
VaE
;
/ / no v a
compiler
'ProtectedDatan'(Identificador no declarado: Datos Protegidos). En este momento, se podria pensar que no hay manera de acceder a 10s datos protegidos de una clase defmida en una unidad diferente. Sin embargo, en cierto mod0 si se puede. Tengarnos en cuenta lo que ocurre si se crea una clase aparentemente derivada inutil, corno:
type TTestHack = alaas (TTest);
Ahora, si realizamos una conversion directa del objeto a la nueva clase y accedemos a 10s datos protegidos a traves de ella, el codigo sera:
var
csre coaigo compua y runciona correctamenre, como se pueae ver si se ejecuta el programa Protection. La raz6n es que la clase TTestHack hereda autornstticamente 10s campos protegidos de la clase birsica TTest y, como est4 en la misma unidad que el ckligo que intenta acceder a 10s datos de 10s campos heredados, 10s datos protegidos resultan accesibles. Como seria de esperar, si se mueve la declaracibn de la clase TTestHac k a una unidad secundaria, el programa ya no cornpilad. Ahora que ya hemos visto d m o se hace, hay que W e t en cuenta que viola el mecanismo de proteccibn de c l a m de este m o b pbdrfir Brigbar errores en el programa (a1 acceder a datos a 10s que no deberimw tener acceso) y no respeta las tbcnicas de orientacih a objetos. Sin embargd, en muchas ocasiones usar esta thnica es la mejor solucibn, como veremos a1 analizar el codigo fuente de Ia VCL y el ddigo fuente de mucbos wmponentes Delphi. Dos ejemplos de ello son el acceso a la propiedad Text de la clase TControl y las posicio&s Row y G o 1 del control DBGrid. EQtas dos ideas aparecen en-10s ejemplos Text Prop y DBGridCol, respectivamente. (Son ejemplos bastante avanzados, asi que es mejor no enfientarse a ellos hasta tener un buen conocimientode Delphi.) Aunque el primer0 es un ^-I :-1 1 ---- A - 13LA- -i -:---la ~jcruylo r ~- L 1 - ~ u- c ~ ~UG IU z;onvwsrun oe upus wwX;sr, c qcmplu uWQ e l DBGrid de Row y Col es en &dad un ejemplo de w o opuesto. que ilustplos riwgm de a c d r a bits que la persona que escribib las clases prefifio s o exgoner. La fib y colbima de una clase DB-id no significan lo mismo gw en u ~ r p w ~ r i d StringGrid &q clpses bhi& Q una cas). En pjmer h p r , ll5Gxld a cuenta las w b @as oomo cedas o rcakr ( & t i a d i s eelda? dehatos dc 10s c l ~ ~ d e ~ b r a t i l i q ] , lo P ~ r ~ que sys indices ,de fils. y aoluraaa.idrib qua aj$#arse a los,efemento#
- - a
----.---: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 ;
// E s t o es correcto // ; E s t o es u n error!
El efecto de la llamada M yAnimal .Voice puede variar. Si la variable MyAnimal se refiere en un momento dado a un objeto de la clase TAnimal, llamara a1 metodo TAnimal . Voice.Si se refiere a un objeto de la clase TDog, llamara en cambio a1 metodo TDog .Voice.Esto ocurre solo porque la funcion es virtual (como veremos si se elimina esta palabra clave y se vuelve a compilar). La llamada a MyAnima1. Voice funcionara en el caso de un objeto que sea una instancia de cualquier descendiente de la clase TAnimal,aunque las clases esten definidas en otras unidades, o aunque todavia no se hayan escrito. El compilador no necesita conocer todos 10s descendientes para hacer que la llamada sea compatible con ellos, solo se necesita la clase ascendiente. En otras palabras, esta llamada a MyAnima 1 . Voice es compatible con todas las futuras clases que hereden de TAnimal.
-- -
i-
[Y
GI
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.
prugriu~laj iuuavla sc yucuc iulrpllar, aunquc sc n a y u
GSGIILU IIIIICS
En la figura 2.7 se puede ver un ejemplo dc la salida del programa PolyAnimals. A1 ejecutarlo, se oiran 10s sonidos correspondientes producidos por la llamada a Playsound.
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;
Hay dos formas muy comunes de sobrescribir un metodo. Una consiste en reemplazar el metodo de la clase ascendiente por una nueva version. La otra, en aiiadir mas codigo a1 metodo existente. Para ello se utiliza la palabra clave i n h e r i t e d que llama a1 mismo metodo de la clase ascendiente. Por ejemplo, se puede escribir:
procedure TMyDerivedClass.0ne; begin / / codigo nuevo
...
/ / 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;
Cuando se sobrescribe un metodo virtual existente de una clase basica, hay que usar 10s mismos parametros. Cuando se presenta una nueva version de un metodo en una clase descendiente, se puede declarar con cualquier parametro. De hecho, este sera un nuevo metodo independiente del metodo ascendiente del mismo nombre, solo que tendra el mismo nombre. Veamos un ejemplo:
type TMyClass = class procedure One; end; TMyDerivedClass = class (TMyClass) procedure One ( S : string) ; end;
NOTA: Si se usan las definiciones de clase anteriores, cuando se crea un objeto de la clase TMyDer ivedClass, se puede usar su m&odo One con . . . _ _ Iel parametro ae cauena, pero no la version sm parametros aeImaa en la clam bit8iaa. Qi se necesita esto, se puede marcar el metodo redeclarado (el de h c h derivada) con la palabra clave overload. Si el &todo time pariimetros diferentes a 10s de la versibn de la clase bbica, se cunvierte cfedtivamente en un mktodo sobrecargado. Si no es asi, reemplaza a1 rnktodo c la olase bbsica. Ffjese en que el m&odo no necesita estar marcado con k overload en la c h e bkica. Sin embargo, si el m h d o de la chse b h i c a es virtual, el c o m p i h d ~ emite la advertencia "Method 'One'hfdes virtual r method o base type "~yClass"'(E1m&odo 'One'oculta el metodo virtual f del tp bbico "TMyClassW). i Para evitar este mensaje e instruir a1compilador de forma m h precisa sobre nuestras intenciones, se puede usar la directiva reintroduce. El c6digo sobre este tema se puede encontrar en el ejemplo Reintr.
3
3 .
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,
que habran de ser procedimientos con un unico parametro var. La directiva message va seguida del numero del mensaje de Windows que el metodo quiere controlar.
ADVERTENCIA;- La directin i e s sage tambien ,est&dispodble ,en Kylix y el bguaja y lsr RTL la soportan por cornpleto. 8ir1embargo, par& visual del mars de hbajo de la apiicacion-CLX nd WJQYb t o b del m mensaje para enviar las natificacioneg a Basqontrolm. Por esq ra26a, impre que sea posible, se deberia usar un mitach virtual: propomionado por la biblioteca en lugar de manejar un m@hscLje de Winhws dlrectamente. Por supuesto, esto importa s61o si queremoa qiie el &go se pueda tramportar.
Por ejemplo, la siguiente porcion de codigo permite manejar un mensaje definido por el usuario, con el valor numirico indicado por la constante vm-User de Windows.
type TForml = class (TForm)
...
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
en tiempo de ejecucion, Delphi creara una escepcion, como muestra el ejemplo AbstractAnimals (una ampliacion del ejemplo PolyAnimals), que usa la siguiente clase:
type TAnimal = c l a s s public function Voice: s t r i n g ; v i r t u a l ; abstract;
NOTA: La rnayoria de 10s lenguajes orientados a objetos usan un enfo m h estricto: generalmente no se pueden crear instancias de cIases que c
tengan m6todos abstractos. Podriamos preguntarnos por la razon del uso de 10s metodos abstractos. Esta razon es el polimorfismo. Si la clase TAnimal tiene el metodo virtual Voice, toda clase heredada puede volver a definirlo. Si se trata de un metodo abstracto Voice,cada clase heredada debe volver a definirlo. En las primeras versiones de Delphi, si un metodo sobrescribia un metodo abstracto llamado inherited,el resultado era una llamada a1 metodo abstracto. A partir de Delphi 6; el compilador se ha mejorado para detectar la presencia dcl metodo abstracto y evitar la llamada inherited.Esto significa que se puede usar con seguridad inherited en todo metodo sobrescrito, a no ser que se desee inhabilitar esplicitamente la ejecucion de parte del codigo de la clase basica.
Los parametros del operador is son un objeto y un tipo de clase y el valor de retorno es un booleano:
i f MyAnimal i s TDog t h e n
...
La expresion is evalua como True si se el objeto MyAnimal se refiere realmente a un objeto de clase T D O o de un tipo descendiente de T D O ~ Esto ~ . significa que si se comprueba si un objeto TDog es de tipo TAnimal,la comprobacion tendra exito. En otras palabras, esta sentencia evalua como True si se puede asignar con seguridad el objeto (MyAnimal) a una variable del tipo de datos (TDO~). Ahora que sabemos con seguridad que el animal es un perro (dog), se puede realizar una conversion de tipos segura. Se puede realizar dicha conversion directa escribiendo el siguiente codigo:
var MyDog: TDog; begin i f MyAnimal i s TDog t h e n begin MyDog := TDog (MyAnimal); Text : = MyDog.Eat; end;
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;
Si solo queremos llamar a la funcion E a t , tambien podemos usar una notacion incluso mas corta:
(MyAnimal a s TDog) .Eat;
El resultado de esta expresion es un objeto del tip0 de datos de clase TDog, por lo que se le puede aplicar cualquier metodo de dicha clase. La diferencia entre la conversion tradicional y el uso de as es que el segundo enfoque crea una excepcion si el tip0 del objeto es incompatible con el tipo a1 que estamos intentando convertirlo. La excepcion creada es E I nva 1 idCa s t . Para evitar esta excepcion, hay que usar el operador is y, si funciona, realizar una conversion de tipos normal (en realidad, no hay ninguna razon para usar is y as de manera secuencial y hacer la verificacion de tipos dos veces):
i f MyAnirnal i s TDog t h e n TDog (MyAnimal) 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;
Se trata de una tecnica habitual en Delphi. Los dos operadores RTTI, i s y as, son realmente potentes y podriamos sentirnos tentados a considerarlos como construcciones de programacion estandar. Sin embargo, probablemente se deberia limitar su uso para casos especiales. Cuando sea necesario resolver un problema complejo relacionado con diversas clases, hay que intentar utilizar primer0 el polimorfismo. Solo en casos especiales, en 10s que el polimorfismo solo no se pueda aplicar, deberiamos intentar usar 10s operadores RTTI para complementarlo. No hay que usar RTTI en lugar del polimorfismo, puesto que daria lugar a programas mas lentos. La RTTI, de hecho, tiene un impact0 negativo en el rendimiento, porque debe pasar por la jerarquia de clases para ver si la conversion dc tipos es correcta. Como hemos visto, las llamadas de metodo virtual solo necesitan una busqueda en memoria, lo cual es mucho mas rapido.
I
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
mecanismo es similar a la forma en que Delphi maneja cadenas largas y administra la memoria casi de forma automatica. Una clase puede heredar de una clase basica simple, per0 puede implementar varias interfaces. A1 igual que todas las clases descienden de T O b j ect, todas las interfaces descienden de 1Interface y forman una jerarquia totalmente independiente. m z E d i a s e r IUnknown has@ ~ e l ~5, per0 ~ e l ~ h i hi 6 le otorgo un nuevo nombre, I I n t e r face,para paarcar de un modo r k n claro el hecho de que de esta f u n c i h del lenguaje es independiente del COM de Microsoft (que usa IUnknown como su iaterfaz base). De hecho, las interfaces Delphi tambikn e s t h disponibles en Kylix.
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;
La interfaz anterior posee un GUID, un identificador numeric0 que sigue a su declaracion y se basa en las convenciones Windows. Estos identificadores (llamados generalmente GUID) se pueden generar pulsando la combinacion de teclas Control-Mayus-G en el editor de Delphi.
-
--
--
NClTkt Awqne se $udden coMilar y usar interfaces sin:especificar un 4&RD para ellas For la general conviene generar uno, puesto que es necesari0 t a r s realizar consbltas & interfaz o la conversibn dinamica de tipos mediante as c y ese tipo de interfaz. Dado que todo el inter& de las interfaces coslsiste'(nomalmentefin aprovechar la flexibilidad mejorada en tiernpo I & ejecuci'6ir,'siT compaqmos cob 10s tipos de clase, las interfaces sin 10s a GUIH no resultan muy utiles.
- - - -- -
---
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
coolpiladm ha de garerar mtinas de d e w p ~ r q ajustar lm puntos b entrada & la llatnada dq infe&gaI r n b cmespgndiente de hi olase de impletneq@ci&y adaptar el punter0 self ~ s tt-$o de c & m6todo de interfaz para m w a p ~ 4 t i c o e muy sencillo: a j u m r ' s s i f y ss p&r al o & # real de la clase. 'tas mtinas de mCtodo de interfaz para m&'&s virtuales son mucho mas complejas y requieren unas cuatro veces has' codigtj (20 a 30 bytes) en cada una que en el caso esthtico. Ademas, aiiadir mas- metodos virtuales a la clase de implementacion contribuye a inflat la tabla.de rn6todos virtuaIes (VMT) en la clase y en todas sus subdases. Una interfaz ya dispone de su propia VMT y volver a declarar una interfaz en las subclases para volver a enlazar la interfaz con 10s nuevos metodos supone tanto polimorfismo como usar metodos virtuales, pet0 requiere un codigo menor,
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;
En el momento en que se asigna un objeto a una variable de tipo interfaz, Delphi comprueba automaticamente si el objeto implementa esa interfaz, mediante el operador as.Se puedc espresar csplicitamente esta operacion de este modo:
Flyerl
:=
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&+zlsa s dw ~~, el s h a de h recuenta de referencias de Del* se interrump y pue&.ariginar errores de diffciles de 1 r -: a J i la piktim, si 3 memoria que sean extre-entc . deberiamosiusa r f i w e n hemos decidido usar interfa~eerprobabkmeot~ 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 ~ jd ct . b
permiten separar el codigo de gestion de errores de codigo normal, en lugar de entremezclar ambos. A1 obligar a mantener una division logica entre el codigo y la gestion de errores y a1 conmutar al manejador de errores automaticamente, se consigue que la logica real resulte mas limpia y clara. Nos permiten escribir un codigo mas compacto y menos inundado por 10s habituales metodos de mantenimiento no relacionados con el objetivo real de programacion. En tiempo de ejecucion, las bibliotecas de Delphi crean excepciones cuando algo va ma1 (en el codigo de tiempo de ejecucion, en un componente, en el sistema operativo). Desde el punto del codigo en el que se crea, la escepcion se pasa a su codigo de llamada, y asi sucesivamente. Por ultimo, si ninguna parte del codigo controla la excepcion, la VCL se encarga de ella, mostrando un mensaje estandar de error y tratando de continuar el programa proporcionando el siguiente mensaje del sistema o peticion a1 usuario. Todo este mecanismo se basa en cuatro palabras clave:
try: Delimita el comienzo de un bloque protegido de codigo. except: Delimita el final de un bloque protegido de codigo e introduce las sentencias de control de excepciones. finally: Se usa para especificar bloques de codigo que han de ejecutarse siempre, incluso cuando se dan excepciones. Este bloque se usa generalmente para realizar operaciones de limpieza que siempre se deberian ejecutar, como cerrar archivos o tablas de bases de datos, liberar objetos y liberar memoria y otros recursos adquiridos en el mismo bloque de programa. raise: Es la sentencia usada para generar la excepcion. La mayoria de las excepciones que encontramos en programacion en Delphi las genera el sistema, per0 tambien'se pueden crear excepciones propias en el codigo, cuando se descubren datos no validos o incoherentes en tiempo de ejecucion. La palabra clave r a i s e tambien puede usarse dentro de un controlador para volver a crear una excepcion, es decir, para propagarla a1 siguiente controlador
. 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 11 y el externo con una sentencia y 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.
--- - - - TRUCO:Controlar la excepcion es generalmente mucho menos importante que utilizar 10s bloques f i n a l l y , puesto que Delphi puede sobrevivir a la mayoria de ellas. Ademas, dernasiados bloques para controlar excepciones en el c6digo probablernente indicarh errores en el flujo del programa y una mala comprension de la funcion de las excepciones en el lenguaje. .-..t r e 10s ejemplos ae este llbr0 ser veran mucaos bloques t r y /.r-l.n a l- l-y , . . . . m unas cuantas sentencias raise, y casi ningin bloque t r y / e x c e p t .
-
.-
. .
...
..
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 ')
Este metodo c r e a t e (heredado de la clase E x c e p t i o n ) tiene un parametro de cadena para describir la excepcion a1 usuario. No es necesario preocuparse de destruir el objeto creado para la excepcion, porque se borrara automaticamente gracias a1 mecanismo de control de excepciones. El codigo presentado en 10s extractos anteriores forma parte de un programa ejemplo, denominado Exception 1. Algunas de las rutinas se han modificado ligeramente, como en la siguiente funcion D i v i d e T w i c e P l u s O n e :
function DivideTwicePlusOne begin
try
..
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;
En el codigo de Esceptionl hay dos excepciones diferentes despues del mismo bloque t r y . Puede haber un numero cualquiera de controladores de este tipo, evaluados consecutivamente. Si se usa una jerarquia de excepciones, tambien se llama a un controlador para las subclases del tip0 a las que se refiere, como haria cualquier procedimiento. Por csta razon es necesario colocar 10s controladores de excepciones de mayor ambito (10s de la clase E x c e p t i o n ) a1 final. Pero hay que tener presente que utilizar un controlador para cada excepcion, como el anterior, no suele ser una buena opcion. Es mejor dejar las excepciones desconocidas para Delphi. El controlador de excepciones por defecto de la VCL muestra el mensaje de error de la clase de escepcion en un cuadro de mensaje y, a continuacion, reanuda el funcionamiento normal del programa. En realidad se puede modificar el controlador de escepciones normales con el evento A p p l i c a t i o n . O n E x c e p t i o n o el evento O n E x c e p t i o n del componente A p p l i c a t i o n E v e n t s , como se demuestra en el ejemplo ErrorLog posterior. Otro importante elemento mas del codigo anterior es el uso del objeto de excepcion en el controlador (vease en E: E x c e p t i o n do). La referencia E de la clase E x c e p t i o n se refiere a1 objeto de excepcion pasado por la sentencia r a i s e . Cuando se trabaja con escepciones, hay que recordar esta norma: se lanza una excepcion mediante la creacion de un objeto y se controla mediante la indicacion de su tipo. Esto nos ofrece una importante ventaja, porque como hemos visto, cuando se controla un tipo de excepcion, en realidad se controlan excepciones del tip0 que se especifica asi como de cualquier otro tip0 descendiente.
A1 arrancar un programa en el entorno Delphi (por ejemplo, a1 pulsar la tecla F9), por lo general se ejecuta en el depurador. Cuanda se encuentra una excepcion, el depurador detendrb por defecto el programa. Asi, sabrever mos donde tuvo lugar la excepcion y podremo~ la llamada del controlador paso a paso. Tambih se puede usar la cafacteristica Stack Tmce de Delphi para ver la secuencia de la funci6n y las llamadas de d o d o que dieron lugar a que el programa crease una ex&pci6n.
Sin X r G , e x caso del progrim & ejemp1o dxceptioni este cirn---~ portamiento confundira a un programador que no sepa bien c6mo funciona
el depurador de Delphi. Aunque se prepare el d d i g o para controlar de f o m a adecuada la excepcih, el depurador detendni la ejecucib del prow grama en la linea de c6digo fuente m& cercana a1 lvgar m ~l q se cfeb la se excepcion. Asi, a1 moverse paso a paso por el cMgo, puede verse controla. Si sirnplemente queremos dejar que el program se ejtcutt mamb la excepci6n se controla correctamente, hay que ejecutar el pr0gms.mdes& el Explorador de Windows o desactivar temporalme& la deteaqhh $top en las opcioncs de Delphi Extxptions de la ficha ~&guage b c d p f h s del cuadro de diirlogo Debugger Options (activada mediaate la wden Tools> Debugger Options), que aparece en la ficha Language Exceptions del cuadro de diilogo Debugger Options que se muestra a continuation. Tambien se puede detener el depurador.
'VCLEAbort Exceptions
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:
--
. .
..
. -
..
--
~~
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.
. . --
- .-
19 - 1
Div by 0
.............. ...................... 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
........
Supongamos que hemos definido la clase T M y C l a s s . Ahora, se puede definir un nucvo tipo de referencia de clase relacionado con dicha clase:
type TMyClassRef = class of TMyClass;
Ahora se pueden declarar variables de ambos tipos. La primera variable se reficre a un objeto, la segunda a una clase:
var
AnOb j ect : TMyClass; AClassRef: TMyClassRef; begin AnObject : = TMyClass.Create; AClassRef : = TMyClass;
Podriamos preguntarnos para que se usan las referencias de clase. En general, las referencias de clase permiten manipular un tipo de datos de clase en tiempo de ejecucion. Se puede usar una referencia de clase en cualquier espresion en la que sea valido el uso de un tipo de datos. En realidad, no hay muchas expresiones de este tipo, per0 10s pocos casos que esisten son interesantes, como la creacion de un objeto. Podemos rescribir las dos lineas anteriores del siguiente modo:
AnObject
: = AC1assRef.Create;
Esta vez hemos aplicado el constructor create a la referencia de clase en lugar de a una clase real. Hemos utilizado una referencia de clase para crear un objeto de dicha clase. Los tipos de referencia de clase no serian tan utiles si no soportasen la misma norma de compatibilidad de tipos que se aplica a 10s tipos de clase. Cuando se
declara una variable de referencia de clase, como MyClas s Ref, se le puede asignar esa clase especifica y cualquier subclase. Por lo tanto, si TMyNewClas s es una subclase de nuestra clase, tambien se puede escribir
AClassRef : = TMyNewClass; "uno"
Delphi declara una larga lista de referencias de clase en la biblioteca de tiempo de ejecucion y en la VCL, como por ejemplo las siguientes:
TClass = class of TObject; TComponentClass = class of TComponent; TFormClass = class of TForm;
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) ;
El primer parametro es una referencia de clase, el segundo es una variable que almacena una referencia a la instancia de objeto creada. Por ultimo, cuando se tiene una referencia de clase, se le pueden aplicar 10s metodos de clase de la clase relacionada. Si tenemos en cuenta que cada clase hereda de TOb j ec t, se pueden aplicar a cada referencia de clase algunos de 10s metodos de TObject.
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.
NOTA: Para que fkncione la construccion polimorfica. el tipo de la clase basica de la referencia de clase habd dc tener un constructor virtual. Si re m a w constructor virtual (corno en el ejemplo), la llamada dcl constructor apljcgda la referencia de clase llamara at constructor del tip0 al que realmente sc refiere la variable de referencia de clase. Pero sin un constructax virtual, el cbrfigQ llamara a1 constructor del tipo dc clase fijo indicado
exi k~&clwa&:de Ia referencia de clase. Los constructores son nccesarios para la construccion,~olimorf~ca mismo modo que 10s mktodos virtuales del son nccesariba para ei poIimorfismo.
biblioteca en tiempo
El lenguaje de programacion Delphi favorece un enfoque orientado a objetos, junto con un estilo visual de desarrollo. Es aqui donde sobresale Delphi y trataremos acerca del desarrollo visual y basado en componentes a lo largo de este libro; sin embargo, deseo subrayar el hecho de que muchas de las caracteristicas listas para ser utilizadas de Delphi proceden de su biblioteca en tiempo de ejecucion (RTL). trata de un gran conjunto de funciones que puede utilizar para realizar Se tareas sencillas, a1 igual que algunas complejas, dentro de su propio codigo Pascal. (Utilizo aqui "Pascal" porque la biblioteca en tiempo de ejecucion contiene principalmente procedimientos y funciones escritas con 10s mecanismos tradicionales del lenguaje y no con las extensiones de orientacion a objetos aiiadidas al lenguaje por Borland.) Existe un segundo motivo para dedicar este capitulo del libro a la biblioteca en tiempo de ejecucion: Delphi 6 supuso un gran numero de mejoras en este campo, y Delphi 7 aporta algunas mejoras mas. Estan disponibles nuevos grupos de funciones, se han desplazado funciones a nuevas unidades y han cambiado otros elementos, lo que crea unas pocas incompatibilidades con el codigo antiguo a partir del cual podria adaptar sus proyectos. Por eso, incluso aunque haya utilizado las versiones antiguas de Delphi y se sienta comodo con la RTL, aun asi deberia leer a1 menos parte de este capitulo. Este capitulo comenta 10s siguientes temas:
Nociones generales de la RTL. Funciones de la RTL de Delphi. El motor de conversion Fechas, cadenas de caracteres y otras nuevas unidades de la RTL. Informacion de clase en tiempo de ejecucion.
lar. Delphi es lo suficientemente listo como para darse cuenta de ello e incluir automiticamente la unidad Variants en proyectos que usan el tipo Variant ,emitiendo unicarnente UM advertencia.
TambiCn se han aplicado ciertos ajustes para reducir el tamaiio m i n i m ~ un de archivo ejecutable, a veces ampliado por inclusiones no deseadas de variables globales o codigo de inicializacion.
ncnte, pero suponc una gran ayuda para 10s desarrolladoTes. En aQpgtbs casos. incfuso anos cuantos KB (rttuItiplicados pur muchas apliCaciones) puedcn reducir cl tam-iio y, cn ultima instancia. el tiempa de descarga. Como pcqueiia prueba, h a s ercado el programa Minisiic, que n o e s a n
scr y muesua el resulraao en mymensajc. c p g a m a nomme vauanas r de alto nivel. A b m i x se utilka la funcibn s k r ~ p a r a cu&ertir un ederoen una cadena y no incluir sysufjls, ,que definc toc4-f mas complejas e implicaria un pequcfia numcntb I@ Si este prograrna se compila con Delphi 5. s e ~ w i cf: . - - .--. - . . . - . . de 18.452 bytes. Uelphi b reduce dicho tamano a mh 1 5 . 3 6 ~ ~ b y t e . recortando unos 3 KB.Con Delphi 7. qFtama5o o~.solg Iigeranl,en& mayor. cle 15.872 bytes. A1 reemplazar laiWena larg&.por m a caden? colta y mod II
i.
7--o--
ta menos de 10 KB. Esto es debido a rque se acabath eliminando las rutinas de scmarte deedenasv tamhih el ,:stor de memoria, lo cud es hicamen-- - - r ---- , -- se
------ -- 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.
Comentare de forma breve el papel de cada unidad y tambien 10s grupos de funciones incluidas. Ademas dedicare mas espacio a las unidades mas nuevas. No se trata de ofrecer una lista detallada de las funciones incluidas, ya que la ayuda electronica incluye un material de referencia similar. Sin embargo, la intencion es fijarse en algunas funciones interesantes o poco conocidas.
La unidad System se compone entre otras cosas de: La clase TO^ j ect, que es la clase basica de toda clase definida en el lenguaje Pascal orientado a objetos, como todas las clases de la VCL. Las interfaces IInterface,IInvokable,IUnknown y IDispatch, asi como la clase de implementation simple T Inter facedOb j ect. I~nterface aiiadio en Delphi 6 para recalcar el hecho de que el tip0 se de interfaz en la definicion del lenguaje Delphi, no depende en mod0 alguno del sistema operativo Windows. I~nvokable aiiadio en Delphi 6 se para soportar las llamadas basadas en SOAP. Codigo de soporte de variantes, como las constantes de tip0 variante, el tip0 de registro TVarData y el nuevo tipo TVariantManager,un amplio numero de rutinas de conversion de variantes y tambien registros variantes y soporte de matrices dinamicas. En este ambito ha habido un monton de cambios en comparacion con Delphi 5. Muchos tipos de datos basicos, como 10s tipos de punteros y de matrices y el tipo TDateTime. Rutinas de asignacion de memoria, como GetMem y FreeMem y el propio administrador de memoria, definido por el registro TMemoryManager y a1 que se accede mediante las funciones GetMemoryManager y SetMemoryManager. Para mas informacion, la funcion GetHeapStatus devuelve una estructura de datos THeapStatus. Dos nuevas variables globales (A11ocMemCount y A1 locMemSize) guardan el numero y tamaiio total de 10s bloques de memoria asignados. En el capitulo sobre la arquitectura de las aplicaciones Delphi encontrara mas informacion sobre la memoria y estas funciones. El codigo de soporte de modulos y paquetes, como el tip0 de punter0 PackageInfo,la funcion global GetPackageInfoTable y el procedimiento EnumModules. Una lista bastante larga de las variables globales, como el caso de aplicacion Windows MainInstance; IsLibrary,que indica si el archivo ejecutable es una biblioteca o un programa independiente; Isconsole,
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.
uso que se hace de la c&qdacibn condicio& con muchas referencias a ($IFDEF L M ) p ($WDEF MSTKMDOWS), que se usan para diferenciar entre 10s dos sistemas o~erativos. Fiiese en me aara Windows. Borland utiliza MSWINDOWS &a indicar 1 plataf&r& ; a1 complete, ya q e WINDOWS se utilizaba en las versiones de 16 bits del sistema operativo (en contraste con el simboIo WIN32).
Por ejemplo, otro aiiadido para la compatibilidad entre Linux y Windows esta relacionado con 10s saltos de linea en 10s archivos de testo. La variable DefaultTextLineBreakStyle, afecta a1 comportamiento de las rutinas que leen y escriben en archivos, como la mayoria de las rutinas de flujos de texto. Los valores posibles para esta variable global son t l b s L F (valor predeterminado en Kylix) y t l b s C R L F (valor predeterminado en Delphi). El estilo de salto de
linea tambien se puede configurar archivo por archivo mediante la funcion S e t T e x t L i n e B r e a k S t y l e . Del mismo modo, la constante global de cadena s L i n e B r e a k tiene el valor #13#10 en la version Windows del entorno de desarrollo y el valor # 1 0 en la version para Linux. Otro cambio es que la unidad System incluye ahora las estructuras T F i leRec y TTex t Rec, que en versiones anteriores de Delphi estaban definidas dentro de la unidad S y s u t i l s .
VER-PLATFORM-WIN32-NT: end:
ShowMessage ( 'Ejecutando en Windows: ' + IntToStr (Win32MajorVersion) + ' . ' + IntToStr (Win32MinorVersion) + ' (Creaci6n ' + IntToStr (Win32BuildNumber) + ' ) ' + #10#13 + ' Actualizacidn: ' + Win32CSDVersion) ;
El segundo fragment0 de codigo crea un mensa-je como el que muestra en la siguiente figura, dependiendo, claro esta, de la version del sistema operativo que se hava instalado.
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.
-
en Delphi, declaradas en la unidad SyncObj s (disporuble baj0 Source / R t 1 /Common) y con correspondencia directa con 10s objetos de sincronizaci6n del sistema operativo'(como eventos y secciones criticas en Windows).
NOTA: El sincronizador multilectura es linico porque soporta bloqueos recursivos y conversi6n de 10s bloqueos de lectura en bloqueos de escritura. El objetivo principal de la clase es permitir un acceso rapid0 y facil a diversos threads de lectura a1 recurso compartido, per0 a h asi pennitir que un thread obtenga el control exclusive del recurs; para reali&r actualizaciones relativamente poco frecuentes. Hay otras clases 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
tipos divisa y fecha-hora: FloatToCurr y FloatToDateTime se pueden usar para evitar una conversion de tipos explicita. Las funciones TryStrToFloat y TryStrToCurr intentan convertir una cadena en un valor de coma flotante o de divisa, y, en caso de error, devuelven el valor False en lugar de generar una excepcion (corno hacen las clasicas funciones StrTo Float y StrToCurr). La Ans iDequotedStr,que elimina comillas de una cadena, se corresponde con la funcion Ans iQuotestr aiiadida en Delphi 5. Con respecto a las cadenas, desde Delphi 6 existe un soporte muy mejorado de cadenas anchas, con una serie de rutinas como W i d e u p p e r c a s e , W i d e L o w e r C a s e , WideCompareStr,WideSameStr,WideCompareText,WideSameText y WideFormat. Todas estas funciones se utilizan como sus homologos AnsiString. Existen tres funciones ( T r y S t r T o D a t e , T r y E n c o d e D a t e y TryEncodeTime) que intentan convertir una cadena en una fecha o codificar una fecha u hora, sin crear una excepcion, de un mod0 similar a las funciones Try antes mencionadas. Ademas, la funcion DecodeDate Fully devuelve information mas pormenorizada, como el dia de la semana y la funcion CurrentY ear devuelve el aiio de la fecha actual. Hay una version que se puede transportar, sobrecargada de la funcion GetEnvironmentvar iab le. Esta nueva version usa parametros de cadena en lugar de parametros PChar y es, en definitiva, m b facil de utilizar:
function GetEnvironmentVariable(Name: string): string;
Otras funciones nuevas estan relacionadas con el soporte de interfaz. Dos nuevas versiones sobrecargadas de la poco conocida funcion support permiten verificar si un objeto o una clase soporta una interfaz dada. La funcion se corresponde con el comportamiento del operador is para clases y se proyecta a1 metodo QueryInterf ace.Veamos un ejemplo:
var W1: IWalker; J1: IJumper; begin W1 : = TAthlete.Create; // mds codigo. . . i f Supports (wl, IJumper) then begin J1 : = W1 as IJumper; Log (J1.Walk) ; end;
SysUtils incluye tambien una funcion IsEqualGUID y dos funciones de conversion de cadenas a GUID y viceversa. La funcion CreateGUID ha sido desplazada a sysutils para que este disponible tambien en Linux (con una implernentacion personalizada, por supuesto).
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).
Decenas de funciones disponen de este nuevo parametro adicional, que se usa en lugar de las opciones globales. Sin embargo, puede inicializarlo con las opciones predeterminadas del ordenador en el que se ejecutar su programa mediante la invocation de la nueva funcion GetLocale Format Settings (solo disponible en Windows, no en 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
i f /th e n / e l s e por una expresion mucho mas breve, escribiendo menos codigo y declarando normalmente menos variables temporales.
RandomRange y RandomFrom se pueden usar en lugar dc la traditional funcion Random para tener un mayor control de 10s valores aleatorios producidos por la RTL. La primera funcion devuelve un numero comprendido cntrc dos cstremos que se especifican, mientras que el segundo escoge un valor aleatorio de una matriz de numeros posiblcs quc sc pasa como un parametro. La funcion booleana I n R a n g e se puede usar para comprobar si un numero sc encuentra entrc otros dos valores. La funcion E n s u r e R a n g e , en cambio, obliga a que el valor cstd dentro del rango especificado. El valor dc retorno es el propio numero o el limite mas bajo o limite mas alto, en el caso de quc el numero sc encuentrc fuera del rango. Veamos un cjcmplo:
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 )
NOTA: DeJphi 7 supone solo una mejora en esta unidad de coaversi6n: -te parastones (la unidad britiinica de medida que es equivalente c , IU~U . . . , " 1 LIGUG ,, IG , ..: ; , .,, :. a ,, , a a 14 lib&). LII C . U"I"..:,, L G Ibaa", , c:,,* ~. U , m l G j a I a* u I u a u G a UG ,...a.;I. 3 l U LIIGUIUU 6 1 1
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
NOTA: Soundex es un algoritmo para comparar nombres basados en el mod0 en que suenan y no en el modo en que se deletrean. El algoritmo calcula un numero para cada sonido de la palabra, de modo que comparando dos de esos numeros se puede decidir si dos nombres suenan igual. El f sistema lo aplic6 por pimerzi vez en 1880 la U.S.Bureau o the census (La Oficina del Censo de EEUU); se patent6 en 1918 y en la actualidad es de dominio publico. El codigo soundex es un sistema de indexado que traduce - - - - - .. - : I : _ _ ---__ _--_ 3 nomores a .. coalgo ae cuatro caracteres Iormaao por una m r a y rres un numeros. Puede encontrar mas information a1 respecto en www.nara.gov/ genealogylcoding .html .
1
3-
-..-L-_
L -
. . A
l-L-_
A_-_
Mas a116 de las comparaciones, otras funciones proporcionan una prueba en dos direcciones (la simpatica funcion I f T h e n , similar a la que ya hemos visto para 10s numeros), duplican e invierten cadenas y sustituyen subcadenas. La mayoria de estas funciones de cadena se aiiadieron por comodidad para 10s programadores en Visual Basic que se pasaban a Delphi. Hemos utilizado algunas de dichas funciones en el ejemplo StrDemo, que usa tambien algunas conversiones de booleano a cadena definidas dentro de la unidad SysUtils. El programa en realidad es algo mas que una prueba para unas cuantas funciones. Por ejemplo, se usa la comparacion "soundex" entre las cadenas introducidas en dos cuadros de edicion. convierte el booleano resultante en una cadena y lo muestra:
ShowMessage (BoolToStr (AnsiResemblesText (EditResemblel-Text, EditResemble.2 .Text) , True) )
;
El programa tambien utiliza las funciones An s i M a t c h T e x t y Ans i I n d e x T e x t , tras haber rellenado una matriz dinamica de cadenas (denominada s t r A r r a y ) con 10s valores de las cadenas del interior del cuadro de lista. Se podria haber utilizado el metodo I n d e x o f de la clase T S t r i n g s , que es mas sencillo, pero esto habria anulado el proposito del ejemplo. Las dos comparaciones de lista se realizan del siguiente modo:
procedure begin
TForml.ButtonMatchesClick(Sender: TObject);
Fijese en el uso de la funcion I f T h e n en las ultimas lineas de codigo; tiene dos cadenas de salida alternativas, que dependen del resultado del test inicial ( n M a t c h >= 0). Tres botones adicionales realizan llamadas sencillas a otras tres funciones nuevas, con las siguientes lineas de codigo (una para cada una):
// r e p i t e ( 3 v e c e s ) u n a c a d e n a ShowMessage (Dupestring (EditSample.Text, 3 ) ) ; // i n v i e r t e l a c a d e n a ShowMessage (Reversestring (EditSample.Text)); // e s c o g e u n a c a d e n a a l e a t o r i a ShowMessage (RandomFrom ( s t r A r r a y ) ) ;
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-
mos de ella. Cuando se buscan multiples apariciones de una cadena dentro de otra, una solucion clasica de Delphi era utilizar la funcion Pos y repetir la busqueda sobre la parte restante de la cadena. Por ejemplo, podria contar el numero de apariciones de una cadena dentro de otra con un codigo como este:
f u n c t i o n CountSubstr (text, sub: string) : Integer; var nPos: Integer; begin Result : = 0; nPos : = Pos (sub, t e x t ) ; while nPos > 0 do begin Inc (Result) ; text : = Copy (text, nPos + Length ( s u b ), MaxInt) ; nPos : = Pos (sub, text) ; end; end;
La nueva funcion PoS E X permite especificar la posicion de comienzo de la busqueda dentro de una cadena, de manera que no se necesita modificar la cadena
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.
NOTA: En Dclphi 7, estas unidades se han arnpliado y se han solucionado algunos problcmas. La impiementacion de variantes ha sido remodelada a conciencia para mejorar la velocidad de esta tecnologia y reducir la ocupacion en memoria de su codigo.
Un arca cspccifica quc ha visto una me-jora significativa en Dclphi 7 es la capacidad de controlar cl comportamicnto de las implemcntacioncs dc variantes, en particular las rcglas dc comparacion. Delphi 6 supuso un cambio en el codigo dc variantes de mancra quc 10s valores null no podian compararsc con otros valores. Estc comportamiento es correct0 desde un punto de vista formal, de manera cspccifica para 10s campos de un conjunto dc datos (un area en que se usan mucho variantcs). pcro cste cambio tuvo cl cfccto colateral de romper el codigo csistcntc. Ahora sc puede controlar cstc comportamiento mediante el uso dc las variablcs globales Nu1 lEqual it yRule y Nu1 lMagnitudeRule; cada una dc las cualcs toma uno de 10s siguicntcs valorcs: n c r E r r o r : Cualquicr tipo de comparacion provoca quc sc levante una escepcionj \,a quc no pucdc compararse un valor indcfinido: cste cra cl comportamicnto prcdctcrminado (nuevo) en Dclphi 6 . ncrstrict: Cualquier tipo de comparacion falla siempre (devuelvc False), sin importar 10s valores. ncrLoose: Las comprobaciones de igualdad solo tienen cxito cntrc valores nulos (un valor nulo es distinto de cualquicr otro valor). En las comparacioncsi 10s valorcs nulos se consideran como valorcs vacios o cero. Otras opcioncs con10 NullStrictConvert y NullAsStringValue controlan cl mod0 cn quc sc rcalizan las comparaciones en caso dc valorcs nulos. Un buen consejo cs cspcrimcntar con el e.jcmplo VariantComp quc se cncuentra disponiblc mas adelantc. Como mucstra la figura 3.2. este programa dispone de un formulario con un RadioGroup que se puede utilizar para modificar 10s \ d o res de las variablcs globales NullEqualityRule y NullMagnitudeRule y unos cuantos botoncs para rcalizar diversas comparacioncs.
un numero. El sistema define conversiones automaticas entre tipos dc variantes (como variantes personalizadas), lo que le permite mezclarlas en las opcraciones. Esta flexibilidad tiene un coste muy alto: las operaciones con variantes son mucho mas lentas que las rcalizadas con tipos originales y las variantcs utilizan memoria adicional.
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.
ADVERTENCIA: Construir una variante personalizada no es una tarea nada sencilla y apenas se pueden encontrar razones para usarlas en lugar de objetos y clases. De hecho, con una variante personalizada se tiene la ven. . . raja ae usar la soorecarga ae operaaores en las esrrucruras ae aaros, per0 se pierde la verification en tiempo de cornpilacion, se hace que el codigo sea
I I . I I I
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).
_ L C -
NOTA: Al contrario que Delphi, Kylix no dispone de unidades DelphiMM y ShareMem, ya que la gestion de memoria se proporciona en las bibliotecas nativas de Linux (en particular, Kylix utiliza malloc de glibc) y por eso se comparte efectivamente entre distintos modulos. Sin embargo, en Kylix, las aplicaciones con multiples modulos deben utilizar la unidad ShareExcept, que permite que las excepciones lanzadas en un modulo se reflejen en otro.
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
finales. La funcion clave es la llamada de conversion, la funcion Convert. Sencillamente, nosotros proporcionamos la cantidad, las unidades en las que se expresa y las unidades a las que queremos que se conviertan. Lo siguiente convertiria una temperatura de 3 1 grados centigrados a Fahrenheit:
Convert (31, tucelsius, tuFahrenheit)
Una version sobrecargada de la funcion convert permite convertir valores que poseen dos unidades, como la velocidad (que tiene una unidad de longitud y una unidad de tiempo). Por ejemplo, se pueden convertir kilometros por hora en metros por segundo con esta llamada:
Convert (20, duKilometre, tuHours, duMeters, tuseconds)
Otras funciones de la unidad permiten convertir el resultado de una suma o una resta, verificar si las conversiones se pueden aplicar e incluso listar las familias y unidades de conversion disponibles. En la unidad StdConvs, se proporciona un conjunto predefinido de unidades de medida. Esta unidad tiene familias de conversion y un impresionante numero de valores, como en el siguiente extracto:
// U n i d a d e s d e c o n v e r s i o n d e d i s t a n c i a s // l a u n i d a d b d s i c a d e m e d i d a e s e l m e t r o cbDistanke: TConvFamily;
duAngstroms : TConvType; dulrlicrons: TConvType; dulrlillimeters: TConvType; duMeters : TConvType; duKilometers: TConvType; duInches: TConvType; duMiles: TConvType; duLightYears: TConvType; duFurlongs: TConvType; duHands : TConvType ; duPicas: TConvType;
Esta familia y las diversas unidades se registran en el motor de conversion en la parte de inicializacion de la unidad y proporcionan ratios de conversion (guardados como una serie de constantes, como MetersPerInch en el siguiente codigo):
cbDistance : = RegisterConversionFamily('Distancia'); duAngstroms : = RegisterConversionType(cbDistance, ' A n g s t r o m s ' , 1E-10) ; d m i l l i m e t e r s : = RegisterConversionType(cbDistance, ' M i l i m e t r o s ' , 0.001) ; duInches : = RegisterConversionType(cbDistance, ' P u l g a d a s ' , MetersPerInch) ;
Para probar el motor de conversion, creamos un e.jemplo generic0 (ConvDemo) quc permite traba.jar con todo el conjunto de conversiones disponibles. El programa rellena un cuadro combinado con las familias de conversion disponibles y un cuadro de lista con las unidades disponibles de la familia activa. Este es el codigo:
procedure TForml. Formcreate (Sender: TObject) ; var i: Integer; begin GetConvFamilies (aFamilies); for i : = Low (aFamilies) to High (aFamilies) do ComboFamilies.1tems.Add (ConvFamilyToDescription (aFamilies [i]) ) ; // obtiene el primer0 y lanza el evento ComboFamilies.Item1ndex : = 0; ChangeFamily (self); end ; procedure TForml.ChangeFamily(Sender: TObject); var aTypes : TConvTypeArray; 1: Integer; begin ListTypes .Clear; CurrFamily : = aFamilies [ComboFamilies.ItemIndex]; GetConvTypes (CurrFamily, aTypes) ; for i : = Low(aTypes) to High(aTypes) do ListTypes.Items.Add (ConvTypeToDescription (aTypes[i])); end;
Las variables aFamilies y CurrFamily se declaran en la parte privada del formulario dcl siguiente modo:
aFamilies: TConvFamilyArray; CurrFamily: TConvFamily;
En este punto, un usuario puede introducir dos unidades de medida y una cantidad en 10s cuadros de edicion correspondientes del formulario, como se puede ver en la figura 3 3.Para que la operacion sea mas rapida, es posible seleccionar un valor de la lista y arrastrarlo hasta uno de 10s dos cuadros de edicion de tipo.
pulsado el boton lzquierdo del raton mientras se arrastra el elemento sobre una de las cajas de edicion que se encuentran en el centro del formulario.
p a r a c o i k g z c & p a z , hay q z - ~ propiedad a DragMode de la caja de lista (el componente fuente) con el valor dmAutomatic e implementar 10s eventos OnDragOver y OnDragDrop de las cajas de edicion objetivo (las dos cajas de edicion se encuentran conectadas a 10s mismos manejadores de eventos, compartiendo el mismo codigo). En el primer mktodo, el programa indica que las cajas de edicion siempre aceptan la operaci6n de arrastre, sin importar la fuente. En el segundo mktodo, el programa copia el texto seleccionado en la caja de lista (el control source para la operation de arrastre) a la caja de edicion que haya disparado el evento (el objeto Sender). Este es el c d i g o para 10s dos metodos:
procedure TForml.EditTypeDragOver(Sender, Source: TObject; X I Y: Integer; State: TDragState; var Accept: Boolean); begin Accept := True; end; procedure TForml.EditTypeDragDrop(Sender, Source: TObject; X I Y: Integer) ; begin ; (Sender ar TEdit) .Text := (Source as TListBox) .Items [ (Source as TListBox) ItemIndexl; end ;
--
E mk a is
D~stance lvper LghlYeats Parsecs Fathom Furbngs Hands Paces Cham Snnple Ted
&&:
1100
Qerlinalica Type
Cmvwted Am&
Las unidades habran de corresponderse con aquellas disponibles en la familia activa. En caso de error, el texto de 10s cuadros de edicion de tipo aparecc en rojo. Este es el efecto de la primera parte del metodo D o C o n v e r t del forinulario, que se activa desde el momento en que el valor de uno de 10s cuadros de edicion para las unidades o la cantidad cambian. Despues de verificar 10s tipos de 10s cuadros de edicion, el metodo D o C o n v e r t realiza la conversion real y muestra el resul-
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;
(DestType = 0 ) or (BaseType = 0 ) then EditConverted.Text : = ' T i p o n o v d l i d o ' else EditConverted.Text : = FloatToStr (Convert ( StrToFloat ( E d i t A m o u n t - T e x t ) , BaseType, DestType)); end;
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 sec) cion 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
La seccion de implementation de la unidad define constantes para diversas tasas de conversion oficiales:
implementation cons t DEMPerEuros = 1 , 9 5 5 8 3 ; ESPPerEuros = 166,386; FRFPerEuros = 6,55957; // y e l r e s t o . .
Finalmente, el codigo de inicializacion de la unidad registra la familia y las diversas divisas, cada una con su propio tip0 de cambio y un nombre legible:
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) ;
NOTA: El motor utiliza como factor de conversion la cantidad de la unidad base necesaria para obtener las unidades secundarias, con una constante como Meters P e r I n c h , por ejemplo. El tipo e s t h d a r de las divisas euro se define al rev&. Por este motivo, se han mantenido las constantes de conversion con 10s valores oficiales (como DEMPerEuros) y se han pasado a1 motor como fracciones ( I / DEMPerEuros).
Tras registrar esta unidad, se pueden convertir 120 marcos alemanes en liras italianas de esta manera:
Convert
( 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~an Francs [BEF) Dutch Gu~lders [NLG] Austrian Sch~ll~ngi [ATS] Poituguese Escudos [PTE) F~nn~sh Marks [FIM] G~eek Drachms [GRD] Luembou'g F'-f [LUFI
Cmml
Ilabn L#e[ITLJ ' BelgianFrancs [BEF) Dutch Gulders [NLG) Auslnan Sch~nlngs (ATS] Porluwese Escudos lPTEl ~ m n &~ a r k o [FIM] ' Greek Drachmas(GRDI LuxembourgFrancs [LUF)
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 ;
function EuroConvert (const AValue: Double; const AFrom, ATo: TConvType; const Decimals: TEuroDecimals = 3): Double; function EuroRound (const AValue: Double): Double; begin Result :=AValue * Power (10, Decimals) ; Result : = Round (Result); Result : = Result / Power (10, Decimals) ; end ; begin // comprobacion d e l c a s o e s p e c i a l : s i n c o n v e r s i o n if AFrom = ATo then Result : = AValue; else begin / / conversion a1 euro y redondeo Result : = ConvertFrom (AFrom, AValue) ; Result : = EuroRound (Result); / / conversion a la divisa y nuevo redondeo Result : = ConvertTo (Result, ATo) ; Result : = EuroRound (Result); end ; end ;
Por supuesto, podria desearse ampliar el ejemplo para ofrecer conversion a otras divisas no europeas, tal vez tomando 10s valores automaticamente desde un sitio Web.
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
Si el parametro Re c u r s e se activa, el procedimiento A d d F i l e s T o L i s t obtiene una lista de subcarpetas inspeccionando 10s archivos locales de nuevo y autoinvocandose para cada una de las subcarpetas. La lista de carpetas se coloca en un objeto de lista de cadenas, con el codigo siguiente:
procedure GetSubDirs (Folder: string; sList: TStringList); var s r : TSearchRec; begin faDirectory, s r ) = 0 then i f FindFirst (Folder + ' * . * I , try repeat i f ( sr.Attr and faDirectory) sList .Add (sr.Name) ; u n t i l FindNext (sr) <> 0; finally Findclose ( s r ); end ; end ;
=
faDirectory then
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)
Figura 3.6. El cuadro de dialog0 del procedimiento s e l e c t ~ i r e c t o r y utilizado por , la aplicacion FilesList.
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
Class Name TButton Parent Chss: TBultonConlrd Instance Sin: 536 TButton ClassType Sender inherits Lom TButton Sender is a TButlm Class Name. TBitBtn Palent Class TBulton Instance Size: 560 Sender &its from TButton Sendm is a TBulton Class Name TCheckBox Parent Class TCustornCheckBon Indance Size. 536 Class Nam: TEB Paent Class: TCustomEdit lnstace Size: 544
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 ...
El codigo de la sentencia with que se refiere a la lista Listparent es parte del ejemplo class Info, que muestra la lista de clases padres y alguna otra informacion sobre una serie de componentes de la VCL (basicamente aquellos de la pagina Standard de la Component Palette). Dichos componentes se aiiaden de forma manual a la matriz dinamica que mantiene las clases y que se declara como:
private ClassArray: array o f TClass;
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.
7
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.
La clase basica TComponent y sus propiedades Componentes y propiedad Eventos. Listas, clases contenedoras y colecciones. Streaming. Las unidades del paquete RTL.
I
I
Figura 4.1. Una representacion grafica de 10s principales grupos de componentes de la VCL.
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.
---
NOTA: Para ser m b precisos, las clases no componentes no pueden estar i disponibles en la Component Palette n se pueden dejar eaer ditectamente en un formulario, pero se pueden,administtar visu.almeste con el Object Inspector, como subpropiedades de otras propiedadis o elemmtos de varios tipos. Por lo que, incluso las clases no componentes son n o ~ a h m t facie les de usar, gracias a la interfaz con el Form Designer. .
Las clases componentes pueden dividirse ademas en dos grupos principales: controles y componentes no visuales. Controles: Todas las clases que descienden de TControl.Tienen una posicion y tamafio en pantalla y aparecen en el formulario en tiempo de diseiio en la misma posicion que tendrian en tiempo de ejecucion. Los controles tienen dos subespecificaciones diferentes, basados en ventanas o graficos.
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-
rrespondientes controles VCL, por lo que podemos adaptar facilmente el codigo de una biblioteca a otra. DataCLX: Engloba todos 10s componentes relacionados con bases de datos de la biblioteca. En realidad, DataCLX es la fachada del nuevo motor de base de datos dbExpress incluido tanto en Delphi como Kylix. Delphi incluye tambien la tradicional interfaz BDE, dbGo e InterBase Express (IBX). Si consideramos todos estos componentes como parte de DataCLX, solo la interfaz dbExpress e IBX resultan transportables entre Windows y Linux. DataCLX incluye tambien el componente C l i e n t Data S e t, ahora llamado MyBa s e y otras clases relacionadas. NetCLX: Incluye 10s componentes relacionados con Internet, desde el marco de trabajo WebBroker, a 10s componentes del productor HTML, desde Indy (Internet Direct) a Internet Express, de WebSnap a1 soporte XML. Esta parte de la biblioteca es, una vez mas, muy facil de transportar entre Windows y Linux.
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 r aplica tarnbien a e 10s archivos XF M, el ~ o m r ae arcolvo que u u ~ ras t@xwiones CLX. o m 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 rAUinrlnrtre . En x r jru in , tnrln Fnrmn.lnAm m u ~n f~rmulfwrip U x l v wuv r w l u r u x u l v v m v u rrruwrru u u CLXIQt, sin XF'IWDFM no importar la extension que se emplee; por eso, la extensi~n tiene importancia en Kylix.
. ..
Sin embargo, el soporte de streaming no esta incluido en la clase TPersistent, aunque nos lo ofrecen otra clases, que tienen como objetivo T P e r s i s t ent y sus descendientes. En otras palabras, con el streaming predefinido de Delphi se puede hacer que "permanezcan" objetos solo de clases que hereden de T P e r s i s tent. Una de las razones de este comportamiento recae en el hecho de que la clase se compila con una opcion especial, { $M+). Este atributo activa la generacion de informacion ampliada RTTI para la parte publicada de la clase. El sistema de streaming de Delphi, de hecho, no intenta guardar datos en memoria de un objeto, algo que seria complejo debido a 10s muchos punteros y a otras posiciones de memoria. En su lugar, Delphi guarda objetos listando el valor de todas las propiedades de una seccion marcada con una palabra clave especial, published. Cuando una propiedad se refiere a otro objeto, Delphi guarda el nombre del objeto o el objeto entero (con el .mismo
mecanismo), dependiendo de su tip0 y relacion con el objeto principal. De 10s metodos de la clase TPersis tent,el iinico que se utilizara por lo gcneral es el procedimiento Assign,que puede utilizarse para copiar el valor real de un objeto. En la biblioteca, este metodo esta implementado por varias clases no componentes, pero por muy pocos componentes. En realidad, la mayoria de las subclases vuelven a implementar el metodo virtual protegido AssignTo, al que llama la implementation predefinida de Assign. Otros metodos son, por ejemplo, Def ineproperties,utilizado para personalizar el sistema de streaming y aiiadir informacion adicional (pseudopropiedades); y Getowner o GetNamePath, utilizados por colecciones y otras clases especiales para identificarse ante el Object Inspector
.
..PC..I+C.-A
n~,.a~.m..;r.
aC...-;h;..
A.LXA;~A- A . . . . ~ I Y ~ ~ + ~
Cada enfoque tiene sus ventajas e inconvenientes. Cuando se genera codigo fbente, se tiene mas control sobre lo que sucede y la secuencia exacta de creacion e inicializacion. Delphi vuelve a cargar 10s objetos y sus propiedades pero retarda algunas asignaciones hasta una fase posterior de retoque, para evitar 10s problemas con las referencias a objetos que aun no se han .... . pero queaa tan .. ocu~to . mlclanzaao. aste proceso es mas complejo, men que resulta mis simple para el p r o g r ~ b r . El lenguaje Java permite que una herramienta como JBuilder vuelva a com.- . pilar ma clase formulano y cargarla en m programa en ejecuci6n para cada cambio. En un sistema cornpilado como Delphi, ese enfoque seria mucho 11lPls complejo (entiempo de diseiio Delphi utiliza una versibn falsa, tecnicahmte Jlamada proxy, del formulario, no el formulario real). Una ventaia del enfoque utilizado por Delphi es que 10s archivos DFM pueden traducirse a distintos lenguajes sin afectar a1 c M g o fbente; es por este motivo que Java ofrece la permanencia XML de formularies. Otra diferen& es c p e Delphi incrusta el grirfico del compomte en el archivo DFM,en & referemias a archivw e?rternoa.'3hxr esto simplifka el desamHq fpprque to& acab farmanda'parte dd,a~lrkvo j ~ t a e bIe] pero tambicn puede imficiwque el e j w t a b l e sea mucho mayor,
T . .
..
~~~
<-
n d e ~ ~ e~ t h &ignar~un objeto a1 & n p o o d ~ & 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
Delphi utiliza un c6digo similar para asignar un manejador de eventos cuando carga un archivo DFM, ya que estos archivos almacenan el nombre de 10s metodos utiiizados para manejar 10s eventos, mientras que 10s componentes guardan el puntero a1 metodo. El segundo metodo, M e t h o d N a m e , realiza la transformation contraria, devolviendo el nombre del metodo para una direccion de memoria dada. Este metodo puede utilizarse para conseguir el nombre de un manejador de evento, dado su valor, algo que Delphi hace cuando envia mediante streaming un componente a un archivo DFM. Finalmente, el metodo F i e l d A d d r e s s de TOb ject dewelve la posicion de memoria de un campo publicado, dado su nombre. Delphi usa este metodo para conectar componentes credos a partir de archivos DFM con 10s campos de su propietario (por ejemplo, un formulario) que tienen el mismo nombre. Fijese en que estos tres metodos rara vez se utilizan en programas "nonnales", per0 juegan un papel central en el funcionamiento de Delphi. E s t h estrictamente relacionados con el sistema de streaming. Solo necesitara utilizar estos mCtodos cuando escriba programas extremadamente dinamicos, asistentes de proposito especial u otras extensiones de Delphi.
RTTI de propiedades es algo posible gracias a un grupo de subrutinas no documentadas, parte de la unidad TypInfo
ADVERTENCIA: Estas subrutinas siempre han estado no documentadas en Ias versiones anteriores de Delphi, asi que Borland ha seguido siendo libre de modificarlas. Sin embargo. desde Delphi 1 a Delphi 7,los cambios han sido muy limitados y solo han estado relacionados con el soporte de nuevas caracteristicas, con un alto nivel de compatibilidad hacia atras. En Delphi 5, Borland aiiadio muchas mas caracteristicas y unas pocas rutinas "auxiliares" que se promocionan oficialmente (aunque no queden completamente documentadas en el archivo de ayuda sino que se explican ~610 con
mmentnrinc e n In ~ ~ n i d a d b
Antes de Delphi 5, era necesario utilizar la funcion GetPropInfo para recuperar un punter0 a alguna informacion interna sobre la propiedad y aplicar a continuation una de las funciones de acceso como Get St rProp,a dicho puntero. Tambicn hay que verificar la existencia y el tipo de la propiedad. Ahora se puede utilizar el nuevo conjunto de rutinas de TypInfo, entre las que se incluye la practica GetPropValue; que devuelve una variante con el valor de la propiedad y levanta una escepcion si la propiedad no esiste. Para evitar la escepcion, sc puede llamar en primer lugar a la funcion IsPublishedProp.A estas funciones simplcmente se pasa el objeto y una cadena con el nombre de propiedad. Un parametro opcional mas de GetPropValue permite escoger el formato para el retorno de 10s valores de las propiedades de cualquier tip0 de conjunto (ya sea una cadena o el valor numeric0 para el conjunto). Por ejemplo, se puede utilizar:
ShowMessage (GetPropValue (Buttonl, '
' C a p t i o n ' )) ;
Esta llamada tiene el mismo efecto que una llamada a ShowMessage en la que se utilice como parametro Buttonl .Caption. La unica diferencia real es que esta version del codigo es mucho mas lenta, ya que el compilador generalmente resuelve el acceso normal a propiedades de una manera mas eficiente. La ventaja del acceso cn ticmpo de ejecucion es que puede hacer que resulte muy flexible, como en el ejemplo RunProp. Este programa muestra en un cuadro de lista el valor de una propiedad de un tip0 cualquiera para cada componente de un formulario. El nombre de la propiedad que buscamos aparece en un cuadro de edicion. Esto hace que el programa resulte muy flexible. Ademas del cuadro de edicion y del cuadro de lista, el formulario tiene un boton para crear la salida y algunos otros componentes solo para verificar sus propiedades. Cuando hacemos clic en el boton, se ejecuta el siguiente codigo:
uses TypInfo;
procedure TForml.ButtonlClick(Sender: TObject); var I: Integer; Value: Variant; begin ListBoxl.Clear; for I : = 0 to Componentcount -1 do begin if IsPublishedProp (Components[I], Edit1 .Text) then begin Value : = Getpropvalue (Components[I], Editl.Text) ; ListBoxl.Items .Add (Components[I] .Name + ' . ' + Editl.Text + ' = ' + string (Value)) end else ListBoxl.1tems.Add ('No ' + Components[I] .Name + ' ' + Editl.Text) ; end; end;
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 &Pmpe~ty l =
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;
Si se crea un componente y se le asigna un propietario, este se aiiadira a la lista de componentes ( I n s e r t c o m p o n e n t ) , a la que se accede utilizando la propiedad de la matriz C o m p o n e n t s . El componente especifico tiene un o w n e r (propietario) y conoce su posicion en la lista de componentes propietarios, gracias a la propiedad C o m p o n e n t I n d e x . Por ultimo, el destructor del propietario se encargara de la destruccion del objeto que posee, llamando a D e s t r o y Component s.Hay unos pocos metodos protegidos mas envueltos en este proceso, per0 esto servira como vision global. Es importante enfatizar que la posesion de componentes puede resolver una gran parte de 10s problemas de administracion de memoria de las aplicaciones, si se usa de forma apropiada. Cuando se usa el Form D e s i g n e r o el Data M o d u l e
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;
Este codigo usa la propiedad C o m p o n e n t c o u n t . que mantiene el numero total de componentes que posce el formulario activo y la propiedad c o m p o n e n t s , que en realidad es la lista de 10s componentes poseidos. Cuando accedemos a un valor desde esta lista, se obtiene un valor del tipo TComponent. Por esa razon, se pueden usar directamente solo las propiedades comunes a todos 10s componentes, como la propiedad N a m e . Para usar propiedades especificas de componentes concretos, hay que w a r la comprobacion de tipos correcta ( a s ) .
componentes Form. Cuando se emplean estos controles, se pueden aiiadir otros componentes dentro de ellos. En este caso, el contenedor es el padre de 10s componentes (como indica la propiedad Parent), mientras que el formulario es su propietario (como indica la propiedad Owner). Se puede
ULYi Y p ~ ~ un formulario o cuadro de grupo pard f de ~ ~ t ma~erse p&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;
Este metodo (extraido del ejemplo ChangeOwner) cambia el propietario del componente. Se le llama junto con el codigo mas simple utilizado para cambiar el
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);
El metodo verifica si el campo B u t t o n l se refiere aun a1 control, porque mientras mueve el componente, Delphi define B u t t o n l como n i l . Podemos ver el efecto de este codigo en la figura 4.3.
1
Figura 4.3. En el ejemplo Changeowner, al hacer clic sobre el boton Change se mueve el componente Buttonl a segundo formulario. 1
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 1 c k. i 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 . . .
NOTA: Delphi incluye t a m b i h una funcion Fi ndGloba lCompone n t, que encuentra un componente de alto nivel, basicamente un fonnulario o un modulo de datos, que tenga un nombre dado. Para ser precisos, la funcion FindGlobalComponent llama a una o mas hnciones instaladas, por lo que en teoria se puede modificar el resultado de la funcion. Sin embargo, cuando el sistema de streaming usa FindGlobalComponent, es muy recomendable no instalar sus propias funciones de sustitucion. Si queremos buscar componentes en otros contenedores de forma personalizada, simplemente hay que escribir una nueva funcion con un nombre personalizado.
utilizada en el formulario, de modo aue el'enlazador intelinente Y el sktema de streaming enlacen el codigo necesario para la clase y lo reconozcan en el archivo DFM.Si, como ejemplo, se eliminan todos 10s campos de un for,..I,,:, IIIUI~IIW ~
,.U -- c , , .- SG , , , , G IGLIGIGU
, ---- , a , a w ., p u n G, i G s ml u
ms - L - 1
I Ldual, wauuu GI
-..---I,
, -1 : , ,- , &
, . , slaicura ,; i r ~ g u ~ r
el fonnulario en tiempo de ejecucion, no sera capaz de crear un objeto de una clase desconocida y emitira un error indicando que la clase no estir disponible. Se pueden emplear las rutinas Reg i s t e r C 1 a s s o Registerclasses para evitar este error. Tambien se puede mantener el nombre del componente y eliminar manualmente el campo correspondiente de la clase de formulario. Aunque el componente no tenga un campo de formulario correspondiente, se creara de todos modos, per0 sera un poco mas dificil usarlo (a traves del metodo Findcomponent, por ejemplo).
procedure TForml.EditlChange(Sender: TObject); begin Buttonl.Enabled : = Length (Edit1 .Text) <> 0; end;
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;
Casi funciona, per0 genera un error de sistema similar a1 comentado en el apartado anterior. Esta vez, las declaraciones privadas daran lugar a que el enlazador relacione las implementaciones de aquellas clases, per0 el sistema de streaming necesita conocer 10s nombres de las clases para localizar la referencia de clase necesaria y construir 10s componentes mientras carga el archivo DFM. El toque final que necesitamos es algun codigo de registro para avisar a Delphi en tiempo de ejecucion de la existencia de las clases de componentes que queremos usar. Deberiamos hacerlo antes de crear el formulario, asi normalmente conviene colocar este codigo en la seccion de inicializacion de la unidad:
initialization Registerclasses ([TButton, TEdit, TListBox]);
La pregunta es si merece el esfuerzo. Se puede obtener un mayor grado de encapsulado y proteger 10s componentes de un formulario de otros formularios (y de otros programadores que 10s escriban). Repetir estos pasos para cada formulario puede ser tedioso, asi que lo ideal es utilizar un asistente para generar el codigo sobre la marcha, ya que se trata de una tecnica muy aconsejable para un
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.
NOTA: Como se vera a continuacibn, 10s eventos en Delphi se basan en punteros a m&odos. E s b es bastante distinto de Java, que emplea clases de escucha (1i s t ene r) con m M o s para una Earnilia de eventos.Estos mCtodos de escucha llaman a 10s controladores & e m t o s . C# y .NET utilizan una idea similar a las clases delegadas. El t&nmiao "delegadon es el mismo que el usado tradicionalmente en la bibliograa sobre Delphi para explicar la idea de 10s controladores de eventos.
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.
NOTA: Esto explica la definicih del tipo genCrico de Delphi TMethod, un registro con un campo Code y un campo Data.
La declaracion de un tip0 de puntero a metodo es similar a la de un tipo de procedimiento, except0 por el hecho de que tiene las palabras clave of ob j e c t a1 final de la declaracion:
Cuando hemos declarado un puntero a metodo, como el anterior, se puede declarar una variable de ese tipo y asignarle un metodo compatible (un metodo que tenga 10s mismos parametros, tipo de retorno, convencion de llamada) de otro objeto. Cuando se aiiade un controlador de eventos OnClick para un boton, Delphi hace exactamente eso. El boton tiene una propiedad de tipo de puntero a metodo, llamada OnClick y se le puede asignar directa o indirectamente un metodo de otro objeto, como un formulario. Cuando un usuario hace clic sobre el boton, se ejecuta este metodo, aunque lo hayamos definido dentro de otra clase. Lo que sigue es un fragmento del codigo que en realidad usa Delphi para definir el controlador de eventos de un componente boton y del metodo relacionado de un formulario:
t m e TNotif yEvent = procedure (Sender: TObj ect) o f object; MyButton = class OnClick: TNotifyEvent; end; TForml = class (TForm) procedure ButtonlClick Buttonl: MyButton; end; var Form1 : TForml ;
(Sender: TObject) ;
La unica diferencia real entre este fragmento de codigo y el codigo de la VCL es que OnC 1 i c k es un nombre de propiedad y 10s datos reales a 10s que se refiere se llaman FOnClic k.Un evento que aparece en la ficha Events del Object Inspector, de hecho, no es nada m h que una propiedad de un tipo de puntero a metodo. Esto significa, por ejemplo, que se puede modificar de forma dinamica el controlador de eventos asociado a un componente en tiempo de diseiio o incluso construir un nuevo componente en tiempo de ejecucion y asignarle un controlador de eventos.
asigna un metodo a la propiedad de evento correspondiente. Cuando hacemos doble clic sobre un valor de evento en el Object Inspector, se aiiade un nuevo metodo a1 formulario propietario y se asigna a la propiedad de evento correcta del componente. Esta es la razon por la cual es posible compartir el mismo controlador de eventos para diversos eventos o cambiar un controlador de eventos en tiempo de ejecucion. Para utilizar esta caracteristica no se necesita mucho conocimiento sobre el lenguaje. De hecho, cuando se selecciona un evento en el Object Inspector, se puede pulsar el boton de flecha situado a la derecha del nombre del evento para ver una lista desplegable de metodos compatibles (una lista de metodos que tienen la misma definicion que el tipo de puntero a metodo). Al usar el Object Inspector, es facil seleccionar el mismo metodo para el mismo evento de diferentes componentes o para diferentes eventos compatibles del mismo componente. Aiiadiremos un evento muy sencillo. Se denominara O n C h a n g e y se puede usar para advertir a1 usuario del componente de que el valor de la fecha ha cambiado. Para definir un evento, simplemente definimos una propiedad correspondiente a1 mismo y aiiadimos algunos datos para almacenar el puntero a metodo real a1 que se refiere el evento. Estas son las nuevas definiciones aiiadidas a la clase, disponibles en el ejemplo DateEvt:
type TDate = c l a s s private FOnChange: T N o t i f y E v e n t ;
...
protected p r o c e d u r e Dochange;
...
dynamic;
.-.
end;
La definicion de propiedad es sencilla. Un usuario de esta clase puede asignar un nuevo valor a la propiedad y, por lo tanto, a1 campo privado F O n C h a n g e . La clase no asigna un valor a1 campo F O n C h a n g e ; es el usuario del componente el que lo hace. La clase T D a t e simplemente llama a1 metodo almacenado en el campo F O n C h a n g e cuando cambia la fecha. Por supuesto, la llamada se realiza solo si se ha asignado el evento correctamente. El metodo D o C h a n g e (declarado como metodo dinamico, como es tradicional en el caso de 10s metodos de lanzamiento de eventos) realiza la comprobacion y la llamada a1 metodo:
p r o c e d u r e TDate.DoChange; begin i f A s s i g n e d (FOnChange) t h e n FOnChange ( S e l f ) ; 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:
p r o c e d u r e TDate SetValue (y, m, d: Integer) ; begin fDate : = EncodeDate (y, m, d); // a c t i v a e l e v e n t o DoChange ;
Si prestamos atencion a1 programa que utiliza esta clase, podemos simplificar en gran medida su codigo. En primer lugar, aiiadimos un metodo personalizado a la clase del formulario:
type TDateForm
=
class (TForm)
...
p r o c e d u r e DateChange (Sender: TObj ect) ;
El codigo del metodo simplemente actualiza la etiqueta con el valor actual de la propiedad T e x t del objeto T D a t e :
p r o c e d u r e TDateForm.DateChange; begin LabelDate.Caption : = TheDay.Text; end :
Parece mucho trabajo, per0 es cierto que el controlador de eventos ahorra bastante programacion. Despues de haber aiiadido algo de codigo, nos podemos olvidar de actualizar la etiqueta cuando se cambien 10s datos de algun objeto. Veamos como ejemplo el controlador del evento O n C l i c k de uno de 10s botones:
p r o c e d u r e TDateForm.BtnIncreaseClick(Sender: begin TheDay.Increase; end; TObject);
El mismo codigo simplificado esta presente en muchos otros controladores de eventos. Cuando hayamos instalado el controlador de eventos, no tendremos que recordar actualizar la etiqueta continuamente. Eso elimina una potencial e importante fuente de errores en el programa. Ademas, fijese en que tenemos que escribir algun codigo a1 principio, porque esto no es un componente instalado en Delphi,
sino sencillamente una clase. Con un componente, simplemente seleccionamos el controlador de eventos en el Object Inspector y escribimos una unica linea de codigo para actualizar la etiqueta, eso es todo.
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
Cuando se extraen 10s elementos de la lista, hay que convertirlos de nuevo a1 tip0 apropiado, como en el siguiente metodo, que esta conectado a1 boton List (se pueden ver sus efectos en la figura 4.4):
p r o c e d u r e TForml.ButtonListDateClick(Sender: TObject); var I: Integer; begin ListBoxl.Clear; f o r I : = 0 to ListDate.Count - 1 d o Listboxl Items .Add ( (TObject (ListDate [I]) a s TDate) .Text) ; end;
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.
hcraposible la exidencia de nada sin6 o m a enen la h t a , la extracci'h mediante una conversion esthtica en lugar de median- . te la conversiiin 4s rqultaria mas eficaz. Sin embargo, cuando existe una Wsibilidad siquie& rsrmotir de tener un objeto errbneo, es mcjor utilizar as.
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
aiiadiendo el concept0 de posesion y definiendo normas de estraccion especificas (que imitan pilas y colas) o capacidades de ordenacion. La diferencia basica entre T L i s t y la nueva clase TOb je c t L i s t, por ejemplo, es que la ultima se define como una lista de objetos TOb j ec t , no como una lista de punteros. Sin embargo, es incluso mas importante el hccho de que si la lista de objetos tiene la propiedad O w n s O b j e c t s definida como True, automaticamente se elimina un objeto a1 reemplazarlo por otro y se elimina cada objeto cuando se destruye la propia lista. Veamos una lista de todas las clases de contenedores: L a clase TObjectList: Representa una lista de objetos, que en ultimo termino son poseidos por la propia lista. La clase heredada TComponentList: Representa una lista de componentes, con total soporte para la notification de la destruccion (una importante caracteristica de seguridad cuando 10s componentes estan conectados mediante sus propiedades, es decir, cuando un componente es el valor de una propiedad de otro). L a clase TClassList: Es una lista de referencias de clase. Hereda de T L i s t y no necesita destruirse de manera especifica, ya que en Delphi no es necesario destruir las referencias dc clase. Las clases TStack y TObjectStack: Representan listas de punteros y objetos, a partir de 10s cuales se pueden extraer unicamente elementos comenzando desde el ultimo insertado. Una pila sigue cl orden LIFO (Last In, F ~ r s Out; ultimo en entrar, primero en salir). Los metodos mas comul nes de una pila son p u s h para la insercion, Pop para la extraccion y Peek para obtener una vista previa del primer elemento sin eliminarlo de la pila. Aun se pueden usar todos 10s metodos de la clase basica, TList . Las clases TQueue y TObjectQueue: Representan listas de punteros y objetos, de 10s que siempre se puede eliminar el primer elemento insertado (FIFO: First In, First Ottt, primero en entrar, primero en salir). Los metodos de estas clases son 10s mismos que 10s de las clases de pila per0 se comportan de un modo distinto.
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 destruir i n an1lpll-a qbjetos 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.
'UY
Para demostrar el uso de estas clases, hemos modificado el anterior ejemplo ListDate para formar uno nuevo, Contain. Primero, cambiamos el tipo de varia-
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
:=
En este punto, podemos simplificar el codigo de destruccion, puesto que a1 aplicar Free a la lista, se liberaran automaticamente las fechas que mantiene. Tambien hemos aiiadido a1 programa un objeto pila y una cola, rellenando cada uno de ellos con numeros. Uno de 10s dos botones del formulario muestra una lista de numeros existentes en cada contenedor y el otro elimina el ultimo elemento (que aparece en un cuadro de mensaje):
procedure TForml.btnQueueClick(Sender: TObject); var I: Integer; begin ListBoxl.Clear; for I : = 0 to Stack-Count - 1 do begin ListBoxl. Items .Add (IntToStr (Integer (Queue.Peek) ) Queue. Push (Queue.Pop) ; end; ShowMessage ( ' E l i m i n a d o : ' + IntToStr (Integer (Stack. Pop) ) ) ; end;
) ;
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.
diferentes en la matriz. De hecho, cuando muchos elementos pueden estar en el mismo cubo, la busqueda se ralentiza. Por esa razon, cuando se crea Tob j ectBuc ketlist , se puede especificar el numero de entradas de la lista, usando el parametro del constructor, eligiendo un valor entre 2 y 256. El valor del cubo se fija tomando el primer byte del puntero (o numero) pasado como clave y haciendo una operacion a n d con un numero que corresponda a las entradas. . - NOTA: Este algoritmo no resulta muy convincente como sistema de verification, pero sustituirlo por uno propio implica sobrescribir la funcion virI tual ~ u c k e For y, en-ultimo caso, cambiar el ntimero de entradas dle la t marnz, esrameclenao un valor alrerenre para la propleaaa ~ u c ~ e i x o u n t .
- -
~--A.~'
..*-S
l . - ? ~
3.4-
r -
-I .
Otra caracteristica interesante. no disponible en el caso de las listas es el metodo ForEach, que permite ejecutar una funcion dada en cada elemento que contenga la lista. Se pasa a1 mdtodo ForEach un puntero a datos propios y un procedimiento, que recibe cuatro parametros: el puntero personalizado, cada clave y ob-ieto de la lista y un parametro boolcano que se puede establecer como False para detener la ejecucion. En otras palabras, estas son las dos definiciones:
type TBucketProc = procedure (AInfo, AItem, AData: Pointer; out AContinue: Boolean) ; function TCustomBucketList.ForEach(AProc: AInfo: Pointer) : Boolean; TBucketProc;
NOTA: Ademas de estos contenedores, Delphi incluye tambien una clase THashedStringList, que hereda de TStringList. Esta clase no tiene una relacion directa con las listas de verificacion e incluso esth defmida en una unidad diferente, I n i F i l e s . La lista de cadena verificada tiene dos tablas asociadas de verificacion (de tipo T S t r i ng Ha sh, que se re) . - . . ..
na. Asi, esta clase solo resulta litil para leer un gran grupo de cadenas fijas, no para manejar una lista de cadenas que cambien con frecuencia. Por otra
--.A-
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
-
AP
rrnn e n n v ~ r c i A n - t i n n c c p n e i l l a A
nnrnrre nn
hrrv
uarantirrc
Ae
que se llame a 10s mktodos descendientes. Se podria acceder a la lista y manipularla utilizando tanto 10s metodos ascendientes como 10s descendientes, por lo que sus operaciones reales han de ser identicas. La 6nica J : f ----- :--- -1 2 - -- a _ --L&- J - - 2 ----- 3:--*-- . . airerencra es er A:- - --*:a:-upo urruzaao en 10s mwwos aeswnolenres, que-permlre
---:A-
evitar una conversion de tipos adicional. Crear una clase con un nombre nuevo que contenga un objeto T L i s t y proyectar 10s metodos de la nueva clase a la lista interna utilizando la verificacion de tipos adecuada. Esta tecnica define una clase envoltorio, una clase que "envuelve" a otra ya existente para ofrecer un acceso diferente o limitado a sus metodos (en este caso, para realizar una conversion de tipo). Hemos implementado ambas soluciones en el ejemplo D a t e L i s t, que define listas de objetos T D a t e . En el codigo siguiente, veremos la declaracion de dos clases, la clase T D a t e L i s t I basada en la herencia y la clase envoltorio
TDateListW.
type
// 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;
Obviamente, la primera es mas sencilla de escribir (tiene menos metodos y simplemente llaman a 10s heredados). Lo bueno es que un objeto T D a t e L i s t I se puede pasar a parametros que esperan una T L i s t . El problema es que el codigo que manipula una instancia de esta lista mediante una variable T L i s t generica no llama a 10s metodos especializados, puesto que no son virtuales y podria acabar aiiadiendo a la lista objetos de otros tipos de datos. En cambio, si decidimos no usar la herencia, escribimos una gran cantidad de codigo, porque hay que reproducir cada uno de 10s metodos originales de T L i s t , llamando sencillamente a1 objeto interno F L i s t . El inconveniente es que la clase T Da t e L is t W no es un tipo compatible con T 1i s t y esto restringe su utilidad. No se puede pasar como parametro a metodos que esperen una TL i s t . Ambos enfoques ofrecen una buena verification de tipos. Tras haber creado una instancia de una de estas clases de lista, solo se pueden aiiadir objetos del tipo apropiado y 10s objetos extraidos seran naturalmente del tip0 correcto. Esto se demuestra con el ejemplo DateList. Este programa tiene unos pocos botones, un cuadro combinado que permite escoger a1 usuario cual de las listas mostrar y un cuadro de lista para mostrar 10s valores reales de la lista. El programa trata de aiiadir un boton a la lista de objetos T D a t e . Para aiiadir un objeto de un tipo diferente a la lista T D a t e L i s t I , podemos simplemente convertir la lista a su clase basica, T L i s t . Esto podria ocurrir de forma accidental si pasamos la lista
como parametro a un metodo que espera un objeto de clase basica. En cambio, para que la lista TDateLi stW falle hemos de convertir explicitamente el objeto a TDate antes de insertarlo, algo que un programador nunca deberia hacer:
p r o c e d u r e TForml.ButtonAddButtonClick(Sender: begin ListW.Add (TDate (TButton.Create (nil)) ) ; TList (Listl) .Add (TButton.Create (nil)) ; UpdateList; 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 ;
El uso basico de un stream implica llamadas a 10s metodos ReadBuf fer y WriteBuf fer,que son muy potentes per0 no muy faciles de usar. El primer
parametro, de hecho, es un buffer sin tipo en el que se puede pasar la variable para guardar o cargar en el. Por ejemplo, se puede guardar en un archivo un numero (en formato binario) y una cadena mediante este codigo:
var
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.
TResourceStream: Define un stream que manipula una secuencia de bytes en memoria y ofrece acceso de solo lectura a datos de recurso enlazados con el archivo ejecutable de una aplicacion (un ejemplo de dichos datos de recursos son 10s archivos DFM). Hcrcda de TCust omMemorySt ream. Las clases de stream definidas en otras unidades son entre otras: TBlobStream: Define un stream que ofrece acceso simple a campos BLOB de bases dc datos. Esisten strcams BLOB similares para otras tecnologias de acccso a bases de datos a partc de BDE, como TSQLBlobStream y TClientBlobStream. Fijcsc en que cada tipo de conjunto de datos utiliza una clase de stream cspccifica para 10s campos BLOB). Todas cstas clases hcrcdan de TMemorySt ream. TOleStream: Dcfinc un stream para lecr y cscribir informacion sobrc la intcrfaz para el strcaming proporcionada por un objeto OLE. TWinSocketStream: Ofrecc soportc de streaming para una conesion socket.
TFileStream;
begin
if 0penDialogl.Execute then
begin
S : = TFileStream.Create fmOpenRead) ;
try
(OpenDialogl.FileName,
(S) ;
S . Free;
end; end ; end;
Como se puede ver en el codigo, el metodo create para streams de archivo tiene dos parametros: el nombre del archivo y un atributo que indica el mod0 de acceso solicitado. En este caso, queremos leer el archivo, por eso utilizamos el indicador fmOpenRead (en la ayuda de Delphi esiste documentacion sobre otros indicadores muy utiles).
NOTA: De 10s diferentes modos, 10s mas importantes son fmshareDenywrite, que se usa cuando simplemente se leen datos de un archivo
un aiobirvd -&mpartido. Existe un ter& par&m,e&o en ~ ~ i i e s t r e a r n . C r e a t e . IM$!do Rights. Este p a r h e t r o se utiliza &a pkrmjsos dei a c e s o a archiws a1 sistema de archivos de Linux c w d o el modo de &cesocs fnicreate(es decir, s61o cuando se crea un arChiVUpzle~~). En
mai'
Windows se ignora este paritmetro. Una gran ventaja de 10s streams sobre otras tecnicas de acceso a archivos es que son intercambiables, por lo que podemos trabajar con streams de memoria y guardarlos despues en un archivo o realizar las operaciones opuestas. Esta podria ser una forma dc mejorar la velocidad de un programa que haga un uso intensivo de archivos. Veamos un fragment0 de codigo, una funcion que copia un archivo, para que hacernos una idea de como se pueden usar 10s streams:
procedure CopyFile (SourceName, TargetName : String) ; var Streaml, Stream2: TFileStream; begin Streaml : = T F i l e S t r e a m - C r e a t e (SourceName, fmOpenRead) ; try Stream2 : = T F i l e S t r e a m - C r e a t e (TargetName, fmOpenWrite or fmcreate) ; try Stream2 .CopyFrom (Streaml, Streaml.Size) ; finally Stream2. Free; end finally Streaml. Free; end end ;
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.
G r de streaming de Delphi7 a tma ni: e e~ l n c d e bk & i . e s ~ e p: & ," e ' h $ s ~ t r e a m ~ r r o rSu constructor to& L . & ~ o c a guu-hwro .. -, . . , , , , .. ,, , , , ,,ase estandariza y un nor.,- , , , ,, ,
, ,
----
m b a116 de la escritura y lectura de bloques de datos. Si queremos cargar o guardar tipos de datos especificos en un stream (sin realizar una conversion de tipos muy exhaustiva), se pueden usar las clases T R e a d e r y TWr i t e r , que derivan de la clase generica T F i l e r . Basicamente, las clases T R e a d e r y T r i t e r existen para simplificar las W tareas de cargar y guardar datos de stream segun su tipo y no solo como secuencia de bytes. Para ello, T W r i t e r incluye marcas especiales (signatures) en el stream, que especifican el tip0 de cada uno de 10s datos del objeto. A su vez, la clase T R e a d e r lee estas marcas del stream, crea 10s objetos adecuados e inicia despues esos objetos utilizando 10s datos que se encuentran a continuacion en el stream. Por ejemplo, se podria haber escrito un numero y una cadena en un stream del siguiente modo:
var
stream: TStream; n: integer; str: string; w: TWriter; begin n : = 10; str : = 'cadena de prueba'; stream : = TFileStream.Create ( 'c: \trrp\test. txt' , fmcreate) ; w : = TWriter .Create (stream, 1024) ; w-WriteInteger (n); w-WriteString (str); w. Free; stream.Free;
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:
Si examinamos la estructura de un archivo DFM de Delphi, descubriremos que, en realidad, se trata simplemente de un archivo de recursos que contiene un recurso de formato personalizado. Dentro de dicho recurso, se encontrara la in-
formacion sobre el componente para el formulario o modulo de datos y para cada uno de 10s componentes que contiene. Como seria de esperar, las clases stream ofrecen dos metodos para leer y escribir estos datos de recurso personalizado para componentes: WriteComponentRes para almacenar 10s datos y ReadComponent Re s para cargarlos. Sin embargo, para experimentar en memoria, (no con archivos DFM reales), normalmente es mejor usar Writecomponent. Despues de crear un stream de memoria y guardar el formulario actual en el, el problema esta en como mostrarlo. Esto se puede conseguir transformando la representacion binaria del formulario en una representacion textual. Aunque el IDE de Delphi, desde la version 5, puede guardar archivos DFM en formato de texto, la representacion utilizada internamente para el codigo compilado es siempre un formato binario. La conversion del formulario se puede realizar mediante el IDE, normalmente con l a o r d e n v i e w as Text del Form Designer, ymedianteotros metodos. Tambien hay una herramienta de linea de comandos, CONVERT. EXE, que se encuentra en el directorio de Delphi, Bin. En nuestro codigo, la forma estandar para obtener una conversion es llamar a 10s metodos especificos de la VCL. Existen cuatro funciones para convertir a1 formato de objeto interno obtenido con el metodo Writecomponent y d l otro formato a este:
p r o c e d u r e ObjectBinaryToText(Input, Output: TStream); overload; p r o c e d u r e ObjectBinaryToText(Input, Output: TStream; v a r OriginalFormat: TStreamOriginalFormat); overload; p r o c e d u r e ObjectTextToBinary(Input, Output: TStream); overload; p r o c e d u r e ObjectTextToBinary(Input, Output: TStream; v a r OriginalFormat: TStreamOriginalFormat); overload;
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; TObject);
try
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 i Ex n
TOP- 113 W d h - 545 Hc,~. 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
Scret3rus = r N c l m a l TabClldcr = 0
Kl~enl
Figura 4.5. Descripcion textual de un cornponente forrnulario, rnostrada dentro de si mismo por el ejernplo ForrnToText.
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.
-. - -
--.
- -
-~
llamente 10s dos mdtodos de lectura y escritura y tiene m a propiedad que almacena un clave:
type TEncodedStream = class (TFileStream) private FKey: Char; public constructor Create (conat FileName: string; Mode: Word) ; function Read (var Buffer; Count : Longint) : Longint ; override: function Write(con6t Buffer; Count: Longint): Longint; override; property Key: Char read FKey write FKey; end ;
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 P& I.
-bow&
a n ~ ~ e ~ o - n & i i eni el primer campo de i l 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):
-.
procedure TFormEncode.BtnSaveEncodedClick(Sender: TObject); var EncStr: TEncodedStream: begin if SaveDialog1.Execute then begin EncStr := TEncodedStream.Create(SaveDialogl.Filename, fmcreate) ; try Memol.Lines.SaveToStream (EncStr); finally EncStr.Free; end; end ; end ;
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 ) ;
Como se puede ver en 10s comentarios del codigo, la operacion de descompresion resulta ligeramente mas compleja ya que no se puede utilizar el metodo CopyFrom: se desconoce el tamaiio del stream resultante. Si se pasa 0 a este metodo, intentara obtener el tamaiio del stream de entrada, que es un TDecompressionstream. Sin embargo, esta operacion causa una excepcion, ya que 10s streams de compresion y descompresion solo pueden leerse desde el principio hasta el final y no permiten buscar el final del archivo.
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.
Todas las clases de streams mencionadas en la seccion anterior. Tambien estan las clases TFiler,TReader y TWriter y una clase TParser utilizada internamente para el analisis sintactico DFM. Clases de utilidades, como TBits para la manipulacion binaria y unas cuantas rutinas de utilidad (por ejemplo, constructores de punto y rectangulo y rutinas de manipulacion de listas de cadenas como Linest art y Extract Str ings). Tambien hay c l a m de registro para notificar a1 sistema la esistencia de componentes, clases, funciones de utilidad especial que se pueden sustituir y muchas mas. La clase TDataModule, una sencilla alternativa a un formulario como contenedor de objetos. Los modulos de datos solo pueden contener componentes no visuales y; por lo general, se usan en bases de datos y en aplicaciones Web. nes anteriores de Delphi, la clase TDa taModule se Forms; desde Delphi 6 se ha desplazado a la unidad .,,,,,., J .e este cambio era eliminar el encabezamiento de cbdigo de las clases GUI de las aplicaciones no visuales (por ejemplo, 10s modulos de servidor Web) y para separar mejor el cirdigo de Windows no transportable de las clases independientes del SO, como TDataModule. Otros cambios esthn relacionados con 10s mbdulos de datos, por ejemplo, para permitir la creacion de aplicaciones Web con diversos m6dulos de datos .
-.
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.
do para pares nombre-valor en la clase TStr ingList, existen unas cuantas AncestorIsValid e funciones globales nuevas, IsDefaultPropertyValue. Ambas funciones se introdujeron para soportar el subrayado de propiedades no predefinidas en el Object Inspector. Sirven para poco mas, y lo mas normal sera no beneficiarse de su uso a no ser que se este interesado en guardar el estado de un componente y un formulario, y escribir un mecanismo de streaming personalizado.
Controles visuales
Ahora que hemos presentado el entorno de Delphi y hemos visto de manera global el lenguaje Delphi y 10s elementos basicos de la biblioteca de componentes, ya estamos preparados para profundizar en el uso de 10s componentes y el desarrollo de las interfaces de usuario de aplicaciones. Esto es realmente de lo que trata Delphi. La programacion visual mediante componentes es una caracteristica clave de este entorno de desarrollo. Delphi incluye una gran cantidad de componentes listos para usar. No vamos a describir cada componente en detalle, con sus propiedades y metodos; si se necesita esta informacion se puede encontrar en el sistema de ayuda. La intencion de este capitulo y 10s siguientes es mostrar como usar algunas de las caracteristicas avanzadas que ofrecen 10s componentes predefinidos de Delphi para construir aplicaciones y comentar tecnicas especificas de programacion. Para empezar compararemos la biblioteca VCL y la VisualCLX y analizaremos las clases basicas (en particular TControl). Despues examinaremos 10s diversos componentes visuales, ya que escoger 10s controles basicos correctos ayxdara a realizar el proyecto mas rapidamente. Este capitulo trata 10s siguientes temas : VCL frente a VisualCLX.
Vision global de 10s componentes estandar Construccion basica y avanzada de menus. Modificacion del menu del sistema. Graficos en menus y cuadros de lista. Estilos y dibujos por el propietario.
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 t ia clase Q con el sufijo fnlde 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 &
cion importante son 10s constructores de clase, que se transforman en fi ciones que devuelven la nueva instancia de la clase.
1T--.
---a
2 ---3- ---- 3nay que uarse cuenra ae que e - oollgaiorw el ---- oe a menos una ae las s - L l : - - ~ - 2 - - 1 uso 2 - -1 1
clases de la capa de proyeccion para la licencia Qt que se incluye con Delphi (y Kylix). Qt es gratuita para el us0 no comercial bajo X Window (se llama Qt Free Edition), pero se debe pagar una licencia a Trolltech para desarrollar aplicaciones comerciales. Cuando se compra Delphi, la licencia Qt ya la ha pagado Borland, pero se debe usar Qt bisicamente a traves de la biblioteca CLX (aunque se permitan llamadas a Qt de bajo nivel dentro de una aplicacion CLX). No se puede usar directamente la unidad Qt.pas y evitar la inclusion de la unidad QForms (que es obligatoria). Borland obliga a esta limitation a1 omitir de la interfaz Qt 10s constructores QFormH y QApplicationH. m En la mavor ~ -a-r~ de estos m o m-~ a s en Debhi sblo usaremos obietos v te , r -.- - - - ..-- . - - - - r mttodos CLX. Es importante saber que si se necesita se pueden utilizar directamente algunas caracteristicas adicionales de Qt; o p&de ser necesario realizar llamadas de bajo nivel para solucionar fallos de CLX.
~ -
-0
- -
aplicacion seleccionada (vease la figura 5.1 para obtcner una comparacion). No se pucde colocar un boton VCL en un formulario CLX; ni se pueden mezclar formularios de bibliotecas en un mismo archivo ejecutablc. En otras palabras. la interfaz de usuario de una aplicacion debe crearse de manera exclusiva con una de las dos bibliotecas, lo que tiene mucho scntido.
a d,a~FcT~&
~
InloBsrt I Weffiavices i ~
lnteld& ~
b
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.
Las dos clases T B u t t o n tienen el mismo nombre y esto es posible debido a t que se guardan en dos unidades diferentes, denominadas s t d ~ r 1 s y Q S t d C t r l s . Por supuesto, no podemos tener 10s dos componentes disponibles en tiempo de diseiio en la paleta, porque el IDE de Delphi solo puede registrar
componentes con nombres unicos. Toda la biblioteca VisualCLX esta definida mediante unidades que se corresponden a las unidades de la VCL, pero con la letra Q como prefijo (de ahi que esista una unidad QForms, una unidad QDialogs, una unidad QGraphics, etc.). Algunas unidades particulares como QStyle no tiencn una unidad correspondiente en la VCL porque se proyectan sobre caracteristicas de Qt que no tienen que ver con la API de Windows. Fijese en que no hay configuraciones del compilador ni otras tecnicas ocultas para distinguir entre las dos bibliotecas. Los que importa es el con.junto de unidades a las que sc hace refcrencia en el codigo. Recuerde que estas referencias habran de resultar coherentes, puesto que no se pueden mezclar controles visuales de las dos bibliotecas en un unico formulario ni tampoco en un unico programa.
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 object Forml: TForml Left = 192 Top = 107 Width = 350 Height = 210 Caption = ' L i b C o n p ' Color = clBtnFace Font.Charset = DEFAULT-CHARSET Font-Color = clWindowText Font.Height = -11 Font.Name = 'MS S a n s S e r i f ' Font.Style = [ I TextHeight = 13 Oldcreateorder = False 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 ItemHeight = 13 1tems.Strings = (
' m r c o'
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 '
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;
El codigo de la clase y del unico controlador de eventos es absolutamente identico. Por supuesto, la clasica directiva del compilador { S R * . dfm) se sustituye por { $ R * .xfm) en la version CLX del programa.
Para efectuar la seleccion, debemos evaluar ciertos criterios. El primer criterio es la capacidad de transporte. Si nos preocupa el hecho de ejecutar un programa en Windows y en Linux con la misma interfaz de usuario, usando CLX probablemente podremos conseguir que las cosas resulten mas sencillas y permitira mantener un archivo de codigo fuente unica con muy pocas I FDEF.Se puede aplicar lo mismo, en el caso de considerar que Linux es (o posiblemente se transformara) en nuestra plataforma clave. En cambio, si la mayoria de 10s usuarios que utilizan nuestro programa son usuarios de Windows y simplemente queremos ampliar la oferta con una version para Linux, se puede mantener el sistema dual VCLJCLX. Esto implica probablemente dos conjuntos diferentes de archivos de codigo fuente o demasiadas IFDEF. Otro criterio es el de la apariencia originaria. A1 utilizar CLX en Windows, algunos de 10s controles se comportaran de un mod0 ligeramente diferente a1 que 10s usuarios esperarian (a1 menos 10s usuarios expertos). En el caso de una interfaz de usuario simple (ediciones, botones, cuadriculas), eso no importara demasiado, per0 si hay muchos controles de vista en arb01 y vista en lista, las diferencias se haran patentes. Por otra parte, con CLX se puede dejar que 10s usuarios seleccionen una apariencia a su gusto, diferente de la apariencia basica de Windows y que la utilicen de manera consistente en las plataformas. Esto significa que un aficionado a Motif sera capaz de escoger este estilo cuando se le obligue a usar la platafonna Windows. Mientras que esta flexibilidad resulta habitual en Linux, es extrafio usar una apariencia no nativa en Windows. Usar controles originarios implica ademas que desde el momento en que se consigue una nueva version del sistema operativo Windows, la aplicacion se adaptara (probablemente) a ella.Esto resulta muy ventajoso para el usuario, per0 podria originar un monton de quebraderos de cabeza en caso de incompatibilidades. Las diferencias en la biblioteca de controles comunes de Microsoft durante 10s ultimos aiios ha sido una gran fuente de frustracion para 10s programadores de Windows en general, incluidos 10s de Delphi. Otro criterio es el despliegue: si se utiliza CLX, habra que incluir en el programa de Windows y Linux las bibliotecas Qt. Segun diversas pruebas, la velocidad de las aplicaciones VCL y CLX es similar. Para comprobarlo, se pueden usar las aplicaciones de ejemplo L i b s p e e d y Q L i b S p e e d , en las que se crean 1000 componentes y se muestran en pantalla. Otro criterio importante para decidir usar CLX en lugar de VCL es la necesidad del soporte de Unicode. CLX tiene soporte Unicode en sus controles de manera predefinida (incluso en plataformas Win9x en que no lo soporta Microsoft). Sin embargo, la VCL tiene muy poco soporte de Unicode incluso en las versiones de Windows que lo ofrecen, lo que hace dificil construir aplicaciones VCL para paises en que el conjunto local de caracteres se gestiona mejor cuando se basa en Unicode.
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 r b de busqueda. De u este modo. ias referkcias a unidades VCIL W e s que queden causariin an error "Unitnot found"' (Unidad no encontrada) .
rl;4Lran+a hain r l u 14h u u w l w u r u vw,v
) 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 Buttons Clipbrd ComCtrls Consts Controls Dialogs ExtCtrls Forms Graphics Grids
QActn List QButtons QClipbrd QComCtrls QConsts QControls QDialogs QExtCtrls QForms QGraphics QGrids
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
Los controles basados en ventanas: Son componentes visuales basados en una ventana del sistema operativo. Un TWinCont r o 1 en una VCL tiene un manejador de ventana, un numero que se refiere a una estructura interna de Windows. Un T W i d g e t C o n t r o 1 en CLX tiene un manejador Qt, una referencia a1 objeto interno Qt. Desde el punto de vista del usuario, 10s controles basados en una ventana pueden recibir el foco de entrada y algunos pueden contener otros controles. Este es el mayor grupo de componentes de la biblioteca de Delphi. Podemos dividir 10s controles basados en una ventana en dos grupos mas: envoltorios de controles originarios de Windows o Qt y controles personalizados, que normalmente heredan de
TCustomControl.
Los controles graficos: Son componentes visuales que no se basan en una ventana del sistema operativo. Por lo tanto, no tienen manejador, no pueden recibir el foco y no pueden contener otros controles. Estos controles heredan de T G r a p h i c C o n t r o 1y 10s pinta su formulario padre, que les envia eventos relacionados con el raton y de otros tipos. Como ejemplos de controles no basados en ventanas estan L a b e l y S p e e d B u t t o n . Existe una serie de controles en este grupo, que resultaban decisivos para minimizar el uso de 10s recursos del sistema en las primeras versiones de Delphi (en Windows de 16 bits). Usar controles graficos para ahorrar recursos de Windows sigue siendo util en Win9x/Me, que ha elevado aun mas 10s limites del sistema per0 no ha conseguido librarse aun de ellos (no como Windows NTl2000).
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.
Una caracteristica importante de la posicion de un componente es que, como cualquier otra coordenada, siempre se relaciona con la zona de cliente de su componente padre (indicada por su propiedad P a r e n t ) . En el caso de un formulario, la zona del cliente es la superficie incluida dentro de sus bordes y la etiqueta (exceptuando 10s propios bordes). Hubiera sido un poco confuso trabajar con las coordenadas de la pantalla, aunque existen algunos metodos preparados para su uso que convierten las coordenadas entre el formulario y la pantalla, y viceversa. Sin embargo, fijese en que las coordenadas de un control siempre son relativas a1 control padre, como un formulario u otro componente contenedor. Si se coloca un panel en un formulario y un boton en un panel, las coordenadas del boton son relativas a1 panel y no a1 formulario que contiene el panel. En este caso, el componente padre del boton es el panel.
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 ~ ~ 0 0verde, el valor SOOOOOOFF a1 rojo, el valor $ 0 0 0 0 0 0 0 0 a1 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:
El byte m b significative del tipo T C o l o r se utiliza para indicar en que paleta deberia buscarse el color correspondiente mas proximo, per0 no hablaremos aqui sobre las paletas. (Los sofisticados programas de tratamiento de imagenes usan tambien este byte para llevar la informacion sobre transparencia de cada elemento que aparece en pantalla.) En cuanto a las paletas y la correspondencia de color, fijese en que Windows sustituye a veces un color arbitrario por el color solido mas proximo, a1 menos en 10s modos de video que usan una paleta. Esto siempre ocurre en el caso de fuentes, lineas, etc., En otras ocasiones, Windows usa una tecnica de punteado para
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&
pKogrr;maddreesmpertos pug& no* q4e en CLX hqy un utibadi3 can *a hecdrencia, EventHandler, que se cmres-
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
i f not begin
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.
El componente MaskEdit
Para personalizar aun mas la entrada de un cuadro de edicion, se puede utilizar el componente Mas kEdit.Tiene una propiedad EditMask que es una cadena que indica para cada caracter si deberia ser una mayhcula, minuscula o un numero y otras condiciones similares. El editor Input Mask permite introducir una mascara, per0 tambien nos pide que indiquemos un caracter que reserve el sitio para la entrada y decidir si se guarda el material presente en la mascara junto con la cadena final. Por ejemplo, se puede escoger mostrar el prefijo de zona del numero de telefono entre parentesis solo como una entrada de sugerencia o guardar 10s parentesis con la cadena que almacena el numero resultante. Estas dos entradas en el editor'lnput Mask
corresponden a 10s dos ultimos campos de la mascara (separados por puntos y coma). Se puede ver el editor de la propiedad EditMask a continuacion:
ILL.kid*...
I T (
06/27/94 09 05 15PM
Hdp
MEW cfBt Inpd Mask ~ditnr. de entrada predefbidas p&3 difkrentes paises.
Test Html
Test text with bold
Figura 5.3. El ejemplo HtmlEdit en tiempo de ejecucion: cuando se aiiade nuevo texto HTML al campo de memo, se puede previsualizar inmediatamente.
Linux. Para daptar este ejcrnplo g Windows y Delphi, sblo es necesarid . copiar Iw archivos y v o h r a compilar.
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-
dro de grupo con algunos clones de botones de radio en su interior. La diferencia es que estos botones de radio internos se gestionan automaticamente desde el control contenedor. Utilizar un grupo de radio es, por lo general, mas sencillo que utilizar el cuadro de grupo, puesto que 10s diversos elementos forman parte de una lista, como en un cuadro de lista. Asi es como se puede obtener el texto del elemento seleccionado:
Text : = RadioGroupl.Items [RadioGroupl.ItemIndex];
Otra ventaja es que un componente R a d i o G r o u p puede alinear automaticamente 10s botones de radio en una o mas columnas (corno indica la propiedad columns) y se pueden aiiadir facilmente nuevas opciones en tiempo de ejecucion, aiiadiendo cadenas a la lista de cadenas 1tems. Sin embargo, aiiadir nuevos botones de radio a un cuadro de grupo resulta bastante complejo.
Cuando hay muchas selecciones, 10s botones de radio no resultan apropiados. El numero de botones de radio mas habitual es inferior a cinco o seis, para no abarrotar la interfaz de usuario. Cuando tenemos mas opciones, podemos usar un cuadro de lista o uno de 10s otros controles que muestran listas de elementos y permiten la seleccion de uno de ellos.
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;
Otra caracteristica importante es que a1 utilizar el componente ListBox, se puede escoger entre permitir solo una seleccion, como en un grupo de botones de radio, y permitir selecciones multiples, como en un grupo de casillas de verificacion. Esta eleccion la hacemos especificando el valor de la propiedad ~ u l iselect.Existen dos tipos de selecciones multiples en cuadros de lista en t Windows y en Delphi: seleccion multiple y seleccion ampliada. En el primer caso,
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 : =
' I ;
1 do
SelItems : = SelItems
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 :
El estilo csDropDown: Define un cuadro combinado tipico, que permite editar directamente y mostrar un cuadro de lista mediante solicitud. El estilo csDropDownList: Define un cuadro combinado que no permite editar (pero en el que se pueden pulsar ciertas teclas para seleccionar un elemento). El estilo cssimple: Define un cuadro combinado que siempre muestra el cuadro de lista bajo el.
Fijese en que acceder a1 texto del valor seleccionado de un cuadro combinado es mas sencillo que hacer la misma operacion en el caso de un cuadro de lista,
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 :
TRUCO:En CLX, el cuadro combinado puede aiiadir automaticamente el texto escrito en el cuadro de edicion a la lista desplegable, cuando el usuaria pulsa la tecla Intro. Ademas, algunos eventos ocurren en diferentes ocasiones en la VCL.
Dcsdc Delphi 6, se incluyen dos nuevos eventos para el cuadro combinado. El cvcnto onC lo s eUp corresponde a1 cicrre de la lista desplegable y complemcnta a1 cvento OnDropDown que existia previamente. El evento onseiect solo se lanza cuando el usuario hace una seleccion en la lista desplegable, en lugar de escribir cn la parte de edicion. Otro mcjora es la propiedad AutoComplete. Cuando se fija, el componente ComboBox (y tambicn el componente List Box) busca automaticamente la cadena mas parecida a aquella que el usuario esta escribiendo, sugiriendo la parte final del testo. La parte principal de esta caracteristica, tambidn disponible en CLX, se implements en el mctodo TCustomList Box. KeyPres s.
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
por una casilla de verificacion. Un usuario puede seleccionar un unico elemento de la lista, pero tambien hacer clic sobre las casillas de verificacion para alternar su estado. Esto hace que CheckListBox sea un componente realmente adecuado para las selecciones multiples o para resaltar el estado de una serie de elementos independientes (en forma de una serie de casillas de verificacion).
10s grupos de colores que queremos ver en la lista (colores estandar, colores ampliados, colores de sistema. etc.).
ADVERTENCIA: El control ListView en la CLX no dispone de 10s estilos de icono pequeiiolgrande de su contraparticla en Windows, pero un control
similar, IconView, proporciona esta capacidad.
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.
Los elementos de una lista de selection desplegable (solicitada en ultimo ) termino en el cvento OnGet Pic kList . La aparicion de un boton que muestre un dialogo de edicion (en el evento OnEditButtonClick). No es necesario decir que este comportamiento se parece al que esta generalmente disponible para las cuadriculas de cadena (string grids) y el control DBGrid, y tambien para el comportamiento del Ob j ect Inspector. La propiedad Itemprops habra de establecerse en tiempo de ejecucion, a1 crear un objeto de la clase T I ternprop y asignarlo a un indice o a una clave de la lista de cadena. Para tener un editor predefinido para cada linea, se puede asignar el mismo objeto de propiedad de elemento varias veces. En el ejemplo, este editor compartido configura una mascara de edicion de hasta tres numeros:
procedure TForml.FormCreate(Sender: TObject); var I: Integer;
begin
SharedItemProp : = TItemProp.Create (ValueListEditorl); Shared1temProp.EditMask : = ' 999;O; ' ; SharedItemProp.EditStyle : = esEllipsis; FirstItemProp : = TItemProp-Create (ValueListEditorl); for I : = 0 to 10 do
FirstItemProp.PickListAdd(1ntToStr
(I));
end ;
Se debe repetir un codigo similar en caso de que cambie el numero de lineas, por ejemplo a1 aiiadir nuevos elementos en el campo de memo y copiarlos a la lista de valores.
procedure TForml.ValueListEditorlStringsChange(Sender: TObject); var I: Integer; begin Memol.Lines : = ValueListEditorl.Strings; ValueListEditorl.1temProps [0] : = FirstItemProp; f o r I : = 0 t o ValueListEditorl.Strings .Count - 1 do i f n o t Assigned (ValueListEditorl. Itemprops [I]) then ValueListEditorl.1temProps [I] : = SharedItemProp; end ;
&a h .
Otra propiedad, K e y O p t i o n s , permite a1 usuario editar tambiin las claves (10s nombres), aiiadir nuevas entradas, eliminar las existentes y contar con nombres duplicados en la primera parte de la cadena. Es raro que no se puedan aiiadir nuevas claves a no ser que se activen tambien las opciones de edicion, lo que dificulta permitir que el usuario aiiada entradas adicionales mientras que se mantienen 10s nombres de las entradas basicas.
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
NOTA: En CLX no existe el control UpDown, sino un SpinEdit que as&ia. .ua;Edit con el ~ p ~ o en un "nico control. w n . .. . . .
. , a
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.
-
ADVERTENCIA: No se debe usar la propiedad Break, que se emplea para incorporar un menu desplegable en diversas columnas. El valor mbMenuBarBreak indica que este elernento apareceri en una segunda linea o en las siguientes. El valor mbMenuBrea k indica que este elemento se aiiadira a una segunda columna o a la siguiente del menu desplegable.
Para conseguir un menu de aspect0 mas modcrno, se puede a5adir un control dc lista dc imagenes al programa, que contenga una scrie de mapas de bits, y conectar la lista dc imagenes con el menu mediante su propiedad Images. Se puedc dcfinir una imagen para cada elemento de menu fijando el valor correct0 para su propicdad I m a g e Index . La definicion de imagenes para mcnus es bastante flexiblc (pucdc asociarse una lista de imagenes con cualquier menu desplegable especifico, e incluso con un elemento de menu dado, mediante la propiedad SubMenuImages). A1 disponer de una lista de imagenes mas pequeiia especifica para cada menu dcsplcgablc en lugar de una gran lista de imagenes para todo el menu se permitc una mayor particularization de una aplicacion en tiempo de ejecucion.
TRUCO: Crear elementos de menu en tiempo de ejecucion es algo tan habitual que Delphi ofrece algunas funciones listas para w a r en la unidad Menus. Los nombres de estas funciones globales son autoexpiicativos: NewMenu, NewPopupMenu, NewSubMenu, Newltem y NewLine.
Menus contextuales y el evento OncontextPopup
El componente PopupMenu aparece normalmente cuando el usuario hace clic con el boton derecho del raton sobre un componente que usa el menu contextual dado como el valor de su propiedad PopupMenu. Sin embargo, ademas de conectar el menu contextual a un componente con la propiedad correspondiente, podemos llamar a su metodo Popup,que necesita la posicion del menu contextual en las coordenadas de la pantalla. Pueden obtenerse 10s valores adecuados a1 convertir un punto local en un punto de pantalla con el metodo ClientToScreen del componente local, que en este fragment0 de codigo es una etiqueta:
procedure TForml.Label3MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var ScreenPoint: TPoint; begin // si se c u m p l e la c o n d i c i d n . . . if Button = mbRight then begin ScreenPoint : = Label3. ClientToScreen (Point (X, Y) ) ; PopupMenul.Popup (ScreenPoint.X, ScreenP0int.Y) end; end ;
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
procesamiento normal del menu contextual (corno contextualizar un dialogo o un menu personalizado, como en este caso), se deberia definir Handled como T r u e y el sistema dejara de procesar el mensaje. Deberiamos establecer H a n d l e d como T r u e en contadas ocasiones, dado que, por lo general, controlamos el evento O n c o n t e x t Popup para crear de forma dinamica o personalizar el menu contextual, per0 a continuacion podemos dejar que el controlador predefinido muestre realmente el menu. El controlador de un evento O n c o n t e x t P o p u p no se limita a mostrar un menu contextual, sino que puede realizar cualquier otra operacion, como mostrar directamente un cuadro de dialogo. Este es un ejemplo de una operacion de hacer clic con el boton derecho utilizada para modificar el color del control.
procedure TFormPopup.Label2ContextPopup(Sender: MousePos: TPoint; var Handled: Boolean) ; begin ColorDialogl.Color : = Label2.Color; if ColorDialogl.Execute then Label2.Color : = ColorDialogl.Color; Handled : = True; end :
TObject;
Todos 10s fragmentos de codigo de esta seccion estan disponibles en el ejemplo CustPop para la VCL y QCustPop para la CLX.
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.
OnEnter
ejemplo. Fijese en el caracter 8 de las etiquetas, que indica una tecla de metodo abreviado y en la conesion de dichas etiquetas con 10s cuadros de edicion correspondientes (usando la propiedad F o c u s C o n t r o 1: )
o b j e c t FocusForm: TFocusForm
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
&
El segundo controlador de eventos del formulario se refiere a1 evento OnExi t del primer cuadro de dialogo. Si el control se deja en blanco, se niega a liberar el foco y lo vuelve a recuperar despues de mostrar un mensaje a1 usuario. Los metodos buscan tambien un valor de entrada dado, rellenan automaticamente el segundo cuadro de edicion y desplazan el foco directamente a1 tercero:
procedure TFocusForm. EditFirstNameExit (Sender: TObj ect) ; begin i f EditFirstNarne.Text = " t h e n begin
// 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 '
end
mtError,
[mbOK] , 0) ;
else i f EditFirstName.Text
begin
'Adrnin' then
--
TRUCO: La versi6n CLX de este ejemplo tiene el m i s m &hgo y estA disponible como programa QInFocus.
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.
- ..
--
TRUCO: Los anclajes, a1 igual que las restricciones, funcionan tanto en tiempo de diseiio como en tiempo dt:ejecucion, por lo que deberiamos defi-
mi&trT
Como ejemplo de ambas tecnicas, podemos probar la aplicacion Anchors. que tiene dos botones en la esquina inferior derecha y un cuadro de lista en el centro. Como muestra la figura 5.8,los controles se mueven automaticamente y disminuyen de tamafio a1 mismo tiempo que cambia el tamaiio del formulario. Para que el formulario funcione de forma correcta, debemos definir tambien su propiedad constraints. si no, a medida que el formulario reduzca su tamaiio 10s controles pueden solaparse o dcsaparecer.
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.
Si climinamos todos 10s anclajes o dos opuestos (por ejemplo, izquierdo y derecho). las operaciones de rnodificacion del tamaiio haran que el control flote en el formulario. El control mantienc su tamaiio actual y el sistema afiade o elimina el mismo numero de pixeles a cada uno de sus lados. Esto puede definirse como anclaje centrado, porque si el componente csta en un principio en el medio del formulario, mantcndra esa posicion. En cualquier caso, si deseamos disponer de un control centrado. deberiamos usar por lo general ambos anclajes, de tal mod0 que si el usuario aumenta el tamaiio del formulario, el tamaiio del control aumenta a su vez. en el caso comentado, aumentar el tamafio del formulario hace que el pequeiio control permanezca en el medio.
mas eficaz, el divisor se puede usar combinado con la propiedad constraints de 10s controles a 10s que haga referencia. Como veremos en el ejemplo Splitl, csto permite definir las posiciones maxima y minima del divisor y del formulario. Para crear este ejemplo, hay que colocar sencillamente un componente ListBos en un formulario y, a continuacion, aiiadir un componente Splitter, una segunda ListBox, otro Splitter y por ultimo un tercer componente ListBox. El formulario tiene tambien una barra de herramientas simple basada en un panel. Simplemente a1 colocar estos dos componentes divisores, se proporciona a1 formulario la funcionalidad complcta de mover y modificar el tamaiio dc 10s controles que contiene en tiempo de ejecucion. Las propiedadcs W i d t h , Beveled y Color de 10s componentes divisores definen su apariencia, y en el ejemplo Splitl podemos usar 10s controles de la barra de herramientas para cambiarlos. Otra propiedad relcvante es MinSi ze,que determina el tamaiio minimo de 10s componentes del formulario. Durante la operacion de division (vease la figura 5.9). una linea marca la posicion final del divisor, per0 no podemos arrastrar esta linea mas alla de un cierto limite. El comportamiento del programa Splitl consiste en no permitir que 10s controles sc hagan demasiado pequeiios. Una tecnica alternativa es definir la nueva propiedad Autosnap del divisor como T r u e . Esta propiedad hara que el divisor oculte el control cuando su tamaiio vaya mas alla del
Wh
- ..
Dog cat
zard
11
hr~~np ug
ee
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
formulario. Incluso aunque se f?ie su propiedad MinSize, un usuario puede rcducir el tamaiio de todo el formulario a su minima espresion, ocultando algunos de 10s cuadros dc lista. Si se prueba la version Split2 dcl ejemplo, se comprendera me.jor. En Split2 se nianipula la propiedad Constraints de 10s controles ListBox.
object ListBoxl: TListBox Constraints.MaxHeight = 400 Constraints,MinHeight = 200 Constraints.MinWidth = 1 5 0
Las restricciones dc tamaiio sc aplican solo cuando se modifica el tamaiio dc 10s controles, por eso, para que este programa funcionc de manera satisfactoria, se debe dar a la propiedad Resizestyle de 10s dos divisores el valor rsupdate. Estc valor indica quc la posicion del control se actualizara con cada movimiento del divisor. no solo a1 final de la operacion. Si en su lugar se escoge el valor rsLine o el nucvo valor rspattern, el divisor simplemente dibujara una linca en la posicion solicitada. comprobando la propiedad MinSize per0 no las rcstricciones dc 10s controles.
-
El programa dispone de una barra de estado, que registra la altura actual de 10s dos componentes de memo. Controlamos el evento OnMoved del divisor (el unico evento de este componente), para actualizar el texto de la barra de estado. Este mismo codigo se ejecuta siempre que se adapte el tamaiio del formulario:
procedure TForml.SplitterlMoved(Sender: TObject); begin StatusBarl.Panels [0].Text : = Format ( ' U p p e r memo: %d memo: % d ' , [Memoup-Height, MemoDown.Height1); end;
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:
El color de fondo de la ventana de sugerencia. El tiempo que habra de mantenerse el cursor sobre un componente antes de que aparezcan las sugerencias.
HintHidePause Hintshortpause
EI tiempo durante el que se muestra la sugerencia. El tiempo que habra de esperar el sistema para mostrar una sugerencia, si acaba de mostrar otra distinta.
Un programa, por ejemplo, podria permitir que un usuario personalizase el color de fondo de la sugerencia, seleccionando uno especifico mediante el siguiente codigo:
ColorDialog.Color : = Application.HintColor; if ColorDialog.Execute then Application.HintColor : = ColorDialog.Color;
Como alternativa, podemos cambiar el color de la sugerencia, controlando la propiedad OnShowH i n t del objeto Appl i c a t i o n . Este controlador puede cambiar el color de la sugerencia solo para controles especificos. El evento OnShowHint se usa en el ejemplo CustHint.
lla, aplicando el metodo C l i e n t To S c r e e n a1 propio control. Ademas podemos actualizar el ejemplo CustHint de un mod0 diferente. El control ListBos del formulario tiene algunos de 10s elementos de testo algo largos, asi que se podria desear mostrar el texto completo en una sugerencia mientras que el raton se mueva sobre el elemento. Fijar una unica sugerencia para el cuadro de lista no serviria, por supuesto. Una buena solucion es personalizar el sistema de sugerencias proporcionando una sugerencia de manera dinamica que
se corresponda con el texto del elemento de la lista que se encuentre bajo el cursor. Tambien se necesita indicar al sistema a que area pertenece la sugerencia, para que al mover el cursor sobre la siguiente linea se muestra una nueva sugerencia. Se puede realizar esto fijando el campo C u r s o r R e c t del registro THi n t I nfo,que indica el area del componente sobre la que puede moverse el cursor sin deshabilitar la sugerencia. Cuando el cursor sale de dicha zona, Delphi oculta la ventana de sugerencia. Este es el fragment0 de codigo relacionado que se ha aiiadido al metodo ShowHint:
else i f Hintcontrol = ListBoxl then begin nItem : = ListBoxl.ItemAtPos( Point (CursorPos.X, CursorPos. Y) , True) ; i f nItem >= 0 then begin
// 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
: = ListBoxl. Items [nItem];
: = ListBoxl. ItemRect (nItem);
/ / 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
do, Se puede particul&zar la aprwicda' de una ListView, TreeYiew, ". . TabCmtrol, Pagecontrol, HeaderConW,-swtus~ar I oolaar. LOS cono treks TdBar, ListView y T m V i e w tambih Soportan un mod0 avanzado de dibqjb personalizado, una capacidad de dibujo mas ajustada introducida por Microsoft en las riltimas versibdebde.la.b~b1ioteca controles comudc nes de Win32. El inconveniente de e s t a t h i c a es que a1 cambiar el cstilo de la inter& de usuario de Windows. en ql fbfuro (y sicmpre succde), 10s co~~tr~les dibujados por el propidaria, que cncajan a la perfection en 10s estibs de interfaz de usuario actual&. p a r e c e r h dcsfasados y 'hcra de k ? Como estamos creando una interfae de usuario ~ersonalizada.sera g. - r nece*d o que la actualicemos nosbtros misrnos. Por contraste. si sc usa la I appcaciones se adaptwan a ona aparicmcia e s t h d a r de 10s contmlesr, lw appcac nueva1 v e r s i h de estos controlo9.
".
I . .
.. .
i\
.-
podemos configurar: W i d t h y H e i g h t . En el evento OnDraw I t em, dibujamos la imagen real. Este controlador de eventos se activa cada vez que hay que volver a dibujar el elemento. Esto ocurre cuando Windows muestra por primera vez 10s elementos y cada vez que cambia su estado, por ejemplo, cuando el raton se mueve sobre un elemento, deberia de aparecer resaltado. Para dibujar 10s elementos del menu, tenemos que considerar todas las posibilidades, como dibujar 10s elementos resaltados con colores especificos, dibujar una marca de verificacion si fuese necesario, etc. Por suerte, el evento Delphi pasa a1 controlador el objeto c a n v a s en que deberia pintarse, el rectangulo de salida y el estado del elemento (si esta seleccionado o no). En el ejemplo ODMenu, se controla el color de resaltado, per0 se omiten otros aspectos avanzados (corno las marcas de verificacion). Se ha fijado la propiedad OwnerDraw del menu y se han escrito controladores para algunos de 10s elementos del menu. Para escribir un controlador unico para cada evento de 10s tres elementos del menu relacionados con el color, hemos configurado su propiedad Tag con el valor del color en el controlador de eventos o n c r e a t e del formulario. Esto hace que el controlador del actual evento o n c l i c k de 10s elementos resulte bastante sencillo:
procedure TForml.ColorClick(Sender: TObject); begin ShapeDemo.Brush.Co1or : = (Sender as TComponent).Tag end ;
El controlador del evento O n M e a s u r e I t e m no depende de 10s elementos reales, sin0 que emplea valores fijos (diferentes del controlador del otro menu desplegable). La parte mas importante del codigo esta en 10s controladores de 10s eventos OnDrawItem. Para el color, empleamos el valor de la etiqueta (tag)para dibujar un rectangulo del color dado, como muestra figura 5.11. Sin embargo, antes de hacer esto, hemos de rellenar el fondo de 10s elementos del menu (la zona rectangular que se pasa como un parametro) con el color esthdar para el menu (clMenu) o 10s elementos del menu seleccionados ( c l H i g h l i g h t ) :
procedure TForml.ColorDrawItem(Sender: TObject; ACanvas: TCanvas; ARect: TRect; Selected: Boolean) ; begin // f i j a e l c o l o r d e l fondo y l o p i n t a i f Selected then ACanvas.Brush.Color : = clHighlight else ACanvas.Brush.Co1or : = clMenu; ACanvas. FillRect (ARect); // m u e s t r a e l c o l o r ACanvas.Brush.Color : = (Sender as TComponent).Tag; Inf lateRect (ARect, -5, -5) ; ACanvas.Rectangle (ARect.Left, ARect.Top, ARect.Right, ARect .Bottom) ; end;
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: Para acomodar el cada vez mayor nhero de egtados en el estiko de interfaz de usuario de Windows 2000 , belphi inahye el evento OnAdvancedDraw I tern para 10s menu:
elementos del cuadro de lista estableciendo la propiedad ItemHeight y quc esta sera la a h a de todos 10s elementos. El segundo estilo de dibujo personalizado indica un cuadro dc lista con elementos de diferentes alturas. En este caso, cl componentc desencadenara el evento OnMeasure It em de cada elemento, para pedir a1 programa por sus alturas. En el ejemplo ODList (y en su version QODList), nos quedaremos con cl primcr enfoque, el mas sencillo. El cjcmplo contiene informacion sobre el color junto con 10s elementos dcl cuadro dc lista y, a continuacion, dibuja 10s elementos usando cstos colores (en lugar de usar un unico color para toda la lista). El archivo DFM o XFM de todo formulario, como este entrc otros, tiene un atributo TextHeight, que indica cl numero de necesarios para mostrar texto. Estc cs el valor que deberiamos usar para la propiedad ItemHeight del cuadro de lista. Una solucion alternativa consiste en calcular cstc valor en tiempo de cjccucion, de forma que si mas tarde cambiamos la fuentc en tiempo de diseiio, no tcngamos que recordar configurar la altura de 10s elcmentos en funcion de la misma.
NOTA: Acabaq~ps e s c r i b i r Text Height como un atributo dcl ford mulario, 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. respuesta es que e mecanismo La l 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. A estudiar el 1 codigo fuente dc la VCL, se ve que este valor se calcula mediante una llamada a un metodo privado del formulario: GetTextHeight.A ser privado, no pode1 mos 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 ' ) ;
Lo siguiente es aiiadir algunos elementos a1 cuadro de lista. Como este es un cuadro de lista de colores, queremos aiiadir nombres de colores a1 Items del cuadro de lista y 10s valores de color correspondientes a1 almacenamiento de datos Objects relacionado con cada elemcnto dc la lista. En lugar de aiiadir 10s dos valores por separado, hemos escrito un procedimiento para aiiadir nuevos elementos a la lista:
procedure T0DListForm.AddColors
var
Este mctodo usa un parametro dc matriz abierta, una matriz de un numero no dcterminado de clementos del mismo tipo. Para cada elemento pasado como paramctro, aiiadimos el nombre dcl color a la lista y aiiadimos su valor a 10s datos rclacionados, llamando a1 metodo AddOb je c t . Para obtener la cadcna correspondicnte al color, llamamos a la funcion Delphi C o l o r T o S t r i n g . ~ s t dea vuelve una cadcna que contiene la constante de color correspondiente, si existe, o cl valor hexadecimal del color. Los datos de color se aiiaden a1 cuadro de lista despues de comprobar su valor de acuerdo con el tipo de datos TOb j e c t (una referencia de cuatro bytcs), segun lo requiere el metodo AddOb je c t .
TRUCO: Ademas de ColorToStr ing, que convierte un valor de color en la correspondiente cadena con el identificador o el valor hexadecimal, la funcion StringToColor de Delphi convierte una cadena de formato apropiado en un color.
En el cjemplo ODList; este metodo se llama en el controlador de eventos o n c r e a t e del formulario (despues de fijar la a h a de 10s elementos):
Addcolors ([clRed, clBlue, clYellow, clGreen, clFuchsia, cllime, clpurple, clGray, RGB (213, 23, 123), RGB (0, 0, O), clAqua, clNavy, clOlive, clTeal] ) ;
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 :
El sistema ya establece el color de fondo adecuado, de mod0 que el elemento seleccionado aparezca de forma adecuada, aunque no aiiadamos codigo adicional. Aun mas, el programa permite aiiadir nuevos elementos a1 hacer doble clic sobre el cuadro de lista:
procedure TODListForm.ListBoxlDblClick(Sender: TObject); begin i f ColorDialogl.Execute t h e n Addcolors ([ColorDialogl.Color]); 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
ADVERTENCIA: El conti01 Listview no tieno ep CLX fas vjstqs de i o cm pequdoa y g r a d e s . En Qt,este tip0 de apariencia esddisponi.ble graciaa a otro a m p a e f i t e . el IcanView.
En el ejemplo RefList (una simple lista de referencias a libros, revistas, CD-ROM y sitios Web), 10s elementos se almacenan en un archivo, puesto que 10s usuarios del programa pueden editar 10s contenidos de la lista, que se guardan automaticamente cuando se abandona el programa. De este modo, las ediciones que realiza el usuario se convierten en permanentes. Guardar y cargar 10s contenidos de un ListView no son tareas insignificantes en absoluto, puesto que el tip0 TList I t e m s no dispone de un mecanismo automatico para guardar datos. Como tCcnica alternativa sencilla, hemos copiado 10s datos en una lista de cadena, usando un formato personalizado. A continuacion, la lista de cadena se puede guardar en un archivo y cargar de nuevo con una unica orden El formato de archivo es sencillo, como se puede ver en el siguiente codigo. Para cada elemento de la lista, el programa guarda el titulo en una linea, el indice de la imagen en otra linea (que lleva como prefijo el caracter @) y 10s subelementos en las lineas siguientes, sangradas por un caracter de tabulacion:
procedure TForml.FormDestroy(Sender: TObject); var I, J : Integer; List: TStringList; begin // almacena 10s elementos List : = TStringList-Create; try for I : = 0 to ListViewl.1tems.Count - 1 do begin // g u a r d a e l t i t u l o List .Add (ListViewl.Items [I] .Caption) ;
// 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
cstablecer la propiedad SortType de ListView como st Both o st Data. De este modo, la ListView realizara la clasificacion no basandose en 10s titulos, sin0 llamando a1 evento Oncompare de cada uno de 10s dos elementos que ha de clasificar.
Fle
View
Heb
I-
Borland Develo..
Delohi
Delohi
Delahi
in Java
fib ~ i m ~ l p r
Borland Develooers Conference ... .:-Delohi ClienVServer 00 Pelohi Develooer's Handbook n ~ $ + D e l o hInformant i Mastering D e b h i n & T h e D e b h i Maaanne q -Thinkina in Java OW marco@marcocantu.com OQwww.borland.com O.dwww.marcocanlu.com
nQ
'igura 5.12. Diferentes ejemplos de las combinaciones de estilos de un componente ListView en el programa RefList, obtenidos al cambiar la propiedad Viewstyle y anadir las casillas de verificacion.
Como queremos realizar la clasificacion de cada una de las columnas de la vista dctallada, tambien controlamos el evento OnColumnClick (quc ocurre cuando el usuario hace clic sobre 10s titulos de la columna cn la vista detallada, pero solo si la propiedad ShowColumnHeaders esta definida como True). Cada vez que hacemos clic sobre una columna, el programa guarda el numero de la misma en el campo privado nsortcol de la clase de formulario:
p r o c e d u r e TForml.ListViewlColumnClick(Sender: Column: TListColumn); begin nSortCol : = Column.Index; ListViewl.AlphaSort; end; TObject;
A continuation; en el tercer paso, el codigo de clasificacion usa el titulo o uno de 10s subelementos segun la columna de clasificacion en uso:
procedure TForml.ListViewlCompare(Sender: TObject; Iteml, I tem2 : TListItem; Data: Integer; var Compare: Integer) ; begin i f nSortCol = 0 t h e n
Compare : = CompareStr (Iteml.Caption, Item2.Caption) else Compare : = CompareStr (1teml.SubItems [nSortCol - 11, Item2 .SubItems [nSortCol - 11 ) ; end;
En la version CLX del programa (llamada QRefList) no es necesario seguir ninguno de estos pasos. El control ya es capaz de realizar la clasificacion por si mismo cuando se hace clic sobre su titulo. Automaticamente se consiguen varias columnas que se auto-ordenan (tanto ascendente como descendentemente). Las caracteristicas finales que hemos afiadido a1 programa estan relacionadas con las operaciones de raton. Cuando el usuario hace clic con el boton izquierdo del raton sobre un elemento, el programa RefList muestra una descripcion del elemento seleccionado. A1 hacer clic con el boton derecho del raton sobre el elemento seleccionado, este pasa a su mod0 edicion y el usuario puede cambiarlo (tengamos en cuenta que 10s cambios se guardan automaticamente cuando finaliza el programa). Veamos el codigo utilizado para ambas operaciones, en el controlador del evento OnMouseDown del control ListView:
p r o c e d u r e TForml.ListViewlMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var strDescr: string; I: Integer; begin // s i h a y u n e l e m e n t o s e l e c c i o n a d o i f ListViewl.Se1ected <> n i l t h e n i f Button = &Left then begin / / crea y muestra una d e s c r i p c i o n strDescr : = ListViewl.Columns [0] .Caption + # 9 + ListViewl.Se1ected.Caption + #13; f o r I : = 1 to ListViewl.Selected.SubItems.Count d o strDescr : = strDescr + ListViewl.Columns [I] .Caption + # 9 + ListViewl.Selected.SubItems [I-l] + #13; ShowMessage (strDescr); end e l s e i f Button = &Right then / / edita e l t i t u l o ListViewl.Se1ected.EditCaption; end;
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:
object ListViewl: TListView Align = alClient Columns = < item Caption = 'Referencia ' Width = 2 3 0 end item Caption = 'Autor' Width = 1 8 0 end item Caption = 'Pais ' Width = 8 0 end> Font.Height = - 1 3 Font .Name = 'MS Sans Serif' Font.Style = [fsBold] FullDrag = True Hideselection = False HotTrack = True HotTrackStyles = [htHandPoint, htUnderlineHot] SortType = stBoth Viewstyle = vsList OnColumnClick = ListViewlColumnClick Oncompare = ListViewlCompare OnMouseDown = ListViewlMouseDown end
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
Node : = TreeViewl. Items .Add (nil, 'First level'); TreeViewl. Items .Addchild (Node, I Second level1) ;
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
TObject);
(ExtractFilePath
El mCtodo LoadFromFile carga 10s datos en una lista de cadena y verifica el nivel de cada elemento fijandose en el numero de caracteres de tabulacion. (Si siente curiosidad, fijese en el metodo TTreeStrings .Get Buf Start que puede encontrarse en la unidad ComCtrls en el codigo fuente de la VCL incluida
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 .
Q Sidney
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.
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]
Qt, Libc, QGraphics, QControls, QForms, QDialogs, QStdCtrls, QComCtrls, QMenus, QTypes, QGrids;
{SENDIF] { S I F D E F MSWINDOWS]
Windows, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, Menus, Grids; {SENDIF)
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]
He omitido algunas de las caracteristicas especificas de Windows, de manera que la unica diferencia en el codigo se encuentra en el metodo FormCreate. El programa carga el archivo de datos de la carpeta predefinida del usuario, no de la misma carpeta que el ejecutable. Segun el sistema operativo del que se trate, la carpeta del usuario sera el directorio home (y el archivo oculto comienza por un punto) o el area especifica Mis Documentos (accesible con una llamada especial de la API):
procedure TForml.FormCreate (Sender: TObject) ; var path: string: begin
{ $ IFDEF 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;
A1 tener estas clases de nodo de arbol personalizadas, el programa crea un arbol de elementos, usando el primer tipo para 10s nodos del primer nivel y la segunda clase para 10s otros nodos. Como solo tenemos un controlador de eventos O n C r e a t e N o d e C l a s s , este utiliza la referencia de clase almacenada en un campo privado del formulario ( C u r r e n t N o d e C l a s s del tipo T T r e e N o declass):
p r o c e d u r e TForml.TreeViewlCreateNodeClass(Sender: TCustomTreeView; v a r NodeClass: TTreeNodeClass); begin NodeClass : = CurrentNodeClass; 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;
Cuando se ha creado el arbol completo, en el momento en que el usuario selecciona un elemento, podemos convertir su tipo a TMyNode y acceder a las propiedades adicionales (pero tambien a metodos y datos):
p r o c e d u r e TForml.TreeViewlClick(Sender: TObject); var MyNode: TMyNode; begin MyNode : = TreeViewl.Selected a s TMyNode; Labell. Caption : = MyNode .Text + ' [ ' + MyNode .ClassName + - ! + IntToStr (MyNode.Extracode) ; end;
'I
Este es el codigo empleado en el ejemplo CustomNodes para mostrar la descripcion del nodo seleccionado en una etiqueta. Fijese en que cuando seleccionamos un elemento dentro del arbol, su valor se multiplica por el de cada uno de 10s nodos padre. Aunque existen formas realmente mas sencillas de obtener este mismo efecto, una vista en arbol con objetos de elementos creados a partir de diferentes clases de una jerarquia ofrece una estructura orientada a objetos que podemos emplear como base de un codigo mas complejo.
Acabamos dk comentar 10s conceptos bisicos de la clase T C o n t rol y sus clases derivadas en las bibliotecas VCL y VisualCLX. Despues hemos dado un rapido repaso a 10s principales controles que se pueden usar para construir una interfaz de usuario, tales como, 10s componentes de edicion, listas, selectores de rango y muchos mas. En este capitulo varnos a centrarnos en otros controles utilizados para definir el diseiio global de un formulario, como Pagecontrol y Tabcontrol. Despues de estos componentes, vamos a comentar las barras de herramientas y de estado, con algunas caracteristicas bastante avanzadas. Con esto conseguiremos la base para el resto del capitulo, en el que se habla de acciones y de la arquitectura Action Manager. Las modernas aplicaciones de Windows suelen tener varios modos de ofrecer ordenes, como elementos de menu, botones de la barra de herramientas, atajos de menu y demas. Para separar las ordenes reales que puede dar un usuario de sus multiples representaciones en la interfaz de usuario, Delphi usa el concept0 de acciones. En las ultimas versiones de Delphi, esta arquitectura se ha extendido para hacer que la construccion de la interfaz de usuario sobre las acciones sea completamente visual. Ahora tambien se puede dejar que 10s usuarios del programa personalicen esta interfaz facilmente, como sucede con muchos programas profesionales. Finalmente, Delphi 7 afiade a 10s controles visuales que soportan la
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.
NOTA: Delphi incluye todavia (en la pestaiia Win 3.1 de la Component Palette) 10scomponentes Notebook, TabSet y TabbedNotebook introducidos en las versiones de 32 bits (es decir, desde Delphi 2). Para cualquier otro fin, 10s componentes PageControl y Tabcontrol, que encapsulan controles comullei de Win32, ofrecen una ikterfaz de usuario mbs modema - - - . - - - -. En realidad. en las verslones de 32 b ~ t s U e l ~ h, .el c o m ~ o n e n t e de i rabbedNotebook se implement0 de nuevo usando el control PageControI (je Win32 de forma interna, para reducir el tamaiio del codigo y actualizarr
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);
Tabs T&
I
Ckk on lha Mbcx to change papc
a,
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.
El otro boton llama a1 mismo procedimiento y pasa False como su parametro para selcccionar la ficha anterior. Fi-jese cn que no es neccsario verificar si estamos cn la primcra o en la ultima ficha, porque el metodo SelectNext Page considera quc la ultima ficha es la quc csta antes de la primcra y nos llevara directamcnte cntre estas dos fichas. Ahora podemos centrarnos de nucvo en la primera ficha. Posce un cuadro de lista, que cn tiempo de ejecucion contiene 10s nombres de las solapas. Si un usuario hace clic sobre un elemento del cuadro dc lista, la pagina actual cambia. Este cs el terccr mctodo del que disponemos para cambiar de ficha (despues de las solapas y de 10s botones Next y Previous). El cuadro de lista se rcllena con cl metodo Formcreate, asociado con el evento Oncreate dcl formulario, y copia cl titulo de cada ficha (la propicdad Page contiene una lista de objetos Tabsheet):
for I : = 0 to Pagecontroll. Pagecount - 1 do ListBoxl.Items.Add (PageContro1l.Pages.Caption);
Cuando hacemos clic sobre un elemento de la lista, podemos seleccionar la pagina correspondiente:
procedure TForml.ListBoxlClick(Sender: TObject); begin Pagecontroll-Activepage : = Pagecontroll-Pages [ListBoxl.ItemIndex]; end;
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;
La ultima ficha posee un componente de memo, de nuevo con 10s nombres de las fichas (aiiadidas en el metodo FormCrea t e ) . Podemos editar 10s nombres de las fichas y hacer clic sobre el boton Change para modificar el texto de las solapas, per0 solo si el numero de cadenas se corresponde con el numero de pestaiias :
procedure TForml.BitBtnChangeClick(Sender: TObject); var I: Integer; begin if Memol.Lines.Count <> PageControll.PageCount then MessageDlg ( ' U n a l i n e d p o r p e s t a d a , p o r f a v o r I , mtError, [mbOKl, 0 ) else for I : = 0 to PageControl1.PageCount -1 do PageControll.Pages [I] .Caption : = Memo1 .Lines [I]; BitBtnChange.Enabled : = 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
Memol. Lines .Add (strcaption); ListBoxl .Items .Add (strcaption); end; end;
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);
Este ejemplo fbnciona, a no ser que seleccionemos un archivo que no contenga un mapa de bits. El programa advertira a1 usuario con una escepcion estandar, ignorara el archivo y continuara ejecutandose. El programa permite tambien pegar el mapa de bits en el portapapeles (aunque sin copiarlo en realidad, sino solamente aiiadiendo una pestaiia que realizara la operacion de pegado real cuando se seleccione) y copiar el mapa de bits actual en el. El soporte para el portapapeles esta disponible en Delphi mediante un objeto Clipboard definido en la unidad ClipBrd. Para copiar y pegar mapas de bits, podemos usar el metodo A s s i g n de las clases TClipboard y TBitmap. Cuando seleccionamos la orden Edit>Paste del ejemplo, se aiiade una nueva solapa, cuyo nombre es Clipboard, a1 conjunto de solapas (a no ser que ya este prescnte). A continuacion, el numero de la nueva solapa se usa para modificar la solapa activa:
procedure TFormBmpViewer.PastelClick(Sender: var TabNum: Integer;
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;
En cambio, la operacion EditXopy resulta tan sencilla como copiar el mapa de bits que esta actualmente en el control de imagen:
Para tener en cuenta la posible presencia de la solapa Clipboard, el codigo del metodo TabControllChange se transforma en:
p r o c e d u r e TFormBmpViewer.TabControllChange(Sender: TObject); var TabText : string; begin 1magel.Visible : = True; TabText : = TabControll.Tabs [TabControll.TabIndex]; i f TabText <> ' C l i p b o a r d ' t h e n // c a r g a e l a r c h i v o i n d i c a d o en l a s o l a p a 1magel.Picture.LoadFromFile (TabText) else { s i l a s o l a p a es ' C l i p b o a r d ' y u n mapa d e b i t s e s t d d i s p o n i b l e en e l p o r t a p a p e l e s ) i f Clipboard.HasFormat (cf-Bitmap) t h e n 1magel.Picture.Assign (Clipboard) else begin / / s i no e l i r n i n a l a s o l a p a c l i p b o a r d TabControll.Tabs.Delete (TabControll.Tab1ndex); i f TabControll. Tabs. Count = 0 t h e n 1magel.Visible : = False; 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
metodo CopylClick se encarga de copiar la imagen actual a1 portapapeles, Delete1C 1ic k sencillamente elimina la solapa actual. Veamos su codigo:
procedure TFormBmpViewer.CopylClick(Sender: TObject); begin Clipboard.Assign (1magel.Picture.Graphic); end; procedure TFormBmpViewer.DeletelClick(Sender: begin with Tabcontroll do begin if TabIndex >= 0 then Tabs.Delete (TabIndex); if Tabs.Count = 0 then Imagel-Visible : = False; end; end; TObject);
Una de las caracteristicas especiales del ejemplo es que el Tabcontrol tiene la propiedad OwnerDraw definida como True.Esto significa que el control no pintara las solapas (que estarin vacias en tiempo de diseiio), sin0 que lo hara la aplicacion, llamando a1 evento OnDrawTab. su codigo, el programa muestra En el texto centrado verticalmente, usando la funcion DrawText de la API. El texto que aparece no es la ruta del archivo completa sino solo el nombre del archivo. A continuacion, si el texto es distinto de None, el programa lee el mapa de bits a1 que se refiere la solapa y pinta una version reducida del mismo en la propia solapa. Para ello, el programa usa el objeto TabBmp,que es del tipo TBitmap y se crea y destruye junto con el formulario. El programa usa tambien la constante BmpSide para colocar de forma adecuada el mapa de bits y el texto:
procedure TFormBmpViewer.TabControllDrawTab(Control: TCustomTabControl; TabIndex: Integer; const Rect: TRect; Active: Boolean); var TabText: string; OutRect : TRect; begin TabText : = TabControll.Tabs [TabIndex]; OutRect : = Rect; InflateRect (OutRect, -3, -3) ; 0utRect.Left : = 0utRect.Left + BmpSide + 3; DrawText (Control.Canvas.Handle, PChar (ExtractFileName (TabText) ) , Length (ExtractFileName (TabText)), OutRect, dt-Left or dt-SingleLine or dt-VCenter); if TabText = 'Clipboard' then if Clipboard .HasFormat (cf-Bitmap) then TabBmp-Assign (Clipboard) ' else TabBmp.FreeImage
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.
Cuando haccmos clic sobrc el boton Next de la primera pagina, el programa mira el cstado de la casilla dc verification y decidc que ficha cs la siguiente. Podriamos habcr escrito el codigo de este modo:
p r o c e d u r e TForml.btnNextlClick(Sender: T O b j e c t ) ; begin B t n B a c k - E n a b l e d : = True; i f CheckInprise.Checked then PageControl1.ActivePage : = TabSheet2 else PageControl1.ActivePage : = TabSheet3; // m u e v e l a i m a g e n y e l b i s e l a d o B e v e l l - P a r e n t : = PageControl1.ActivePage; 1 m a g e l . P a r e n t : = PageControl1.ActivePage; end;
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);
procedure TForml.MoveTo(TabSheet: TTabSheet); begin // adade l a illtima f i c h a a l a l i s t a B a c k P a g e s . A d d (PageControl1.ActivePage); BtnBack.Enabled := True; // c a m b i a d e f i c h a PageControl1.ActivePage : = Tabsheet; // m u e v e l a i m a g e n y el B e v e l
A d e m b del codigo que ya hemos explicado, el metodo MoveTo aiiade la ultima ficha (la que esta antes del cambio de ficha) a una lista de fichas visitadas, que se comporta como una pila. De hecho, el objeto BackPages de la clase TList se crea a1 arrancar el programa y la ultima ficha siempre se aiiade a1 final. Cuando el usuario hace clic sobre el boton Back, que no depende de la ficha, el programa extrae la ultima ficha de la lista, borra su entrada y se mueve a dicha ficha:
procedure TForml.btnBackClick(Sender: TObject); var LastPage: TTabSheet; begin // o b t i e n e l a u l t i m a f i c h a y s a l t a a e l l a LastPage := TTabSheet (BackPages [BackPages .Count PageControl1.ActivePage : = LastPage; // b o r r a l a u l t i m a f i c h a d e l a l i s t a BackPages .Delete (BackPages.Count - 1) ; // f i n a l m e n t e d e s a c t i v a e l b o t o n b a c k BtnBack. Enabled := not (BackPages.Count = 0) ; // mueve l a imagen y e l B e v e l Bevell-Parent : = PageControll.ActivePage; Imagel-Parent : = PageControl1.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;
StrUrl : = C o p y (Caption, Pos ('http://', Caption), 1000) ; ShellExecute (Handle, 'open ', PChar (StrUrl) , ' ', ' ', sw-Show) ; end;
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:
El estilo tbsButton: Indica un boton pulsador estandar. El estilo tbscheck: Indica un boton con el comportamiento de una casilla de verificacion, o de un boton de radio si el boton esta agrupado con otros en su bloque (determinado por la presencia de separadores). El estilo tbsDropDown: Indica un boton desplegable, una especie de cuadro combinado. La parte desplegable se puede implementar facilmente en Delphi conectando un control PopupMenu a la propiedad DropdownMenu del control. Los estilos tbsseparator y tbsDivider: Indican separadores con lineas verticales diferentes o sin ellas (dependiendo de la propiedad Flat de la barra de herramientas).
Para crear una barra de herramientas grafica, podemos aiiadir un componente ImageLis t a1 formulario, cargar algunos mapas de bits en el y a continuacion, conectar la ImageList con la propiedad Images de la barra de herramienta. Por defecto, las imagenes se asignaran a 10s botones en el orden en el que aparecen, per0 podemos cambiar facilmente este comportamiento fijando la propiedad
ImageIndex de cada boton de la barra de herramientas. Podemos preparar listas de imagenes adicionales para condiciones especiales de 10s botones y asignarlas a las propiedades DisabledImages y HotImages de la barra de herramientas. El primer grupo se usa para 10s botones desactivados, el segundo para el boton actual que esta bajo el raton.
tr
-
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~ortamiento alrmno 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
w
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.
An inllotluclion lo the Ijasic fealules o f the RichBa~example. ilisc~rssetl Chapter 6 of the in anti copyrighted Ily Mmco 13ant11. book "lvlaste~ingDelphi 7". W ~ i n e n
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~ile Operations
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-
&- CI, - ,L
i,
,-,,,
6,.
,i'
Los distintos botones implementan caracteristicas, una de las cuales consiste en un esquema completo para abrir y guardar archivos de texto (se pide a1 usuario que guarde cualquier archivo modificado antes de abrir uno nuevo, para no perder ningun cambio). La parte del programa encargada de la administracion de archivos es bastante compleja, per0 vale la pena explorarla, dado que muchas aplicaciones basadas en archivos utilizan un codigo similar. Ademas de las operaciones de archivo, el programa soporta las operaciones de copiar y pegar, y la administracion de fuentes. Para las operaciones de copiar y pegar no es necesario una interaccion real con el portapapeles, dado que el componente puede controlarlas con ordenes sencillas como:
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;
La operacion de copia, en cambio, no se puede decidir mediante una accion del usuario, puesto que depende del contenido del portapapeles, que esta influido tambien por otras aplicaciones. Un enfoque es utilizar un temporizador y verificar el contenido del portapapeles de vez en cuando. Otra mejor consiste en utilizar el evento OnIdle del objeto Application (o el componente ApplicationEvents). Dado que el control RichEdit soporta diversos formatos de portapapeles, el codigo no puede fijarse simplemente en ellos, sino que deberia preguntar a1 propio componente, usando una caracteristica de bajo nivel no exteriorizada por el control Delphi:
p r o c e d u r e TForrnRichNote.ApplicationEventslIdle(Sender: v a r Done: B o o l e a n ) ; begin // a c t u a l i z a b o t o n e s d e la b a r r a d e h e r r a m i e n t a s tbtnPaste.Enabled : = S e n d M e s s a g e (RichEdit.Handle, em-CanPaste, 0, 0) <> 0; end; TObject;
La administracion basica de la fuente se realiza mediante 10s botones Bold e Italic, que poseen un codigo similar. El boton Bold alterna el atributo relativo del texto seleccionado (o cambia el estilo de la posicion de edicion activa):
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);
De nuevo, el estado actual del boton se establece mediante la seleccion activa, por lo que habra que aiiadir la siguiente linea a1 metodo R i c h E d i t S e l e c tionchange:
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;
Como el control ToolBar es un contenedor de control muy complete, podemos coger directamente un cuadro de edicion, un cuadro combinado y otros controles y colocarlos en la barra de herramientas. El cuadro combinado de la barra de herramientas se inicia con el metodo Formcreate, que extrae las fuentes de pantalla disponibles en el sistema:
ComboFont . Items : = Screen. Fonts; ComboFont.ItemIndex : = ComboFont.Items.IndexOf (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.
primer panel de Ia barra de estado. Esto se podria haber sinapWoado en el codigo mediante el aso de la propiedad AutoHint, pas mstrar un ckbgo d s detallado permite personalizar este wmportami&ct,
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-
trolando el evento 0 n H i n t de la aplicacion, utilizando el componente A p p l i c a t ionEvent s y copiando el valor actual de la propiedad Hint de la aplicacion a la barra de estado:
procedure TFormRichNote.ApplicationEventslHint (Sender: TObject); begin StatusBarl.Panels[sbpMessage].Text : = Application.Hint; end;
Este codigo muestra de manera predefinida en la barra de estado el mismo testo de las sugerencias contextuales, que no son generadas por 10s elementos de mcnu. En realidad, podemos usar la propiedad H i n t para especificar cadenas diferentes para 10s dos casos; escribiendo una cadena dividida cn dos partes mediante un separador, el caracter "tuberia" (().Por ejemplo, podriamos introducir el siguiente como valor de la propiedad H i n t :
' N u e v o 1 C r e a r un n u e v o d o c u m e n t o '
La primera parte de la cadena, Nuevo, la usan las sugerencias contextuales y la segunda parte, Crenr un nztevo documento, la barra de estado. La figura 6.6 muestra un ejemplo.
II 1
AII i n t ~ o d ~ ~ cto i the ~ t o ~ 11asic fe.itt~~es the RicllBar example. ~ l i s c ~ ~ sin e h a p t e ~ nf the of r t~l G L.11rti1. book "Maste~itrgD e l p l ~7". W l i n e n an11 copyriglrted I)y I r l a ~ c o i
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~onal apphcahon 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 -
." 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~tHint GetLongHint par# y 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,
El segundo panel muestra el estado de la tecla BloqMayus, que se obtiene a1 llamar a la funcion G e t K e y s t ate de la API, que devuelve un numero de estado. Si se activa el bit menos significativo de dicho numero (si el numero es impar), quiere decir que la tecla esta activada. Este estado se verifica cuando la aplicacion esta en espera, de forma que la comprobacion se realice cada vez que se pulsa una tecla, per0 tambien desde el momento en que un mensaje alcanza la ventana (en caso de que el usuario cambie esta configuracion mientras trabaja con otro programa). Hemos aiiadido a1 controlador A p p 1i cat io n E v e n t s 1I d 1e una llamada a1 metodo personalizado C h e c k c aps 1o c k , implementado del siguiente modo:
procedure TFormRichNote.CheckCapslock; begin if Odd (GetKeyState (VK-CAPITAL)) then StatusBarl.Panels [sbpcaps].Text : = ' C A P S ' else StatusBarl. Panels [sbpcaps].Text : = ' '; end ;
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.
Ya que esta tecnica se encuentra incrustada en Qt, tambien esta disponible en la version para Windows de la biblioteca; CLX la pone a disposicion de 10s desarrolladores en Delphi, de manera que una aplicacion puede tener una apariencia de Motif en un sistema operativo de Microsoft. El objeto global A p p l i c a t ion de la CLX tiene una propiedad s t y l e que se puede usar para establecer un estilo pcrsonalizado o uno predefinido, indicado por la subpropiedad D e f a u l t S t y l e . Por ejemplo, se puede seleccionar una apariencia de Motif mediante este codigo:
En el programa StylesDemo, entre varios controles de muestra, se ha incluido un cuadro de lista con 10s nombres de 10s estilos predefinidos, tal y como se indican en la enumeracion T D e f a u l t S t y l e y este codigo para su evento OnDblClick:
procedure TForml.ListBoxlDblClick(Sender: T O b j e c t ) ; begin Application.Style.Defau1tStyle : = TDefaultStyle (ListBoxl.Item1ndex); end
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
archivo de recurso incluye el archivo s a m p l e . manifest,que esta disponible en el mismo sitio. Usar el componente XpManifest, que Borland ha aiiadido en Delphi 7 para simplificar aun mas estas tareas. Al dejar este componente aparentemente inutil sobre el formulario de un programa, Delphi incluira automaticamente su unidad XPMan, que importa el archivo de recurso VCL comentado anteriormente.
ADVERTENCIA: Cuando se elimina el componente XpMani fe s t de una aplicacion, tambien se debe borrar la unidad XPMan de la sentencia uses manualmente (Delohi no lo hace). Si no se hace esto. incluso sin el
hace pregunrarse por que aorlana creo el componenre en lugar ae proporcionar la unidad o el archivo de recurso relacionado). Ademb, este componente no esth en absoluto documentado.
/>
</dependentAssernbly> </dependency> </assembly>
Como muestra, en la carpeta del ejemplo Pages comentado anteriormente se incluye el archivo de manifiesto del listado 6.2. A1 ejecutarlo sobre Windows XP con el tema estandar de XP, se conseguira un resultado similar a1 mostrado en la
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
[ 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 n entre el objeto cliente y la accion. El objeto ~ ci ot ~ 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
aparece en la Component Palette. La lista de acciones recibe las acciones ejecutadas que no controlan 10s objetos de accion especificos y activa O n E x e c u t e A c t i o n . Si la lista de acciones no controla la accion, Delphi hace una llamada a1 evento O n E x e c u t e A c t i o n del o b j e t o A p p 1 i c a t i o n . El componente ActionList tiene un editor especial que se puede utilizar para crear diversas acciones, como se muestra la figura 6.9.
AI I
Figura 6.9. El editor del cornponente ActionList, con una lista de acciones predefinidas que se pueden w a r .
En el editor, las acciones aparccen en grupos, como indica su propiedad C a t e g o r y . A1 definir esta propiedad con un valor nuevo, se le indica a1 editor que introduzca una nueva categoria. Estas categorias son basicamente grupos logicos, aunque en algunos casos un grupo de acciones puede funcionar so10 con un tip0 especifico de componente de destino. Se podria querer definir una categoria para cada menu desplegable o agruparlos logicamente de otro modo.
Acciones de edicion: Reflejadas en el ejemplo siguiente. Son entre otras: cortar, copiar, pegar, seleccionar todo, deshacer y borrar. Acciones RichEdit: Complementan las acciones de edicion para 10s controles RichEdit y son entre otras: negrita, cursiva, subrayado, resaltar, viiietas y varias acciones de alineacion. Acciones de ventana MDI: Son todas las operaciones MDI mas comunes: organizar, cascada, cerrar, dividir (horizontal o verticalmente) y minimizar todo. Acciones de conjuntos de datos: Relacionadas con tablas de bases de datos y con consultas. Todas las operaciones que se pueden realizar en un conjunto de datos. Delphi 7 aiiade a las acciones de conjuntos de datos basicas un grupo de acciones especificamente adaptadas a1 componente Client DataSet,incluyendo: aplicar, invertir, deshacer. Acciones de ayuda: Permiten activar la pagina de contenidos o el indice del archivo de ayuda de la aplicacion. Acciones de busqueda: Buscar, buscar primero, buscar siguiente y reemplazar. Acciones de 10s controles solapa y pagina: El desplazamiento pagina anterior y pagina siguiente. Acciones de dialogo: Activan color, fuente, abrir, guardar e imprimir dialogos. Acciones de lista: Borrar, copiar, mover, eliminar y seleccionar todo. Estas acciones permiten interactuar con un control de lista. Otro grupo de acciones, como la lista estatica, la lista virtual y algunas clases de soporte, permiten definir listas que se pueden conectar a la interfaz de usuario. Acciones Web: Explorar el URL, descargar el URL y enviar correo electronic~. Acciones de herramientas: Solo incluyen el dialogo para personalizar las barras de accion. Ademas de manejar el evento OnExecute de la accion y cambiar el estado de la accion para causar un efecto en la interfaz de usuario de 10s controles clientes, una accion puede controlar tambien el evento Onupdate,que se activa cuando la aplicacion no esta en uso. Esto proporciona la oportunidad de verificar el estado de la aplicacion o del sistema y cambiar la interfaz de usuario de 10s controles en funcion de ello. Por ejemplo, la accion estandar PasteEdit activa 10s controles de cliente solo cuando hay algun texto seleccionado en el portapapeles.
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
, - -
NOTA: Las teclas de mktodo abreviado e s t h almacenadas en 10s archivos DFM usando numeros de teclas virtuales, entre 10s que hay valores para las
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 >.
Todas estas acciones estan conectadas a 10s elementos de un componente MainMenu y algunas de ellas tambitn a 10s botones de un control T o o l B a r . Como muestra la figura 6.10, las imagenes seleccionadas en el control ActionList afectan solamente a las acciones del editor. Para que las imagenes del lmageList aparezcan en 10s elementos del menu y en 10s botones de la barra de herramientas, hay que seleccionar tambien la lista de imagenes en 10s componentes MainMenu y ToolBar
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);
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
TObject);
Style
else
:= :=
Style
[fsBold]
Style
end ;
Style + [fsBold] ;
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);
) ;
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.
la barra de herramientas, sin0 que modifica el estado de las acciones. El metodo RichEdi t Se lec t ionchange no actualiza el estado del boton de negrita (Bold), que esta conectado a una accion con el siguiente controlador OnUpdate:
p r o c e d u r e TFormRichNote.acBoldUpdate(Sender: T O b j e c t ) ; begin acBold.Checked : = fsBold i n RichEdit.SelAttributes.Sty1e; end;
Para la mayoria de las acciones existen otros controladores de eventos OnUpdate similares, como por ejemplo para operaciones de recuento (disponible solo si hay algun texto en el control RichEdit), la operacion save (disponible si el texto ha sido modificado) y las operaciones Cut y Paste (solo disponibles si hay texto seleccionado):
p r o c e d u r e TFormRichNote.acCountcharsUpdate(Sender: TObject); begin acCountChars.Enab1ed : = RichEdit.GetTextLen > 0; end; p r o c e d u r e TFormRichNote.acSaveUpdate(Sender: begin acSave.Enabled : = Modified; end; TObject);
En el ejemplo antiguo, el estado del boton Paste se actualizaba en el evento OnIdle del objeto Application. Ahora que se utilizan acciones, se puede convertir en otro controlador OnUpdate mas:
p r o c e d u r e TFormRichNote.acPasteUpdate(Sender: TObject); begin a c P a s t e - E n a b l e d : = S e n m e s s a g e (RichEdit.Handle, em-CanPaste, 0, 0 ) <> 0; end ;
Los tres botones de la barra de herramientas para la justificacion de parrafos y 10s elementos de menu asociados deberian funcionar como botones de radio, siendo mutuamente exclusivos en cada una de las tres opciones seleccionadas. Por ello, las acciones tienen un GroupIndex definido como 1,los correspondientes elementos de menu tienen la propiedad Radio1 tern definida como True y 10s tres botones de la barra de herramientas tienen su propiedad Grouped definida como T r u e y la propiedad A l l o w A l l U p como False. (Ademas estan visualmente encerrados entre dos separadores). Esto es necesario para que el programa defina la propiedad Checked de la accion correspondiente con el
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 ;
Cuando se selecciona uno de estos botones, el controlador compartido utiliza el valor de Tag, definido como el valor correspondiente de la enumeracion TAl ignme nt , para determinar la justification correcta:
procedure begin
TFormRichNote.ChangeAlignment(Sender:
: = TAlignment
TObject);
RichEdit.Paragraph.Alignment
T A c t i o n ) .Tag) ; end;
((Sender a s
El componente CoolBar: Es un control comun de Win32 introducido por Internet Explorer y usado por algunas aplicaciones de Microsoft. El componente ControlBar: Esta totalmente basado en la VCL, sin dependencias de bibliotecas externas.
Ambos componentes pueden almacenar controles de barra de herramientas asi como algunos elementos adicionales, como cuadros combinados y otros controles. En realidad, una barra de herramientas puede reemplazar tambien a1 menu de una aplicacion. Ya que el componente CoolBar no se suele usar en las aplicaciones Delphi, hablaremos brevemente de el a continuacion.
.-- .
4 .
.,. .
..
Pnn 1R
LL'
uL
3 r nnr L ~ ~p 1, L 1
rjr111
D ; D ~ ~ ] ~ .
1lay que utilizar controlcs parcialmentt: transparentcs. El componentc ti1pic0 una CoolBar es el ToolBar per0 10s cuadros combinados. (:ua. , .,,t:..L , t,,,,,*, " .. 1.,-, A, , , , . , 0 1 ~ U C GUIGIUII y GUIILIUIG3 U C i l l l l l l l i l G I U 1 1 L;d1IIUICII 5 U I I USISLillllC GUIIIUIICS. 5 Se puede colocar una banda en cada linea o todas ellas en la misma. Cada una utilizara una parte de la superficie disponible y aumentara de tamaiio automaticamente cuando el usuario pinche sobre su titulo. Resulta mas facil utilizar este componente que explicarlo. Se puede probar con el ejemplo CoolDemo:
,-. a,:,,
El formulario del ejemplo CoolDemo tiene un componente Tco o 1Bar con cuatro bandas, dos por cada una de las dos lineas. La primera banda incluye un subconjunto de la barra de herramientas del ejemplo anterior, akdiendo ahora una ImageList para las imageries resaltadas. La segunda tiene un cuadro de edicion utilizado para establecer la fuente del texto; la tercera tiene un componente C o l o r G r i d , usado para escoger el color de la fuente y el de fondo. La ultima banda tiene un control ComboBox con las fuentes disponi-
es. La interfaz de usuario del componente ~ o o l Microsoft la utiliza en sus aplicaciones, pero alternativas como el componente C o n t r o l B a r ofrecen una interfaz de usuario similar sin ningtin tip0 de problema ahdido. El control CoolBar de Windows ha tenido muchas versiones distintas e incompatibles, ya que Microsoft ha hecho publir n c rlictintsrc v ~ r e i n n ~ c 1% h i h l i n t p r n dp
-"-" a--
--"
versiones de hternet Explorer. AIgunas de estas versiones "estropean" 10s programas existentes creados con Delphi, lo c u a es una b u m razon para no usarlo ahora, induso aunque sea mis estable.
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
Figura 6.12. El ejemplo MdEdit2 en tiempo de ejecucion, mientras que un usuario reordena las barras de herramientas.
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 ;
implementa la interfaz IDockManager. Esta interfaz tiene muchas finciones para personalizar el comportamiento de un contenedor de anclaje, como el soporte para streaming de su estado. Como se puede ver en esta pequeiia descripcion, el soporte de anclaje en Delphi se basa en un extenso numero de propiedades, eventos y metodos. El siguiente ejemplo introduce las principales caracteristicas que necesitaremos normalmente.
-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 V . m 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.
El programa controla 10s eventos O n D o c k O v e r y O n D o c k D r o p de un panel de anclaje anfitrion para mostrar mensajes a1 usuario, como el numero de controles anclados en ese momento.
procedure TForml.PanellDockDrop(Sender: TDragDockObject; TObject; Source:
De la misma forma, el programa controla tambien 10s eventos de anclaje del formulario principal. Los controles tienen un menu de metodo abreviado a1 que se puede recurrir para realizar operaciones de anclaje y desanclaje, sin necesidad del arrastre con el raton, con un codigo como este:
procedure TForml.menuFloatPanelClick (Sender: TObject); begin Panel2 .ManualFloat (Rect (100, 100, 200, 300) ) ; end ; procedure TForml.FloatinglClick(Sender: TObject); var aCtrl: TControl; begin aCtrol : = Sender as TControl; / / conmuta a estado flotante if aCtrl. Floating then aCtrl .ManualDock (Panell, nil, alBottom) ; else aCtrl.ManualFloat (Rect (100, 100, 200, 300)); end :
Para hacer que el programa se ejecute de manera correcta en el arranque, deberian anclarse 10s controles a1 panel principal en el codigo inicial; de no ser asi se observaria un efecto algo extraiio. Aunque resulte raro, para que el programa se comporte de manera adecuada, se necesita aiiadir controles a1 administrador de anclaje y anclarlos tambien a1 panel (una operacion no activa automaticamente la otra):
// anclar memo Memol.Dock (Panell, Rect (0, 0, 100, 100) ) ; Panell.DockManager.InsertControl(Memol, alTop, Panell); / / anclar cuadro de lista ListBoxl-Dock(Panell, Rect (0, 100, 100, 100) ) ; Panell.DockManager.Inse~:tContr01(ListBoxl, alleft, Panell); // anclar panel2 Panel2Dock (Panell, Rect (100, 0, 100, 100) ) ; Panell.DockManager.InsertControl(Panel2, alBottom, Panell);
La caracteristica final del ejemplo es, probablemente, la mas interesante (y la mas complicada de implementar correctamente). Cada vez que se cierra el programa, se guardan 10s estados actuales de anclaje del panel, utilizando el soporte del administrador de anclaje. Cuando se vuelve a abrir el programa, se vuelve a aplicar la informacion de anclaje, restaurando la configuracion previa de la ventana. Este es el codigo que se podria escribir para guardar y cargar este estado:
procedure TForml .Fordestroy (Sender: TObject) ; var FileStr: TFileStream; begin if Panell.DockClientCount > 0 then begin FileStr : = TFileStream.Create (DockFileName, fmCreate or fmOpenWrite) ; try Panel1.DockManager.SaveToStream (FileStr); finally FileStr-Free; end ; end else // e l i m i n a e l a r c h i v o DeleteFile (DockFileName); end; procedure TForml. Formcreate (Sender: TObject) ; var FileStr: TFileStream; begin // c o d i g o d e i n i c i a l i z a c i o n . ..
// v u e l v e a c a r g a r l a c o n f i g u r a c i o n DockFileName : = ExtractFilePath (Application-Exename) + 'dock-dck'; if FileExists (DockFileName) then begin FileStr := TFileStream. Create (DockFileName, fmOpenRead) ; try Panell.DockManager.LoadFromStream (FileStr); finally FileStr.Free; end: end ; Panel1.DockManager.ResetBounds (True); 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);
if (aRect.Bottom - aRect .Top <= 0 ) then begin aCtrl-ManualFloat (aCtrl-ClientRect); Panell.DockManager.RemoveControl(aCtrl); end ; end;
El listado cornpleto incluye codigo mas comentado, que se ha usado durante el desarrollo de este programa; podria usarse para comprender lo que sucede (que suele ser algo distinto de lo esperado). En pocas palabras, 10s controles que no tienen un tamaiio especificado en el administrador de anclaje (el unico mod0 en que se pucde detectar que no estan anclados) se muestran en una ventana flotante y se eliminan de la lista del administrador de anclaje. Si se analiza el codigo completo del controlador del cvento oncreate, se vera una gran cantidad de codigo complejo, solo para conseguir un comportamiento sencillo. Se podrian aiiadir mas caracteristicas a un programa de anclaje, per0 para hacer eso deberian eliminarse otras caracteristicas, ya que algunas podrian entrar en conflicto. Aiiadir un formulario de anclaje personalizado choca con las caracteristicas del administrador de anclaje. Los alineamientos automaticos no se llevan bien con el codigo del administrador de anclaje para recuperar el estado. Lo me-jor es tomar este programa y explorar su comportamiento, ampliandolo para soportar el tip0 de interfaz de usuario que se prefiera.
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 r unsporic i en h 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 '
object Panell: TPanel Align = alLeft DockSite = True OnMouseDown = PanellMouseDown object Pagecontroll: TPageControl Activepage = TabSheetl Align = alClient DockSite = True DragKind = dkDock object TabSheetl: TTabSheet Caption = ' List' object ListBoxl: TListBox Align = alClient end end end end object Splitterl: TSplitter Cursor = crHSplit end object Memol: TMemo Align = alClient end end
Observe que el panel tiene la propiedad UseDockManage definida como True y que el PageControl siempre alberga una pagina con un cuadro de lista porque a1 quitar todas las fichas, el codigo utilizado para el ajuste automatico del tamaiio de 10s contenedores de anclaje podria causar algun problema. Ahora el programa tiene otros dos formularios mas, con valores similares (aunque albergan controles diferentes):
object Form2: TForm2 Caption = 'Small Editor' DragKind = dkDock DragMode = dmAutomatic object Memol: TMemo Align = alClient end end
Se puede arrastrar estos fomularios a1 control de ficha para aiiadirle nuevas fichas, con 10s titulos correspondientes a cada formulario. Se puede incluso desanclar cada uno de estos controles e incluso el PageControl completo. Para ello, el programa no activa el arrastre automaticamente, lo cual haria que el carnbio entre fichas ya no fuese posible. En carnbio, la caracteristica se activa cuando el usuario hace clic en la zona sin solapas del PageControl, es decir, en el panel subyacente:
procedure TForml.PanellMouseDown(Sender: TMouseButton;
TObject;
Button:
Si se ejecuta el ejemplo DockPage, se puede comprobar este comportamiento, que trata de mostrar la figura 6.15. Observe que a1 quitar el Pagecontrol del formulario principal, no se pueden anclar directamente 10s demas formularios al panel, ya que lo impide el codigo especifico del programa (simplemente porque en ocasiones no se tratara de un comportamiento correcto).
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
26
6 13 20 27
7 14 21 28
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.
bitn llamado AC t i o n ~ o ~ u ~ ~ e n u ) k a el repositorio Web CodeCentraI de Borland (numero 1 8870). Ademis, se puede cncontrar mas infomacion en el sitio Web del autor deI componente (homepages.borland.com/strefhen). Es un miembro deI equipo de I+D de Delphi en Borland. El componente se encuentra en el sitio Web, pero no esta oficialmente soportado.
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
Las tres paginas del editor son asi: La primera ficha proporciona una lista visual de contenedores de acciones (barras de herramientas o menus). Para aiiadir nuevas barras de herramientas, se hace clic en el boton New. Para aiiadir nuevos menus, hay que aiiadir el componente correspondiente a1 formulario, despues abrir la coleccion A c t i o n B a r s del ActionManager, seleccionar una barra de acciones o aiiadir una nueva y engancharle el menu usando la propiedad A c t i o n B a r . Estos son 10s mismos pasos que podriamos seguir para conectar una nueva barra de herramientas a esta estructura en tiempo de ejecucion. La segunda ficha del editor de ActionManager es muy similar a la del editor de ActionList, que ofrece una forma estandar de aiiadir acciones nuevas o personalizadas, organizarlas en categorias y modificar su orden. Sin embargo, la caracteristica importante de esta ficha consiste en que se puede arrastrar una categoria o una simple accion desde la misma y soltarla en un control de una barra de acciones. Si se arrastra una categoria a un menu, se consigue un menu desplegable con todos 10s elementos de la categoria. Si se arrastra a una barra de herramientas, cada una de las acciones de la categoria genera un boton en la barra de herramientas. Si se arrastra una orden sencilla a una barra de herramientas, se obtiene el correspondiente boton, si se arrastra al menu, se obtiene una orden directa de menu, algo que como norma general se deberia evitar. La ultima pagina del editor ActionManager permite (a1 programador y opcionalmente a un usuario final) activar el visor de 10s elementos de menu usados recientemente y modificar algunas de las propiedades visuales de la barra de herramientas. El programa AcManTest es un ejemplo que usa algunas de las acciones estandar y un control RichEdit para explicar el uso de esta estructura (no se ha escrito nada de codigo personalizado para hacer que las acciones funcionen mejor, porque el objetivo era solo el administrador de acciones). Se puede experimentar con el en tiempo de diseiio o ejecutarlo, hacer clic en el boton Customize y ver lo que el usuario final puede hacer para personalizar la aplicacion. (Vease la figura 6.17.) En realidad, en el programa se puede evitar que el usuario realice algunas operaciones sobre acciones. Cualquier elemento especifico de la interfaz de usuario (un objeto T ~ c t i o n ~ l i e n t ) una propiedad C h a n g e A l l o w e d tiene que se puede usar para desactivar la modificacion, el desplazamiento y la eliminacion de operaciones. Cualquier contenedor de clientes de accion (las barras visuales) tiene una propiedad para inhabilitar su ocultacion (A1 l owH i d i ng , fijada por defect0 a T r u e ) . Cada coleccion de It e m s de una barra de accion tiene una opcion C u s t o m i z a b l e que se puede inhabilitar para desactivar todos 10s cambios de usuario en toda la barra.
Figura 6.17. Mediante el componente CustomizeDlg se puede permitir que un usuario personalice las barras de herramientas y el menu de una aplicacibn arrastrando elementos desde el cuadro de dialog0 o moviendolos en las barras de accion.
1.
TRUCO: ~ & d o mencionarnos ActionBar no nos referimos a las b a n s de herramientas visuales que contienen elementos de accion del componente ActionManager, que a su vez dispone de una coIeccion Items. El mejor modo de comprender esta estnrctura es fijarse en el subarbol mostrado por el Object TreeView para un componente ActionManager. Cada elemento de la coleccion TAc t i o n B a r tiene un componente visual Tcus tomAct ionBar conectado, pero lo contrario no ocurre (por eso, por ejemplo, no se puede alcanzar la propiedad Customizable, si se inicia seleccionando la barra de herramientas visual). Debido a la sirnilitud de 10s dos pombres, puede llevar un tiempo entender a q u e se refiere realmente la ayuda de Delphi.
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-
nencia se consigue mediante streaming de cada ActionClientItem conectado con el administrador de acciones. Como estos elementos de cliente de accion estan basados en la configuracion de usuario y mantienen la informacion de estado, un simple archivo recoge tanto 10s cambios que el usuario ha realizado en la interfaz como 10s datos de uso. Dado que Delphi almacena valores de usuario e informacion de estado en un archivo que nosotros ofrecemos, se puede hacer que la aplicacion soporte varios usuarios en un solo ordenador. Simplemente, hay que usar un archivo de configuraciones para cada uno de ellos (dentro de la carpeta Mi s document 0s) y conectarlo con el administrador de accion cuando el programa arranque (utilizando el usuario actual del ordenador o despues de algun nombre de usuario que se solicite). Otra posibilidad es almacenar estas configuraciones en la red, de forma que si un usuario esta en otro ordenador, su configuracion personal viaje con el. En el programa hemos decidido guardar 10s valores en un archivo dentro de la misma carpeta que el programa, asignado la ruta relativa (el nombre de archivo) a la propiedad FileName del ActionManager. El componente rellenara el nombre de archivo completo con la carpeta del programa, encontrando sin problemas el archivo que cargar. Sin embargo, el archivo incluye entre sus datos su propio nombre, con una ruta absoluta. Por eso, cuando llegue el momento de guardar el archivo, la operacion puede referirse a la ruta antigua. Esto impediria que se copiara este programa con sus configuraciones a una carpeta distinta (por ejemplo, esto es un problema para la prueba AcManTest). Se puede devolver su valor a la propiedad FileName despues de cargar el archivo. Como otra alternativa mejor, podria establecerse el nombre de archivo en tiempo de ejecucion, en el evento oncreate del formulario. En este caso tambien habria que obligar a que el archivo se recargase, ya que se asigna despues de que ya se hayan creado e inicializado 10s componentes ActionManager y 10s distintos ActionBar. Sin embargo, podria desearse forzar el nombre de usuario despues de la carga:
procedure TForml. Formcrate (Sender:TObject) ; begin ActionManagerl.Fi1eName : = ExtractFilePath (Application.ExeName) + ' s e t t i n g s ' ; ActionManagerl.LoadFromFi1e(ActionManagerl.FileName); // devolvemos el nombre a 1 fichero d e configuracion despues d e cargarlo (ruta relativa) ActionManagerl.FileName : = ExtractFilepath (Application.ExeName) + 'settings'; end ;
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).
Figura 6.18. El ActionManager desactiva 10s elementos de menu menos utilizados recientemente. Alin pueden verse mediante el comando de extension del menu.
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;
El resultado obtenido es la version textual del archivo settings actualizado automaticamente con cada ejecucion del programa. A continuacion, aparece una pequeiia parte del archivo, con 10s datos de uno de 10s menus desplegables y muchos comentarios adicionales:
item / / desplegable File de la barra de acciones del menu principal Items = < item Action = Forml.FileOpen1 LastSession = 19 // utilizado en la ultima sesion Usagecount = 4 // utilizado cuatro veces end item Action = Forml.FileSaveAs1 / / no usado nunca end item Action = Forml.FilePrintSetup1 LastSession = 7 / / usado hace algun tiempo
UsageCount = 1 // s o l o una vez end item Action = Forml.FileRun1 // no u s a d o nunca end item Action = Forml.FileExit1 // no usado nunca end> Caption = ' & F i l e 1 Lastsession = 19 UsageCount = 5 / / la suma del r e c u e n t o d e uso d e 10s e l e m e n t o s end
ponente ActionToolBar, pasa como uno de sus parametros un destino en blanco cuando el sistema crea un formulario flotante para albergar el control, por ese motivo no fue facil dotar a estos formularies de un nuevo titulo personalizado. (Vease el metodo EndDock del formulario.)
. . .>
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
El programa tiene tambien dos cuadros de lista en su formulario, utilizados como objetos de accion. Las acciones Copy y Move estan ligadas a estos dos cuadros de lista mediante sus propiedades Listcontrol y Destination. Sin embargo, la accion Delete trabaja automaticamente con el cuadro de lista que tiene el foco de entrada. En su coleccion Items, la StaticListAction define una serie de elementos alternativos. Esta no es una simple lista de cadena, ya que cada elemento tambien t i m e un ImageIndex que permite aiiadir elementos graficos a1 control que muestra la lista. Por supuesto. se pueden aiiadir mas elementos mediante programacion a esta lista. Sin embargo, en el caso de una lista altamente dinamica. tambien s e puede utilizar 1aVirtualListAct ion.Este accion no solo define una lista de elementos. sino que tiene dos eventos que se pueden usar para proporcionar cadenas e imagenes para la lista. El evento OnGetItemCount permite indicar el numero de elementos a mostrar y OnGe t Item se llama entonces para cada elemento especifico. En el ejemplo ListActions, la VirtualListAction t h e 10s siguientes controladores de eventos para su definicion y produce la lista que se aparece en el cuadro combinado de la figura 6.19.
procedure TForml.VirtualListActionlGetItemCount(Sender: TCustornListAction; var Count: Integer) ; begin Count : = 100; end; procedure TForml.VirtualListActionlGetItem(Sender: TCustornListAction; const Index: Integer; var Value: String; var ImageIndex: Integer; var Data: Pointer) ; begin Value : = 'Item' + IntToStr (Index); end;
Figura 6.19. La aplicacion ListActions tiene una barra de herrarnientas que hospeda una lista estatica y una lista virtual.
Ambas listas, la estatica y la virtual, tienen un controlador de eventos On1 temSe lected.En el controlador de eventos compartido, hemos escrito el codigo siguiente, para aiiadir el elemento actual a1 primer cuadro de lista del formulario:
procedure TForml.ListActionItemSe1ected(Sender: TCustomListAction; . Control: TControl) ; begin ListBoxl.Items.Add ((Control as TCustornActionCombo) .SelText) ; end:
bargo, accediendo a1 control visual que muestra la lista, obtenemos el valor del elemento seleccionado.
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.
Cuadros de dialogo y formularios modales y no modales. Creacion dinamica de formularios secundarios Cuadros de dialogo predefinidos. Construccion de una pantalla de inicio.
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.
procedure ShowStringForm (str: string) ; var form: TForm; begin Application.CreateForm (TForm, form); form.caption : = 'DynaForrn'; form-Position : = poScreenCenter; with TMemo.Create (form) do begin Parent : = form; Align : = alclient; Scrollbars : = ssvertical; ReadOnly : = True; Color : = form.Color; Borderstyle : = bsNone; WordWrap : = True; Text : = str; end; form.Show; end;
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).
Figura 7.1. El formulario dinamico generado por el ejemplo DynaForm se crea completamente en tiempo de ejecucion.
-TRUCO: Una ventaja indirecta de esta tecnica, comparada con el uso de archivos DFM para formularios en tiempo de disefio, es que supondria una mayor dificultad para un programador externo conseguir informacion soL-- l3- l- --l:---:Ap..-L ----- ----A- ---- -1 VIC la ~ s n u c ~ u u~ la ayl1c;ac;lutl. LUIIIU nernus vlsrv, sr; pur;ur; r;xrrar;i GI ra DFM del archivo ejecutable real de Delphi, per0 tambien se puede hacer lo mismo con cualquier archivo ejecutable compilado con Delphi del que no tengamos el c6digo fuente. Si es importante guardarse un conjunto especifico de componentes que se utilicen (quizas en un formulario especifico), junto con 10s valores predefinidos para sus propiedades, escribir el codigo adicional puede merecer la pena.
--&---A *---:A .--
mentos Multiples (Multrple Doctment Interface, MD1). En este caso, usaremos cl estilo fsMDIForm para la ventana padre MDI (es decir, la ventana marco de la aplicacion MDI) y el estilo f s M D I C h i l d para la ventanas MDI hijo. Una cuarta opcion es el estilo fs S t a y O n T o p , que establece si el formulario ha de permanecer siemprc sobre todas las demas ventanas, a escepcion de algunas que puedan ser ventanas "fijadas por encima". Para crear un formulario superior (un formulario cuya ventana permanece siempre por encima), es necesario definir la propiedad F o r m S t y l e . como se indico anteriormente. Dicha propiedad causa dos efectos distintos, dependiendo del tip0 de formulario a1 que se aplique: El formulario principal de una aplicacion permanecera por encima de todas las demas aplicaciones (a menos que las demas aplicaciones tengan
tambien este mismo estilo de ventana). A veces. esto genera un efecto visual bastante desagradable, por lo que solo tiene sentido en programas de alerta para usos especiales. Un formulario secundario permanecera por encima de 10s demas formularios de la aplicacion a la que pertenecc. Pero las ventanas de otras aplicaciones no se veran afectadas. Esto se usa normalmente para barras de herramientas flotantes que deberian estar sobre la ventana principal.
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.
ADVERTENCIA: Definir la propiedad Border style en tiempo de diseiio no ~ r o d u c ninein efecto visible. De hecho. hav diversas ~ r o ~ i e d a d e s e a r 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 n - --: -..--- . -- _ > - r . mos el programa. r o r ejemplo, no poariamos reajusrar el ramano aer P - rormulario 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.
Y
~.-:---L--
-1
--
3-1
El estilo bssingle: Se puede usar para crear una ventana principal en la que no podamos modificar el tamaiio. Muchos juegos y aplicaciones basados en ventanas con controles (corno 10s formularios para introducir datos) utilizan este valor, simplemente porque no tiene sentido ajustar el tamaiio de estos formularios. Aumentar un formulario para ver un area vacia o reducir su tamaiio para que algunos componentes sean menos visibles no suele ayudar al usuario de un programa (aunque las barras de desplazamiento automaticas de Delphi resuelvan en parte este problema. El estilo bsNone: Se usa solo en situaciones muy especiales y dentro de otros formularios. Jamas se vera una aplicacion con una ventana principal que no tenga borde o titulo (except0 quizas como ejemplo en un libro de programacion para demostrar que no tiene sentido).
Los valores, bsToolWindow y bsSizeToolWin: Estan relacionados con el estilo especifico ampliado de Win32, ws ex Toolwindow. Este estilo transforma la ventana en un cuadro deherramientas flotante, con un
titulo en una fuente pequeiia y un boton de cierre. Para la ventana principal de una aplicacion no se deberia usar este estilo.
lag & u -fbs Iform border style, estilo del h d e del formuhrio). Asi n 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;
TRUCO: Siempre que bay6 quq fiferirse a otra unidRd de un pi@mha, hay que .colocq 1a correspamdiente sentencia udes en lrr3ec&n
irnp;lemsn$ation y no en la secci6n inter face, 3 se; podble, E s t ~ rrcelerie#pptoceso de compilaci6n. origins un cbdigo miis limpio (porque laa pnfdadses'que se incluyen esdn sep&adas de lasque incluie ~ e l ~ hyi ) evita emks de @qilacion ~irculaPde unidades. Para hacer referencia ti otr& archivos del Ynismo proyecto, tambitn se puede usar la opcion de m d FileNse UNt.
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
Mientras se ejecuta el ejemplo Blcons, se pueden fijar y eliminar facilmente 10s diversos elementos visuales del borde del formulario. Enseguida se vera que algunos de estos elementos se encuentran intimamente relacionados. Si eliminamos el menu de sistema, desapareceran todos 10s iconos del borde. Si eliminamos el boton de minimizar o el de masimizar, se pondran en gris. Si eliminamos ambos botones, desapareceran. Fijese en que ademas, en estos dos ultimos casos, 10s elementos correspondientes del menu de sistema se desactivan automaticamente. Este es el comportamiento estandar de cualquier aplicacion Windows. Cuando se han desactivado 10s botones de maximizar y minimizar, se puede activar el boton de ayuda. Actualmente en Windows 2000, si solo se ha inhabilitado uno de 10s botones de maximizar o minimizar, aparecera cl boton de ayuda per0 no sera funcional. Como metodo abreviado para conseguir este efecto, se puede hacer clic sobre el boton que esta dentro del formulario. Ademas, tambien se puede hacer clic sobre el boton despues de habcr hecho clic sobre el icono del menu de ayuda para que aparezca un mensaje de ayuda, como muestra la figura 7.3. Como funcion adicional, el programa muestra tambikn en el titulo el momento en que se activa la ayuda, controlando el evento O n H e l p del formulario.
Figura 7.3. El ejemplo Blcons. Al seleccionar el icono de ayuda dt borde y hacer clic sobre el boton. aparece la ayuda.
rn
ADVERTENCIA: Si se analiza la versi6n QBIcons, creada con CLX, se puede comprobar que un fallo de la biblioteca impide modificar 10s iconos de 10s bordes en tiempo de ejecuci6n. Las distintas configuraciones en tiempo de diseiio h n c i o n a r b completamente, asi que sera necesario modificar el programa antes de ejecutarlo si se quiere ver algun tipo de efecto. Este programa no hace nada en tiempo de ejecucion.
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 "CreateWindowM "CreateWindowEs". Se vera que la API y 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;
Para eliminar el titulo, es necesario cambiar el estilo solapado por un estilo contextual, puesto que de otro modo, el titulo se quedara adherido. Para aiiadir un titulo personalizado, hemos colocado una etiqueta alineada con el borde superior
del formulario y un pequeiio boton en la esquina superior. Se puede ver en tiempo de ejecucion en la figura 7.4
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;
La funcion G e t s y s temMetrics de la API utilizada en el listado anterior es una consulta al sistema operativo sobre el grosor vertical ( c y ) en piseles del borde que hay alrededor de una ventana con un titulo no redimensionable. Es importante realizar esta peticion cada vez porque 10s usuarios pueden personalizar la mayoria de estos elementos utilizando la ficha Apariencia de las opciones Propiedades de pantalla (en el Panel de Control) y otros parametros de Windows. En cambio, el boton pequeiio tiene una llamada a1 metodo C l o s e en su controlador del evento O n c l i c k . El boton se mantiene en su posicion aunque se ajuste el tamafio de la ventana, usando el valor LakTop, a k R i g h t ] para la propiedad A n c h o r s . El formulario tiene tambien restricciones de tamaiio, para que un usuario no pueda reducirlo en exceso.
Aid,
..I .. ..
..
I '-
,"On=
..
::::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;
Ahora empezaremos a recibir 10s eventos OnKeyPress y podremos realizar una de las tres acciones solicitadas por 10s tres botones especiales del grupo de radio. La accion depende del valor de la propiedad ItemIndex del componente grupo de radio. Esta es la razon por la que el controlador de eventos se basa en una sentencia case:
p r o c e d u r e TForml.FormKeyPress(Sender: TObject; begin c a s e RadioPreview.Item1ndex of var Key: Char);
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 );
documentado. Existen unos cuantos de estos memsajes, y resulta bastante . .- . - - - .. - . .interesante construir componentes avanzados parra cllos y usarlos para realizar una codificaicibn especial, pero Borland ja d s 10s ha descrito. Hay que resaltar que este estilo exacto de codificaciclu u a ~ a GU lucuaqsJca uu a esta disponible bqjo CLX.
-- ----
Para escribir texto en el titulo del formulario mediante el teclado, el programa aiiade sencillamente el caracter a1 Caption actual. Existen dos casos especiales mas. Cuando se pulsa la tecla Retroceso, se elimina el ultimo caracter de la cadena (a1 copiar al Caption todos 10s caracteres del Caption actual menos el ultimo). Cuando se pulsa la tecla Intro, el programa detiene la operacion, redefiniendo la propiedad ItemIndex del control del grupo de radio. Veamos el codigo:
2 : // e s c r i b i r e n e l t i t u l o begin i f Key = # 8 t h e n // r e t r o c e s o : e l i m i n a r u l t i m o c a r a c t e r Caption : = Copy (Caption, I, Length (Caption) - 1) e l s e i f Key = #13 t h e n // i n t r o : d e t i e n e la o p e r a c i o n RadioPreview.ItemIndex : = 0 e l s e // o t r a c o s a : a d a d e c a r a c t e r
Por ultimo, si sc selecciona el ultimo elemento de radio, el codigo verifica si el caracter es una vocal (buscando su inclusion en un "conjunto de vocales" fijo). En cste caso, el caracter se omitc:
3 : / / omite las vocales if Key i n ['A', 'E', ' I t , Key : = # O ;
'O',
'U'] then
..-
&- I m
el uso 6el teclado' usar if ratbn es& ' bien, per0 suele ser m b lento. Si se es habil con el teclado, no se querra utilizar el raton para arrastrar una palabra de un texto; se utilizarb las teclas de rn~todd abreviado para copiar y pegar el texto sin separar las manos del teclado. - estas razones, siempre deberia establecer un orden de tabulacion coPar rrecto para 10s componentes de un formulario. Hay que recordar aiiadir teclas para 10s botones y para acceder a 10s elementos de menu mediante el
.^^l^l^ &:I:-....
oars sop&.a;
--
~ U I G V I ~ U U para U~GIUIIGS
-L-.^-.:^l^
----
--^:^-^-
uc IIIG~U y w s a s
J^
----
-.
asi.
,, a , , , :
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, , -I : ,h -, , , ~ l u c u ~ i a s u a l c a . El L I ~ U uo ~uulgu u v u j u UIIGGLU r w uc 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.
VU. 'I
bC111VC1a.
" L
b:,,
mado MouseOne en la version VCL y QMouseOne en la version CLX. Muestra en el titulo del formulario la posicion actual del raton:
procedure TMouseForm.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin // r n u e s t r a 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 , [ X , Y]) ; end;
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 ;
6 no incluye una matriz Pixels. En carnbio, se puede llarnar a1 mttodo Drawpoint tras haber fijado el color adecuado para el Iaviz. como hemos . becho en el ejemplo QMouseOne. Kylix 2 y Dellphi 7 vuelven a introducir la propiedad de matriz Pixels.
-
Sin embargo, la caracteristica mas importante de este ejemplo es el soporte de arrastre directo del raton. A1 contrario de lo que se podria pensar, Windows no posee soporte del sistema para el arrastre, que esta implementado en la VCL
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:
Una accion importante de este metodo es la llamada a la funcion setcapture de la API, obtenida a1 activar la propiedad Capture del objeto global Mouse.
Ahora, aunque el usuario mueva el raton fuera de la zona de cliente, el formulario recibira igualmente todos 10s mensajes relacionados con el raton. Se puede comprobar moviendo el raton hacia la esquina superior izquierda de la pantalla. El programa muestra coordenadas negativas en el titulo.
-
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;
En Windows 2000 (y otras versiones), la funcion D r a w F o c u s R e c t no dibuja rectangulos con un tamaiio ncgativo, asi que el codigo del programa se ha preparado para comparar la posicion actual con la posicion inicial del arrastre, guardada en el punto d r a g s t a r t . Cuando se suelta el boton del raton, el programa termina la operacion de arrastre redefiniendo la propiedad C a p t u r e del objeto M o u s e , que llama internamente a la funcion R e l e a s e c a p t u r e de la API y definiendo el valor del campo fD r a g g i n g como F a l s e :
procedure TMouseForm.F'ormMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if fDragging then begin Mouse.Capture : = 0; / / l l a m a a R e l e a s e C a p t ~ i r e fDragging : = False; Invalidate; end ; end ;
La llamada final, I n v a l i d a t e , desencadena una operacion de pintado y ejecuta el siguiente controlador de eventos O n p a i n t :
procedure TMouseF'orm.FormPaint(Sender: TObject); begin C a n v a s - R e c t a n g l e (fRect.Left, fRect.Top, f R e c t - R i g h t , fRect .Bottom) ; end;
Esto transforma el resultado del formulario en permanente, aunque se oculte tras otro formulario. En la figura 7.6 aparece la version anterior del rectangulo y una operacion de arrastre en marcha.
Figura 7.6. El ejernplo MouseOne usa una linea de puntos para dibujar, durante la operacion de arrastre, un rectangulo.
El mttodo Invalidate: Informa a Windows de que hay que volver a pintar la superficie total del formulario. Lo mas importante es que Invalidate
no desencadena una operacion de pintado de forma inmediata. Windows almacena simplemente la solicitud y respondera solo despues de que se haya ejecutado por completo el procedimiento actual (a no ser que se llame a Application. ProcessMessages oUpdate) y e n cuanto no haya otros eventos pendientes en el sistema. Windows retrasa deliberadamente la operacion de pintado porque es una de las operaciones para las que mas tiempo se necesita. A veces, con dicho retraso, solo es posible pintar el formulario despues de que se hayan producido diversos cambios, lo cual evita que haya muchas llamadas consecutivas a1 metodo (lento) de pintar. El mCtodo Update: Solicita a Windows que actualice el contenido del formulario, pintandolo de nuevo inmediatamente. Sin embargo, hay que recordar que esta operacion se realizara solo si existe una zona no valida. Esto ocurre si se acaba de llamar a1 metodo Invalidate o como resultad0 de una operacion realizada por el usuario. Si no existe una zona no valida, una llamada a Update no tiene ningun efecto. Por esa razon, es frecuente ver una llamada a Update inmediatamente despues de una Ilamada a Invalidate.Como hacen exactamente 10s dos mktodos de Delphi: Repaint y Refresh.
El mCtodo Repaint: Llama a Invalidate y, a continuacion, a Updat e. Como consecuencia, activa el evento onpaint inmediatamente. Existe una version ligeramente diferente de este metodo denominada Refresh. El hecho de que existan dos metodos para la misma operacion se remonta a 10s dias de Delphi 1, cuando ambos eran sutilmente distintos entre si.
Cuando hay que pedir a1 formulario que se vuelva a pintar, normalmente deberiamos llamar a Invalidate,siguiendo el enfoque estandar de Windows. Esto es importante sobre todo cuando hay que solicitar dicha operacion con bastante frecuencia, porque si Windows emplea demasiado tiempo en actualizar la pantalla, las solicitudes de pintado se pueden acumular en una sencilla accion de pintado. El mensaje wm Paint de Windows es un mensaje de baja prioridad; si hay una solicitud pend&e, per0 hay mas mensajes esperando, 10s otros mensajes se controlan antes de que el sistema lleve a cab0 la accion de pintado. Por otra parte, si se llama varias veces a1 metodo Repaint, habra que volver a pintar la pantalla cada vez antes de que Windows pueda procesar otros mensajes y debido a que para las operaciones de pintado se realizan calculos de forma exhaustiva, la capacidad de respuesta de la aplicacion sera menor. Sin embargo. si queremos que la aplicacion pinte de nuevo una superficie lo antes posible hay que llamar a Repaint .
NOTA: Otra consideracih imprtantq es que durante una.operacih de pintado Windows vuelve a dibujs solo b denominada 'ked6riactudiidam, para acelerar dicha uperaeib. Pbr es& i$i;bn, si aiempre se inv562ida
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: , 51UIU el codino fuente aue su~uestamente deberia actualizarl~. . vywawvu 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
~-
Figura 7.7. El resultado de CkKeyHole, que rnuestra el efecto de las nuevas propiedades Transparentcolor y AlphaBlend y de la API AnimateWindow.
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).
La propiedad entera SnapBuffer: Determina la distancia con respecto a 10s bordes que se considera cercana. Aunque no es una caracteristica particularmente vistosa, es practica para permitir que 10s usuarios ajusten 10s formularios en un lateral de la pantalla y aprovechen toda la superficie de la pantalla; resulta particularmente practico para aplicaciones con multiples formularios visibles a1 mismo tiempo. Pero hay que tener cuidado y no usar un valor demasiado grande para la propiedad SnapBuff e r (algo tan alto como toda la pantalla) o se confundira a1 sistema.
TRUCO: En Windows, tambikn se puede producir resultados y capturar entradas desde el Area no de cliente del formulario (es decir, de su borde). Pintar sobre el borde y recibir una entrada cuando se hace clic sobre el son asuntos complejos. Si interesa, se puede buscar en el archivo de ayuda la descripcion de mensajes de Windows como w m N C P a i n t , w m N C C a l c S i z e y wm N C H i t T e s t , y la serie de mekajes no de clienG relacionados con la gntrada de raton, corno wrn NCLButtonDown. La dificultad de esta ttcnica reside en combinar el cgdigo propio con el comportarniento predefinido de Windows.
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 ...
...
El unico objetivo de este programa es actualizar 10s valores del cuadro de herramientas cada vez que se modifica el tamaiio del formulario o que este se desplaza (como muestra la figura 7.8). La primera parte es muy sencilla. Se puede controlar el evento O n R e s i z e del formulario y copiar simplemente un par de valores en ambas etiquetas. Estas etiquetas forman parte de otro formulario, por lo que sera necesario aiiadirles como prefijo el nombre de la instancia del formulario, S t a t us :
procedure TForml. FormResize (Sender: TObject) ; begin Status.Label3.Caption : = IntToStr(C1ientWidth); Status.Label4.Caption : = IntToStr(HorzScrollBar.Position); end;
r1
I
Figura 7.8. El resultado del ejemplo Scrolll.
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
no esiste un evento O n S c r o l l para formularios (aunque 10s componentes ScrollBar independientes tengan uno). Omitir este evento time sentido, porque 10s formularios de Delphi gestionan las barras de desplazamiento de un mod0 muy potente. En Windows, por contraste, las barras de desplazamiento son elementos de un nivel muy bajo, lo que requiere mucha codificacion. Manejar el evento de desplazamiento solo tiem sentido en casos especiales, como cuando se desea seguir la pista con precision de las operaciones de desplazamiento realizadas por un usuario. Veamos el codigo que hay que escribir. Primero, aiiadimos una declaracion de metodo a la clase y la asociamos con el mensaje de desplazamiento horizontal de Windows (wm-H S c r o l l ) . A continuacion, escribimos el codigo de este procedimiento, que es casi el mismo que el del metodo F o r m R e s i z e visto antes:
public procedure FormScroll wm_HScroll; (var ScrollData: TWMScroll) ; message
procedure TForml.FormScroll (var ScrollData: TWMScroll); begin inherited; Status.Label3.Caption : = IntToStr(C1ientWidth); Status.Label4.Caption : = IntToStr(HorzScrollBar.Position); end;
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 desa p l a z ~ e n t o 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
cuando se reduce el tamaiio del formulario, la barra de herramientas vuelve a aparecer. Esta caracteristica resulta sobre todo interesante cuando la propiedad A u t o s c r o l l del formulario se establece como T r u e . En este caso, las posiciones extremas de 10s controles situados mas a la derecha y mas abajo se copian automaticamente en las propiedades Range de las dos barras de desplazamiento del formulario. El desplazamiento automatic0 funciona bien en Delphi. En el ejemplo anterior, el tamaiio virtual del formulario se estableceria en el borde derecho del ultimo cuadro de lista. Esto se definia con 10s siguientes atributos:
object ListBox6: TListBox Left = 832 Width = 145 end
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
Como una mejor alternativa, en lugar de calcular la coordenada correcta para cada operacion de salida, podemos llamar a SetWindowOrg Ex de la API para desplazar el origen de las coordenadas de la propia Canvas. De este modo, nuestro codigo de dibujo se referira directamente a las coordenadas virtuales per0 las lineas se mostraran correctamente:
procedure TForm2.FormPaint(Sender: TObject); begin SetWindowOrgEx (Canvas.Handle, HorzScrollbar.Position, VertScrollbar.Position, nil);
// 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.
NOTA: El ajuste a escala del formulario se cafcula segun la diferencia entre la altura de la fuente en tiempo de ejecucion y la altura de la fuente en tiempo de disefio. La escala garantiza que 10s controles de edicion y otros controles sean lo suficientemente grandes como para mostrar su texto, utilizando las preferencias del usuario sobre fuentes sin recortar el texto. La - - - - 1- 2 - 1 rvrrnu~ar~o 1-2--escala uel r rarnulen se auapra, ---- 1- - - z - : 1rnponanLe es que 10s peru lo mas ---A--L:L->--A---A-
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);
Este metodo almacena el valor de entrada actual en el campo privado A m o u n t s c a l e d del formulario y activa el boton Restore, desactivando el que estaba pulsado. A continuacion, cuando el usuario pulsa el boton Restore, se ajustara la escala a1 contrario. A1 tener que restaurar el formulario antes de realizar otra operacion de ajuste de escala, evitamos una acumulacion de errores de redondeo. Tambien hemos aiiadido una linea para definir la altura del componente
UpDown como igual a la del cuadro de cdicion a1 que se encuentra conectado. Esto cvita pequcfias difercncias entrc ambos. debido a problemas dc escalado del control UpDown.
- -- 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.
podemos cambiar o comprobar algunas de las propiedades o campos iniciales del formulario. La sentencia responsable de la creacion del formulario esta en el archivo fuente del proyecto:
begin Application-Initialize; Application. CreateForm(TForm1, Forml) ; Application.Run; end.
Para saltarse la creacion automatica del formulario, se puede modificar este codigo o utilizar la ficha Forms del cuadro de dialogo Project Options (vCase figura 7.10). En este cuadro de dialogo, se puede decidir si el formulario deberia crearse de manera automatica. Si desactivamos la creacion automatica, el codigo de inicializacion del proyecto se transforma en el siguiente:
begin Applications.Initialize; Application.Run; end.
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 rh adaptable de Delphi n 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 o fuente 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;
Antes de la llamada a1 m e t o d o c r e a t e de la clase basica, las propiedades del formulario no se han cargado todavia y 10s componentes internos no estan disponibles. Por esa razon, la tecnica estandar consiste en llamar a1 constructor de la clase basica primcro y, a continuacion, hacer las operaciones personalizadas.
-
--
- -
.-
..
. 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:
Si O n C l o s e Q u e r y indica quc el formulario deberia cerrarse, se efectua una llamada a1 evento o n c l o s e . El tercer paso consiste en llamar a1 evento OnDes t r o y , que es el contrario del evento OnCrea t e y se usa por lo general para eliminar objetos relacionados con el formulario y liberar la memoria correspondientc. -- . - - ..- -- -- - -- - . -NOTA: Para ser m b precisos, el metodo Be foreDestruct ion genera un evento OnDestroy antes de llamar a1 destructor Destroy.Es deck, L J-P--:>1a mmo~ que nayamos aerln~aola ---- l - 2 - J u1aLrear;euraer como propieaaa
-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~ libcra su memoria y por ultimo finaliza sc 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.
-
NOTA: C u a n d o un usuario cierra Windows. s e activa el evento OnCloseQuery que puede usar un programa para detener el proceso de c i e r r e . En ese c a s o , n o se llama a1 evento O ~ l C s e a u n q u e o
7
- -.
- 3
en el buen camino, aunque la descripcion no es precisa. En Delphi (igual que en Windows), podemos tener tambien cuadros de dialogo no modales y formularios modales. Tenemos que considerar dos elementos diferentes: el borde del formulario y su interfaz de usuario establecen si su apariencia es la de un cuadro de dialogo; y el uso de dos metodos diferentes (Show o ShowModal) para mostrar el formulario secundario determina su comportamiento (no modal o modal).
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-
lario secundario consiste sencillamente en ocultarlo, por lo que el formulario secundario no se destruye cuando se cierra, sino que se mantiene en memoria (no es el mejor enfoque) y esta disponible si queremos volver a mostrarlo.
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.
Cuando se crean varias copias de un formulario de manera dinamica, hay que recordar destruir cada objeto de formulario cuando se cierra, controlando el evento correspondiente:
procedure TForm3.FormClose(Sender: TObject; var Action: TCloseAction) ; begin Action : = caFree; end :
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.
Como la llamada a ShowModal puede producir una excepcion, deberiamos escribirla dentro de un bloque try seguido de un bloque finally para asegurarnos de que se libera la memoria asignada para el mismo. Normalmente dicho bloque incluye tambien codigo que inicializa el cuadro de dialogo antes de mostrarlo y codigo que extrae 10s valores definidos por el usuario antes de destruir el formulario. Los valores finales son de solo lectura si el resultado de la funcion ShowModal es mrOK. La situacion es algo mas compleja cuando queremos mostrar una sola copia de un formulario no modal. Tenemos que crear el formulario, si no existe todavia, y despues mostrarlo:
if not Assigned (Form2) then Form2 : = TForm2 .Create (Application) ; Form2.Show;
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) ;
TObject; var Action:
begin
Fijese en que despues de destruir el formulario, la variable global Form2 se define como n i l . Sin este codigo, a1 cerrar el formulario se destruiria su objeto, per0 la variable Form2 remitiria aun asi a la posicion de memoria original. En cste punto, si se intenta mostrar el formulario una vez mas con el metodo b t n S i n g l e C l i c k , funcionara la prueba i f n o t A s s i g n e d ( ) , puesto que sencillamente verifica si la variable Form2 es n i l . El codigo no crea un objeto nuevo y el metodo show (invocado sobre un objeto inexistente) producira un error de memoria del sistema. Como esperimento, se puede generar este error eliminando la ultima linea del listado anterior. Como se ha visto, la solucion es asignar n i l a1 objeto Form2 antes de destruir el objeto, de manera que un codigo escrito correctamente sera capaz de "ver" que se debe crear un nuevo formulario antes de utilizarlo. Una vez mas. esperimentar con el ejemplo Mu1t i W i n / Q M u l t i W i n puede ser muy util para comprobar distintas situaciones.
NOTA: Conviene definir la variable del fonnulario como nil (y funciona) si solo va a haber una instancia del fonnulario presente en un momento dado. Si queremos crear diversas copias de un formulario, tendremos que 11tilivar n t r a c t b ~ n i r a c a r a m a n t e n e r wn c o r n ~ i m i e n t n n Ae lac m i c m a c AApmas, hay que tener en cuenta que en este caso, no podemos usar el procedimiento FreeAndNil, porque no podemos llamar a Free sobre Form2.
1 ,, A ,
~a
I ~ L U I GJ ~ I
a ,
, . a
1 1 ~ 1 4 CULCGJ UG ~ 1 1 ~
,, , + a
,A,a . ,,,, , +. :
U CGIIIIIUGII UG G
A,
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.
$1-
-..-__
La unica caracteristica interesante de este formulario en el ejemplo VCL es el uso del componente ComboBoxEx,que esta conectado a la misma ImageList utilizada por el control ListView para controlar el formulario principal. Los elementos de la lista desplegable, usados para escoger un tip0 de referencia, incluyen tanto una descripcion textual como la imagen correspondiente. Como ya se ha comentado, este cuadro de dialogo se usa en dos casos distintos. El primer0 ocurre cuando el usuario selecciona File>Add Items en el menu:
procedure TForml.AddItemslClick(Sender: TObject); var NewItem: TList Item; begin FormItem.Caption : = 'New Item'; FormItem.Clear; if FormItem.ShowModal = mrOK then begin NewItem : = ListViewl.1tems.Add; NewItem.Caption : = FormItem.EditReference.Text; NewItem.ImageIndex : = FormItem.ComboType.ItemIndex; NewItem.SubItems.Add (Form1tem.EditAuthor.Text); NewItem.SubItems.Add (Form1tem.EditCountry.Text); end ; end ;
Ademas de definir el titulo correct0 para el formulario, este procedimiento necesita iniciar el cuadro de dialogo porque introducimos un nuevo valor. Sin embargo, si el usuario hace clic sobre OK, el programa aiiade un nuevo elemento
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;
'';
Para editar un elemento ya existente, es necesaria una tecnica ligeramente distinta. En primer lugar, 10s valores actuales se llevan tambien a1 cuadro de dialog0 antes de que aparezca. En segundo lugar, si el usuario hace clic sobre OK, el programa modifica la lista actual en lugar de crear una nueva. Veamos el codigo:
procedure TForml.ListViewlDblClick(Sender: TObject); begin if ListViewl.Selected <> nil then begin // i n i c i o d e l d i d l o g o FormItem.Caption : = ' E d i t I t e m ' ; FormItem.EditReference.Text : = ListViewl.Se1ected.Caption; FormItem.ComboType.ItemIndex : = ListViewl.Selected.Image1ndex; Form1tem.EditAuthor.Text : = ListViewl.Se1ected.SubItems Form1tem.EditCountry.Text : = ListViewl.Selected.SubItems
[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.
- . NOTA: Lo que sucede internamente cuando el usuario hace clic sobre el boton OK o Cancel del cuadro de dialogo es que un cuadro de dialogo modal se cierra estableciendo su propiedad ModalResul t, y devolviendo el valor de esta propiedad. Se puede indicar el valor de retorno usando la propiedad ModalResul t del boton. Cuando el usuario haga clic sobre el boton, su valor Modal Resul t se copiarh a1 formulario, que lo que cierra el formulario y devuelve el valor como el resultado de la funcion ShowModal.
. .- - - ...
.
-
..
procedure TForml.LabelClick(Sender: TObject); var I: Integer; begin for I : = 0 to Componentcount - 1 do if (Components[I] is TLabel) and (Components[I] .Tag = 1) then TLabel (Components[I]) .Font.Color : = clBlack; / / define el color de la etiqueta pulsada como rojo [Sender as TLabel) .Font.Color := clRed; end ;
.Bob
C njy
Jane Jelf John
Sample label
Figura 7.12. Los tres forrnularios del ejernplo DlgApply en tiempo de ejecucion (un formulario principal y dos cuadros de dialogo).
El segundo metodo comun a todas las etiquetas es el controlador del evento OnDoubleClic k . El mitodo LabelDoubleClick selecciona el titulo de la etiqueta en uso (indicada por el parametro Sender) en el cuadro de lista del dialogo y despues muestra el cuadro de dialogo modal. Si el usuario cierra el cuadro de dialogo haciendo clic sobre OK y hay seleccionado un elemento de la lista, este se copia de nuevo en el titulo de la etiqueta:
procedure TForml.LabelDoubleClick(Sender: TObject); begin with ListDial .Listbox1 do begin / / selecciona el nombre actual del cuadro de lista ItemIndex := Items.IndexOf (Sender as TLabel) .Caption); / / muestra el cuadro de didlogo modal, verificando el valor de retorno if (ListDial.ShowModa1 = mrOk) and (ItemIndex >= 0 ) then // copia el elemento seleccionado en la etiqueta (Sender as TLabel) .Caption : = Items [ItemIndex]; end ; end;
Por el contrario, el cuadro de dialogo no modal tiene mucho codigo en su interior. El formulario principal simplemente muestra el cuadro de dialogo cuando se pulsa el boton Style (fijese en que el titulo de este boton termina con tres puntos para indicar que lleva a un cuadro de dialogo), llamando a su metodo s h o w . El cuadro de dialogo aparece en la anterior figura 7.12. Existen dos botones, Apply y Close, que sustituyen a 10s botones OK y Cancel en un cuadro de dialogo no modal. (El mod0 mas rapido de obtener dichos botones es seleccionar el valor bkOK o b k C a n c e l para la propiedad K i n d y despues editar la propiedad C a p t i o n . ) De vez en cuando, puede que vea un boton Cancel que funciona como un boton Close, pero el boton OK no suele tener significado en un cuadro de dialogo no modal. En su lugar, uno o mas botones podrian realizar acciones especificas sobre la ventana principal, como Apply, Change Style, Replace, Delete y muchos mas. Si el usuario hace clic sobre una de las casillas de verificacion del cuadro de dialogo no modal, el estilo del texto de la etiqueta de muestra que esta en la parte inferior cambia en funcion de la eleccion. Para ello, se aiiade o elimina el indicador especifico que muestra el estilo, como en el siguiente controlador del evento O n C l i c k:
procedure TStyleDial.ItalicCheckBoxClick(Sender: TObject); begin if 1talicCheckBox.Checked then LabelSample.Font.Style : = LabelSample.Font.Style + [fsItalic] else LabelSarnple.Font.Sty1e : = LabelSarnple.Font.Sty1e [ f s I t a l i c ]; end;
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;
Como alternativa, en lugar de hacer referencia directamente a cada etiqueta, se puede buscar llamando a1 metodo F i n d c o m p o n e n t del formulario, pasando el
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.
El componente FontDialog: se puede usar para mostrar y seleccionar todos 10s tipos de fuentes, fuentes que se pueden usar tanto en la pantalla como en la impresora seleccionada (WYSIWYG), o solo fuentes TrueType. Se puede mostrar u ocultar la parte relacionada con 10s efectos especiales y obtener diferentes versiones definiendo su propiedad o p t i o n s . Tambien se puede activar un boton Apply ofreciendo sencillamente un controlador de eventos para su evento OnApp l y y usando la opcion f dApplyBut t o n . Un cuadro de dialogo de fuentes con un boton Apply (vease la figura 7.13) se comporta casi como un cuadro de dialogo no modal (pero no lo es). El componente ColorDialog: Se usa con distintas opciones para mostrar el dialogo totalmente abierto a1 principio o evitar que se abra por completo. Dichas configuraciones se corresponden a 10s valores c d ~ ul l p e n ' o ~ c d P r e v e n t F u l l O p e n de la propiedad O p t i o n s .
0 Tfehuchet US 0 Tunga
0 Verdana
Elector - -
A
I
IE
r Eismph -
16
A
I
Figura 7.13. El cuadro de dialogo de selection de fuentes con un boton Apply (Aplicar).
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 ;
El temporizador se activa justo antes de finalizar el metodo. Despues de que su intervalo de tiempo haya pasado (en el ejemplo, tres segundos) se activa el evento OnTimer y el formulario inicial lo controla cerrandose y destruytndose, mediante Close y despues Release.
. -.. --- -..----~-. NOTA: El mitodo Release de un formulario es similar a1 m M o Free de 10s objetos, pero la destruction del formulario se retrasa hasta que todos 10s cont*olado;es de eventos haya completado su ejecuci6n. US& Free dentro de un formularia podria provocar- una-violaci6n de acceso,- .que el ya . -. . -. . - . codigo rnterno que disparo el controlador de eventos podria referirse de nuevo a1 objeto fomulario.
~
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
Busqueda de las instancias previas de una aplicacion. Aplicaciones MDI. Herencia de formularios visuales Marcos. Formularios base e interfaces.
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 On~dle utilizado 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);
Como se lfe en este codigo estandar, el objeto Application puede crear formularios, definiendo el primero como MainForm (una de las propicdades de Application), y cerrar toda la aplicacion cuando se destruye dicho formulario principal. La ejecucion del programa esta incluida en el mCtodo Run, que contiene el bucle del sistema que procesa 10s mensajes del sistema. Este bucle continua hasta que la ventana principal (la ventana creada en primer lugar) esta cerrada.
TRUCO: El formulario principal no es necesariarnente el que se crea en primer lugar, per0 es el primero que se crea con la 1lamadaApplicat i o n . C r e a t e Form.
El bucle de mensaje de Windows contenido en el metodo Run envia 10s mensajes del sistema a la ventana de aplicacion adecuada. Cualquier aplicacion Windows necesita un bucle de mensaje, per0 en Delphi no tenemos que escribirlo porque el objeto Application ofrece uno predefinido. Aunque Qta es la funcion principal del objeto Application, tambien controla otras funciones interesantes: Las sugerencias. El sistema de ayuda, que incluye la capacidad de definir el tipo de visor de ayuda.
Information general sobre la aplicacion, como MainForm. el nombre del archivo ejecutable y su ruta (ExeName), el icono y el titulo que aparecen en la barra de tareas de Windows y cuando buscamos en las aplicaciones en ejecucion con las teclas AIt-Tab.
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.
El segundo formulario tiene una etiqueta y codigo similares. El formulario principal tambien muestra el estado de toda la aplicacion. Utiliza un componente A p p l i c a t i o n E v e n t s para controlar 10s eventos O n A c t i v a t e y OnDeact ivate del objeto Appl icat ion.Existen dos controladores de eventos similares a 10s dos anteriores, con la unica diferencia de que modifican el texto y color de una segunda etiqueta del formulario y que uno de ellos emite un sonido. A1 ejecutar este programa, veremos si la aplicacion esta activa y, de ser asi, cual de sus formularios es el activo. Si nos fijamos en el resultado (vease la figura 8.1) y escuchamos el sonido, entenderemos como se desencadena cada uno de 10s eventos de activacion en Delphi.
IntToStr
Figura 8.1. El ejemplo ActivApp muestra si la aplicacion esta activa y cual de sus formularios esta activo.
Tscreen informa tambien sobre el numero y resolucion de 10s monitores de un sistema de varios monitores. Sin embargo, nos centraremos en la lista de formularios almacenada por la propiedad Forms del objeto screen,el formulario superior que indica la propiedad ActiveForm y el evento relacionado OnActiveFormChange. Observe que 10s forrnularios a 10s que se refiere el objeto screen son 10s formularios de la aplicacion y no 10s del sistema. Estas funciones se demuestran mediante el ejemplo Screen, que mantiene una lista de 10s formularios actuales en un cuadro de lista. Esta deberia actualizarse cada vez que se crea un nuevo formulario, se destruye un formulario existente o cambia el formulario activo del programa. Para ver su funcionamiento, se pueden crear formularios secundarios haciendo clic sobre el boton New:
procedure TMainForm.NewButtonClick(Sender: TObject); var NewForm: TSecondForm; begin // crea un forrnulario nuevo, define su titulo y lo ejecuta NewForm := TSecondForm-Create (Self) ; Inc (nForms); NewForm-Caption : = 'Second ' + IntToStr (nForms); NewForm-Show; end ;
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;
El codigo usado para cubrir el cuadro de lista Forms esta en un segundo procedimiento, FillFormsList,que tambien esta instalado como controlador de eventos para el evento OnAct iveFormChange del objeto Screen:
procedure TMainForm. FillFormsList var
(Sender: TObject) ;
I: Integer;
begin / / ornite codigo en la fase de destruccidn if Assigned (FormsListBox) then begin
FormsLabel.Caption : = 'Forms: ' + IntToStr (Screen.Formcount) ; FormsListBox.Clear; // escribe un nombre de clase y un titulo de forrnulario en el c~ladrode lista for I : = 0 to Screen.FormCount - 1 do FormsListBox. Items .Add (Screen. Forms [I] .ClassName + ' - ' + Screen.Forms [I] .Caption) ; ActiveLabel.Caption : = 'Active Form : ' +
Screen.ActiveForm.Caption; end; end;
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 _ u nc; o s e oer rormulario principal y asignar n 1 1 a s; c r e e n . OnActiveFormChange.
.
P.
. 1
. -
.-
.-
El metodo Fill FormsList rellena el cuadro de lista y define un valor para las dos etiquetas que estan sobre 61 para mostrar el numero de formularios y el
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.
Aclive Farn : S e c d 3
M I
Farns: 4
TSecondForm . Second 3 TSecondForm - Second 2 TSecondForrn .Second 1 TMa~nForrn Sc~een Info
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 (var Message: TMessage);
begin
FillFormsList
end:
( S e l f );
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) ;
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~ularios secundarios 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 ;
hacer que 10s formularios secundarios avisaran a1 principal cuando hiran destruidos. FreeNot i f i c a t i o n tecibe como parametro el componente del que tiene que notificar que ha sido destruido. Este metodo es utitizado ------l---L--- ~ -1- ----- 11--1----1---generalmeme pur I - - u uesarrwauores ue curnpunenies para cunecmr curns ponentes de diferentes formularios o modulos de datos de forma segura.
El ultimo cambio aiiadido a ambas versiones del programa es sencillo: cuando se hace clic en un elemento del cuadro de dialogo, el formulario correspondiente se activa mediante el metodo B r i n g T o F r o n t . Pero esta version tiene un pequeiio fallo, si se hace clic en el cuadro de dialogo cuando el formulario principal no esta activo, primer0 se activa este y despues se reordena el cuadro de dialogo; de este mod0 puede ocurrir que seleccionemos un formulario distinto del esperado. Este error del programa es un ejemplo de 10s riesgos de actualizar informacion dinamicamente y permitir que el usuario trabaje con ella a1 mismo tiempo.
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.
que haya pasado un tiempo establecido, el sistema interrumpe la aplicacion actual e inmediatamente pasa el control a la siguiente en la lista. El primer programa se reanudara solo despues de que haya pasado el turno de cada aplicacion. A esto se le llama multitarea no cooperativa. Asi, una aplicacion que realice una operacion para la que emplee mucho tiempo en un controlador de eventos no evita que el sistema funcione correctamente (porque otros procesos tienen su porcion de tiempo de procesador), per0 normalmente no puede pintar ni siquiera de nuevo sus propias ventanas correctamente, lo cual causa un efecto horroroso. Si no se ha esperimentado este problema, podemos hacer la siguiente prueba: Escribimos un bucle consumidor de tiempo que se ejecute al hacer clic en un boton; intentarnos mover el formulario o mover otra ventana encima de el. El efecto es realmente molesto. Si aiiadimos la llamada Application. ProcessMessages dentro del bucle veremos que la opcracion se vuelve mucho mas lenta, pero el formulario se refresca inmediatamentc. Como cjemplo del uso de Application. ProcessMessages dentro de un bucle consumidor de tiempo, podeinos acudir a1 ejemplo BackTask. Este es el codigo que utiliza esta aproximacion:
procedure T F o r m l . B u t t o n 2 C l i c k ( S e n d e r : TObject); var I, Tot: Integer; begin Tot : = 0; f o r I : = 1 t o Max do begin i f IsPrime ( I ) then Tot : = Tot + I; ProgressBarl.Position : = I * 100 div Max; App1ication.ProcessMessages; end; ShowMessage (IntToStr (Tot)) ; end;
-1
L!
Si una aplicacion ha respondido a sus eventos y esta esperando su turno para procesar mensajes, no tiene oportunidad de recuperar el control hasta que recibe otro mensaje (a no ser que utilice multiples hilos). Esta es una razon para el uso de un temporizador, un componente del sistema que envia un mensaje a la aplicacion siempre que se cumple un interval0 de tiempo dado. La utilizacion de un temporizador es la unica manera de hacer que una aplicacion realice operaciones
automaticamente de forma regular, incluso cuando el usuario esta ausente o no esta usando el programa (y, por lo tanto, no esta procesando ningun evento). Cuando hablamos de eventos, hay que recordar que 10s eventos de entrada (generados mediante el raton o el teclado) suponen solo un pequefio porcentaje del total del flujo de mensajes de una aplicacion Windows. La mayoria de mensajes son 10s internos del sistema o 10s intercambiados entre diferentes controles y ventanas. Incluso, una operacion de entrada tan familiar como un clic de raton puede derivar en un gran numero de mensajes, la mayoria de 10s cuales son mensajes internos de Windows. Esto puede comprobarse utilizando la utilidad W inS ight incluida en Delphi. En Wins ight,elegiremos ver las trazas de 10s mensajes ( M e s s a g e Trace) y seleccionaremos 10s mensajes de todas las ventanas. Haremos clic sobre el boton S t a r t y, despues, realizaremos algunas operaciones con el raton. Se mostraran cientos de mensajes en pocos segundos.
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);
El metodo E x e c u t e declarado como un procedimiento abstracto virtual, debe ser redefinido en cada hilo. ~ s t contiene el c6digo principal del hilo. es decir. el e codigo que habitualmente se utiliza en una funcion de hilo a1 usar las funciones del sistema. El metodo S y n c h r o n i z e se utiliza para evitar el acceso concurrente a componentes VCL. El codigo VCL sc cjecuta dentro del hilo principal del programa, por ello. es necesario sincronizar el acceso a 10s componentes VCL para cvitar 10s problemas de reentrada (errores por la reentrada de una funcion antes de completar una ejecucion previa) y el acceso concurrente a recursos compartidos. El unico parametro de S y n c h r o n i z e es un metodo sin parametros, normalmente un mctodo de la misma clase hilo. Dado que no se le pueden pasar parametros a este metodo, habitualmente se guardan algunos valores entre 10s datos del objeto hilo en el metodo E x e c u t e y se usan esos valores en 10s metodos sincronizados.
NOTA: Delphi 7 incluye dos nuevas versiones de S y n c h r o n i z e que permiten sincronizar un metodo con el hi10 principal sin necesidad de llamarlo desde el objeto hilo. Am'bos metodos sobrecargados, s y n c h r o n i z e y S t a t i c s y n c h r o n i z e son metodos de 1 a c l a s e T T h r e a d y requieren un hilo como parametro.
Otro mod0 de evitar conflictos es utilizar las tecnicas de sincronizacion que ofrccc cl sistema operativo. La unidad S y n c O b j s define unas pocas clases VCL para algunos de cstos objetos de sincronizacioi~ bajo nivel, tales como eventos de (con las clases T E v e n t y T S i n g l e E v e n t ) y secciones criticas (con la clase T C r i t i c a l s e c t i o n ) . (Los eventos de sincronizacion no deben de confundirse con 10s cucntos dc Dclphi. dado que ambos conceptos no estan relacionados.)
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 ;
El metodo E x e c u t e es muy similar a1 codigo usado para 10s botones en el ejemplo Backstack presentado anteriormente. La unica diferencia esta en la llamada final a S y n c h r o n i z e , como puede comprobarse en el siguiente fragmento:
procedure TPrimeAdder.Execute; var I, Tot: Integer; begin Tot : = 0; f o r I : = 1 t o FMax do begin i f IsPrime (I) then Tot : = Tot + I; i f I mod (Max d i v 100) = 0 then begin FPosition : = I * 100 d i v Max; Synchronize (UpdateProgress) ; end ; FTotal : = Tot; Synchronize (ShowTotal); end ; procedure TPrimeAdder.ShowTota1; begin ShowMessage ( ' Thread: ' + IntToStr end :
( FTotal) ) ;
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 :
procedure TForml.Button3Click(Sender: TObject); var AdderThread: TPrimeAdder; begin AdderThread : = TPrimeAdder .Create (True); AdderThread.Max : = Max; AdderThread.Free0nTerminate : = True; AdderThread.Resume; end;
En lugar de fijar el numero maximo utilizando una propiedad, hubiera sido mejor pasar este valor como un parametro adicional de un constructor hecho a medida; esto se ha evitado a fin de centrar el ejemplo en el uso del hilo. Se estudiaran mas ejemplos de hilos en capitulos posteriores.
',
nil ) ;
Application.CreateForm(TForm1, Forml);
(Hwnd)
Para activar la ventana de la instancia anterior de la aplicacion, se puede usar la funcion Set ForegroundWindow,que funciona en el caso de ventanas que poseen otros procesos. Esta llamada produce su efecto solo si se ha minimizado la ventana pasada como parametro. De hecho, cuando se minimiza el formulario principal de una aplicacion Delphi, se oculta y, por esa razon, el codigo de activacion no tiene efecto. Desafortunadamente, si se ejecuta un programa que utilice la llamada Findwindow que acaba de aparecer en el IDE de Delphi, puede que ya exista una ventana con ese titulo y clase: el formulario en tiempo de diseiio. Por lo tanto, el programa no arrancara ni siquiera una vez. Sin embargo, se ejecutara si cerramos el formulario y su archivo de codigo fuente correspondiente (en realidad, cerrar el formulario oculta sencillamente la ventana) o si cerramos el proyecto y ejecutamos el programa con el Explorador de Windows. Tambien debemos considerar que tener un formulario llamado Form 1 puede, probablemente, hacer que no funcione como se espera ya que muchas aplicaciones Delphi pueden tener un formulario con el mismo nombre. Esto se corregira en las proximas versiones del codigo.
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
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.
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
- ..
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) ;
La funcion P o s tMessage de la API envia un mensaje a la cola de mensajes dc la aplicacion que posee la ventana de destino. En el codigo del formulario, se puede aiiadir una funcion especial para controlar dicho mensaje:
public p r o c e d u r e W M A p p ( v a r msg : TMessage) ; message C p p ; p r o c e d u r e TForml .WMApp ( v a r msg : TMessage) ; begin Application.Restore; end;
App en lugar de wm U s e r ; algunas ventanas del sistema usan wm ~ s e r , p o lo que no hay gar&ia de r enviarhn este mensaje. Esta es la que otras aplicaciones o el sistema razon por la que Microsofi introdujo wm ~ p para 10s mensajes que estim p 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.
lista desplegables de una aplicacion MDI y existen metodos especificos de Delphi que activan la funcionalidad MDI correspondiente, para organizar las ventanas hijo en forma de mosaic0 o cascada. A continuacion presentamos la estructura tecnica de una aplicacion MDI en Windows: La ventana principal de la aplicacion actua como marco o contenedor Una ventana especial, conocida como el "cliente MDI", cubre toda la zona de la ventana marco, Este cliente MDI es uno de 10s controles predefinidos Windows, a1 igual que un cuadro de edicion o un cuadro de lista. La ventana cliente MDI carece de cualquier elemento especifico de interfaz de usuario, per0 esta visible. De hecho, se puede cambiar el color de sistema estandar de la zona de trabajo MDI (denominadaApplication Background) en la ficha Apariencia del cuadro de dialogo Propiedades de pantalla de Windows. Existen diversas ventanas hijo, del mismo tipo o de distintos tipos. Estas ventanas hijo no se colocan en la ventana marco directamente, sin0 que cada una se define como un hijo de la ventana cliente MDI, que a su vez es hijo de la ventana marco.
ChildForm: TChildForm;
begin
Otra caracteristica importante consiste en aiiadir un menu desplegable Window y utilizarlo como el valor de la propiedad WindowMenu del formulario. Este menu desplegable lista automaticamente todas las ventanas hijas disponibles. Podemos escoger, por supuesto, cualquier otro nombre para el menu desplegable, per0 Window es el estandar.
Para que el programa funcione correctamente, podemos aiiadir un numero a1 titulo de cualquier ventana hijo cuando se crea:
procedure TMainForm.NewlClick(Sender: TObject); var ChildForm: TChildForm; begin WindowMenu := Window1 ; Inc (Counter); ChildForm := TChildForm.Create (Self); ChildForm-Caption : = ChildForm.Caption + ' ' (Counter); ChildForm-Show; end;
+ 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
realizaran las acciones correspondientes y se desactivaran si no existe ninguna ventana hijo disponible. El ejemplo MdiDemo, que veremos a continuacion, demuestra el uso de acciones MDI, entre otras cosas. Tambien existen otros metodos y propiedades interesantes relacionadas estrictamente con MDI en Delphi: ActiveMDIChild: Es una propiedad solo de lectura en tiempo dc ejecucion del formulario marco MDI y aloja a la ventana hijo activa. El usuario puede cambiar este valor seleccionando una nueva ventana hijo o el programa puede cambiarlo usando 10s procedimientos Next y Previous, que activan la ventana hijo siguiente o anterior a la activa en ese momento. La propiedad CIientHandle: Aloja el controlador Windows de la ventana cliente MDI, que cubre la zona de cliente del formulario principal. La propiedad MDIChildren: Es una matriz de ventanas hijo. Podemos usar esta y la propiedad MDIChildCount para movernos por todas las ventanas hijo. Esto puede resultar util para encontrar una ventana hijo concreta o trabajar en una dc ellas. Fijese en que el orden interno de las ventanas hijo es el inverso a1 ordcn de activacion. Esto significa que la ultima ventana seleccionada es la ventana activa (la primera en la lista interna), la penultima ventana hijo seleccionada es la segunda y la primera ventana hijo seleccionada es la ultima. Este orden establece el mod0 en que se organizan las ventanas en pantalla. La primera de la lista esta encima del resto, mientras la ultima esta deba-jo de todas y, probablemente, oculta. Podemos imaginarlo como un eje (el eje z ) saliendo de la pantalla hacia nosotros. La ventana activa tiene un valor mayor para la coordenada z y, por tanto, cubrc cl resto de ventanas. Por ello, a1 esquema de ordenacion de Windows se le conoce como z-order.
-
-- --NOTA: El menu Window puede manejarse con ActionManager y con el control de menu ActionMainMenuBar, a partir de Delphi 7. Este control u, tiene un propiedad especifica, ~ i n d o w ~ e n que debemos usar para es, : pecificar el menG que listara las ventanas hijo MDI.
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.
Una de las acciones mas sencillas es el objeto ActionFont, que tiene un controlador OnExecute, que usa un componente FontDialog, y un controlador Onupdate, que desactiva la accion (y; por lo tanto, el elemento de menu asociado y boton de la barra de herramientas) cuando no hay formularios hijo:
procedure TMainForm.ActionFontExecute(Sender: TObject); begin if FontDialog1.Execute then (ActiveMDIChild as TChildForm) .Memol. Font :=
TObject);
La accion denominada New crea el formulario hijo y define un nombre de archivo predefinido. La accion Open llama a1 metodo Act ionNewExcecute antes de cargar el archivo:
procedure TMainForm.ActionNewExecute(Sender: TObject); var ChildForm: TChildForm; begin Inc (Counter); ChildForm : = TChildForm.Create (Self); ChildForm.Caption : = Lowercase (ExtractFilePath (App1ication.Exename)) +
'texto'
IntToStr (Counter) + ' . t x t '; ChildForm.Show; end; procedure TMainForm.ActionOpenExecute(Sender: TObject); begin if 0penDialogl.Execute then begin ActionNewExecute (Self); (ActiveMDIChild as TChildForm).Load (OpenDialogl-FileName); end; end ;
En realidad, el archivo se carga mediante el metodo Load del formulario. De igual modo, el metodo save del formulario hijo lo usan las acciones save y Save As.Fijese en que el controlador OnUpdate de la accion Save activa la accion solo si el usuario ha cambiado el texto del memo:
procedure TMainForm.ActionSaveAsExecute(Sender: TObject); begin // s u g i e r e e l nombre d e l a r c h i v o a c t u a l SaveDialogl.FileName : = ActiveMDIChild.Caption; i f SaveDialogl.Execute then begin // m o d i f i c a e l n o m b r e d e l a r c h i v o y g u a r d a ActiveMD1Child.Caption : = SaveDialogl.Fi1eName; (ActiveMDIChild as TChildForm) .Save; end; end; procedure TMainForm.ActionSaveUpdate(Sender: begin TObject);
Actionsave. Enabled := (MDIChildCount > 0) and (ActiveMDIChild as TChildForm) .Modified; end; procedure TMainForm.ActionSaveExecute(Sender: TObject); begin (ActiveMDIChild as TChildForm) .Save; end;
Figura 8.4. El resultado del ejemplo MdiMulti, con una ventana hijo que rnuestra 10s circulos.
Si preparamos un menu principal para el formulario hijo, sustituira a1 menu principal de la ventana marco cuando se active el formulario hijo. De hecho, una ventana MDI hijo no puede tener un menu propio. Pero el hecho de que una ventana hijo no pueda tener menus no deberia preocuparnos porque este es el comportamiento estandar de las aplicaciones MDI. Podemos usar la barra de menu de la ventana marco para mostrar 10s menus de la ventana hijo. Una tecnica mejor es mezclar la barra de menu de la ventana marco y la del formulario hijo. Por ejemplo, en este programa, el menu del formulario hijo puede colocarse entre el marco de 10s menus desplegables File y Window de la ventana. Para ello se usan 10s siguientes valores GroupIndex: Menu desplegable File, formulario principal: 1. Menu desplegable Circle, formulario hijo: 2. Menu desplegable Window, formulario principal: 3 . A1 usar estas definiciones para 10s indices de grupo del menu, la barra de menu de la ventana marco tendra dos o tres menus desplegables. A1 arrancar, la barra de menu tiene dos menus. Desde el momento en que creamos una ventana hijo, existen tres menus y cuando se cierra la ultima ventana hijo, el menu desplegable Circle desaparece. El segundo tip0 de formulario hijo muestra una imagen en movimiento. El cuadrado, un componente Shape,se mueve por la zona de cliente del formulario a intervalos fijos de tiempo, utilizando un componente Timer y rebota a1 chocar contra 10s extremos del formulario, cambiando su direccion. Este proceso de cambio utiliza un complejo algoritmo que no examinaremos aqui, ya que el objetivo principal del ejemplo es mostrar como se comporta la combinacion de menus cuando tenemos un marco MDI con formularios hijo de diferentes tipos. (Queda en manos del lector examinar el codigo fuente para ver como funciona).
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.
Figura 8.5. La barra de menu de la aplicacion MdiMulti cambia de forrna autornatica para reflejar la ventana hijo seleccionada, corno puede verse cornparando la barra de menu con la de la figura 8.4.
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) ;
NOTA: Un procedimiento de ventana es una funcion que recibe todos 10s mensajes de una ventana. Cada mensaje habrh de tener un procedimiento de ventana, solo uno. hcluso 10s formularios Delphi tienen un procedimiento de ventanas, aunque estA oculto en el sistema. ~ s t llama a la funcion vire tual WndProc, que podemos usar. Pero la VCL tiene un mod0 de control
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 '
.l -a
, : , , ,
,, , ,.l AC:-,
,. .,
, ,
.., :,
I,,
,+, &, A
A,
....-...+-,l
A,
, . , .
jes de un formulario despuks de cierto procesado previo. Con todo este soporte, es necesario controlar 10s procedimientos de ventana explicitamente solo cuando se trabaje con ventanas que no s e a de Delphi, como en este caso. A mcnos que tengamos una razon para cambiar el comportamiento que tiene por defecto esta ventana de sistema, podemos guardar cl procedimiento original y llamarlo para obtener el procesamiento por defecto. Los dos procedimientos (viejo y nuevo) a 10s que se refieren 10s dos punteros de funcion se guardan en dos campos locales del formulario:
private OldWinProc, NewWinProc: Pointer; procedure NewWinProcedure (var Msg: TMessage) ;
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:
El procedimiento de ventana que instalamos llama al procedimiento predefinido. A continuacion, si el mensaje es wm-EraseBkgnd y la imagen no esta en blanco, la dibujamos en pantalla varias veces usando el metodo D r a w de un lienzo temporal. Dicho objeto lienzo se crea cuando el programa arranca (vease el codigo anterior) y se conecta al controlador que el mensaje pasa como parametro wParam. Con esta tecnica, no tenemos que crear un nuevo objeto T C a n v a s para cada operacion de pintado del fondo solicitada, asi se ahorra un cierto tiempo en esta frecuente operacion. Veamos el codigo, que produce la salida que aparece en la figura 8.5:
p r o c e d u r e TMainForm.NewWinProcedure (var Msg: TMessage); var BmpWidth, BmpHeight: Integer; I, J: Integer; begin / / p r o c e s a d o predefinido p r i m e r 0 Msg.Result : = CallWindowProc (OldWinProc, ClientHandle, Msg.Msg, Msg.wParam, M s g - l P a r a m );
/ / controla el pintado d e fondo i f Msg.Msg = ~ E r a s e B k g n dt h e n begin BmpWidth : = MainForm.1magel.Width; BmpHeight : = MainForm.Image1.Height; i f (BmpWidth <> 0 ) a n d (BmpHeiyht <> 0 ) then begin 0utCanvas.Handle : = Msg.wParam; f o r I : = 0 t o MainForm.ClientWidth d i v BmpWidth d o f o r J : = 0 t o MainForm.ClientHeight d i v BmpHeight d o 0utCanvas.Draw (I * BmpWidth, J * BmpHeight, MainForm.Imagel.Pictu~e.Graphic); end; end; 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 diseio en las dos versiones de un formulario/marco. Sin embargo, en la hereni cia 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
En cl dialogo New Items, se puede escoger el formulario del que se quiere heredar. El nucvo formulario tiene 10s mismos cuatro botones. Veamos la descripcion textual inicial del nuevo formulario:
inherited Form2: TForm2 Caption = 'Form2 ' end
Esta es la declaration dc clase inicial, en la que vemos que la clase basica no es la habitual T F o r m sino el formulario de clase basica actual:
type TForm2 = class (TForml) private { Declaraciones privadas } public { D e c l a r a c i o n e s pLiblicas ) end;
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);
Esta no es la unica posibilidad. Tambien podemos crear un nuevo controlador de eventos y no ejecutar el codigo de la clase base, como hemos hecho con el tercer boton del ejemplo VFI: para ello, solo debemos quitar la palabra clave inherited. Otra posibilidad consiste en llamar a un metodo de una clase base despues de ejecutar algun codigo a medida, llamandolo cuando se cumple una condicion o llamando a1 controlador de otro evento de la clase base, como hemos hecho con el cuarto boton:
TObject);
No es habitual heredar de un controlador diferente, per0 debemos tener en cucnta quc cs posible. Podemos. por supuesto, considerar cada metodo del formulario basc como un metodo de nuestro formulario, para asi llamarlo libremente. Este ejcmplo nos permite explorar algunas caracteristicas de la herencia de formularios visuales, pero para comprobar su valia debemos pensar en ejemplos del mundo real, que son mas complejos de lo que este libro puede tratar. Ahora; cstudiaremos cl polimorfismo de formularios visuales.
NOTA: La herencia de formularios visuales no funciona muy bien con colecciones. n n nnrlemnc extender iinn nrnnierlnrl rle cnlecciirn rle Iin cnmponente en I uso de serie! les de lista con detalles. Estos componentes pueden usarse tanto en el tormulario padre como en el heredado, por supuesto, per0 no podemos extender 10s elementos que contienen, ya que estan almacenados en una coleccion.
TT-,
.. ,I A, : , , c , , I L , , , & A , , ,:,,, A,,:, , I A, , . c , ,,,,. :,I. ulla SUIUGIUII CSLC ~ I U U I C I CSW~ cu C V I F ~ I a a a q p a w u u uc c r r GU~CGGIUa II l sia
,.,, :.
nes en tiempo de diseiio para, en su lugar, hacerlo en tiempo de ejecucion. Seguiriamos usando la herencia de formularios per0 perderiamos la parte visual de esta. Si intentamos usar el componente ActionManager, descubriremos que ni siquiera podemos heredar de un formulario que lo contenga. Borland deshabilito esta caracteristica porque podria causar demasiados problemas.
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 end;
TViewerForm.LoadlClick(Sender: T O b j e c t ) ;
LoadFile;
Este metodo se define en la clase T V i e w e r Form como un metodo abstracto (de forma que la clase del formulario base es en realidad una clase abstracts). Dado que este es un metodo abstracto, sera necesario redefinirlo (y sobrescribirlo) en 10s formularios heredados. El codigo de este metodo L o a d F i l e utiliza sencillamente el componentc O p e n D i a l o g l para pedir a1 usuario que seleccione un archivo de entrada y lo cargue en el componente de imagen:
procedure TImageViewerForm.LoadFi1e; begin if 0penDialogl.Execute then
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 ;
Fo rmL i s t es una matriz polimorfica de objetos TVi ewe r Fo rm genericos, declarada en la clase T M a i n Fo rm.Para hacer esta declaracion en la clase debemos aiiadir la unidad Viewer (pero no 10s formularios especificos) en la clausula uses de la parte de interfaz del formulario principal. La matriz de formularios se usa para cargar un archivo nuevo en cada formulario de visor cuando hacemos clic en alguno de 10s dos botones. Los controladores del evento oncl ic k de cada uno de 10s botones funcionan de diferente manera:
//ReloadButtonlClick for I := 1 to 2 do FormList [I] .ButtonLoadClick //ReloadButtonZClick for I := 1 to 2 do FormList [I] .LoadFile;
(Self);
El boton secundario llama a un metodo virtual, funcionando sin problemas. El boton primario llama a un controlador de eventos y siempre llega a la clase generica TFormView (mostrando el mensaje de error de su metodo ButtonLoadClick). Esto ocurre porque el metodo es estatico en lugar de virtual. Para hacer que esto funcione, podemos declarar el metodo But tonLoadClic k de la clase T FormVie w como virtual y sobrecargarlo en cada una de las clases formulario heredadas, como hacemos con cualquier otro metodo virtual:
type TViewerForm = class (TForm) p r o c e d u r e ButtonLoadClick
public procedure LoadFile; virtual; abstract; end; type TImageViewerForm = class (TViewerForm) procedure ButtonLoadClick (Sender: TOb ject) ; override; public procedure LoadFile; override; end;
Este truco funciona aunque no se mencione en la documentacion de Delphi. Esta capacidad para usar controladores de eventos virtuales es lo que llamamos polimorfismo de formularios visuales. En otras palabras, podemos asignar un metodo virtual a una propiedad de evento, que capturara la direccion del metodo en funcion de la instancia disponible en tiempo de ejecucion.
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 ;
L a diferencia con respecto a u11 formulario esta en que podemos aiiadir el marco a un formulario. Hemos usado dos instancias del marco en el ejemplo (como muestra la figura 8.9) y modificado el comportamiento ligeramente. La primera instancia del marco tiene 10s eleme~itos cuadro de lista organizados. del A1 cambiar una propiedad de un componente de un marco, el archivo DFM del formulario anfitrion listara las diferencias, como hace con la herencia de formularios visuales:
object FormFrames : TFormFrames Caption = ' F r a m e s P ' inline FrameListl: TFrameList Left = 8 Top = 8 inherited ListBox: TListBox Sorted = True end end inline FrameList2: TFrameList Left = 232 Top = 8 inherited btnclear: TButton OnClick = FrameList2btnClearClick end
end end
&d(~I-l Du I , .: .; .: .; .; .:
meted
......
Some !ex(
Figura 8.9. Un rnarco y dos instancias del rnisrno en tiempo de disetio, en el ejemplo Frarnes2.
Como se ve en el listado, el archivo DFM de un formulario que tiene marcos usa una nueva palabra clave DFM, inline.Las referencias a 10s componentes modificados del marco, en cambio, utilizan la palabra clave inherited,aunque este termino se usa con un significado ampliado: inherited aqui no se refiere a la clase basica de la que se hereda, sin0 a la clase de la que estamos sacando una instancia de un objeto (o heredando). Hemos utilizado una caracteristica existente en la herencia de formularios visuales para aplicarla a este nuevo contexto. Asi, en realidad, se puede usar la orden Revert to Inherited del Object Inspector o del formulario para cancelar 10s cambios y volver a1 valor predefinido de las propiedades. Los componentes de la clase marco que no han sido modificados tampoco son mostrados en el archivo DFM del formulario que utiliza el marco, y aunque 10s componentes de ambos formularios tengan el mismo nombre 10s dos marcos tienen nombres diferentes. Estos componentes no son propiedad del formulario sino del marco. Esto implica que el formulario debe referencia 10s componentes a traves del marco. como puede verse en el codigo de 10s botones que copian elementos de un cuadro de lista a otro:
procedure TFormFrames.btnLeftClick (Sender: TObject); begin FrameListl.ListBox.Items.AddStrings (FrameList2.ListBox.Items); end ;
Ademas de modificar las propiedades de cualquier instancia de un marco, tambien podemos cambiar el codigo de cualquiera de sus controladores de even-
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:
La linea de codigo aiiadida automaticamente por Delphi corresponde a una llamada a1 controlador de eventos hcrcdado dc la clase base mediante herencia de forrnularios visuales. En este caso, para que se de el comportamiento original dcl marco, debemos llamar a un controlador de eventos y aplicarlo a una instancia especifica, el propio objeto marco. El marco actual no incluye este controlador ni sabe nada de dl. Tanto si decidimos dcjar esta llamada como si la quitamos dependera del efecto que busquemos.
-
...
TRUCO:Debemos tener en cuenta que, dado que el controlador de eventos tiene codigo, dejarlo como lo ha generado Delphi y guardar el formulario no lo eliminara como es habitual, ya que no esta vacio. Si queremos omitir el codigo por defect0 para un evento, tenemos que aiiadirle, a1 menos, un comentario para evitar que el sistema lo borre automaticamente.
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.
Poder utilizar diversas instancias de un marco es una de las razones para la introduccion de esta tecnica y personalization del marco en tiempo de diseiio resulta incluso mas importante. Dcbido a que aiiadir propiedades a un marco y hacer que estCn disponibles en tiempo de diseiio requiere cierto codigo complejo y personalizado, esta bien poder usar un componente que contenga estos valores personalizados. Tambien esiste la opcion de ocultar estos componentes (como la ctiqueta dcl ejcmplo) si no pertenecen a la interfaz de usuario. En el ejemplo, es neccsario cargar el archivo cuando se crea la instancia del marco. Como 10s marcos no tienen un evento Oncreate. la mejor opcion es probablemente sobrescribir el metodo CreateWnd.Crear un constructor a medida no funciona, porque es ejecutado demasiado pronto, antcs de que la etiqueta de testo especifica este disponible. En el metodo CreateWnd, sencillamente cargamos el contenido del cuadro de lista desde un archivo.
NOTA: La creacion de la ventana marco (como ocurre con la mayoria de controles) se retrasa por razones de eficiencia. Causa mas problemas la utilization de herencia entre formularios que contienen marcos, por lo que, para evitar este problema, se desactivo el controlador de eventos oncreate para marcos (asi lo programadores pueden escribir el codigo que consideren razonable).
Formularios en fichas
A pesar de que podemos utilizar marcos para definir las fichas de un PageControl en tiempo de diseiio, tambien podemos usar otros formularios en tiempo de ejecucion. Esta tecnica nos da la flexibilidad de tener las fichas definidas en unidades y ficheros DFM separados y, al mismo tiempo, permite utilizar esos formularios como ventanas independientes. Cuando tenemos un formulario principal con un control de fichas y uno o mas formularios secundarios para mostrar en el, todo lo que tenemos que hacer es escribir este codigo para crear 10s formularios secundarios y situarlos en las fichas:
var
Form: TForm; Sheet: TTabSheet;
begin // c r e a r una h o j a d e t a b u l a c i o n e n e l c o n t r o l f i c h a Sheet : = TTabSheet.Create(PageContr011); Sheet.PageContro1 : = PageControll; // c r e a r e l f o r m u l a r i o y s i t u a r l o e n l a h o j a d e t a b u l a c i o n Form := TForm2 .Create (Application); Form.BorderStyle := bsNone; Form.Align : = alclient; Form. Parent := Sheet; Form. Visible := True; // a c t i v a r l o y p o n e r l e t i t u l o PageContro1l.ActivePage : = Sheet; Sheet.Caption : = Form.Caption; end;
Se puede encontrar este texto en el ejemplo Formpage, per0 el programa no hace nada mas. Para ver una aplicacion, puede consultarse el programa de demostracion RWBlocks, en el capitulo 14.
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
Figura 8.11. La primera ficha del ejemplo FrameTab en tiernpo de ejecucion. El marco dentro de la solapa es creado en tiempo de ejecucion.
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 ;
Para que el codigo funcione, debemos recordar afiadir una llamada a Reg i s t e rC 1a s s en la parte de inicializacion de cada una de las unidades que definen el marco.
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.
Los dos metodos sobrecargados son llamados a la vez que el controlador de eventos por lo que podemos agregarle codigo extra (que nos permita definir el controlador de eventos como hacemos usualmente). Dentro de 10s dos metodos cargamos y guardamos la posicion del formulario en un archivo IN1 de la aplicacion, en una seccion marcada con el nombre del formulario. Este es el codigo de ambos metodos:
p r o c e d u r e TSaveStatusForm.DoCreate; var Ini: TIniFile; begin inherited; Ini : = TIniFile.Create (ExtractFileName ( A p p 1 i c a t i o n . E x e N a m e )) ; Left : = Ini .ReadInteger (Caption, 'Izquierda ', Left) ; T o p : = Ini .ReadInteger (Caption, 'Arriba ', T o p ) ; W i d t h := Ini. ReadInteger (Caption, 'Anchura ', W i d t h ) ; Height : = Ini .ReadInteger (Caption, 'Altura ', H e i g h t ) ; Ini. Free; end ; p r o c e d u r e TSaveStatusForm.DoDestroy; var Ini: TIniFile; begin Ini : = T I n i F i l e - C r e a t e (ExtractFileName (Application.ExeName));
Ini .WriteInteger (Caption, Ini .WriteInteger (Caption, Ini .WriteInteger (Caption, 1ni.WriteInteger (Caption, Ini. Free;
' I z q u l e r d a ', Left) ; ' A r r i b a ', Top) ; ' A n c h u r a ', Width) ; ' A l t u r a ', Height) ;
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
-
--
- - --
Existen dos posibilidades para colocar un archivo MI. El &%go que acabamos de listar guarda el archivo en el directorio Windows o en una carpeta de usuario con las configuraciones en Windows 2000. Para
guardar'los datos de forma locd en la itpii~&i&n oposicibn a bacer(en lo 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~ntnc tree tinnc nncihlac- rarlenac antarnc n hnnlaannc AP
. .
La clase T I n i F i l e tiene tres mCtodos Read, uno para cada tipo de datos: R e a d B o o l , R e a d I n t e g e r y R e a d s t r i n g . TambiCn hay . . .. .. . .
SI lir
co~~cspouurcr~u cn-
trada no existe en un archivo INI. Debemos tener en cuenta que Delphi utiliza 10s ficheros IN1 muy a menudo, per0 con nombres diferentes. Por ejemplo, 10s archivos de escritorio (.dsk) y de opciones (.dof) e s t h estructurados como archivos INI. Las clases TRegistry y TRegIniFile: El Registro es una base de datos jerarquica de inforrnacion sobre el ordenador, la configuraci6n de software y las preferencias del usuario. Windows tiene un conjunto de funciones API para interactuar con el Registro. Basicamente abrimos una clave (o carpeta) y, a continuacion, trabajamos con subclaves (o subcarpetas) y con valores (o elementos), pero debemos ser conscientes de la estructura y 10s datos del Registro. Delphi ofrece basicamente dos tkcnicas a1 uso del Registro: la clase T R e g i s t r y un encapsulado del Registro API, mientras que Ia clase T R e g I n i F i l e la interfaz de la clase T I n i F i l e per0 guardando 10s datos en el Registro. Esta clase es la opcion natural para conseguir el intercambio entre la informacibn basada en IN1 y las veniones basadas en Registro de un mismo programa. Cuando creamos un objeto TReg I n i F i l e , nuestros datos termina.n en la infonaacb5n de usuario actual, por lo que nomlmente usamos un ~wstructor cmm:
IniFile := TRegIniFile.Create ( 'Software\MyCompany \MyProgramp) ;
A1 usar las clases TJniFile y TRegistryhiFile que nos &ece la VCL, podemos desplazamos de un modelo de almacenamiento local y por usuario a otro. A pesar de todo esto, no deberiaanos usar el registro demasiado porque tener un repositorio centralizado para la configmacion de cada aplicaci6n fue un error de diseilo de la arquitectura. Incluso Microsoft reconoce este becho (sin admitir realmente el error) a1
sugerir, en 10s requisites de compatibilidad con windows 2000, que deberia evitarse usar el Registro para almacenar la configuracih de las aplicaciones y, en su lugar, volver a utilizar 10s archivos IN1 dentro de la carpeta de documentos del usuario actual (algo de lo que muchos programadores no han oido hablar).
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;
Esta clase se llama TForm y hereda de TForm de la unidad Forms (esta ultima referencia es obligatoria para evitar una especie de definicion recursiva). En el resto del programa, no hay que cambiar la definicion de clase del formulario, sino sencillamente aiiadir la unidad que define la clase de interposicion (la unidad SaveStatusForm en este caso) en la sentencia u s e s despues de la unidad que define la clase Delphi. El orden de las unidades en la sentencia u s e s es importante, y es la razon por la que algunas personas critican esta tecnica, ya que
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 ;
Tengamos en cuenta que en una aplicacion empresarial podemos sincronizar todos 10s formularios con 10s datos de una empresa especifica o un evento empresarial especifico. Ademas, a diferencia de la herencia, podemos tener varios formularios que implementen cada uno varias interfaces, con combinaciones ilimitadas. Esta es la razon por lo que utilizar una arquitectura como esta puede mejorar mucho una compleja aplicacion Delphi y hacerla mucho mas flexible y mas sencilla de adaptar para la implernentacion de cambios.
Es importante conocer como estas funciones son llamadas a1 crear un objeto ya que podemos insertarlas en dos pasos. Como llamamos a un constructor, Delphi invoca la funcion de clase virtual NewInstance, definida en TObject.Esta funcion es virtual por lo que podemos modificar el gestor de memoria para una clase especifica sobrecargandola. Para llevar a cab0 la asignacion de memoria NewIns tance, normalmente, termina llamando a la funcion GetMem del gestor de memoria activa, lo que nos da una segunda oportunidad para modificar el comportamiento esthdar . A no ser que tengamos necesidades especiales, generalmente no necesitaremos intervenir en el gestor de memoria para modificar el funcionamiento de la asignacion. De todas maneras, resulta practico hacerlo para determinar si este funcionamiento es correcto, es decir, para asegurarnos de que el programa no tiene agujeros de memoria. Por ejemplo, podemos sobrecargar 10s metodos New I ns t ance y
F r e e I n s t a n c e de una clase para llevar la cuenta del numero de objetos que se crean y destruyen para verificar si el total es cero. Una tecnica mas simple es hacer el mismo analisis sobre el total de objetos asignados por el gestor de memoria. En las primeras versiones de Delphi, hacer esto requeria codigo extra, per0 el gestor de memoria proporciona dos variables (A1 1ocMemCount y A 1 1ocMemSize) que pueden ayudarnos a saber que esta ocurriendo en el sistema. El mod0 mas simple de determinar si nuestro programa esta tratando la memoria adecuadamente es comprobar si A 1 1ocMemCount vuelve a cero. El problema es decidir cuando hacer esa comprobacion. Un programa empieza ejecutando la seccion de inicializacion de sus unidades, que normalmente asigna memoria liberada por las respectivas secciones de finalizacion. Para garantizar que nuestro codigo se ejecuta a1 final, debemos escribirlo en la seccion de finalizacion de una unidad y colocarlo a1 principio de la lista de unidades en el archivo de codigo fuente del proyecto. Podemos ver una unidad como esta en el listado 8.1. Esta es la unidad SimpleMemTest del ejemplo ObjsLeft, que tiene un formulario con un boton para mostrar el contador de asignaciones actual y un boton para crear un agujero de memoria (que es capturado a1 terminar el programa).
Listado 8.1. Una unidad para cornprobar agujeros de rnernoria, del ejemplo ObjsLeft.
unit SimpleMemTest; interface implementation uses Windows ; var
msg: string;
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) ;
Este programa es practico, per0 realmente no ayuda a comprender que ha ido mal. Para conocer esto, existen herramientas muy potentes (algunas de las que tienen versiones de prueba gratuitas), o podemos utilizar el gestor de memoria descrito es este libro, que analiza las asignaciones de memoria.
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
/
---
- -
~r
1
--
- .-
-r
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
archivos de la unidad (DCU), que contienen tanto la informacion de simbolo como el codigo maquina compilado. Esto reduce el tiempo de compilacion y permite distribuir solo 10s paquetes sin 10s archivos de unidad previamente compilados. Las unidades precompiladas son necesarias para enlazar de forma estatica 10s componentes en una aplicacion. La distribucion de archivos DCU precompilados (o codigo fuente) puede ser util dependiendo del tipo de componentes que desarrollemos. Veremos como crear un paquete despues de presentar algunas recomendaciones generales y crear un componente.
.. .
Usar excepciones. Cuando algo falla, el componente deberia crear una excepcion. Cuando se asignan recursos de algun tipo, debemos protegerlos con bloques t r y / f i n a l l y y llamadas a1 destructor. Para completar un componente, hay que aiiadirle un mapa de bits, para que lo utilice la Component Palette de Delphi. Si queremos que nuestro componente lo use mucha gente, debemos considerar tambien la idea de aiiadirlc un archivo de ayuda. Prepararnos para cscribir codigo rcal y olvidar 10s aspectos visuales de Dclphi. Por lo gcncral. cscribir componcntes significa escribir codigo sin soporte visual (aunque la f u n c i o n c l a s s C o m p l e t i o n puede acelerar bastantc la codificacion de clases normales). La excepcion a esta norma es que podemos m a r marcos para escribir componentes de forma visual. NOTA: Tambien podemos usar herramientas de escritura de componentes de terceros para crear nuestros componentes o acelerar su desarrollo. La herramienta mas potente de terceros para crear componentes Delphi que conocemos es Component Development Kit (CDK) de Eagle Software (www .eagle-software.com), per0 hay muchas mas.
TComponent: Es la clase padre de todos 10s componentes (incluidos 10s controles) y se pueden usar como clase padre directa de componentes no visuales. En el resto del capitulo, crearemos algunos componentes usando varias clases padre y analizaremos todas las diferencias entre ellas. Empezaremos con componentes que heredan de componentes o clases existentes a un bajo nivel de la jerarquia. Despues trataremos ejemplos de clases que heredan directamente de las clases precedentes que acabamos de mencionar.
&kUePapa
hid
A
J
El nombre del tip0 ascendente: la clase de componente de la que se quiere heredar. En este caso podemos usar TComboBox. El nombre de la clase del nuevo componente que vamos a crear. Podemos usar TMdFontCombo. La ficha de la Component Palette en la que queremos que aparezca el componente, que puede ser una ficha nueva o una que ya exista. Podemos crear una nueva ficha, llamada Md. El nombre del archivo de la unidad en la que queremos que Delphi coloque el codigo fuente del nuevo componente. Podemos escribir MdFont Box. La ruta de busqueda actual (que deberia aparecer de forma automatica). Hacemos clic sobre el boton OK y el asistente para componentes generara el archivo fuente mostrado en el listado 9.1 con la estructura de nuestro componente. El boton Install se puede usar para instalar el componente en un paquete de forma inmediata. Veamos el codigo primer0 y despues trataremos la instalacion.
Listado 9.1. Codigo del TMdFontCombo, producido por el asistente para componentes.
u n i t MdFontBox; interface uses windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMdFontCombo = c l a s s (TComboBox) private { Private declarations 1 protected { Protected declarations 1 public { Public declarations 1 published { Published declarations ] end; p r o c e d u r e Register; implementation p r o c e d u r e Register; begin Registercomponents ( ' M d ' , [TMdFontCombo] ) ; end; end.
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~ornvres clase u~sr~rl~os. esa razo11,la mayorla ue los uesarrollauores ue ror
de componentes en Delphi han escogido aiiadir un prefijo de dos o tres letras a 10s nombres de nuestros componentes. En este libro, hemos escogid o Md para identificar 10s componentes escritos en este. La ventaja de esta tecnica esta en que podemos instalar un componente TMd F o n t C o m b o , aunque ya hayamos instalado un componente denominado T F o n t Combo. Observe que 10s nombres de unidad han de ser unicos para todos 10s componentes instalados en el sistema, por lo que hemos aplicado el mismo prefijo a los nombres de unidad. Esto es todo lo que hay que hacer para crear un componente. Este codigo, por supuesto, no incluye demasiado codigo. Ahora, solo hay que copiar todas las fuentes del sistema en la propiedad I t e m s del cuadro combinado a1 arrancar. Para ello, podemos intentar sobrescribir el metodo c r e a t e en la declaracion de clase, afiadiendo la sentencia I t e m s : = S c r e e n . F o n t s . Sin embargo, esta no es la tecnica adecuada. El problema esta en que no podemos acceder a la propiedad I t e m s del cuadro combinado, antes de que el manejador de ventana del componente este disponible. El componente no puede tener un manejador de ventana hasta que se defina su propiedad P a r e n t y esa propiedad no se define en el constructor, sin0 mas adelante. Por esa razon, en lugar de asignar las nuevas cadenas en el constructor Create. debemos realizar esta operacion en el procedimiento C r e a t e W n d , a1 que se llama para crear el control ventana despues de que se construya el componente, se defina su propiedad P a r e n t y su manejador de ventana este disponible. De nuevo, ejecutamos el comportamiento predefinido y, a continuacion, podemos escribir nuestro codigo personalizado. Podiamos habernos saltado el constructor C r e a t e escribiendo todo el codigo en C r e a t e W n d , per0 hemos usado ambos metodos iniciales para mostrar las diferencias entre ellos. Veamos la declaracion de la clase del componente:
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.
La otra propiedad que hemos vuelto a definir, I t e m s , se define como una propiedad que no deberia guardarse en el archivo DFM, sea cual sea el valor real. Esto se consigue con la directiva s t o r e d seguida del valor F a l s e . El componente y su ventana se van a crear de nuevo cuando el programa arranca, por lo que no tiene sentido guardar en el archivo DFM informacion que mas tarde se va a dcscartar (para ser sustituida por la nueva lista de fuentes).
AP UV
ejecucion. Esto se puede hacer utilizando sentencias como i f not (csDesigning in ComponentState). Pero en el caso de este
..-;...a-
~ 1 1 1 1 1 ~ U I I L V U I I G I I L ~ G G ~ L ~ I I ~ U G ~ U U kU I I I G I I U ~ ~ L I ~ I G I I C~ ~1 UU I I ~ 1 G
n~...nn..a..**
-..a
P"+~-A"
,...a..-rlfi
a1
ma..-
af
,.;a..*a
-a-n
I 1 1 Va a S G I I ~ I 1
YX"
m .,; a..
110 metodo que hemos d e s c30ofrece un enfoque mas claro del procedi~ miento basico. La tercera propiedad, C h a n g e F o r m F o n t , no se hcreda sino que es introducida por el componente. Se usa para establecer si la seleccion de la fuente actual del cuadro combinado, deberia especificar la fuente del formulario en el que se incluye el componente. De nuevo, esta propiedad se declara con un valor predefinido, declarado en el constructor. Se usa la propiedad ChangeFormFont en el codigo del mdtodo C r e a t e W n d , que aparecia anteriormente, para establecer la seleccion inicial del cuadro combinado que depende de la fuente dcl formulario que ale-ja a1 componente. Estc es normalmente el propietario del componente, aunque podiamos habernos desplazado por el arb01 P a r e n t en busca de un componente formulario. Este codigo no es perfecto, per0 las verificaciones A s s i g n e d e i s ofrecen una seguridad adicional. La propiedad C h a n g e F o r m F o n t y la misma prueba i f tienen una funcion clave en el metodo C h a n g e d , que en la clase basica desencadena el evento O n C h a n g e . A1 sobrescribir este mCtodo, ofrecemos un comportamiento predefinido, que sc puede desactivar cambiando el valor de la propiedad, pero tambien permite la ejecucion del evento OnChange, para que 10s usuarios de dicha clase puedan personalizar por completo su comportamiento. El ultimo metodo; S e t C h a n g e F o r m F o n t , se ha modificado para refrescar la fuente del formulario en caso de quc se estd activando la propiedad. Este es el codigo completo:
procedure TMdFontCombo.Change; begin // asigna la fuente a 1 formulario propietario if FChangeFormFont and Assigned (Owner) and (Owner is TForm) then TForm (Owner).Font.Name : = Text; inherited; end ;
Boolean);
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:
La lista Contains: lndica 10s componentes incluidos en el paquete (0, para scr mas exactos, las unidades que definen esos componentes). La lista Requires: Indica 10s paquctcs necesarios para dicho paqucte. Norinalinentc, nucstro paquete necesitara 10s paquetes rtl y vcl (el paquetc de la bibliotcca cn ticmpo dc cjccucion y el paquete principal VCL), pero podria neccsitar tambicn cl paqucte vcldb (que contiene la mayoria de las clases relacionadas con bases dc datos) si 10s componentes del nuevo paquctc rcalizan alguna operacion relacionada con bases de datos.
22
2
Add
s
Remove
T I
Opl~ons
Compk
I
I
NOTA: Los nombres de Daauetes desde Debhi 6 va no son es~ecificos de la version, aunque 10s paquetes compilados tengan todavia el numero de la version en el nombre del archivo. Para conocer mas detalles acerca de como . * . * * . .. se logra esro recnlcamenre poaemos acuair mas aaelanre, a la seccron que trata 10s cambios en 10s nombres de proyecto y biblioteca.
1
..
I
(
1 .
3.
Si afiadimos el componente a1 nuevo paquete que acabamos de definir y, a continuacion, sencillamente compilamos el paquete y lo instalamos (usando 10s dos botones correspondientes de la barra de herramientas del Package Editor), veremos aparecer inmediatamente el nuevo componente en la ficha Md de la Component Palette. El procedimiento Register del archivo de la unidad del componente inform6 a Delphi sobre donde instalar el nuevo componente. Por defecto, el mapa de bits utilizado sera el mismo que el de la clase padre, porque no hemos ofrecido un mapa de bits personalizado (haremos esto en ejemplos posteriores). Fijese en que si movemos el raton sobre el nuevo componente, Delphi mostrara en forma de sugerencia el nombre de la clase sin la letra inicial T.
...
{ $ D E S C R I P T I O N 'Mastering D e l p h i Package { $ IMPLICITBUILD ON)
requires vcl ; contains MdFontBox end.
I }
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
activar tambidn seleccionando el elemento del menu Component>lnstall Packages. Este cuadro de dialogo aparece en la figura 9.2.
..
Desciition Campier DrectarmlConddimls
,
Luiker Packages
Design packages
Ademas de dircctivas comunes como D E S C R I P T I O N ; hay otras directivas especificas para paquetes. Se puede acceder facilmente a las opciones mas comuncs mediante el boton Options del Package Editor. Despues de esta lista de opciones, estan las palabras clave requires y contains,que listan 10s elementos que aparecen visualmente en las dos fichas del Package Editor. De nuevo. la primera es la lista de paquetes que neccsita el actual y la segunda una lista de las unidades que instala dicho paquete. Veamos 10s efectos a nivel tecnico de la creacion de paquetes. Ademas del archivo DPK con el codigo fuente, Delphi genera un archivo BPL con la version de enlace dinamico del paquete y un archivo DCP con la informacion de simbolos. En la practica, este archivo DCP es la suma de la informacion de simbolo de 10s archivos DCU de las unidades contenidas en el paquete. En tiempo de diseiio, Delphi necesita tanto 10s archivos BPL como DCP, porque el primero tiene el codigo real de 10s componentes creados en el formulario de diseiio y la informacion de simbolos necesaria para la tecnologia Code Insight. Si enlazamos el paquete de forma dinamica (usandolo como un paquete en tiempo de ejecucion), el archivo DCP se utilizara tambien para el editor de enlaces y el archivo BPL se enviara con el archivo ejecutable principal de la aplicacion. En cambio, si enlazamos el paquete de forma estatica, el editor de enlaces hara referencia a 10s archivos DCU y solo sera necesario distribuir el archivo ejecutable final. Por ese motivo, como diseiiadores de componentes, deberiamos distribuir normalmente a1 menos el archivo BPL, el archivo DCP y 10s archivos DCU de las
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.
TRUCO:Los archivos DCR son sencillamente archivos RES esthdar con una extension distinta. Si lo preferimos, se pueden crear con cualquier editor de recursos, como el Borland Resource Workshop, que es realmente una herramienta mas potente que el ed itor Delphi Image. A1 finalizar la creaci6n del archivo de recurs;, s61o hlay que dar otro nombre a1 archivo r .a . . .. -n" m para w a r una extension ULK. 3
Ahora podemos aiiadir un nuevo mapa de bits a1 recurso, seleccionando un tamaiio de 24x24 piseles y podemos dibujar el mapa de bits. Las otras normas importantes hacen referencia a la denominacion. En este caso, la norma de deno-
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
.- -
--
. .. . . .-
. -
.. -
Md4ctiveBtn pas D:\md7code\OS\Mdpack Md4rrow.dcr D:\md7code\OS\Mdpack k Md4rrow.pas J D:\md7code\OS\Mdpack MdClock.dc1 D:\md7code\OS\Mdpack MdClock.pas D:\rnd7code\OS\Mdpack E MdClockFfarns D:\rnd7code\OS\Mdpack MdCollect.pas D:\md7code\OS\Mdpack MdfontCombo.pas D:\md7code\OS\Mdpack MQntfTest.pas D:\md7code\OS\M&ack MdLifl4ct pas D.\rnd7de\OS\Md~zck K MdLiflDial D:\rnd7code\OS\Mdpack 9 MdListDddcr D:\rnd7code\OS\Mdpack D:\md7codeU!S\Mdpack MdNumEd pas D:\md7code\OS\Mdpack MdPersonalData D:\md?code\OS\Mdpack ... MdSounB dcr D:\rnd7code\OS\Mdpack MdSounB.pas D:\rnd7code\OS\Mdpack 3 0 Requires ~N.dcp vcl.dcp
a
9
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: Son creados y gestionados por el componentc principal, que puede mostrar algunas de sus propiedades y eventos. Componentes Externos: Se conectan usando propiedadcs. Automatizan la interaccion entre componentes separados, que pueden estar en el mismo formulario o diseiiador o en uno diferente.
En ambos casos, el desarrollo sigue algunas reglas estandar. Una tercera alternativa, menos esplorada, implica el desarrollo de contenedores de componentes, que pueden interactuar con 10s controles hijo. Este es un tema mas avanzado por lo que no se tratara aqui.
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.
NOTA: La primera caracteristica se ha convertido en algo mis relevante desde Delphi 6, puesto que el Object Inspector de esta ultima version, permite exponer propiedades de subcomponentes directamente.
Dado que el rcloj digital ofrecera una salida con un cierto texto, hemos considerado el heredar de la clase TLabel.Sin embargo, esto permitiria que el usuario cambiase el titulo de la etiqueta (es decir, el testo del reloj). Para evitar este problema, sencillamente hemos utilizado el componente TCustomLabel como clase padre. Un objeto TCustomLabel tiene las mimas capacidades que un objeto TLabel,per0 pocas propiedades publicadas. En otras palabras, una clase que hereda de TCUS tomLabel puede decidir que propiedades deberian estar disponibles y cuales deberian permanecer ocultas.
--
_ I
NOTA: L a mayoria de 10s componentes Delphi, sobrc todo 10s basados en Windows, tienen una clase basica TCustomXxx, que implementa toda la funcionalidad pero expone solo un conjunto limitado de propiedades. Heredar de estas clases basicas es la forma estandar de exponer solo algunas de las propiedades de un componente en una version personalizada. De hecho, no se pueden ocultar las propiedades publicas ni publicadas de una clase b h i c a , a no ser que las ocultemos definiendo una nueva propiedad con el mismo nombre en la clase heredada.
Con versiones previas de Dclphi, el componentc debia dcfinir una propiedad nucva. A c t i v e , envolvicndo la propiedad E n a b l e d del Tcmporizador. Quc una propicdad sca envolvcr~le significa que sus mctodos set y gel lccn y cscribcn el valor de la propiedad er?vuelto, que pcrtcnece a un componente intcrno (gcncralmcnte, una propicdad envolvcnte no contiene datos locales). Veamos cl codigo de cstc caso cspccifico:
f u n c t i o n T M d C l o c k - G e t A c t i v e : Boolean; begin Result : = FTimer.Enabled; end; p r o c e d u r e TMdClock.SetActive begin FTlmer . Enabled : = Value; end; (Value: Boolean);
Publicacion de subcomponentes
Ya desdc Delphi 6. podemos exponer simplemcnte el componente completo (cl tcmporizador) en una sola propiedad, quc ampliara norn~allnente Object Insel pector 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 c k propietario (como indica el parametro de su constructor como C r e a t e ) , por lo tanto se destruira automaticamente cuando se destruya el componente reloj.
NOTA: El efecto real de la llamada a1 m&odo SetSubCom~onent el en codigo anterior es que define un i dicador interno, guardado en el conjunto de propiedades Components t l e . El indicador (csSubComponent)
L.
afecta a! ; i i E Z Z s t r e a m i n g , que permite-que d's u b c e n t e ~ G u i propiedades se guarden en el archivo DFM. De hecho, el sistema de streaming ignora por defecto 10s componentes que no posee el formulario.
Pfoyr~ks Events
1- -
..
~elc
Name ParelllCobr
Figura 9.4. El Object Inspector puede ampliar automaticamente 10s subcomponentes, mostrando sus propiedades, como en el caso de la propiedad Timer del componente TMdClock.
La parte claw del codigo del componente cs el proccdimiento U p d a t e c l o c k , que consiste en una sola sentencia:
procedure TMdLabelClock.UpdateC1ock (Sender: TObject); begin / / d e f i n e l a h o r a a c t u a l como t i t u l o Caption : = TimeToStr ( T i m e ) ; end;
Estc metodo usa C a p t i o n , que es una propiedad no publicada, para quc un usuario del componentc no pueda modificarlo en el Object Inspector. El resultad0 de la scntencia es mostrar la hora actual. Esto ocurre continuamente, porque el mctodo esta conectado a1 evento O n T i m e r del temporizador.
-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.
Para mostrarlo, hemos creado un componente no visual que puede mostrar informacion acerca de una persona en una etiqueta y actualizarla automaticamente. El componente publica estas propiedades:
type TMdPersonalData published property property property property property end ;
=
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;
Hay cierta informacion basica y una propiedad de solo lectura D e s c r i p t i o n que devuelve toda la informacion de una vez. La propiedad O u t L a b e l esta conectada con un campo local privado llamado F L a b e l . En el codigo del componente, hemos usado esta etiqueta externa por medio de la referencia interna F L a b e l , como aqui:
p r o c e d u r e TMdPersonalData.UpdateLabe1; begin i f Assigned (FLabel) t h e n FLabel.Caption : = Description; end;
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);
if FLabel <> Value then begin FLabel : = Value; if FLabel < > n i l then begin UpdateLabel; FLabel-FreeNotification ( S e l f ) ; end; end ; end ;
NOTA: Esta caracteristica se usa raramente en Delphi 7. Dado que es probablemente dernasiado tarde para actualizar la arquitectura de componentes relacionados con datos que utilizan interfaces, todo lo que podemos esperar es que sera utilizada para expresar fbturas relaciones complejas dentro de la biblioteca.
Si tenemos componentes que ofrecen una interfaz concreto (aunque no sean parte de la misma subjerarquia), podemos declarar una propiedad de tipo interfaz y asignarle cualquiera de esos componentes. Por ejemplo, supongamos que tenemos un componente no visual asociado a un control para mostrar su resultado, algo parecido a lo que vimos en la seccion anterior. Habiamos usado una tecnica tradicional, uniendo el componente a una etiqueta, per0 ahora podemos definir una interfaz asi:
tYPe IMdViewer
interface
['{97668600-8E4A-4254-9843-59B98FEE6C54}']
Un componente puede usar su interfaz viewer para mostrar su resultado a otro control (de cualquier tipo). El listado 9.2 muestra como declarar un componente que usa esta interfaz para referirse a un componente externo.
Listado 9.2. Un cornponente que referencia un cornponente externo usando una interfaz. tYPe TMdIntfTest = class (TComponent) private FViewer: IViewer; FText: string; procedure SetViewer (const Value: IViewer); procedure SetText (const Value: string); protected procedure Notification (AComponent: TComponent; Operation: TOperation) ; override; pub1i shed property Viewer: IViewer read FViewer write SetViewer; property Text: string read FText write SetText; end;
{
TMdIntfTest
procedure TMdIntfTest.Notification (AComponent: TComponent; Operation: TOperation) ; var int f : IMdViewer; begin inherited; if (Operation = opRemove) and (Supports (AComponent, IMdViewer, intf) ) and (intf = FViewer ) then begin FViewer : = nil; end; end; procedure TMdIntfTest.SetText(const Value: string); begin FText : = Value; i f Assigned (FViewer) then FViewer .View (FText); end; procedure TMdIntfTest.SetViewer(const Value: IMdViewer);
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;
El uso de una interfaz implica dos diferencias relevantesj comparado con el traditional uso de un tip0 clase para referenciar un componente esterno. Primero, en el metodo Notification. debemos estraer la interfaz del componente pasado como parametro y compararlo con la interfaz que ya tenemos. Segundo, para llamar a1 metodo FreeNotification,debemos ver si el objeto que pasamos como parametro soporta la interfaz I Inter facecomponentReference. Esto se declara en la clase TComponent y ofrece un mod0 de volvernos a referir a1 componente (Getcomponent)y llamar a sus metodos. Sin esta a y d a hubieramos tenido que aiiadir un metodo similar a nuestra interfaz personalizada, porque a1 extraer una interfaz de un ob-jeto no hay forma automatica de referirse de nucvo a1 objeto. Ahora que tenemos un componente con una propiedad interfaz podemos asignarlo a cualquier componente (de cualquier parte de la jerarquia VCL) aiiadilndolc la interfaz Iviewer e implemcntando el mttodo View.Veamos un ejemplo:
type TViewerLabel = c l a s s (TLabel, IViewer) public p r o c e d u r e View(str: S t r i n g ) ; end: p r o c e d u r e TViewerLabel.View(const begin Caption : = str; end:
' I
str: String);
I .nc m a r m c h
a r ~ r l ~rl ~ c a r r n l l n mmnnn l d~
aiiadlendolo a1 KepOsltory o creando una plantllla usando la orden A d d t o Palette del menu de mCtodo abreviado del marco.
Como alternativa, podemos querer compartir el marco situandolo en un paquete y registrandolo como un componente. Tecnicamente, esto no es complicado: aiiadimos un procedimiento Register a la unidad del marco, aiiadimos la unidad a un paquete y lo escribimos. El nuevo componente/ marco aparece en la Component Palette igual que cualquier otro componente. Cuando colocamos este componente/marco en un formulario vemos sus subcomponentes. No podemos seleccionar estos subcomponentes con un clic de raton en el Form Designer, per0 si en la Object Tree View. A pesar de todo, cualquier cambio en estos componentes en tiempo de disefio se perdera a1 arrancar el programa o a1 guardar y recargar el formulario, porque 10s cambios en esos subcomponentes no se conservan (a diferencia de lo que ocurre con 10s marcos estiindar colocados en forrnularios). Veremos como aplicar una tecnica bastante sencilla para utilizar marcos en paquetes, demostrada mediante el componente MdFr amedC l o c k. La idea consiste en convertir 10s componentes que posee el formulario en subcomponentes, llamando a1 metodo Set SubComponent . Tarnbien hemos expuesto 10s componentes internos con propiedades, aunque no sea obligatorio (pueden seleccionarse la Object Tree View). Esta es la declaration del componente y el codigo de sus mttodos:
type TMdFramedClock = class (TFrame) Lahell: TLabel; Timerl: TTimer; Bevell: TBevel; procedure TimerlTimer(Sender: TObject); publia constructor Create (AOnwer: TComponent ) ; override; pub1 i shed property SubLabel: TLabel read Labell: property SubTimer: TTimer read Timerl; end; constructor TMdFramedClock.Create(A0nwer: TComponent); begin inherited; Timer1.SetSubComponent (true); Label1.SetSubComponent (true); end; procedure TMdFramedClock.TimerlTimer(Sender: TObject): begin Labell. Caption := TimeToStr (Time); end;
En este caso. en oposicibn a1 caso del reloj, no es necesario d e f i r las propiedades del temporizador ni conectar el evento temporizador a su funcion controlador manualmente, puesto que eso se realiza visualmente y se guarda en el archivo DFM del marco. Fijese tambikn en que no hemos
Component en el), por 10 que podemos intentar editarlo en tiempo de disefio y ver que todos 10s cambios se pierden, como hernos meecionado antes. Tras haber i~lstalado marco/componente, pademos usarlo en d q u i e r este aplicacion. En este caso concreto, &sde el momento en quc tqm1 5 el ij .0 marco en el formulario, el temporizador comamra a iwttdbm k etiqueta con la hora actual. Sin embargo, todavia se puede controh el evento OnTimer y el IDE de Delphi (que reconoce que el cornpone& BstB en el marco) definlra un m h d o con este c6digo predefinido:
procedure TForml.MdFramedCLocklTimerlTimer(Sender: bagi n MdFramedClockl.TimerlTimer(Sender); end;
TObject);
Desde el momento en que se conecta el temponzador, &haq en tiempo de diseilo, el reloj en vivo se detendrfi, porque se descollecta su contrdador de eventos 0rigma.I. Sin embargo, despub de compib-g cjerc,.latarel pr~grama, se restaurara el comportamiento ori& fa1d m , ai nahrraniodIa linea r anterior) y tambib se ejecutara c6digo persod* adic;ional, E t cornse de la9 marcos. Podernos enportmiento es cxa&mente lo qqe contra una demostracih c o r n p l e t a z m marMlcomponente en el ejemplo FrameCbck. @mica no cs'en absolutb Bt Como conclusi6e, podemos afinpar que neal. Es mucho mejor que en v a r h w s a&d~res & Delphi, .en la que 10s marcos dentro de 10s paquetes w se p d i a n util izar. per0 pfobablernente no merezca la pena el esfuerm. En el caso de pcquefias ~qanbaciones gruo pos de trabajo es mejor usar mmeos sinipl;es dnmcenados en el Repostbiy. En orgapiswkmes nub pp'a9des spa disbljbuk los,marcos a una audiencia mayor, mkha g&te preferid Great m eomponentes de rnodo tradicioi d, 9jIE maqiw. ETT palabaras,esperamoq que Borland ofrezca un sopork mis,(ieqpple<o para 4 desanollo vjsual de mrnponentes paquete basado m a&tk
La definicion de nuevas propiedades enumeradas, basadas en tipos de datos enumerados. La implementacion del metodo P a i n t del componente, que ofrece su interfaz de usuario y deberia ser lo suficientemente generic0 como para acomodar todos 10s valores posibles de las diversas propiedades, como su W i d t h y H e i g h t . El metodo P a i n t tiene una funcion importante en este componente grafico. El uso de propiedades de clases derivadas de TPer s i s t e n t , como T Pen y TBrush, y 10s temas relacionados con su creation, destruccion y control de sus eventos OnChange de forma interna en nuestro componente. La definicion de un controlador de eventos personalizado para el componente, que responda a la entrada del usuario (en este caso, hacer doble clic en la punta de la flecha). Para esto, sera necesario controlar directamente 10s mensajes de Windows y el uso de la API de Windows para partes graficas. El registro de propiedades en categorias del Object Inspector y la definicion de una categoria personalizada.
'=me
TMdArrowDir = (adup, adRight, adDown, adleft);
Este tipo enumerado define un miembro de datos privado del componente, un parametro del procedimiento utilizado para cambiarlo y el tip0 de la propiedad correspondiente. La propiedad A r r o w H e i g h t establece el tamaiio y F i l l e d si se rellena la punta de la flecha:
'=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) ;
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
cuando se coloca en un formulario, su t a m d o sera un h i c o pixel. Por esa r d n , es importante aiiadir un valor predefinido para las propiedades Width -. n e l g n r ; -. aennlr I - - ~iunpos2- CMSG C u r n o -.-I 1 w E ; s ue PIVPIGU~SIGS -LL -I------ J- ---- :-2-J-y y d-C-:- los UG va
7.-J
predefinidas en el constructor de la clase. Las tres propiedades personalizadas se leen directamente del campo correspondiente y se escriben usando tres metodos Set, que tienen todos la misma estructura estandar:
procedure TMdArrow.SetDirection begin i f FDirection <> Value then begin FDirection : = Value; ComputePoints; Invalidate; end; end;
(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.
que Estas directrices b 10s programas resulten m h fhiles de leer. El cornpilador no obliga a ello. Estas reglas son las seguidas por el mecanismo que utiliza Delphi para completar 10s nombres de las clases.
Estos puntos 10s establece el metodo privado Cornput e p o i n t s, llamado cada vez que cambia alguna de las propiedades del componente. Veamos un extracto de su codigo:
procedure TMdArrow.ComputePoints; var
// 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;
Cuando el componente sabe la posicion de la punta de flecha, su codigo de pintado resultara mas sencillo. Veamos un extract0 del codigo del metodo P a i n t :
procedure TMdArrow-Paint; var
// 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)
...
procedure SetPen (Value: TPen) ; procedure RepaintRequest (Sender: TObject); published property Pen: TPen read FPen write SetPen; end;
(AOwner: TCornponent);
...
Estos eventos OnChange se producen cuando una de las propiedades del lapiz cambia, todo lo que hay que hacer es pedir a1 sistema que pinte de nuevo nuestro componente:
procedure TMdArrow.RepaintRequest begin Invalidate; end;
(Sender: TObject);
Tambien debemos aiiadir un destructor a1 componente, para eliminar el objeto grafico de la memoria (y liberar sus recursos de sistema). Todo lo que tiene que hacer el destructor es llamar a1 metodo F r e e del objeto P e n . Las propiedades relacionadas con estos dos componentes requieren cierto control especial: en lugar de copiar el punter0 a 10s objetos, deberiamos copiar 10s datos internos del objeto pasado como parametro. La operacion estandar := copia 10s punteros, por lo que en este caso tenemos que usar en cambio el metodo
Assign.
procedure TMdArrow.SetPen begin FPen.Assign (Value); Invalidate; end:
(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;
Como C a n v a s usa una rutina de asignacion para el objeto lapiz, no solo estamos almacenando una referencia a1 lapiz en un campo del C a n v a s , sin0 que
estamos copiando toda su informacion. Esto significa que podemos destruir el objeto lapiz local (FPen) sin problemas y que modificar F P e n no afectara al lienzo hasta que P a i n t sea llamado y el codigo anterior se ejecute otra vez.
.
Figura 9.7. El resultado del cornponente Arrow con un lapiz grueso y una trarna especial.
TMdArrow = class (TGraphicControl) pub1 i shed property OnClick; property OnDragDrop; property OnDragOver; property OnEndDrag;
Gracias a esta declaration, 10s eventos anteriores (declarados originalmente en una clase padre) estaran ahora en el Object Inspector cuando se instale el componente. Sin embargo, a veces es necesario un evento personalizado. Para definir un evento nuevo, primer0 hay que asegurarse de que existe un tip0 de puntero de metodo valido para el evento; si no, debemos definir un tipo de evento nuevo. Este tip0 es un tipo de puntero de metodo. En ambos casos, tenemos que aiiadir a la clase un campo del tipo del evento. Veamos la definicion aiiadida en la parte privada de la clase TMdArrow:
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:
(Una vez mas utilizamos la convencion estandar al dar a1 evento un nombre que empieza con On). El puntcro de metodo FArrowDblCl i c k se activa (ejecutando la funcion correspondiente) dentro del metodo dinamico especifico ArrowDblClick. Esto ocurre solo si se ha especificado un controlador de eventos en el programa que usa el componente:
procedure TMdArrow.ArrowDb1Click; begin if Assigned (FArrowDblClick) then FArrowDblClick (Self) ; end;
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~lSfd evento, @neralmcntc, un usuario d d agqmnentes.
Uso de llamadas de bajo nivel a la API de Windows
, .
El metodo fArrowDblClic k se define en la parte protegida de la definition de tipo para permitir que las subclases futuras lo llamen y lo modifiquen. Basicamente, el controlador del mensaje de Windows wm LButtonDblCl k llama a1 metodo ArrowDblClick, pero s61o si se hizo doble clic dentro de la punta de flecha. Para probar csta caracteristica, podemos usar alguna de las funciones de la zona Windows API.
. - .
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.azJc q ~ ~ & x ~ i n & w s ~ a r 3 la 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; -
// 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
Nuestro codigo registra la propiedad Filled en dos categorias diferentes. Esto no es un problema, porque la misma propiedad se puede mostrar varias veces en el Object Inspector en diferentes grupos, como puede verse en la figura 9.8. Para probar el componente flecha, hemos creado un programa ejemplo muy sencillo, ArrowDemo, que permite modificar la mayoria de sus propiedades en tiempo de ejecucion. Este tip0 de prueba, despues de haber escrito un componente o mientras lo creamos resulta muy importante.
Figura 9.8. El componente Arrow define una categoria de propiedades personalizada, Arrow, como puede verse en el Object l n s p e c t o r . ~ a s propiedades pueden verse en multiples secciones, como la propiedad Filled en este caso.
m - X " ~ a categoria dc propieda&s Loca 1izable posee una funcion especial, relacionada con el uso del ITE (Entorno de Traduccion I n t e
do). Cuando una propiedad forma park de dicha categoria. su valor apz& cera fstado en el ITE como una propiedad que se puede traducir a o k o lenguaje.
a una subclase del componente. Un ejemplo claro es el de 10s cuadros de edicion que aceptan solo una entrada numerica. En lugar de unir a cada uno de ellos un controlador de eventos comun O n C h a r , podemos definir un nuevo componente simple. Sin embargo, dicho componente no controlara el evento, 10s eventos son solo para 10s usuarios de 10s componentes. En cambio, el componente puede o bien controlar el mensaje Windows directamente o sobrescribir un metodo, llamado con frecuencia manejador de mensajes de segundo nivel. La tecnica anterior se utilizaba habitualmente en el pasado pero hacia a 10s componentes ser especificos para la plataforma Windows. Para crear un componente portable a CLX y Linux (y, en el futuro, a la arquitectura .NET) deberiamos evitar 10s mensajes de segundo nivel Windows y, en su lugar, sobrescribir 10s metodos virtuales del componente basico y las clases de control.
NOTA: Cuando la mayoria de componentes VCL manejs. u mensaje . Windows, llaman a un rnanejador de mensajes de s e g h nivel (normalmente, un mitodo dhhmico), en lugar de ejecutar c6digo directammte con el metodo de respuesta a mensajes. Esta tecnica facilita personalizar el componente en una clase derivada. Habitualmente, un rnanejador de segundo nivel hara su propio trabajo y llamara despds a cudqukr controlador de eventos que el usuario del componente haya adtipado. Por tauto, siempre ci deberiamos llamar a inherited para dejar a1compoaente a tw d wento como se espera.
Ademas de la portabilidad, no hay razon por la que sobrescribir 10s manejadores de segundo nivel esistentes, sea una tecnica mejor que manejar directamente 10s mensajes Windows. Primero, esta tecnica es mas cercana a la perspectiva orientada a objetos. En lugar de duplicar el codigo de respuesta a mensajes de la clase b k i c a para despues personalizarlo, estamos sobrescribiendo una llamada a un metodo virtual, que 10s diseiiadores de VCL planearon para ser sobrescrita. Segundo, si alguien necesita derivar otra clase de una de nuestras clases de componentes, deberiamos facilitarle la posibilidad de personalizarla, y es muy facil que sobrescribir 10s manejadores de segundo nivel induzca a errores. Por ejemplo, podiamos haber creado este control de cuadro de edicion numeric0 manejando el mensaje de sistema wm-C h a r :
type TMdNumEdit = class (TCustomEdit) public procedure WmChar (var Msg: TWmChar); message wm-Char;
De todas maneras, el codigo es mas portable si sobrescribimos el metodo Keypress, como hemos hecho en el codigo del siguiente componente. En un
ejemplo posterior tendremos que manejar mensajes Windows personalizados porque no hay un metodo correspondiente que sobrescribir.
Este componente hereda de TCustomEdit en lugar de hacerlo de Tedit, por lo que puede ocultar la propiedad Text y exteriorizar en cambio la propiedad entera Value. Fijese en que no hemos creado un nuevo campo para alojar este valor, porque podemos utilizar la propiedad Text existente (pero ahora no publicada) . Para ello, sencillamente convertiremos el valor numeric0 en una cadena de texto y viceversa. La clase TCustomEdit (0, en realidad, el control de Windows que envuelve) dibuja automaticamente la informacion de la propiedad Text en la superficie del componente:
function TMdNumEdit-Getvalue: Integer; begin // d e f i n i d a como 0 en caso de e r r o r Result : = StrToIntDef (Text, 0) ; end ; procedure TMdNumEdit SetValue begin Text : = IntToStr (Value); end;
(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.
Podemos hacer esto sobrescribiendo el metodo interno Change y formateando el numero correctamente. Solo hay un par de pequeiios problemas a tener en cuenta. El primer0 es que para formatear el numero debemos tener una cadena que contenga un numero, per0 el texto del cuadro de edicion no es una cadena numerica que Delphi reconozca, ya que tiene separadores de millares, y no puede ser convertido directamente a un numero. Hemos creado una version modificada de la funcion StringToFloat llamada StringToFloatSkipping, para realizar esta conversion.
El segundo pequeiio problema, es que si modificamos el texto del cuadro de edicion, la posicion actual del cursor se perdera. Por eso, necesitamos salvar la posicion original del cursor, reformatear el numero, y restaurar la posicion del cursor (considerando que la posicion del cursor deberia cambiar en funcion de si se ha aiiadido o quitado un separador). Todas estas soluciones se contemplan en el siguiente codigo completo de la clase TMdThousandEdit:
type TMdThousandEdit = class (T~dNumEdit) public procedure Change; override; end; function StringToFloatSkipping (s: string) : Extended; var sl: string; I: Integer; begin // q u i t a r c a r a c t e r e s no numericos sl : = ' 1 ; for i : = 1 to length (s) do if s[i] i n [ ' 0 1 . . ' 9 ' ] then sl : = sl + s [i]; Result : = StrToFloat (sl); end; procedure TMdThousandEdit.Change; var CursorPos, // p o s i c i o n o r i g i n a l d e l c u r s o r LengthDiff: Integer; // n u m e r o d e n u e v o s s e p a r a d o r e s begin if Assigned (Parent) then begin CursorPos : = SelStart; LengthDif f : = Length (Text); Text : = FormatFloat ( I # , # # # ' , StringToFloatSkipping (Text)) ; LengthDif f : = Length (Text) - LengthDif f ; // mover e l c u r s o r a l a p o s i c i o n apropiada SelStart : = CursorPos + LengthDiff; end; inherited; end;
(+
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:
Hemos llamado a la version heredada de 10s metodos antes de hacer ninguna otra cosa. En el caso de la mayor parte de 10s controladores de scgundo nivel, esta es una buena costumbre, porque garantiza que ejecutamos el comportamiento estandar antes que cualquier comportamiento personalizado. A continuacion, se llama a la funcion de la API de Win32 P l a y S o u n d para que reproduzca el sonido. Podemos usar dicha funcion, definida en la unidad M S y s t e m , para m reproducir archivos WAV o sonidos del sistema, como demuestra el ejemplo SoundB. Esta es la descripcion textual del formulario del programa de ejemplo (del archivo DFM):
object MdSoundButtonl: TMdSoundButton Caption = 'Press ' SoundUp = 'RestoreUp ' SoundDown = 'RestoreDown ' end
NOTA: Elegir un valor apropiado para estos sonidos no es nada simple. Mas adelante en este capitulo, mostraremos cbmo aiIadir un editor de propiedades a1 componente para simplificar la operacion.
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).
ADVERTENCIA: Este es un tema bastante avanzado, por lo que aquellos lectores que sean nuevos en la creaci6n de componentes Delphi pueden saltarse esta seccion. Los mensajes de componente no estan documentados en el archivo de ayuda de Delphi, por lo que se ha considerado importante citarlos aqui.
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,
per0 algunos son mas complejos, traducciones de alto nivel y no simples reproyecciones. Ademas, 10s componentes envian sus propios mensajes y reenvian aquellos recibidos de Windows. Por ejemplo, cambiar un valor de propiedad o alguna otra caracteristica del componente puede requerir el informar a uno o mas componentes sobre dicho cambio. Podemos agrupar 10s mensajes en categorias: Los mensajes de activacion y foco de entrada se envian a1 componente que se activa o desactiva y que recibe o pierde el foco de entrada:
c m A c t i v a t e : Corresponde a1 evento OnActivate de formularios y de la aplicaci6n. c m-D e a c t i v a t e : Corresponde a OnDeactivate. c m-E n t e r : Corresponde a OnEnter.
c m-E x i t : Corresponde a OnExit.
c m F o c u s C h a n g e d : Se envia siempre que cambia el foco entre 10s componentes del mismo formulario (mas adelante, veremos un ejemplo con este mensaje).
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
Si se siguen estos mensajes, eso puede ayudarnos a mantener la pista de 10s cambios de una propiedad. Podemos necesitar responder a estos mensajes en un nuevo componente, per0 no es probable. Los mensajes relacionados con las propiedades ParentXxx: c m P a r e n t F o n t C h a n g e d , c m P a r e n t C o l o r C h a n g e d , cm ParentCtl3DChanged,cm~arentBiDiModeChanged y c m-P a r e n t ~ h o w ~ i n t ~ h a n g e d similares a 10s mensajes del grupo Son anterior.
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 r: a 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 ~oik~ain~indow y UnhookMainWindow . 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 1 Key: Controlado por controles que interpretan
tecGs especiales de un mod0 poco usual (por ejemplo, usando la tecla Tabulador para navegar como hacen componentes Grid). Mensajes para componentes especificos:
cm-~ e t ~ a t a L i k : Usado por controles DBCtrlGrid. n cm-TabFontChanged: Usado por 10s componentes TabbedNotebook. cm B u t t o n P r e s s e d : Usado por SpeedButtons para notificar a otros componentes SpeedButton parejos (para activar el comportarniento boton de radio). cm-De f e r L a y o u t : Usado por componentes DBGrid.
Mensajes de implementation de metodos, como cm R e c r e a t e nd, llaW : 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 cn-CtlColorEdit, cn-CtlColor~istbox, -CtlColorMsgbox, cn-CtlColorScrollbar y cn-CtlColorStatic. Hay mas notificaciones de controles definidas para soporte de controles comunes (en la unidad ComCtrls).
Para evitar posteriores procesamientos de la tecla Intro, se asigna a1 resultado del mensaje un 1:
p r o c e d u r e TForml.CMDialogKey(var Message: TCMDialogKey); begin if (Message.CharCode = VK-RETURN) t h e n begin Perform (CM-DialogKey, VK-TAB, 0 ) ; Message .Result : = 1; end else inherited; end;
El segundo mensaje, cm DialogChar,monitoriza teclas abreviadas. Esta tkcnica puede resultar util para crear accesos directos personalizados sin definir un menu extra para ellos. Aunque este codigo es correct0 para un componente, en una aplicacion normal esto puede lograrse mas facilmente controlando el evento OnShortCut del formulario. En este caso. trazamos las teclas especiales en una etiqueta:
p r o c e d u r e TForml.CMDialogChar(var Msg: TCMDialogChar); begin Labell.Caption : = Labell.Caption + Char (Msg.CharCode); inherited; end;
Finalmente, el formulario maneja el mensaje cmd F o cuschanged,para responder a cambios de foco sin necesidad de controlarel evento OnEnter de cada uno de estos componentes. Una vez mas, la accion muestra una descripcion del componente que recibe el foco.
p r o c e d u r e TForml.CmFocusChanged(var Msg: TCmFocusChanged); begin Label5.Caption : = 'El foco e s t d en ' + Msg.Sender.Name; end ;
La ventaja de esta tecnica esta en que funciona independientemente del tip0 y numero de componentes que aiiadamos a1 formulario, y hace esto sin necesidad de ninguna accion por nuestra parte. Es un ejemplo trivial para un concept0 tan avanzado, per0 si le aiiadimos el codigo del componente ActiveButton, tendremos a1 menos unas pocas razones para observar estos mensajes especiales no documentados. A veces, escribir el mismo codigo sin su soporte, puede volverse extremadamente complejo.
ponentes graficos sencillos, vamos a crear ahora un componente no visual. La idea basica es que 10s formularios son componentes. Cuando hemos creado un formulario que podria resultar especialmente util cn varios proyectos, podemos aiiadirlo a1 Object Repository o hacer de el un componente. La segunda tecnica es mas compleja que la primera, pero hace que el uso del nuevo formulario resulte mas sencillo y permite distribuir el formulario sin su codigo fuente. Como ejemplo, crearemos un componente basado en un cuadro de dialogo personalizado. intentando imitar a1 masimo posible el comportamiento dc 10s componentes cuadro de dialogo estandar de Delphi. El primer paso para construir un cuadro de dialogo en un componentc consiste en escribir el codigo para el propio cuadro de dialogo, usando la tdcnica estandar de Delphi. Definimos un nuevo formulario y trabajamos como siempre sobre el. Cuando un componente se basa en un formulario, podemos diseiiar casi visualmente el componente. Por supuesto, cuando se ha creado el cuadro de dialogo, tenemos que definir un componente sobre el de un mod0 no visual. El cuadro de dialogo estandar que vamos a construir se basa en un cuadro de lista, porquc cs comun para dejar que un usuario escoja un valor de una lista de cadenas. Hemos personalizado este comportamiento comun en un cuadro de dialogo y. despues. lo hemos utilizado para crear un componente. El formulario simple ListBoxForm que hemos creado tiene un cuadro de lista y 10s tipicos botones OK y Cancel; como se puede ver en su descripcion textual:
object M d L i s t B o x F o r r n : T M d L i s t B o x F o r r n Borderstyle = bsDialog C a p t i o n = 'ListBoxForm' object L i s t B o x l : T L i s t B o x OnDblClick = ListBoxlDblClick end object B i t B t n l : T B i t B t n K i n d = bkOK end object B i t B t n 2 : T B i t B t n Kind = bkCancel end end
El unico mdtodo de este formulario cuadro de dialogo se refiere a1 evento doble clic del cuadro de lista, que cierra el cuadro de dialogo como si el usuario pulsase el boton OK, configurando la propiedad ModalResult del formulario como mr0 k . Cuando el formulario funciona, podemos comenzar a cambiar su codigo fuente, aiiadiendo la definicion de un componente y eliminando la declaracion de la variable global del formulario.
NOTA: Para componentes basados en un formulario, podemos usar dos archivos de codigo fbente en Pascal: uno para el formulario y el otro para el componente que lo encapsula. Tambien es posible colocar tanto el compo-
ejemplo. En teoria, seria mejor declarar la clase formulario en la parte de implementacion de la unidad, oculthndola de 10s usuarios del componente. Pero en la practica tsta no es una buena idea. Para manipular el formulario visualmente en el Diseiiador de Formularios, la declaracion de la clase formulario debe aparecer en la seccion de interfaz de la unidad. La logica de este comportamiento de la IDE de Delphi esth, entre otras, en que minimiza la cantidad de codigo que el gestor de modulos debe cornprobar para enwntrar la declaracion del formulario (una operacion que debe realizar a menudo para mantener la sincronia del formulario visual con la definicion de la clase). Lo mas importante es la definicion del componente TMdLis t B o x D i a l o g . Este componente se define como "no visual" porque su clase ascendente inmediata es TComponent. El componente tiene una propiedad publica y estas tres propiedades publicadas: Lines: Es un ob.jeto TS t r i n g s , a1 que se accede mediante dos metodos, y S e t L i n e s . Este segundo metodo utiliza el procedimiento A s s i g n para copiar 10s nuevos valores en el campo privado que corresponde a esta propiedad. Este objeto interno se inicia en el constructor C r e a t e y se destruye en el metodo D e s t r o y .
G e tLines
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;
La mayoria del codigo de este ejemplo pertenece a1 metodo E x e c u t e , una funcion que devuelve T r u e o Fa 1 e , dependiendo del resultado modal del cuas dro de dialogo. Esto coincide con el metodo E x e c u t e de la mayoria de componentes de cuadro de dialogo estandar de Delphi. La funcion E x e c u t e crea el formulario de forma dinamica, configura algunos de sus valores utilizando las propiedades del componente, muestra el cuadro de dialogo y, si el resultado es correcto, actualiza la selection actual:
f u n c t i o n TMdListBoxDialog.Execute: Boolean; var ListBoxForm: TListBoxForm; begin i f FLines .Count = 0 then r a i s e EStringListError.Create ('No hay elementos en la lista') ; ListBoxForm : = TListBoxForm.Create ( S e l f ) ; try ListBoxForm.ListBoxl.Items : = FLines; ListBoxForm.ListBoxl.ItemIndex : = FSelected; ListBoxForm.Caption : = FTitle; i f ListBpxForm.ShowModa1 = mrOk then begin Result : = True; Selected : = ListBoxForm.ListBoxl.ItemIndex; end else Result : = False; finally ListBoxForm. Free; end ; end;
El codigo esta dentro de un bloque t r y / f i n a l l y , de mod0 que si ocurre un error de ejecucion a1 mostrar el cuadro de dialogo, el formulario sera destruido de todas maneras. Hemos usado excepciones tambien para lanzar un error si la lista esta vacia cuando un usuario lo ejecuta. Este es un error de diseiio, y usar una excepcion es una buena tecnica para corregirlo. El resto de metodos del componente son sencillos. El constructor crea la lista de cadenas F L i n e s , que es borrada por el destructor; 10s metodos G e t L i n e s y S e t L i n e s operan sobre la cadena como un todo, y la funcion G e t S e l I tern (mostrada a continuation) devuelve el texto de un elemento dado:
f u n c t i o n TMdListBoxDialog.GetSel1tem: begin
string;
if (Selected >= 0 ) and (Selected < FLines.Count) then Result : = FLines [Selected] else Result : = " ; end ;
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
lista de valores. Son ejemplos de propiedades de coleccion en Delphi la propiedad Colums del componente DBGrid y la propiedad Panels del componente TStatusBar
one
three
Figura 9.10. El ejemplo ListDialDemo muestra el cuadro de dialogo encapsulado en un componente ListDial.
Una coleccion es, basicamente, un contencdor de objetos de un tip0 concreto. Por esta razon. para definir una coleccion, debemos heredar una nueva clase de TCollection y otra de TCollectionItem.Esta segunda clase definc 10s contenidos en la colcccion; la coleccion se crea pasandole la clasc de 10s objctos que contendra. La clase coleccion no solo manipula 10s elementos de la misma, sin0 que tambien cs responsable de crear nuevos objctos cuando es llamado su metodo A d d . No podcmos crear un objeto y afiadirlo despues a una coleccion existente. El listado 9.3 muestra dos clases para 10s elementos y la coleccion, con su codigo mas significativo:
Listado 9.3. Las clases para una coleccion y sus elementos.
type TMdMyItem = class (TCollectionItem) private FCode: Integer; FText: string; procedure SetCode(const Value: Integer); procedure SetText (const Value: string) ; published property Text: string read FText write SetText; property Code: Integer read FCode write SetCode; end; TMdMyCollection = class (TCollection) private FComp : TComponent; FCollString: string;
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: function TMdMyCollection.Get0wner: begin Result : = FComp; end;
(CollOwner: TComponent);
TPersistent;
procedure TMyCollection.Update(Item: TCollectionItem); var str: string; i: Integer; begin inherited; // a c t u a l i z a r t o d o en c u a l q u i e r c a s o . . . str : = ' 1 . for i : = 0 to Count - I do begin str : = str + (Items [i] as TMyItem) .Text; i f i < Count - 1 then str : = str + end; FCollString : = str; end;
I - ' ;
La coleccion debe definir el metodo GetOwner para que el IDE de Delphi lo muestre correctamente en el editor de propiedades de coleccion. Por esta razon, necesita un enlace a1 componente que lo contiene, el propietario de la coleccion (almacenado en el campo FComp en el codigo). Podemos ver la coleccion de este componente de muestra en la figura 9.1 1. Cada vez que cambia la informacion en un elemento de coleccion, su codigo llama a1 metodo C h a n g e d (pasando T r u e o F a l s e para indicar si el cambio se limita a1 elemento o se refiere a1 conjunto de elementos en la coleccion). Como resultado de esta llamada, la clase T C o l l e c t i o n llama a1 metodo virtual U p d a t e , que recibe como parametro el elemento unico que solicita la actualizacion, o n i l si han cambiado todos 10s elementos (y cuando en el metodochanged es llamado con T r u e como parametro). Podemos sobrescribir este metodo para actualizar 10s valores de otros elementos de la coleccion, de la propia coleccion o del componente destino.
Figura 9.11. El editor de colecciones, con la Object TreeView y el Object Inspector para el elemento coleccion.
En este ejemplo actualizamos un string con un resumen de la informacion de la coleccion, que hemos afiadido a esta y que el componente contenedor mostrara como una propiedad. Usar una coleccion dentro de un componente es sencillo. Declaramos una coleccionj la creamos en el constructor, la liberamos a1 final y la mostramos a traves de una propiedad:
type TCanTest = class (TComponent) private FColl: TMyCollection; function GetCollString: string; public constructor Create (downer: TComponent); override; destructor Destroy; override; published property MoreData: TMyCollection read FCollwrite SetMoreData; property CollString: string read GetCollString; end; constructor TCanTest.Create(a0wner: TComponent); begin inherited; ; FColl : = TMyCollection.Create (Self) end; destructor TCanTest.Destroy; begin FColl. Free; inherited; end: procedure TCanTest.SetMoreData(const Value: TMyCollection);
begin
string;
Result
end;
:=
FColl.FCollString;
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 Code
end end
= =
'tres' 3
>
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;
El metodo HandlesTarget,uno de 10s tres metodos clave de las acciones, lo proporciona la clase TLi stControlAction,con el siguiente codigo:
function TListControlAction.HandlesTarget(Target: TObject) : Boolean;
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;
La funcion TargetList utiliza la funcion GetControl de la clase TLi stCont ro lAct ion,que devuelve el cuadro de lista conectado a la accion en tiempo de disefio o el control de destino, el control cuadro de lista con el foco de entrada. Por ultimo, 10s tres metodos ExecuteTarget realizan sencillamente las acciones correspondientes en el cuadro de lista de destino:
procedure TMdListCopyAction.ExecuteTarget (Target: TObject); begin with TargetList (Target) do Clipboard.AsText : = Items [ItemIndex]; end; procedure TMdListCutAction.ExecuteTarget(Target: 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;
Para probar el uso de estas tres acciones personalizadas, hemos creado el ejemplo ListTest (incluido con el codigo fuente de este capitulo). Este programa tiene dos cuadros de lista junto a una barra de herramientas que contiene tres botones conectados a las tres acciones y un cuadro de testo para introducir nuevos valores. El programa permite cortar, copiar y pegar elementos de un cuadro de lista. Podemos pensar que no es nada especial pero lo raro es que el programa no time codigo.
ADVERTENCIA: Para establecer una imagen para una accion (y para definir 10s valores por defect0 de las propiedades en general) debemos usar el tercer parimetro del procedimiento Regis t e rAc t ions, que es un modulo de datos que contiene la lista de imhgenes y una lista de acciones con 10s valores predefinidos. Como tenemos que registrar las acciones antes de poder establecer dicho modulo de datos, necesitaremos de un doble registro a1 desarrollar estas acciones. Este tema es bastante complejo, poi 10 que no se tratara aqui, pero puede encontrarse una description detallada en h t t p : / / w w w . b l o n g . c o m / ~ o n f e r e n c e s / B o r ~ o n 2 O O 2 / . . . .-,. - .. .. HC t ions / z I I u .nrm en las secclones "Kegrsrenngxanaara Acnons.. y "StandardActions And Data Modules". .
,-a
v . . .
,.
1 I
TRUCQ: La convencibn predefinida en Delphi @ Morni- a h c l a k do t editor de propiedades cop un nombrg gue acabc con Proper& y. & s la$, editom de componentes con un nombre t$e akabe eon Edit*
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;
El mCtodo GetValues llama sencillamente a1 procedimiento que recibe como parametro varias veces, una vez para cada cadena que quiere aiiadir a la lista desplegable (como se ve en la figura 9.12):
procedure TSoundProperty.GetValues(Proc!: begin 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).
Los metodos GetValue y Setvalue son definidos por la clase basica, el editor de propiedades de cadena. Leen y escriben el valor de la propiedad del componente que estamos editando. Como alternativa, podemos acceder a1 componente que estamos editando usando el mdtodo Getcomponent (que requiere un
parametro indicando en cual de 10s componentes seleccionados estamos trabajando, 0 indica el primer componente). ~ u a n d o accedemos 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.
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
TObject);
ConboBoxl.Text
end ; procedure begin end ;
:=
0penDialogl.FileName;
TSoundForm.btnPlayClick(Sender:
TObject);
Es complicado determinar si un sonido esta debidamente definido y disponible (podriamos comprobar el fichero, per0 10s sonidos del sistema crean algunos problemas). La funcion Playsound devuelve un codigo de error si no encuentra
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.
TRUCO: Si queremos amplid este ejemplo, pademos afiadir g r a f i c o ~ la a lista desplegable del Objed lirspector (si p$demos decidir q u t gr%fico asociar a un sonido en particular).
Esta llamada registra el editor especificado en el ultimo parametro para usar con propiedades del tip0 string (el primer parametro), per0 solo para un componente especifico y para una propiedad con un nombre especifico. Estos dos ultimos valores se pueden omitir para ofrecer mas editores generales. Registrar este editor permite al Object Inspector mostrar una lista de valores y el cuadro de dialog0 al que llama el metodo E d i t . Para instalar este componente, podemos aiiadir simplemente su codigo fuente a un paquete nuevo o existente. En lugar de aiiadir esta unidad y las otras al paquete MdPack, hemos creado un segundo paquete, que contiene todos 10s afiadidos incorporados en el capitulo. Su nombre es MdDesPk. Lo nuevo de este paquete es que hemos compilado utilizando la directiva de compilador ($DESIGNONLY). Esta directiva se usa para marcar paquetes que pueden interactuar con el entorno Delphi, instalando componentes y editores, per0 que las aplicaciones que hemos creado no necesitan en tiempo de ejecucion.
-
- -
NOTA: El codigo fuente de todas las lwmierrtcua adicionales esta en el subdirectorio MdDesPk?junto cone1 &dig0 ddpaqueteutilizado para instalarlas. No hay ejemplos que demuestren el modo & utilizacion de dichas
La unidad del editor de propiedades usa la unidad SoundB, que define el componente TMdSoundButton. Por esa razon, el nuevo paquete deberia referirse a1 paquete existente. Veamos el codigo inicial (aiiadiremos otras unidades mas adelante en este capitulo):
package MdDes Pk; ( $ R *. RES] ($ALIGN O N ]
...
'1
(SoundPorm];
propiedades. Particularmente, la intencion original era permitir a un asistente, o a algun codigo directo, establecer multiples propiedades de golpe, en lugar de hacerlo de uno en uno.
Cuando nos acostumbramos a la idea de que un "verbo" (verb) no es otra cosa que un nuevo elemento del menu con una accion correspondiente que ha de ejecutar, 10s nombres de 10s metodos de esta interfaz resultan bastante intuitivos. Esta interfaz es mucho mas simple que las que hemos visto para 10s editores de propiedades.
Para implementar este editor de propiedades, el programa habra de sobrescribir 10s cuatro metodos expuestos anteriormente:
uses DesignIntf; type TMdListCompEditor = c l a s s (TComponentEditor) f u n c t i o n GetVerbCount: Integer; override; f u n c t i o n GetVerb (Index: Integer) : string; override; p r o c e d u r e ExecuteVerb(1ndex: Integer); override; p r o c e d u r e Edit; o v e r r i d e ; end;
El primer metodo sencillamente devuelve el numero de elementos del menu que afiadiremos a1 menu local, en este caso 3 . A este metodo se le llama so10 una vez, antes de mostrar el menu. En cambio, a1 segundo metodo se le llama para cada elemento del menu, por lo tanto, tres veces:
f u n c t i o n TMdListCompEditor.GetVerb (Index: Integer): string; begin c a s e Index o f 0: Result : = ' MdListDialog (Cantu)'; 1: Result : = '&Acerca de es te componente. . . '; 2: Result : = '&Vista previa.. '; end; end;
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:
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
Los archivos ejecutables de Windows pueden tener dos formatos: programas (EXE) y bibliotecas de enlace dinamico (DLL). Cuando escribimos una aplicacion Delphi, generalmente creamos un archivo de programa. Pero las aplicaciones Delphi utilizan a menudo llamadas a funciones almacenadas en bibliotecas din& micas. Cada llamada directa a una funcion de la API de Windows, accede realmente a una biblioteca dinamica. Es sencillo generar una DLL en el entorno Delphi, aunque pueden surgir algunos problemas debido a la naturaleza de las DLL. Escribir una biblioteca dinamica en Windows no es siempre tan simple como parece, ya que la biblioteca dinamica y el programa que la llama, deben de acordar las condiciones de la llamada, el tipo de parametros y otros detalles. Este capitulo describe 10s fundamentos de la programacion de las DLL desde el punto de vista de Delphi. La segunda parte del capitulo se centrara en un tipo especifico de biblioteca de enlace dinamico: el paquete Delphi. Los paquetes Delphi ofrecen una buena alternativa a las DLL simples, a pesar de que pocos programadores les sacan partido si no es para el desarrollo de componentes. Aqui veremos algunos trucos y tecnicas para utilizar paquetes para dividir en partes mas pequeiias una aplicacion grande. Este capitulo comenta 10s siguientes temas: Creacion y utilizacion de las DLL en Delphi. Llamadas a funciones DLL en tiempo de ejecucion.
Compartir datos en las DLL. Estructura de 10s paquetes Delphi. Inclusion de formularios en 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:,,
la flexittilidad obtenida rnediante las funciones virtuales y el reducido ta. - - .. 'I . mano ae 10sarcmvos ejecurames que se conslgue Ilmltanao el uso ae esas
3
d
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
informacion de la declaracion externa de la subrutina para instalar algunas tablas en el archivo ejecutable. Cuando Windows carga el archivo ejecutable en memoria, carga primer0 todas las DLL necesarias y, a continuacion, arranca el programa. Durante este proceso de carga, Windows rellena las tablas internas del programa con las direcciones de las funciones de las DLL en memoria. Si por alguna razon no se encuentra la DLL o una rutina referenciada no esta en una DLL encontrada, el programa ni siquiera arranca. Cada vez que el programa llama a una funcion externa, utiliza esta tabla interna para reenviar la llamada a1 codigo de la DLL (que ahora esta situado en el espacio de direcciones del programa). Fijese en que en esta estructura no hay dos aplicaciones diferentes. La DLL se transforma en parte del programa en ejecucion y se carga en el mismo espacio de direcciones. Todo el paso de parametros tiene lugar en la pila de la aplicacion (porque la DLL no tiene una pila aparte) o en 10s registros del procesador. Dado que una DLL se carga en cl espacio de direcciones de la aplicacion, cualquier asignacion de memoria de la DLL o cualquier informacion global que esta crea, reside cn el espacio de direcciones del proceso principal. Por ello, pueden pasarse informacion y punteros a memoria directamente entre la DLL y el programa. Esto puede extenderse a1 paso de referencias a objetos, lo que puede resultar problematico porque el ejecutable y la DLL pueden tener una clase compilada diferente (para solucionar esto se pueden utilizar 10s paquetes, como se vera posteriormente en este capitulo). Existe otra tecnica de uso de las DLL aun mas dinamica que la que acabamos dc mcncionar. Dc hccho, cn ticmpo dc cjccucion, podemos cargar una DLL en memoria, buscar una funcion (siempre que sepamos su nombre) y llamar a la funcion por su nombre. Esta tecnica requiere un codigo mas complejo y emplea mas tiempo en localizar la funcion. Sin embargo, la ejecucion de la funcion posee la misma velocidad que la llamada de una DLL cargada de forma implicita. Por el contrario, no es necesario que la DLL este disponible a1 arrancar el programa. Usaremos este enfoque mas adelante en el ejemplo DynaCall.
--
--
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.
Tendra que aparecer en la lista de la clausula exports de la DLL. Esto hace que la rutina sea visible para 10s programas externos. Las funciones exportadas deberian declararse tambien como stdcall, para utilizar la tecnica de paso de parametros estandar de Win32, en lugar de la tecnica de paso de parametros optimizada reg is ter (predefinida en Delphi). La excepcion a esta norma es si queremos usar estas bibliotecas solo desde otras aplicaciones Delphi. Podemos usar otra tecnica de llamada, suponiendo que el otro compilador la entienda (como cdecl, que es la que utilizan por defect0 10s compiladores de C). Los tipos de parametros de una DLL deberian ser tipos predefinidos de Windows (sobre todo, tipos de datos compatibles con C), a1 menos si queremos ser capaces de usar la DLL en otros entornos de desarrollo. Aun hay mas normas para la exportacion de cadenas de caracteres, como se vera en el ejemplo FirstDLL. Las DLL pueden utilizar datos globales que no compartiran las aplicaciones que las llamen. Cada vez que una aplicacion carga una DLL, almacena 10s datos globales de la DLL en su propio espacio de direcciones, como veremos en el ejemplo D11Mem. Las bibliotecas Delphi deberian capturar todas las excepciones internas, salvo que pretendamos usar la biblioteca solo desde otros programas Delphi.
A continuacion, en la parte de implementacion, en lugar de ofrecer el codigo de cada funcion, la unidad remite a la definicion externa en una DLL:
cons t gdi32 function function function function
=
'gdi32.dl11;
PlayMetaFile; external gdi32 name 'PlayMetaFile'; PaintRgn; external gdi32 name ' PaintRgn' ; PolyPolygon; external gdi32 name 'PolyPolygon'; PtInRegion; external gdi32 name 'PtInRegion';
--_-1 - - L quc aparozca c~sIm u ~ -~ o- 1 - ~ p r u-o n e L: u GUrrespondiente 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.
ca a ;
P , r)..:i~-LTT
NOTA: En Windows.PAS se utiliza mucho la directiva {SEXTERNALSYM i de n t i fie r ) . Esto no tiene mucho que ver con Delphi, sin0 que se apli-
-- -
, Duuucr.
JXCG S ~ D V I U WIISL
-i-t-i-
-__:A-
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;hi incluye la traduccih a1 lenguaje D e w de uaa $ran cantidad de las API de Windows, como podemas ver en bs ficheros de la
www .d e l p h i - j edi org.
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 n) ; e --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 " ) . que sigue son declaraciones de las tres funciones en el ejemLo plo 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 ' ;
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,
si 10s dos nombres se corresponden, podemos omitir la directiva name, como en el caso de la tercera funcion anterior. Si no estamos seguros de 10s nombres reales de las funciones exportadas por la DLL, podemos usar el programa de linea de comandos TDump de Borland, disponible en la carpeta Delphi BIN, con el parametro -ee. Hay que recordar aiiadir la directiva s t d c a l l a cada definicion, por lo que el modulo de llamada (la aplicacion) y el modulo que se va a llamar (la DLL) usan la misma convencion para pasar parametros. De no hacerlo asi, obtendremos valores aleatorios pasados como parametros, un error que es muy dificil rastrear.
~-
-~ -
---
.....-.
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.
La sentencia l i b r a r y indica que queremos crear una DLL en lugar de un archivo ejecutable. Ahora, podemos aiiadir rutinas a la biblioteca y listarlas en una sentencia e x p o r t s :
function Triple ( N : Integer) : Integer; begin try R e s u l t : = N * 3; except stdcall;
:= N
1;
(N:
Integer) : Integer;
stdcall;
* * *
2;
2;
-1;
Double;
En esta version basica de la DLL, no es necesaria la sentencia u s e s , per0 en general, el archivo principal del proyecto incluye solo la sentencia e x p o r t s , mientras que las declaraciones de funcion se colocan en una unidad aparte. En el codigo fuente final del primer ejemplo FirstDLL, hemos cambiado ligeramente el codigo respecto a la version que se muestra anteriormente, para que aparezca un mensaje cada vez que se llama a una funcion. Hay dos formas de hacer esto. La mas sencilla consiste en usar la unidad Dialogs y llamar a la funcion ShowMessage. El codigo necesita que Delphi enlace una cantidad considerable de codigo VCL con la aplicacion. Si enlazamos estaticamente la VCL con esta DLL, el tamaiio resultante sera de unos 37.5 KB. La razon es que la hncion ShowMessage muestra un formulario VCL que contiene controles VCL y utiliza clases graficas VCL. Estas remiten de forma indirecta a conceptos como el sistema de streaming de la VCL y 10s objetos VCL aplicacion y pantalla. En el caso de este ejemplo, una alternativa mejor consiste en mostrar 10s mensajes usando las llamadas directas de la API, utilizando la unidad Windows y llamando a la funcionMessageBox, de tal mod0 que el codigo VCL no sea necesario. Este cambio en el codigo reduce el tamaiio de la aplicacion a aproximadamente 40 KB.
NOTA: Esta enorme diferencia de tamaiio subraya el hecho de que no debemos w a r en exceso las DLL en Delphi, para no compilar el cbdigo de la VCL en diversos archivos ejecutables. Por supuesto, podemos reducir el tamaiio de una DLL de Delphi utilizando paquetes en tiempo de ejecucibn.
Si ejecutamos un programa de prueba como el ejemplo CallFrst, que utiliza la version de la DLL basada en API, su comportamiento no sera el adecuado. De hecho, podemos pulsar 10s botones que llaman a las funciones DLL varias veces sin cerrar primer0 10s cuadros de mensaje que muestra la DLL. Esto se debe a que
el primer parametro de la llamada MessageBox de la API anterior es cero. En cambio, su valor deberia ser el manejador del formulario principal del programa o dcl formulario de la aplicacion, informacion no disponible dentro de la propia DLL.
;
- -
- - -- - NOTA: Tambien es ~ o s i b l hacer lo contrario: ~ ~ d e m io s~ o r t a una serie e m r de funciones sirnilares desde una DLL y definirlas todas como funciones sobrecargadas en Ia declaration en Delphi. La unidad 0penGL.PAS de
desarrollo de Windows deben soportar 10s tipos basicos de la API, por lo que si nos atenemos a ellos, nuestra DLL podra ser usada en otros entornos de desarrollo. Ademas, las variables de archivos en Delphi (archivos de texto y archivos binarios de registro) no se deberian pasar fuera de las DLL, per0 podemos usar 10s manejadores de archivo de Win32. Aunque pensemos en usar la DLL solo desde una aplicacion en Delphi, no podemos pasar cadenas en Delphi (ni matrices dinamicas) mas alla de 10s limites de la DLL sin tomar ciertas precauciones. Esto se debe a1 mod0 en que Delphi administra las cadenas en memoria (asignando, reasignando y liberando la memoria automaticamente). La solucion a1 problema consiste en incluir la unidad de sistema ShareMem tanto en la DLL como en el programa que la usa. Esta unidad debera incluirse como la primera de cada proyecto. Es mas, debemos distribuir el archivo BorlndMM.DLL (Borland Memory Manager, Administrador de memoria de Borland) junto con el programa y la biblioteca especifica. En el ejemplo FirstDLL, hemos incluido ambas tecnicas: una funcion recibe y devuelve una cadena en Delphi y la otra recibe como parametro un punter0 P C h a r , que a continuacion rellena la propia funcion. La primera funcion es muy sencilla:
function Doublestring (S: string; Separator: Char) : string; stdcall ; begin try Result := S + Separator + S; except Result : = ' [ e r r o r ] '; end ; end :
La segunda funcion es bastante compleja porque las cadenas P C h a r no tienen un operador + sencillo ni son directamente compatibles con caracteres. El separador habra de convertirse en una cadena antes de aiiadirlo. Veamos el codigo completo, que utiliza buffers P C h a r de entrada y salida, compatibles con cualquier entorno de desarrollo Windows:
function DoublePChar (BufferIn, Bufferout: PChar; BufferOutLen: Cardinal; Separator: Char): LongBool; stdcall; var SepStr: array [O. .l] of Char; begin try // s i e l b u f f e r e s 1 0 s s u f i c i e n t e m e n t e a m p l i o if BufferOutLen > StrLen (BufferIn) * 2 + 2 then begin // c o p i a e l b u f f e r d e e n t r a d a e n e l b u f f e r d e s a l i d a StrCopy (BufferOut, Buf ferIn) ; // c r e a l a cadena d e l s e p a r a d o r ( e l v a l o r mds e l terminador nulo)
SepStr [0] : = Separator; SepStr [I] : = #O; // adjunta e l separador StrCat (Bufferout, SepStr) ; / / a d j u n t a e l b u f f e r d e e n t r a d a u n a v e z mis StrCat (BufferOut, Buff erIn) ; Result : = True; end else
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&
[ K O 1
~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 muv c6modo Dara ~~. . -" . -. ,. 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 a e c 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 '
procedure TForml.ButtonlClick(Sender: TObject); var HInst : THandle; FPointer: TFarProc; MyFunct: TIntFunction; begin ; HInst : = SafeLoadLibrary (DllName) if HInst > 0 then try FPointer : = GetProcAddress (HInst, PChar (Edit1.Text)) ; if FPointer <> nil then begin MyFunct : = TIntFunction (FPointer); SpinEditl-Value : = MyFunct (SpinEditl.Value); end else ShowMessage ( ' f u n c i o n DLL ' + Editl.Text + ' n o e n c o n t r a d a I) ; finally ; FreeLibrary (HInst) end else ShowMessage (' b i b l i o t e c a ' + DllName + ' no encontrada I ; ) end ;
usa el administrador de memoria de nhicamente debe hacer lo mismo. Yor ello, debemOS asiadlr la unldad shareMem en el proyecto del ejemplo DynaCall. Evidentemente, esto no se hacia asi con versiones anteriores de Delphi, en caso de que la biblioteca no utilizara cadenas de caracteres. Debemos tener en cuenta que si omitimos esta inclusion, se producira un error de sistema.
Cuando tenemos el puntero para llamar a un procedimiento en Delphi, podemos convertirlo en un tip0 de procedimiento y: a continuacion, usar la variable de
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
de la biblioteca. No todos 10s problemas se resuelven con el codigo del ejemplo. Una solucion mejor puede ser compilar el programa y la biblioteca para usar paquetes Delphi, de mod0 que el codigo VCL y la inforrnacion no se dupliquen. Pero esta tecnica causa aun algunos problemas: generalmente esta recomendado no usar las DLL y 10s paquetes de Delphi juntos. La mejor tecnica para hacer que 10s formularios de una biblioteca sean accesibles para otros programas Delphi es usar paquetes en lugar de las DLL.
Este codigo muestra, en una etiqueta, la direccion de memoria de la funcion, en el espacio de direcciones de la aplicacion que llama. Si ejecutamos dos programas que usen este codigo, por lo general, ambos mostraran la misma direccion. Esto demuestra que el codigo se carga solo una vez en una direccion de memoria comun. Otra manera de conseguir mas informacion acerca de lo que esta ocurriendo es usar la ventana Modules de Windows, que muestra la direccion base de cada
biblioteca referenciada por el modulo y la direccion de cada funcion dentro de la biblioteca, como se muestra aqui:
N m e . _ .-- . ~ - B a s s ! W GDl32 dl $77C40OO0 ADVAPI32 dl $77DAOWO RPCRT4 dl1 $77C90000 OLEAUT32 dl $770F0000 rnsvul dl1 $77BE0000 de32 dl1 $771800W VERSION dll $77BD0000 CDMCTL32 d Y 977310000
P h
bhmndl
MSCTF dl TabHook d R UxTherne dl1 MOUSEDLL dl1
-W m W
$74680000 $10000WO $58150000 $00780000
- D:\md7de\lO\DMm\~
C \WINDOWS\syslern32\M
C \WINDOWS\Syslern32\I C \WINDOWS\Syslern32\u C W~chwos progma\Br de
1
.
'
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.
Si se carga el codigo de la DLL solo una vez, podriamos preguntarnos que ocurre con 10s datos globales. Basicamente, cada copia de la DLL tiene su propia copia de 10s datos, en el espacio de direcciones de la aplicacion que llama. Sin embargo, posiblemente sera necesario compartir datos globales entre las aplicaciones que usen una DLL. La tecnica mas comun para compartir datos consiste en usar archivos proyectados en memoria. Usaremos esa tecnica en el caso de una
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
El codigo real esta en la unidad secundaria (DllMemU. PAS), que tiene el codigo de las cuatro rutinas que leen o escriben dos posiciones de memoria globales. st as contienen un entero y un puntero a un entero. Veamos las declaraciones de variables y las dos rutinas set:
var PlainData: I n t e g e r = 0 ; // no compartido S h a r e D a t a : " I n t e g e r ; // c o m p a r t i d o p r o c e d u r e SetData begin PlainData := I ; end; ( I : Integer); stdcall;
( I: Integer) ; s t d c a l l ;
CreateFileMapping: Requiere como parametros el nombre de archivo (o $ F F F F F F F F para usar un archivo virtual en memoria), algunos atributos de seguridad y proteccion, el tamaiio de 10s datos y un nombre interno (que habra de ser el mismo para compartir el archivo proyectado desde diversas aplicaciones que lo llamen).
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 ' ) ;
~ile-Map-Write, 0,
0,
Cuando termina la aplicacion y se libera la memoria asignada a la DLL, hay que liberar el puntero a1 archivo proyectado y el propio archivo proyectado:
finalization UnmapViewOfFile (ShareData); CloseHandle (hMapFile);
El formulario de esta aplicacion tiene cuatro cuadros de edicion (dos con un control UpDown conectado), cinco botones y una etiqueta. El primer boton guarda el valor del primer cuadro de edicion en 10s datos de la DLL, obteniendo el valor del control conectado UpDown:
SetData (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 u lm . ry. x & ;n un \y a b u b u b r a laa A;ot;m+nr a a 1 r a v 1 ~ aUJLLUUU . . r . .U L n r n o D v W ~ n . r U n U b n r r a d d a I.. nnnnrtnr n Ina u r a ~ r u ~ .rnm4nLlno mmonrrdn ~ U m t G L U r\ n m L L d U va
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
es necesario si aiiadimos metodos o propiedades a una clase, per0 no si aiiadimos nuevos simbolos globales (o modificamos algo no utilizado por aplicaciones de cliente). Con respecto a 10s cambios que afecten solo a la seccion de implernentacion de la unidad de paquete no existe problema alguno. Un archivo DCU en Delphi posee un indicador de version basado en su sello de informacion sobre la ultima actualizacion y en una suma de verificacion, calculados desde la parte de interfaz de la unidad. Cuando cambiamos la parte de interfaz de una unidad, todas las demas unidades basadas en ella deberian compilarse de nuevo. El compilador compara el sello de informacion sobre la ultima actualizacion y la suma de verificacion de la unidad de compilaciones previas con el nuevo sello de informacion sobre la ultima actualizacion y la nueva suma de verificacion y decide si la unidad dependiente ha de ser compilada de nuevo. Esa es la razon por la que debemos compilar de nuevo cada unidad cuando conseguimos una nueva version de Delphi que ha modificado unidades de sistema. En Delphi 3 (cuando se introdujeron por primera vez 10s paquetes), se aiiadio una suma de verificacion del paquete, como funcion de entrada adicional a la biblioteca de paquetes, obtenida a partir de la suma de verificacion de las unidades contenidas y la suma de verificacion de 10s paquetes necesitados. Esta suma de verificacion era posteriormente llamada por 10s programas que usaban el paquete de mod0 que cualquier ejecutable basado en una version antigua no arrancaria. Delphi 4 y las versiones siguientes hasta Delphi 7 han disminuido las restricciones en tiempo de ejecucion del paquete. Sin embargo, las restricciones en tiempo de diseiio en archivos DCU siguen siendo identicas. La suma de verification de 10s paquetes ya no se comprueba, por lo que podemos modificar directamente las unidades que forman parte de un paquete y desplegar una nueva version del paquete que se va a usar con el archivo ejecutable existente. Dado que nos referimos a 10s metodos por nombre, no podemos eliminar ningun metodo existente. No podemos ni siquiera cambiar sus parametros, debido a las tecnicas name mangling aiiadidas especificamente a 10s paquetes para protegerlos contra cambios en sus parametros. Eliminar un metodo referenciado desde un programa lo detendra durante el proceso de carga. Si hacemos otros cambios el programa puede fallar inesperadamente durante la ejecucion. Por ejemplo, si sustituimos un componente de un formulario compilado en un paquete por un componente similar, el programa que hace la llamada puede ser capaz aun de acceder a1 componente en esa posicion de memoria, aunque ahora sea diferente. Si decidimos cambiar la interfaz de las unidades de un paquete sin recompilar todos 10s programas que las usan, deberiamos limitar 10s cambios. A1 aiiadir propiedades nuevas o metodos no virtuales a1 formulario, deberiamos ser capaces de mantener la compatibilidad total con 10s programas que ya usan el paquete. Ademas, aiiadir campos y metodos virtuales puede afectar a la estructura interna de la clase, derivando en problemas con programas existentes, que esperan una informacion de clase y un formato de tablas de metodos virtuales (VMT) diferentes.
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.
cuenta que un paquete es una coleccion de unidades compiladas y que nuestro programa usa varias unidades. Las unidades referenciadas por el programa se recompilaran dentro del archivo ejecutable, a no ser que especifiquemos a Delphi que 10s coloque dentro del paquete. Como hemos dicho anteriormente, esta es una de las razones principales para usar paquetes. Para definir una aplicacion de tal mod0 que su codigo este dividido entre uno o mas paquetes y un archivo ejecutable principal, solo hay que compilar algunas de las unidades en un paquete y, a continuacion, configurar las opciones del programa principal para enlazar dinamicamente ese paquete. Por ejemplo, hemos hecho una copia del formulario de seleccion de color "habitual" y hemos llamado a su unidad PackScrollF, despues hemos creado un nuevo paquete y lo hemos aiiadido a la unidad, como muestra la figura 10.5.
Figura 10.5. Estructura de un paquete que contiene un formulario en el Package Editor de Delphi.
Antes de compilar este paquete, deberiamos cambiar sus directorios de salida predefinidos para remitir al directorio actual, no al subdirectorio estandar / Projects / ~ p de Delphi. Para ello, vamos a la ficha Directories/Conditional l de las Project Options del paquete y definimos el directorio actual (un solo punto, abreviado) como directorio Output (para la BPL) y directorio de salida DCP. DespuCs compilamos el paquete y no lo instalamos en Delphi, no hace falta. Ahora, podemos crear una aplicacion normal y escribir el codigo estandar que usaremos en un programa para mostrar un formulario secundario, como en el siguiente listado:
uses PackScrollF; procedure TForml.BtnChangeClick(Sender: TObject); var FormScroll: TFormScroll; begin FormScroll := TFormScroll .Create (Application); try
procedure TForml.BtnSelectClick(Sender: TObject); var FormScroll: TFormScroll; begin FormScroll : = TFormScroll .Create (Application); // inicia 10s datos y la IU FormScroll.SelectedColor : = Color; FormScroll.BitBtnl.Caption := 'Apply'; FormScroll.BitBtnl.0nClick : = FormScroll.ApplyClick; FormScroll.BitBtn2.Kind : = bkclose; / / muestra el formulario FormScroll.Show; 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 .coalgo ael slgulenre ejemplo esra en 1- rmsma carpaa. F l paqueze - 10s la EI y proyectos son todos referenciados por el archivo & grupo de proyecfo (BPG) de la carpeta.
-13:.
. I _
_:_...-I_
I -
I--
el paquete es necesario para ejecutar la aplicacion y que se carga cuando arranca el programa. Estos dos aspectos se pueden evitar cargando el paquete de forma dinamica, como hemos hecho con las DLL. El programa resultante sera mas flexible, arrancara mas rapido y usara menos memoria. Es importante tener en cuenta que habra que llamar a las funciones L o a d P a c k a g e y U n l o a d P a c k a g e de Delphi en lugar de a l a s funciones de la API de Windows L o a d L i b r a r y o S a f e L o a d L i b r a r y y F r e e L i b r a r y . La diferencia esta en que las funciones de Delphi cargan 10s paquetes y tambien llaman a su codigo de iniciacion y finalizacion correspondientes. Ademas de este importante aspecto, sera necesario algun codigo adicional en el programa, puesto que no podemos hacer referencia desde el programa principal a la unidad que alberga el formulario. No podemos usar la clase de formulario directamente, ni acceder a sus propiedades ni componentes. Por lo menos, no podemos hacerlo con el codigo estandar de Delphi. Sin embargo, ambos problemas se pueden resolver utilizando referencias de clase, registro de clases y RTTI (informacion de tipos en tiempo de ejecucion) . En la unidad del formulario, en el paquete, hemos aiiadido este codigo de inicializacion:
initialization Registerclass (TFormScroll);
Cuando se carga el paquete, el programa principal puede usar la funcion de Delphi G e t C l a s s para obtener la referencia de clase de la clase registrada y, a continuacion, llamar a1 constructor c r e a t e para dicha referencia de clase. Para resolver el segundo problema, hemos definido la propiedad s e l e c t e d C o l o r del formulario en el paquete como una propiedad publicada, por lo que esta accesible mediante la RTTI. A continuacion, hemos reemplazado el codigo que accede a esta propiedad ( F o r m S c r o l l .C o l o r ) con el siguiente codigo:
SetPropValue (FormScroll, ' S e l e c t e d C o l o r l , Color);
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' ) )
if Assigned (FormClass) then begin FormScroll : = FormClass .Create (Application); try // inicia 10s datos SetPropValue (FormScroll, ' SelectedColor' , Color) ; // muestra el formulario if FormScroll.ShowModal = mrOK then Color : = GetPropValue (FormScroll, lSelectedColor'); finally FormScroll.Free; end; end else ShowMessage ('Form class not found'); UnloadPackage (Handlepack);
end else ; ShowMessage ( I Package not found1) end;
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' ..1 -l *..- _ .., . . I I ria. Cuando el n r " -e -h -se c -.,- -i I*..~ &..-A* muznr*l-.-.: . - -1-cac uulcru 1-.- ~ I I U o - v - - ~ , uucuc UUGI a ~. . =~ ~ ... -r : 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 . . . . -. . t aeoemos asegurarnos a e aesrrulr roaos ros oojnos mres ae aescargar er
:-&--*-*
-L:-b-
- - -
---
- - - ----~
-1.
- - - -
-1
implementation
procedure RegisterColorSelect (AClass: TClass); begin i f ClassesColorSelect. IndexOf (AClass) < 0 then ClassesColorSelect.Add (AClass); end; initialization ClassesColorSelect finalization
ClassesColorSelect.Free;
:=
~~1assList.Create;
end.
Cuando ya tenemos esta interfaz, podemos definir formularios que la implementen, como en el siguiente ejemplo de IntfFormPack:
type TFormSimpleColor = class (TForm, IColorSelect)
...
private procedure SetSelColor (Col: TColor) ; function GetSelColor: TColor; public function Display (Modal: Boolean = True) : Boolean;
Los dos metodos de acceso leen y escriben sencillamente el color de algunos componentes del formulario (un control ColorGrid en este caso especifico), mientras el metodo Display llama internamente a Show o ShowModal,dependiendo del parametro:
function TFormSimpleColor.Display(Modal: begin Result : = True; // predefinido if Modal then Result : = (ShowModal = mrOK) else begin BitBtnl.Caption : = 'Apply'; BitBtnl-OnClick : = Applyclick; BitBtn2.Kind : = bkClose; Show; end ; end; Boolean): Boolean;
Como puede verse en este codigo, cuando el formulario es no modal el boton OK se convierte en un boton Apply. Por ultimo, la unidad tiene el codigo de registro en la parte de inicializacion, para ejecutarlo cuando se carga el paquete dinamicamente:
RegisterColorSelect (TFormSimpleColor);
El segundo paquete, IntFormPack2, tiene una arquitectura similar per0 un formulario diferente como podemos ver en su codigo fuente.
Con esta arquitectura, podemos crear un programa mas flexible y elegante, basado en un solo formulario. Cuando se crea el formulario, define una lista de paquetes (denominada Handles Packages) y 10s carga todos. Por ultimo, despues de cargar 10s paquetes, el programa muestra las clases registradas en un cuadro de lista. Este es el codigo de 10s metodos LoadDynaPackage y Formcreate:
procedure TFormUseIntf.FormCreate(Sender: TObject); var I: Integer; begin // carga t o d o s 1 0 s paquetes e n tiempo de e j e c u c i o n Handlespackages : = TList.Create; LoadDynaPackage ( ' IntfFormPack. b p l ' ) ; LoadDynaPackage ( ' IntfFormPack2. b p l ' ) ;
// adade 10s nombres de c l a s e y s e l e c c i o n a e l primer0 f o r I : = 0 t o ClassesColorSelect.Count - 1 do 1bClasses.Items.Add (ClassesColorSelect [I].ClassName); IbClasses. ItemIndex := 0; end;
procedure TFormUseIntf.LoadDynaPackage(PackageName: string); var Handle : HModule ; begin // i n t e n t a cargar e l paquete Handle : = Loadpackage (PackageName) ; i f Handle > 0 then // adade a l a l i s t a para e l i m i n a r m ' s t a r d e HandlesPackages.Add (Pointer(Hand1e)) else ShowMessage ( ' P a c k a g e ' + PackageName + ' n o t f o u n d ' ) ; end;
La razon principal para mantener la lista de controladores de paquete es poder descargarlos todos cuando termina el programa. De hecho, no necesitamos estos controladores para acceder a 10s formularios definidos en dichos paquetes. El codigo en tiempo de ejecucion utilizado para crear y mostrar un formulario utiliza simplemente las clases de componentes correspondientes. Este es un extract0 de codigo utilizado para mostrar un formulario no modal (una opcion controlada por una casilla de verificacion):
var
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;
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 ' ;
Ademas de acceder a la informacion sobre el paquete desde el interior del componente (corno en el codigo anterior), tambien podemos hacer lo mismo desde un punto de entrada especial de las bibliotecas de paquetes, la funcion G e t P a c k a g e I n f o T a b l e . Esta devuelve cierta informacion especifica sobre el paquete que Delphi guarda como recursos e incluye en el paquete DLL. Afortunadamente, no necesitamos tecnicas de bajo nivel para acceder a esta informacion, porque Delphi ofrece algunas funciones de alto nivel para manipularlo. Podemos usar dos funciones para acceder a la informacion sobre el paquete:
GetPackageDescription: Devuelve una cadena que contiene una descripcion del paquete. Para llamarla, tenemos que dar el nombre del modulo (la biblioteca de paquetes) como unico parametro. GetPackageInfo: No devuelve directamente informacion sobre el paquete, sino que hay que pasarle una funcion a la que llamar para cada entrada de la estructura de datos interna del paquete. En la practica, G e t P a c k a g e 1n f o llamara a nuestra funcion para cada unidad contenida en el paquete. Ademas, G e t P a c k a g e I n f o define varios indicadores en una variable I n t e g e r .
Estas dos llamadas a funciones nos permiten acceder a informacion interna sobre un paquete, per0 para saber que paquetes esta usando nuestra aplicacion, podemos mirar el archivo ejecutable que utilizan las funciones de bajo nivel. Sin embargo, Delphi nos proporciona un metodo mas sencillo. La funcion EnumModules no devuelve directamente informacion sobre 10s modulos de la aplicacion, sino que nos permite pasarle una funcion, a la que llama para cada modulo de la aplicacion, el archivo ejecutable principal y para cada uno de 10s paquetes que necesita la aplicacion. Para demostrar esta tecnica, hemos construido un sencillo ejemplo que muestra la informacion sobre paquetes y modulos en un componente TreeView. Cada nodo del primer nivel corresponde a un modulo y dentro de cada modulo hemos creado un subarbol que muestra 10s paquetes que contiene y necesarios para dicho modulo, asi como la descripcion del paquete y 10s indicadores de compilacion (RunOnly y D e s i g n O n l y ) . Podemos ver el resultado de este ejemplo en la figura 10.6. Ademas del componente TreeView, hemos aiiadido otros componentes a1 formulario principal, per0 10s hemos ocultado: un DBEdit, un Chart y un FilterComboBox. Hemos aiiadido estos componentes para incluir mas paquetes en tiempo de ejecucion en la aplicacion, ademas de 10s ubicuos paquetes VCL y RTL. El unico metodo de la clase de formularios es F o r m C r e a t e , que llama a la funcion de enumeracion del modulo:
procedure TForml. FormCreate (Sender: TObject) ; begin EnumModules (ForEachModule, nil) ; end;
D Contams
Und Packlnfo [ M a ~ n ) 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\\AIIN~~\AI<\<,,*IP~~~\I~-~~ M
Figura 10.6. El resultado del ejernplo Packlnfo, con la lista de paquetes que usa.
La funcion EnumModules acepta dos paramctros. El primcro cs la funcion de retrollamada (o callback). quc en nuestro caso scra F o r E a c h M o d u l e , y el scgundo es un puntero a la cstructura de datos que utilizara la funcion de respuesta a la llamada (en nuestro caso. n i l , puesto que no necesitamos esto). La funcion de rcspuesta a la llamada habra de accptar dos parametros (un valor H I n s t a n c e y un puntero sin tipo) y habra dc dcvolvcr un valor booleano. La funcion EnumModules llamara, a su vcz. a nuestra funcion de respuesta a la llamada para cada modulo. pasando el manejador de la instancia de cada modulo como primcr parametro y cl puntero de estructura de datos ( n i l cn el ejemplo) como el segundo:
f u n c t i o n ForEachModule (HInstance: Longint; Data: Pointer) : Boolean; var Flags: Integer; ModuleName, ModuleDesc: string; ModuleNode: TTreeNode; begin w i t h Forml .Treeview1 . Items d o begin SetLength (ModuleName, 200) ; GetModuleFileName (HInstance, PChar (ModuleName), Length (ModuleName)) : ModuleName : = PChar (ModuleName) ; / / a j u s t a ModuleNode : = Add (nil, ModuleName) ;
/ / 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
ModuleDesc : = GetPackageDescription (PChar (ModuleName)); ContNode : = AddChild (ModuleNode, ' C o n t a i n s ' ) ; ReqNode : = AddChild (ModuleNode, ' R e q u i r e s ' ) ;
// afiade information s i e l modulo e s un p a q u e t e GetPackageInfo (HInstance, nil, Flags, ShowInfoProc); if ModuleDesc <> " then
begin AddChild (ModuleNode, ' D e s c r i p t i o n : ' + ModuleDesc); if Flags and pfDesignOnly = pfDesignOnly then AddChild (ModuleNode, ' D e s i g n O n l y ' ) ; if Flags and pfRunOnly = pfRunOnly then AddChild (ModuleNode, ' R u n O n l y ' ) ; end ; end ; Result : = True; end:
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 ' ;
i f Flags and ufPackageUnit <> 0 then FlagStr : = FlagStr + ' P a c k a g e U n i t ' ; i f Flags and ufWeakUnit <> 0 then FlagStr : = FlagStr + ' W e a k U n i t ' ; i f FlagStr <> ' ' then FlagStr : = ' ( ' + FlagStr + I ) ' ; with Forml.TreeViewl.Items do c a s e NameType o f ntContainsUnit : AddChild (ContNode, Name + FlagStr) ; ntRequiresPackage: AddChild (ReqNode, Name); end; end ;
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.
se edita el modelo mediante 10s diversos editores de ModelMaker, 10s cambios se aplican a1 modelo interno (no a 10s archivos externos de codigo, a1 menos no hasta que se indica a ModelMaker que vuelva a generar 10s archivos externos). Comprender esta diferencia nos ahorrara una importante cantidad de frustracion. Otro concept0 importante es que ModelMaker es capaz de representar un unico modelo de codigo interno a traves de multiples vistas en su interfaz de usuario. Por ejemplo, el modelo puede verse y editarse como una jerarquia de clases, o como una lista de unidades con clases contenidas en ellas. Los miembros de clase pueden ordenarse, filtrarse, agruparse y editarse de muy diversas maneras. Cualquier numero de vistas puede verse en las diversas extensiones disponibles para ModelMaker. Pero, lo que es mas importante, el propio editor de diagramas UML es otra vista mas del modelo. Cuando se visualizan 10s elementos del modelo (corno clases y unidades) en 10s diagramas, se crean representaciones visuales de 10s elementos del modelo de codigo; si se elimina un simbolo de un diagrama, no se esta borrando necesariamente el elemento del modelo, simplemente se elimina su representacion en el diagrama. Aunque ModelMaker ofrece diversos asistentes y prestaciones de automatizacion en la visualizacion, el product0 no leer6 el codigo y generara automaticamente unos diagramas UML atractivos sin ningun esfuerzo por parte del programador. Despues de importar el codigo fuente y aiiadir las clases a 10s diagramas, se necesitara recolocar 10s simbolos para crear diagramas UML utiles.
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;
.
+
M* o
+Create( )
Anteriormente, comentamos que el editor de diagramas de ModelMaker es simplemente otra vista mas del modelo interno. Algunos de 10s simbolos presentes en 10s diagramas de ModelMaker se proyectan directamente sobre elementos del modelo de codigo y otros no. Con 10s diagramas de bajo nivel como 10s diagramas de clase, la mayoria de 10s simbolos representan elementos reales del modelo de codigo. Manipular estos simbolos puede modificar el codigo generado por ModelMaker. Como caso contrario, en 10s diagramas de casos de uso la mayor parte (si no todos) de 10s simbolos no tienen representacion dentro del modelo de codigo. En 10s diagramas de clase se pueden aiiadir nuevas clases, interfaces, campos, propiedades e incluso documentacion a1 modelo. Del mismo modo, se puede modificar la herencia de una clase en el modelo desde el propio diagrama. A1 mismo tiempo, se pueden afiadir diversos simbolos a un diagrama de clase que no tengan una representacion Iogica dentro del modelo de codigo. Los diagramas de clase en ModelMaker tambien permiten codificar interfaces, trabajando a un mayor nivel de abstraccion. La figura 11.2 muestra las relaciones entre las clases y las interfaces en un ejemplo complejo de uso de interfaz, IntfDemo.
PUS htegsr,
check in
rmwt
Ncur~lssham
Cuando se usan'interfaces en 10s diagramas de clase, se puede especificar las relaciones de implementacion de interfaces entre clases e interfaces, y esas implementaciones se afiadiran a1 modelo de codigo. Aiiadir la implementacion de una interfaz en un diagrama implica la aparicion de una de las prestaciones de ModelMaker mas atractivas: el asistente Interface Wizard (vease figura 11.3). Activar el Interface Wizard para una clase simplifica en gran medida la tarea de implementar un interfaz. El asistente enumera 10s metodos y propiedades que necesita una clase para implementar una interfaz (o interfaces) dadas; si se ordena, el asistente aiiadira todos estos miembros necesarios a la clase de implementacion. Fijese que depende del programador proporcionar un codigo con significado para cualquier metodo aiiadido a la clase. En la figura 11.3, el asistente evalua TAhlete para su implementacion de IWalker e IJumper y sugiere 10s cambios que son necesarios para la correcta implementacion de estas interfaces.
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-
t h i i lrxepn
IJumpei
--
J w *;le
w a :rcr*
Podlan-InNpe,
Redundanl rnembar TAthlele TAIhlete TAlhlele TAthlele TAtHele TAlhlcte Leave m!undanl rnrrbs Leave ~ A n d a nm d m f l Leave ledurdanl memba Leave rcdmdanl member Leave l e d d a n t mcmbcr
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.
dos para ofrecer un camino desde el modelado de la interaccion del usuario a1 mayor nivel de 10s diagramas de casos de uso hasta 10s diagramas de clases y secuencia de bajo nivel. Los diagramas de caso de uso estan entre 10s diagramas mas utilizados, a pesar de que sus simbolos no tengan ninguna relacion con 10s elementos del modelo de codigo. Estos diagramas estan pensados para modelar lo que se supone que hace el software, y son lo suficientemente autoesplicativos como para usarlos en sesiones de analisis con personas que no sean desarrolladores.
-13 s
+i
ru
a PissglSwce 7 0 4 4
Oeatcly rn b lntepn)
*
iu
!a(
1
ChFck h
Insert
Nmw~-nct-~.m
Figura 11.4. Un d~agrama secuencla para un controlador de eventos del ejemplo de NewDate.
Un diagrama de caso de uso simple consiste en actores (usuarios o subsistemas de la aplicacion) y casos de uso (cosas que hacen 10s actores). Una de las preguntas mas frecuentes relativas a 10s casos de uso es como manejar 10s textos de 10s casos de uso en ModelMaker. Los textos para 10s casos de uso son un paso siguiente tipico cuando se realiza un analisis preliminar. Por ejemplo, un caso de uso es una breve descripcion de una accion que puede emprender un actor ("Obtener una vista previa de un informe de ventas" o "Ajustar el tamaiio de una ventana"); el texto de un caso de uso es una descripcion mas detallada del texto. ModelMaker no soporta especificamente 10s testos de casos de uso mas grandes; se puede usar un simbolo de anotacion dentro del diagrama conectado a1 simbolo del caso de uso, o se puede vincular el
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~on Dlagram m ~ l a s D~aglam s Use Case D~agram
.Yr
I.^
jd
16 4
R_UnModelMaker
&mp to ModelMaker Sh~ftcCtrlcM
Q Add to Model ,
Cmvert to Model
Cgnvert pro@ to Model
j
:
OpmModel
Inlegratan Opkm..
.
b b
I
8s
Version Control
R-ce
-cut
S t r r q W~zard
Wizard.
La mayoria de las opciones de menu solo se encuentran disponibles si ModelMaker esta en funcionamiento. Una vez que se haya arrancado ModelMaker (desde la opcion de menu Run ModelMaker o de un mod0 normal), el resto de 10s elementos quedara disponible. El menu de integracion contiene unos cuantos modos de aiiadir codigo a un modelo. Los elementos Add to Model,Add Files to Model, Convert to Model y Convert Project to Model hacen que ModelMaker importe las unidades especificadas: 10s elementos Add importan unidades en el modelo cargado actualmente en ModelMaker, y 10s elementos Convert crean un modelo nuevo e importan las unidades. Convert Project to Model es un buen sitio para comenzar (nos aseguraremos de hacer una copia del codigo, y despues seleccionaremos este elemento del menu mientras que esta abierto uno de 10s proyectos en Delphi). El proyecto a1 completo se importara a un nuevo modelo en ModelMaker.
Importar unidades de esta manera hara que aparezca el cuadto de d i i h g o Import Source File, donde se pueden fijar las opciones sobre el mod0 de importar el cbdigo. Aprovechar estas opciones permite importar parte de la VCL como clases de reserva para que se puedan emplear las herrarnientas de herencia de ModelMaker sin inflar desmedidamente el modelo. Tambien en el menu de integracion, esta Refresh in Model, que obliga a ModelMaker a volver a importar la unidad actual. Ahora es un buen momento para comentar una de las consecuencias del modelo de codigo interno de ModelMaker comentado anteriormente. Ya que ModelMaker trabaja sobre su codigo interno y no sobre 10s archivos de codigo f~iente esternos (hasta que se vuelven a generar 10s archivos), es habitual descubrir que se han editado tanto el modelo como 10s archivos de codigo, con lo que el modelo habra perdido la sincronia con respecto a 10s archivos fuente. Cuando han cambiado 10s archivos fuente per0 no el modelo, se puede vol\~er sincronizar el modelo volviendo a a importar las unidades fuente. Pero si tanto el modelo como 10s archivos han cambiado, la situacion es mas complicada. Afortunadamente, ModelMaker ofrece un robusto conjunto de herramientas para manejar problcmas de sincronizacion. En la seccion "Vista de diferencias" se puede ver mas informacion a1 respccto. Otro elemento notable en el mcnu de integracion es Jump to ModelMakcr. Cuando se sclecciona este elemento, ModelMaker trata de encontrar la posicion actual del codigo dentro de su modelo cargado, trayendo ModelMaker a1 frente del proceso. Aunque ModelMaker puede controlarsc desde Delphi, la integracion es bidirectional. Al igual que el menu de ModelMaker en el IDE de Delphi, un mcnu Delphi aparece en ModelMaker. En esc menil se encuentran comandos que permiten saltar del modelo seleccionado actualmente a su posicion correspondiente en el archivo de codigo fuente en Delphi, al igual que comandos que provocan que Delphi realice una verificacion de sintaxis, compile o construya el proyecto. Por eso se puede editar el codigo, generarlo y compilarlo, todo ello desde ModelMaker.
r,
,,
Todo lo que se hace es escoger 10s parametros apropiados en el editor. y ModelMaker creara 10s miembros de clase de soporte necesarios. Esto es algo que vas mas alla de las ventajas que ofrece el IDE de Delphi con su Class Completion. Fijese en que 10s atributos de una propiedad que normalmente habria que escribir manualmente estan representados por varios controles en el cuadro de dialogo. Las especificaciones V i s i b i l i t y , Type, Read, Write y otras se gestionan desde el editor. Las ventajas tienen que ver con el ambito de la reingenieria (por no mencionar la elimination de ciertas tareas de escritura repetitivas). Por e.jemplo, ya que ModelMaker gestiona una propiedad como un objeto en su modelo de codigo, modificar algo sobre la propiedad (como su tipo) provocara que ModelMaker aplique ese cambio a cualquier referencia de la que tenga conocimiento. Si despues se desea modificar el acceso de lectura desde un campo a un metodo, se puede hacer ese carnbio en el editor, y ModelMaker se encargara de aiiadir el metodo get y modificar la declaracion de la propiedad. Lo que es mejor, si se decide renombrar la propiedad o llevarla a otra clase, la propiedad dispone de sus propios miembros de clase de soporte: automaticamente cogeran otro nombre o se desplazaran tal y como resulte apropiado. El mismo enfoque se usa para cada uno de 10s tipos de miembros de clase; existen editores similares para metodos, eventos, campos e incluso clausulas de resolution de metodos.
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;
Aunque este codigo parece vagamente familiar a un programador de Delphi, obviamente no podra compilarse. Estamos viendo la envoltura que el motor de generacion de codigo de ModelMaker usara cuando expanda o genere una unidad
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;
En este caso, la etiqueta indica a ModelMaker que expanda la interfaz de clase para TDate Form en este punto del codigo de la unidad. Entonces, cuando se edite codigo en el Unit Code Editor, se vera una mezcla de codigo gestionado por el desarrollador y codigo gestionado por ModelMaker. Hay que tener cuidado cuando se edite este codigo para no perturbar el codigo administrador por ModelMaker a no ser que se sepa lo que se esta haciendo. Es algo analog0 a editar codigo en un archivo DPR gestionado por Delphi: se pueden tener problemas si no se es cuidadoso. Aun asi, es aqui donde deberia aiiadirse una declaracion de tip0 que no sea una clase (un tipo enumerado, por ejemplo). Se manejaria como en Delphi, aiiadiendo la declaracion de tip0 en la seccion t y p e de la unidad:
unit <!UnitName!>; interface uses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs, Dates, StdCtrls;
ID=37;
implementation
MMW1N:CLASSIMPLEMENTATION end.
TDateForm;
ID=37;
En este ejemplo, ModelMaker emitira la declaracion de tipo tal y como se ha escrito (corno texto plano) y despues expandira la declaracion de la clase TDateForm. ModelMaker ofrece herramientas dentro del Unit Code Editor para gestionar metodos a nivel de unidad y existen unas facilidades significativas cuando se tienen unidades del tipo de grandes bibliotecas de rutinas. Sin embargo, ahora que se usa ModelMaker, se pueden usar sus potentes prestaciones de reingenieria para convertir en objetos algunas de estas rutinas.
ADVERTENCIA: Un gran inconveniente de escribir c6digo dentro de la ventana Implementation de ModelMaker es que carece de todas las formas de Code Completion que ofrece el IDE de Delphi.
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
--
--
---
4.7 * + , . C 1
Id 7
+.
-3 .
7
+
Id 9
1
I
.
.A
*.
I
w'
>
PLS
L-c 7
Irmt
--
ModelMaker ofrece una gran variedad de maneras de ver diferencias. Se puede ver una comparacion estandar de archivos de testo, diferencias en las marcas de tiempo o incluso la comparacion de dos clases seleccionadas dentro del modelo. Una vista muy usada es la que muestra la figura 1 1.8, una diferencia estructurada. ModelMaker importa temporalmente el archivo fuente desde el disco (convirtiendolo en objetos del mismo mod0 que el modelo de codigo interno) y compara el archivo importado con la misma unidad y clases en el modelo a1 nivel de objeto y atributos mas que de texto. El resultado es una comparacion mucho mas rapida y precisa. Fijese en 10s iconos que muestran diferencias en la vista en arbol de la figura 11.8. Los <> en rojo indican que tanto el modelo como el archivo fuente contienen el miembro de clase indicado ( b t n U I T e x t C l i c k en el ejemplo) pero que las dos instancias son distintas. El codigo que cs distinto se muestra en 10s controles de memo de la derecha. La + verde de la vista en arbol indica que el miembro de clase indicado solo esistc en el modelo. no en el disco. La - am1
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
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~~lnWMAPII rLLe--r:= = zr, a-z:=-=n=~z~z:. rT.> C.\Devebpmenl\ModelMake~ ExpertsWMAPII ~CH~ ;~,C,\Devebprnent\ModelMaker ExperlswMAP1l pzaccdure T ~ L S - W ~ ~ F I I X ~ ~ O : C E T Cp r w ~ 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 : - - . . ~ ~ u l l rrsize ;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,-
Este enfoque implica que es muy importante conocer cuando ha perdido la sincronia el modelo, para que no se sobrescriban 10s cambios en el disco cuando se vuelvan a generar 10s archivos fuente. Afortunadamente, ModelMaker ofrece varias caracteristicas de seguridad que pueden prevenir esta situacion. Una de estas es un D e s i g n C r i t i c . Si esta habilitado D e s i g n C r i t i c s , la vista de mensajes de ModelMaker emitira una advertencia cuando cambie un archivo del disco.
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.
Declaration en la seccion Method Implementation del grupo In Source Documentation Generation. Ahora cualquier metodo que contenga documentacion usara la macro predefinida para generar comentarios de codigo fuente.
[Class
,.------Cbss s v m g
--
--
Qwhn Tcs:T';.FIIxplrze:
ze-e - 5 t h e tazkm . . e c c l a c c d v i r h c r e a s r n g !.!cdeLKaAer e?.~e:;3 .s: "plug-inl", us:?.q t h e XcdelXnne: +en Tc:;s ;.GI. P x g ~ e cvhlc:? u. a Trcc':ler c l any s c r t ~ s to display the h i e r a r c h l c r ~ ncrucruz. c f t h e r r d a l c r a Trea7.'ie-; + L i s t X a ~ Crnfigczar1Cr.t cEn b.nafi5 cF> f r c z chis ccq3n.n;.
ModelMaker tambikn puede importar comentarios desde una unidad fuente y asociar esos comentarios con 10s elementos apropiados del modelo de codigo. Para hacer esto, se debe firmar 10s comentarios con una Document Import signature (vease la pestaiia Source Doc Import del cuadro de dialog0 Project Options) e indicar a ModelMaker que lineas importar a1 modelo. Por eso, si la irnplementacion del metodo tiene comentarios como 10s siguientes, se puede indicar a ModelMaker que ignore las cinco primeras lineas y solo importe el texto real del comentario:
TmyClass.DoSome thing Returns: String Visibility: Public C o m e nt: Este es el comentario real que queremos que importe ModelMaker. Las cinco primeras lineas d e este bloque de comentario no deberian importarse a 1 modelo. )
Cuando se configura ModelMaker para 10s comentarios de codigo fuente, es importante prestar atencion a1 desplazamiento de comentarios. Puede suceder cuan-
do 10s parametros de importacion y exportacion no se corresponden por completo. Por ejemplo, si la macro que controla la salida de comentarios en el codigo fuente aiiade seis lineas para el comentario antes de aiiadir el texto de la documentacion, per0 10s parametros de importacion solo eliminan cinco lineas, entonces cada ciclo de importacion/generacion aiiadira una linea redundante de texto a1 comentario.
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.
Muchos textos se dedican a este concepto, asi que simplemente prestaremos atencion a las formas que tiene ModelMaker de ayudar a reorganizar el codigo. Una vez mas, el modelo de codigo interno de ModelMaker tiene un papel importante (recordemos que desarrollar en ModelMaker no es simplemente desarrollar codigo orientado a objetos, tambien es un proceso de desarrollo que es asistido por la orientacion a objetos). Ya que todos estos elementos del modelo de codigo se guardan internamente como objetos (objetos que tienen referencias cruzadas) y ya que las unidades de codigo fuente se vuelven a generar completamente a partir de este modelo cada vez que se decide generar el codigo, cualquier cambio en 10s elementos del codigo se propagara a traves de las clases instantaneamente. El ejemplo perfecto vuelve a ser una propiedad de clase. Si se tiene una propiedad llamada M y N e w P r o p e r t y con metodos de lectura y escritura (mantenidos por ModelMaker y llamados G e t M y N e w P r o p e r t y y S e t M y N e w P r o p e r t y ) y se quiere renombrar la propiedad como M y p r o p e r t y , solo es necesario un paso: renombrar la propiedad. ModelMaker se encargara del resto (10s metodos de acceso se renombraran automaticamente como G e t M y P r o p e r t y y S e t M y P r o p e r t y ) . Si la propiedad aparece en un diagrama, el diagrama se actualizara automaticamente para representar el cambio. (Una advertencia: ModelMaker no buscara automaticamente instancias deMyNewPropert y en el codigo, habra que realizar una busqueda y sustitucion global dentro de ModelMaker. Se trata de un ejemplo sencillo, per0 muestra el mod0 en que ModelMaker simplifica las labores de reingenieria, como mover y renombrar elementos de codigo, donde ModelMaker se encargara de la mayoria de 10s detalles en lugar del desarrollador. Estos son algunos casos especificos: Renombrado sencillo: Esta tarea es bastante simple y ya la hemos comentado, per0 su utilidad nunca se resaltara lo suficiente. Los cambios en el nombre de un elemento del modelo de codigo se propagaran gracias a ModelMaker a traves del modelo de codigo a todas las instancias de ese elemento que conozca. Reasignacion de clases: Este proceso absurdamente simple puede realizarse de unas cuantas maneras distintas. Lo mas habitual es simplemente arrastrar una clase en la vista Classes desde un nodo padre a otro (tambien se puede hacer en un diagrama de clase arrastrando la flecha de generalizacion desde el antecesor anterior hasta el nuevo), y las clase tendra un nuevo padre. Si la herencia esta restringida, ModelMaker actualizara automaticamente 10s metodos heredados de la clase hija para que se correspondan con las declaraciones de la nueva clase padre. La siguiente vez que se genere el codigo, estos cambios apareceran automaticamente. Mover clases entre unidades: Tambien es una labor sencilla. En la vista Units, se arrastra la clase desde su lugar actual a la nueva unidad. Todo el codigo relevante (declaraciones, implementaciones y comentarios) se volvera a generar en la nueva unidad.
Mover miembros entre clases: En la reingenieria, este proceso se conoce como "desplazamiento de prestaciones (o responsabilidades) entre objetos". La idea es sencilla: a medida que el desarrollo progresa, se puede descubrir que es mas apropiado mover ciertas responsabilidades (implementadas como miembros de clase) a otra clase. Se puede hacer mediante arrastrar y soltar. Seleccionamos 10s miembros de clase deseados de entre las lista de miembros y 10s arrastramos sobre la nueva clase en la vista Classes (manteniendo pulsada la tecla Mayus para mover en lugar de copiar). Conversi6n de miembros: Se trata de una de las prestaciones de reingenieria de ModelMaker mas practicas. Hacemos clic con el boton derecho del raton sobre un miembro en la Member List para que aparezca el menu contextual que contiene las opciones y subopciones de menu Convert To. Seleccionando una de estas subopciones podremos convertir un miembro de clase ya existente de un tip0 de miembro a otro. Por ejemplo, si tenemos un campo privado llamado FMyInteger y queremos convertirlo a un propiedad, ModelMaker crea automaticamente una propiedad publica llamada My1 nteger, que lee desde y escribe en FMyInteger.Del mismo modo, se puede convertir este campo en un metodo (sera una funcion privada Ilamada MyInteger que devolvera un entero). Herencia restringida: En el cuadro de dialog0 del editor de metodos hay una casilla de verificacion Inheritance Restricted. Cuando se inarca esta casilla, ModelMaker no permite modificar la mayoria de 10s atributos del metodo, ya que esos atributos se determinan de acuerdo con la implernentacion del metodo sobrescrito en la clase antecesora. Si se cambia la declaracion de un metodo en una clase antecesora, esos cambios se aplicaran automaticamente a cualquier clase descendiente donde se haya restringido la herencia del metodo que sobrescribe. Si se tiene experiencia con la reingenieria (o se han usado las ultimas versiones de JBuilder). esto puede no parecer un conjunto de herramientas de reingenieria particularmente impresionante. Sin embargo, cuando se compara con lo que es posible hacer solo en Delphi, se trata de un increible conjunto de posibilidades. Ademas, la API OpenTools de ModelMaker ofrece acceso a la mayor parte del modelo de codigo. Si no se esta contento con lo que ModelMaker ofrece tal y como se instala, se pueden ampliar sus capacidades por uno mismo.
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.
El patron Singleton obliga a1 uso de un punto de entrada unico (la funcion de clase I n s t a n c e en la implementacion de ModelMaker de este patron, como se vera) para tener acceso a la unica instancia de la clase. Si la instancia no existe aun, se creara y devolvera. De no ser asi, se devolvera la instancia ya existente. Ya que I n s t a n c e es el punto de entrada, se evitara el uso de c r e a t e para esta clase. Una vez que se aplica el patron Singleton a la clase en ModelMaker, tendra este aspecto:
tYPe TOneTimeData = class (TObject) private FGlobalCount: Integer; procedure SetGlobalCount(const Value: Integer); protected constructor CreateInstance; class function AccessInstance (Request: Integer) : TOneTimeData; public constructor Create; destructor Destroy; override; class function Instance: TOneTimeData; class procedure ReleaseInstance; property GlobalCount: Integer read FGlobalCount write SetGlobalCount; end:
No vamos a proporcionar las implementaciones de 10s metodos, se pueden ver aplicando el patron como prueba o analizando el codigo fuente del ejemplo PatternDemo.
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
nI W e ~ i c s W I Aai o W Y V ~ W U V Lf k w a r, ~Wm W Dc ~I W I U C P t r& UtC U AP r W I U C L An1 U c n i i Y AP c n C V I U I W m Y m W I m ~ UV % s a U W i a y U
11U I
n 1 r r a n ; v aW fAV Y r l W r6 L U I L O & i n U e V
11U A
estructura global de un programs. Contemplando distintas soluciones de disefio de la gente ante distintos problemas, se pueden ver elementos comunes y ciertas similitudes. Un patron es el conocimiento de un disefio comun como estos, expresado de un mod0 esthdar y suficientemente abstract0 como para ser aplicable a un cierto numero de situaciones distintas. Los patrones de diseilo tienen que ver con . . . . . .. - s - . .. . . ..- . . el reciclaje de disefios m que de codlgo. Aunque la soluci6n comcada de un patron puede s e ~de inspiration para el programador, el foco real esth r en el diseilo: incluso aunque se podria tener que volver a escribir el cbdigo, comenzar con un diseiio claro y probador ahorrarh una cantidad de tiempo considerable. Los patrones de disefio no tratan acerca de 10s bloques constructivos primitives (corno m a tabla hash o m a lista mhida) o problemas especificos de un dominio (como hacen 10s pattoqes de d i s i s ) . El creador reconocido del movimiento del patr6n no fie un diseiIador de sofhare sino un arquitecto, que se fi.6 en el uso de patrones en las construcciones. "Cada pat& describe m problema que aeoritece une\. y otra vez en el entorno, y despues describe el nucko de la s o l u c b a em problema, de un modo tal que pueda usarse la solucih un milion de veces miis, sin volver a hacer lo mismo dos veces." Erich Gamma,Richard Helm, Raiph Johnson y John Vlissides escribieron el iibro qub dio pie al movimiento de pajrones en el rnundo del software: "Design Patterns: Elements of Reusable ObjectOriented Software (Addison-Wesley, 1995)". Zos autores suelen indicarse como "Gamma et al." pero tambih se llama el Grupo dk 10s Cuatro, Gang o Four o GoF. Por eso el libro suele r~ferirse f coloquialmmte como " l ~ r o GoF". El libro describe el c0m-epi.o& 10s patrones softovare, W c a p n
.a
modo p m e s c r i b i r l o s y o h c e un catidogo de 2 patrones divididos 3 en tres grugos: sreativos, estructurales y de comportadento. La rnayoria de 10s patrones de libro e s t b implernentados en C-H, y algunos en Smalltalk, aunque generalmente se abstr& el lenguaje y son i&al&ente aplicables en Java o Delphi. La estructura central de un patron es la siguiente:
Cl ,,A.L,, GI UUUUIG
a , . .
pad
a . ,
", , , . A L ", . . , , , c : , ~ U 3~ ~ U G WU ~ ~ I G L G I C L I L I ~ G G I
revistas be Delphi [como Dslphi Wormant y The Delphi Magazine). Los patroms GoF clhsicos han sido fuente de inspiracirin para muchos articu105, junto con la discusi6n M l a d a de b patrones disponibles con la s docamentircib de ModelMaker. No siempre es posible estar de acuerdo con la irnplementaci6n Delphi de algunos paones esthdar. De hecho, solemos tender a centramos en el diseflo snbyacetrte y eomo mantenerlo el pasar de la implementation GoF (m& en C++ o Java) a h l ~ h mientras aue se amovechan las i S iprcstaciones I w,qrrew&b :s dores y se qrenderh modos mejores de aplicar ttcnicas de orientation a objeta (em particular la en~apsulaci6n un acoplamicmw reducido). y Cotho mgerencia W,tengamm a wenta gue l qsayor parte de 10s patroa na se impleinm@ui mjor en Delp& mediante interfaces que con clases er +orno suele.hacerModelMslker, de h e r d o con el enfoque clbico). ModelMaker ofrece implementaciones de varios patrones mas, como Visitor, Observer, Wrapper, Mediator y Decorator. Estan incrustados en ModelMaker para aplicarse de un mod0 especifico, y algunas de las implementaciones son mejores que otras. Esto ha sido un tema controvertido entre algunos desarrolladores y por eso (entre otras cosas) ModelMaker soporta otros sistemas de aplicacion de patrones: las plantillas de codigo. Este enfoque permite la creacion y personalizacion en la parte del desarrollador. Sin embargo, no hay que olvidar el extenso
soporte de ModelMaker para patrones; son bastante buenos y ofrecen una implernentacion Delphi solida, fija y que funciona de estos problemas habituales.
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;
Se puede comprobar lo flexible que resulta esta tecnica. Es facil implementar otras tareas habituales (corno listas de tipado fuerte) e implementaciones propias de 10s patrones de diseiio, como vamos a ver.
I Desapllcn name nl array prope~ty type of anray p~operty Method returnmg I Items TLlst F~eld stormg Items
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
TCodeTemplate = class (TObject) private < ! FItems!>: TList; protected function Get<!ItemCount!>: Integer; function Get<!Items!> (Index: Integer): <!TObject!>; public property <!Itemcount!>: Integer read Get<!ItemCount!>;
property <!Items!> [Index: Integer] : <!TObject!> read Get<! Items ! >; end;
Fijemonos en las lineas que comienzan con / /DEFINEMACRO.Es aqui donde se declaran 10s parimetros; apareceran en el cuadro de dialog0 Code Template Parameters que se vio anteriormente. Cada linea es un par nombre 1 valor: el elemento a la izquierda de = es el valor editable, y el elemento que esta a la derecha es la descripcion que se puede proporcionar para explicar el parametro. Despues de proporcionar una lista de parametros, pueden usarse como macros en el codigo de la plantilla. Existen unas lineas en el ejemplo que son como estas:
property < ! Items!>[Index: Get<!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
Durante aiios, desde la aparicion de Windows 3 .O, Microsoft ha prometido que su sistema operativo y su API se basarian en un modelo de objetos real en lugar de en funciones. De acuerdo con lo esperado, Windows 95 (y mas tarde Windows 2000) deberian haberse basado en este enfoque tan revolucionario. No sucedio nada de esto, per0 Microsoft continuo introduciendo COM (Component Object Model, modelo de objetos componentes), construyendo el envoltorio de Windows 95 sobre este modelo, promoviendo la integracion de aplicaciones con COM y sus tecnologias derivadas (como Automation) y hasta llegar a presentar COM+ con Windows 2000. Ahora, poco despues de la aparicion a1 completo de 10s cimientos necesarios para una programacion sobre COM de alto nivel, Microsoft ha decidido pasar a una nueva tecnologia central, parte de la iniciativa .NET. Parece que COM no se encontraba realmente preparado para la integracibn de objetos detallados, aunque tuvo exito a1 proporcionar una arquitectura para integrar aplicaciones u objetos mayores. A lo largo de este capitulo construiremos nuestro primer objeto COM, prestando atencion a 10s elementos basicos para permitir la mejor comprension del papel de esta tecnologia sin profundizar demasiado en 10s detalles. Continuaremos comentando Automation y el papel de las bibliotecas de tipos, y veremos como trabajar con 10s tipos de datos de Delphi en 10s servidores y clientes de Automation.
Por ultimo, exploraremos el uso de objetos incrustados, con el componente Olecontainer y el desarrollo de controles ActiveX. Tambien hablaremos de las tecnologias COM sin estado (MTS y COM+) y de algunos otros conceptos avanzados como el soporte a la integracion con .NET ofrecido por Delphi 7. Este capitulo trata 10s siguientes temas: El concept0 de COM. COM, GUID y factorias de clases Las interfaces de Delphi y COM. Las clases de soporte a COM de la VCL. Creacion y uso de servidores Automation. Uso de bibliotecas de tipos. El componente Container Creacion de un ActiveX y un ActiveForm. Presentacion de COM+. COM y .NET en Delphi 7.
A medida que esta tecnologia se iba extendiendo y cobrando importancia para la plataforma Windows, Microsoft volvio a cambiar el nombre a OLE, despues a COM y finalmente a COM+ para Windows 2000. Estos cambios en el nombre solo estaban relacionados en parte con cambios tecnologicos y basicamente se debian a propositos de mercado. Basicamente, COM es una tecnologia que define un mod0 estandar para comunicar un modulo cliente y un modulo servidor a traves de una interfaz especifica. En este caso, modulo indica tanto una aplicacion como una biblioteca (una DLL); 10s dos modulos pueden ejecutarse en el mismo ordenador o en maquinas distintas conectadas mediante una red. Son posibles muchas interfaces, segun el papel del cliente y el servidor, y se pueden aiiadir nuevas interfaces para propositos especificos. Estas interfaces las implementan objetos de servidor. Un objeto de servidor suele implementar mas de una interfaz, y todos 10s objetos de servidor tienen unas cuantas prestaciones comunes, ya que todos deben implementar la interfaz IUnknown (que se corresponde con la interfaz IInterface especifica de Delphi). La buena nueva es que Delphi es completamente conforme con COM. Cuando aparecio Delphi 3, su implementation de COM era mucho mas sencilla y estaba mas integrada en el lenguaje que C++ u otros lenguajes de la epoca, hasta el punto de que incluso programadores del equipo de investigacion y desarrollo de Windows comentaron que deberian haber creado COM del mod0 en que lo hizo Delphi. Esta simplicidad se deriva principalmente de la incorporacion de tipos de interfaz en el lenguaje Delphi. (Las interfaces tambien se usan de un mod0 similar para integrar Java con COM en la plataforma Windows.) Como ya se ha comentado, la intencion de las interfaces COM es la comunicacion entre modulos de software, que pueden ser archivos ejecutables o DLL. Implementar objetos COM en archivos DLL suele ser mas sencillo, ya que en Win32 un programa y la DLL que utiliza ocupan el mismo espacio de direcciones de memoria. Esto significa que si el programa pasa una direccion de memoria a la DLL, la direccion sigue siendo valida. Cuando se usan dos archivos ejecutables, COM debe realizar mucho trabajo interno para permitir que las dos aplicaciones se comuniquen. Este mecanismo se llama marshalling (que, para ser precisos, tambien es necesario para las DLL si el cliente es multihilo). Hay que hacer notar que una DLL que implementa objetos COM se describe como un servidor en proceso, mientras que cuando el servidor es un ejecutable independiente, se llama servidor fuera de proceso. Sin embargo, cuando las DLL se ejecutan sobre otra maquina (DCOM) o dentro de un entorno de servidor (MTS), tambien son fuera de proceso.
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}']
Los metodos -AddRef y -Release se utilizan para implementar el recuento n de referencias. El metodo ~ u e r Iy t e r f ace controla la information de tipo y compatibilidad de tipos de 10s objetos.
out,un p a r h e t r o devuelto desde dl mCtodo a1 prc , programa que lo ilama per0 . , . . ..- m *- . --. sin ningun valor inicial pasado por el programa que llama a1 metodo. Los parametros out se han aiiadido a1 lenguaje Delphi especificamente para el soporte de COM, per0 pueden utilizarse en una aplicacion normal, ya que en ciertas circunstancias esto hace que el paso de parkmetros sea mas efiI -1
GU
,-1---, -1
---- A - ~nr~rratics, ---- y --*.2--- ulnarmr;as). rF--A , A:---:^^^\ \C;UIIIU GI caw UG :-&--A7 ---- r;aut;nas -.rrlau~r;cs CIJ 1 am-
b i h es importante seiialar que aunque la definition del lenguaje Delphi para el tipo de interfaz esti diseiiada para tener compatibilidad con COM, las interfaces de Delphi no requieren COM. De hecho, ya hemos construido anteriomente en el libro algiin ejemplo basado en interfaces sin soporte para COM. Normalmente, no sera necesario implementar estos metodos, ya que se pueden heredar de una de las clases de Delphi que ya 10s soportan. La clase mas importante es TComOb j e c t , definida en la unidad ComObj. Cuando se construye un servidor COM, generalmente se hereda de esta clase. Esta clase implementa la interfaz IUnknown (proyectando sus mktodos sobre O b j A d d R e f , O b j Q u e r y I n t e r f a c e , y O b j R e l e a s e ) y la interfaz
ISupportErrorlnfo (mediante el metodo ~ n t e racesupports~rror~nf f 0. ) La implernentacion del recuento de referencias para la clase TComOb ject soportar seguridad de hilos, ya que en lugar de utilizar los procedimientos Inc y Dec, el codigo utiliza las funciones de la API InterlockedIncrement e InterlockedDecrement. La implernentacion del metodo Release de TInt erfacedObject destruye el objeto cuando ya no hay m g referencias a el. La clase TComObject hace lo mismo. Tambien debemos recordar que cuando se utilicen variables de interfaz para referirse a 10s objetos (incluidas variables COM), Delphi automaticamente aiiade llamadas de recuento de referencias a1 codigo compilado, lo que destruye inmediatamente 10s objetos a 10s que no hay referencias. Finalmente, hay que fijarse en que el papel del metodo QueryInterf ace tiene dos vertientes:
QueryInt erface se utiliza para la comprobacion de tipos. El programa puede formularle la siguiente pregunta a un objeto: ~ E r e del tipo que s me interesa? iImplementas la interfaz y 10s metodos especificos que quiero llamar? Si la respuesta es no, el programa puede buscar otro metodo, quizas preguntando a otro servidor.
Si la respuesta es si, Query Interface normalmente devuelve un punt e r ~ objeto, utilizando su parametro de referencia de salida (obj ) . a1 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 IPersistInit, Storage, 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~e alguna 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:
444555540000)';
TGUID
'
[1AFA6D61-7B89-llDO-98DO-
Tambiln se puede pasar una interfaz identificada mediante un IID donde se necesita un GUID, y una vez mas, Delphi estraera automaticamente el IID
referenciado. Si tenemos que crear un GUID manualmente sin el entorno de Delphi, sencillamente podemos llamar a la funcion de la API de Windows CoCreateGuid, como se muestra en el ejemplo NewGuid (vease la figura 12.1). Este ejemplo es tan simple que no mostramos su codigo.
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.
Para controlar 10s GUID, Delphi ofrece la funcion GUIDToSt ring y su opuesta St ringToGUI D. Tambien se pueden utilizar las funciones correspondientes de la API de Windows, como StringFromGuid2;per0 en este caso, se debe utilizar el tipo WideString en lugar del tipo de cadena. Siempre que se utilice COM, se debe usar el tipo WideString, a menos que se utilicen las funciones de Delphi que realizan automaticamente las conversiones requeridas. Cuando se necesite sortear las funciones de Delphi que pueden llamar directamente a las funciones de la API de COM, se puede usar el tipo PWideChar (punteros a matrices de caracteres amplios terminadas en cero), o convertir el tip0 WideString en PWideChar (del mismo mod0 en que se convierte una cadena al tipo PChar cuando se llama a la API de bajo nivel de Windows).
Esta funcion de la API busca en el Registro, encuentra el servidor que registra el objeto con el GUID dado, lo carga, y, si el servidor es una DLL, llama al mCtodo DLLGetClassOb j ect de la DLL. Esta es una funcion que todo servidor de proceso debe proporcionar y exportar:
f u n c t i o n DllGetClassObject (const CLSID, IID: TGUID; var O b j ) : HResult; stdcall;
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;
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.
Despues de declarar la interfaz personalizada, podemos agregar el objeto real a1 servidor. Para ello, podemos emplear el COM Object Wizard (disponible en la ficha ActiveX del cuadro de dialog0 File>New>Other). Podemos ver este asistente en la figura 12.2. En el hay que escribir el nombre de la clase del servidor y una descripcion. Hemos desactivado la generacion de la biblioteca de tipos (en cuyo caso el asistente desactiva el campo de interfaz en Delphi 7, no como sucedia en Delphi 6) para evitar presentar demasiados temas a la vez. Tambien hay que elegir un modelo de instancia y de threahng.
I1,d 1
, , , 1 8 1 , ~ -'
1
I
I1 -i .
,,.:
I
Servidor COM: La Biblia de Delphi 7
Desc~ifliar:
El codigo generado por el asistente COM Object Wizard es muy sencillo. La interfaz contiene la definicion de la clase que hay que rellenar con metodos y datos:
type TNumber = c l a s s (TComObject, INumber) protected [ D e c l a r e I N u n ~ e rm e t h o d s h e r e )
end ;
Ademas del GUID para el servidor (almacenado en la constante C l a s s N u m b e r ) , tambiln hay c6digo en la secci6n i n i t i a l i z a t i o n de la unidad, que usa la mayoria de las opciones especificadas en el cuadro de dialog0 del asistente:
initialization TComObjectFactory.Create(ComServer, TNumber, Class-Number, ' Number ' , ' N u m b e r S e r v e r ' , ciMultiInstance, tmApartment) ;
Este codigo crea un objeto de la clase T C o m O b je c t F a c t o r y, pasando como parametros el objeto global C o r n s e r v e r , una referencia de clase para la clase que acabamos de definir, el GUID para la clase, el nombre del servidor, la descripcion del servidor, y 10s modelos de instanciacion e hilos (threading) que queremos usar. El objeto global C o m s e r v e r , definido en la unidad C o m s e r v , es un gestor de las fabricas de clases disponibles en la biblioteca del servidor. ~ s t usa su e propio metodo F o r E a c h F a c t o r y para buscar la clase que soporta una solicitud dada de un objeto COM, y guarda la pista del ni~mero objetos encontrados. de Como ya hemos visto, de hecho, la unidad C o m S e r v implementa las funciones requeridas por la DLL para ser una biblioteca COM.
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
lndica que cuando varias aplicaciones cliente necesitan el objeto-COM,el sisiema debe arrancar multiples instancias del servidor. fJnica: Indica que, incluso cuando varias aplicaciones cliente necesitan . .a . . . . .. - , serviel oojeto LVM, SOIO existe una unica insrancia ae la ap~icac~on dor; crea multiples objetos internos parar servir Ias peticiones.
I .
I .
Interna: lndica aue el obieto solo ~ u e d e crearse 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 mediante intermediation (marshaling), que puede ralentizar las operaciones. . 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 . n que llegan a1 servlaor LUIW se serializan para que el clienre pueaa realizar una operacion cada vez.
I1 1
.-I'
I.
Modelo apartamento, o " apartamento monohilo": Solo el hilo que creo el objeto puede llamar a sus metodos. Esto significa que las peticiones para cada objeto de sewidor se serializan, per0 que otros objetos del mismo sewidor pueden recibir peticiones a1 mismo tiempo. Por este motivo, el objeto de servidor debe tomar precauciones adicionales a1
acceder a datos globales del servidor (rnediante secciones criticas, mutex u otras tdcnicas-de sincronizaci6n). ~ s t mode10 de hilos se suele usar e
nara rnntrnlpc A r t i v ~ X n r l ~ ~ i r en ~ nt~rnt-t v n l n r ~ r i l 1 F
Modelo libre, o "apartamento multihilo": El cliente no tiene restricciones, lo que significa que mfiltiples hilos pueden usar el mismo objeto a1 -- - ---- -- tiemnn Pnr ecte mntivn cnda -" ----- Ae cada nhictn dche -- minmn -- ----=-. - -- ---- ----.-,--- mhndn ---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.
"
I
Ambos: Este objeto de sewidor soporta el modelo libre y el modelo apartamento. Neutral: Introducido en Windows 2000 v disponible s61o baio COM+, - este modelo indica que multiples clientes pueden llamar a1 objeto en diferentes hilos a1 mismo tiempo, pero COM garantiza que el mismo metodo no se invocara dos veces a la vez. Es necesario tornar precau, . . . . -. clones rrente a accesos concurrentes a 10saatos ael oojeto. rrajo LVM, se proyecto sobre el modelo apartamento.
I
. . . ..
..-..
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);
Numl : = CreateComObject (Class-Number) as INumber; Numl. SetValue (SpinEdit1.Value); Label1 .Caption : = ' N u m l : ' + IntToStr (Numl.Getvalue) ; Buttonl.Enab1ed : = True; Button2.Enabled : = True;
/ / 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 ;
Se debe tener en cuenta particularmente la llamada a C r e a t eComOb j e c t y la siguiente conversion a s . La llamada a la API inicia el mecanismo de construccion del objeto COM que ya hemos descrito. Esta llamada tambien carga dinamicamente el servidor DLL. El valor de retorno es un objeto IUnknown. Este objeto debe convertirse a1 tip0 de interfaz adecuado antes de asignarlo a 10s campos Numl y Num2; que ahora tienen el tipo de interfaz INumber como su tip0 de dato.
. - II---L .a
A:---.-
- nm.A-.~..~n~--n - em
, . . m ~ ~ eC-
-1
---- L
interfaces, la conversion as (o una llamada a una funcibn especifica) es la "nica f o m a de extraer una interfaz de otra. Convertir un punt&o de interfaz .. . a orro punrera a e inrerraz airecramenre es un gran error.
C
.-
El programa tambien tiene un boton (hacia la parte inferior del formulario) con un controlador de eventos que crea un nuevo objeto COM usado para obtener el valor del numero siguiente a1 100. Para ver por que se ha aiiadido este metodo a1 ejemplo, es necesario hacer clic sobre el boton del mensaje que muestra el resultado. Entonces se observa un segundo mensaje que indica que el objeto ha sido destruido. Esto demuestra que simplemente al permitir que una variable de la interfaz salga del ambito. automaticamente llama a1 metodo Release del objeto, disminuye el recuento de referencias a1 objeto y lo destriye si su recuento de referencias llega a cero. Lo mismo ocurre con 10s otros dos objetos en cuanto termina el programa. Aunque el programa no destruya esplicitamente 10s dos objetos, ambos se veran destruidos, como muestra claramente el mensa-je de su destructor D e s t r o y . Esto ocurre porque fueron declarados para ser de tipo interfaz, y Delphi va a utilizar para ellos el recuento de referencias. Por cierto, en caso de que se desce destruir una referencia a un objeto COM con una interfaz, no se puede llamar a un metodo F r e e (las interfaces no disponen de F r e e ) sino que se puede asignar nil a la variable de la interfaz; esto provocara la eliminacion de la referencia y posiblemente la destruccion del objeto.
y despues las directivas read y write. Se pueden tener propiedades de solo lectura o de solo escritura, per0 las clausulas read y write deben referirse siempre a un metodo, porque las interfaces solo contienen metodos. Aqui esta la interfaz actualizada, que forma parte del ejemplo PropCom:
type INumberProp = i n t e r f a c e ['{B36C5800-8E59-11D0-98D0-444553540000}'] f u n c t i o n GetValue: Integer; stdcall; p r o c e d u r e SetValue (New: Integer); stdcall; property Value: Integer r e a d GetValue write SetValue; p r o c e d u r e Increase; stdcall; end ;
Se le ha dado un nuevo nombre a esta interfaz y, lo que es mas importante, un nuevo identificador de interfaz. Se podria haber heredado el nuevo tip0 de interfaz del anterior, per0 esto no ofreceria ninguna ventaja real. COM no soporta herencia y, desde la perspectiva de COM, todas las interfaces son diferentes simplemente porque tienen distintos identificadores de interfaz. No es necesario decir que en Delphi se puede usar la herencia para mejorar la estructura del codigo de las interfaces y de 10s objetos de servidor que las implementan. En el ejemplo PropCom, se ha actualizado la declaracion de la clase del servidor simplemente haciendo referencia a la nueva interfaz y proporcionando un nuevo identificador del objeto de servidor. El programa cliente (llamado Testprop) puede simplemente utilizar ahora la propiedad value en lugar de 10s metodos SetValue y GetValue. Aqui vemos un pequeiio fragment0 del metodo Formcreate:
Numl : = CreateComObject (Class-NumPropServer) as INumberProp; Numl.Value : = SpinEditl.Value; Labell .Caption : = ' N u m Z : ' + IntToStr (Numl.Value) ;
La diferencia entre utilizar metodos o propiedades para una interfaz es solo sintactica, porque las propiedades de la interfaz no pueden acceder a datos privados como hacen las propiedades de clase. A1 usar propiedades se puede hacer el codigo un poco mas legible.
El programa principal recibe de vuelta una variable de interfaz con la VMT de la interfaz solicitada. Esta VMT puede usarse para invocar metodos, pcro tambien para realizar peticiones sobre otras interfaces soportadas por el objeto COM (ya que el metodo Q u e ry I n t e r f a c e esta disponible como parte de la interfaz I U n k n o w n de la VMT). El programa principal no necesita conocer las direcciones de mernoria de estos metodos, porque 10s objetos la saben, del mismo mod0 que si hiciesen una llamada polimorfica. Pero COM es incluso mas potente y no hay que saber que lenguaje de programacion se us6 para crear el objeto, siempre que la tabla de metodos virtuales siga las normas deterrninadas por CON.
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)"
function GetTypeInfoCount(out Count: Integer): HResult; s tdcall ; function GetTypeInfo(Index, LocaleID: Integer; o u t TypeInfo) : HResult; stdcall; function GetIDsOfNames(const IID: TGUID; Names: Pointer; Namecount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall; function Invoke (DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExceptInfo, ArgError: Pointer): HResult; s tdcall ; end;
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. Puede llamar directamente a la interfaz, mediante la tabla virtual de la interfaz, tratandola por ejemplo como un objeto COM normal. Esto funciona en la mayoria de 10s casos ya que la mayor parte de las interfaces de 10s servidores de Automatizacion ofrecen interfaces duales (que soportan tanto IDispatc h como una interfaz COM simple).
.. .--.-
_ ---. -.---
- .- _
a " . .
N O T A r Sa niteAe iicnr lrna vnrinnte nnrn oi~nrAnr rinn referencia a rrn n hJ-"-t n r..-..- . .----- . .. . --. -- i e
-a a
-A
--
--- -- .-.
- - -- a
.. ,
de Autornatizacion. En el lenguaje Delphi, una variante es un tipo de datos de tipo variable, es decir, una variable que puede tomar distintos tipos de datos como valor. Los tipos de datos variantes incluyen 10s basicos (como valores enteros. cadenas- caracteres v booleanosl. oero tambikn el tioo de - - -- - . . - -, - - -----.--,, -- - - --r - - interfaz I D i s p a t c h . Se comprueba el tipo de las variantes en tiempo de ejecucion; por lo que el compilador puede compilar el c6digo incluso aunque no conozca 10s metodos del sewidor de Automatizacion.
)
estan disponibles en un servidor COM generic0 o en un servidor de Automatizacion, La diferencia clave entre una biblioteca de tipos y otras descripciones de estos clementos (como codigo C o Pascal) es que una biblioteca de tipos es independiente del lenguaje. Los elementos de tipos son definidos por COM como un subconjunto de 10s elementos estandar de lenguajes de programacion y cualquier herramienta de desarrollo puede usarlos. Esta informacion es necesaria, porque si se invoca un metodo de un objeto de Automatizacion mediante una variante, el compilador de Delphi no necesita conocer nada acerca de este metodo en tiempo de compilation. Un pequeiio fragment0 de codigo quc usa la antigua interfaz de Automatizacion de Word, registrada como Word. Basic, muestra lo simple que resulta para un programador:
var
VarW: Variant; begin V a r W : = Createoleobject ( 'Word.Basic') ; VarW. FileNew; V a r W . Insert ( ' L a b i b l i a d e Delphi 7 ' ) ;
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
El termino usado para describir esta capacidad de conectarse a un servidor de dos maneras distintas, usando un enfoque mas dinamico o mas estatico, es interfaz dual. Cuando se escribe un controlador COM, se puede escoger acceder a 10s metodos de un servidor de dos maneras: se puede usar el enlace tardio y el mecanismo proporcionado por d i s p i n t e r f a c e o se puede usar el enlace temprano y el mecanismo basado en las VTables, 10s tipos de interfaz. Es importante tener presente que (ademas de otras cosas) distintas tecnicas tendran como resultado una ejecucion mas o menos rapida. Buscar una funcion por su nombre (y realizar la comprobacion de tipos en tiempo de ejecucion) es el enfoque mas lento, mientras que usar d i s p i n t e r f a c e es mucho m h rapido, y usar la llamada directa a la VTable es el enfoque mas rapido de todos. Veremos una demostracion de esto en el proximo ejemplo TLibCli.
En este asistente, escribimos el nombre de la clase (sin la T inicial, ya que se aiiadira automaticamente a la clase de Delphi que la implemente) y hacemos clic sobre el boton OK. Delphi abrira a continuacion el editor de la biblioteca de tipos.
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.
NOTA:Los m&odos contenidos en interfaces de Automatization en Delphi suelen utilizar la convencion de llamadas s a f e ca 11.Esto envuelve en un bloque t rylexcept a cada mitodo y proporciona un valor de retorno
^
~ I C U C L C I I I I I U ~ U quc I L I U I C ; ~ I I U I U C
:--I:--
^--^-
GXILU. ~ U I I U I C plcpala ~
- ^ ^ a - - -
--A -:^
'P--L:*-
---uujcru us; un
^L:^c^
-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;
A1 hacer clic sobre el boton Refresh de la barra de herramientas del editor Type Library se genera (o actualiza) la unidad Delphi con la interfaz.
Delphi correspondiente y la declaracion del objeto servidor. La biblioteca de tipos esta conectada a1 proyecto mediante una sentencia de inclusion de recursos, aiiadida a1 codigo fiente del archivo del proyecto:
{$R
*. TLB]
Siempre es posible volver a abrir el editor Type Library usando la orden View>Type Library o seleccionando el archivo TBL adecuado en el cuadro de dialog0 habitual File Open de Delphi. Como hemos dicho anteriormente, la biblioteca de tipos se convierte tambien en una definicion de interfaz y se aiiade a una nueva unidad Pascal. Esta unidad es bastante grande, asi que solo hablaremos de sus elementos clave. La parte mas importante es la nueva declaracion de interfaz:
type IFirstServer = interface (IDispatch) ['{89855B42-8EFE-llD0-98D0-444553540000]'] procedure ChangeColor; safecall; function Get-Value : Integer; safecall; procedure Set-Value(Va1ue: Integer); safecall; property Value: Integer r e a d Get-Value write Set-Value; end :
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:
Todas las declaraciones de este archivo se pueden considerar un soporte de implernentacion interno oculto. No es precis0 comprenderlas totalmente para escribir la mayoria de las aplicaciones de Automatizacion. Finalmente, Delphi genera un archivo que contiene la declaracion del objeto de Automatizacion. Esta unidad se aiiade a la aplicacion y es la unica con la que trabajaremos para terminar el programa.
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
--
Todo esto no es muy distinto de la creacion de fabricas de clases que hemos visto anteriormente. La unidad c o r n s e r v e r se conecta a la funcion I n i t Proc del sistema para registrar todos 10s objetos COM como parte del arranque de la
aplicacion de servidor COM. La e.jecucion de este codigo se dispara mediante la llamada Application. I nit izali ze,que atiade Delphi de manera predeterminada al codigo fuente del proyecto de cualquier programa. Tambien se puede agregar la informacion del servidor a1 Registro de Windows ejecutando esta aplicacion en el equipo de destino (donde se desea instalar el servidor de Automatizacion), o al e.jecutarla pasandole el parametro /regserver en la linea de comandos. Se puede hacer seleccionando Inicio>Ejecutar. usando el Esplorador de Windows para crear un atajo, o e.jecutando el programa en Delphi despues de haber introducido el parametro para la linea de comandos (mediante Run> Parameters). Otro parametro de linea de comandos, / unreg s e rve r,se utiliza para eliminar este servidor del Registro.
cion cliente, porque se trata de escribir el cilntrol#lor.de Awtdmati~aci6n, no un s e r v i d ~;El proyeqto pelphi da un sontmlador no debeda inchi-la ~. -, b i b l i w a &-tipis 44sepiid&4qa$ se,c~pe&. La unidad de importacion de la biblioteca de tipos toma en Delphi su nombre de acuerdo con la biblioteca de tipos, afiadiendo TLB al final. En este caso, el nombre de la unidad TlibdemoLib TLB. Ya hekos comentado que uno de 10s elementos de esta unidad, generado p i r el editor de la biblioteca de tipos, es la clase creation. Esta es la implernentacion de la primera de las dos funciones definidas en la interfaz de esta clase:
TAP13 Tetrnlnal Manage1 1 0 Type Lbra~y Wers~on 0) 1 Te~rnRecognt~on0 rJers~on 01 2 2 , TlFFLoader 1 0 Type L~brary rJerston 1 0)
names: TF~rstServer
I
.. I
Ud 6 name:
I ~ . ~ r c h w o sprograma\8orIand\D~h17\Impo de
Se puede emplear para crear un objeto servidor (y posiblemente arrancar la aplicacion de servidor) en el mismo ordenador. Como se puede ver en el codigo, la funcion es simplemente un metodo abreviado de la llamada a createcomOb j etct, que nos permite crear una instancia de un objeto COM si se conoce su GUID. Como alternativa, es posible utilizar la funcion Creat eOleOb j ect, que precisa como parametro el nombre registrado del servidor. Esiste otra diferencia entre estas funciones de creacion: CreateComOb ject devuelve un objet0 del tipo IUnknown, mientras que CreateOleOb j ect devuelve un ob.jeto del tipo IDispatch. En este ejemplo, utilizaremos CoFirstServer. Create.A1 crear el ob.jeto servidor se obtiene como valor de retorno una interfaz IFirstServer que puede usarse directamente o almacenarse en una variable variante. Veamos un e.jemplo del primer metodo:
var MyServer: Variant; begin M y S e r v e r : = CoFirstServer.Create; MyServer.ChangeCo1or;
Este codigo, basado en variantes, no es muy distinto del correspondiente a1 primer controlador creado en este capitulo (el que usaba Microsoft Word). Este es el codigo alternative, que tiene el mismo efecto:
var
IMyServer: IFirstServer; begin IMyServer : = CoFirstServer.Create; 1MyServer.ChangeColor;
Ya hemos visto como usar la interfaz y la variante. En cuanto a la intcrfaz de envio, se puede declarar una variable del tip0 de la interfaz de envio. que en este caso seria:
var
DMyServer: IFirstServerDisp;
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 ;
Con este codigo, cuando el programa cliente arranca, el programa servidor se activa inmediatamente. Cuando finaliza el programa, se destruye el campo del formulario y se cierra el servidor. Otra alternativa mas es declarar el objeto en el formulario, pero despuds crearlo solamente cuando se use: como en estos dos fragmentos:
// M y S e r v e r B i s : V a r i a n t ; if varType (MyServerBis) = varEmpty then M y S e r v e r B i s : = CoFirstServer.Create; MyServerBis.ChangeColor; // IMyServerBis : IPirs tServer; if not Assigned (IMyServerBis) then IMyServerBis : = CoFirstServer.Create; 1MyServerBis.ChangeColor;
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~os ~utoma%zacion de (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.
Como podemos ver en el Object Inspector, 10s componentes tienen pocas propiedades. Autoconnect indica cuando activar el servidor COM. Cuando el valor es True, el objeto servidor se carga en cuanto se crea el componente envol-
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 e e r & A ~ r 1 1 a m - n A n Q 1-~ f iua n~ ; Aun v 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 a vu r\= y s a L G L ~
ren;c+m
Activeobject de la API. Por supuesto, solo se puede registrar una instancia para cada servidor COM en un momento dado.
ADVERTENCIA: Para ejecutar esta apLicaci6n y otras sirnilares, debe instalarse y registrarse la biblioteca StdVCL en el ordenador cliente. En nuestro ordenador, se registra durante la instalacibn de Delphi.
Los metodos Set y Get de las propiedades de tipos complejos copian informacion de las interfaces COM a 10s datos locales y luego desde estos a1 formulario y viceversa. Los dos metodos de las cadenas, por ejemplo, hacen esto llamando a las funciones Getolestrings y Setolestrings de Delphi. La aplicacion cliente usada para demostrar esta caracteristica se llama ListCli. Los dos programas son complejos; per0 en lugar de mostrar aqui todos sus detalles, es mejor que se estudie el codigo por si mismo, ya que 10s programadores de Delphi no suelen utilizar esta tecnica.
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.
mente, se activa utilizando el portapapeles y con la operacion Pegar como hipervinculo. A1 editar 10s datos en la aplicacion contenedor, en realidad se modifican 10s datos originales, que se almacenan en un archivo diferente. Ya que el programa servidor hace referencia a un archivo cornpleto (solo parte del cual puede estar enlazado en el documento cliente), el servidor se activara en una ventana independiente, y actuara sobre el archivo original cornpleto, no solo sobre 10s datos que se han copiado. Sin embargo, cuando se tiene un objeto incrustado o insertado, el contenedor puede soportar la edicion visual (o en el sitio), que significa que se puede modificar el objeto en el context0 dentro de la ventana principal del contenedor. Las ventanas de servidor y de la aplicacion contenedor, sus menus y sus barras de herramientas se uniran automaticamente, permitiendo que el usuario trabaje con una sola ventana sobre varios tipos de objetos distintos (y por ello con distintos servidores OLE) sin abandonar la ventana de la aplicacion contenedor. Otra diferencia clave entre la insercion y el enlace es que 10s datos de un objeto incrustado se almacenan y gestionan desde la aplicacion contenedor. El contenedor guarda el objeto incrustado en sus propios archivos. Por contra, un objeto enlazado reside fisicamente en un archivo independiente. En ambos casos, la aplicacion por el contrario tiene que saber como gestionar el objeto y sus datos (ni siquiera como mostrarlos) sin la ayuda del servidor. Teniendo en cuenta la relativa lentitud de OLE y la cantidad de trabajo necesaria para desarrollar servidores COM, son comprensibles las causas de que este enfoque jamas consiguiera pegar. Los contenedores de documentos compuestos pueden soportar COM en diverso grado. Se puede colocar un objeto en un contenedor insertando un nuevo objeto, pegando uno desde el portapapeles, arrastrandolo desde otra aplicacion, etc. Una vez que el objeto se encuentra en el contenedor, se pueden realizar operaciones sobre el, mediante las acciones o verbos disponibles del servidor. Normalmente la accion de edicion es la accion predefinida (la que se realiza cuando se hace doble clic sobre el objeto). Para otros objetos, como fragmentos de audio o video, la reproduccion es la accion predefinida. Tipicamente se puede ver una lista de las acciones soportadas por el objeto contenido si se hace clic con el boton derecho del raton sobre el. La misma informacion t a m b i h se encuentra disponible en muchos programas mediante la opcion de menu EdibObject, que muestra una lista de las acciones disponibles para el objeto actual.
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. y Mas especificamente, 10s menus desplegables File (0) 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.
TRUCO:Para que todas las operaciones de ajuste de tamaiio funcionen sin problema, deberiamos colocar el componente del contenedor OLE en un componente de panel y alinear ambos con la zona de cliente del formulario.
lmagen Cclores A y d a
Figura 12.6. La segunda barra de herramientas del ejemplo OleCont (arriba) es sustituida por la barra de herramientas del servidor (debajo).
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.
ADVERTENCIA: Ya que el ejemplo WordCont incluye un objeto de un tipo especifico (un documento de Microsoft Word) no se ejecuta si esa aplicacion no esta instalada. Cuando la version del servidor es diferente tambien se pueden producir ciertos problemas. Para estas versiones problematicas, puede que sea necesario reconstruir el prograrna siguiendo 10s mismos pasos.
En cl formulario de este ejemplo, se ha aiiadido un componente
OleContai ner, luego se ha definido su propiedad AutoAct ivate como aaManual (para que la unica interaction posible sea con nuestro codigo) y
aiiadido una barra de herramientas con un par de botones. El codigo es sencillo, una vez que se sabc que el objeto insertado correspondc a un documento de Word. Este es un ejemplo (la figura 12.7 muestra el efecto de estc codigo):
procedureTForml.Button3Click(Sender: TObject);
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
+ ' ) '#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-
que el concept0 de componentes software reciclables sea anterior a Visual Basic (proccde de las teorias de la programacion orientada a objetos). El primer estandar tecnico promovido por Visual Basic fue VBX, una especificacion de 16 bits que estaba completamente disponible en Delphi 1. A1 pasar a las plataformas de 32 bits, Microsoft sustituyo el estandar VBX con 10s mas potentes y mas abiertos controles ActiveX.
Figura 12.7. El ejernplo WordCont muestra como usar Autornatizacion con un objeto incrustado.
__P
cambio de nombre refleja una nueva estrategia de ventas por parte de Microsoft mas que una innovacion tkcnica. No es sorprendente entonces I-.--*--I-1 -*:-.-w - >-.- - . ---L! . --.. I -- -..- -. . qut: 10s conrrom ncaveA se guaraen en arcnwos con la extension .ocx.
L
11-
Desdc un punto de vista general, un control ActiveX no es muy distinto de un control de Windows. La diferencia principal esta en la interfaz del control, la interaccion entre el control y el resto de la aplicacion. Los controles de Windows tipicos usan una interfaz basada en mensajes; 10s objetos de Automatizacion y 10s controles ActiveX usan propiedades, metodos y eventos (como 10s propios componentes de Delphi). Empleando la jerga de COM, un control ActiveX es un "objeto de documento compuesto que es implementado como un servidor DLL en proceso y soporta Automatizacion, edicion visual y una activacion endogena". Algo que esta perfectamente claro. Veamos lo que significa. Los servidores COM pueden implementarse de tres maneras: Como aplicaciones independientes (por ejemplo, Microsoft Escel). Como servidores fuera de proceso, es decir, archivos ejecutables que no pueden ejecutarse por si mismo y solo pueden ser invocados por un servidor (por ejemplo, Microsoft Graph y aplicaciones similares).
Como servidores en proceso, como las DLL que se cargan en el mismo espacio de memoria que el programa que las utiliza. Los controles ActiveX solo pueden implementarse mediante esta ultima tecnica, que es tambien la mas rapida: como servidores en proceso. Aun mas, 10s controles ActiveX son servidores de Automatizacion. Esto significa que se puede acceder a propiedades de estos objetos y llamar a sus metodos. Se puede ver un control ActiveX en la aplicacion que se usa e interactuar directamente con el en la ventana de la aplicacion contenedor. Este es el significado del termino edicion visual o activacion in situ. Un simple clic activa el control, en lugar del doble clic usado por 10s documentos OLE, y el control se activa siempre que esta visible (que es lo que significa el termino activacion endogena) sin tener que hacer doble clic sobre el. En un control ActiveX, las propiedades pueden identificar estados, per0 tambien pueden activar metodos. Las propiedades pueden referirse a valores agregados, matrices, subobjetos. Las propiedades tambien pueden ser dinamicas (o de solo lectura, por usar el mismo termino que en Delphi). Las propiedades de un control ActiveX se dividen en dos grupos: propiedades de reserva que necesitan implementar la mayoria de 10s controles; propiedades de ambiente que ofrecen informacion sobre el contenedor (como las propiedades parent C o 1or y Parent Font en Delphi); las propiedades extendidas gestionadas por el contenedor, como la posicion del objeto; y las propiedades personalizadas, que pueden ser cualquier cosa. Los eventos y 10s metodos son exactamente eso. Los eventos tienen que ver con un clic de raton, la pulsacion de una tecla, la activacion de un componente y otras acciones especificas del usuario. Los metodos son funciones y procedimientos relacionados con el control. No existe una gran diferencia entre 10s conceptos ActiveX y Delphi de eventos y metodos.
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
.-
-*"-
< -
. -
4. Hacemos clic sobre cl boton Install para afiadir esta nueva unidad a un paquete Delphi y a la Component Palette.
en la ficha ActiveX de la paleta, sino en la ficha Internet. El control se llama WebBrowser y e s un envoltorio del motor de Internet Explorer de Microsoft. El ejemplo WebDemo es un navegador Web muy simple; tiene un control ActiveX TWebBrowser que cubre su zona de cliente, una barra de control cn la parte superior y una barra de estado en la parte inferior. Para acceder a una pagina Web dada, el usuario puede escribir una URL en el cuadro combinado de la barra de herramientas. seleccionar una URL ya visitada (se guardan en el cuadro combinado) o bien hacer clic sobre el boton Open File y seleccionar un archivo local. La figura 12.8 muestra un ejemplo de este programa.
Login
Code Central Qual~ty Central The Coad Letter Get Published BOOKS Developer Support Shop Chat Downloads Search Logm
-I
Figura 12.8. El programa WebDemo despues de escoger una pagina muy conocida
La implernentacion real del codigo empleado para seleccionar un archivo Web o un archivo HTML local, se halla en el metodo Gotopage:
procedure begin
TForml.GotoPage(ReqUr1: (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:
do en multiples controles Delphi o en componentes Delphi que no desciendan de T W i n C o n t r o l . En cualquier caso, opcionalmente se puede preparar una ficha de propiedades para el control y utilizarla como una especie de editor de propiedades para definir el valor inicial de las propiedades del control en cualquier entorno de desarrollo (una alternativa a1 Object Inspector de Delphi). Dado que la mayoria de 10s entornos permiten edicion limitada, es mas importante escribir una ficha de propiedades que un editor de componente o de propiedades para un control Delphi.
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
sobre el boton OK: Delphi construira el codigo fuente completo de un control ActiveX. El uso de las tres casillas de verificacion de la parte inferior de la ventana del asistente puede no resultar obvio. Si hacemos que el control sea licenciado, Delphi incluira una clave de licencia en el codigo y proporcionara este mismo GUID en un archivo .LIC independiente. Este archivo de licencia es necesario para usar el control en un entorno de diseiio sin la clave de licencia apropiada para el control o para usarlo dentro de una pagina Web. La segunda casilla de verificacion permite incluir informacion sobre la version para el ActiveX en el archivo OCX. Si esta activada la tercera casilla de verificacion, el asistente aiiadira automaticamente a1 control un cuadro Acerca de. Si echamos un vistazo a1 codigo que genera el asistente, veremos que el elemento clave es la creacion de una biblioteca de tipos y, por supuesto, una unidad de importacion de la biblioteca de tipos correspondiente con la definicion de una interfaz (dispinte r face) y otros tipos y constantes. En este ejemplo, el archive de importacion se llamax~rrowT L B . PAS.Lo mas aconsejable es estudiarlo para comprender corn0 define elp phi un control ActiveX. La unidad incluye un GUID para el control, constantes para la definicion de 10s valores correspondientes a 10s tipos COM enumerados utilizados por las propiedades del control Delphi (como TxMdAr rowDir) y la declaracion de la interfaz IMdArrowX. La parte final de la unidad de importacion incluye la declaracion de la clase TMdArrowX. Se trata de una clase derivada de Tolecontrol que se puede utilizar para instalar el control en Delphi, como se vio a1 principio de este capitulo. No es necesaria para construir el control ActiveX, solo para instalarlo en Delphi. El resto del codigo y el que personalizaremos esta en la unidad principal, que en el ejemplo se llama MdWArrowImpll. Esta unidad tiene la declaracion del objeto servidor ActiveX, TMdWArrowX,que hereda de TActiveXControl e implementa la interfaz especifica IMdWArrowX.Antes de personalizar este control, conviene ver su funcionamiento. Primero hay que compilar la biblioteca ActiveX y luego registrarla con la opcion de menu RuwRegister ActiveX Server de Delphi. Despues se puede instalar el control como hemos hecho anteriormente, a excepcion de que hay que especificar un nombre diferente para la nueva clase, para evitar conflictos de nombres. Si se usa este control, no parecera muy diferente del control VCL original, per0 la ventaja es que el mismo componente ahora puede instalarse en otros entornos de desarrollo.
de propiedades, metodos o eventos a un control ActiveX, per0 no para un control VCL. Se puede abrir la unidad Delphi con la implementacion del control ActiveX y elegir Edit>Add to Interface. Como alternativa, se puede emplear la misma orden desde el menu contextual del editor. Delphi abre el cuadro de dialogo Add To Interface:
&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 ;
Las declaraciones que tenemos que introducir en el cuadro de dialogo Add TO Interface se aiiaden automaticamente a1 archivo TLB (de la biblioteca de tipos) del control, a su unidad de importacion de la biblioteca y a su unidad de implementacion. Todo lo que tenemos que hacer para completar el control ActiveX es rellenar 10s metodos G e t y S e t de la implementacion. Si ahora instalamos de nuevo el control ActiveX en Delphi, apareceran las dos nuevas propiedades. El unico problema es que Delphi utiliza un editor de enteros sin formato que dificulta la entrada de valores de nuevos colores a mano. Por el contrario, un programa puede emplear la funcion RGB para crear el valor de color adecuado.
colores, fuentes, imhgenes y cadenas. Los GUID de esas clases se indicarL con las constantes C l a s s-D C o l o r P r o p P a g e , C l a s s-D F o n t P r o p - --r a g e , ~ ~ a s s - v r l c ~ u r e r r o p r ay e ~- -a uss c r l g r r o p r a g e g ~ - s en la unidad A c C t r l s .
. .
-
--
-.-
- 7
--2
-L
.--.-..-
---
. -.a .
,-."L.-2
..
- - .
En la ficha de propiedades podemos aiiadir controles como a un formulario normal de Delphi y escribir codigo para que 10s controles interactuen. En el e.jemplo XArrow, hemos aiiadido a la ficha de propiedad un cuadro combinado con 10s valores posibles de la propiedad D i r e c t i o n , una casilla de verificacion para la propiedad F i l l e d , un cuadro de edicion con un control UpDown para establecer la propiedad A r r o w H e i g h t y dos figuras con 10s botones correspondientes para 10s colores. Se puede ver este formulario en el IDE de Delphi mientras que se traba.ja con el control ActiveX en la figura 12.9. El unico codigo aiiadido a1 formulario tiene que ver con 10s dos botones utilizados para cambiar el color de las dos figuras, que ofrecen una vista previa de 10s colores del control ActiveX real. El evento O n C l i c k del boton emplea un componente C o l o r D i a l o g , como de costumbre:
procedure TPropertyPagel.ButtonPenClick(Sender: TObject); begin with ColorDialogl do begin Color := ShapePen.8rush.Color; if Execute then
begin
. . . #. . . . . . . . . . . . .'. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . .
Direction. adR~ghf(3)
11
New...
II
I
.....................................
....................................... ...................................... ....................................... . . . . . . . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ........... .
Pencolor:
h o w point color:
NW.
Figura 12.9. El control ActiveX XArrow y su pagina de propiedades, dentro del entorno de Delphi.
Lo importante de este codigo es la llamada a1 mktodo Modified de la clase T Proper t yPage.Esta llamada es necesaria para que el cuadro de dialogo de la ficha de propiedad sepa que hemos modificado uno de 10s valores y tambien para activar el boton Apply. Cuando el usuario interactua con uno de 10s otros controles del formulario, la llamada a M o d i f i e d se hace automaticamente a1 metodo de la clase TPropertyPage que gestiona el mensaje c m Changed interno.Sin embargo, como usuario no se cambian 10s botonei de estos controles, se necesita aiiadir esta linea esplicitamente.
3
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
datos desde la ficha de propiedades a1 control ActiveX y viceversa, como se puede ver en el codigo de ejemplo. El ultimo paso es conectar la ficha de propiedades en si con el control ActiveX. Cuando se creo el control, el asistente ActiveX Control Wizard de Delphi aiiadio automaticamente una declaracion para el metodo Def i n e p r o p e r t y P a g e s en la unidad de irnplementacion. En este metodo, simplemente hemos llamado a1 metodo D e fi n e P r o p e r t y P a g e (esta vez el nombre del metodo es singular) para cada ficha de propiedades que queramos aiiadir a1 control. El p a r h e t r o de este metodo es el GUID de la ficha de propiedades, algo que podemos hallar en la unidad correspondiente:
p r o c e d u r e TMdWArrowX.DefinePropertyPages( Definepropertypage: TDefinePropertyPage); begin DefinePr~pertyPage(Class~PropertyPagel); end;
Ya hemos acabado de desarrollar la ficha de propiedades. Despues de volver a compilar y a registrar la biblioteca ActiveX, ya podemos instalar el control ActiveX dentro de un entorno de desarrollo anfitrion (incluido Delphi) y ver el aspect0 que presenta, como puede ver en la anterior figura 12.9.
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
metodos G e t y S e t , que cambian o devuelven las propiedades correspondientes a1 formulario Delphi; este codigo tambien implementa 10s eventos, que vuelven a ser eventos del formulario. Los eventos de T Form estan conectados con 10s metodos internos cuando se crea el formulario. Por ejemplo:
procedure TAXForml.Initialize; begin OnActivate : = ActivateEvent; end ;
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;
Debido a esta proyeccion no es precis0 controlar 10s eventos del formulario directamente. En su lugar, se puede aiiadir codigo a estos controladores predeterminados o simplemente sobrescribir 10s metodos de T F o r m que terminan Ilamando 10s eventos. Este problema de la proyeccion tiene que ver solo con 10s ejemplos del propio formulario, no con 10s eventos de sus componentes. Se pueden seguir controlando 10s eventos de 10s componentes como siempre.
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 formularios activos no disponible para un formulario normal.
- -
ADVERTENCIA: Debido a lo que se puede considerar un error, en Delphi 7 estos comandos ~610 activan para un ActiveForm. Si esthn desactivados se se puede usar el truco siguiente: se aiiade un ActiveForm a la biblioteca
ActiveX actual, que habilitara 10s elementos del menu; inmediatamente desputs se elimina el ActiveFom y 10s elementos del menu seguirhn estando disponibles. El problema es que hay que repetir esta operacion cada vez que se vuelve a abrir el proyecto (a1 menos hasta que Borland solucione el error). La primera orden permite especificar donde y como proporcionar 10s archivos adecuados. En este cuadro de dialog0 se puede dcfinir el directorio del servidor para desplegar el componente ActiveX, la URL de este directorio y el directorio del servidor para desplegar el archivo HTML (que tendra una referencia a la biblioteca Active X mediante el URL proporcionado). Tambien se puede especificar la utilization de un archivo comprimido CAB, que puede almacenar el archivo OCX y otros archivos auxiliares, como paquetes, facilitando la entrega de la aplicacion a1 usuario. Un archivo comprimido significa una descarga mas rapida. Hemos generado el archivo HTML y el archivo CAB para el proyecto XClock en el mismo directorio. Al abrir este archivo HTML en lnternet Explorer se produce el resultado que muestra la figura 12.10. Si todo lo que se ve es una cruz roja que indica un fa110 en la descarga del control, existen varias posibles explicaciones para este problema: Internet Explorer no permite la descarga de controles, no cumple el nivel de seguridad para el control sin firma, existe una diferencia en el numero de version del control, y cosas asi.
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:
Aunque podria parecer una tecnica util? es importante tener en consideracion el limitado papel de un formulario ActiveX situado en una pagina Web. Irnplica permitir que un usuario descargue y ejecute una aplicacion de Windows personalizada, que conlleva muchas preocupaciones acerca de la seguridad. Un control ActiveX puede acceder a la informacion del sistema, como el nombre de
usuario, la estructura de directorios y cosas por el estilo. Podriamos decir mas, per0 no decirlo mejor.
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.
Hemos llamado a la aplicacion COM+ La biblia de Delphi 7 Przrebn, tal y como muestra la figura 12.12 en la consola de administracion de Servicios de componentes de Microsoft. Este es el terminal que se puede usar para ajustar el comportamiento de sus componentes COM+, estableciendo su modelo de activacion (activacion en el instante, reserva de objetos, y otros), su soporte de transaccion y 10s modelos de seguridad y concurrencia que se desea usar. Tambien se puede usar esta consola para vigilar 10s objetos y las llamadas de metodos (en caso de que tarden mucho tiempo en ejecutarse). En la figura 12.12 se puede ver que hay dos objetos activos.
i@ PI&
kch -
vu
Ventma
- -
I~
?' 1 ~
!-.
>:
* : 1
-~~
& I a mo > = , m y E l J
ompansntompansnt~--.-
,-
.
-ompansnt
--
.~-
* -
+
"
~ ddepoqdma_. .
. CLSID- .... . .
rnK
Apkadonc. COM+
l i
...
md. L
Necesar~o
5 y f . o ~ . Moddo.., ~ Subpro. ..
&, .wrLnmie5
[i i 3
E
+A cCorrponcr*r
*. ' J
:< +
7 (ck~
a CmFlurl .ComFi
Interkc5
1susmpoone
.4 I -..
i ,
cTe5
--
. ---
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
ADVERTENCIA: Ya que se ha; ireado uno o mits objetw, la bibli&ea COM sigue wgada en el entomo COM+ y alpnos de 10s objctos putden P pen0 haya clhtsp consctados a euos. ror esre mouvo, generameme no se pude rc~ompilmb biblioteca COM despds de usarla, a no ser que se use M M C para cmafla o se establezca un Transaction Timeout de 0 segundosa MMC.
'
---- - -
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;
--.
- - -- -
--
--
.-
La unidad principal incluye el objeto COM falso (el metodo es abstracto, de manera que no tiene implementacion) y su factoria de clase, para permitir que el servidor se registre a si mismo. Se puede compilar la biblioteca e instalarla en el entorno COM+, siguiendo estos pasos:
1. En la consola de Servicios de componentes de Microsoft, se escoge una aplicacion COM+, vamos a la carpeta Componentes y usamos el menu contextual para aiiadir un nuevo componente.
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;
Despues de compilar esta biblioteca, se puede instalar en primer lugar en el entorno COM+, y despues enlazarla a1 evento. Este segundo paso se realiza en la consola de adrninistracion de Servicios de componentes seleccionando la carpeta Subscripciones dentro del registro del objeto de eventos, y usando el atajo de menu Nuevo~Subscripcion. el asistente que aparecera, se debe escoger la En interfaz a implementar (probablemente solo haya una interfaz en la biblioteca de eventos COM+); se vera una lista de componentes COM+ que implementan esta interfaz. Escoger uno o mas de ellos, prepara el enlace de la suscripcion, que se muestra dentro de la carpeta Subscripciones. La figura 12.13 muestra un ejemplo de la configuracion mientras que se crea este ejemplo.
kchm
Prodn
Ver
\&a
AWa
+*
Bl5 5
-@-I
w e - _. dcntdaz Id I
I Scmdu-
.+ :
.
E UYes l
4&
@$
&ma
r1ptk.a;
$ cm+
8-
a cmPCI51c a u
+
' Lnterfaer J
*I
1
- -- - - - - -
17
Figura 12.13. Un evento COM+ con dos suscripciones en la consola Servicios de componentes.
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) ;
El ejemplo crea el objeto COM en el metodo Formcreate para mantener presente la referencia, per0 el efecto es el mismo. Ahora el programa cliente piensa que esta llamando a1 objeto del evento COM+, per0 este objeto (ofrecido por el entorno COM+) llama a1 metodo para cada uno de 10s suscriptores activos. En este caso se acabara viendo este cuadro de mensaje:
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
e inhabilitar con facilidad desde la consola de administracion, modificando el entorno COM+ sin modificar el codigo del programa.
1;
5
TRUCO: Los pasos--suge~dbs deberian funCibna~ tambitn en Delphi 5 , . Delphi 7 aiiad; un s i s t e k de irnportaciirn automitico que a veces tiene problemas con p a r k del codigo generado pqr'el compilador d q Delphi for .NET Preview.
..
,(
Para demostrar las caracteristicas de importacion de .NET, hemos creado una biblioteca .NET con una interfaz y una clase que la implementa. La interfaz y la clase se parecen a las del ejemplo FirstCom ya comentado. Este es el codigo de la biblioteca, que debe compilarse con el compilador de Delphi for .NET Preview. Hay que crear un objeto, o el enlazador eliminara casi todo de la biblioteca compilada (ensamblaje, en la jerga de .NET):
library uses
NetNurnberClass in NetLibrary
'NetNumberClass.pasl;
El codigo se encuentra en la unidad NetNumberClass, que define una interfaz y una clase que la implementa.
type INumber = interface function GetValue: Integer; procedure SetValue (New: Integer) ; procedure Increase; end ; TNurnber = class(TObject, INumber); private fValue: Integer; public constructor Create; function GetValue: Integer; procedure SetValue (New: Integer) ; procedure Increase; end ;
Hay que fijarse en que, a1 contrario que un servidor COM, la interfaz no necesita un GUID, de acuerdo con las reglas de .NET (aunque puede tener uno usando un atributo de la clase G u i d A t t r i b u t e ) . El sistema generara automaticamente uno. Despues de compilar este codigo (disponible en la carpeta N e t I m p o r t del codigo de este capitulo) con Delphi for .NET Preview (in0 con Delphi 7!), se necesita realizar dos pasos: en primer lugar, ejecutar . N E T Framework A s s e m b l y R e g i s t r a t i o n U t i l i t y d e M i c r o s o f t ( r e g a s m ) ; en segundo lugar, ejecutar T y p e L i b r a r y I m p o r t e r d e B o r l a n d (tlibimp). En teoria deberiamos poder saltarnos este paso y usar directamente el cuadro de dialog0 Import Type Library, per0 con algunas bibliotecas el uso del programa tlibimp es necesario. En la practica, hay que ir a la carpeta en que se haya compilado la biblioteca y escribir en la linea de comandos 10s dos comandos en negrita (deberia verse como resultado el resto del texto capturado aqui):
D:\md7code\l2\NetImport>regasm NetLibrary.dl1 Microsoft (R) .NET Framework Assembly Registration Utility 1.1.4322.573 Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
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
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 1 e n t D a t S e t para acceder a 10s datos locales, sin i 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
adaptarse a CLX y a Linux recompilandolos y haciendo referencia a archivos CDS en las carpetas apropiadas. Este capitulo trata 10s siguientes temas: Componentes de bases de datos en Delphi. Alternativas de acceso a bases de datos. Uso de controles data-aware. El control DBGrid.
Manipulation de campos de tablas.
TRUCO:En Kylix, la perspectiva general es ligerarnente distinta. Borland ha decidido no adaptar la antigua tecnologia BDE a Linux y en su lugar se ha centrado en urn nueva fina capa de acceso a bases de datos, dbExprcss.
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.
TRUCO:La dis@bdidsrd de un controlador dbExpress para SQL Server de Microsoft en flelphi 7 I' & huec6 sigdficativo.ksta base de datos se l& ed la pr aWindo~&s, 10s desjutoiladores que aecesitaban w i m y suele usar ma solucih f~cfh&W3 ttailspo~'te entre disthtos senridores de bases de datos solfan ten& $Ire incluir el sopotte para SQL Server. Ahora existe una razbn rnenos para &r que seguir utilizaBdo BDE. Borland ha publicado una actualitaci6n del controlador &Express de SQL Server que se incluye con Delphi 7 para solucionar un par de Mectos.
NOTA: Por este motivo no se encontrara en este libro ninguna explicacion sobre BDE.
ble y permitir una configuracion y mantenimiento del servidor desde la propia aplicacion cliente personalizada.
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.
NOTA: Microsoft eski sustituyendo ADO por su version .NET, que basa en Ias mismas ideas centrales. Por eso, usar ADO podria I
ruts
r
.,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.
ADVERTENCIA: Esto explica que no se pueda usar este enfoque en una situation multiusuario o multiaplicacion. Si dos programas o dos instancias del mismo programa cargan el mismo archivo ClientDataSet en ' memoria y moairlcan 10saatos, la umma tama guaraaaa soorescrmira 10s cambios realizados por otros programas.
J'C.
1
'lr'
I 1
.?.
Este soporte para la permanencia del contenido de un ClientDataSet se creo hace unos cuantos afios como un mod0 de implementar el llamado modelo de maletin. Un usuario podia (y sigue pudiendo) descargar datos de su servidor de bases de datos a1 cliente, guardar algunos de 10s datos, trabajar en desconesion (mientras viaja con un portatil, por ejemplo) y volver a conectarse finalmente para enviar 10s cambios.
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
end end
Shared\Da t a \ C u s t o m e r . c d s '
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
,, .
6133 1/3 Stone Avenue Box 744 63365 Nez Perce S l m t 1455 North 44th St.
d l
Figura 13.1. Una tabla local de rnuestra activa en tiempo de disefio dentro del IDE de Delphi.
Cuando se hacen cambios y se cierra la aplicacion, 10s datos se guardan automaticamente en el archivo. (Podria quererse desactivar el registro de cambios, del que ya hablaremos, para reducir el tamaiio de estos datos.) El conjunto de datos tambien dispone de un metodo s a v e T o F i 1e y un metodo L o a d F r o m F i l e que se pueden usar en el codigo. Se ha inhabilitado el c l i e n t Dataset en tiempo de diseiio para evitar incluir todos sus datos en el archivo DFM del programa y en el archivo ejecutable compilado, prefiriendo mantener 10s datos en un archivo independiente. Para hacer esto; es necesario cerrar el conjunto de datos en tiempo de diseiio, despues de comprobarlo, y aiiadir una linea a1 evento o n c r e a t e del formulario para abrirlo:
procedure TForml.FormCreate begin c d s .Open; end;
(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.
La bibliotcca Midas es una biblioteca en lenguaje C, per0 desde Delphi 6 puede enlazarse directamente con un ejecutable a1 incluir la unidad M i d a s L i b (una DCU especial producida por un compilador de C). En este caso, no sera necesario distribuir la biblioteca en formato DLL.
Hay que fijarse en la propiedad S t o r e De f s , que se fija automaticamente como T r u e cuando se edita el conjunto de definiciones de campos. De manera predeterminada, un conjunto de datos en Delphi carga sus metadatos antes de abrirse. Solo se utiliza estos metadatos locales si se almacena una definition local en el archivo DFM (guardar las definiciones de campos en el archivo DFM tambien ayuda a crear una cache para estos metadatos en una arquitectura clientel servidor) . Para tratar la creacion del conjunto de datos opcional, la desactivacion del registro y la representacion de la version XML de 10s datos iniciales en un control Memo, la clase del formulario del programa tiene el siguiente controlador para el evento O n c r e a t e :
p r o c e d u r e TForml.FormCreate(Sender: TObject); begin i f n o t FileExists (cds FileName) t h e n cds-CreateDatSet;: cds .Open; cds-MergeChangeLog; cds .Logchanges : = False; Memol.Lines.Text : = StringReplace ( C d s - X M L D a t a , ' > I , ' > ' + sLineBreak, [rfReplaceAll]); end ;
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>
Figura 13.2. La representacion XML de un archivo CDS en el ejernplo MyBase2. La estructura de tabla se define en el prograrna, que crea un archivo para el conjunto de datos durante su prirnera ejecucion.
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 end;
TForml.DbGridlTitleClic(Colurnn:
TColumn);
cds.IndexFieldNames : = Column.Field.FieldName;
A1 contrario que otras bases de datos locales, c 1i e n t D a t a s e t puede tener este tipo de indexado dinamico sin ninguna configuracion de la base de datos ya que 10s indices sc calculan en memoria.
. r
disponible para kste o n o h lados normales, que se calculan cada vez que se usa el registro, 10s val'ores A n 1ne rramnna ~rrln-m.l-Arre ;n+nmnmnn+aJ W fiat-am tan una sola vez y se mancuu tienen en memoria. Por este motivo, 10s indices 10s consideran como simples campos.
~ V w a x u p r a w a a u u a a u w ~ J IUC~ILLZULIUUCV
v a a v u J
Ademas de asignar un nuevo valor a la propiedad IndexFieldNames,se puedc definir un indice mediante la propiedad IndexDefs . De esta manera se pueden definir varios indices y mantenerlos todos en memoria, conmutando aun mas rapido de uno a otro.
TRUCO: Definir un indice separado es el unico modo de tener un indice descendente, en lugar de un indice ascendente.
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:
u La comparacion estandar y 10s operadores Iogicos ( ~ o p 1ation > 1000 and Area < 1000).
Operadores aritmeticos ( Population / Area < 10 ) . Funciones de cadena (Substring(Last Name, 1, 2 ) Otras, como la funcion Like, comodines y un operador In.
=
Ca ' )
Estas prestaciones de filtrado se encuentran perfectamente documentadas en el archivo de ayuda de la VCL: Deberia buscarse la pagina "Limiting what records appear" vinculada a la descripcion de la propiedad F i l t e r de la clase T C l i e n t DataSet, o llegar a ella desde la pagina Help Contents, siguiendo esta cadena: Developing Database ApplicationsAJsing client datasets> Limiting what records appear.
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:
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
ciudad o zona dadas, con lo que se conseguira un conjunto de resultados de tamailo reducido.
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 :
Una ampliacion del soporte para deshacer modificaciones es la posibilidad de guardar una especie de marcador de la posicion de registro del cambio (el estado actual) y restaurarla mas tarde deshaciendo todos 10s cambios sucesivos. La propiedad s a v e p o i n t puede utilizarse para guardar el numero de cambios en el registro o para redefinir el registro segun una situation anterior. De todos modos; solo podemos eliminar registros del registro de carnbio, no insertar cambios de nuevo. En otras palabras, la propiedad s a v e p o i n t se refiere a una posicion en un registro, de manera que solo puede volverse a una posicion en la que habia menos registros. Esta posicion de registro es un numero de cambios, asi que si se guarda la posicion actual, se deshacen algunos cambios y despues se edita mas, no se podra volver a la posicion marcada.
-
- - -. -
- -
TRUCO: Delphi 7 ti'ieflexinanueva acci6n e&hdar proyectada sobre la operacibn de 4fdacer &I ~ ~ i e ntta~ t. Enttc akzw acciones nuevas ~a e
--
puede eliminar ese codigo y volver a activarlo para ver la diferencia de tamafio entre el archivo CDS y el texto XML despuds de la edicion de 10s datos.
a un componente Datasource especifico y las'acciones se aplicaran a1 ~. . conjunto ae aatos conecraao a1 control visual que riene en ese momenro el
3 3 3 1
'
-1
foco de entrada. Asi, se puede usar una sola barra de herrarnientas para
1
(
vafiogconjwtone datoa mostrados por un fomu1ario.1 pue pucde resul6 tu muy conh~lso i para 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
un numero limitado de alternativas. Una caracteristica interesante es que 10s valores que muestre este componente pueden ser exactamente 10s mismos que queramos insertar en la base de datos, pero tambien podemos activar una especie de proyeccion. Los valores de la interfaz de usuario (algunas cadenas descriptivas almacenadas en la propiedad I t e m s ) se proyectaran a 10s valores correspondientes almacenados en la base de datos (algunos codigos basados en caracteres o numeros listados en la propiedad V a l u e s ) . Por ejemplo, podemos proyectar algunos codigos numericos, que indiquen departamentos sobre una serie de cadenas descriptivas:
object DBRadioGroupl: TDBRadioGroup Caption = ' D e p a r t m e n t ' DataField = ' D e p a r t m e n t ' Datasource = DataSourcel Items-Strings = ( 'Sales' ' Accounting' ' Production' ' M a n a g e m e n t ') Values .Strings = ( '1' '2' '3' '4' ) end
D B C h e c k B o x es un componente ligeramente distinto. Solo se utiliza para mostrar y alternar una opcion, correspondiente a un campo booleano. Se trata de una lista limitada, porque solo tiene dos valores posibles, mas un estado indeterminado para 10s campos con valores nulos. Podemos establecer cuales son 10s valores que hay que enviar de vuelta a la base de datos configurando las propiedades V a l u e c h e c k e d y V a l u e U n c h e c k e d de este componente.
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.
ft String
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
qye contiene 10s datos de pedidos porno el oampacompgny, la lista despleg&le mostrarh las empresas en &den a l f a b ~ i c o hgar de hacerla por en .. @ h e r o de cliente del pedido. 4si se hjt hecho en el ejemplq.
Figura 13.4. El aspect0 del ejemplo CustLookup, con el DBLookComboBox mostrando varios campos.
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 D t aSe t , en el listado 13.2 hemos incluido 10s a 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 = public class(TComponent, IProviderSupport)
/ / 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;
function ActiveBuffer: PChar; property IsUniDirectional: Boolean read FIsUniDirectional write FIsUniDirectional default False; function Updatestatus: TUpdateStatus; virtual; property Recordsize: Word read GetRecordSize; property Objectview: Boolean read FObjectView write SetOb jectView; property Recordcount: Integer read GetRecordCount; function IsSequenced: Boolean; virtual; function IsLinkedTo(DataSource: TDataSource): Boolean;
// 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
read FAutoCalcFields write FAutoCalcFields default True; property OnCalcFields: TDataSetNotifyEvent read FOnCalcFields write FOnCalcFields;
// 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:
dsBrowse: Indica que el conjunto de datos esta en un mod0 de navegacion normal y se usa para ver 10s datos e inspeccionar 10s registros. dsEdit: Indica que el conjunto de datos esta en mod0 de edicion. Un conjunto de datos entra en este estado cuando el programa llama a1 metodo Edit o cuando el Datasource tiene la propiedad A u t oEdi t configurada como T r u e y el usuario comienza a editar un control data-aware, como un DBGrid o DBEdit. Cuando se envia el registro que ha cambiado, el conjunto de datos abandona el estado dsEdit. dsInsert: Indica que se esta aiiadiendo un nuevo registro a1 conjunto de datos. Esto podria ocurrir cuando se llama a 10s metodos Insert o Append, moviendo la ultima linea de un componente DBGrid o usando la orden correspondiente del componente DBNavigator. dsInactive: Es el estado de un conjunto de datos cerrado. dsCalcFields: Es el estado de un conjunto de datos mientras se esta realizando el calculo de un campo, es decir, durante una llamada a un controlador de eventos OnCalcFields. dsNewValue, dsOldValue y dsCurValue: Son 10s estados de un conjunto de datos cuando se esta actualizando la cache. dsFilter: Es el estado de un conjunto de datos mientras se esta definiendo un filtro, es decir durante una llamada a un controlador de eventos
OnFilterRecord.
En 10s ejemplos sencillos, las transiciones entre estos estados se controlan de forma automatica, per0 es importante comprenderlos porque hay muchos eventos que se refieren a las transiciones de estado. Por ejemplo, todo conjunto de datos
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:
zando su propiedad Value o propiedades de tipos especificos, como A s Da t e , Asstring, AsInteger, etc.:
var
strName: string; begin strName : = cds. Fields [0].Asstring strName : = cds.FieldByname('LastName') .Asstring
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
: = c d s .Fieldvalues [ ' L a s t N a m e '] ; : = cds [ ' L a s t N a m e l] ;
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.
NOTA:Aunque el editor Fields es similar a 10s editores de las colecciones utiliaadas por Delphi, 10s campos no forrnan parte de una coleccih Son canpci6&$ creados en tiempo de diseiio, enumerados en la seccih publicada de La blase de fopnulario y disponibles en el cuadro combinado desplegable en la parte superior & Dbj'eet. Inspector. !
A1 abrir el editor Fields de un conjunto de datos, aparece vacio. Hay que activar el menu local de este editor o el del pseudonodo Fields en la vista Object
TreeView para acceder a sus capacidades. La operacion mas sencilla consiste en seleccionar la orden A d d , que permite aiiadir cualquier otro campo del conjunto de datos a la lista de campos. La figura 13.5 muestra el cuadro de dialogo Add Fields, que lista todos 10s campos disponibles en una tabla. Estos son 10s campos de tabla de base de datos que aun no estan presentes en la lista de campos en el editor.
I
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 cam-- -1 - >,.._ y cuanao 3un campo en el --l*-J-0 r rlle-, y m a r n e ae 1- clase-xuar;aseL _. m a w r laa l -I-a
2-
biarse por cualquier text0 apropiado. Se usa, entre otras cosas, para buscar
...--
Todos 10s campos que aiiadimos o definimos se incluyen en el editor Fields y lo pueden usar 10s controles data-aware o aparecer en una cuadricula de base de datos. Si un campo del conjunto de datos fisico no esta en esta lista, no se podra acceder a el. Cuando utilizamos el editor Fields, Delphi aiiade la declaration de 10s campos disponibles a la clase del formulario como componentes nuevos (de un mod0 muy parecido a como aiiade el menu Designer 10s componentes T M e n u I t e m a1 formulario). Los componentes de la clase T F i e l d , o mas concretamente sus subclascs, son campos del formulario y podemos referirnos directamente a estos componentes en el codigo del programa para cambiar sus propiedades en tiempo dc ejecucion o para obtener o establecer su valor. En el editor Fields, tambien podemos arrastrar 10s campos para modificar su orden. Resulta especialmente importante ordenar 10s campos correctamente cuando definimos una cuadricula, puesto sus columnas se ordenan utilizando ese orden.
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 ~aracterictiea miiv nrictica niw n11~Ae ahnrrar miiehn
TObject);
'
###,###,###I
Cuando definimos propiedades de campo relacionadas con entradas o salidas de datos, 10s cambios se aplican a cada registro de la tabla. En cambio, cuando definimos propiedades relacionadas con el valor del campo siempre nos referimos unicamente a1 registro actual. Por ejemplo, podemos obtener la poblacion (population) del pais actual en un cuadro de mensaje escribiendo el siguiente codigo:
p r o c e d u r e TForm2.SpeedButton2Click(Sender: T O b j e c t ) ; begin ShowMessage ( s t r i n g ( c d s [ 'Name'] ) + ' : ' + s t r i n g (cds [ ' Population' 1 ) ) ; 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;
Esto afecta a la salida de la DBGrid y del control DBEdit aiiadido a la barra de herramientas, que muestra el nombre del pais (country), como muestra la figura 13.6.
Formal
ShowPop
Ccnln
t I . NdmO .
- I ..
Cuba
- . .
u
I.
4
II
NorthArnexa
Figura 13.6. El aspect0 del ejemplo F~eldAcc despues de haber pulsado 10s botones Center y Format
TADTField
TObj ectField
Un carnpo ADT (Abstract Data Type, Tipo de Datos Abstracto), correspondiente a un campo de objeto en una base d e datos relacional de objetos. Un camp0 agregado representa un agregado rnantenido. Se usa en el componente ClientDataSet. Una rnatriz d e objetos en una base de datos relacional. Un ndrnero enter0 positivo conectad0 con un campo autoincremental de una tabla Paradox (un campo especial al que se asigna
TAggregateField
TField
TArrayField
T O b j ect Field
TAutoIncField
TIntegerField
automaticamente un valor diferente para cada registro). Fijese en q u e 10s campos A u t o l n c de Paradox no siempre funcionan correctamente.
TNumericField
Numeros reales con un numero fijo de digitos despues de una coma decimal. Normalmente no utilizado de forma directa. Esta es la clase basics de las dos clases siguientes. Datos binarios y sin limite de tamafio ( B L O B significa objeto binario grande). El limite maximo teorico son 2GB. Un valor booleano. Datos arbitrarios con un tamafio amplio per0 fijo (hasta 64 KB de caracteres). Valores monetarios, con el mismo rango que el tipo de datos Real. Un objeto correspondiente a una tabla separada en una base de datos relational de objetos.
TField
TField
TField TBinaryField
TFloatField
Un valor de fecha. Un valor de fecha y hora. Numeros de coma decimal (8 bytes). (Nuevo tip0 de campo en Delphi 6). Verdadero decimal en codigo binario (binary-coded decimal, B C D ) , en oposicion al tipo TBCDField ya existente, que convertia valores BCD al tipo de moneda. Este tip0 de campo se usa solo automaticamente en conjuntos de datos dbExpress. Grafico de longitud arbitraria.
TNumericField
TStringField
Un carnpo que representa un ldentificador Global Unico de COM, parte del soporte ADO. Un carnpo que representa punteros a las interfaces I D i s p a t c h de COM, parte del soporte ADO.
TIntegerField
TNumericField
Nllrneros enteros en el rango de enteros largos (long integer) (32 bits). Norrnalmente no se usa directarnente. Es la clase basica de 10s carnpos que contienen punteros a interfaces (IUnknown) corno datos. Enteros rnuy arnplios (64 bit). Texto de longitud arbitraria. Normalrnente no se usa directarnente. Es la clase basica d e todas las clases de carnpo numericas. Normalmente no se usa directamente. La clase basica de 10s campos que ofrecen soporte a bases de datos relacionales de objetos. Un punter0 a un objeto en una base relational de objetos. Nhmeros enteros en el rango de enteros (integer) (16 bits). (Nuevo tip0 de campo en Delphi 6). Soporta la representacion de fecha y hora utilizada en 10s controladores dbExpress. Datos de texto de una longitud fija (hasta 81 92 bytes). Un valor de hora. Datos arbitrarios, hasta caracteres de 64 KB. Muy similar a la clase bdsica TBytesField.
TField
TObjectField
TField
TObjectField
TIntegerField
TField
TStringField
TField
TDateTimeField TBytesField
TVariantField
TField
Un carnpo que representa un tip0 de datos variante, parte del soporte ADO. Un campo que representa una cadena Unicode (16 bits por caracter). Enteros positivos en el rang0 de palabras o enteros sin signo (16 bits).
TWideStringField
TStringField
TWordField
TIntegerField
La disponibilidad de un tip0 de campo concreto y la correspondencia con la definition de datos dependen de la base de datos que se use, sobre todo, con respecto a 10s nuevos tipos de datos que ofrecen soporte para bases de datos relacionales de objetos.
a las defmiciones de tabla. Sin embargo, si hay algunos campos en tiempo de disefio, Delphi usa esos campos sin aiiadir ninguno adicional.
Por supuesto, tambien tenemos que proporcionar un mod0 de calculo para el nucvo campo. Para ello, usamos el evento O n C a l c F i e l d s del componente C 1ient Dat aSe t , que tiene el siguiente codigo (a1 menos en una primera version):
procedure TForm2.cdsCalcFields(DataSet: TDataSet) ; begin cdsPopulationDensity.Value : = cdsPopulation.Va1ue cdsArea.Value; end;
NOTA: Los campos calculados se calculan para cada registro y se vuelven a calcular cada vez que el registro se carga en un buffer interno, recurrien. n > ao a1 evenro u n c a l c r ' l e las una y orra vez. r o r eso, el conrrolaaor ae este evento deberia ser muy ripido en su ejecucion y no puede alterar el estado del conjunto de datos, accediendo a registros diferentes. El compoproporciona una version m k eficiente en tiempo nente C l i e n t D a t aSe t - (pero menos en memoria) de un campo calculado con 10s campos calculados intemamente: estos campos se calculan una sola vez (cuando se cargan) y el resultado se almacena en memoria para peticiones futuras.
3
- . - ..
Pero eso no es todo. Si introducimos un nuevo registro y no definimos el valor de la poblacion ni la superficie, o si accidentalmente definimos la superficie como cero, la division creara una excepcion y habra problemas para continuar usando el programa. Para evitarlo, podriamos haber controlado cualquier excepcion de la expresion de division y fijar sencillamente el valor resultante como cero:
try
Se puede hacer aun mejor: se puede comprobar que se encuentre definido el valor del area (si no es nulo) y que no sea cero. Es mejor evitar el uso de excepciones cuando se pueden anticipar las posibles condiciones de error:
i f n o t cdsArea. IsNull and (cdsArea.Value <> 0) then cdsPopulationDensity.Value : = cdsPopulation.Value cdsArea.Value else cdsPopulationDensity.Value : = 0;
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-
A_--*_
3-
..
, .
. -.
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
32 300 003
2 777 815
1163
Figura 13.8. El resultado del ejemplo Calc. Fijese en la columna calculada de densidad de poblacion (Population Density) y e n el boton de tres puntos que aparece cuando se edita.
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 1 m n s . u 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.
Figura 13.9. El resultado del ejemplo FieldLookup, con la lista desplegable de la cuadricula que muestra valores tornados de la tabla de otro conjunto de datos.
sobre un formulario sino en un contenedor especial para componentes no visuales llamado modulo de datos. Se puedc conseguir un modulo de datos mediante la opcion de menu File>New de Delphi. Despues de afiadirle componentes, se pueden vincular con controles de otros formularios mediante la orden F i l e N s e Unit.
-- --
I ,
. I
I I
C U I I G G ~ L VUG
( (
.^-^^-*-
-I- x x 7 : - 1 - - - .
w IIIUUWS
-1-
-.^-L^-^
f-.
-. .
^^--^-l^L--^-L^
---L-Ll-
^-L-^
jistintos sistemas operativos). Al contrario que un formulario, un Imodulo je datos tiene simplemente unas cuantas lxopiedades y eventos. Po r eso, .... . results utli pensar en 10s moaulos ae aaros como en conteneaores de cornponentes y metodos. Al igual que un fonnulario o un marco, un modulo de datos tiene un diseiiador. Delphi crea una unidad especifica para la definicion de la clase de modulo de datos y un archivo de definicion de formulario que lista sus componentes y propiedades.
I.
. . I
x x i s t e n varios motivos Dara usarm6dulos dejatos. El mis simde gs aue compartir componentes de acceso a datos entre varios formulaI~ermiten 1 -ios. Esta tecnica funciona junto con el enlace de formularios visuates (la
G a y a u u i r o UG acc;r;ur;I
1 1 - 1 1- -----I--
^^--^-^-*^^ a GUIII~UIIGIILG~
^
UG ULIU
-1-
^*-^ r
LUIIIIUI~IIU
----
I--:-
u IIIUUUIU
-LA-I-
us;
-I- -I^*^-
uarus
en tiempo de diseiio mediante la orden FiIeNse Unit). El segundo motivo es que 10s modulos de datos separan 10s datos de la interfaz de usuario, mejorando la estructura de una aplicacion. Los m6dulos de datos en Delphi tienen incluso versiones especificas para aplicaciones multicapa (modulos de datos remotos) y aplicaciones HTTP de servidor (modulos de datos Web).
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 :
La figura 13.10 muestra un ejemplo de la salida del programa, con valores no definidos (o nulos) para algunas fechas de envio
OrdnNo
ri -E i
C&No
pi%r
Sald)ak
112/1995 ShpDae
Itundslned,
01brNo U1298CN2315 U1300 CN 1384 Ill302 CN 1231 #13&5 CN 1356 W1309 CN 3615 W1315CN1651 W1317CN1984 #1350 CN 3052
ICU~NO ~ a t e l~ak
9/1/1995 1011/1995 16/1/1995 M/1/1995 22/1/1995 26/1/1995 1/2/1995
I~hpDate 91111995 1W111995 16/1/1995 2Wlt1995 2211/I995 26/1/1995 112/1995 5/2/1995 <un&lmeb
IE~~NO EmpU 0011 EmpW OOB ErnpU 0052 EmpU DO65 ErwW 0094
EmpU 012'1 Emp# 0138 Empll0071 EmpW 0141
lm995
5/2/1 995 4/2/1996
Emflo
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
-
afectada por cambios en el mod0 de funcionamiento de las variantes nulas. Comparar un campo con un valor nulo con otro carnpo tiene un efecto distinto en las ultimas versiones de Delphi que en el pasado. En Delphi 7 se pueden usar variables globales para ajustar el efecto de las comparaciones en las que intervengan variantes.
v i m i e n t 0). Podemos movernos a1 registro siguiente o a1 anterior, volver atras o adelante por una serie de registros datos (con MoveBy) o ir directamente a1 primer0 o a1 ultimo registro del conjunto de datos. Estas operaciones del conjunto de datos estan, por lo general, disponibles gracias a1 componente DBNavigator y a las acciones estandar de 10s conjuntos de datos y no resultan especialmente complejas de comprender. La forma en que un conjunto de datos controla las posiciones extremas no resulta tan obvia. Si abrimos cualquier conjunto de datos con un navegador asociado, podemos ver que a medida que nos movemos registro por registro, el boton Next permanece activado incluso cuando hemos alcanzado el ultimo registro. ~nicamente cuando intentamos movernos hacia delante, despues del ultimo registro, se desactiva el boton (y no cambia el registro actual). Esto se debe a que la comprobacion E o f (de fin de archivo) solo resulta satisfactoria cuando el cursor se ha movido a una posicion especial despues del ultimo registro. Si saltamos a1 final con el boton Last, en cambio, pasaremos inmediatamente a1 final del todo. En el caso del primer registro (y de la comprobacion Bof), veremos exactamente el mismo comportamiento. Ademas de movernos registro por registro o por un numero dado de registros, 10s programas podrian necesitar saltar a registros o posiciones concretas. Algunos conjuntos de datos soportan la propiedad R e c o r d c o u n t y permiten el movimiento a un registro situado en una pocion especifica del conjunto de datos utilizando la propiedad RecNo. Estas propiedades solo se pueden usar en el caso de conjuntos de datos que soporten posiciones de forma nativa, lo cual excluye basicamente todas las arquitecturas clientelservidor, a menos que llevemos todos 10s registros a la cache local (algo que normalmente querremos evitar) y, a continuacion, naveguemos por la cache. Cuando se abre una consulta en un servidor SQL, solo se consiguen 10s registros que se usan, de manera que Delphi no conoce la cuenta de registros (a1 menos no de antemano). Existen dos alternativas que podemos usar para referirnos a un registro de un conjunto de datos, sea cual sea su tipo: Podemos guardar una referencia a1 registro actual y, a continuacion, volver de nuevo a el despues de habernos movido por otros registros. Para ello hay que utilizar marcadores ya sea TBoo kmar k o TBoo kmar kS t r , que es mas moderno, como explicaremos mas adelante. Podemos encontrar un registro del conjunto de datos que se ajuste a una serie de criterios, utilizando el metodo L o c a t e . Esto funciona incluso tras haber cerrado y abierto de nuevo el conjunto de datos? porque trabajamos a un nivel logico (no fisico).
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
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);
au tomaticamente. Esta propiedad se implementa tecnicamente como una cadena opaca, una estructura que depende de la gestion de la vida efectiva de una cadena, pero no es una cadena, por lo que se supone que no miramos lo que hay en ella. Podemos modificar el codigo anterior del siguiente modo:
var
...
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 ordenador del cliente. El serv~dor r solo 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 m e ofrecen --- aerenados. -- m e hemos comentado aaui es una - - - - - -- - -- 1--------- 10s -=--=----. Lo ------ -solution generics, que deberia funcionar para cualquier conjunto de datos.
--------I--
La primera sentencia pone la tabla en cl mod0 de edicion, de mod0 que 10s cambios de 10s campos tengan un efecto inmediato. La segunda sentencia calcula el nuevo sueldo, multiplicando el antiguo por el valor del componente S p i n E d i t (de manera predeterminada, 105) y dividiendolo entre cien. Eso significa un aumento del cinco por ciento, aunque 10s valores se redondean de acuerdo con el valor mas prosimo en dolares. En este programa, podemos modificar 10s sueldos en una cantidad cualquiera, incluso duplicar el salario de cada empleado haciendo clic sobrc un boton.
-
ADVERTENCIA: Observe que la tabla entra en el mod0 de ehcion cada vez que se ejecuta el bucle w h i l e . Esto se debe a que en un conjunto de datos las operaciones de edicion solo pueden aplicarse a un registro cada vez. Tendremos que terminar la operacion de edicion, Ilamando a p o s t o pasar a un regist& diferente cornokn el c6digo anterior. En ese momento, si queremos carnbiar a otro registro, tenemos que volver a entrar en el modo de edicion.
plo mostrara como pintar en una cuadricula y el segundo como usar la caracteristica de selection multiple de la cuadricula.
Se llama al controlador de eventos OnDrawColumnCe 11 una vez para cada celda de la cuadricula. Este controlador tiene diversos parametros, como el rectangulo correspondiente a la celda, el indice de la columna que tenemos que dibujar, la propia columna (con el campo, su alineacion y otras subpropiedades) y el estado de la celda. Para establecer el color de celdas especificas como rojo, podemos cambiarlo en 10s casos especiales:
procedure TForml.DBGridlDrawColumnCe11(Sender: TObject; c o n s t Rect : TRect; DataCol : Integer; Column: TColumn; State: TGridDrawState) ; begin // color d e fuente rojo, s i la longitud > 1 0 0 if (Column.Field = cdslengthcm) and (cdsLengthcm-AsInteger> 100) then DBGridl.Canvas.Font.Color : = clRed;
// d i s e d o p r e d e f i n i d o DBGridl.DefaultDrawDataCel1
end;
El siguiente paso consiste en dibujar 10s campos de memo y graficos. Para el memo, podemos simplemente implementar 10s eventos OnGetText y OnSetText del campo memo. La cuadricula permite incluso editar en un campo de memo si el evento OnSetText es distinto de nil.Este es el codigo de 10s dos controladores de eventos. Hemos usado Trim para eliminar 10s caracteres finales que no se imprimen, que hacen que el texto parezca estar en blanco a1 editar:
procedure TForml.cdsNotesGetText(Sender: TField; var Text: String; DisplayText: Boolean) ; begin Text : = Trim (Sender.Asstring); end; procedure TForml.cdsNotesSetText(Sender: String) ; begin Sender-AsString : = Text; end; TField; const Text:
Para la imagen, la tecnica mas sencilla consiste en crear un objeto TPicture temporal, asignarle el campo grafico y pintar el mapa de bits sobre el lienzo de la cuadricula. Como alternativa, hemos eliminado el campo grafico de la cuadricula, definiendo su propiedad Visible como Fa1se y aiiadido la imagen a1 nombre del pez, con el siguiente codigo adicional en el controlador de evento OnDrawColumnCell:
var Picture: TPicture; OutRect : TRect; PictWidth: Integer; begin // r e c t d n g u l o d e s a l i d a p r e d e f i n i d o OutRect : = Rect: if Column-Field = cdsComrnon-Name then begin // d i b u j a l a imagen Picture : = TPicture.Create; try Picture.Assign (cdsGraphic); PictWidth : = (Rect-Bottom - Rect.Top) * 2; 0utRect.Right : = Rect.Left + PictWidth; DBGrid1.Canvas.StretchDraw (OutRect, Picture.Graphic); finally Picture. Free; end;
/ / redefinir el rectdngulo d e salida, dejando espacio para / / el grdfico OutRect : = Rect; 0utRect.Left : = 0utRect.Left + PictWidth; end;
/ / color d e fuente rojo, si la longitud > 100 (omitido, / / vease anterior) / / dibujo predefinido DBGridl.DefaultDrawDataCel1
(OutRect, Column.Field, S t a t e ) ;
Como podemos ver en el codigo anterior, el programa muestra la imagen en un pequeiio rectangulo a la izquierda de la celda de la cuadricula y, a continuacion, cambia el rectangulo de salida a la zona restante antes de activar el dibujo predefinido. Podemos ver el efecto en la figura 13.12
1s p e w s No
c&cP
ICummnOmmMNama
Clown Triggerfish ~ e ~d p e r o r m Z ~Glant Maor1 Wrasse J Blue Angell~sh SW Lunarta~l Rockcod F~ref~sh Ornate Bunefflylish -Swell Shark 6.1 Ray " -7California Moray 4 - Lingmd -
15ceclcr ~
90020 90030 90050 90070 90080 90090 90100 901 10 90120 90130 90140
6-3
Ray
Eel Cod
+=
Ball~sloides conspicillum Lutjanus sebae Che~l~nus undulatus Pomacanlhus nauarchus Var~ola lout1 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 propiedad C o u n t , podemos acceder a cada marcador con la propiedad It e m s , que es la propiedad de matriz predefinida. Cada elemento de la lista es de un tip0 TBoo k m a r k S t r , que representa un punter0 de marcador que podemos asignar a la propiedad Bookmark de la tabla.
La Paz
Guyana Jarna~ca
Figura 13.13. El ejemplo MltGrid tiene un control DBGrid que permite la seleccibn de varias filas.
NOTA: TBoo k m a r kStr es un tipa cadena de conveniencia, pero sus datos deberian considerarse "opacos" y volatiles. No deberiamos confiar en ninguna estructura concreta de datos que podamos encontrar, si nos fijamos un valor de marcador, ni deberiamos guardar 10s datos durante demasiado tiempo o almacenarlos en un archivo independiente. Los datos de marcador variaran segun el controlador de la base de datos y la configuracion del indice y pueden resultar inutiles cuando nosotros o 10s usuarios de la base de datos afiadamos o eliminemos filas del conjunto de datos.
Veamos el codigo del ejemplo MltGrid; que se activa a1 hacer clic sobre el boton para mover el campo N a m e de 10s registros seleccionados a1 cuadro de lista:
procedure TForml.ButtonlClick(Sender: TObject); var I: Integer; BookmarkList: TBookmarkList; B o o k m a r k : TBookmarkStr; begin // almacena l a p o s i c i o n a c t u a l B o o k m a r k : = cds.Bookmark; try // v a c i a e l c u a d r o d e l i s t a ListBoxl.Items.Clear; // o b t i e n e l a s f i l a s s e l e c c i o n a d a s d e l a c u a d r i c u l a
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)
procedure TFormDrag.DBGridlDragDrop(Sender, Source: TObject; X, Y: Integer); var gc : TGridCoord; begin gc := TDBGHack (DbGridl).MouseCoord (X, Y) ; if (gc.y > 0) and (gc.x > 0) then begin DBGrid1.DataSource.DataSet.MoveBy (gc-y TDBGHack (DBGridl).Row) ;
DBGrid1.DataSource.DataSet.Edit;
:=
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;
Figura 13.14. La salida del ejernplo NonAware en el m o d de navegacion. El ~ programa consigue rnanualrnente 10s datos cada vez que cambia el registro actual.
El controlador del evento Onstatechange del control muestra el estado de la tabla en un control de barra de estado. Cuando el usuario escribe en uno de 10s cuadros de edicion o despliega la lista del cuadro combinado, el programa pone la tabla en mod0 de edicion:
p r o c e d u r e TForml .EditKeyPress (Sender: TObject; v a r Key: Char) ; begin i f n o t (cds .State i n [dsEdit, dsInsert] ) t h e n
c d s .Edit; end;
Este metodo esta conectado a1 evento OnKe y p r e s s de 10s cinco componentes y es similar a1 controlador de 10s eventos OnDropDown del cuadro combinado. Cuando el usuario abandona uno de 10s controles visuales, el controlador del evento O n E x i t copia 10s datos a1 campo correspondiente, como en este caso:
procedure TForml.EditCapitalExit(Sender: TObject); begin i f (cds .State i n [dsEdit, dsInsert] ) then cdsCapital.AsString : = E d i t c a p i t a l - T e x t ; end:
La operacion solo se realiza si la tabla esta en mod0 de edicion, es decir, solo si el usuario ha escrito en este u otro control. No se trata del comportamiento ideal, porque se realizan operaciones adicionales incluso si no se ha modificado el texto del cuadro de edicion; sin embargo, 10s pasos adicionales son lo suficientemente rapidos como para que suponga un problema. En el caso del primer cuadro de edicion, verificamos el texto antes de copiarlo, creando una excepcion si el cuadro de edicion esta en blanco:
procedure TForml.EditNameExit(Sender: T O b j e c t ) ; begin i f (cds.State i n [dsEdit, dsInsert] ) then i f EditName.Text <> " then cdsName.AsString : = EditName.Text else begin EditName-SetFocus; r a i s e Exception. C r e a t e ( ' U n d e f i n e d C o u n t r y ' ) ; end; 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);
procedure TForml.ComboNameKeyPress(Sender: TObject; Char) ; begin if Key = $13 then GetData; end;
var Key:
Para que el ejemplo funcione rapidamente, a1 arrancar el cuadro combinado se rellena con 10s nombres de todos 10s paises de la tabla:
p r o c e d u r e TForml.FormCreate(Sender: TObject); begin // r e l l e n a l a l i s t a de nombres c d s .Open; w h i l e n o t cds.Eof d o begin ComboName.Items.Add (cdsName.AsString); cds .Next; end ; end ;
Con esta tecnica, el cuadro combinado se transforma en una especie de selector para el registro, como muestra la figura 13.15. Gracias a esta seleccion, el programa no necesita botones de navegacion.
Jamaica
&=
1756943
Figura 13.15. En el ejernplo SendToDb se puede usar un cuadro combinado para seleccionar el registro que se desea ver.
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' )
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'
Cuando se activa un grupo, se puede hacer obvio para el usuario si se muestra la estructura de agrupacion en el control DBGrid, como muestra la figura 13.16. Todo lo que hay que hacer es controlar el evento O n G e t T e x t para el campo agrupado (en el ejemplo, el campo C o n t i n e n t ) y mostrar el texto solo si el registro es el primer0 del grupo:
procedure TForml.ClientDataSetlContinentGetText(Sender: TField; var Text: String; DisplayText: Boolean) ; begin i f gbFirst i n ClientDataSetl .GetGroupState (1) then Text : = Sender.AsString. else Text : = "; end;
S d h America
Paraguay
U~WW
Venezuela Peru hgmfina Guyana Ecuador
Cob&
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, actualizar para 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 file esd en 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
muchos registros detallados en una tabla secundaria. Un ejemplo clasico de esto es una factura y 10s elementos de la factura; otro es una lista de clientes y 10s pedidos de cada uno de ellos. Se trata de situaciones habituales en la programacion de bases de datos, y Delphi ofrece un soporte explicit0 mediante la estructura maestro/detalles. La clase TDat a S e t tiene un propiedad Data S our c e para configurar una fuente de datos maestra. Esta propiedad se usa en un conjunto detallado para conectarse a1 registro actual del conjunto de datos maestro en combinacion con la propiedad MasterFields.
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 1356 2511 211394 1356 2412/1983 1356 5/5/1339 1356201111995 2611 2 1 994 1 25/2/1583 6/5/1989 2011/1995
1 W
1305
45 65
;L i I
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:
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.
Una vez dicho esto, podemos empezar a centrarnos en tecnicas particulares para la programacion clientelservidor. El objetivo general es distribuir correctamente la carga de trabajo entre el cliente y el servidor, asi como reducir el ancho de banda de red necesario para transportar la informacion. La base de este enfoque es un buen diseiio de la base de datos, que implica tanto la estructura de tablas como la validacion y restricciones apropiadas para 10s datos (las reglas de negocio). Obligar a la validacion de 10s datos en el servidor resulta importante, porque la integridad de la base de datos es uno de 10s objetivos claves de cualquier programa. Sin embargo, el lado del cliente deberia incluir tambien validacion, para mejorar la interfaz de usuario y hacer que la entrada y procesamiento de 10s datos sea mas amigable. Tiene poco sentido permitir que el usuario introduzca datos no validos y reciba mas tarde un mensaje de error por parte del servidor, cuando se puede impedir desde el comienzo una entrada erronea.
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 o ver con el diseiio 16nico de una base de datos aue 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.
Las primeras encarnaciones de la teoria relacional dictaban el uso de claves logicas, lo cual significa scleccionar uno o mas registros que indiquen una entidad sin riesgo dc confusion. Normalmente, esto resulta mas facil de decir que de realizar. Por ejemplo, 10s nombres de empresa no suelen ser unicos y ni siquiera el nombre de la empresa y su ubicacion nos ofrecen una completa garantia. Ademas, si una empresa cambia de nombre (algo que no es improbable, como Borland puede enseiiarnos) o de ubicacion, y tenemos referencias a la empresa en otras tablas, debemos cambiar tambien todas las referencias, con el riesgo de acabar dejando rcferencias dcscolgadas. Por este motivo, y tambien por razones de eficiencia (usar cadenas para referencias implica utilizar mucho espacio en tablas secundarias, en las que normalmcnte hay las referencias), las clavcs logicas se han ido reemplazando siempre por claves fisicas o de sustitucion: Claves fisicas: Se remiten a un solo campo dc la tabla que identifica un elemento de un mod0 unico. Por ejemplo, cada persona en 10s EEUU tiene un numcro de la Seguridad Social (SSN). pero casi todos 10s paises tienen un idcntificador fiscal u otro numero asignado por el gobierno para identificar a cada persona. En el caso de las empresas, normalmente sucede lo mismo. Aunque estos numero de identificacion son unicos, podrian cambiar en funcion del pais (creando problemas para las bases de datos de una emprcsa que venda tambien sus productos en el estranjero) o incluso en un mismo pais (ante nuevas leyes fiscales). Normalmente, tampoco son eficaces, puesto que podrian ser bastante largos (Italia, por ejemplo, usa un codigo de 16 caracteres. letras y numeros para identificar a las personas). Claves sustitutas: Un numero que identifica a cada registro, en forma de codigos de cliente, numeros de pedido, etc. Estas claves sustitutas se usan normalmente en el diseiio de bases de datos. Sin embargo, en muchos casos, acabaran siendo identificadores logicos, con codigos de cliente en todas partes (lo que no es una gran idea).
ADVERTENCIA: La situation se vuelve especialmente compleja cuando estas claves sustitutas tienen tambikn un significado y ban de seguir nonnas concretas. Por ejemplo, las empresas han de numerar las facturas con nu-
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-*-- la rmsesios numerus C;unseCuiivos_'--:--unlws cuanuo envlamos uaios nuevos a 1 - -:-_'_-----
ma. A1 mismo tiempo, necesitamos identificar el registro antes de enviarlo a la base de datos, si no, no seremos capaces de ir de nuevo a buscarlo. Algunos ejemplos practicos del capitulo 15 mostrarhn como solucionar esta situacion.
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 empresas clientes v una tabla de em~leados.vodriamos ureguntarnos uor auk , . . 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 :L- --L--a - - - I - - - I -- la - L I - 1- -I:--&-L--:-que r e p e ~ ~- rmrorrnauon swore GI ernpleauo en 1 - .raola ue cuenws, nawenla :-r
----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.
NOTA: Para conseguir mas infonnacion sobre el lenguaje de definicion de datos (DDL)de SQL y el lenguaje de manipulacibn de datos (DML), consultense las referencias del anexo D en el CD-ROM.
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 .
NOTA: El caso de un DBGrid utilizado para explorar una tabla completa es comun en programas locales, per0 normalmente deberia evitarse en un entorno cliente/servidor. Es mejor filtrar solo parte de 10s registros y so10 aquellos campc1s que nos interesen. Si se necesitara ver una lista de nombres, es aconsejable conseguir primer0 aquellos que empiecen con la letra A, luego 10s que comiencen con la B, etc. 0 pedir a1 usuario la inicial del nombre.
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
( 1 resulta muy comodo para calcular el numero de registros devueltos por la consulta. En lugar de la mascara *, podriamos haber usado el nombre de un campo especifico, como en c o u n t ( F i r s t Name ) , posiblemente combinado con d i s t i n c t o a l l . para contar s%lo registros con valores diferentes para el campo o todos 10s registros que tengan un valor no nulo.
+
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.
Un lenguaje compatible con el estandar SQL de ANSI. Prestaciones de programacion avanzada: Como disparadores de posicion, procedimientos almacenados seleccionables, vistas que se pueden actualizar, excepciones, eventos; generadores, etc. Una instalacion y adrninistracion muy sencilla: Con pocos dolores de cabeza administrativos.
puiiado de empresas, InterBase ha sido escogido por varias empresas importantes, desde Ericsson a1 Departamento de Defensa de 10s Estados Unidnc decde -"-- ----- de cnmhin a sistcmac de hanca rlnmkctica Entre --.--.-, -- mercndns -- ------ ----- ---- ------.-----. - lnc 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~endiente Dara " 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\ ------ ---maron r n n ~n o- :n - x m r - - e - ~ 1www.mpnoenlx.corn) con la :>-- ue orrecer soporre a laea >- -c
--
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 ----x1----- I-1lr;encla rraumonal [que cuesm una pequerra parre ue 1- que cues~an w IUS
L-
--A-
-. -A
A--
servidores SQL profesionales m h competitivos), conviene seguir con Borland; per0 si se prefiere un modelo de cbdigo abierto, totalmente gratuito, lo mejor es consultar el proyecto Firebird (y contratar en ultima instancia el soporte profesional de IBPhoenix).
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
-
89 '*%I 3
3 InterBaseServels
-
- -- . -
--
-I
_Descrptm D~sconnrct from the curent database Show database properhes D~splay database std~st~cs ShutdownIhe database Perform a database sweep V~ew Database Metadata Restart a database Dlop the current database Backup an InlerBase database VIM a llsl d users curlently connectedto the server Restorean InterBwe database
" LmalServer J
38
-1
Databases
'$
@ Domalns
Database Stal~st~cs Shutdown Sweep V~ew Metadata Database Restart Drop Database DatabaseBackup ConnectedUsers RestoreDatabase
a
3
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
Figura 14.1. IBConsole permite administrar desde un unico ordenador bases de datos de InterBase, hospedadas en varios servidores.
La aplicacion IBConsole permite abrir varias ventanas para ver information detallada, como la ventana de tablas que muestra la figura 14.2. En esta ventana, se pueden ver listas de las propiedades claves de cada tabla (columnas, disparadores, restricciones e indices), ver 10s metadatos en bruto (la definition SQL de la tabla), 10s permisos de acceso, very modificar 10s datos, y analizar las dependencias de la tabla. Hay disponibles ventanas parecidas para cada una de las demas entidades que se pueden definir en una base de datos.
I I
TIMESTAMP [DEPTNO) CHAR01 IJOBCODEI VARCHARISI ~JOBGRADEJ SMALLINT. (COUNTRYNAME] VARCHARIlS] [SALARY] NUMERIC[lS. 21 VARCHAR
DEFAULT 'NOW
DEFAULT 0
Yes No No No No No No Yes
--
I\ C
\exam&s\~atabese\e&
adb
--
---
Tables
Figura 14.2. IBConsole puede abrir ventanas independientes para rnostrar 10s detalles de cada entidad (en este caso, una tabla).
IBConsole incluye una version mejorada de la aplicacion Windows Interactive SQL original (vease figura 14.3). Se puede escribir una sentencia SQL en la parte superior de la ventana (lamentablemente sin ninguna ayuda por parte de la herramienta) y ejecutar a continuacion la consulta SQL. Como resultado, se veran 10s datos, per0 tambien la planificacion de acceso utilizada por la base de datos (que un experto podria usar para determinar la eficiencia de la consulta) y estadisticas sobre la operacion realizada por el servidor. Esto ha sido una breve descripcion de IBConsole, que es una potente herramienta (y la unica que incluye Borland junto con el servidor, ademas de herramientas en linea de comandos). Aun asi, IBConsole no es la herramienta mas completa de su propia categoria. Otras aplicaciones de administracion de InterBase de terceros son mas potentes, aunque no tan estables o amigables. Algunas herramientas de InterBase son programas shareware, y otros son gratuitos. Dos ejemplos, entre otros muchos, son InterBase Workbench (www.upscene.com) e IB-WISQL (creada con y parte de InterBase Objects, www.ibobjects.com).
l a 7 - @ - 8 1 B I h a I k % l f i= z .. . :.
1
.-
. . ..
.- .
--
. . ..
.!
A
k e l e c t last-name, f r m employee
hire-date,
salary
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);
La funcion g e n i d extrae el nuevo valor unico del generado pasado como primer parimetro; el segundo parimetro indica de cuinto seri el incremento (en este caso, de uno). En este punto, se puede aiiadir un disparador a una tabla (un controlador automatico para uno de 10s eventos de la tabla). Un disparador es como un controlador de eventos del componente T a b l e , per0 se escribe en SQL y se ejecuta en el servidor, no en el cliente. ~ s t es un ejemplo: e
c r e a t e t r i g g e r set-cust-no f o r customers before i n s e r t p o s i t i o n 0 a s begin new. cust-no = gen-id (cust-no-gen, 1); end
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.
NOTA: La inclusion de un controlador para SQL Server de Microsoft es la actualizacion mas importante de dbExpress en Delphi 7. No se implementa ofreciendo una interfaz con otras bibliotecas nativas del fabricante, como otros controladores dbExpress, sino enlazando con el proveedor OLE DB de Microsoft para SQL Server. (Hablaremos mas sobre 10s proveedores OLE DB en el siguiente capitulo.)
cuadricula de base de datos a un conjunto de datos como este. Sin embargo, un conjunto de datos unidireccional es bueno para 10s siguientes usos: Podemos usar un conjunto de datos unidireccional para generar informes. En un informe impreso, per0 tambien en una pagina HTML o en una transformacion XML, nos movemos de un registro a otro, generamos la salida y ya esta. No hay que volver a registros pasados y, por lo general, no es necesaria la interaccion del usuario con 10s datos. Los conjuntos de datos unidireccionales son probablemente la mejor opcion para las arquitecturas Web y multicapa. Podemos usar un conjunto de datos unidireccional para alimentar una cache local, como la que ofrece un componente C l i e n t Dat aSe t . En este punto, podemos conectar componentes visuales a 10s conjuntos de datos en memoria y operar en ellos con todas las tecnicas estandar, como el uso de cuadriculas visuales. Podemos navegar con libertad y editar 10s datos en la memoria cache, per0 tambien controlarlos mucho mejor que con el BDE o ADO. Lo importante es que, en dichas circunstancias, evitar guardar el almacenamiento en cache del motor de base de datos ahorra en realidad tiempo y memoria. La biblioteca no tiene que utilizar memoria adicional para la cache y no necesita perder tiempo almacenando datos, duplicando la informacion. Durante 10s ultimos aiios, muchos programadores han pasado las actualizaciones en cache basadas en el BDE a1 componente C 1 e nt Data Se t , que ofrece mas flexibilidad en la gestion del i contenido de 10s datos y la actualizacion de la informacion que mantienen en memoria. Sin embargo, usar un C l i e n t D a t a S e t sobre el BDE (o ADO), tiene el riesgo de tener dos caches separadas, que desperdicia mucha memoria. Otra ventaja del uso del componente C l i e n t D a t a S e t es que su cache soporta operaciones de edicion y las actualizaciones almacenadas se pueden aplicar a1 servidor de base de datos original mediante el componente Dataset Provider. Este componente puede generar las sentencias SQL adecuadas de actualizacion y puede hacerlo de un mod0 mas flexible que el BDE (aunque ADO es tambien bastante potente en este sentido). En general, el proveedor puede usar tambien un conjunto de datos para las actualizaciones, per0 no resulta posible directamente con 10s componentes de conjunto de datos dbExpress.
Cuando usamos dbEspress, se nos ofrece un marco de trabajo comun, que es independiente del servidor de bases de datos SQL que planeamos usar. dbExpress incluye controladores para MySQL, InterBase, Oracle, Informix, Microsoft SQL Scrver e DB2 de IBM.
NOTAt Es posible escribir controladores personalizad~s para 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 / O , 1 O,224H,OO.htmI6~ & a b l k m ~ dpub 14 e clan cncbntrarse controladores de terceros. Por ej&plo, c;uiste un ~ o n ~ o l p dor ~ ~ ique conecta dbExpress y ODBC. Hay una Bsta cornpletar en el t o articdd h ~ p : / / c m u n i t ~ . b o r l a n d .comlarticle/0,14f 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
nInfoSize : = GetFileVersionInfoSize (pChar(strDriverNarne), nDetSize) ; if nInfoSize > 0 then begin GetMern (pVInfo, nInfoSize) ; try GetFileVersionInfo (pChar(strDriverName), 0, nInfoSize, pVInf o) ; VerQueryValue (pVInfo, ' \ ' , pDetail, nDetSize) ; Result : = HiWord (TVSFixedFileInfo ( p D e t a i l A ) .dwFileVersionMS) ; finally FreeMem (pVInfo) ; end; end; end;
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') ;
Si se prueba a colocar el controlador que se encuentra en la carpeta bin de Delphi 6 en la carpeta de la aplicacion, se vera el error. Habra que modificar esta comprobacion adicional de seguridad para tener en cuenta versiones actualizadas de 10s controladores o bibliotecas, pero este paso deberia ayudar a enviar 10s problemas de instalacion que dbExpress trata de solucionar, antes de nada. Existe tambien otra alternativa: se puede enlazar estaticamente el codigo de 10s controladores de dbEspress en la aplicacion. Para hacer esto, se incluye una unidad determinada (como dbexpint .dcu o dbexpora .dcu) en el programa. indicandolo en una de las sentencias uses .. - - - . - - - ..- -. .- - . - - .. - . - .. - - - .
-
ausiliares. Para diferenciar estos componentes de otras familias de acceso a bases de datos, 10s componentes llevan las letras SQL como prefijo, subrayando el hecho de que se usan para acceder a servidores RDBMS. Dichos componentes incluyen un componente de conesion de bases de datos, algunos componentes de conjuntos de datos (uno generico; tres versiones especificas para tablas, consultas y procedimientos almacenados; y uno que encapsula a1 componente C l i e n t D a t a S e t ) y una utilidad de seguimiento.
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).
TRUCO: A diferencia de otras familias de componentes, en dbExpress la conexion es obligatoria. En cada uno de 10s componentes de conjuntos de
uarva, uw yvucauva G a p G u u L a I
A+" ,,
,A
..,.Aa-*"
, ,, c., ",: ,.
U I I G ~ L Q I I I C ~ L GYUG
AJ ,, .
,.,A
I...,,
u a a UG ~
A,
A , " UULVB
., , uam,
" : , a
SUIV
solo hacer referencia a una SQLConnection. El componente de conexion utiliza la informacion disponible en 10s archivos drivers.ini y connectionshi, que son 10s dos unicos archivos de configuration de dbExpress (dichos archivos se guardan de manera predeterminada en Archivos comunes\Borland Shared\DBEspress). El primero, drivers.ini, lista 10s controladores dbEspress, uno para cada base de datos soportada. Para cada controlador, existe un conjunto de parametros de conexion predefinidos. Por ejemplo, la seccion InterBase es como sigue:
[Interbase] GetDriverFunc=getSQLDriverINTERBASE LibraryName=dbexpint.dll VendorLib=GDS32.DLL Blobsize=-l CommitRetain=False Database=database.gdb Password=masterkey RoleName=RoleName ServerCharSet=ASCII SQLDialect=l Interbase T r a n s I s o l a t i o n = R e a d C o m r n i t e d User-Name=s ysdba WaitOnLocks=True
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.
El archivo connections.ini ofrece la descripcion especifica de la base de datos. Esta lista asocia configuraciones con un nombre, y se pueden escribir varios datos de conexion para cada controlador de base de datos. La conexion describe la base de datos fisica a la que queremos conectar. Como ejemplo, esta es la parte de la definicion predefinida de IBLo ca 1:
[ IBLocal] Blobsize=-1 CommitRetain=False Database=C:\Archivos de programa\Archivos comunes\Borland Shared\Data\employee.gdb DriverName=Interbase Password=masterkey RoleName=RoleName ServerCharSet=ASCII SQLDialect=l Interbase TransIsolation=ReadCommited User-Name=s ysdba WaitOnLocks=True
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
En tiempo de ejecucion, nuestro programa confiara en las propiedades para tener toda la informacion necesaria, por lo que no hay que desplegar 10s dos archivos de configuracion junto con 10s programas. En teoria, 10s archivos seran necesarios si queremos cambiar las propiedades Drive rName o Co nnectionName en tiempo de ejecucion. Sin embargo, en caso de que queramos conectar nuestro programa a una nueva base de datos, podemos establecer directamente las propiedades oportunas. Cuando aiiadimos un nuevo componente SQLConnection a una aplicacion, podemos proceder de distintas formas. Podemos configurar un controlador utilizando la lista de valores disponible para la propiedad DriverName y, a continuacion, seleccionar una conexion predefinida, seleccionando uno de 10s valores disponibles en la propiedad Connect ionName.Esta segunda lista se filtra segun el controlador que ya hayamos seleccionado. Como alternativa, podemos comenzar eligiendo directamente la propiedad Connect ionName,que en este caso incluye la lista completa. En lugar de conectar una conexion existente, podemos definir una nueva (o ver 10s datos de las conexiones existentes) haciendo doble clic sobre el componente SQLConnection y lanzando el dbExpress Connection Editor (vease figura 14.4). Este editor lista, a la izquierda, todas las conexiones predefinidas (para un controlador especifico o todas ellas) y permite editar las propiedades de conexion mediante la cuadricula que se encuentra a la derecha. Podemos emplear 10s botones de la barra de herramientas para aiiadir, borrar, dar un nuevo nombre y probar conexiones y abrir la ventana dbExpress Drivers Settings de solo lectura, que muestra tambien la figura 14.4. Ademas de editar las configuraciones de conexion predefinidas, el dbExpress Connection Editor tambien permite seleccionar una conexion para el componente SQLConnection haciendo clic sobre el boton OK. Observe que si cambiamos algunas configuraciones, 10s datos se escriben inmediatamente en 10s archivos de configuracion: hacer clic sobre el boton Cancel no deshace 10s cambios. Si queremos definir el acceso a una base de datos, lo mejor es editar las propiedades de conexion. De ese modo, cuando necesitamos acceder a la misma base de datos desde otra aplicacion o desde otra conexion dentro de la misma aplicacion, todo lo que hay que hacer es seleccionar la conexion. Sin embargo, dado que esta operacion copia 10s datos de conexion, actualizar la conexion no refresca
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
082 lnlelbase O~acle
I Lb~w N
~ M
I V&
lbra19
dbexpmd
Figura 14.4. El dbExpress Connection Editor con el cuadro de dialogo dbExpress Drivers Settings.
Lo que realmente importa para el componente SQLConnection es el valor de sus propiedades. El controlador y las bibliotecas de fabricante se listan en propiedades que podemos cambiar libremente en tiempo de diseiio (aunque rara vez querremos hacer esto), mientras la base de datos y otras configuraciones de conexion especificas de bases de datos se listan en las propiedades Params.Se trata de una lista de cadenas que incluye information como el nombre de la base de datos, el nombre de usuario y la contraseiia, etc. En la practica, podriamos configurar un componente SQLConnect ion configurando el controlador y asignando directamente el nombre de la base de datos en la propiedad Params, olvidandonos de la conexion predefinida. No estamos sugiriendo que sea la mejor opcion, pero es una posibilidad: las conexiones predefinidas son practicas, per0 cuando cambian 10s datos aun sera necesario refrescar manualmente todos 10s componentes SQLConnec t ion. Para ser completos, tenemos que mencionar que existe una alternativa. Se puede configurar la propiedad LoadParamsOnConnect para indicar que queremos refrescar 10s parametros del componente desde 10s archivos de inicializacion cada vez que se abra la conexion. En este caso, un cambio en las conexiones predefinidas se volvera a cargar cuando se abra la conexion, ya sea en tiempo de diseiio o de ejecucion. En tiempo de diseiio, esta tecnica resulta util (tiene el mismo efecto que volver a seleccionar la conexion); pero usarla en tiempo de ejecucion significa que tambien habra que desplegar el archivo connections.ini, lo que puede ser una buena idea o no, segun el entorno de despliegue. La unica propiedad del componente SQLConnect ion que no esta relacionado con el controlador ni las configuraciones de la base de datos es Loginprompt.
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.
NOTA: Tecnicamente, algunas operaciones de desplazamiento liaman a la funcion intema CheckBiDirectional'y puedm crear una exception. C h e c k B i D i r e c t i o n a l se refiere a la propiedad publica 1sunidirectional de la clase TDataSet,que po&mos usar enultimo termino en nuestro propio ckligo para desactivar las operaciones ilegales en conjuntos de datos unidireccionales.
Ademas de tener capacidades de navegacion limitadas, estos conjuntos de datos no tienen soporte de edicion, por lo que muchos metodos y eventos comunes a otros conjuntos de datos no estitn disponibles. Por ejemplo, no existe un evento A f terEdit ni Bef orePos t . Como ya mencionamos, de 10s cuatro componentes de conjuntos de datos para dbExpress, el fundamental es TSQLDataSet, que se puede usar tanto para obtener un conjunto de datos como para ejecutar una orden. Estas dos alternativas se activan llamando al metodo Open (o definiendo la propiedad Active como True) y llamando a1 metodo ExecSQL. El componente SQLDataSet puede recuperar la tabla completa o usar una consulta SQL o un procedimiento almacenado para leer un conjunto de datos o
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
---
--
~ d p
_I
Figura 14.5. El CommandText Editor usado por el componente SQLDataSet para consultas.
Cuando utilizamos una tabla, el componente creara una consulta SQL automaticamente, puesto que dbExpress tiene como destino solo bases de datos SQL. La consulta generada incluira todos 10s campos de la tabla, y si especificamos la propiedad SortFieldNames,incluira una clausula sort by. Los tres componentes de conjuntos de datos especificos tienen un comportamiento similar, pero especificamos la consulta SQL en la propiedad de lista de cadenas SQL,el procedimiento almacenado en la propiedad S toredProcName y el nombre de la tabla en la propiedad TableName (como en 10s tres componentes homologos del BDE).
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 a land indica &e todos estos comorl ponentes son obsoletos. Sin embargo, Demos\Db\SQLClientDataSet con. - . . . - . - - -.t~ene copla del componente ongmal, y se puede rnstalar en Delph17 por una 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.
Month : Word;
-.
day : Pidrd;
--
AS
rn-,-.-L-",l--
/-^-^-:-:A\GIJ uyusluuu a 1- y ~ o la
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.
El componente SQLConnection: Ofrece la conexion con la base de datos y el controlador dbExpress adecuado. El componente SQLDataSet: Enlaza con la conexion (mediante la propiedad S Q L C o n n e c t i o n ) e indica que consulta SQL ejecutar o que tabla abrir (usando las propiedades CommandT y p e y CommandTex t mencionadas antes). El componente Datasetprovider: Conectado con el conjunto de datos, extrae 10s datos del SQLDataSet y puede generar las sentencias de actualizacion SQL adecuadas. El componente ClientDataSet: Lee del proveedor de datos y almacena todos 10s datos (si su propiedad P a c k e t R e c o r d s esta definida como -1) en memoria. Necesitaremos llamar a1 menos a su metodo A p p l y U p d a t e s para enviar las actualizaciones de vuelta a1 servidor de la base de datos (a traves del proveedor). El componente Datasource: Permite exponer 10s datos del ClientDataSet a 10s controles data-aware.
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.
Ambos ejemplos tienen tambien algunos controles visuales: una cuadricula y una barra de herramientas basados en la arquitectura del administrador de acciones .
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
locales de nuevo en el servidor de la base de datos. Esto normalmente se realiza llamando a1 metodo A p p l y U p d a t es . Podemos mantener 10s cambios en la cache local durante algun tiempo y aplicar despues una serie de actualizaciones a la vez o enviar cada cambio directamente. En estos dos ejemplos, hemos empleado la ultima tecnica, adjuntado 10s siguientes controladores de eventos a 10s eventos A f t e r P o s t (que se activa despues de las operaciones de edicion o insercion) y A f t e r D e l e t e de 10s componentes C l i e n t D a t a S e t :
p r o c e d u r e TForml .Doupdate (Dataset: TDataSet) ; begin // a p l i c a i n m e d i a t a m e n t e 10s c a m b i o s l o c a l e s a la b a s e d e // d a t o s SQLClientDataSetl.ApplyUpdates(0); end;
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 i actualizacion del componente C 1 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 1 D a t a S e t incluye la conexion, el monitor no puede conectarse e 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):
procedure TForml.SQLMonitorlLogTrace(Sender: TObject; CBInfo: pSQLTRACEDesc; var LogTrace: Boolean) ; begin Memol.Lines : = SQLMonitor1.TraceList; end;
INTERBASE ~sc-cwnnwt-relammg INTERBASE ~sc-dsql-free-statement INTERBASE . ISC-start-lransact~on INTERBASE - ISC-dsql-allocale-statement #updale EMPLOYEE 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
A1 configurar las propiedades del SimpleDataSet no esiste un mod0 de cambiar como se genera el codigo de actualizacion (lo que resulta peor que con el
componente SQLClientDataSet,que tenia la propiedad UpdateMode para ajustar las sentencias de actualization). En el ejemplo DbxMulti, puede usarse la propiedad UpdateMode del componente Datasetprovider configurando el valor como upwherechanged o upWhereKeyOnly.En este caso se generaran las dos sentencias siguientes, respectivamente:
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.
NOTA: Analizaremos este tipo de problema m b adelante de nuevo cuando examinemos 10s detalles del componente ClientDataSet, el proveedor, el resolutor y otros detalles tbcnicos m b adelante en este mismo capitulo y en el capitulo 16.'
Acceso a metadatos de la base de datos con SetSchemalnfo
Todos 10s sistemas RDBMS usan tablas con fines especiales (denominadas normalmente tablas de sistema) para almacenar metadatos, como la lista de tablas, sus campos, indices y restricciones y cualquier otra informacion de sistema. A1 igual que dbExpress ofrece una API unificada para trabajar con diferentes
servidores SQL, tambien ofrece una forma de acceso comun a metadatos. El componente SQLDat aSet posee un metodo, Set SchemaInf o, que rellena el conjunto de datos con informacion de sistema. Este metodo Set Schema Inf o tiene tres parametros: SchemaType: Indica el tipo de informacion solicitada y entre sus valores se inchyen stTables, stSysTables, stProcedures, stcolumns
y stProcedureParams.
Schemaobject: Indica el objeto a1 que nos referimos, como el nombre de la tabla para la que estamos solicitando las columnas. SchemaPattern: Es un filtro que permite limitar nuestra solicitud a tablas, columnas o procedimientos que comiencen con las letras dadas. Esto es muy comodo si usamos prefijos para identificar grupos de elementos. Por ejemplo, en el programa SchemaTest, un boton Tables lee dentro del conjunto de datos todas las tablas de la base de datos conectada:
El programa usa el habitual grupo de proveedor de conjunto de datos, conjunto de datos cliente y componente de fuente de datos para mostrar 10s datos en una cuadricula, como muestra la figura 14.7. Despues de obtener las tablas, podemos scleccionar una fila en la cuadricula y hacer clic sobre el boton FieIds para ver una lista de 10s campos de dicha tabla:
SQLDataSetl.SetSchemaInfo (stcolumns, ClientDataSetl [ ' Table-Name' 1 , ' '); C1ientDataSetl.Close; ClientDataSetl.0pen;
Ademas de acceder a metadatos de bases de datos, dbEspress ofrece un mod0 de acceso a su propia informacion de configuracion, como 10s controladores instalados y las conexiones configuradas. La unidad DbConnAdmin define una clase TConnectionAdmin para dicho fin, pero el objetivo de este soporte esta limitad0 a utilidades adicionales de dbExpress para desarrolladores (no se espera que 10s usuarios finales accedan a varias bases de datos de un mod0 totalmente dinamico).
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 f ayuda con la leyenda "The structure o 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 COUNTRY CUSTOMER DEPARTMENT EMPLOYEE EMPLOYEE_PROJECT ITEMS JOB PHONE-LIST PROJECT PROJ-DEPT-BUDGET SAIARY-HISTORY SALES
1TABLE-',
Figura 14.7. El ejemplo SchemaTest permite ver las tablas de una base de datos y las columnas de una tabla dada.
:country
En esta sentencia SQL, : country es un parametro. Puede establecerse su tip0 de datos y valor inicial mediante el editor del conjunto de propiedades Params del componente SQLDataSet. Cuando se abre el editor del conjunto Params (como se muestra en la figura 14.8), se puede ver una lista de 10s parametros definidos en la sentencia SQL. Puede fijarse el tip0 de datos y el valor inicial de estos parametros en el Object Inspector. El formulario que muestra este programa, llamado ParQuery, utiliza un cuadro combinado para proporcionar todos 10s valores disponibles para 10s parametros. En lugar de preparar 10s elementos del cuadro combinado en tiempo de diseiio, se puede extraer el contenido disponible de la misma tabla de la base de datos cuando arranque el programa. Esto se realiza usando un segundo componente de consulta, con esta sentencia SQL:
select d i s t i n c t job-country f r o m employee
'
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 ;
El usuario puede escoger un clement0 distinto en el cuadro combinado y haccr despues clic sobre el boton Select (Buttonl) para modificar el parametro y activar (o volvcr a activar) la consulta:
p r o c e d u r e TQueryForm.ButtoniClick(Sender: TObject); begin SqlDataSetl.Close; C1ientDataSetl.Close; Queryl. Params [O] .Value : = ListBoxl. Items [Listboxl.ItemIndex]; SqlDataSetl.0pen; ClientDataSetl.0pen; 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
Sa
120
En-
A1 utilizar consultas paramktricas, se suele poder reducir la cantidad de datos que se dcsplazan desde el servidor a1 cliente y seguir usando una DBGrid y la interfaz de usuario estandar habitual en las aplicaciones de bases de datos locales.
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 .
El ejemplo, denominado UniPrint, posee un componente unidireccional SQLDataSet, vinculado a una conexion InterBase y basado en la siguiente sentencia SQL, que une la tabla de empleados (employee) con la tabla de departamentos (department) para mostrar el nombre del departamento en el que trabaja cada empleado:
select d.DEPARTMENT, e.FULL-NAME, e.JOB-COUNTRY, e.HIRE-DATE from EMPLOYEE e inner join DEPARTMENT d on d.DEPT-NO = e.DEPT-NO
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, IncludeTrailing~athDelimiter (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);
Printer.Canvas.Font : = Font;
try
data.Open;
try
sizeStr : = IntToStr (min (data.Fields [i] .Displaywidth, maxSize) ) ; Write (PrintFile, Format ( ' B - ' + sizeStr + 's', [data.Fields [i] . FieldName] ) ) ;
end;
Writeln (PrintFile);
/ / para cada registro del conjunto de da tos Printer.Canvas.Font.Sty1e : = [ I ; w h i l e not data.EOF do begin / / imprime cada campo del registro f o r I : = 0 t o data-Fieldcount - 1 do begin sizeStr : = IntToStr (min (data.Fields [i].Displaywidth, maxSize) ) ; Write (PrintFile, Format ( ' % - I + sizeStr + 's', [data.Fields [i].Asstring]) ) ; end; Writeln (PrintFile); // avanza la ProgressBar progress.Position : = progress.Position + 1; data.Next; end; finally // cierra el conjunto de da tos data.Close; end ; finally / / reasigna la fuente de impresion original 01dFont.Free; end ; finally // cierra la impresora/archivo CloseFile (PrintFile); end ; end ;
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), devuelve el numero de registros de la tabla de empleados. Esta que 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:
erve que 2mtes de definir un indice para 10s datos, )doel conj,unto de datos (yendo a su ultimo registro ,+D,,,-A, u U G l l U G l I U U la ylurlGuad Packt L L I C C I V L U ~ WIW -I 1 nn LIU 3 G l -1, -1, UG . tendremos un indice extraiio basado en datos parciales.
., ..
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).
.
-
TRUCO:En Delphi, las operaciones Applyupdates y Undo del componente Client D a t aSe t tarnbien son accesibles a traves de acciones predefinidas.
El estado de 10s registros
El componente nos permite realizar un seguimiento de lo que ocurre en 10s paquetes de datos. El metodo U p d a t e s t a t u s devuelve uno de 10s siguientes indicadores para el registro actual:
type TUpdateStatus = (usunmodified, usModified, usInserted, usDeleted) ;
Para comprobar el estado de cada registro en el conjunto de datos del cliente facilmente? podemos aiiadir un campo calculado de tip0 cadena a1 con-junto de datos (lo hemos llamado c l i e n t D a t a S e t 1S t a t u s ) y calcular su valor con el siguiente controlador del evento O n C a l c F i e l d s :
procedure TForml.ClientDataSetlCalcFields(DataSet: begin ClientDataSet1Status.AsString : = GetEnurnName (TypeInfo(TUpdateStatus), Integer (ClientDataSet1.UpdateStatus)); end;
TDataSet);
Este metodo (basado en la funcion RTTI GetEnumName) convierte el valor actual de la enumeracion T U p d a t e S t a t u s en una cadena, con el efecto que muestra la figura 14.10.
U W ~ StxwDda
-. .-
1
. .. . .
--
Data
I
115 125 100 123 623 621 672 622 622
- -
_I
- ---
- - ..
JDEPT-NO(EMP-NO [FIRST-NAME
usUnmodlied usUnmod111ed 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
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;
El formato usado por la propiedad D e l t a cs cl mismo que el usado para transmitir 10s datos dcsde el cliente a1 servidor. Lo que podemos hacer es aiiadir otro componcntc C l i e n t Dat a S e t a una aplicacion y conectarlo a 10s datos de la propicdad D e l t a del primer conjunto de datos dcl cliente:
i f ClientDataSetl Changecount > 0 then begin ClientDataSet2.Data : = ClientDataSetl-Delta; ClientDataSet2.0pen;
En el ejemplo CdsDelta, hemos aiiadido un modulo de datos con 10s dos componentes C l i e n t D t a S e t y una fuente de datos: un SQLDataSet proyectado a 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.
-
El formulario de esta aplicacion tiene un control de paginado con dos fichas, cada una con un DBGrid, uno para 10s datos reales y otro para el delta. Algo de codigo oculta o muestra la segunda solapa dependiendo de la esistencia de datos en el registro de cambios, como lo devuelve el metodo Changecount y actualiza el delta cuando se selecciona la solapa correspondiente. La p a r k principal del codigo utilizada para manipular 10s datos delta es muy similar a1 ultimo fragmento de codigo y se puede estudiar el codigo fuente del e.jemplo en el CD. La figura 14.11 muestra cl registro de cambios de la aplicacion CdsDelta. Fijese en que el conjunto de datos delta time dos entradas por cada registro modificado (10s valores originales y 10s campos modificados) a menos que se trate dc un nuevo registro o uno eliminado, como indica su estado.
SWW
) - usllnrnnd~kd - usMwhf~ed
IDEPT-NOIEMP-NO
600
IFIRST-NAME 2 Robert
(LAST-NAME
Nelson Rdand Guckenhec
uslnsertcd - usUnrndhed
622
- wModtfd
622
? 21 7 23
I
105900
32Q30
3M(JO
usD&ed - usUmd~hed
-usModltnj
Osba~ne
Gbn
llOOW 390W
937
A
Figura 14.1 1. El ejemplo CdsDelta permite ver las solicitudes de actualizacion temporal almacenadas en la propiedad Delta del ClientDataSet.
.
- - -
.-
-- - ~ .
puede ocurrir debido a una actualizacion concurrente de dos personas distintas. Es habitual utilizar un bloqueo optimista en aplicaciones clientelservidor, por lo que esto deberia contemplarse como una situacion normal. El evento OnRe c o n c i l e E r r o r permite modificar el parametro A c t i o n (que se pasa como referencia), que determina el mod0 en que deberia comportarse el servidor:
procedure TForml.ClientDataSet1ReconcileError(DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction) ;
Este metodo tiene tres parametros: el componente de conjunto de datos del cliente (en caso de que haya mas de un conjunto de datos de cliente en la aplicacion actual), la excepcion que ocasiono el error (con el mensaje de error) y el tip0 de operacion que fa110 ( u k M o d i f y , u k I n s e r t o u k D e l e t e ) . El valor de retorno, que almacenaremos en el parametro A c t i o n , puede ser uno de 10s siguientes :
type TReconcileAction = (raSkip, raAbort, raMerge, racorrect, racancel, raRefresh) ;
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;
La unidad Reconc, que contiene el dialogo Reconcile Error (una ventana titulada Update Error habria sido mas comprensible para 10s usuarios finales de 10s programas) contiene mas de 350 lineas de codigo, por lo que no podemos describirla en detalle. Sin embargo, deberia ser facil comprender el codigo fuente si se estudia con detenimiento. Ademas, puede usarse sin preocuparse por como hnciona. El cuadro de dialogo aparecera en caso de error, informando del cambio solicitad0 que causo el conflict0 y permitiendo a1 usuario escoger uno de 10s posibles valores TReconcileAct ion.Podemos ver un ejemplo en la figura 14.12.
-R
c Skip
Cbnd C Coned C Rdmh
.
C Mape
--
Fild Name
EMP-NO
M d f i Vdue
Ih f l i c ! i n g ~ a b a
tunchanoed, <Unchanged> <Unchanged> <Unchanged>
10iipnd ahr re 2
Robert
I CUnchanaed,
<Unchanged, <Unchanged> tUnchmgecb
Nelson
105900 250
PHONE-W
251
333
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):
StartTransaction: Marca el comienzo de la transaccion. Commit: Confirma todas las actualizaciones de la base de datos realizadas durante la transaccion. Rollback: Devuelve la base de datos a su estado anterior a la transaccion.
Podemos utilizar tambien la propiedad In T r a n s a c t i o n para comprobar si una transaccion esta activada. Lo mas normal es usar un bloque t r y para deshacer una transaccion cuando se lanza una excepcion, o se puede confirmar la tran-
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;
Cada metodo relacionado con la transaccion tiene un parametro que describe la transaccion con la que trabaja. El parametro utilizaa el tip0 de registro T T r a n s a c t ionDesc y equivale a un nivel de aislamiento de la transaccion y a un identificador. El nivel de aislamiento de la transaccion es una indicacion de como deberia comportarse la transaccion cuando otras transacciones modifiquen 10s datos. Los tres valores predefinidos son 10s siguientes: tiDirtyRead: Hace que la actualizaciones de la transaccion resulten visibles inmediatamente para otras transacciones y usuarios, antes incluso de que se confirmen. Se trata de la unica posibilidad para algunas bases de datos y se corresponde con el comportamiento de las bases de datos sin soporte de transacciones. tiReadCommitted: Hace que esten disponibles para otras transacciones so10 las actualizaciones que ya han sido confirmadas. Este valor es el recomendado para la mayor parte de las bases de datos, para conservar la eficacia. tiRepeatableRead: Oculta 10s cambios de cualquier otra transaccion iniciada por otros usuarios despues de la actual, incluso aunque se hayan confirmado 10s cambios. Llamadas repetidas en una transaccion produciran siempre el mismo resultado, como si la base de datos tomara una instantanea de 10s datos cuando comienza la transaccion actual. Solo InterBase y algunos otros servidores de bases de datos funcionan con eficiencia mediante este modelo.
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;
Tambien se puede indicar que conjuntos de datos pertenecen a que transaccion fijando la propiedad Transact ionLevel de cada conjunto de datos al valor de un identificador de transaccion. como se muestra en la ultima sentencia. Para seguir realizando y experimentando con 10s niveles de aislamiento de transacciones, se puede usar la aplicacion TranSample. Como vemos en la figura 14.13, 10s botones de radio permiten escoger las alternativas y 10s botones de pulsador permiten trabajar sobre las transacciones y aplicar actualizaciones o refrescar 10s datos. Para captar la verdadera idea de 10s distintos efectos, deberiamos ejecutar varias copias del programa (siemprc que tengamos suficientes licencias en el servidor InterBase)
podemos cambiar el servidor de bases de datos utilizado por la aplicacion, aunque en la practica no resulte tan sencillo. Si la aplicacion que vamos a crear empleara siempre una misma base de datos, se pueden escribir programas enlazados directamente con la API de ese servidor especifico. Este enfoque hara que 10s programas Sean explicitamente no adaptables a otros servidores SQL
Figura 14.13. El formulario de la aplicacion Transample en tiempo de diseRo Los botones de radio permiten configurar distintos niveles de aislamiento de transaccion.
Por supuesto, generalmente no utilizaremos directamente estas API, sino que basaremos el desarrollo en otros componentes de conjuntos de datos como envoltorio de dichas API y que encajen de forma natural en Delphi y en la arquitectura de su biblioteca de clases. Un ejemplo de este tip0 de familia de componentes es InterBase Express (IBX). Las aplicaciones creadas usando estos componentes deberian funcionar mejor y mas rapido (siquiera ligeramente), ofreciendo un mayor control sobre las caracteristicas especificas del servidor. Por ejemplo, IBX proporciona un conjunto de componentes administrativos especificos para InterBase 6.
-
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
.. i
Los conhnt& he datos ie%ten co&Grar el d o m p o r rniento automatic& de generador c o i o una especie de campo de incrernento automati'ca. Para ello, se establece la propiedad G e n e r a t o r F i e l d utilizando su edit# de propiedad especifico.
IBTransaction y un componente de conjunto de datos (en este caso un IBQuery). Cualquier aplicacion IBX necesita a1 menos una instancia de 10s dos primeros componentes. No se pueden establecer conexiones a bases de datos en un conjunto de datos de IBX, como si se podia con otros conjuntos de datos. Y, es necesario a1 menos un objeto de transaccion para leer siquiera el resultado de una consulta. Estas son las propiedades claves de estos componentes en el ejemplo IbsEmp:
o b j e c t IBTransactionl: T I B T r a n s a c t i o n A c t i v e = False DefaultDatabase = IBDatabasel end o b j e c t IBQueryl: T I B Q u e r y Database = IBDatabasel T r a n s a c t i o n = IBTransactionl CachedUpdates = False SQL-Strings = (
'SELECT
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
Ahora podemos conectar un componente Datasource a IBQueryl y crear facilmente una interfaz de usuario para la aplicacion. Hemos escrito el nombre de ruta de la base de datos de muestra de Borland. Sin embargo, no todo el mundo time la carpeta Archivos de programa, que depende de la version local de Windows y 10s archivos de datos de muestra de Borland podrian estar en cualquier otra parte del disco. Resolveremos estos problemas en el proximo ejemplo.
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.
LAST-NAME = :LAST-NAME , ' SALARY = :SALARY, ' DEPT-NO = :DEPT-NO, ' JOB-CODE = :JOB-CODE,' JOB-GRADE = : JOB-GRADE, ' JOB-COUNTRY = :JOB-COUNTRY' 'where' ' EMP-NO = :OLD-EMP-NO ' ) InsertSQL-Strings = ( 'insert into 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) ' ) DeleteSQL. Strings = ( ' delete from EMPLOYEE ' ' where EMP-NO = :OLD-EMP-NO ' )
'
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
Si se conecta el componente I B Q u e r y l o el componente I B D a t a S e t 1a la fuente de datos y se ejecuta el programa, se vera que su comportamiento es iddntico. No solo tienen 10s componentes un efecto similar; incluso las propiedades y eventos disponibles son similares. En el programa IbxUpdSql hemos hecho que la referencia a la base de datos sea un poco mas flexible. En lugar de escribir el nombre de la base de datos en tiempo de diseiio, hemos estraido el directorio de datos compartidos de Borland desde el Registro de Windows (en el que Borland lo guarda durante la instalacion de Delphi). Este es el codigo que se ejecuta cuando se inicia el programa:
uses Registry; p r o c e d u r e TForml.FormCreate(Sender: TObject); var Reg: TRegistry; begin Reg : = TRegistry.Create; try Reg.RootKey : = HKEY-LOCAL-MACHINE; Reg.OpenKey('\Software\Borldnd\Borland S h a r e d \ C u r r e n t V e r s i o n l , False) ; 1BDatabasel.DatabaseName : = Reg. Readstring ( ' R o ot D i r e c t o r y l) + 'exarnples\da t a b a s e \ e m p l o y e e . g d b l; finally Reg. Free; end ; EmpDS.DataSet.Open; 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);
Cuando se ejecutan, realizan la operacion principal pero tambien necesitan abrir de nuevo el conjunto de datos en una nueva transaccion (lo que tambien puede hacerse mediante "retencion" del context0 de transaccion). En realidad, Cornrni t R e t a i n i n g no reabre una nueva transaccion, sino que permite que la transaccion actual permanezca abierta. Asi, podemos seguir usando 10s conjuntos de datos, que no se refrescaran (por lo que no veremos las ediciones ya confirmadas por otros usuarios) per0 seguiran mostrando 10s datos que hemos modificado. ~ s t es el codigo: e
procedure TForml.acCommitExecute(Sender: begin 1BTransactionl.CommitRetaining; 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 ) -v oher a adquirir 10s datos,.aunque no hayarnos . - .' y . . -. hecho cas~blos. cambib, cumdo continnirmos 10s datos podemos pedtrle Bn a I tr e que. Verigi el "amtptto de transa&ibp1?(que no rcierre 10s n w e conjut$& ck, abiartoij;,&do uqa b r h ~ogmit~etaining, o cm ya meficioiamott. L& d ' d eeste winpottamiedto i28 InteiSw 6s que una trmsaccih se coneqxmde con una instantheor i lox dams. thando la k tfansa&Bn iia termhtatlo.. se ~~e que 1 8 b r d m i & ndetro para voher a ewaer 10s regigtros que pdedad habm si& mcrdificadcm*por otros US~&&. La v e ~ i S n60 tie InftrBase lndh$e tambftn bnzt orden . ~ o $ . & b a c e t ining, que Bemqs.$eo;dndp nvusar, porque ep ma opek~ a ra&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
arrancar y cierra automaticamente la transaccion actual a1 terminar, tras haber preguntado a1 usuario que hacer, con el siguiente controlador de eventos Onclose:
procedure TForml.FormClose(Sender: TObject; var Action: TCloseAction) ; var nCode: Word; begin if 1BTransactionl.InTransaction then begin nCode : = MessageDlg ( ' Commit T r a n s a c t i o n ? (No t o rollback) ' , mtconfirmation, mbYesNoCance1, 0 ) ; case nCode of mrYes: 1BTransactionl.Commit; mrNo: 1BTransactionl.Rollback; mrcancel: Action : = caNone; // no cierra end ; end; end:
212850 0
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
De Sowa
15 Kalherme
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~
1E900 6 27000 6
33620 63 1 2335 1
3922406 1
2L
Figura 14.14. La salida del ejemplo IbxUpdSql.
procedure TForrnl.IBSQLMonitorlSQL(EventText: String); begin if Assigned (RichEditl) then RichEditl.Lines.Add (TirneToStr (Now) + ' : ' + EventText); end;
La comprobacion i f A s s i g n e d puede ser util cuando recibimos un mensaje durante el cierre de la conexion, y se necesita cuando aiiadimos este codigo directamente a la aplicacion sobre la que vamos a realizar el seguimiento. Para recibir mensajes desde otras aplicaciones (o desde la actual), tenemos que activar las opciones de seguimiento del componente IBDatabase. En el anterior ejemplo IbxUpdSql, las hemos activado todas:
o b j e c t IBDatabasel: TIBDatabase
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.
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
IBX administrativos, que muestran estadisticas del servidor, algunas propiedades del servidor y todos 10s usuarios conectados. Podemos ver un ejemplo de las propiedades del servidor en la figura 14.16 y el codigo para extraer 10s usuarios en el siguiente fragment0 de codigo.
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] ) ) ;
TRUCO: La base de datos que comentaremos en este apartado se llama mastering gdb y puede encontrarse dentro de la subcarpeta data del
nArl;nn V V U ~
~ V a a UJCU p a
mar-
net-
.ran;t..ln
u a p ~ b u n u .U C I Y U f ua l C I & i UU l + ( a
CP
mmmnrl~ m - l ; v a r o
I ~ L U U I ~ ~ C C I UCCIIYQJU
m ~ r l ; n n tT n t ~ r R o n m V U Pc n C~, ~ P n n V~ l I U
preferiblemente desputs de hacer una copia en una unidad con pe~misos de escritura para que se pueda interactuar con ella con plena libertad.
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.
-
--
--
NOTA: Normalrnente usamos una sola secuencia de identificadores para todo un sistema, algo que a veces se llama identificador de objeto (OID). Sin embargo, en este caso, 10s identificadores de las dos tablas deberin ser imicos. Ya que no podemos saber de antemano quk objetos podrian utilizarse en lugar de otros, acloptar un OID global permite una mayor libertad. El inconveniente es que, si tenemos muchos datos, usar un entero de 32 bits como identificador podria no resultar suficiente. Por eso, InterBase 6 soporta generadores de 64 bits.
Para generar esos identificadores cuando hay varios clientes en funcionamiento, mantener una tabla con el valor mas reciente creara problemas, puesto que las diversas transacciones concurrentes (de diferentes usuarios) veran 10s mismos valores. Si no usamos tablas, podemos usar un mecanismo independiente de la base de datos, como 10s bastante amplios GUID de Windows o la, asi denominada, tecnica de alto-bajo (la asignacion de un numero base a cada cliente durante el arranque [el numero alto] que se combina con un numero consecutivo [el numero bajo] determinado por el cliente). Otra tecnica, ligada a las bases de datos, consiste en usar mecanismos internos para secuencias, indicados con distintos nombres en cada servidor SQL. En InterBase se llaman generadores. Estas secuencias funcionan y se incrementan de manera externa a las transacciones, de tal mod0 que proporcionan numeros unicos incluso a usuarios concurrentes (recordemos que InterBase nos obliga a abrir una transaccion incluso para leer datos). Ya hemos visto como crear un generador. Esta es la definicion de uno en nuestra base de datos de muestra, seguida por la definicion de la vista que podemos usar para consultar un nuevo valor:
create generator g-master ; create view v-next-id next-id
(
Dentro de la aplicacion RWBlocks, hemos aiiadido un componente IBQuery a un modulo de datos (puesto que no necesitamos un conjunto de datos que pueda editarse) con la siguiente sentencia SQL:
select next-id f r o m v-next-id;
La ventaja, en comparacion con el uso de la sentencia directa, es que es mas sencillo de escribir y mantener, incluso si el generador subyacente cambia (o si pasamos a usar una tecnica diferente de forma interna). Ademas, en el mismo modulo de datos hemos aiiadido una funcion que devuelve un nuevo valor para el generador:
f u n c t i o n T D m M a i n - G e t N e w I d : Integer; begin // d e v u e l v e e l p r o x i m o v a l o r d e l g e n e r a d o r QueryId.Open; try Result : = QueryId.Fields[O].AsInteger; finally QueryId.Close; end; end;
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.
. -
I
I
r OnPost
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.
Para que la busqueda no distinga entre mayusculas y minusculas, podemos usar la funcion u p p e r en ambas partes de la comparacion para comparar 10s valores de cada cadena en mayusculas, per0 una consulta similar seria muy lenta, puesto que no se basara en un indice. Por otra parte, guardar 10s nombres de las empresas (o cualquier otro nombre) en letras mayusculas no tendria mucho sentido, porque cuando tengamos que mostrar 10s nombres, el resultado sera poco natural (aunque sea muy frecuente en sistemas de informacion antiguos). Si podemos conseguir algo de espacio de disco y memoria para obtener mayor velocidad, podemos usar un truco: aiiadir un campo adicional a la tabla para almacenar el valor en mayusculas del nombre de la empresa y usar un disparador del servidor para generarlo y actualizarlo. Entonces podemos pedir a la base de datos que mantenga un indice para la version en mayusculas del nombre, para
acelerar aun mas la operacion de busqueda. En la practica, la definition de tabla tendra este aspecto:
c r e a t e domain d-uid as integer; create table companies
(
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;
"'
TRUCO: Usando una consulta preparada con p a r b e t r o s , podriamos hacer que el codigo fuese a h msJ rapido.
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)
alter table locations add constraint locations-fk-companies foreign key (id-company) references companies (id) on update no action on delete no action;
La definicion final de una clave externa relaciona el campo i d company de la tabla de ubicaciones con el campo identificador de la tabla deempresas. La otra tabla lista 10s nombres e informacion de contacto de personas en ubicaciones especificas de las empresas. Para seguir las reglas de normalizacion de bases de
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)
) ;
a l t e r t a b l e people add c o n s t r a i n t people-fk-companies f o r e i g n key (id-company) r e f e r e n c e s companies ( i d ) on u p d a t e no a c t i o n on d e l e t e cascade; a l t e r t a b l e p e o p l e add c o n s t r a i n t people-fk-locations f o r e i g n key (id-company, id-location) r e f e r e n c e s l o c a t i o n s (id-company, i d ) ; c r e a t e t r i g g e r people-ai f o r people active a f t e r i n s e r t position 0 as begin
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;
Como sugiere este codigo, un modulo de datos alberga 10s componentes de conjunto de datos. En realidad, el programa posee un modulo de datos por cada
formulario (conectado de forma dinamica, ya que podemos crear varias instancias de cada formulario). Cada modulo tiene una transaccion independiente, para que las diversas operaciones realizadas en distintas fichas Sean totalmente independientes. La conexion de base de datos, en cambio, es centralizada. Un modulo de datos principal alberga el componente correspondiente, al que hacen referencia todos 10s conjuntos de datos. Cada uno de 10s modulos de datos lo crea de forma dinamica el formulario haciendo referencia a el, y su valor se almacena en cl campo privado d m del formulario:
procedure TFormCompanies.FormCreate(Sender: TObject); begin dm : = TDmCompanies .Create (Self); dsCompanies.Dataset : = dm.DataCompanies; dsLocations.Dataset : = dm.DataLocations; dsPeople.Dataset : = dm.DataPeople; end:
De este mod0 podemos crear facilmente varias instancias de un formulario, con una instancia del modulo de datos conectada a cada una de ellas. El formulario conectado al modulo de datos posee tres controles DBGrid, cada uno de ellos ligado a un modulo de datos p a uno de 10s conjuntos de datos correspondientes. Podemos ver este formulario en tiempo de ejecucion con algunos datos en la figura 14.18.
I I L
ID
I
I~D-COMP~ID-LOCPTIONIKEY_CONTACT~WE
13 14
g
1 ~ ~ 6 9 1 ~
I.;
3 9
11 T 11 F
Chuck J
David l
Figura 14.18. Un formulario que rnuestra empresas, ubicaciones de oficinas y gente (parte del ejemplo RWBlocks).
rn
El formulario se encuentra dentro de un formulario principal, que a su vez esta basado en un control de pagina, que incluye otros formularios. Solo el formulario creado con la primera pagina se crea durante el arranque del programa. El metodo ShowForm que hemos escrito se encarga de que el formulario sea "adoptado" por la hoja con solapa del control de ficha, despues de eliminar el borde del formulario:
procedure TFormMain.FormCreate(Sender: TObject); begin ShortDateFormat : = 'dd/m/yyyyr; ShowForm (TFormCompanies.Create (Self), TabCompanies) ; end; procedure TFormMain. ShowForm (Form: TForm; Tab: TTabSheet) ; begin Form.BorderStyle : = bsNone; Form.Align : = alclient; Form.Parent : = Tab; Form.Show; end;
El formulario de empresas (companies) alberga la busqueda por nombre de empresa que ya hemos comentado, mas una busqueda por ubicacion. Escribiremos el nombre de una ciudad y conseguiremos una lista de empresas que tengan una oficina en esa ciudad:
procedure TFormCompanies.btnTownClick(Sender: TObject); begin with dm.DataCompanies do begin Close; SelectSQL.Text : = 'select c.id, c.name, c.tax-code' + ' from companies c ' + ' where exists (select loc.id from locations loc ' t ' where loc.id-company = c.id and upper (loc.town) =
r r !
"
';
dm.DataPeople.0pen; end;
end ;
Si nos fijamos en el codigo fuente del formulario, veremos mucho codigo. Parte del mismo esta relacionado con el permiso de cierre (puesto que un usuario no puede cerrar el formulario mientras hay ediciones pendientes no enviadas a la base de datos), mientras que otra parte considerable esta relacionada con el uso del formulario como dialog0 de busqueda.
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-class)
id
d-uid
not null,
( 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'
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).
d d
19 Bc~lmdCorp
23 Wlr$echItaha Srl
L ID
-
21 NAMEl Davd
AMOUNT 300
24 Chuck J
3W
--
Las tres sentencias SQL alternativas se crean cuando arranca el programa, o cuando se crea y muestra el formulario con las matriculas para las clases. El programa almacena la parte final de las tres instrucciones alternativas (la clausu-
la where) en una lista de cadenas y selecciona una de las cadenas cuando se cambia de solapa:
p r o c e d u r e TFormClasses.FormCreate(Sender: TObject); begin d m : = TDmClasses .Create (Self); // c o n e c t a 10s c o n j u n t o s d e d a t o s a l a s f u e n t e s d e d a t o s dsClasses.Dataset : = dm.IBClasses; dsClassReg.DataSet : = dm.IBClassReg; d s P e o p l e R e g - D a t a S e t : = dm.IBPeopleReg; // a b r e 10s c o n j u n t o s d e d a t o s dm.IBClasses.Active : = True; dm.IBClassReg.Active : = True; dm.IBPeop1eReg.Active : = True;
// 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;
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.
.....".__-... ,
selecl ' lrom classes
--
~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.
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.
NOTA: Ademh, se puede acceder a conjuntos de componentes ADO para Delphi de terceros como Adonis, AdoSolutio, Diamond ADO y Karniak.
En este capitulo trataremos 10s siguientes temas: Microsoft Data Access Components (MDAC). dbGo de Delphi. Archivos de enlace de datos. Adquisicion de informacion esquematica. Uso del motor Jet.
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
BDE. Cuando se instala MDAC, se instalan automaticamente 10s proveedores OLE DB que muestra la tabla 15.1
Tabla 15.1. Proveedores OLE DB incluidos con MDAC.
Controladores ODBC Jet 3.5 Jet 4.0 SQL Server Oracle Servicios OLAP Proveedor de muestra Proveedor sencillo
Controladores ODBC (predeterrninados) Solo acceso a bases de datos de MS Access 97 MS Access y otras bases de datos Bases de datos de MS SQL Server Bases de datos de Oracle Online Analytical Processing Ejernplo d e un proveedor OLE DB para archivos CSV Para crear proveedores propios para datos de texto sirnpie
El proveedor de OLE DB ODBC se usa por compatibilidad regresiva con ODBC. A medida que se aprenda mas sobre ADO, se descubriran 10s limites de este proveedor. Los proveedores OLE DB Jet soporta MS Access y otras bases de datos de sistemas de escritorio. Los trataremos mas adelante. El proveedor SQL Server soportar SQL Server 7, SQL Server 2000 y Microsoft Database Engine (MSDE). MSDE es una version reducida de SQL Server, sin la mayoria de las herramientas y algo de codigo aiiadido para degradar intencionadamente el rendimiento cuando existan mas de cinco conexiones activas. MSDE es importante porque es gratuito y completamente compatible con SQL Server. El proveedor OLE DB para OLAP puede usarse directamente, per0 suele usarlo mas a menudo ADO Multi-Dimensional (ADOMD). ADOMD es una tecnologia ADO adicional diseiiada para ofrecer procesamiento analitico en red (Online Analytical Processing, OLAP). Si se ha usado alguna vez Decision Cube de Delphi, Pivot Tables de Excel o Cross Tabs de Access, entonces se habra usado algun tipo de OLAP.
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.
Conexion a una base de datos Ejecuta un comando SQL de accion Descendiente de TDataSet de proposito general Encapsulacion de una tabla Encapsulacion de la sentencia SELECT de SQL
ADOTable ADOQuery
Table Query
ADOStoredProIC RDSConnection
Encapsulacion de un procedirniento alrnacenado StoredProc Conexion a Remote Data Services Sin equivalente
Los cuatro componentes de conjuntos de datos (ADODataSet, ADOTable, ADOQuery y ADOStoredProc) estan casi completamente implementados por su clase padre inrnediata, TCustomADODataSet. Este componente proporciona la mayor parte de la funcionalidad del conjunto de datos, y sus descendentes son en general envoltorios ligeros que exponen distintas caracteristicas del mismo componente. Como tales, 10s componentes tienen mucho en comun. Sin embargo, en general ADOTable, ADOQuery y ADOStoredProc se contemplan como componentes de "compatibilidad" y se usan para facilitar la curva de aprendizaje y de codigo desde sus homologos BDE. Un aviso: estos componentes de compatibilidad son parecidos a sus homologos, per0 no identicos. Se encontraran diferencias en cualquier aplicacion except0 en las m h triviales. ADODataSet es el componente a escoger, en parte debido a su versatilidad, per0 tambien a su mayor parecido con la interfaz Recordset de ADO en la que se basa. A lo largo de este capitulo, usaremos todos 10s componentes de conjunto de datos para dar una idea sobre como usar cada uno.
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
se sabe lo que se esta haciendo. En general, se usara un editor de cadenas de conexion (el editor de propiedad para la propiedad C o n n e c t i o n s t r i n g ) , que muestra la figura 15.1.
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.
I
-_
I*,
Ahora que se ha determinado la cadena de conesion, se puede escoger una tabla. Desplegaremos la lista de tablas usando la propiedad TableName en el Object Inspector. Seleccionados la tabla Customer. Aiiadimos un componente DataSource y un control DBGrid, y 10s conectamos entre si; ahora vamos a usar ADO en un programa real (aunque trivial), disponible como ejemplo FirstAdoExample. Para ver 10s datos, se establece la propiedad A c t i v e del conjunto de datos como True, o se abre el conjunto de datos en el evento Formcreate (corno en el ejemplo), para evitar errores en tiempo de diseiio si no se encuentra disponible la base de datos.
L
.
TRUCO: Si se va a utilizzk dbGo como la principal tecnologia de a c e so a r bases de datos, podria descm s e llevar el componente DataSource a la pi~gi.. . . na ADO de la Component ra~erre para ey1ra.r tener que cambia-entre la piginaAb0 y iap&ha Data Access. Si se utilizan tanto ADO como otra tecaologia de bases de dabs, se puede simular la instalacibn de DataSource en varias p & g hmw-m plarrtilla de componente para un DataSource e ih&&ndola en la pkgina ADO.
-1
--._--
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
;
Aunque puede darse cualquier extension aun archivo de enlace de datos, la extension recomendad es .U D L . Se puede crear un enlace de datos mediante cualquier editor de texto, o se puede hacer clic con el boton derecho sobre el Explorador de Windows, escoger Nuevo~Documento texto, renombrar el de archivo con una extension .U D L (suponiendo que se muestren las extension con la configuracion del Explorador que se utilice), y despues hacer doble clic sobre el archivo para llamar a1 editor de cadenas de conexion de Microsoft. Cuando se escoge un archivo en el editor de conexion, la propiedad C o n n e c t i o n s t r i ng tomara el valor 'FILE NAME=', seguido del nombre real del archivo, como muestra el ejemplo DataLinkFile. Se pueden colocar 10s archivos de enlace o vinculo de datos en cualquier parte del disco duro, per0 si se busca una ubicacion habitual y compartida, se puede usar la funcion D a t a L i n k D i r de la unidad ADODB de Delphi. Si no se ha modificado la configuracion predeterminada de MDAC, D a t a L i n k D i r devolvera lo siguiente:
C:\Archivos de programa\Archivos comunes\System\OLE DB\Data Links
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.
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.
Para muchos tipos de informacion, querremos filtrar el conjunto de resultado. Por supuesto que podemos aplicar un filtro tradicional de Delphi a1 conjunto de resultad0 usando las propiedades F i l t e r y F i l t e r e d o el evento O n F i l t e r R e c o r d . Sin embargo, esto aplica el filtro en la parte de cliente de este ejemplo. Usando el segundo parametro, podemos aplicar un filtro mas eficaz en la fuente de la informacion esquematica. El filtro se especifica como una matriz de valores. Cada elemento de la matriz posee un significado especifico importante para el tipo de datos que se van a devolver. Por ejemplo, la matriz de filtro para claves primarias posee tres elementos: El primer0 es el catalogo (catalogo es el tkrmino ANSI para base de datos), el segundo es el esquema, y el tercero es el nombre de la tabla. Este ejemplo devuelve una lista de claves primarias para la tabla Customer:
var
Filter: OLEVariant; begin Filter : = VarArrayCreate ( [0, 2 1 , varvariant) ; Filter [ Z ] : = 'CUSTOMER'; ADOConnectionl.OpenScherna( siPrimaryKeys, Filter, Emptyparam, ADODataSetl); end;
Name
CustNo
EmpNo ItemNo 01derNo
ems
Figura 15.3. El ejemplo Openschema obtiene las claves primarias de las tablas de la base de datos.
-- - - -- ----- - de paede dbtener @ xqi~ma$nfoma&m ufilizanda-ADQX, ADOX 9 ecnologia A ~ adicianal qpepemite obtener y actualizm m&maO ci6n esi$uematica. Es el ,egu'rv@en'te en N O a1 lenguaje de desdi&~ de de SQL (DofaD e f i n i t i ~ n L n n ~ o g e , !con las sentenciasCREWI2'~. ~L), mTEIR, DROP. y al lenguaje t.!C ,eor#roI da-datos (DafaCo~ltrdl h?guage, # t ) GRANT y REVOKE. dbGo no soporta directamente ADOX, per6 K ;con . puede importarse la biblioteca de tipos ADOX y usarla con exito en aplica-.
- --
N,
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.
NOTA: El motor Jet se incluia con MDAC en algunas versiones (pero no en todas). No se incluye en la versidn 2.6 de MDAC v2.6. Ha habido un gran debate sobre si 10s programadores que usan una herramienta de desarrollo que no es de Microsoff tienen derecho a distribuir el mator Jet. La respuesta oficial es positiva, y el motor Jkt se encuentra disponible como descarga gratuita (ad& de distribuirse con muchos productos de soft.ware de Micros&].
Existen dos proveedores OLE DB de Jet: el proveedor Jet 3.5 1 OLE DB y el proveedor Jet 4.0 OLE DB. Jet 3.5 1 OLE DB usa el motor Jet 3.5 1 y solo soporta bases de datos Access 97. Si queremos usar Access 97 y no Access 2000, mejoraremos el funcionamiento usando este proveedor OLE DB en la mayoria de 10s casos en lugar del proveedor Jet 4.0 OLE DB. Jet 4.0 OLE DB soporta Access 97, Access 2000 y controladores de Installable Indesed Sequential Access Method (IISAM). Los controladores Installable ISAM son aquellos escritos especificamente para que el motor Jet soporte acceso a formatos ISAM tales como Paradox, dBase y texto, y es esta capacidad la que conviertc a1 motor Jet en una herramienta tan util y versatil. La lista completa de controladores ISAM instalados en nuestro equipo depende del software que hayamos instalado en el mismo. Podemos encontrar dicha lista en el Registro, en:
H K E Y ~ L O C A L ~ ~ C H I N E \ S o f t w a r e \ M i c r o s o f t \ J e t \ 4 . O \ I S A Formats M
Sin embargo, el motor Jet incluye controladores para Paradox, dBase, Excel, texto y HTML.
trlss mlor p?cpkbhs dc &idnacibn de ede b o de data Pmqmaddica m v*. deccim 1 . . wledad Y. a 1 e d n . &M o e r a v e h
N n b e _ Data Souce
- - --
Jet OLEDB CompactW~lh Jet OLEDB Create System Jet OLEDB Dalabase Loc Jet OLEDB Database Par Jet OLEDB Don1Copy Lo Jel OLEDB Enc~ypl Dalab Jet OLEDB Engne Type Jel OLEDB Globd B& TI Jel OLEDB GlobdPalhd Jet OLEDB New Database Jei OLEDB Regdry Path I . n, ,-,.n.ci-n . -
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.
TRUCO: Tambidn se puede leer un archivo Excel mediante el componente XLSReadWrite (disponible en www .axolot .corn). No requiere que Excel se encuentre instalado en el ordenador ni el tiempo necesario para arrancarlo (corno hacen las tkaicas de OLE Automation).
Nos aseguraremos de que la hoja de calculo no este abierta en Escel, ya que ADO necesita acceso exclusive a1 archivo. Aiiadimos un componente ADODataSet a un formulario. Definimos su propiedad C o n n e c t i o n s t r i n g para usar el proveedor Jet 4.0 OLE DB y definimos Estended Properties como Escel 8.0. En la solapa Conexion, escribimos el nombre de la base de datos con la especificacion completa de ruta y archivo de la hoja de calculo Excel (o usamos una ruta relativa si tenemos planeado desplegar el archivo junto con el programa). El componente ADODataSet funciona cuando abrimos o ejecutamos un valor en su propiedad ComrnandText. Este valor podria ser el nombre de una tabla, una sentencia SQL, un procedimiento almacenado o el nombre de un archivo. Especificamos el mod0 en que se interpreta dicho valor estableciendo la propie-
dad CommandType.Definimos CommandType como cmdTableDirect para indicar que el valor en CornrnandText se corresponde con el nombre de una tabla y que todas las columnas deberian devolverse desde dicha tabla. Seleccionamos CommandText en el Object Inspector y veremos una flecha desplegable. La desplegamos y aparecera una pseudo-tabla: Employees$. (Los libros de Excel llevan como sufijo $ .) Aiiadimos un Datasource y un DBGrid y 10s conectamos, con lo que obtendremos el resultado del ejemplo JetExcel, que se muestra en la figura 15.5 en tiempo de diseiio. De manera predeterminada seria un poco dificil ver 10s datos en la cuadricula, porque cada columna tiene 255 caracteres de ancho. Podemos cambiar el tamaiio de visualizacion del campo aiiadiendo columnas a la cuadricula y cambiando sus propiedades Width o aiiadiendo campos permanentes y cambiando sus propiedades Size o Displaywidth.
Dent
+ 55 41
338.5031
Ford Marvin
Prelecl
Robot
Tr~ll~an
BecMebrm
273-3522
A1
Figura 15.5. ABCCompany.xls en Delphi.
Fijese en que no podemos mantener abierto el conjunto de datos en tiempo de diseiio y ejecutar el programa, ya que el controlador Excel IISAM abre el archivo XSL en mod0 exclusivo. Por ello cerraremos el conjunto de datos y aiiadiremos a1 programa una linea para que lo abra durante el arranque. Cuando se ejecute el programa, observaremos otra limitacion de este controlador IISAM: podemos aiiadir nuevas filas y editar las que ya existen, per0 no podemos borrarlas. Por cierto, podria haberse utilizado un componente ADOTable o un ADOQuery, en lugar del ADODataSet, pero es necesario conocer el mod0 en que ADO trata 10s simbolos en cosas como 10s nombres de tablas y campos. Si se usa un ADOTable y se despliega la lista de tablas, se vera la tabla Employee$, como era de esperar. Lamentablemente, si se trata de abrir la tabla, se recibira un error. Lo mismo sucede con la sentencia SELECT * FROM Employees$ en un ADOQuery. El problema tiene que ver con el signo de dolar en el nombre de la tabla. Si se usan caracteres como signos de d o h , puntos o, mucho mas importante, espacios en un nombre de tabla o campo, entonces hay que encerrar el nombre entre corchetes (por ejemplo, [Employees$]).
Aiiadimos un componente ADOTable a un formulario, definimos su Connectionstring para usar el proveedor Jet 4.0 OLE DB y definimos Extended Properties como Text. El Test IISAM considera un directorio como una base de datos, por lo que hay que escribir como nombre de la base de datos el directorio que contiene el archivo NightShif t .TXT.Volvemos a1 Object Inspector y desplegamos la lista de tablas de la propiedad TableName.Observaremos que el punto del nombre del archivo se ha transformado en una almohadilla, como en Night Shift#TXT.Establecemos Active como True,aiiadimos un Datasource y un DBGrid y 10s conectamos, con lo que veremos el contenido del archivo de texto en una cuadricula.
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;
Los archivos de texto pueden tener cualquier formato o tamaiio. Normalmente no es necesario preocuparse por el formato de un archivo de texto porque Text IISAM se fija en las primeras 25 filas para ver si puede determinar el formato por si mismo. Utiliza esta informacion y alguna otra adicional del Registro para decidir como interpretar el archivo y como comportarse. Si tenemos un archivo que no se corresponde con un formato habitual que pueda establecer el Text IISAM, entonces podemos ofrecer esta informacion en forma de un archivo SCHEMA. IN1 situado en el mismo directorio que 10s archivos de texto a 10s que se refiere. Este archivo contiene informacion esquematica, tambien denominada metadatos, sobre cualquier archivo de texto en el mismo directorio (o todos ellos). Cada archivo de texto recibe su propio apartado, identificado mediante el nombre del archivo de texto, como [NightShift .T X T ] . Por lo tanto, podemos especificar el formato del archivo: 10s nombres, tipos y tamaiios de las columnas, cualquier conjunto de caracteres especiales que se vayan a usar y cualquier formato especial para las columnas (corno fecha y hora o moneda). Supongamos que cambiamos el archivo NightShif t .TXT a1 siguiente formato:
Ne o Trinity Morpheus ICincinnati
l London l Milan
En este ejemplo, 10s nombres de columna no se incluyen en el archivo de texto y el delimitador es una barra vertical. Un archivo SCHEMA. IN1 asociado podria parecerse a1 siguiente:
[Nightshift-TXT] Format=Delimited ( l ) ColNameHeader=False Coll=CrewPerson Char Width 10 ColZ=HomeTown Char Width 30
Utilicemos o no un archivo SCHEMA. INI, encontraremos dos limitaciones con el Text IISAM: no se pueden borrar ni editar las filas.
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 ' ) ;
En este caso, la base de datos es el directorio, como para Paradox aunque no para Excel. El nombre de la tabla debe incluir la extension . htm y, por lo tanto, debera ir entre corchetes. Observe que el nombre del controlador IISAM es HTML Export, no solo HTML,ya que este controlador solo puede utilizarse para ex-
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).
Un cursor de servidor lo administra el RDBMS. En una arquitectura clientel senidor fundarnentada en una base de datos como SQL Server, Oracle o InterBase, esto significa que el cursor se administra fisicamente en el servidor. En una base de datos de escritorio como Access o Paradox, la ubicacion del "servidor" es solo una ubicacion logica, puesto que la base de datos se ejecuta en el escritorio. Los cursores de servidor normalmente son mas rapidos de cargar que 10s cursores de cliente porque no se transfieren todos 10s datos al cliente cuando se abre el conjunto de datos. Gracias a esto, son mas apropiados para conjuntos de resultados muy grandes en 10s que el cliente no tenga memoria suficiente para mantener todo el conjunto de resultados en memoria. Con frecuencia, podemos decidir que tipos de caracteristicas estaran disponibles con cada ubicacion de cursor al pensar en el mod0 de funcionamiento del cursor. Un buen ejemplo del mod0 en que sus caracteristicas nos ayudan a decidir el tip0 de cursos es el cierre o bloqueo. Para colocar un cierre sobre un registro hace falta un cursor de servidor, porque debe haber una comunicacion entre la aplicacion y el RDBMS. Otro aspect0 que afectara a la eleccion de la ubicacion del cursor es la escalabilidad. Los cursores de servidor son administrados por el RDBMS. En una base de datos cliente/senidor, este estara ubicado en el servidor. A medida que aumente el numero de usuarios de la aplicacion, la carga del senidor aumenta con cada cursor de senidor. Una mayor carga en el senidor significa que el RDBMS se convierte en un cuello de botella cada vez mas rapido, por lo que la aplicacion resulta menos escalable. Podemos mejorar la escalabilidad utilizando cursores de cliente. El impact0 inicial a1 abrir el cursor es normalmente mas fuerte, porque todos 10s datos se transfieren a1 cliente, per0 el mantenimiento del cursor abierto puede ser menor. Como puede comprobarse, hay muchas cuestiones conflictivas relacionadas con la eleccion de la ubicacion del cursor apropiada para 10s conjuntos de datos.
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
en que se crea un componente dbGo, generalmente se tendra un valor para cada propiedad. como consecuencia, no hay mucha necesidad de tener valores sin especificar en muchos tipos enumerados. Los tipos de cursor afectan al mod0 en que se leen y actualizan 10s datos. Existen cuatro opciones: solo de avance, estatico, conjunto de claves y dinamico. Antes de profundizar mas en todas las combinaciones de ubicaciones de cursor y tipos de cursor, deberia quedar claro que solo hay disponible un tip0 de cursor para 10s cursores de cliente: el cursor estatico. Todos 10s demas tipos de cursor son unicamente para cursores de servidor. Hablaremos mas adelante sobre la disponibilidad de 10s tipos de cursor despues de comentar 10s distintos tipos de cursor, en orden creciente de coste: Cursor s61o de avance: Este tipo de cursor es el menos costoso y, por lo tanto, el tipo con el mejor rendimiento posible. Como su nombre indica, permite desplazarse hacia delante. El cursor lee el numero de registros especificados por Cachesi ze (predefinido como 1) y cada vez que se queda sin registros, lee otro conjunto de CacheSi ze registros. Cualquier intento de desplazarnos hacia atras por el conjunto de resultados mas alla del numero de registros que hay en cache creara un error. Este comportamiento es parecido a1 de un conjunto de datos dbExpress. Un cursor solo de avance no resulta apropiado para se utilizado en la interfaz de usuario en la que el usuario puede controlar la direccion a traves del conjunto de resultados. Sin embargo, si resulta apropiado para las operaciones por lotes, 10s informes y las aplicaciones Web sin estado, porque dichas situaciones comienzan por la parte superior del conjunto de resultados y fimcionan de forma progresiva hasta el final, cerrando a continuacion el conjunto de resultados. Cursor estatico: Un cursor estatico funciona leyendo el conjunto de resultad0 completo y ofreciendo una ventana de registros Caches i ze en el conjunto de resultado. Dado el servidor ha recuperado el conjunto de resultad0 completo, podemos desplazarnos adelante y atras a traves del conjunto de resultados. Sin embargo, a cambio de esta facilidad, 10s datos son estaticos, es decir, las actualizaciones, inserciones y eliminaciones realizadas por otros usuarios no se pueden ver porque ya se han leido 10s datos del cursor. Cursor de conjunto de claves: Un cursor conjunto de claves se comprende mejor si separamos el termino en dos palabras: conjunto y clave. Clave, en este contexto, se refiere a un identificador para cada fila. Normalmente se tratara de una clave primaria. Por ello, un cursor de conjunto de claves es un conjunto de claves. Cuando se abre el conjunto de resultados, se lee la lista completa de claves para el conjunto de resultados. Si, por ejemplo, el conjunto de datos fuese una consulta como SELECT * FROM CUSTOMER, la lista de claves se crearia a partir de SELECT CUSTID FROM CUSTOMER.
Este conjunto de claves se mantiene hasta que se cierra el cursor. Cuando la aplicacion solicita datos, el proveedor OLE DB lee las filas que utilizan las claves del conjunto de claves. Como consecuencia, 10s datos siempre estan actualizados. Si otro usuario cambia una fila del conjunto de resultados, entonces 10s cambios se veran cuando se lean de nuevo 10s datos. Sin embargo, el conjunto de claves, por si mismo, es estatico. se lee solo cuando el conjunto de resultados se abre a1 principio. Por tanto, si otro usuario aiiade registros nuevos, dichas adiciones no se veran. Los registros eliminados resultan inaccesibles y 10s cambios en las claves primarias (algo que no deberia permitirse a 10s usuarios) tambien lo son.
Cursor dinamico: El ultimo tip0 de cursor y el mas costoso es el dinamico. Un cursor dinamico es casi identico a un cursor de conjunto de claves. La unica diferencia es que el conjunto de claves se vuelve a leer cuando la aplicacion solicita datos que no estan en cache. Ya que, de manera predeterminada TADOData Set .Caches ize es 1,dicha solicitud resulta muy frecuente. Puede imaginarse la carga adicional que esto supone para el RDBMS y la red, y es el motivo de que este sea el cursor mas costoso. Sin embargo, el conjunto de resultados puede ver y responder a las adiciones y eliminaciones realizadas por otros usuarios.
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.
El proveedor Oracle OLE DB cambia todos 10s tipos de cursor a solo de avance. El proveedor ODBC OLE DB realiza diversos cambios segun el controlador ODBC en uso.
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
escribimos DESC en mayusculas y no "desc" ni "Desc". Seguro que no resulta sorprendente que 10s datos se ordenen de manera descendente. Esta caracteristica tan simple per0 potente permite resolver algunos de 10s grandes problemas de 10s desarrolladores de bases de datos. Los usuarios podrian pedir algo muy razonable e inevitable, si se podria hacer clic sobre las columnas de la cuadricula para ordenar 10s datos. Las respuestas como sustituir las cuadriculas por controles que no Sean sensibles a 10s datos como ListView que tengan incluida la capacidad de ordenacion, o como capturar el evento O n T i t l e C l i c k del componente DBGrid y rehacer la sentencia SQL SELECT tras incluir una clausula ORDER BY apropiada no son nada satisfactorias. Si se tienen 10s datos en la cache del cliente (corno ya se ha visto durante el uso del componente ClientDataSet), se puede usar un indice de cliente calculado en memoria. Podemos ariadir el siguiente evento OnT it leC1i c k a la cuadricula:
procedure TForml.DBGridlTitleClick(Column: TColumn); begin if ADODataSetl.IndexFieldNames = Column.Field.FieldName then
ADODataSet1.IndexFieldNames
DESC'
: = Column.Field.FieldName
+ '
: = Column.Field.Fie1dName
Este sencillo evento comprueba si el indice actual se ha creado en el mismo campo que la colurnna. De ser asi, se crea un nuevo indice en la colurnna pero en orden descendente. De no ser asi, se crea un indice nuevo en la colurnna. Cuando el usuario hace clic sobre la colurnna por primera vez, se organiza en orden ascendente y cuando hace clic por segunda vez, se organiza en orden descendiente. Podriamos ampliar este comportamiento para permitir que el usuario mantenga pulsada la tecla Control y haga clic sobre varios titulos de columnas para crear indices mas complejos.
NOTA: Todo esto se puede conseguir utilizando Cl i e n t Da t a s e t , per0 esa solucion no es tan elegante por dos razones. C l i e n t D a t a s e t no soporta la palabra clave DESC,por lo que habra que crear un elemento de conjunto de indices para conseguir un indice descendente, algo que requiere rnis c d i g o . Ademb, cuando se cambia un indice ascendente por una version desckdente, el C l i e n t D a t a S e t re:construye el indice, una operacion innecesaria y posiblemente lent..
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
mas potentes y fiables. Una de estas caracteristicas es la replicacion o clonacion. Un conjunto de registros clonado es un nuevo conjunto de registros que posee las mismas propiedades que el original a partir del que se ha clonado. Vamos a ver en primer lugar como crear un clon y despuks veremos porqud son tan utiles.
Se puede clonar un conjunto de registros (0; en terminologia dbGo, un conjunto de datos) usando el metodo Clone. Podemos clonar cualquier conjunto de datos ADO. per0 en este ejemplo utilizaremos ADOTable. El ejernplo DataClone (vease figura 15.6) utiliza dos componentes ADOTable, uno conectado con la basc de datos y otro vacio. Ambos conjuntos de datos estan conectados a un Datasource y a una cuadricula. Una sola linea de codigo, ejecutada cuando el usuario haga clic sobre el boton, replicara el conjunto de datos:
1354 Cayman Divers Wolld U4rnited 1356 Torn S a r y n D~ving Centre 13XJ BheJadt Aqua Ceder 1384 VIP Dive~s Club 1510 O n m Para& 1513 FmtastiqueAqu&ia 1551 Marmd D k s Club 1560 TheDeplhCha~c 1563 BlueSpuls 1624 Makai SCUBA Cbb 1645 A l C b cm h
1380 BlueJack Aqua Cenlu 1384 VlPDEverrClub 1513 Fanlasltql~e Aqud~w 1551 Marmot Dtvers Club 1560 The Deplh Cha~ge
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:
Con este programa, se pueden realizar cambios sobre la tabla de la base de datos y despues descartarlos, y se dara marcha atras como es de esperar. Resaltamos este punto porque el soporte de transacciones varia segun la base de datos y el proveedor OLE DB que se utilice. Por ejemplo, si se realiza una conesion con Paradox empleando el proveedor ODBC OLE DB Driver; sencillamente se recibira un error que indica que la base de datos o el proveedor OLE DB no es capaz de iniciar una transaccion. Podemos averiguar el nivel de soporte de procesamiento de transacciones usando la propiedad dinamica T r a n s a c t i o n D D L de la conexion:
if ADOConnectionl .Properties [ ' Transaction DDL' ] .Value > DBPROPVAL-TC-NONE then ADOConnection1.BeginTrans;
Si tratamos de acceder a 10s mismos datos de Paradox empleando el proveedor Jet 4.0 OLE DB, no se vera el error, per0 tampoco se podran deshacer 10s cam-
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-
que este seria un nombre mas logico). Cuando se incluye xaComitRetaining en Attributes y se confirma una transaccion, se inicia automaticamente una nueva transaccion. Cuando se incluye xaAbortRe taining en Attributes y se deshace una transaccion, se inicia automaticamente una nueva transaccion. Esto significa que si incluimos estos valores en Attributes,siempre habra una transaccion en marcha, porque cuando finalizamos una transaccion siempre se inicia otra nueva. La mayoria de 10s programadores prefiere tener un mayor control sobre sus transacciones y no permitirles que se inicien automaticamente, por lo que estos valores no suelen usarse. Sin embargo, poseen especial importancia en relacion con las transacciones anidadas. Si anidamos una transaccion y definimos Attributes como [xaComitRetaining, xaAbortRetaining1,la transaccion externa nunca se puede finalizar. Veamos esta secuencia de eventos:
La transaccion externa no acaba nunca porque cuando una interna finaliza se iniciara una nueva transaccion. Como conclusion, el uso de la propiedad Attributes y el uso de transacciones anidadas deberian resultar mutuamente exclusivos.
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.
TRUCO: Si siente curiosidad sobre las sentencias SQL que se generan, y usa SQL Server, se pueden ver estas sentencias mediante el SQL Server
Profiler.
A pesar de entender como funciona este proceso, una forma mejor de enfocar el problema es desde la perspectiva del usuario. Desde su punto de vista, cuando se elimina una fila de la cuadricula, casi seguro que el 99 por ciento de 10s usuarios espera eliminar el pedido, no el pedido y el cliente. Afortunadamente, podemos conseguir esactamente esto mediante otra propiedad dinamica, en este caso, la propiedad dinamica Unique Table. Podemos especificar que las eliminaciones se refieren solo a la tabla Orders y no a Customer mediante el siguiente codigo:
A D O Q u e r y l . P r o p e r t i e s [ ' U n i q u e T a b l e ' ] .Value
:=
' Products'
Puesto que este valor no se puede asignar en tiempo de diseiio, la siguiente mejor alternativa consiste en colocar esta linea en el evento Oncreate del formulario.
motor de cursor de ADO. A partir de aqui, 10s cambios se realizan en un "delta" (es decir, una lista incremental de cambios). El conjunto de datos se comporta para todo proposito como si 10s datos hubieran cambiado, per0 10s cambios solo se han realizado en memoria; no se han aplicado a la base de datos. Para que 10s cambios Sean permanentes, utilizamos U p d a t e Bat c h (equivalente a ApplyUpdat e s en actualizaciones mediante cache):
Para rechazar el lote completo de actualizaciones, usamos CancelBatch o Cance lUpdate s. Existen muchas similitudes en 10s nombres de metodos y propiedades entre las actualizaciones por lotes de ADO y las actualizaciones mediante cache del BDE y ClientDataSet. Update Status, por ejemplo, se puede utilizar exactamente del mismo mod0 que para las actualizaciones mediante cache para identificar registros segun hayan sido insertados, actualizados, eliminados o no modificados. Esto resulta particularmente util para resaltar registros con distintos colores en una cuadricula o mostrar su estado en una barra de estado. Algunas diferencias entre las sintaxis son muy leves, como el cambio de RevertRecord por CancelBatch (arcurrent) Otras son mas compli. cadas. Una caracteristica util de las actualizaciones mediante cache que no esta presente en las actualizaciones por lotes de ADO es la propiedad updatesPending de un conjunto de datos. Dicha propiedad es verdadera si se han realizado cambios per0 aun no se han aplicado. Esto resulta especialmente util en el evento OnCloseQuery de un formulario:
procedure 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;
Sin embargo, con un poco de conocimiento y un poco de ingenio podemos implementar una funcion ADOUpda te sPending apropiada. El conocimiento necesario es que 10s conjuntos de datos de ADO poseen una propiedad llamada FilterGroup,que es una especie de filtro. A diferencia de la propiedad Filter de un conjunto de datos, que filtra 10s datos basandose en una comparacion de 10s datos con una condicion, Fi1terGroup filtra basandose en el estado del registro. Uno de esos estados es fgPendingRecords,que incluye todos 10s registros que hemos modificado per0 cuyos cambios no hemos aplicado aun. Por eso, para permitir que el usuario vea todos 10s cambios que se han realizado hasta ahora, solo hay que ejecutar dos lineas:
El conjunto de resultados incluira ahora los registros que se hayan eliminado. El efecto es que se vera que 10s campos se dejan en blanco, lo que no es de mucha ayuda porque no se puede saber que registros se han borrado. (El comportamiento de la primera version de ADOExpress no era este, si no que se mostraban 10s valores de 10s campos de 10s registros eliminados.) El ingenio necesario para resolver el problema de Updatespending implica a de 10s clones, que mencionamos antes. La idea de la funcion ADOUpdate spending es que definira el FilterGroup para restringir el conjunto de datos a solo aquellos cambios que todavia no se hayan aplicado. Todo lo que tenemos que hacer es ver si existen registros en el conjunto de datos despues de aplicar FilterGroup. Si 10s hay, significa que hay actualizaciones pendientes. Sin embargo, si hacemos esto con el conjunto de datos real, la definicion de FilterGroup desplazara el punter0 de registro y se actualizara la interfaz de usuario. La mejor solucion consiste en utilizar un clon:
function ADOUpdatesPending(AD0DataSet: TCustomADODataSet) : boolean; var Clone: TADODataSet; begin Clone : = TADODataSet. Create (nil); try Clone.Clone(AD0DataSet); Clone.FilterGroup : = fgPendingRecords; Clone.Filtered : = True; Result : = not (Clone.BOF a n d Clone. EOF) ; Clone. Close; finally Clone. Free; end; end;
En esta funcion, clonamos el conjunto de datos original, configuramos Fi 1terGroup y verificamos si el conjunto de datos se encuentra a1 principio del archivo y tambien a1 final del mismo. De ser asi, no hay registros pendientes.
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.
3. Ahora, en la segunda copia del programa, buscamos el mismo registro y cambiamos el nombre de la empresa a Bottom-Pound Markets.
4. Salimos del registro y hacemos clic sobre el boton para actualizar el lote. No funcionara.
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"
El numero de registros afectados por esta sentencia de actualizacion se espera que sea uno, porque se busca el registro original utilizando la clave primaria y el contenido del campo CompanyName tal como estaba cuando el registro se ley6 por primera vez. En este ejemplo, sin embargo, el numero de registros afectados por la sentencia UPDATE es cero. Esto solo puede ocurrir si se ha eliminado el registro, ha cambiado la clave primaria del registro o el campo que estamos modificando fue modificado por otra persona. Por lo tanto, la actualizacion no se realiza. Si nuestro "segundo usuario" hubiera cambiado el campo ContactName y no el campo CompanyName,la sentencia UPDATE se habria parecido a esta:
UPDATE CUSTOMER SET ContactName="Liz Lincoln" WHERE CustomerID="BOTTM" AND ContactName="Elizabeth Lincoln"
En nuestro caso, esta sentencia habria funcionado porque el otro usuario no cambio la clave primaria ni el nombre de contacto. Este comportamiento es similar a1 comportamiento del BDE con el mod0 de actualizacion en caso de cambios. La propiedad UpdateMode del BDE en ADO se sustituye por la propiedad dinamica Update Criteria de un conjunto de datos. La siguiente tabla muestra 10s valores posibles que se pueden asignar a esta propiedad dinamica:
Solo columnas de d a v e primaria. Todas las columnas. Solo columnas de clave primaria y columnas modificada. Solo columnas de clave primaria y columna de marca temporal de ljltima modificacion.
No hay que caer en el error de pensar que una de estas configuraciones es mejor que otra para toda la aplicacion. En la practica, la eleccion dependera del contenido de cada tabla. Digamos que la tabla Customer solo posee 10s campos CustomerID,N a m e y City. En este caso, la actualizacion de cualquiera de estos campos es logicamente no mutuamente excluyente con respecto a la actualizacion de cualquier otro campo, por lo que una buena eleccion seria adCriteriaUpdCols (la opcion predefinida). Sin embargo, si la tabla Customer incluyera un campo Post alCode,entonces la actualizacion de un campo Postalcode seria mutuamente escluyente con la actualizacion del campo City por parte de otro usuario (porque si cambia la ciudad, seguramente tambien deberia cambiar el codigo postal y viceversa). En este caso, podriamos argumentar que adCr iteriaAl1Co 1s seria una solucion mas segura.
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.
En el caso de cada registro que haya fallado, podemos informar a1 usuario de tres elementos de informacion criticos sobre cada campo mediante las siguientes propiedades T Fie 1d :
Newvalue curvalue
El valor al que el usuario ha cambiado el registro. El nuevo valor obtenido de la base de datos. El valor cuando se ley6 por primera vez de la base de datos.
I OldValue
II
Los usuarios del componente C 1 e n t Da t a Se t conoceran ya el practico i dialog0 T R e c o n c i l e E r r o r Form, que envuelve el proceso de mostrar a1 usuario 10s registros nuevos y 10s antiguos y les permite especificar que accion realizar. Por desgracia, no existe un equivalente en ADO para este formulario y T R e c o n c i l e E r r o r F o r m se ha escrito teniendo tan P r e s e n t e C l i e n t D a t a S e t que es dificil usarlo para conjuntos ADO. Hay que tener en cuenta una cosa mas acerca del uso de estas propiedades T F i e l d : se toman directamente de 10s objetos Field de ADO subyacentes a 10s que hacen referencia. Esto significa que se depende del soporte del proveedor OLE DB para las caracteristicas que se espera utilizar. Todo suele hncionar bien con la mayoria de 10s proveedores, per0 el proveedor Jet OLE DB devuelve el mismo valor para C u r V a l u e y O l d v a l u e . En otras palabras, si utilizamos Jet, no podemos determinar el valor a que cambio el campo el otro usuario a menos que recurramos a medidas propias. Sin embargo, mediante el proveedor OLE DB para SQL Server, se puede acceder a c u r v a l u e solo despues de llamar a1 metodo R e s y n c del conjunto de datos con el parametro A f f e c t R e c o r d s configurado como addAf f e c t G r o u p y R e s y n c v a l u e s como a d R e s y n c U n d e r 1 i ngVa l u e s ; como en el siguiente fragment0 de codigo: y
adoCustomers.FilterGroup : = fgConflictingRecords; adoCustomers.Filtered : = true; adoCustomers.Recordset.Resync(adAffectGroup, adResyncUnderlyingValues);
nil;
A partir de ahi, el conjunto de registro seguira teniendo 10s mismos datos, soportando las mismas hnciones de navegacion y permitira que se aiiadan registros, se editen y se eliminen. La unica diferencia relevante es que no podemos actualizar el lote porque es necesario que este conectado a1 servidor para actualizar el servidor.
Para volver a establecer la conexion (y usar UpdateBatch), podemos usar este codigo:
ADODataSetl.Connection : = ADOConnectionl;
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
ha de confundir con el pooling de sesiones, permite que las conesiones a la base de datos se reutilicen despues de que hayan finalizado. Esto se realiza de forma automatica y, si nuestro proveedor OLE DB la soporta y esta activada, no es necesario hacer nada para beneficiarnos del pooling de conesiones. Existe una unica razon por la que querriamos utilizar esta tecnica para nuestras conesiones: el rendimiento. El problema de las conexiones a bases de datos esta en que puede llevar cierto tiempo establecer una conexion. En una base de datos de escritorio como Access, esto se reduce normalmente a un corto period0 de tiempo. En una base de datos clientelservidor como Oracle, que se utiliza en una red, este tiempo podria medirse en segundos. Tiene sentido promover la reutilizacion de este tipo de recurso tan costoso (en cuanto a rendimiento). Si activamos la fusion de conexiones de ADO, 10s objetos Connection de ADO se colocan en una cola cuando la aplicacion 10s "destruye". Los subsiguientes intentos de crear una conesion ADO buscaran automaticamente en la cola de conesion una conexion con la misma cadena de conesion. Si se encuentra una conesion apropiada, se reutiliza. De no ser asi, se crea una nueva conexion. Las propias conesiones permanecen en cola hasta que se reutilizan, se cierra la aplicacion o expiran. De manera predeterminada, las conesiones expiraran despues de 60 segundos, pero a partir de MDAC 2.5 podemos configurar este tiempo utilizando la clave de registro HKEY C L A S S E S R O O T \ C L S I D \ < Provid e r C L S I D > \ S PT i r n e o u t . El procesi de poolingde conesion tiene lugar de forma homogenea, sin la intervencion ni el conocimiento del desarrollador. Este proceso es similar alpooling de bases de datos del BDE en Microsoft Transaction Server (MTS) y COM+, con la importante escepcion de que ADO realiza su propio pooling de conexiones sin la ayuda de MTS ni de COM+. De manera predeterminada, elpooling de conexiones esta activado en 10s proveedores MDAC OLE DB para bases de datos relacionales (como SQL Server y Oracle), con la notable excepcion del proveedor Jet OLE DB. Si usamos ODBC, deberiamos escoger entre el pooling de conexiones de ODBC y el de ADO, per0 no deberiamos utilizar ambos. A partir de MDAC 2.1, el pooling de conesiones de ADO esta activado y el de ODBC esta desactivado.
NOTA: Elpooling de conexiones no se realiza en Windows 95 a pesar del proveedor OLE DB.
Para que encontrarnos realmente comodos con el pooling de conesiones, sera necesario ver como se realiza el pooling y como expiran. Lamentablemente, no existen herramientas de analisis depooling de conesiones para ADO, per0 podemos utilizar el Performance Monitor de SQL Server que puede espiar con precision las conexiones de bases de datos SQL Server. Podemos activar o desactivar el pooling de conexion en el Registro o en la cadena de conexion. La clave del Registro es OLEDB S E R V I C E S y puede enEs . contrarse en HKEY -C L A S S E S R O O T \ C L S I D \ < P ~ & ~ ~ ~ ~ C L S I D > una
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).
En tercer lugar, el formato XML de ADO es especifico de Microsoft, a1 igual que la mayoria de las implementaciones XML de las empresas. Esto significa que el XML producido en ADO, noes legible por el ClientDataSet de Borland, y viceversa. Afortunadamente, este ultimo problema se puede superar utilizando el nuevo componente XMLTransform de Delphi, que se puede usar para traducir entre las diferentes estructuras XML. Si queremos usar estas caracteristicas solamente para aplicaciones de una capa y no como parte del modelo de maletin, podemos ahorrar esfuerzos utilizando un componente ADODataSet y definiendo su propiedad ComrnandType como c m d F i l e y su C o r n m a n d T e x t como el nombre del archivo. Esto nos ahorrara el esfuerzo de tener que llamar a L o a d F r o m F i l e de forma manual. Sin embargo, tendremos que llamar a S a v e T o F i l e . En una aplicacion de maletin, esta tecnica es demasiado restrictiva, puesto que el conjunto de datos se puede usar de dos formas distintas.
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.
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
muy elevada, que a lo largo del tiempo se fue reduciendo significativamente). Esta nueva licencia de desarrollo aumentara con toda seguridad el atractivo de DataSnap para 10s desarrolladores, que es una buena razon para comentar esta herramienta con algo de detalle. Este capitulo trata 10s siguientes temas: Arquitectura logica de tres capas. Fundamento tecnico de DataSnap. Los protocolos de conexion y 10s paquetes de datos. Componentes de soporte de Delphi (de cliente y de sewidor). El agente de conexion y otras caracteristicas extendidas.
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 precisamente el objetivo de 10s m6due
10s de datos remotos introducidos en Delphi 3 . Los modulos de datos remotos se ejecutan en un ordenador servidor, llamado generalmente servidor de aplicacion. Por su parte, este se comunica con el RDBMS (que puede ejecutarse en el servidor de aplicacion o en otro ordenador dedicado a ese fin). Asi, las maquinas cliente no se conectan directamente a1 sewidor SQL, sino de forma indirecta a traves del servidor de aplicacion. En este punto se plantea una cuestion importante: ~ S i g u e siendo necesario instalar el software de acceso a la base de datos? La tradicional arquitectura clientelservidor de Delphi (incluso con tres capas logicas) requiere la instalacion de dicho software en cada cliente, lo que puede resultar bastante problematico cuando la cantidad de ordenadores a configurar y mantener es elevada. En la arquitectura de tres capas fisica, basta con instalar y configurar el software de acceso solo el servidor de aplicacion, no en las maquinas cliente. Ya que 10s programas cliente contienen tan solo el codigo de la interfaz de usuario y su instalacion es realmente sencilla, ahora entran en la categoria denominada clientes ligeros. Para usar el lenguaje del mercado, podriamos llamar a esto una arquitectura de clientes ligeros de configuracion cero. Pero sera mejor que nos dediquemos a cuestiones tecnicas en lugar de a la terminologia del mercado.
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
IAppServer,ya que Delphi incluye componentes que implementan esta interfaz en las aplicaciones del lado del servidor y componentes que llaman a la interfaz en las aplicaciones del cliente. Estos componentes simplifican el soporte de la interfaz IAppServer y en ocasiones incluso la ocultan por completo. En la practica, el servidor pondra a disposicion del cliente ob.jetos que implementen esta interfaz, posiblemente junto con otras interfaces personalizadas.
Listado 16.1. La definicion de la interfaz IAppServer.
type IAppServer = interface (IDispatch)
[
' /lAEPCC20-
7 A 2 4 - 1 lD2-9SBO-C69BEB+'B5B6D/
']
function AS-Applyupdates (const ProviderName: WideString; Delta: OleVariant; MaxErrors: Integer; out Errorcount: Integer; var Ownerdata: OleVarlant): OleVariant; safecall; function AS-GetRecords (const ProviderName: WideString; Count: Integer; out RecsOut: Integer; Options: Integer; const CommandText: WideString; var Params: OleVariant; var OwnerData: OleVariant): OleVarlant; safecall; function AS-DataRequest(c0nst ProviderName: WideString; Data: OleVariant): OleVariant; safecall; function AS-GetProviderNames: OleVariant; safecall; function AS-GetParams(const ProviderName: Widestring; var Ownerdata: OleVariant): OleVariant; safecall; function AS-RowRequest(const ProviderName: WideString; Row: OleVariant; RequestTpe: Integer; var OwnerData: OleVariant): OleVariant; safecall; procedure AS-Execute(const ProviderName: WideString; const CommandText: WideString; var Params: OleVariant; var OwnerData: OleVariant); safecall; end;
--
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:
Distributed COM (DCOM) y Stateless COM (MTS o COM+): DCOM esta disponible directamente en Windows NT/2000/XP y 98/Me, y no necesita aplicaciones en tiempo de e.jecucion adicionales en el servidor. DCOM
es basicamente una ampliacion de la tecnologia COM; que permite a las aplicaciones cliente utilizar objetos de servidor que ya existen y ejecutar10s en un ordenador aparte. La infraestructura DCOM permite la utilizacion de objetos COM sin estado (stateless), disponibles en las arquitecturas de COM+ y de versiones anteriores de MTS (Microsoft Transaction Sewer). COM+ y MTS ofrecen ciertas caracteristicas como seguridad, capacidad de gestion de componentes y transacciones de bases de datos y estan disponibles en Windows NTl2000lXP y en Windows 98lMe. Debido a la complejidad de configuracion de DCOM y sus problemas a la hora de atravesar cortafuegos, incluso Microsoft trata de abandonar DCOM en favor de soluciones basadas en SOAP. Sockets TCPIIP: Estan disponibles en la mayoria de 10s sistemas. Usando TCPIIP, podremos distribuir 10s clientes por la toda la Web, donde DCOM no siempre es aplicable, y podemos ahorrarnos problemas de configuracion. Para utilizar 10s sockets, el ordenador de la capa intermedia debe ejecutar la aplicacion scktsrvr . e x e proporcionada por Borland: un sencillo programa ejecutable como aplicacion o como servicio. Este programa recibe las peticiones del cliente y las reenvia a1 modulo de datos remoto (que se ejecuta en el mismo servidor) a traves de COM. Los sockets no ofrecen ninguna proteccion frente a 10s posibles errores que puedan surgir en el cliente, ya que el servidor no recibe informacion y podria no liberar recursos~cuando cliente se desconecte de forma inesperada. un H T T P y SOAP: El uso de HTTP como protocolo de transporte de Internet simplifica las conesiones a traves de 10s cortafuegos o 10s servidores intermedios o prosy (que no suelen gustar de 10s sockets TCPIIP personalizados). Se requiere una aplicacion de servidor Web especifica, h t tpsrvr dl 1; que acepte las solicitudes de 10s clientes y Cree 10s modulos de datos remotos pertinentes mediante COM. Estas conexiones Web tambien pueden emplear la seguridad SSL. Ademas, las conexiones Web basadas en el transporte HTTP pueden usar el soporte de interconesion de objetos de DataSnap.
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.
Debido a problemas de compatibilidad con las nuevas versiones de la solucion CROBA de Borland, VisiBroker, esta caracteristica ha dejado de estar presente en Delphi 7. Por ultimo, hay que fijarse en que como extension de esta arquitectura se pueden convertir 10s paquetes de datos a XML y enviarlos a un navegador Web. En este caso, solo se contara con una unica capa adicional: el servidor Web toma 10s datos de la capa intermedia y 10s hace llegar a1 cliente. En un capitulo posterior hablaremos de esta arquitectura, llamada Internet Express.
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: Es fundamental recordar que 10s paquetes de datos no estan sujetos a ningun protocolo. Se tratan unicamente de una secuencia de bytes, de mod0 que alla donde se pueda transferir una secuencia de bytes, se podra asimismo enviar un paquete de datos. Se proporciono esta prestacion para que la arquitectura resultara adecuada para multiples protocolos de transporte (inclusive DCOM,HTTP y TCPIIP) y para multiples plataformas.
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.
EI componente SharedConnection: Puede utilizarse para conectar con un modulo de datos secundario (tambien denominado "hijo") de una aplicacion remota, adhiriendose a una conesion fisica ya esistente con el modulo de datos principal. Dicho de otra forma, una aplicacion puede conectarse a varios modulos de datos del servidor a traves de una unica conesion compartida.
El componente LocalConnection: Puede utilizarse para atacar a un proveedor local de conjuntos de datos como fuente del paquete de datos. Conectando directamente el ClientDataSet al proveedor se lograrh el mismo efecto. No obstante, a1 usar Localconnection se puede escribir una aplicacion local con el mismo codigo que una aplicacion multicapa completa, mediante la interfaz IAppServer de la conesion "ficticia". A consecuencia de esto, el programa sera mas facil de ampliar, en comparacion con un programa de conexion directa.
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~uliile Instance
AI
Ademas de tomar muchos elementos de la clase baseTRemoteDataModule, esta clase implementa la interfaz personalizada IAppServerOne, que se deriva de la interfaz estandar de DataSnap (IAppServer). Por otra parte, tambien sobrescribe el metodo U p d a t eReg is t r y para aiiadir el soporte necesario de cara a1 transporte mediante sockets y Web, como puede observarse en el codigo generado por el asistente. A1 final de la unidad, se encontrara la declaracion de la fabrica de clase:
initialization TComponentFactory.Create(ComServer, TAppServerOne, Class-AppServerOne, ciMultiInstance, tmApartment); end.
Ahora se puede aiiadir un componente de conjunto de datos a1 modulo de datos (en el ejemplo se ha utilizado el SQLData s e t de dbEspress). Se conecta a una base de datos y a una tabla o consults, se activa, se aiiade un DataSetProvider y, por ultimo, se enlaza con el componente de conjunto de datos. El resultado sera un archivo DFM como este:
object AppServerOne: TAppServerOne object SQLConnectionl: TSQLConnection ConnectionName = ' I B L o c d l l LoginPrompt = False end object SQLDataSetl: TSQLDataSet SQLConnection = SQLConnectionl CommandText = 'select * f r o m EMPLOYEE' end
El formulario principal dc cste programa es casi completamente inutil, de mod0 que sc puede simplemente insertar una etiqueta para indicar que se trata del formulario de la aplicacion del servidor. Cuando se construya el servidor, deberia compilarse y c.jecutarse una vez. Esta operacion lo registrars automaticamente como servidor de Automatizacion en el sistema, a disposicion de las aplicaciones clicnte. Por supuesto, el servidor deberia registrase en el ordenador en el que se quiera ejecutar, ya sea el del cliente o el de la capa intermedia.
TRUCO: Por regla general, en 1a fase (je diseiio, la propiedad Con~ n e c t e d .a-I -- - r t i ~ e UGI c o ~ ~ ~ p u u ~ u r u r n ~ u c;l ~ o n deberia permanecer coma False, u~ c 1 . . rnc~uso un oraenador en el para asi poder abrir el proyecto en De~pn~, en que a h no estt registrado el servidor DataSnap.
&-
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.
IFULL-WE
Pidsan, Robnt Yang, B~uce Lambert, Km i
m1 1 1
28/12
-E 2 b ~ ~ ~ ~ m w e c & o n ~
5 Kim
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
Johnson, Lesb
- 622 Clia;DaaSdl
Forest, Phil
Wedm, K. J
"
De Swza. R o w B a l k . Jan&
Figura 16.2. Al activar un componente ClientDataSet conectado a un modulo de datos remoto durantela fase de diseiio, 10s datos del servidor se mostraran de la forrna habitual.
-OOOOOlOOA,?7B)
'
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
La expresion 'x <> '#39#39 es la transposicion DFM de la cadena x <> ",que indica que no se desea recibir una cadena vacia. La restriccion final, 'not x is null', acepta en cambio cadenas vacias, per0 no valores nulos.
Con esta configuracion, se puede actualizar la capa intermedia de la misma forma en que se configuran 10s campos de las aplicaciones servidortcliente estandar. Este enfoque agiliza a su vez el paso de aplicaciones existentes desde una arquitectura clientetservidor a una arquitectura multicapa. El principal inconveniente que presenta el envio de campos a1 cliente es que transmitir toda la informacion adicional consume tiempo. Si se desactiva poIncFieldProps,se puede experimentar una notable mejora en el rendimiento en red de 10s conjuntos de datos dotados de muchas columnas. Por lo general un servidor puede filtrar 10s campos deweltos a1 cliente; realiza esta operacion declarando objetos de campo persistentes con el editor Fields y omitiendo algunos de 10s campos. Es posible que uno de 10s campos filtrados sea necesario para identificar el registro en futuras actualizaciones (si el campo forma parte de la clave primaria), por lo que tambien se puede utilizar la propiedad Provider Flags del campo en el servidor para enviar el valor de campo a1 cliente, per0 evitar que este disponible para el componente ClientDataSet (de esta forma, se conseguira algo de seguridad adicional, en comparacion con lo que supondria enviar el campo a1 cliente y luego ocultarlo en dicho entorno).
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.
El servidor tambien proporcionara un valor predefinido para el campo Salary (de salario) de un nuevo registro y transmitira el valor de su caracteristica Display Format. En la figura 16.3 puede verse uno de 10s mensajes de error que puede mostrar esta aplicacion cliente, que a su vez recibe el servidor. Este mensaje se muestra durante la edicion local de 10s datos, no cuando se remiten 10s datos a1 servidor.
Update
Snapshot.
Reload.
Show Delta
).
1Status
usunmodified
( FIRST-NAME
121 Roberto
0
L"* ,~!s.Jus.s-w
99912 is not a vaM value fa fed 'EMPMPNONO. allowed range is -32768to 32767. il The
...............................,
,
kodified
---
,-
-. -. .--.
621
99912
Bruce
-
28/12/1988
Figura 16.3. Mensaje de error mostrado por el ejemplo ThinCli2 cuando el identificador del empleado es demasiado largo.
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.
4. En caso de producirse un error de servidor, el proveedor lanzara el evento OnUpda t e E r r o r (en la capa intermedia), dando a1 programa la oportunidad de solucionar el error a dicho nivel. 5. Si el programa de la capa intermedia no soluciona el error, la peticion de actualizacion correspondiente permanecera en el delta. El error se devuelve a1 cliente en ese precis0 instante o una vez alcanzado un determinado numero de errores, segun el valor del parametroMaxErrors de la llamada A p p l y U p d a t e s .
6. Por ultimo, el paquete delta que incluya las actualizaciones restantes se reenvia a1 cliente, lanzando el evento 0 n R e c o n c i 1e E r r o r del ClientDataSet para cada una de estas actualizaciones. En este controlador de eventos, el programa cliente puede tratar de solucionar el problema (posiblemente solicitando ayuda a1 usuario), modificando la actualizacion en el delta y luego volviendola a enviar.
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 refrescara el registro actual, per0 e 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;
Cuando 10s datos se ven sometidos a modificaciones frecuentes efectuadas por multiples usuarios y cada usuario deba tener constancia de 10s cambios inmediatamente, deberia aplicarse inmediatamente cualquier rnodificacion en 10s metodos A f t e r P o s t y A f t e r D e l e t e , y llamar despues a1 m e t o d o R e f r e s h ~ e c o r d s para el registro activo (como se mostro anteriormente) o para cada uno de 10s
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.
IFIRST-NAMEIHIREDATE
2 Robe11 4 Bruce 28112/1988 2811211988 6/2/1989 5/4/1989 171411989 17/1/1990 1/St1 990
I~q.1
- 600
621
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.
Esta operacion se ha llevado a cab0 aiiadiendo un boton a1 ejemplo ClientRefresh. El controlador del boton del registro actual a1 primer registro visible de la cuadricula y luego al ultimo registro visible. Esto se logra teniendo en cuenta que hay R o w C o u n t - 1 filas visibles, partiendo de que la primera fila es la que se ha fijado para albergar 10s nombres de campo. El programa no llama el metodo R e f res h R e c o r d cada vez, ya que cada movimiento desencadenaria un evento A f t e r S c r o l l dotado con el codigo mostrado mas arriba. Este codigo refresca las filas visibles, algo que podria desencadenarse mediante un temporizador:
/ / arreglo de acceso protegido
type TMyGrid end ;
=
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
// 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
de un registro en el entorno del cliente. La ultima caracteristica se logra mediante esta llamada:
p r o c e d u r e TClientForm.ButtonFetchClick(Sender: TObject); begin B u t t o n F e t c h - C a p t i o n : = IntToStr (cds.GetNextPacket); end;
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
Para fijar el tip0 y el valor predeterminado del parametro usamos la propiedad Params. En el entorno del cliente, puede utilizar la orden Fetch Params del menu de metodo abreviado del ClientDataSet, despues de haberlo conectado a1 proveedor adecuado. En tiempo de ejecucion, se puede llamar a1 metodo Fet chParams equivalente del componente ClientDataSet . Ahora, a1 actuar sobre la propiedad Params se puede dotar a1 p a r h e t r o de un valor local predefinido. El valor del parametro se enviara a la capa intermedia a1 realizar la extraccion de datos. El ejemplo ThinPlus refresca el parametro con el siguiente codigo:
p r o c e d u r e TFormQuery.btnParamClick(Sender: TObject); begin cdsQuery-Close; cdsQuery.Params[O].AsString : = EditParam.Text; cdsQuery.0pen; end;
La figura 16.5 muestra el formulario secundario de este ejemplo, en el que se muestra el resultado de la consulta por parametros en una cuadricula. En la figura, se aprecian ademas una serie de datos personalizados enviados por el servidor tal y como se explica mas adelante.
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
e SCUBA Company r 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
exception que se genere en el servidor se reenvia y se muestra automaticamente en el cliente. Asi, cuando un usuario marca la casilla de verificacion Connect, se interrumpe el controlador de eventos utilizado para habilitar 10s conjuntos de datos de cliente, de mod0 que un usuario con una clave de acceso incorrecta no podra acceder a 10s datos.
NOTA: Ademas de llarnadas de mktodo direct0 desde el cliente a1 servidor, tambitn se pueden implementar retrollamadas desde el servidor a1 cliente. Este enfoque puede servir, por ejemplo, para infonnar a cada uno de 10s clientes acerca de eventos esieciflcos. LO; eventos COM son una forma de realizar esto. Como alternativa, puede aiiadirse una nueva interfaz, . - -. . - .. - . .. . implementada por el cliente, que transmits el 0bjet0 de implementacibn a1 servidor. Asi el servidor puede llamar el metodo ubicado en el ordenador cliente. No obstante, las conexiones HTTP no permiten aealizar retrollamadas.
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'
MasterFields = ' C u s t N o ' Mastersource = DataSourceCust TableName = ' ORDERS. DB' end o b j e c t DataSourceCust: TDataSource DataSet = TableCustomer end o b j e c t Providercustomer: TDataSetProvider DataSet = TableCustomer end
En el lado de cliente, la tabla detallada aparecera como campo adicional del ClientDataSet y el control DBGrid la mostrara como una columna adicional con un boton con tres puntos. A1 hacer clic sobre el boton aparecera un formulario secundario con una cuadricula que presenta la tabla de detalle (vease la figura 16.6). Si se necesita crear una interfaz de usuario flexible en el entorno del cliente, mediante la propiedad D a t a S e t F i e l d podra aiiadirse un ClientDataSet secundario conectado a1 campo del conjunto de datos del conjunto de datos maestro. Basta con crear campos permanentes para el ClientDataSet principal y enlazar a continuacion la propiedad:
o b j e c t cdsDet: TClientDataSet DataSetField = cdsTableOrders end
Con este parametro se puede visualizar el conjunto de datos detallados en una DBGrid aparte ubicada como de costumbre en el formulario (la cuadricula inferior de la figura 16.6) o del mod0 que se prefiera. Hay que tener en cuenta que, con esta estructura, las actualizaciones solo afectaran a la tabla maestra y que el servidor deberia tratar la secuencia de actualizacion adecuada incluso en situaciones complejas.
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
conjunto de datos en una cuadricula dentro de una ventana flotante o extraerse rnediante un ClienteDataSet para rnostrarlo en un forrnulario secundario. Generalrnente se hare una de estas cosas, no arnbas.
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.
realizar una descarga de 10s BLOB estableciendo la propiedad FetchOnDemand del ClientDataSet con el valor True o llamando el metodo FetchBlobs para registros especificos. De la misma forma, puede desactivarse la descarga autornatica de datos de registros detallados estableciendo la opcion po FetchDetailsOnDemand. nuevo, el cliente De puede utilizar la propiedad Fe t chOnDemand o llamar el metodo FetchDetails. Cuando se aplica una relacion maestro/detalle, las cascadas pueden controlarse con cualquiera de las dos opciones. El indicador pocascadeDe1etes controla si el proveedor debe borrar 10s registros detallados antes de borrar un registro maestro. Esta opcion puede activarse si el servidor de bases de datos realiza borrados en cascada como parte de su soporte de integridad referencial. Del mismo modo, la opcion poCas cadeupdates puede activarse cuando el servidor pueda realizar automaticamente actualizaciones de valores clave de una relacion maestro/ detalle. Pueden limitarse las operaciones en el entorno del cliente. La opcion mas restrictiva, poReadOnly,inhabilita cualquier actualization. Si se desea dotar a1 usuario de una capacidad de edicion limitada, se puede usar poDisableInserts,poDisableEditsopoDisableDeletes. Se puede usar poAutoRef resh para volver a enviar al-cliente una copia de 10s registros que haya modificado el cliente, lo cual resulta util cuando otros usuarios hayan efectuado simultaneamente cambios compatibles. Tambien se puede especificar la opcion poPropagateChanges para devolver a1 cliente las modificaciones efectuadas en 10s controladores de eventos Bef oreUpdateRecord o AfterUpdateRecord.Esta opcion tambien resulta util cuando se utilizan campos autoincrementales, disparadores y otras tecnicas que modifican 10s datos en el servidor o en la capa intermedia, mas alla de las modificaciones solicitadas desde la capa cliente. Si se desea habilitar a1 cliente para que lleve a cab0 las operaciones, puede activarse la opcion poAllowComrnandText.Esto permitira determinar la consulta o nombre de tabla SQL de la capa intermedia desde el cliente, mediante 10s metodos GetRecords o Execute.
Ademas, si se activa la propiedad LoadBa lanced el componente escogera aleatoriamente uno de 10s servidores; cuando un numero elevado de clientes utilicen la misma configuracion, las conexiones se distribuiran automaticamente entre 10s semidores. Si parece una especie de agente de objetos "para pobres", hay que tener en cuenta que algunos sistemas de balance0 de carga muy costosos no ofrecen mucho mas que esto.
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.
Por supuesto, se pueden pasar propiedades del componente de conjunto de datos relacionado, per0 tambien cualquier otro valor (propiedades ficticias extra). En el ejemplo AppSPlus, se transmite a1 cliente la hora en la que se ejecuto la consulta y sus parametros:
procedure
TAppServerPlus.ProviderQueryGetDataSetProperties(
Properties : = VarArrayCreate([O,l], varvariant); Properties [O] : = VarArrayOf ( [ ' Time', Now, True] ) ; Properties [l] : = VarArrayOf ( [ ' Param', Query. Params [O] .Asstring, False] ) ;
end ;
En el entorno del cliente, el componente C l i e n t D a t a Se t tiene un metodo G e t O p t i o n a 1 a r a m e t e r para obtener el valor de la propiedad adicional P con el nombre indicado. El componente C l i e n t D a t a S e t cuenta ademas con el metodo s e t op t i o na 1 a r a m e t e r para aiiadir mas propiedades a1 conjunto P de datos. Estos valores se grabaran en disco (en el modelo de maletin) e incluso se devolveran a la capa intermedia (a1 establecer como T r u e el componente I n c l u d e I n D e l t a de la matriz de variantes). A continuacion se presenta un ejemplo de la obtencion del conjunto de datos en el codigo anterior:
Caption : = 'Data sent at ' + TimeToStr (TDateTime ( cdsQuery.GetOptionalParam( 'Time' ) ) ) ; Label1 .Caption : = ' Param ' + cdsQuery. GetOptionalParam ( Param') ;
El efecto de este codigo se apreciaba en la figura 16.5. Un enfoque alternativo y mas potente para personalizar el paquete de datos enviado a1 cliente, consiste en tratar el evento O n G e t D a t a del proveedor, que recibe el paquete de datos saliente en forma de conjunto de datos de cliente. Mediante 10s metodos de este conjunto de cliente se pueden editar 10s datos antes de enviarlos a1 cliente. Por ejemplo, se podrian codificar algunos de 10s datos o filtrar 10s registros sensibles.
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.
Delphi utiliza 10s objetos DataSource y DataLink para una comunicacion bidireccional. El conjunto de datos utiliza la conexion para avisar a 10s controles data-aware de que hay nuevos datos disponibles (porque se ha activado el conjunto de datos, o se ha modificado el registro, etc). Los controles data-aware utilizan la conexion para solicitar el valor actual de un campo o actualizarlo, a la vez que avisan a1 conjunto de datos de este evento. Las relaciones entre todos estos componentes son complejas, ya que algunas de las conexiones pueden ser de uno a varios. Por ejemplo, se pueden conectar diversas fuentes de datos a1 mismo conjunto y, generalmente, tenemos varios enlaces de datos a la misma fuente de datos, por la sencilla razon de que se necesita un enlace para cada componente data-aware. Ademas, en la mayoria de 10s casos, conectamos varios controles data-aware a cada fuente de datos.
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;
FocusControl(Field: TFieldRef); virtual; Editingchanged; virtual; LayoutChanged; virtual; ~ e c o r d C h a n g e d ( F i e 1 d : T F i e l d ) ; virtual; UpdateData; virtual;
El metodo privado Da t a E v e n t , una especie de procedimiento ventana para una fiente de datos, llama a todos estos metodos virtuales. Este procedimiento se desencadena mediante varios eventos de datos (que se encuentran definidos en la enumeracion T D a t e E v e n t ) . Estos eventos tienen origen en el conjunto de datos, en 10s campos o en la fiente de datos y generalmente se aplican a1 conjunto de datos. El metodo Da t a E v e n t del componente del conjunto de datos envia 10s eventos a las fuentes de datos conectadas. Cada fiente de datos llama a1 metodo N o t i fy D a t a L i n k s para reenviar el evento a cada enlace de datos conectado y, despues, la fiente de datos activa su propio O n D a t a C h a n g e o un evento
OnUpdateData.
La clase T F i e l d D a t a L i n k tambien contiene las propiedades F i e l d y F i e l d N a m e que permitiran conectar el control data-aware a un campo especifico de un conjunto de datos. El enlace conserva una referencia a1 componente visual actual utilizando la propiedad C o n t r o 1 .
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; procedure TMdDbProgress-SetDataField begin FDataLink-FieldName : = Value; end ; function TMdDbProgress.GetDataSource: begin Result : = FDataLink.DataSource; end; procedure TMdDbProgress.SetDataSource begin FDataLink.DataSource : = Value; end; string;
(Value: string);
TDataSource;
(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 ;
Es en el constructor anterior donde el componente instala uno de sus metodos como controlador de eventos para el enlace de datos. Es aqui donde se encuentra el codigo mas importante del componente. Cada vez que se modifiquen 10s datos, modificaremos la salida de la barra de progreso para reflejar 10s valores del campo actual:
procedure TMdDbProgress-Datachange (Sender: TObject); begin i f (FDataLink.Field <> n i l ) and (FDataLink.Field i s TNumericField) then Position : = FDataLink.Field.As1nteger else Position : = Min; end :
Segun la convencion de 10s controles da/a-aware VCL, si el tipo del campo no es valido, el componente no muestra un mensaje de error, simplemente no permite la salida. Del mismo modo, podriamos querer comprobar el tip0 de campo cuando cl metodo de S e t D a t a F i e l d lo asigna a un control. En la figura 17.1, podemos ver un ejemplo dc la salida de una aplicacion DbProgr, que utiliza tanto una etiqueta como una barra de progreso para mostrar informacion cuantitativa de un pedido (order). Gracias a esta indicacion visual, podemos saltar de un registro a otro y ver 10s pedidos de diversos elementos con facilidad. Una ventaja evidente de este componente es que la aplicacion apenas contiene codigo, ya que todo el codigo importante se encuentra en la unidad MdProgr que define el componente.
Como hemos visto no es dificil escribir un componente data-aware de solo lectura. No obstante, puede resultar estremadamente complejo utilizarlo dentro de un contenedor DBCtrlGrid,
NOTA: Si recordarnos el analisis realizado sobre el mCtodo Not i fi ca tion, podemos preguntarnos quC ocurriria si se destruye la fuente de datns a l a m e hace referencia el cnntrnl dnta-owore ------ -- ..-. Podmna e d a r trannuilns --,
I -
--- - ---
- ----
- - I I -
--
--
- . -----------
- ' -
ya que la hente de datos contiene un destructor que la elimina de sus propios enlaces de datos. Por lo tanto, no es necesario un metodo Notification para 10s controles data-mare, aunque en algunos sitios se sugiera asi y la VCL incluya mucho c6digo inutil de este t i p .
.--a
-_L
-1
A. : .
3-
- 1 1
- -
- -
l ~ v e n l ~ o CUSINO
INmTickelt l ~ m l _ ~ a i dI~y-~e(hod(Card-No
6 7 B 6 3 10 C52 50 DINERS E40 00 DINERS C45.00 DINERS E37.50 DINERS E50 00 DINERS
1-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
controladores de eventos para 10s enlaces de datos. El componente instala estos controladores de eventos en el constructor, que ademas inhabilita el componente:
constructor TMdDbTrack-Create (AOwner: TComponent); begin inherited Create (AOwner); FDataLink : = TFieldDataLink.Create; FDataLink.Contro1 : = Self; FDataLink.0nDataChange : = Datachange; FDataLink.0nUpdateData : = UpdateData; FDataLink.0nActiveChange : = Activechange; Enabled : = False; end;
Todos 10s metodos de adquisicion y establecimiento (get y set) y el controlador d e eventos D a t a C h a n g e son muy parecidos a 10s del componente TMdDbProgress.La unica diferencia radica en que siempre que se modifique la fuente de datos o el campo de datos, el componente comprobara el estado actual para determinar si debe habilitarse a si mismo.
procedure TMdDbTrack.SetDataSource (Value: TDataSource); begin FDataLink.DataSource : = Value; Enabled : = FDataLink.Active and (FDataLink.Field <> nil) and not FDataLink.Field.ReadOnly; end;
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 ;
La parte mas interesante del codigo de este componente es la referente a su interfaz de usuario. Cuando un usuario comienza a mover el control de desplazamiento, el componente debe hacer lo siguiente: poner el conjunto de datos en el mod0 de edicion, dejar que la clase base actualice la posicion del control de desplazamiento y avisar a1 enlace de datos (y por lo tanto a la fuente de datos) de que 10s datos han sido modificados. El codigo es el siguiente
p r o c e d u r e TMdDbTrack.CNHScroll(var Message: TWMHScroll); begin // e n t r a en e l modo e d i c i d n FDataLink.Edit; // a c t u a l i z a 10s d a t o s inherited; // n o t i f i c a a 1 s i s t e m FDataLink-Modified; end : p r o c e d u r e TMdDbTrack.CNVScroll(var Message: TWMVScroll); begin // e n t r a e n e l m o d o e d i c i d n FDataLink.Edit; // a c t u a l i z a 1 0 s d a t o s inherited; // n o t i f i c a a 1 s i s t e m FDataLink-Modified; 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 ;
Si se dan las condiciones adecuadas, el componente simplemente actualiza 10s datos en el campo apropiado de la tabla. Por ultimo, si el componente pierde el foco de entrada, deberia forzar una actualizacion de 10s datos (si 10s datos han cambiado), de tal forma que cualquier otro componente data-aware que muestre el valor de ese campo mostrara 10s valores correctos tan pronto como el usuario se desplace a un campo diferente. Si 10s datos no hubiesen cambiado, el componente no se molestara en actualizar 10s datos de la tabla. Este es el codigo estandar de CmExit para componentes utilizados por la VCL y que nuestro componente tambien toma prestado:
p r o c e d u r e TMdDbTrack.CmExit(var begin try Message: TCrnExit);
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.
Como se puede observar, la clase sobrescribe 10s metodos relacionados con el evento principal (en este caso tan solo se modifica la activacion y 10s datos, o registros). De forma alternativa, podriamos haber exportado eventos y despues dejar que el componente 10s controlase, como hace T F i e l d D a t a L i n k . El constructor requiere como hnico parametro el componente asociado.
constructor T M d R e c o r d L i n k - C r e a t e (View: TMdRecordView); begin i n h e r i t e d Create; R V i e w : = View; end;
Una vez que se ha almacenado una referencia a1 componente asociado, 10s otros metodos pueden funcionar sobre el directamente:
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;
// l o p i n t a t o d o d e n u e v o . . 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. existe No la propiedad Data Field para este componente, ya que hace referencia a todo
un registro. El metodo constructor del componente es muy importante porque establece 10s valores de numerosas propiedades no publicadas, entre las que se incluyen las opciones de cuadricula:
constructor TMdRecordView.Create (AOwner: TComponent); begin inherited Create (AOwner); FDataLink : = T M d R e c o r d L i n k - C r e a t e (self); // d e f i n e e l numero d e c e l d a s y d e c e l d a s f i j a s RowCount : = 2; // p r e d e f i n i d o ColCount : = 2 ; FixedCols := 1; FixedRows : = 0; Options : = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRowSizing]; DefaultDrawing : = False; ScrollBars : = ssvertical; FSaveCellExtents : = False; end ;
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
DrawText (Canvas.Handle, PChar (Text), Length (Text), ARect, dt-WordBreak o r dt-Noprefix) end else / / d i b u j a una l i n e a s i m p l e c e n t r a d a d e forma v e r t i c a l DrawText (Canvas.Handle, PChar (Text), Length ( T e x t ) , ARect, dt-vcenter o r dt-Singleline o r dt-Noprefix); i f gdFocused i n AState t h e n Canvas. DrawFocusRect (ARect); end;
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;
/ / duplica la altura del memo y del grafico f o r I : = 0 to DataSet.FieldCount - 1 d o i f DataSet.Fields [I] is TBlobField then // volver a pintarlo todo.. .
RView-Invalidate; end ; RView.RowHeights [I] : = RView.DefaultRowHeight
* 2;
I
Blue Angelhsh Species Name
Panafanltusnararchus
11 81 1 a236220472
Habrtal 1 around bouldels. caves. s coral ledges and crevices m shallow waters. Swims alone or in groups.
Figura 17.3. El ejemplo ViewGrid muestra la salida del componente RecordView, mediante el uso de la tabla de base de datos de muestra BioLife de Borland.
En este punto, nos encontramos con un pequeiio problema. En el metodo Def ineproperties,la clase TCustomGrid guarda 10s valores de las propiedades RowHeight s y ColHeights.Podriamos desactivar este proceso de streaming sobrescribiendo el metodo y no llamando a inherited (que es, en general, una mala tecnica), per0 tambien cabe la posibilidad de modificar el campo protegido FSaveCellExtents para desactivar esta caracteristica (como en el codigo del componente).
Bdrtudes
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
Snappei
Red Emperor
Lut~anus sebae
W~asse
Giant Maor1W~asse
Che~l~nur undulatus
hwptlsh
Bbe Angdlish
Pmacanlhus nauarchus
This is the la~gesl dl h e d uaassn It is l w n d in dense reef areas. 1eedh-g ,: ,A an a Wide valbty d mobks, fishes, sea Hab~tat a w n d boulders, is caves. c n d ledges and uewces n ~hanow waters Swrms alone or in pwps.
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!
-
NOTA: A diferencia de la cuadricula genkrica que hemos utilizado antes, una DBGrid es una vista virtual del conjunto de datos. No existe ninguna relacion entre el numero de filas que se muestran en la pantalla y el numero de filas del conjunto de datos. Cuando nos movemos hacia arriba y hacia abajo por 10s registros de datos del conjunto de datos, no nos estamos moviendo por las filas de la DBGrid: las filas son estaticas mientras que 10s datos se mueven de una fila a otra para dar sensacion de movimiento. Por este motivo, el programa trata de mar la altura de una fila individual para adaptarla a sus datos, sino que establece la altura de todas las filas de datos
En esta ocasion, el control no tiene que crear un enlace de datos personalizado, ya que se deriva de un componente que ya tiene una conesion compleja con 10s datos. La nueva clase tiene una nueva propiedad para especificar el numero de lineas de texto para cada fila y sobrescribe algunos metodos virtuales:
type TMdDbGrid = c l a s s (TDbGrid) private FLinesPerRow: Integer; procedure SetLinesPerRow (Value: Integer); protected
procedure DrawColurnnCell(const Rect: TRect; DataCol: Integer; Column: TColurnn; State: TGridDrawState); override; procedure LayoutChanged; override; public constructor Create (AOwner: TComponent); override; published property LinesPerRow: Integer read FLinesPerRow write SetLinesPerRow default 1; end;
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);
10s numerosos parametros de salida. En el codigo de este metodo, el componente llama en primer lugar a la version heredada, y despues establece la altura de cada fila. Como base para el calculo, utiliza la misma formula que la clase TCus t omDBGr i d : la altura del texto se calcula utilizando la palabra de muestra Wg en la fuente actual (se utiliza este texto porque incluye tanto una mayuscula de altura completa como una minuscula con extensiones inferiores). Este es el codigo:
procedure TMdDbGrid.LayOutChanged; var PixelsPerRow, PixelsTitle, I: Integer; begin inherited LayOutChanged; Canvas.Font : = Font; PixelsPerRow : = Canvas.TextHeight ( ' W g ' ) if dgRowLines i n Options then Inc (PixelsPerRow, GridLineWidth) ; Canvas.Font : = TitleFont; PixelsTitle : = Canvas .TextHeight ( ' W g ' if dgRowLines i n Options then Inc (PixelsTitle, GridLineWidth);
3;
+ 4;
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;
ADVERTENCIA: Font y T i t l e Font son 10s valores predefinidos de la cuadricula, que pueden ser sobrescritos por Ias propiedades de 10s objetos individuales de columna de DBGrid. El componente realmente ignora esos parhetros.
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.Free;
end ; end else i f Column.Field i s TMemoField then begin
DrawText (Canvas.Handle, PChar (Column.Field.DisplayText), Length (Column.Field. DisplayText) , OutRect, dt-vcenter or dt-Singleline or dt-Noprefix); end ; end ;
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.
gestionar 10s buffers, hacer un seguimiento de la posicion actual y el numero de registros y controlar muchas otras caracteristicas. Deberiamos prestar atencion a otra declaracion que se encuentra a1 principio: una estructura utilizada para almacenar datos adicionales para cada registro de datos que se coloca en un buffer. El conjunto de datos ubica esta inforrnacion en el buffer de cada registro, a continuacion de 10s datos reales.
Listado 18.2. La declaracion de TMdCustomDataSet y TMdDataSetStream
// 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;
function GetRecNo: Longint; override; function GetRecordCount: Longint; override; procedure SetRecNo (Value: Integer) ; override; // m a r c a d o r e s procedure InternalGotoBookmark(Bookmark: Pointer); override; procedure InternalSetToRecord (Buffer: PChar) ; override; procedure SetBookmarkData(Buffer: PChar; Data: Pointer); override; procedure GetBookmarkData (Buffer: PChar; Data: Pointer) ; override; procedure SetBookmarkFlag(Buffer: PChar; Value: TBookmarkFlag); override; function GetBookmarkFlag (Buffer: PChar) : TBookmarkFlag; override; // e d i c i d n ( v e r s i o n e s f i c t i c i a s ) procedure InternalDelete; override; procedure InternalAddRecord(Buffer: Pointer; Append: Boolean) ; override; procedure InternalPost; override; procedure InternalInsert; override; // o t r o procedure InternalHandleException; override; published // p r o p i e d a d e s d e l c o n j u n t o d e d a t o s r e d e c l a r a d a s property Active; property Bef oreopen; property Afteropen; property Beforeclose; property Afterclose; property BeforeInsert; property AfterInsert; property Bef oreEdit; property AfterEdit; property BeforePost; property AfterPost; property Beforecancel; property Aftercancel; property BeforeDelete; property AfterDelete; property BeforeScroll; property Afterscroll; property OnCalcFields; property OnDeleteError; property OnEditError; property OnFilterRecord; property OnNewRecord; property OnPostError; end ;
// 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 :
A1 dividir 10s metodos en apartados (como podemos comprobar en 10s archivos de codigo fuente), 10s hemos marcado con un numero romano. Veremos dichos numeros en un comentario donde se describe el metodo, de tal forma que mientras nos desplacemos por esta larga lista podamos saber en todo momento en cual de 10s tres apartados nos encontramos.
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:
// 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-
sarias a1 principio, como la comprobacion de si puede abrirse el conjunto de datos y la lectura de la informacion del cabecera del archivo. El codigo comprueba si un numero de version interno se corresponde con el valor que se guard6 cuando se
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;
not
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); (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 ;
// i n i c i a d a t o s l o c a l e s
El segundo metodo, In t ernalAf t eropen, se utiliza para operaciones que son necesarias una vez que se hayan establecido las definiciones de campo y va seguido por codigo que compara el tamaiio del registro leido desde el archivo con el valor calculado en el metodo InternalInitFieldDef s . El codigo tambien comprueba si el numero de registros que se leen desde la cabecera es compatible con el tamaiio real del archivo. Esta verificacibn puede fallar si el conjunto de datos no esta cerrado de forma adecuada: podria desearse modificar este codigo para permitir que el conjunto de datos refresque aun asi el tamaiio del registro
en la cabecera. Es responsabilidad especifica del metodo 1 nternalopen de la clase del conjunto de datos personalizado llamar a InternalInit FieldDef s, que establece las definiciones de 10s campos (en tiempo de ejecucion o de diseiio). Para este ejemplo, se ha decidido basar las definiciones de campo en un archivo externo (un archivo IN1 que proporciona un apartado para cada campo). Cada apartado contiene el nombre y 10s tipos de dato del campo, asi como su tamaiio si se trata de datos de cadena. El listado 17.3 muestra el archivo Con t r ib . IN I que utilizaremos en la aplicacion de prueba del componente:
Listado 17.3. El archivo Contrib.lNI para la aplicacion de prueba.
[Fields] Number = 6 [Fieldl] Type = f t S t r i n g Name = Name S i z e = 30 [Field2] Type = f t I n t e g e r Name = L e v e l [Field31 Type = f t D a t e Name = B i r t h D a t e [Field41 Type = f t c u r r e n c y Name = S t i p e n d [Fields] Type = f t S t r i n g Name = Email S i z e = 50 [Field61 Type = f t B o o l e a n Name = E d i t o r
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.
FieldName : = IniFile .Readstring ( 'Field' + IntToStr (I), 'Name', " ) ; if FieldName = " then raise EDataSetOneError-Create ('InitFieldsDefs: No name for field ' + IntToStr (I)); nSize : = IniFile-ReadInteger ('Field' + IntToStr (I), 'Size', 0) ; FieldDefs.Add (FieldName, FieldType, nSize, False); / / guarda el desplazamiento y calcula el tarnado FFieldOf fset .Add (Pointer (TmpFieldOffset) ) ; case FieldType of ftString: Inc (TmpFieldOffset, nSize + 1) ; ftBoolean, ftSmallInt, ftWord: Inc (TmpFieldOffset, 2) ; ftInteger, ftDate, ftTime: Inc (TmpFieldOffset, 4) ; ftFloat, ftcurrency, ftDateTime: Inc (TmpFieldOffset, 8) ; else raise EDataSetOneError-Create ('InitFieldsDefs: Unsupported field type' ) ; end ; end; / / del for finally
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;
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 ;
Se utiliza otra funcion relacionada para comprobar si el conjunto de datos esta abierto, algo que se puede resolver utilizando el campo local correspondiente:
function TMDCustomDataSet.IsCursor0pen: begin Result : = FIsTableOpen; end ;
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) ;
El sistema nos va pedir que almacenemos estos indicadores en el buffer de cada registro y, mas adelante, nos pedira que recuperemos 10s indicadores para un buffer de registro en concreto. En resumen, la estructura de un buffer de registro almacena 10s datos del registro, el marcador y 10s indicadores de marcador como se puede observar en la figura 17.5.
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;
Los metodos que se utilizan para establecer y obtener el marcador actual de un registro son muy parecidos a 10s dos anteriores, per0 aiiaden complejidad ya que se recibe un puntero a1 marcador en el p a r h e t r o Data. Se obtiene el valor del marcador convirtiendo el valor referido por este puntero a un entero:
procedure TMDCustomDataSet.GetBookmarkData (Buffer: PChar; Data: Pointer) ; begin PInteger (Data)^ : = PMdRecInfo (Buffer + FRecordSize).Bookmark; end; procedure TMDCustomDataSet.SetBookmarkData (Buffer: PChar; Data: Pointer) ; begin PMdRecInfo (Buffer + FRecordSize) .Bookmark : = PInteger (Data)"; end;
El metodo clave para la gestion de marcadores es I n t e r n a l G o t o B o o kmar k, que utiliza el conjunto de datos para convertir un registro dado en el actual. No se trata de la tecnica de navegacion estandar, es mucho mas habitual desplazarse a1 registro anterior o a1 siguiente (lo cual se puede realizar utilizando el metodo G e t R e c o r d del que hablaremos en breve) o desplazarse a1 primer o ultimo registro (para lo cual se utilizan 10s metodos I n t e r n a 1F i r s t e I n t e r n a l L a s t que tambien describiremos). Por sorprendente que pueda resultar, el metodo I n t e r na l G o t oBoo kmar k no espera un parametro de marcador, sin0 un punter0 a un marcador, por lo tanto hay que resolver la referencia para determinar el valor del marcador. El metodo siguiente, I n t e r n a l S e t T o R e c o r d , es el que se utiliza para saltar a un marcador determinado, per0 debe extraer el marcador de un buffer de registro pasado como parametro. En ese momento, I n t e r n a l S e t T o R e c o r d llama a I n t e r n a l G o t oBoo kmar k . A continuation, se muestran 10s dos metodos:
procedure TMDCustomDataSet.InternalGotoBookmark (Bookmark: Pointer) ; var ReqBookmark: Integer; begin ReqBookmark : = PInteger (Bookmark)"; i f (ReqBookmark >= 0) and (ReqBookmark < InternalRecordCount) then FCurrentRecord : = ReqBookmark else raise EMdDataSetError.Create ( 'Bookmark ' + IntToStr (ReqBookmark) + ' not found' ) ; end ; procedure TMDCustomDataSet.1nternalSetToRecord (Buffer: PChar); var ReqBookmark: Integer; begin ReqBookmark : = PMdRecInfo (Buffer + FRecordSize) .Bookmark; InternalGotoBookmark (@ReqBookmark); end;
Ademas de 10s metodos de gestion de marcadores que se acaban de describir, existen algunos otros metodos de navegacion que se utilizan para moverse hasta ubicaciones especificas dentro del conjunto de datos, como el primer registro o el ultimo. Estos dos metodos no mueven realmente el punter0 de registro actual a1 primer o ultimo registro, sino que lo desplazan a una ubicacion especial de entre dos posibles, antes del primer registro y despues del ultimo. No se trata de registros reales, Borland 10s denomina cracks (brechas). El crack del comienzo de un archivo, o Bof Crac k, tiene un valor -I(establecido en el metodo Internalopen ), ya que la posicion del primer registro es cero. El crack de final de archivo, o Eo fCra c k, tiene el valor del numero de registros, ya que el ultimo registro esta en la posicion FRecordCount - 1. Se han utilizado dos campos locales, llamados Eo fCrac k y Bo fCrac k para facilitar la lectura del codigo:
procedure TMDCustomDataSet.Interna1First; begin FCurrentRecord : = BofCrack; end; procedure TMDCustomDataSet.Interna1Last; begin EofCrack : = InternalRecordCount; FCurrentRecord : = EofCrack; end :
El metodo I nt ernalRecordCount es un metodo virtual que se introdujo en la clase TMDCustomDataSet, ya que distintos conjuntos de datos pueden tener un campo local para este valor (como en el caso del conjunto de datos basado en streams, que contiene un campo FRecordCount) o calcularlo sobre la marcha. Se utiliza otro grupo de metodos opcionales para conseguir el numero de registro actual (utilizado por el componente DBGrid para mostrar una barra de desplazamiento vertical proporcional), establecer dicho numero o determinar el numero de registros. Estos metodos son muy sencillos, si se tiene presente que el rango del campo interno FCur r ent Re cor d se encuentra entre 0 y el numero de registros menos 1. Por el contrario, el numero de registro que se comunica a1 sistema varia entre 1 y el numero de registros:
function TMDCustomDataSet.GetRecordCount: begin CheckActive; Result : = InternalRecordCount; end; Longint;
:=
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;
Fijese en que la clase del conjunto de datos personalizado generic0 implementa todos 10s metodos de este apartado. El conjunto de datos derivado basado en streams no necesita modificar ninguno de ellos.
(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);
El componente tambien necesita implementar una funcion para reiniciar el buffer, InternalI nit Recor d,generalmente rellenandolo con ceros numericos o espacios. Por sorprendente que parezca, tambien se debe implementar un metodo que devuelva el tamaiio de cada registro, per0 solo del fragment0 de datos, no del buffer de registro completo. Este metodo es necesario para implementar la propiedad de solo lectura Re cordSi ze,que solo se utiliza en un par de casos particulares en todo el codigo fuente de la VCL. En el conjunto de datos personalizado generico, el metodo G e t R e c o r d S i z e devuelve el valor del campo FRecordSize. Hemos llegado a1 nucleo de un componente de conjunto de datos personalizado. Los metodos de este grupo son GetRecord,que lee 10s datos desde el archivo, InternalPost e InternalAddRecord,que actualizany aiiadennuevos datos a1 archivo e I nt ernal De l e te, que elimina 10s datos y no esta implementado en este conjunto de datos de muestra. El metodo mas complejo de este grupo probablemente sea GetRecord,que sirve para fines muy diversos. De hecho, el sistema utiliza este metodo para recuperar 10s datos del registro actual, rellenar un buffer pasado como parametro y conseguir 10s datos del registro anterior o del siguiente. El parametro GetMode determina su accion:
t e m
Resulta evidente que un registro anterior o posterior puede no existir; incluso puede no existir el registro actual (por ejemplo, cuando la tabla esta vacia o en caso de un error interno). En estos casos no se obtienen 10s datos, sino un codigo de error. Por lo tanto, el resultado de este metodo puede ser uno de 10s siguientes valores:
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:
/ / 111: Recupera datos para el registro actual, el anterior o el // siguiente (yendo a el si es necesario) y devuelve el estado function TMdCustomDataSet.GetRecord(Buffer: PChar; GetMode: TGetMode; DoCheck: Boolean): TGetResult;
begin
Result : = grEOF; / / fin de archivo gmPrior: / / retrocede i f FCurrentRecord > 0 then Dec (FCurrentRecord)
else
Result : = grBOF; / / comienzo de archivo gmcurrent: / / verifica si estd vacio i f FCurrentRecord >= InternalRecordCount then Result : = grError;
end;
Si hay un error y el parametro D o C h e c k era T r u e , G e t R e c o r d lanza una excepcion. Si no todo va bien durante la seleccion de registros, el componente carga 10s datos del stream, pasando a la posicion del registro actual (obtenido mediante el tamaiio del registro multiplicado por el numero del registro). Ademas, necesitamos iniciar el buffer con el indicador de marcador adecuado y el valor del marcador (o numero de registro). Otro metodo virtual nos permite llevar a cab0 este proceso, de tal forma que las clases derivadas solo necesiten implementar esta porcion de codigo, mientras que el complejo metodo G e t R e c o r d permanece invariable:
procedure TMdDataSetStream.Interna1LoadCurrentRecord
(Buffer:
PChar) ;
begin
FStream.Position : = FDataFileHeaderSize + FRecordSize * FCurrentRecord; FStream.ReadBuffer (BufferA, FRecordSize); with PMdRecInfo(Buffer + FRecordSize)" do
begin
Movemos 10s datos a1 archivo en dos casos distintos: cuando modificamos el registro actual (esto es, un envio despues de una edicion) o cuando aiiadimos un nuevo registro (un envio despues de una insercion o aiiadido). En ambos casos se utiliza el metodo Int erna 1Post, per0 se puede comprobar la propiedad st ate del conjunto de datos para determinar que tipo de envio estamos realizando. En ninguno de 10s casos se recibe un buffer de registro como uno de 10s parametro, por lo que es necesario utilizar la propiedad Act iveRecord de TDataSet, que indica apunta a1 buffer para el registro actual:
procedure TMdDataSetStream.Interna1Post; begin CheckActive; if State = dsEdit then begin // s u s t i t u i r d a t o s p o r d a t o s n u e v o s FStream.Position : = FDataFileHeaderSize + FRecordSize FCurrentRecord; FStream.WriteBuffer (ActiveBufferA, FRecordSize); end else begin // a d a d i r s i e m p r e InternalLast; FStream.Seek (0, soFromEnd); FStream-WriteBuffer (ActiveBufferA, FRecordSize); Inc (FRecordCount); end; end;
Existe ademas otro metodo relacionado: InternalAddRecord. A este metodo lo llama el metodo A d d R e c o rd, a1 que a su vez es llamado por InsertRecord y AppendRecord. Los dos ultimos son metodos publicos a 10s que puede llamar cualquier usuario. Este recurso es una alternativa para insertar o anexar un nuevo registro a1 conjunto de datos, editando 10s valores de diversos campos y enviando despues 10s datos, ya que las llamadas Insert Record y AppendRecord reciben 10s valores de 10s campos como parametros. Todo lo que hay que hacer en ese momento es repetir el codigo utilizado para aiiadir un nuevo registro en el metodo InternalPost:
procedure TMdDataSetOne.InternalAddRecord(Buffer: Pointer; Append: Boolean) ; begin // a d a d i r s i e m p r e a 1 f i n a l InternalLast; FStream. Seek (0, soFromEnd) ; FStream.WriteBuffer (ActiveBufferA, FRecordSize); Inc (FRecordCount); end;
La ultima operacion de archivo que se deberia haber implementado era una que eliminase el registro actual. Esta operacion, aunque habitual, es bastante compleja. Si se usa un enfoque sencillo, como puede ser crear un punto vacio en el archivo, habra que realizar un seguimiento de dicho punto y conseguir que el codigo de lectura y escritura de un registro especifico funcione se salte esa posicion. Una solucion alternativa es hacer una copia de todo el archivo, sin el registro especifico, y despues sustituir a1 archivo original por la copia.
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;
El metodo G e t F i e l d deberia devolver 10s valores T r u e o F a l s e para indicar si el campo contiene datos o esta vacio (es un campo nulo, para ser mas precisos). No obstante, salvo que utilice un marcador especial para campos en blanco, es muy dificil determinar esta condicion, ya que se almacenan valores de diferentes tipos de datos. Por ejemplo, una prueba como Pt r < > # 0 solo tiene sentido si se esta utilizando una representacion de cadena para todos 10s campos. Si utilizamos esta prueba, 10s valores enteros que Sean cero y las cadenas vacias apareceran como valores nulos (10s controles data-aware estaran vacios). El problema es que 10s valores booleanos falsos no aparecen o, todavia peor, 10s valores de coma flotante sin decimales y con pocos digitos no se mostraran, ya que la parte exponencial de su representacion sera cero. No obstante, para conseguir que este ejemplo funcione, es necesario considerar como vacios 10s campos de fecha y hora con un cero inicial. Sin este codigo, Delphi intenta convertir la fecha cero interna no valida (internamente, 10s campos de datos no utilizan un tipo de datos T D a t e T i m e , sino una representacion diferente) creando una excepcion. El codigo funcionaba con las versiones anteriores de Delphi.
A
[-
--.
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
A-
-Paul
) Marco salsald
1 9/8/1965 2 3/12/1990
5
~&m&l
F=?
Figura 17.6. El formulario del ejemplo de StreamDSDemo. El conjunto de datos personalizado se ha activado para que se puedan ver 10s datos en tiempo de diseiio.
La figura 17.6 muestra el formulario del ejemplo en tiempo de diseiio, pero se ha activado el conjunto de datos de forma que 10s datos ya esten visibles. Evidentemente, ya se ha preparado el archivo IN1 con la definicion de la tabla (se trata del archivo a1 que ya se hizo referencia cuando se analizo el inicio del conjunto de datos), y se ha ejecutado el programa para afiadir algunos datos a1 archivo.
Tambien es posible modificar el formulario utilizando el editor de campos de Delphi y establecer las propiedades de 10s diversos objetos de campo. Todo funciona igual que con uno de 10s controles de conjuntos de datos estandar. Sin embargo, para conseguir que funcione, sera necesario escribir el nombre del archive del conjunto de datos personalizado en la propiedad T a b l e N a m e , utilizando la ruta completa.
--
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 ;
Vemos que primer0 se crea el codigo, abriendolo y cerrandolo dentro de la llamada C r e a t e T a b l e . y despues se abre la tabla. Este es el mismo comportsmiento que el del componente T T a b l e (que realiza este proceso utilizando el metodo C r e a t e T a b l e ) . Para abrir o cerrar la tabla, podemos hacer clic sobre la casilla de verificacion:
procedure TForml.CheckBoxlClick(Sender: T O b j e c t ) ; begin MdDataSetStream1.Active : = CheckBoxl.Checked; end;
Por ultimo, se ha creado un metodo que comprueba el codigo de gestion de marcadores de conjuntos de datos personalizados (funciona).
un sistema, una lista de archivos de una carpeta, las propiedades de algunos objetos, algunos datos basados en XML, etc. A mod0 de ejemplo, el segundo conjunto de datos que se presenta en este capitulo es una lista de archivos. Se ha elaborado un conjunto de datos generico basado en una lista de objetos en memoria (utilizando una TObjectList), del que despues se ha derivado una version en la que 10s objetos se corresponden con 10s archivos de una carpeta. El ejemplo se ha simplificado por el hecho de que es un conjunto de datos de solo lectura, por lo que podria parecer incluso mas sencillo que el anterior.
NOTA: &lguna de las ideas,comenttmbi4s agwecqn en an afticula publicado en Eunib de 2000 a el ORL bdn,borkd.darticb/ b,"ll0,2#587,~0.~titd,
Se puede ver que a1 escribir una clase de datos personalizada es posible sobrescribir algunos metodos virtuales de la clase T D a t a S e t y de esta clase de conjunto de datos personalizada y tener un conjunto de datos que funcione (aunque se trate todavia de una clase abstracta, que necesita codigo adicional de subclases para funcionar). Cuando se abre el conjunto de datos, se debe crear la lista y establecer el tamaiio de registro para indicar que solo se va a guardar el indice de la lista en el buffer:
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..ms datos.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;
La funcion GetCanModif y es otro metodo virtual de TDataSet que se utiliza para determinar si el conjunto de datos es de solo lectura. En ese caso su salida es Fa1 s e . No es necesario escribir codigo para el procedimiento S e t Fie 1 dDa t a, per0 si hay que definirlo ya que se trata de un metodo abstracto virtual. Ya que vamos a manejar una lista de objetos, la unidad incluye una clase para esos objetos. En este caso se trabaja 10s datos de archivo se extraen desde un buffer T Sear chRe c gracias a1 constructor de la clase T F i leDa t a .
type TFileData = class public ShortFileName: string; Time: TDateTime; Size: Integer; Attr: Integer; constructor Create (var FileInfo: TSearchRec); end; constructor TFileData.Create (var FileInfo: TSearchRec); begin ShortFileName : = FileInfo.Name; Time : = FileDateToDateTime (FileInfo.Time); Size : = FileInfo-Size; Attr : = FileInfo.Attr; 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 .
La parte mas compleja de la escritura de este codigo fue diseiiar el formato interno de las fechas almacenados en 10s campos de fecha y hora. No se trata del habitual formato TDateTime que se utiliza en Delphi, ni siquiera el TTimeStamp interno, sino del que se conoce, de forma interna, como formato de fecha y hora "nativo". Se ha escrito una funcion de conversion a partir de una que se encontro en el codigo de la VCL para campos de fecha y hora:
function DateTimeToNative(DataType: TFieldType; Data: TDateTime) : TDateTimeRec;
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 ;
Una vez que el conjunto de datos esta disponible, la tarea de construir un programa de muestra, como el que muestra la figura 17.7, se reduce a conectarle un componente DBGrid y aiiadirle un componente de seleccion de carpetas, el
control ShellTreeView de ejemplo. Se prepara este control para trabajar solo con archivos fijando su propiedad Root como C : \ . Cuando el usuario selecciona una nueva carpeta, el controlador de eventos OnChange del control ShellTreeView actualiza el conjunto de datos:
procedure TForml.ShellTreeViewlChange(Sender: TObject; Node:
TTreeNode) ;
begin
MdDirDatasetl.Close;
end;
13/9/2002 10:49:10 AN 13/9/2002 10:49:1O M 9/8/2002 2:OO:OO PI 9/8/2002 2:00:00 PI 9/8/2002 2:00:00 PH 14/9/2002 6:28:32 9/8/2002 4:OO:OO 70.bpl 9/8/2002 4:00:00 9/812002 4:OO:OO 9/8/2002 4:OO:OO 64.272 645.792 6.646 3.712 66.560 160.256 4.449 3.120 18.796 18.110 21.875 11.956 4.114 14 -447
Tu r. Tu r.
Falsa False False ?I@ .s Pals* Ialsm hlsc ?aha False Fmlse Ialse False Ids* Fmlr.
13 Altova
Pn P n
PH PI PI PI PI PI PI PII
PR
HTHLZ-strict.dcd
9/8/2002 4:OO:OO 3/8/2002 4:OO:OO 9/8/2002 4:OO:OO 9/8/2002 4:OO:OO 9/8/2002 4:OO:OO 9/9/2002 4:DO:dD
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 ~se wtiILa h n e problemas con los cdroles de la shell & e-b disponibles a D wsq p d e e , usar k versih DirDemoNoShell del ejempio, qae asa 1 v i e s d o l e s de arcihivos de Delphi ~ornpatibles W n o s 3.1. con i d w
Este componente de conjunto de datos hereda de TMdLis t D a t a S e t , como en el ejemplo anterior. Solo hay que proporcionar un unico parametro: la clase objetivo, guardada en la propiedad o b j c l a s s (el listado 17.5 muestra la definicion cornpleta de la clase TMdOb jD a t a S e t ) .
Listado 17.5. La definicion cornpleta de la clase TMdObjDataSet.
type TMdObjDataSet = class (TMdListDataSet) private PropList: PPropList; nProps : Integer; FObjClass: TPersistentClass; ObjClone: TPersistent; FChangeToClone: Boolean; procedure SetObjClass (const Value: TPersistentClass); function Getobjects (I: Integer) : TPersistent; procedure SetChangeToClone (const Value: Boolean); protected procedure InternalInitFieldDefs; override; procedure Internalclose; override; procedure InternalInsert; override; procedure InternalPost; override; procedure Internalcancel; override; procedure InternalEdit; override; procedure SetFieldData (Field: TField; Buffer: Pointer) ; override; function GetCanModify: Boolean; override; procedure Internalpreopen; override; pub1 i c function GetFieldData ( Field: TField; Buffer: Pointer) : Boolean; override; property Objects [I: Integer] : TPersistent read GetObj ects; function Add: TPersistent; published property ObjClass : TPersistentClass read FObjClass write SetObjClass; property ChangeToClone: Boolean read FChangeToClone write SetChangeToClone default False; end :
La clase se usa en el metodo I n t e r n a l I n i t F i e l d D e f s para determinar 10s campos del conjunto de datos basandose las propiedades publicadas de la clase objetivo, que se extraen mediante la RTTI:
procedure TMdObjDataSet.InternalInitFie1dDefs; var i: Integer; begin i f FObjClass = nil then raise Excepction.Create ( ' TMdObjDataSet: Unassigned class' )
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 )
Move (IntValue, B u f f e r A , sizeof (Integer)) ; end ; tkFloat : begin FlValue : = GetFloatProp (Obj, PropList [Field.FieldNo-11); Move (FlValue, B u f f e r A , sizeof (Double)) ; end ; tkstring, tkLString, tkWString: StrCopy (Buffer, pchar (GetStrProp (Obj, PropList [Field.FieldNo-11 ) ) ) ; end ; Result : = True; end ;
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
) ;
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 un conjunto. A1 llamar a ~ d del codigo crea un nuevo objeto d , 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;
P a r a mostrar el uso de este componente, hemos escrito el ejemplo ObjDataSetDemo. Tiene una clase objetivo de prueba con unos cuantos campos y botones para crear automaticamente objetos, como muestra la figura 17.8. No obstante, la caracteristica mas interesante del programa es algo que habra que probar para poder ver. Habra que ejecutar el programa y prestar atencion a las columnas del componente DbGrid. Despues editaremos la clase objetivo, T D e m o , aiiadiendole una nueva propiedad publicada. A1 volver a ejecutar el programa, la cuadricula incluira una nueva columna para la propiedad.
21 33 28 24
-
12
23 14
John
62
Figura 17.8. El ejemplo ObjDataSetDemo dispone de un conjunto de datos proyectados sobre objetos rnediante el uso de RTTI.
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
que resulta importante disponer de informes de gran calidad y de una arquitectura flexible que permita que 10s usuarios puedan personalizarlos.
NOTA:Este capitulo no podria ser como es sin la ayuda de Jim G Nevrona Designs, la empresa que ha desarrollado el motor Rave.
Este capitulo trata 10s siguientes temas: Presentacion de Rave (Report Authoring V ~ s u aEnvironment). l Componentes Delphi de Rave. Componentes de Rave Designer. De bases de datos a informes. Caracteristicas avanzadas de Rave.
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
Thrs Islome text I r e added l a Vlc memo And me#. toO And mow text And mow I d text And mom text And mom l e a And mom And mole text. And m o w text. And more text text And more text And m w l t n b An4 men more text. And more tckt And mole b*. An4 And m o l l text And mole text
JI
NOTA: S i e m p ~ - q w , squiera yer el resultado dcl trabajo, habra que pule sar 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
Beginner, Intermediate o Advanced mediante el cuadro de dialogo Edit> Preferences (en la seccion Environment), para que 10s usuarios finales trabajen a1 nivel con el que se sientan comodos y no dispongan de mas potencia de la que se les quiera ofrecer. Tambien se pueden bloquear algunas caracteristicas del informe para que no Sean modificables.
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.
modificar el orden de impresion de 10s componentes mediante 10s botones de orden: Move Forward, Move Behind, Bring to Front y Send to Back. Los botones de pulsar permiten desplazar componentes poco a poco.
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).
de componentes. El componente central es R v P r o j e c t . Hay que colocar este componente sobre un formulario o modulo de datos, conectar su propiedad P r o j e c t F i l e con un archivo de Rave y escribir este codigo de control de eventos para un boton:
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.
I
I
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. 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 . - .
--. -
I
A\
.---
-- - --
" I
camp-
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.
Figura 18.3. Despues de ejecutar un proyecto Rave, un usuario puede escoger el formato de salida o el motor de representacidn.
Estos son 10s componentes de representacion disponibles a traves de la pagina Rave de la Component Palette de Delphi: RvRenderPreview: Puede usarse para mostrar un archivo o flujo NDR en la pantalla. Se usa un componente ScrollBox para mostrar las paginas del informe, y hay disponibles muchos mktodos y propiedades para crear dialogos de vista previa personalizados. A no ser que se necesite una vista previa personalizada de impresion, deberia utilizarse el componente RvSystem en lugar de RvRenderPreview para la visualizacion previa.
RvRenderPrinter: Envia un archivo o flujo NDR a la impresora. A no ser que se necesite una interfaz de vista previa o impresion personalizada, no deberia ser necesario el uso de este componente (RvSystem proporciona la funcionalidad estandar de impresion). RvRenderPDF: Convierte un archivo o flujo NDR a1 formato PDF (de Adobe Acrobat). Se pueden usar propiedades y eventos para personalizar el tip0 de salida que se crea y proporcionar soporte de compresion. RvRenderHTML: Convierte un archivo o flujo NDR a1 formato HTML (DHTML). Cada pagina se convierte en una pagina independiente y puede combinarse con plantillas para conseguir un mayor control sobre el resultado. RvRenderRTF: Convierte un archivo o flujo NDR a1 formato RTF. El formato RTF usa areas de campo para colocar y reproducir con precision el informe. RvRenderText: Convierte un archivo o flujo NDR a1 formato de texto. Muchos comandos y componentes graficos, como lineas y rectangulos, se ignoraran durante la generacion del texto. Ademas de permitir a1 usuario seleccionar un formato de archivo en el cuadro de dialogo, se puede realizar la generacion de forma programada. Por ejemplo, para convertir directamente un informe a un archivo PDF, se puede escribir el codigo siguiente (extraido una vez mas del ejemplo Raveprint):
procedure TFormRave. btnPdfClick (Sender: TObject) ; begin RvSysteml.DefaultDest : = rdFile; RvSysteml.DoNative0utput : = False; RvSysteml.RenderObject : = RvRenderPDFl; RvSysteml.0utputFileName := 'Simple2.pdf'; RvSystem1.SystemSetups : = RvSystem1.SystemSetups [ssAllowSetup]; RvProj ectl .Engine : = RvSysteml; RvProj ectl .Execute; end;
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.
Figura 18.4. El inforrne Ravesingle (generado con ayuda de un asistente) en tiempo de diseiio.
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. componente mte se r e i I TRUCOZI Section es i m p Adisefiocuando dentroa ldei n eopias I de espejo, ya que pemiten la herencia del visual 10s disefios de infome.
-
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.
para usuarios que saben exactamente lo que necesitan, porque es necesario un cierto conocimiento previo sobre codigos de barras y su uso. Para definir el valor de un codigo de barras, hay que acceder a1 panel Property y escribir el valor en el cuadro de propiedad Text. Entre 10s codigos de barras que soporta Rave estan 10s siguientes: POSTNET (Postal Numeric Encoding Technique): Es un codigo de barras utilizado de forma particular por el servicio postal de EEUU. I2ofSBarCode (entrelazado 2 de 5 ) : Es un codigo solo para informacion numerics. Code39BarCode: Es un codigo de barras alfanumerico que puede codificar numeros decimales, el alfabeto de las mayusculas y algunos simbolos especiales. CodelZSBarCode: Es un codigo de barras alfanumerico de alta densidad diseiiado para codificar 10s 128 caracteres ASCII al completo. UPCBarCode (Universal Product Code): Es un codigo que tiene una longitud fija de 12 digitos y se diseiio para codificar productos. EANBarCode (European Article Numbering System): Es un codigo que es identico al UPC per0 tiene 13 digitos: 10 caracteres numericos, 2 caracteres de codigo de pais y un digito de comprobacion.
@ Database Cmnedion
Direct D d a Vlew
Componente RaveDatabase (conexi6n con la vista de datos): Ofrece parametros de conexion con una base de datos para el componente DriverD a t aView (la vista de datos del controlador). Solo se permiten conexion a bases de datos para las que esten instalados controladores DataLink. Componente RaveDirectDataView (vista directa de datos): Proporciona un medio de obtener datos desde un componente de conexion de datos que se encuentre en la aplicacion Delphi anfitriona, como en el ejemplo anterior. (La seleccion se llama vista directa de datos o Direct Data View incluso aunque no exista la conexion directa con la base de datos desde el informe, sino que se trate de la conexion indirecta basada en datos extraidos de la base de datos de una aplicacion anfitrion.) Componente RaveDriverDataView (vista de datos de controlador): Proporciona un mod0 de definir una consulta para una conexion de base de datos especifica mediante un lenguaje de consultas como SQL. Se mostrara un constructor de consultas para definir la consulta. Componente RaveSimpleSecurity (controlador de seguridad simple): Implementa la forma mas basica de seguridad mediante el empleo de una sencilla lista de pares de nombre de usuario y contraseiia en la propiedad US erList . user Lis t contiene un par de nombre de usuario y contraseiia por linea, de acuerdo con este formato: username = password. Ca seMa t ter s es una propiedad booleana que controla si la contraseiia tiene en cuenta las mayusculas. Componente RaveLookupSecurity (seguridad de busqueda de datos): Permite verificar 10s pares de identificacion mediante entradas en una tabla de base de datos. La propiedad Dat aview especifica la vista de datos a utilizar para buscar el nombre de usuario y la contraseiia. Las propiedades UserField y PasswordField se usan para buscar el nombre de usuario y la contraseiia que se deben verificar.
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 no directameite componentes ~ & d . n, 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:
El simbolo de triangulo (Flecha arriba o Flecha abajo): Indica que la banda esta controlada por una banda maestra con el mismo color (nivel), que puede encontrarse en la direccion de la flecha. El simbolo diamante: Representa una banda maestra o de control. Estos simbolos tienen una codification de colores e indentacion para representar el nivel de flujo maestro-esclavo. Recordemos que se puede tener una relacion maestro-detalle-detalle en la que ambos detalles esten controladores por el mismo maestro o uno de 10s detalles este controlado por el otro detalle.
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
La propiedad D a t a F i e l d tiene un Data Text Editor, como muestra la figura 18.5, que ayuda a crear campos compuestos. Si se hace clic sobre 10s puntos suspensivos se abrira el Data Text Editor, que permite encadenar campos, parametros o variables para construir un campo de texto data-aware complejo seleccionando elementos de 10s cuadros de lista. Este editor incluye una gran cantidad de combinaciones; hablaremos sobre ellas brevemente, ya que es mejor probarlas en la practica. Fijese en que el cuadro de dialog0 se divide en cinco grupos: Data Fields, Report Variables, Project Parameters, Post Initialize Variables y Data Text. Data Text es la ventana resultante: Hay que prestar atencion a esta ventana cuando se introducen elementos. Los dos botones que se encuentran a la derecha de esta ventana con la suma (t) y &. El boton + junta dos elementos sin espacios, y el boton & 10s encadena con un unico espacio (siempre que el campo
anterior no estuviera en blanco). Por eso, el primer paso es decidir si se quiere usar + o &, y, despues, escoger el texto de no de 10s tres grupos de la parte superior de la ventana Data Text. El componente DataTest no esta limitado a imprimir datos de una base de datos: tambien se pueden usar parametros de proyecto y variables de informe. Si accedemos a1 grupo Report Variables y nos fijamos en el cuadro de lista, podremos ver las variables que se encuentran disponibles. La lista Project Parameters podria contener parametros UserName, Repor tTit le o UserOpt ion con valores iniciales proporcionados por la aplicacion. Para crear la lista de 10s parametros de objeto hay que escoger el nodo Project en el Project Tree (el elemento superior). Despues, en el panel Properties, hay que hacer clic sobre el boton de puntos suspensivos que se encuentra junto a la propiedad Parameters para abrir el tipico editor de cadena, donde se pueden escribir distintos parametros que se deseen pasar a Rave desde la aplicacion (como UserName).
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.
TRUCO: Para aprender mhs sobre estas caracteristicas, y otros aspectos de Rave, es recomendable visitar el sitio Web de Nevrona. Deberia explorarse la colecci6n de trucos que se encuentra disponible en www.nevrona.com/
raveltinc chtml
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
El informe tiene dos vistas de datos correspondientes, cada una conectada a un componente DataBand (ambos contenidos en una region). El primer DataBand, utilizado para el conjunto de datos principal, no tiene parametros especiales. El DataBand secundario define la relacion maestro-detalle mediante unas cuantas propiedades. La propiedad MasterDataView se refiere a la vista de datos del conjunto de datos maestro, y las propiedades MasterKey y DetailKey a 10s campos que definen la union (en este caso ambos se refieren a1 campo DEPT-NO).
La propiedad C o n t r o 1l e r B a n d se refiere a1 DataBand que muestra 10s datos del coniunto de datos maestro. En el caso del informe maestro-detalle, el parametro mas importante lo gestiona cl Band Style Editor, en el que hay que definir la banda como un detalle. La figura 18.6 muestra este editor para el ejemplo RaveDetails. La propiedad KeepRowTogether de la vista de datos maestra se define como T r u e para evitar tener detalles quc se muestren en una pagina distinta a 10s datos maestro.
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
Designer permite personalizar o ajustar el resultado de un informe de un mod0 muy sofisticado. El lenguaje que utiliza Rave para 10s guiones se basa en Pascal y es una variante del lenguaje Delphi, por lo que no deberia haber mucho problema en su comprension. El ejemplo RaveDetails muestra en negrita 10s sueldos que son mayores que una cantidad cspecificada. El mod0 mas obvio de realizar esto es escribir un codigo interpretado que se ejecute para cada instancia de la banda detalle (es decir, para cada registro dc la base de datos de empleados). En lugar de modificar directamente la propiedad Font, hemos decidido aiiadir dos componentes FontManager distintos a la pagina del informe y cambiarles el nombre para que sc comprenda su funcion: fmPlain Font y fmBold Font. Se puede abrir el informe para vcr sus propiedades y estructura. En cl informe, para resaltar 10s valores que se encuentrcn por encima de un rango dado, se controla el evento Bef orePr int del componente Da t a T e x t . Para cllo, iremos a la pagina Event Editor, escogeremos el componente DataText conectado con el campo Salary y escogeremos el evento. En la ventana de edicion de codigo dcl evento, escribiremos este codigo:
if DataView2Salary.AsFloat > 1 0 0 0 0 0 then self.FontMirror : = fmBoldFont;
else
self.FontMirror
end if;
:=
fmPlainFont;
El guion modifica la propiedad FontMirror del objeto actual (self) para hacer referencia a uno de 10s dos componente FontManager de la pagina, de acuerdo con el valor del campo. Fijese en que DataView2Salary es una refercncia a uno de 10s dos campos de la vista de datos (el que csta conectado con el componentc DataTest actual). Compilaremos el guion y ejecutaremos el informe para coinprobar su efecto. como muestra la figura 18.7.
ADVERTENCIA: Cada vez que se modifique un guion hay que acordarse de hacer clic sobre el boton Compile, para que tengan efecto 10s carnbios.
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
o nacionales. La plantilla para 10s usuarios internacionales podria incluir una linea para el pais con el texto centrado en el sobre, mientras que el formato nacional podria no incluir la linea de pais y podria tener el testo en la parte inferior derecha del sobre.
I
I
DEPARTMENT
Corporate Headquarters Lee, Tern Bender, Ollver H. Sales and Marketing
MacDond. Mary S Yanowski, Mchael
BudgetCjalary
1 000.000.00
53 793.00
LOCATION
Monterey
212.850.00
2.000 000,OO
111.262.50
44.WOPO
San Franc~sco
1.100.000.(
105.900.0
270M,C
age 1 o f 3
Figura 18.7. El texto en negrita del informe se establece en tiempo de ejecucion gracias a un guion.
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) darle y formato de acuerdo con la propiedad DisplayFormat.Puede resultar util cuando se realicen calculos totales que se utilizaran en otros calculos antes de presentarse.
Si el valor del CalcTotal solo se va a usar en otros componentes de calculo, como CalcOp, deberia dejarse en blanco la propiedad De stParam.
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
Con la llegada de la era de Internet, la creacion de programas basados en protocolos de Internet se ha convertido en algo muy comun, por lo que vamos a dedicar a este tema 10s proximos cinco capitulos. Este capitulo se centra en la programacion de sockets a bajo nivel y de protocolos de Internet, el capitulo 20 se dedica a la programacion Web de servidor, el 2 1 habla de IntraWeb, y 10s capitu10s 22 y 23 tratan de XML y 10s servicios Web. En este capitulo comenzaremos a fijarnos en la tecnologia de sockets en general. Despues pasaremos a1 uso de 10s componentes Indy (de Internet Direct) que soportan tanto la programacion de sockets a bajo nivel como 10s protocolos de Internet mas habituales. Presentaremos algunos elementos del protocolo HTTP, hasta llegar a la creacion de archivos HTML a partir una base de datos. Aunque probablemente quiera utilizar un prot o c o l ~ alto nivel, la explicacion de la programacion para Internet comienza a de partir de 10s conceptos basicos y las aplicaciones a bajo nivel. Comprender el protocolo TCPIIP y 10s sockets ayudara a asimilar el resto de 10s conceptos mas facilmente. En este capitulo trataremos 10s siguientes temas: Uso de sockets. Componentes Internet Direct (Indy). Programacion de sockets a bajo nivel
La API WinInet. Protocolos de correo (SMTP y POP3). El protocolo HTTP. Generacion de HTML. De bases de datos a HTML.
~~
~~
- -
-~
-~
3-
Con mas de 160 componkntes instaradoseda pafeta de Delphi, h d y tiene una gran cantidad de prestaciones, desde el desarrollo de aplicaciones TCPI IP de cliente y servidor para varios protocolos a1 cifrado y la seguridad. Se pueden reconocer 10s componentes Indy por el prefijo I d . En lugar de ------A :-&3 L-LI 2 presenrar una I~isra - 10salversos componenies, namarernos ae unos cuanae I - - >: tos de ellos durante este capitulo.
A_-
. .
. .
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
puerto cuando se implementen esos servicios y no utilizarlos en ningun otro caso. Esta es una breve lista:
HTTP (Protocolo de transferencia de hipertexto) FTP (Protocolo de transferencia de archivos) SMTP (Protocolo de transferencia simple de correo) POP3 (Protocolo de oficina de envio, version 3) Telnet
80 21 25 110 23
El archivo s e r v i c e s (otro archivo de texto parecido a1 archivo H O S t s ) contiene una lista de 10s puertos estandar que utilizan 10s servicios. Se pueden aiiadir entradas propias a la lista, proporcionando a esos servicios un nombre propio. Los sockets cliente siempre especifican el numero de puerto o el nombre de servicio del socket de servidor a1 que desean conectarse.
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.
NOTA: Los sockets de servidor de Indy permiten conectarse a multiples direcciones IP ylo puertos, mediante el conjunto Bindings.
El programa sewidor tiene un cuadro de lista que se utiliza para registrar informacion. Cuando se conecta o desconecta un cliente, el programa muestra la direccion IP de ese cliente junto con la operacion, como en el siguiente controlador del evento Onconnect:
procedure begin
TFormServer.IdTCPServerlConnect(AThread: TIdPeerThread) ;
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
<
Command = ' t e s t ' Name = ' T I d C o r r u n a n d H a n d l e r O ' Parseparams = False ReplyNormal.NumericCode = 100
ReplyNorma1.Text.Strings
= (
end
IdTCPClientl.SendCmd
' t e s t ')
+ '
' +
Para casos mas complejos deberia ejecutarse el codigo en el servidor y leer y escribir directamente sobre la conexion del socket. Este enfoque se muestra en la segunda orden del protocolo tan trivial que hemos creado para este ejemplo. La segunda orden del servidor se llama execute,y no tiene ningun conjunto especial de propiedades (solo el nombre de la orden), per0 tiene el siguiente controlador para el evento Oncommand:
procedure TFormServer.IdTCPServerlTIdCommandHandlerlCommand( ASender : TIdCommand) ; begin ASender.Thread.Connection.Write1n ('This is a dynamic response'); 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);
El efecto es similar a1 del ejemplo anterior, per0 ya que utiliza un enfoque de bajo nivel, deberia ser mas facil de particularizar para las necesidades del momento. Una de estas extensiones se ofrece como tercer y ultimo comando del ejemplo, que permite que el programa cliente solicite un archivo de mapa de bits del servidor (como una especie de arquitectura para compartir archivos). La orden del servidor tiene parametros (el nombre de archivo) y se define de esta manera:
object IdTCPServerl: TIdTCPServer CommandHandlers = < item CmdDelimiter = ' ' Command = 'getfile' Name = ' T I d C o m n d H a n d l e r 2 ' Oncommand = IdTCPServerlTIdCommandHandler2Command ParamDelimiter = ' ' ReplyExceptionCode = 0 ReplyNormal.NumericCode = 0 Tag = 0 end>
El codigo utiliza el primer parametro como nombre de archivo y lo devuelve en un flujo. En caso de error lanza una excepcion, que interceptara el componente
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);
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 co& en el capitulo 16) o con sopoite SOAP (como en el nexi6n de sockets ( 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
registros a1 servidor. Se ve que un registro es nuevo fijandose en si el registro tiene un valor valido para el campo CompID. Este campo no lo establece el usuario, si no la aplicacion servidor cuando se envian 10s datos. Para todos 10s registros nuevos, el programa cliente empaqueta la informacion de campos en una lista de cadenas, utilizando la estructura F i e l d N a m e = Fie ldValue.La cadena correspondiente a la lista completa, que es un registro, se envia entonces a1 servidor. En este punto, el programa espera a que el servidor envie de vuelta el identificador de la empresa, que se guarda entonces en el registro actual. Todo este codigo se ejecuta en una hebra, para evitar bloquear la interfaz de usuario durante operaciones pesadas. A1 hacer clic sobre el boton Send, un usuario lanza una nueva hebra:
procedure TForml.btnSendClick(Sender: TObject); var SendThread: TSendThread; begin SendThread : = TSendThread.Create(cds); SendThread.OnLog : = OnLog; SendThread.ServerAddress : = EditServer.Text; SendThread.Resume; end :
La hebra tiene unos cuantos parametros: el conjunto de datos que se pasa en el constructor, la direccion del servidor guardada en la propiedad ServerAddress, y un evento de registro para escribir en el formulario principal (dentro de una llamada S ynch r o n i z e segura). El codigo de la hebra crea y abre una conexion y sigue enviando registros hasta completar su labor:
procedure TSendThread.Execute; var I: Integer; Data: TStringList; Buf: String; begin try Data : = TStringList.Create; f IdTcpClient : = TIdTcpClient .Create ( n i l ); try f1dTcpClient.Host : = ServerAddress; fIdTcpClient.Port : = 1051; fIdTcpClient.Connect; fDataSet.First; while n o t fDataSet.Eof do begin // s i el r e s g i s t r o a u n n o s e ha registrado i f fDataSet.FieldByNarne('CompID1) .IsNull or (fDataSet.FieldByName('CompID').AsInteger = 0 ) then begin FLogMsg : = 'Sending ' + fDataSet.FieldByNarne('Company1) .Asstring;
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;
Fijemonos ahora en el servidor. Este programa tiene una tabla de base de datos, guardada una vez mas en el directorio local, con dos campos mas que la tabla de la aplicacion cliente: LoggedBy, un campo de cadena; y LoggedOn, un campo de datos. Los valores de estos dos campos adicionales 10s determina automaticamente el servidor cuando recibe datos, junto con el valor del campo CompID. Todas estas tres operaciones se realizan en el controlador de la orden senddata:
procedure TForml.IdTCPServerlTIdCommandHandlerOCommand( ASender: TIdCommand) ; var Data: TStrings; I: Integer; begin Data : = TStringList.Create; try ASender.Thread.Connection.ReadStrings(Data);
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:
Generation automitica d e mensajes d e correo: Una aplicacion que se haya escrito puede tener un cuadro Acerca d e ... para enviar un mensaje de registro a1 departamento de comercializacion o un elemento de menu especifico para enviar una peticion a1 grupo de soporte tecnico. Podria decidirse incluso habilitar una conexion con el soporte tecnico cuando se produzca una escepcion. Otras tareas relacionadas podrian automatizar el envio de un mensaje a una lista de gente o generar un mensaje automatico desde el sitio Web (un ejemplo que mostraremos hacia el final de este capitulo).
Addless
l~lmanza
Stale
cm~uly ulanl
plaly
:
I
1
Emd
Canla3
Marco Canlu
rnarco@marcocanlu a n c
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
cada usuario una aplicacion cliente especializada en la interaccion con dicho servidor. Una alternativa es utilizar una aplicacion de servidor ya existente, como un servidor de correo, y escribir 10s dos programas especializados basandose en 10s protocolos de correo. Los datos enviados mediante esta conexion tendran en general un formato especial, por lo que se querran utilizar direcciones de correo electronico especificas para estos mensajes (y no la direccion de correo principal). Como ejemplo, se podria rescribir el anterior ejemplo IndyDbSock para enviar mensajes de correo en lugar de utilizar una conexion de socket personalizada. Este enfoque tiente tambien la ventaja de funcionar mejor con 10s cortafuegos, y de permitir que el servidor este temporalmente desconectado, porque las peticiones se guardaran en el servidor de correo.
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).
',
FileName,
sw-ShowNormal)
Mediante She1 lExecut e, se puede ejecutar simplemente un documento, como un archivo. Windows arrancara entonces el programa asociado con la extension HTM, utilizando la accion pasada como parametro (en este caso open, per0 si se pasara nil se llamaria a la accion estandar, produciendo el mismo
efecto). Se puede utilizar una llamada similar para visitar un sitio Web, utilizanen do una cadena como 'http://www.ejemplo.com' 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.
';
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 ;
La cadena URL se forma con la direccion principal del motor de busqucda. seguida de unos cuantos parametros. El primero, a s q, indica las palabras que se estin buscando. El segundo, nun= 10 0, indica el %mere de sitios a obtener: no se pueden usar 10s numeros que se quiera, sin0 que se esta limitado a unas cuantas alternativas; siendo 100 el mayor valor posible
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-
El metodo Execute de la hebra, activado mediante la llamada Resume, llama a 10s dos metodos que realizan el trabajo (como muestra el listado 19.1). En el primero, GrabHtml,el programa se conecta con el servidor HTTP utilizando un componente IdHttp creado dinamicamente y lee el codigo HTML con el resultad0 de la busqueda. El segundo metodo, HtmlToList, estrae las URL que hacen referencia a otros sitios Web a partir del resultado, la cadena strRead.
Listado 19.1. La clase TFindWebThread (del prograrna WebFind).
unit FindTh; interface uses Classes, Idcomponent, SysUtils, IdHTTP; type TFindWebThread = class (TThread) protected Addr, Text, Status : string; procedure Execute; override;
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.
SDK Help incluida con Delphi, pero puede mmbtrarse en Meinet eh el sitio Web de MSDN en ~ . m i c r o s o f i . ~ i h a r y / e m - u ~ h & t e. . wininet_refere&% asp.
La funcion 1n t e r ne t Ope n establece una conexion generica y devuelve un manejador que puede usarse en la llamada a InternetOpenURL.Esta segunda llamada devuelve un manejador de la URL que puede pasarse a la funcion Internet ReadFile para leer bloques de datos. En el siguiente codigo de ejemplo, 10s datos se guardan en una cadena local. Cuando se han leido todos 10s datos, el programa cierra la conexion a la URL y la sesion de Internet mediante dos llamadas a la funcion InternetCloseHandle:
var hHttpSession, hReqUrl: HInternet; Buffer: array [O. .I0231 o f Char; nRead: Cardinal; strRead: string; nBegin, nEnd: Integer; begin strRead := "; hHttpSession : = Internetopen ( ' F i n d w e b ' , INTERNET-OPEN -TYPE-PRECONFIG, nil, nil, 0) ; try hReqUrl : = InternetOpenURL (hHttpSession, PChar(StrUrl), nil, 0 , 0 , 0 ) ; t r y / / l e e t o d o s 10s d a t o s repeat InternetReadFile (hReqUrl, @Buffer, sizeof (Buffer), nRead) ; strRead : = strRead + string (Buffer); u n t i l nRead = 0; finally InternetCloseHandle (hReqUrl); end; finally InternetCloseHandle (hHttpSession); end; end ;
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);
donde NewUrl es la ubicacion completa del recurso Web a1 que se desea acceder. En el ejemplo BrowseFast, se escribe este URL en un cuadro combinado,
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.
GO
1~~4.1
II
Books
D c v ~ m t
&
O c t a s 231d The D e w 7 verslon ol GnTods W~zards available lor dowload in IS CmTooln sectton ol the EL O c t o b ~ st I'm l~nish~nq:o wnle MdsteiulgD e r ~ t7. l book should be out in 1 i h JanuarylFebruary h M L
!5-6June 2002
11 I. . . :.... .
. .
.T,.
l-t 8lh. 2 Ths sde has been moved to a Lim problems here and there w t h case inc~sislencies. you If w c l c a r e or lowncasc the lir* IeUer, a email me with the problem..
. , .
. ..
--
Goto:
Solamente hemos aiiadido a1 programa un soporte muy limitado de hipervinculos. Cuando un usuario mueve el raton sobre un enlace, su texto de enlace se copia en una variable local ( N e w R e q u e s t ) , que se utiliza despues en caso de que se haga clic sobre el control para calcular la nueva peticion HTTP a enviar. Sin embargo, fusionar la direccion actual (LastUrl) con la peticion no es nada trivial, incluso con la ayuda de la clase IdUrl que proporciona Indy. Este es el codigo, que solo maneja 10s casos mas simples:
procedure TForml.TextBrowserlClick(Sender: TObject); var Uri: TIdUri; begin if NewRequest <> " then begin Uri : = TIdUri-Create (LastUrl); if Pos ( ' h t t p : ', NewRequest) > 0 then
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.
El sewidor utiliza el puerto 8080 en lugar el puerto 80 estandar para que pueda ejecutarse junto con otro servidor Web. Todo el codigo personalizado se encuentra en el controlador del evento O n C o m r n a n d G e t , que devuelve una pagina fija ademas de algo de informacion sobre la peticion:
p r o c e d u r e TForml .I d H T T P S e r v e r l C o r x n a n d G e t (AThread: TIdPeerThread; RequestInfo: TIdHTTPRequestInfo; ResponseInfo: TIdHTTPResponseInfo); var HtmlResult: String; begin // r e g i s t r o Listboxl.Items.Add (Request1nfo.Document); // r e s p u e s t a HtmlResult : = '<hl>Ht t p S e r v Demo</hl> ' + ' < p > T h i s i s t h e o n l y page y o u " l 1 g e t f r o m t h i s e x a m p l e . </p><hr> ' +
' < 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;
A1 pasar una ruta y algunos parametros en la linea de comandos a1 navegador, se podran ver estos datos reinterpretados y representados. Por ejemplo, en la figura 19.6 se puede ver el efecto de esta linea de comandos:
'
I HttpSen-Demo
Recargar
-
['d I
Inca
http ,,localhost
8080/1e.
Request /test
The headers of the request follow Host localhost 8080 User-Agent M o d a t 5 0 (Wadows. U. Wmdows NT 5 1. es-AR, rv 1 4b) GeckoQ0030516 Moz.lla FuebudlO 6 Accept text/..unl,apptcahonl~~ddapptcahod~h~+wnl,te~/html,q=O ~,textfplm,q=O8 , ~ 1 d e o / x - ~ . l m a g e / p n g , r n a g e / j p e g . ~ Accept Language es-AR,es.q=O 5 Accept-Encodmg gap.deflate.compress.q=O 9 Accept-Charset ISO-8859-1.utf-8.q=0 7.*.q=O 7 Keep-PiLve 300 Comechon keep-&ve
Figura 19.6. La pagina mostrada al conectar un navegador con el programa HttpServ personalizado.
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.
El PageProducer: Es el componente productor de HTML mas sencillo, es el que manipula un archivo HTML en el que se han incluido etiquetas especiales. El HTML puede guardarse en un archivo interno o en una lista interna de cadenas. La ventaja de este enfoque es que puede generarse un archivo de este tipo mediante el editor HTML que se prefiera. En tiempo de ejecucion, el PageProducer convierte las etiquetas especiales en codigo HTML, con lo que consigue un metodo sencillo para modificar partes de un documento HTML. Las etiquetas especiales tienen el formato basico < #nombredee t i q u e t a>, per0 tambien se pueden proporcionar parametros con nombre dentro de la etiqueta. Las etiquetas se procesaran en el controlador del evento OnTag del PageProducer. El DataSetPageProducer: Amplia a1 PageProducer sustituyendo automaticamente las etiquetas que se correspondan con nombres de campo de una fuente de datos conectada. El componente DataSetTableProducer: Resulta util generalmente para mostrar 10s contenidos de una tabla, consulta u otro conjunto de datos. La idea es producir una tabla HTML a partir de un conjunto de datos, de un mod0 simple aunque flexible. El componente ofrece una vista previa apro-
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.
AD UW
1-n
1-3
b b ~ y u \ r b a a , WAUV
nt;n**atmn
t n n an - m
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
fonnato necesario para HTML 4 y XHTML. El componente PageProducer tiene una propiedad S t r ipParamQuot es que puede activarse para eliminar estas comillas adicionales cuando el componente procesa el c M g o (antes de llamar a1 controlador del evento OnHTMLTag).
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
parametro T a g S t r ing) y devuelve un testo HTML distinto (en el parametro de referencia ReplaceText), con lo que se produce el resultado que muestra la figura 19.7.
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~od exe</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
-
TRUCO: El componente Pageproducer soporta etiquetas definidas por el usuario, que puede ser cualquier cadena que se quiera, pero deberian revisarse en primer lugar las etiquetas especiales definidas en la enurneracion TTags. Entre 10s posibles valores se incluyen t g L i n k (para la etiqueta de enlace), tgImage (para la etiqueta de imagen), tgTable (para la etiqueta de tabla) y algunas mas. Si se crea una etiqueta personalizada, como en el ejemplo Pageprod, el valor del parametro Tag del controlador HTMLTag sera t g C u s t o m .
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.
TRUCO: El componente DataSetTableProducer comienza desde el registro actual en lugar de desde el primero. Por eso, la segunda vez que se haga clic sobre el boton Print Table no se verb ningun registro en la salida. A1 afiadir . . k' . . . una Ilamaaa a1 mecoao -. 1 r . s ~ ; ael conjunto ae aaros antes ae llamar a1 metodo content del componente productor se solucionara este problema.
., . .
..
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
Figura 19.9. El editor de la propiedad Columns del componente DataSetTableProducer permite acceder a una vista previa de la tabla HTML final (si la tabla de la base de datos esta activa).
Se pueden utilizar tres tecnicas para personalizar la tabla HTML, y merece la pena revisarlas: Puede usarse la propiedad columns del componente productor de la tabla para establecer propiedades, como el texto y el color del titulo, o el color y el alineamiento de las celdas en el resto de la columna. Pueden usarse las propiedades TField, en particular las relacionadas con la salida. En el ejemplo, hemos fijado la propiedad DisplayFormat
del objeto de campo ClientDataSetlArea como # # # , # # # , # # # . Se trata del enfoque a utilizar si se quiere definir la salida de cada campo. Incluso podria irse mas alla e incluir etiquetas HTML en la salida de un campo. Se puede controlar el evento 0 n F o r m a t C e 1 1 del componente DataSetTableProducer para personalizar aun mas la salida. En este evento, pueden fijarse 10s diversos atributos de columna solamente para una celda dada, per0 tambien puede personalizarse la cadena de salida (guardada en el parametro Ce llDat a) e incluir etiquetas HTML. No se puede hacer esto mediante la propiedad Columns. En el ejemplo HtmlProd hemos usado un controlador para este evento; asi convertimos el texto de las columnas Population y Area a una fuente en negrita y con un fondo rojo para valores grandes (a menos que se trate de la fila de cabecera). Este es el codigo:
procedure TFormProd.DataSetTableProducerlFormatCell( Sender: TObject; CellRow, Cellcolumn: Integer; var BgColor: THTMLBgColor; var Align: THTMLAlign; var VAlign: THTMLVAlign; var CustomAttrs, CellData: String) ; begin i f (CellRow > 0 ) and ( ( (CellColumn = 3 ) and (Length (CellData) > 8 ) ) or ((CellColumn = 4 ) and (Length (CellData) > 9 ) ) ) then begin BgColor : = 'red '; CellData : = '<b>' + CellData + '</b>'; end; end;
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:
A continuacion se puede actualizar el codigo del controlador del evento OnForma t C e l l con la siguiente accion (en lugar de las dos lineas que modifiquen el color y aiiadan la etiqueta de fuente en negrita):
La hoja de estilo proporcionada ( t e s t . c s s , disponible en el codigo fuente del ejemplo) define un estilo de resaltado o h i g h l i g h t , que tiene una fuente en negrita y un fondo rojo que se encontraban incrustados en el codigo en el primer componente DataSetTableProducer. La ventaja de este enfoque es que ahora un artista grafico puede modificar el archivo CSS y proporcionar un mejor aspect0 a la tabla sin tocar su codigo. Cuando se quieren ofrecer muchos elementos de formato, el uso de una hoja de estilo tambien puede reducir el tamaiio total del archivo HTML. Se trata de un elemento muy importante que puede reducir el tiempo de descarga.
En el caso de que no se encuentre el parametro (o no sea valido), el servidor respondera con un menu basado en HTML de 10s componentes disponibles:
Html : = ' < h l > H t m l P r o d M e n u < h l > < p > < u l > '; for I : = 0 to Componentcount - 1 do if Components [i] is TCustomContentProducer then Html : = Html + ' < l i > < a href="/' + Components [i].Name + "'>' + Components [i].Name + ' < / a > < / l i > ' ; Html : = Html + ' < / u l > < / p > ';
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 ;
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.
-- -
..
---.
----
--
A D ~ ~ W & K I A :para probpr algunos de 10s ejemplos de este capitulo se n e c e s ~ a c c e s o un m i d o r w&. L'B sqlpcibn mas sencilla es utilizar la a --
En este capitulo se tratan 10s siguientes temas: Paginas Web dinamicas. CGI, ISAPI y modulos de Apache. La arquitectura de WebBroker. Web App Debugger. La arquitectura de WebSnap. Adaptadores y guiones de servidor.
- - - - - - . - - - - - -- NOTA: Hay que tener presente que la tecnologia WebBroker de Delphi (disponible en las ediciones Enterprise Studio y Professional) reduce las diferencias entre CGI y las API de servidor a1 ofrecer un marco de trabajo de clases comun. De este modo, se puede convertir con facilidad una aplicacion CGI en una biblioteca ISAPI o integrarla con Apache.
-
- --- -
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
( ' 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 > ' ); ('</head><body>') ; ('<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 > ' );
( I
Los programas CGI generan normalmente un encabezamiento que va seguido del texto HTML utilizando la salida estandar. Si ejecutamos directamente este programa, podremos ver el texto en una ventana de terminal. Si por el contrario, lo ejecutamos desde un servidor Web y enviamos la salida a un navegador, aparecera el texto HTML formateado como muestra la figura 20.1.
0- -t4) --,
Rhm
@ IU
r
7
yi
Ad
R y p a C
lmo
---
---
. -
IS
La creacion de aplicaciones avanzadas y complejas con CGI requiere mucho trabajo. Por ejemplo, para extraer informacion de estado sobre la solicitud HTTP, es necesario acceder a variables relevantes del entorno, tal y como sigue:
/ / obtiene el nombre de la ruta GetEnvironmentVariable ( ' P A T H - I N F O ' , (PathName)) ;
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.
En cada caso, Delphi generara un proyecto con un WebModule, que es un contenedor no visual similar a un modulo de datos. Esta unidad sera identica, sin importar el tip0 de proyecto; so10 cambia el archivo principal del proyecto. Para una aplicacion CGI tendra este aspecto:
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- U. L U ~ L U U I C S varios mbdulos Web. o . -. .- . .. - - . . . . - . . .. . . . . . . . . -.S ~ . . .Tambitn hay que tener en cuenta que todas las acciones del WebDispatcher no tienen nada que ver con las acciones almacenadas en un componente
.
- -7-
--
- -
-- -
- -
---
~ * l I
..
- - I S - .
-. .
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;
En la propiedad C o n t e n t del parametro R e s p o n s e es donde tenemos que insertar el codigo HTML que queremos que vean 10s usuarios. El unico inconveniente de este codigo es que en un navegador la salida se mostrara correctamente en varias lineas, per0 si miramos el codigo fuente HTML, podremos ver una sola linea que corresponde a toda la cadena. Para que el codigo HTML sea mas legible, tenemos que insertar el codigo de caracter #13 (nueva linea), dividiendolo en multiples lineas (o aun mejor, el valor multiplataforma s L i n e B r e a k ) . Para permitir que otras acciones controlen esta solicitud, tenemos que definir el ultimo parametro, Hand l e d , como F a l s e . De otra forma, el valor predeterminado sera T r u e y una vez que hayamos controlado la solicitud con la accion, el WebModule asumira que hemos terminado. La mayor parte del codigo de una aplicacion Web se encuentra en 10s controladores del evento OnAct i o n para las funciones definidas en el contenedor WebModule. Estas funciones reciben una solicitud del cliente y devuelven una respuesta utilizando para ello 10s parametros Request y Response. Cuando se utilizan componentes productores, el evento O n A c t i o n suele devolver, como R e s p o n s e .C o n t e n t , el C o n t e n t del componente productor, con una simple operacion de asignacion. Se puede acceder directamente a este codigo asignando un componente productor a la propiedad P r o d u c e r de la accion, sin necesidad de tener que escribir nunca mas estos controladores de evento (pero no hay que hacer ambas cosas, pues podria traer problemas).
'
clases productoras personalizadas que+h&edartde la c b 7 ! c u s tomcontent Producer, sin0 queimplernentan la i n t e r f e 3Pr~duceContent. &a propiedad P r o d u c e r C o n t e n t es.casi una propiedad de interfaz que se comporta de la misma manera gracias orsu editor depropiedad y no esta basada en el soporte para propiedadeg-de i n t e d b de DeIphi 6.
pcticiones en un puerto que puede especificarse (de manera predefinida el 1024). Cuando llega una peticion, el programa puede redirigirla a un ejecutable independiente. En Delphi 6, esta comunicacion se basaba en las tecnicas de COM; en Delphi 7 se basa en sockets de Indy. En ambos casos, puede ejecutarse la aplicacion de servidor Web desde el IDE de Delphi, establecer todos 10s puntos de ruptura necesarios y, despues, (cuando el programa se active mediante el Web App Debugger) depurar el programa tal y como se haria con un sencillo archivo ejecutable. El Web App Debugger hace un buen trabajo con el registro de todas las peticiones recibidas y las respuestas devueltas a1 navegador. El programa tambien tiene una pagina Statistics que realiza el seguimiento del tiempo necesario para cada respuesta, con lo que se puede comprobar la eficacia de una aplicacion en distintas condiciones. Otra nueva caracteristica del Web App Debugger en Delphi 7 es que ahora es una aplicacion CLX en lugar de una aplicacion VCL. Este cambio en la interfaz de usuario y la conversion de COM a sockets se han llevado a cabo para que pueda utilizarse en Kylix.
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 w e t d s D
G J o
sewer~nfo Sewerlnfo WSnapl WSnapl WSnap2 WSnap2 WSnapMD WSnapMD WSnapSess~on WSnapSession 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.
ADVERTENCIA: Aunque en la m y a & de los c m $epoch4adaptar un programa de una tecnologia W b a @m, alemprs serh aqi. ?or ejemplo, e no en el ejempk, CustQueP (&l que ya hablaratwa), heboa t e d o q u e evitar la propiedad ScriptName de la pt;tici(m (ips fi&onsr, pare m programa CGI) y u t d h t e n so h g propmbd knternb13.@l;$pt~ame. gr
Existen otros dos elementos bastante interesantes involucrados en el uso de Web App Debugger. En primer lugar, se pueden probar 10s programas sin tener instalado un servidor Web y sin tener que ajustar su configuration. En otras palabras, no es necesario desplegar 10s programas para probarlos (se pueden probar inmediatamente). En segundo lugar, en vez de realizar un desarrollo rapi-
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).
Hemos aiiadido a1 final del proceso de generacion de la pagina, el codigo HTML inicial y final porque esto permite que 10s componentes produzcan el HTML como si lo estuvieran haciendo ellos todo. El hecho de empezar con HTML en el evento OnBeforeDispatch,significa que no podemos asignar de forma directa 10s componentes productores a las funciones, ya que el componente productor sobrescribiria la propiedad Content que ya hemos proporcionado en la respuesta.
El componente PageTail incluye una etiqueta personalizada para el nombre del guion, reemplazado por el siguiente codigo, el cual utiliza el objeto de solicitud actual disponible dentro del modulo Web:
procedure TWebModulel.PageTailHTMLTag(Sender: TObject; Tag: TTag; const TagString: String; Tagparams: TStrings; var ReplaceText: String); begin if TagString = 'script' then ReplaceText : = Request.ScriptName; end ;
Este codigo se activa para expandir la etiqueta <#script > de la propiedad HTMLDoc del componente PageTail. El codigo de las acciones de hora y fecha es sencillo. Realmente, la parte interesante empieza con la ruta del Menu, que es la accion predeterminada. En su controlador del evento OnAct ion,la aplicacion utiliza un bucle for para crear una lista de las acciones disponibles (usando sus nombres sin las dos primeras letras, las cuales son siempre Wa, en este ejemplo), proporcionando un vinculo para cada una de ellas con un ancla (una etiqueta <a>):
procedure TWebModulel.MenuAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var I: Integer; begin Response.Content : = ' < h 3 > M e n u < / h 3 > < ~ 1 > ' # 1 3 ; for I : = 0 to A c t i o n s - C o u n t - 1 do Response .Content : = Response. Content + <li> <a href="' Request .ScriptName + Action[I] .PathInfo + ' "> + Copy (Action[I].Name, 3, 1000) + '</a>'#13; Response.Content : = Response.Content + '</ul>'; end;
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
Response .Content : = ' <h3>Status</h3>'#13 + 'Method: ' + Request.Method + '<br>'#13 + ProtocolVersion: ' + Request.ProtocolVersion + '<br>'#13 + 'URL: ' + Request-URL + '<br>'#13 + 'Query: ' + Request.Query + '<br>'#13 + ...
Para producir toda la tabla, simplemente conectamos el DataSetTableProducer a la propiedad P r o d u c e r de las acciones correspondientes sin proporcionar ningun controlador de evento especifico. La tabla se hace mas potente si aiiadimos vinculos internos a 10s registros especificos. El codigo siguiente se ejecuta para cada celda de la tabla, per0 solamente se crear un enlace para la primera columna a partir de la primera fila (no se incluye la celda del titulo):
procedure TWebModulel.DataSetTableProducerlFormatCell(Sender: TObj ect; CellRow, CellColumn: Integer; var BgColor: THTMLBgColor; var Align: THTMLAlign; var VAlign: THTMLVAlign; var CustomAttrs, CellData: String) ; begin if (CellColumn = 0) and (CellRow <> 0) then CellData : = '<a href="' + ScriptName + '/record?LastName=' + dataEmployee [ ' LastName' ] + ' &FirstName=' + dataEmployee [ ' FirstName' ] + ' "> ' + CellData + ' </a>'; end;
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
L
First Name
Phone Ext.
250 233 22 410 229
Hire Date
12/28/1988 12/28/1988
Salary
105900 97500 102750
Nelson young
Lambcrt
Robert
4
5
2
Bruce
Kun
Leshe
Johnson
Forest
--
64635 75060
Phil
f Figura 20.3. La sahda correspondiente a la ruta table del ejemplo BrokDemo, que genera una tabla HTML con h~pervinculos internos.
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;
j i 0 W D '
=='
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 >
NOTA: A1 igual que en Delphi, un formulario HTML contiene una serie de controles. Existen herramientas visuales que ayudan a disefiar estos formularios, o tambien se puede escribir manuaimente el &dig0 HTML.Entre 10s controles disponibles se incluyen botones, cuadros de edicibn, (selecciones) cuadros combinados y botones de entrada (o de radio). Tambien se pueden . .* . . . . .. . . . aerinir oorones con ripos especmcos como ae envlo o reinlciallzacion. un elemento muy importante de 10s formularios es el metodo de envio, que puede ser POST (10s datos se envian de forma oculta, y se reciben en la . . - . - . . - -- . . . . . .. propledad c o n t e n t F i e l d s ) o GET (10s datOS se pasan como parte ael URL, y se pueden extraer de la propiedad Q u e r y F i e l d s ) .
>-.-~-1-L-*.
-
3 .
. . ! - l . ..tl.
.I.
Hay un elemento importante que debemos tener en cuenta en el formulario: 10s nombres de 10s componentes de entrada (State y Country) deberian de coincidir con 10s parametros de un componente SQLQuery:
SELECT customer, FROM CUSTOMER WHERE
State-Province,
Country
State-Province
Este codigo se utiliza en el ejemplo CustQueP. Para crearlo, hemos puesto un componente SQLQuery dentro del WebModule y hemos generado 10s objetos de campo adecuados. En el mismo WebModule hemos aiiadido un componente SQLQueryTableProducer que se encuentra conectado a la propiedad P r o d u c e r de la accion / s e a r c h . El programa genera la respuesta adecuada. Cuando se activa el componente SQLQueryTableProducer, llamando a su funcion C o n t e n t , este inicia el componente SQLQuery obteniendo 10s parametros de la solicitud HTTP. El componente puede examinar automaticamente el metodo de solicitud y luego utilizar la propiedad Q u e r y F i e l d s (si la solicitud es una solicitud GET) o la propiedad C o n t e n t F i e l d s (si la solicitud es POST). Un problema derivado del uso de un formulario HTML estatico (como el anterior), es que no indica 10s estados y paises que se pueden buscar. Para solucionar
este problema, podemos utilizar un control de seleccion en lugar de un control de edicion en el formulario HTML. Sin embargo, si el usuario aiiade un nuevo registro a la tabla de la base de datos, tendremos que actualizar la lista de elementos automaticamente. Como solucion final, podemos diseiiar la DLL ISAPI para producir un formulario sobre la marcha y rellenar 10s controles de seleccion con 10s elementos disponibles. Generaremos el HTML para esta pagina en la accion / f o r m , que esta conectada con un componente PageProducer. El PageProducer contiene el siguiente texto HTML que incluye dos etiquetas especiales:
<h4>Customer Queryproducer Search Form</h4> <form action="CustQueP.dll/search" method="POST"> <table> <trXtd>State:</td> <tdXselect name="StateW><option> </
option><#State-Province></select~/td~/tdX/tr>
<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;
:=
I ) ' ;
Este metodo utiliza un segundo componente SQLQuery,que hemos colocado manualmente en el formulario y conectado a un componente SQLConnection compartido. La figura 20.4 muestra la salida de este formulario.
State
Canada England
1
el estado actual de la base de datos.
Figura 20.4. La accion de formulario del ejemplo CustQueP produce un formulario HTML con un componente de selection que se actualiza dinamicamente para reflejar
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 : = ' < h t n ~ l > < h e a d > < t i t l e > C u s t o m e Record</title></ r 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.
ADVERTENCIA: Mientras que Delphi 7 soporta la version 2.0.39 de Apache, no soporta la mas actual y popular version 2.0.40, debido a un cambio en las interfaces de biblioteca. No se recomienza el uso de la version 2.0.39 ya que tiene un pioblema de seguridad. Hay disponible informacion sobre como ada~tar cbdiao de la VCL v conseguir aue 10smodulos el - . Sean compatibles con Apache 2.0.40 y superior en 10s grupos de noticias, gracias a 10s miembros del equipo de investigation y desarrollo de Borland. Actualmente se encuentra en el sitio Web de Bob Swart en el U I U
.. .-.
,.
..-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;
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;
Si no se establecen estos valores, Delphi les asignara valores predeterminados, que se construyen aiiadiendo 10s sufijos module y -handler a1 nombre de proyecto (con lo que se consiguen 10s dosiornbres que hemos comentado). Un modulo de Apache no suele desplegarse dentro de una carpeta de guiones, sino dentro de la subcarpeta modules del servidor (de manera predefinida, C:\Archivos de programa\Apache\modules). Hay que editar el archivo http.conf y aiiadirle una linea para cargar el modulo, de este modo:
LoadModule apachel-module modules/apachel.dll
Finalmente, tenemos que indicar cuando se invocara el modulo. El controlador definido por el modulo puede asociarse con una extension de archivo dada (para que el modulo procese todos 10s archivos que tengan esa extension) o con una carpeta fisica o virtual. En el ultimo caso, la carpeta no existe per0 Apache simula que esta alli. De esta manera podemos establecer una carpeta virtual para el modulo de Apache 1:
Ya que Apache tiene en cuenta las maylisculas (debido a su herencia de Linux), tambien podria desearse aiiadir una segunda carpeta virtual, en minusculas:
Ahora se puede llamar a la aplicacion de muestra mediante el URL http:// localhost/Apachel. Una gran ventaja del uso de carpetas virtuales en Apache es que un usuario no distingue realmente entre las partes fisicas y dinamicas del sitio, tal y como puede comprobarse si se experimenta con el ejemplo Apachel (que incluye el codigo aqui comentado).
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
'
una peticidn de o6a hebra tenga abieho el archivo p a i a 7 z 6 r a m i e - n t r esta otra hebra trate de abrir el archivo para escritura. Para soportar una situacibn parecida, sera necesario utilizar un rnutex (o una seccion critica en un programa multihilo) para permitir que cada hebra espere hasta que la hebra actual deje de utilizar el archivo cuando complete su tarea.
Es mas interesante crear un contador grafico que pueda incrustarse facilmente cn cualquier pagina HTML. Hay dos enfoques para crear un contador grafico: se puede preparar una imagen de bits para cada digito y combinarlos en el programa, o dejar que el programa dibuje sobre una imagen en memoria para producir el grafico quc se quiere devolver. En el programa WebCount hemos escogido la segunda tdcnica. Basicamente, puede crearse una componente Image que contenga una imagen en memoria, que puede pintarse mediante 10s metodos habituales de la clase TCanvas. Despues se puede conectar esta imagen a un objeto TJpegImage. A1 acceder a la imagen a travds del componente JpegImage la imagen se convierte a1 formato JPEG. Despues pueden guardarse 10s datos JPEG en un flujo y devolverlos. Como puede versej consiste en muchos pasos, pero el codigo no es complicado:
// c r e a una i m a g e n e n memoria Bitmap : = TBitmap.Create; t rY Bitmap.Width : = 120; B i t m a p - H e i g h t : = 25; // d i b u j a 1 0 s d i g i t o s Bitmap. Canvas. Font. Name : = ' A r i d 1 '; Bitmap.Canvas.Font.Size : = 14; Bitmap.Canvas. Font .Color : = RGB (255, 127, 0) ; Bitmap.Canvas.Font.Sty1e : = [fsBold]; Bitmap.Canvas .Textout (1, 1, ' H i t s : ' + FormatFloat ( ' # # # , # # # , # # # I , Int (nHit)) ) ; // c o n v i e r t e a JPEG y m u e s t r a Jpegl : = TJpegImage.Create; t rY Jpegl.CompressionQua1ity : = 50; Jpegl .Assign (Bitmap); S t r e a m : = TMemoryStream.Create; Jpegl SaveToStream (Stream); Stream.Position : = 0; Response.ContentStream : = Stream; Response. ContentType := ' i m a g e / j p e g ' ; Response.SendResponse; //el o b j e t o d e respuesta liberard el f l u j o finally Jpegl.Free; end; finally
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:
El metodo G r a b H t m l es identico al ejemplo WebFind. El metodo HtmlStringToCds es parecido a1 metodo correspondiente del ejemplo WebFind (que aiiade 10s elementos a un cuadro de lista): aiiade las direcciones y sus textos descriptivos mediante la Ilamada:
cds . InsertRecord
(
El componente ClientDataSet se configura con tres campos: dos cadenas mas un contador de linea. Este campo vacio adicional es necesario para incluir la columna adicional en el productor de la tabla. El codigo rellena la columna en el evento de formato de celda, que aiiade tambien el hiperenlace:
procedure TWebModulel.DataSetTableProducerlFormatCell(Sender: TObject; CellRow, CellColumn: Integer; var BgColor: THTMLBgColor; var Align: THTMLAlign; var VAlign: THTMLVAlign; var CustomAttrs, CellData: String); begin i f CellRow <> 0 then case CellColumn of 0 : CellData := IntToStr (CellRow); 1: CellData : = ' < a h r e f = " ' + CellData + "'>' + SplitLong (CellData) + ' < / a > '; 2 : CellData : = SplitLong (CellData); end; end;
La llamada a SplitLong se utiliza para afiadir espacios adicionales en el texto de salida, para evitar que las columnas de la cuadricula Sean demasiado grandes (el navegador no dividira el texto en varias lineas a no ser que contenga espacios u otros caracteres especiales). El resultado de este programa es una aplicacion bastante lenta (debido a las varias peticiones HTTP que debe reenviar) que produce una salida con el aspect0 que muestra la figura 20.5.
cod
Figura 20.5. El programa Websearch muestra el resultado de varlas bdsquedas realizadas mediante Google.
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-
logo New Items (File>New>Other) o bien utilizando la barra de herramientas Internet del IDE. El cuadro de dialogo resultante, que muestra la figura 20.6, nos permite escoger el tip0 de aplicacion (como una aplicacion WebBroker) y personalizar 10s componentes iniciales de la aplicacion (despues podremos ariadir alguno mas). La parte inferior del dialogo determina el comportamiento de la primera pagina (generalmente, la pagina predeterminada o de inicio del programa). Un cuadro de dialogo similar se mostrara tambiCn para paginas posteriores.
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.
Si escogemos 10s valores predefinidos y escribimos un nombre para la pagina de inicio, el cuadro de dialogo creara un proyecto y abrira un TWebAppPageModule.Este modulo contiene 10s componentes que hemos escogido, que son de manera predeterminada:
Un componente WebAppComponents: Es un contenedor para todos 10s servicios centralizados de la aplicacion WebSnap, tales como la lista de usuario, repartidores basicos, servicios de sesion, etc. No es obligado activar todas estas propiedades, ya que puede haber alguna aplicacion que no necesite todos 10s servicios.
El componente PageDispatcher: Ofrece uno de estos servicios basicos, y alberga automaticamente una lista de las paginas disponibles de la aplicac i h , al mismo tiempo que define la pagina predeterminada.
El componente AdapterDispatcher: Manipula 10s formularies HTML de envio y las peticiones de imageries. ApplicationAdapter: Es el primer componente de la familia de 10s adaptadores. Estos componentes proporcionan campos y funciones a 10s guiones de servidor evaluados por el programa. Concrctamente, el ApplicationAdapter es un adaptador de campos que muestra el valor de su propiedad A p p 1ica t ionT it le. Si introducimos un valor para esta propiedad, 10s guiones tambien podran disponer de ella. Un PageProducer: El modulo conticne un PageProducer que incluye el codigo HTML dc la pagina, en este caso la pagina predeterminada del programa. A diferencia de las aplicaciones WebBroker, el HTML para este componente no se almacena dentro de su propiedad HTMLDoc de lista de cadena, ni se hace referencia a el mediante su propiedad HTMLFile.El archivo HTML es un archivo externo, almacenado de manera predeterminada en la carpeta que alberga cl codigo fuente del proyecto y que se referencia desde la aplicacion mediante una sentencia similar a una sentencia de inclusion de recurso: { * . h t m l } Archivo HTML: Dado que el archivo HTML incluido por el PageProducer se guarda como un archivo independiente (el componente LocateFileService nos ayudara en su despliegue), podemos editarlo para cambiar la salida de una pagina del programa sin necesidad de recompilar la aplicacion. Gracias a1 soporte del guiones de servidor, estos cambios no se refieren solamente a la parte fija del archivo HTML, sino tambien a su contenido dinamico. A1 basarse en una plantilla estandar, el archivo HTML por defecto ya contiene algunos guiones.
---
---
..
--
ADVERTENCIA: El parecido entre incluir recursos y las referencias a HTML es basicamente sintictico. La referencia HTML se usa ~ 6 1 0 para
encontrar en tiempo de disefio el archivo, mientras que la directiva de inclusion de un recurso enlaza 10s datos a 10s que hace referencia con el archivo
..~
-
L - , x # -
Gracias a esa directiva es posible ver el archivo HTML dentro el editor de Delphi con un buen resaltado de sintaxis. Simplemente tendremos que seleccionar la solapa inferior correspondiente. El editor tambien tiene otras paginas para un modulo WebSnap, incluyendo de manera predefinida una pagina HTML Result en donde podemos ver el HTML generado despuis de evaluar 10s guiones y una pagina Preview que contiene lo que el usuario vera en un explorador. El editor de Delphi 7 para un modulo WebSnap tambien incluye un editor HTML mucho mas potente que el de Dephi 6; incluye un resaltado de sintaxis y unas prestaciones de completitud de codigo mucho mejores. Si se prefiere editar el codigo HTML de la
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)
uses WebReq, WebCntxt , WebFact function home : Thome ; begin Result end;
Variants;
initialization if WebRequestHandler <> nil then WebRequestHandler.AddWebModu1eFactory (TWebAppPageModuleFactory.Create(Thome, TWebPageInfo. Create([wpPublished { , wpLoginRequired)], ' . h t z d l ) , caCache) ) ; end.
El modulo utiliza una funcion global en vez de un objeto global tipico de formularios para soportar el almacenamiento en cache de las paginas. Esta aplicacion tambien tiene codigo adicional en la seccion de inicializacion (concretamente codigo de registro) para permitir que la aplicacion sepa c u d es la funcion de la pagina y su comportamiento.
- --
- - --
-- - ---A
- ..-
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
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)
(
if ( C > 0 ) s += ' & n b s p ;I & n b s p ; ' if (Page.Name ! = e.item ( ) .Name) s += ' < a href="' + e.item() .HREF + e.item() .Title + ' < / a > ' else s += e.item() .Title
C++
> .
< / td></table>
.
lugar de estar incrustado en un componente Delphi, podemos pasarselo a un buen diseiiador Web para que lo convierta en algo mas atractivo visualmente.
TRUCX$~ h a w publicar p a phgina o invertir d i d o estado, no hay Fiqa qoe frjarse en ningunapmpledad d d m 6 d d o & h $hg&i Web. Estc estado i
cod Uas~icontfola nn indici&x d&meto& kdd~izbEf6dule~actc)r~, mId.0 cn el &digb de in&&;ic~n del mMbltrld de hp&* Web. Simplemente mitntarribE 0n6dicf.mjndicador para ccmseguird efecto deseado. Como muestra de lo que podemos hacer mediante 10s guiones de lenguajes interpretados, hemos aiiadido a1 ejemplo WSnap2 (una ampliacion de WSnap l), una pagina demoscript. El guion de esta pagina puede generar una tabla completa; llena de valores multiplicados utilizando el siguiente codigo (vease la figura 20.8):
<table border=l cellspacing=O> <tr> <th> </th> <% for (j=l;j<=5;j++) ( %> <th>Column <%=j %></th>
<%
%>
</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>
En este guion, el simbolo <%=reemplaza a la orden Response .W r i t e . Otra caracteristica importante de la escritura de guiones de sewidor es la inclusion de unas paginas dentro de otras. Por ejemplo, si queremos modificar el menu, podemos incluir el codigo HTML relacionado y el guion en un unico archivo, en lugar de cambiarlo y mantenerlo en varias paginas. Incluir un archivo es tan sencillo como usar esta sentencia:
En el listado 20.1, podemos encontrar el codigo fuente completo del archivo incluido para el menu, a1 que se hace referencia desde todos 10s demas archivos HTML del proyecto. La figura 20.9, muestra un ejemplo de este menu, que se muestra en la parte superior de la pagina mediante el guion de generacion de tabla que ya hemos mencionado.
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
TObject; var Value:
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:
El componente PagedAdapter: Tiene soporte interno para mostrar su contenido en diversas paginas.
El componente DataSetAdapter: Se utiliza para acceder desde un guion a un conjunto de datos de Delphi y que veremos dentro de poco. El StringValuesList: Contiene una lista de pares nombrelvalor, en forma de lista de cadenas, que puede utilizarse directamente o para proporcionar una lista de valores a un campo de adaptador. El adaptador DataSetValueList heredado juega el mismo papel per0 obtiene la lista de pares nombrel valor de un conjunto de datos, proporcionando soporte para busquedas y otras selecciones.
Los adaptadores relacionados con el usuario, como 10s adaptadores EndUser, Endusersession y LoginForm, que se utilizan para acceder a informacion de sesion y usuario, y para crear el formulario de entrada para la aplicacion, el cual esta ligado automaticamente con la lista de usuarios. Hablaremos de estos adaptadores tambien mas adelante.
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
El adaptador tiene tambien un par de acciones utilizadas para enviar la entrada del usuario actual y para aiiadir un signo + a1 texto. El mismo signo se aiiade cuando activamos el campo Auto. Si utilizamos un codigo HTML basico, el desarrollo de la interfaz de usuario para este formulario y el guion relacionado con ella llevaria bastante tiempo. Pero el componente AdapterPageProducer (utilizado en esta pagina) tiene un diseiiador HTML integrado, que Borland llama Web Surface Designer. A1 utilizar esta herramienta, podemos aiiadir visualmente un formulario a la pagina HTML y aiiadirle un AdapterFieldGroup. Hay que conectar este grupo de campo a1 adaptador para que aparezcan automaticamente 10s editores para 10s dos campos. Despues podemos aiiadir un AdapterComrnandGroup y conectarlo a1 AdapterFieldGroup para conseguir botones para todas las acciones del adaptador. La figura 20.9 muestra un ejemplo de este diseiiador: Es decir, para ser mas precisos, 10s campos y 10s botones se muestran automaticamente si activamos las propiedades Add De f a u 1 t Fie 1 d s y AddDef aul tcommands de 10s grupos de campos y de ordenes. El efecto de estas operaciones visuales para construir este formulario queda resumido en el siguiente fragment0 de codigo DFM:
object AdapterPageProducer: TAdapterPageProducer object AdapterForml: TAdapterForm object AdapterFieldGroupl: TAdapterFieldGroup Adapter = Adapter1 object FldText: TAdapterDisplayField FieldName = ' Text ' end object FldAuto: TAdapterDisplayField FieldName = 'Auto' end end object AdapterCornrnandGroupl: TAdapterCommandGroup
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; Params:
TObject;
Params:
Como alternativa, 10s campos de 10s adaptadores tienen una propiedad EchoAc t ion Fie l d V a l u e publica que podemos establecer para obtener el valor introducido por el usuario y colocarlo en el formulario resultante. Esta tecnica se utiliza tipicamente en caso de errores, para permitir que el usuario modifique la entrada, partiendo de 10s valores ya introducidos.
hojas de estilo dn cascada ( ~ a s c a d i n ~ ~ Sheet, CSS). ~ e + p u e d e tyle definir la CSS para una pagina utilizando ya sea ia propiedad S t y l e s F i l e o la lista cie cadena s t y l e s . Cualquier elemento del editor de 10s elementos del productor puede definir un estilo basico o escoger un estilo del CSS enlazado. Se realiza esta uItima operation (que es el enfoque que se sugiere) utilizando la propiedad S t y l e R u l e ) .
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
El productor de pagina correspondiente tiene un formulario que contiene dos grupos de ordenes y una cuadricula. El primer grupo (que se muestra por encima de la cuadricula) tiene las siguientes ordenes predefinidas para manipular paginas: CmdPrevPage, CmdNextPage y CmdGotoPage. Esta ultima genera una lista de numeros para las paginas, para que de esta forma el usuario pueda acceder directamente a cada una de ellas. El componente AdapterGrid contiene las columnas predeteminadas y una mas que alberga 10s ordenes de edicion ( E d i t ) y borrado ( D e l e t e ) . El grupo de ordenes inferior tiene un boton que se utiliza para crear un nuevo registro. La figura 20.10 muestra un ejemplo del aspect0 de la tabla y la configuracion completa del AdapterPageProducer se muestra en el listado 20.2.
Listado 20.2. La configuracion del AdapterPageProducer para la pagina principal de WSnapTable.
o b j e c t AdapterPageProducer: TAdapterPageProducer object AdapterForml: TAdapterForm object AdapterCommandGroupl: TAdapterCommandGroup Displaycomponent = AdapterGridl object CmdPrevPage: TAdapterActionButton ActionName = ' P r e v P a g e ' Caption = ' P r e v i o u s P a g e ' end object CmdGotoPage: TAdapterActionButton ... object CmdNextPage: TAdapterActionButton ... Act ionName = ' N e x t P a g e ' Caption = ' N e x t P a g e ' end end object AdapterGridl: TAdapterGrid TableAttributes.Cel1Spacing = 0 TableAttributes.Cel1Padding = 3 Adapter = DataSetAdapterl AdapterMode = ' B r o w s e '
...
object AdapterCommandColumnl: TAdapterCommandColumn Caption = 'COMMANDS' object CmdEditRow: TAdapterActionButton ActionName = 'EditRow' Caption = 'Edlt' PageName = 'formview' DisplayType = ctAnchor end object CrndDeleteRow: TAdapterActionButton ActionName = 'DeleteRowt Caption = 'Delete' DisplayType = ctAnchor end end end object AdapterCommandGroup2: TAdapterCommandGroup Displaycomponent = AdapterGridl object CmdNewRow: TAdapterActionButton Act ionName = ' NewRow' Caption = 'New' PageName = ' formview' end end end end
\\'SnapTa ble
table
Prenous Page 1 2
3 Ntxt Pme
Figura 20.10. La pagma que rnuestra el ejemplo WSnapTable al inicio incluye la parte in~cial una tabla paginada. de
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: ~ambi6n pucdc modificarse el m o d ~ adaptador mediante gguiodel
o ode .
En segundo lugar, hemos modificado la forma de representar las ordenes en la cuadricula usando el valor c tAnchor para la propiedad Di splayT ype, en lugar de utilizar el boton de estilo predeterminado. En la mayoria de 10s componentes de esta arquitectura encontraremos propiedades similares, que nos permiten ajustar el codigo HTML que producen.
ActionName = ' C a n c e l ' PageName = ' t a b l e ' end object CmdDeleteRow: TAdapterActionButton ActionName = ' D e l e t e R o w l Caption = ' D e l e t e ' PageName = ' t a b l e 1 end end object AdapterFieldGroupl: TAdapterFieldGroup Adapter = table.DataSetAdapter1 AdapterMode = ' E d i t ' object FldCUST-NO: TAdapterDisplayField Displaywidth = 10 FieldName = ' CUST-NO' end object FldCUSTOMER: TAdapterDisplayField end end end
-iJp1
--
- ----. - -- . -
- --
--
AdaplmPagcRoduar AdaptetForrnl
E
@fowsw HTML Scrlpl
Apply
Cancel
I Delete (
L
A
CUST-NO pE7S~gnature Des~gn CXTsTOhE.R ~c Bivd ADDRESS-LINE1 15500 P a c ~ f He~ghts San D~ego CFY STATE_PRO\ WCE CA USA COUNTRY
Q
2l
Figura 20.11. La pag~na forrnv~ewmostrada por el ejernplo WSnapTable en t~ernpo de ejecuc~on, el Web Surface Des~gner el ed~torde Adapterpageproducer). en (o
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
La segunda pagina no se publica, porque seleccionarla sin hacer referencia a un registro especifico no tendria sentido. Para no publicar la pagina, hay que comentar el indicador correspondiente en el codigo de inicializacion. Finalmente, para hacer que 10s cambios en el conjunto de datos Sean persistentes, podemos llamar a1 metodo A p p l y u p d a t e s en 10s eventos O n A f t e r P o s t y O n A f t e r D e l e t e del componente C l i e n t D a t a S e t que esta en el modulo de datos. Otro problema (el cual no hemos arreglado) tiene que ver con el hecho de que el servidor SQL asigne un ID a cada cliente, de manera que cada vez que introducimos un nuevo registro, no se alinean 10s datos en el ClientDataSet y en la base de datos. Esto puede causar errores de tipo "Record Not Found" que indican que no se encuentra un registro, por este problema de desalineamiento.
El componente DataSetAdapter tiene un soporte especifico para las relaciones maestroldetalle entre 10s conjuntos de datos. Despues de crear la relacion entre 10s conjuntos de datos, como siempre, definimos un adaptador para cada uno de ellos y, a continuation, conectamos la propiedad M a s t e r A d a p t e r del adaptador del conjunto de 10s datos de detalle. A1 establecer la relacion maestroldetalle entre 10s adaptadores hacemos que estos trabajen de una forma mas fluida. Por ejemplo, cuando cambiamos el mod0 de trabajo del conjunto maestro, o introducimos nuevos registros, el conjunto de detalle pasa automaticamente a1 mod0 de edicion o se actualiza. El ejemplo WSnapMD utiliza un modulo de datos para definir una relacion de ese tipo. Incluye dos componentes c 1i e n t D a t ase t ,cada uno de ellos conectado a un SQLDataSet mediante un proveedor. Cada componente de acceso a datos se refiere a una tabla, y 10s componentes C l i e n t D a t aSe t definen una relacion maestroldetalle. El mismo modulo de datos contiene dos adaptadores de conjunto de datos que se refieren a 10s dos conjuntos y siguen definiendo dicha relacion:
object dsaDepartment: TDataSetAdapter DataSet = cdsDepartment end object dsaEmployee: TDataSetAdapter DataSet = cdsEmployee MasterAdapter = dsaDepartment end
fallo cierra el conjunto de datos en cada interaction, con lo que se pierde information de estado. La unica pagina de esta aplicacion WebSnap tiene un componente AdapterPageProducer conectado a ambos adaptadores. El formulario de esta pagina tiene un grupo de campos enganchado a1 maestro y una cuadricula conectada con el detalle. A diferencia de otros e.jemplos, hemos tratado de me.jorar la interfaz de usuario aiiadiendo atributos personalizados para diversos elementos. Hemos usado un fondo gris, mostrado algunos bordes de cuadricula (Web Surface Designer suele usar cuadriculas HTML), centrado la mayoria de 10s elementos y aiiadido espacios. Fijese en que hemos aiiadido espacios adicionales a 10s titulos de boton para impedir que Sean demasiado pequeiios. Puede verse el codigo relacionado en el siguiente fragment0 detallado y su efecto en la figura 20.12:
object AdapterPageProducer: TAdapterPageProducer object AdapterForrnl: TAdapterForrn Custom = 'Border="lU CellSpacing="OrrCellPadding="lO"
' +
' BgColor="Silver"
align="center"'
object AdapterCommandGroupl: TAdapterCommandGroup Displaycomponent = AdapterFieldGroupl Custom = 'Align="Center"' object CmdFirstRow: TAdapterActionButton . . . object CmdPrevRow: TAdapterActionButton ... object CmdNextRow: TAdapterActionButton . . . object CmdLastRow: TAdapterActionButton ... end object AdapterFieldGroupl: TAdapterFieldGroup Custom = ' BgColor="Silver " ' Adapter = WDataMod.dsaDepartment AdapterMode = ' Browse' end object AdapterGridl: TAdapterGrid TableAttributes.BgCo1or = 'Silver' TableAttributes.CellSpacing = 0 TableAttributes.CellPadding = 3 HeadingAttributes. BgColor = ' Gray' Adapter = WDataMod.dsaEmployee AdapterMode = 'Browse' object ColEMP-NO: TAdapterDisplayColumn ... object ColFIRST-NAME: TAdapterDisplayColumn object ColLAST-NAME: TAdapterDisplayColumn ... object ColDEPT-NO: TAdapterDisplayColumn . . . object ColJOB-CODE: TAdapterDisplayColumn ... object ColJOB-COUNTRY: TAdapterDisplayColumn ... object ColSALARY: TAdapterDisplayColumn ... end end end
...
First
Revtous
Nexl
Lns!
Tm
I
Lee
Bender
000 000
' A h
USA /USA
53733
CEO
,212850
1&3'sf0
-' d.
q
.
Figura 20.12. El ejemplo WSnapMD muestra una estructura maestroldetalle y tiene una representacion personalizada.
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 [ 'SessionHits '1 ) + 1 ; ext end:
NOTA: El objeto Webcontext (de tipo ~ ~ e b ~ o n t e x t ) variaes una ble de hebra creada por Websnap para cada'peticih. Proporciona un a c a so seguro fiente a hebras a otras d k s globales usadas por el programa.
El codigo HTML asociado muestra inforrnacion de estado utilizando tanto etiquetas personalizadas evaluadas por el evento OnTag del productor de pagina como el guion evaluado por el motor. Esta es la parte mas importante del archivo HTML:
<h3>Plain Tags</h3> <p>Session id: <#SessionID> <br>Session hits : < # S e s s i o n ~ i ts></p> <h3>Script</h3> <p>Session hits (via application) : <%=Application. SessionHi ts. Val ue%> <br>Application hits: <%=Application.Hits.Value%></p>
Los parametros de la salida 10s proporciona el controlador del evento OnTag y 10s eventos OnGetValue de 10s campos:
procedure TSessionDemo.PageProducerHTMLTag(Sender: TObject; Tag: TTag; const TagString: String; Tagparams: TStrings; var ReplaceText : String) ; begin if TagString = 'SessionID' then ReplaceText : = WebContext.Session.Session1D else if TagString = 'SessionHitsl then ReplaceText : = WebContext.Session.Va1ues ['SessionHits'] end; procedure TSessionDemo.HitsGetValue(Sender: Variant) ; begin Value : = nHits; end; TObject; var Value:
(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 TR y
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.
rios registrados. Esto se lleva a cab0 activando el indicador wpLoginRequired en el constructor de las clases TWebPageModuleFactory y TWebAppPageModuleFactory en el codigo de inicializacion de la unidad de la pagina Web.
-. .
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
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
}
:></hl>
%> i f ( E n d U s e r . L o g o ~ r tE n a b l e d )
( %>
< 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.
Desde 10s dias de Delphi 2, Chad Z . Hower se ha encargado de la creacion de una arquitectura Delphi que simplifique el desarrollo de aplicaciones Web, con la idea de hacer que la programacion Web tan simple y visual como la programacion de formularios Delphi estandar. Algunos programadores estan completamente acostumbrados a las tecnologias HTML, JavaScript, hojas de estilo en cascada y las mas recientes tecnologias de Internet. Otros programadores simplemente quieren crear aplicaciones Web en Delphi del mismo mod0 en que crean aplicaciones VCL 0 CLX. IntraWeb esta pensada para este segundo tipo de desarrolladores, aunque es tan potente que, incluso programadores expertos en Web pueden sacar partido de su utilizacion. Segun Chad, IntraWeb esta pensado desarrollar aplicaciones Web, no sitios Web. Aun mas, 10s componentes de IntraWeb pueden usarse en una aplicacion especifica o pueden utilizarse en un programa de WebBroker o WebSnap. En este capitulo no hablaremos en detalle de IntraWeb, ya que es una biblioteca muy grande, como 50 componentes instalados en la paleta de Delphi y varios diseiiadores de modulos. Lo que vamos a hacer es comentar su base, de manera que pueda sopesarse su uso para futuros proyectos o para partes de estos proyectos, lo que resulte mas adecuado.
'I I
I)
TRUCO: Si se desea conseguir documentation sobre IntraWeb, se pueden consultar 10s manuales PDF que se encuentran en el Companion CD de Delphi 7. Si no pueden encontrarse aqui, tambidn se pueden descargar desde el sitio Web de Atozed Software. Para soporte sobre IntraWeb, es conveniente acudir a 10s grupos de noticias de Borland.
NOTA: Este capitulo ha sido especialmente revisado por Chad Z. Hower (tambien conocido como "Kudzu", el autor y coordinador de proyecto original de Internet Direct (Indy) y autor de IntraWeb. Entre las especialidades de Chad se incluyen las redes y programacion TCPIIP, la comunicacion entre procesos, la programacion distribuida, 10s protocolos de Lnternet y la programacion orientada a objetos. Cuando no esta programando, tambiin le gusta montar en bicicleta, kayak, escalar, descender en ski, conducir y hacer casi cualquier cosa a1 aire libre. Chad tambien publica articulos, programas y herramientas gratuitas (y otras curiosidades) en Kudzu World Chad en el URL http:llwww.Hower.o~Kudrul. es un estadounidense expatriado que pasa sus veranos en San Petersburgo (Rusia) y sus inviernos en Limassol (Chipre). Se puede contactar con Chad a traves de cpub@Hower.org.
En este capitulo trataremos 10s siguientes temas: IntraWeb, aplicaciones Web y sitios Web. Uso de componentes IntraWeb. lntegracion con WebBroker y WebSnap. Aplicaciones Web de bases de datos. Uso de componentes de cliente.
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.
TRUC0 :Entre 10s componentes de terceras partes parar IntraWeb se inclu yen IW'Char de Steema (10s creaclores de Teechart), l N Bold de CentillelY 7 .. - - -. -- - -
(para la 1ntegraci6n con Bold), IWOpenSource, IWTranslator, IWD~alogs, IWDataModulePool d e Arcana, IW Component Pack d e T M S y IWGranPrimo de GranPrimo. Se puede encontrar una lista actualizada de este tip0 de componentes en www.atozedsoftware.com.
Aunquc no se dispone del codigo fuente para la biblioteca central (disponible bajo solicitud y previo pago), la arquitectura IntraWeb es bastante abierta, y el codigo fuente completo dc 10s componentes esta plenamente disponible. IntraWeb forma ahora parte de la instalacion estandar de Delphi, per0 tambien esta disponible para Kylis. Si se escriben con cuidado, las aplicaciones IntraWeb pueden ser completamente multiplataforma.
siones para C++ Builder y Java. Se esta trabajando en una version .NET y probablemente estara disponible junto con una futura version de Delphi
Como propietario de Delphi 7, se puede recibir la primera publicacion de una actualizacion significativa (la vcrsion 5. I) y se pucde actualizar la licencia a una version completa de la edicion Enterprise de IntraWeb que incluye actualizaciones y soporte de Atozed Software (como puede comprobarse en su sitio Web). Si se desea una documentacion mas completa (archives de ayuda y PDF), es recomendable acceder a esta actualizacion a la version 5.1.
Por el momento, creemos un ejemplo (llamado IWSimpleApp en el codigo fuente del libro). Para construirlo, habra que seguir estos pasos: 1. Accedemos a1 formulario principal del programa y le aiiadimos un boton, un cuadro de edicion y un cuadro de lista desde la pagina IW Standard de la paleta de componentes, Es decir, no aiiadimos 10s componentes VCL de la pagina Standard de la paleta, sino que utilizaremos 10s correspondientes componentes IntraWeb: IWButton, IWEdit y IWListbox. 2. Modificaremos ligeramente sus propiedades de esta manera:
object IWButtonl: TIWButton Caption = 'Add I t e m ' end object IWEditl: TIWEdit Text = ' f o u r ' end object IWListboxl: TIWListbox 1tems.Strings = ( 'one ' 'two' ' t h r e e ') end
3. Controlamos el evento O n c l i c k del boton haciendo doble clic sobre el componente en tiempo de diseiio (como siempre) y escribiendo este codigo tan familiar:
procedure TforrnMain.IWButtonlClick(Sender: begin 1WListBoxl.Items.Add (1WEditl.Text); end; 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
3wmi- 1 -Ia
lnlraweb Version. 5 0 43
Fie
Run I.ldp
HTTP Pmt.8080
Packaged Enterprise L~cense Number 0
II
,, ,
El codigo IntraWeb es bbicamente codigo de servidor, pero IntraWeb tambien puede generar JavaScript para controlar algunas de las caracteristicas de la aplicacion. Tambien puedc e.jecutarse codigo adicional en el lado del cliente. Para haccr esto se utilizan componente especificos de cliente de IntraWeb o se escribe un codigo JavaScript personalizado. Como comparacion, 10s dos botones de la parte inferior del formulario en el ejemplo IWSimpleApp muestran un cuadro de mensaje utilizando dos tecnicas distintas. El primer0 de 10s dos botones (IWButton2) muestra un mensa.je mediante un cvento de servidor, con este codigo Delphi:
procedure T f o r m M a i n . 1 ~ B u t t o n 2 C l i c k ( S e n d e r : TObject); var nItem: Integer; begin nItem : = 1WListboxl.ItemIndex; if nItem >= 0 then WebApp1ication.ShowMessage (1WListBoxl.Items [nItem]) else WebApplication.ShowMessage ('No ltem s e l e c t e d ' ) ; end;
El segundo de estos dos botones (IWButton3) utiliza JavaScript, que se inserta cn el programa Delphi preparando el controlador de eventos JavaScript apropjado en cl cditor especial de propiedades para la propiedad S c r i p t E v e n t s :
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:
IntraWeb utiliza estilos para determinar no solo la apariencia visual de cada componente, como su fuente y color, sin0 tambien la posicion del componente, mediante el posicionamiento absoluto predetern~inado. Cada estilo se ve afectado por un cierto ni~mero propiedades de 10s componentes IntraWeb, asi que se de puede experimentar sin problemas si se sabe algo de ho-jas de estilo. Si no se esta familiarizado con las ho-ias de estilo, lo mas facil es utilizar simplemente las propiedades y confiar en que IntraWeb hara lo mejor que pucda para representar 10s componentes en la pagina Web. El segundo bloque consiste cn codigo de guiones JavaScript. El bloque de guiones principal contiene el codigo de inicializacion y el codigo de 10s controladores de evcntos de cliente para 10s componentes, como el estracto siguiente:
function IWBUTTONl-OnClick(ASender) ( r e t u r n SubmitClickConf irm( 'IWBUTTONI
)
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)
(
La seccion de guiones de la pagina tambien hacer referencia a otros archivos necesarios para el navegador y que IntraWeb pone a su disposicion. Algunos de estos archivos son genericos: otros estan enlazados con un navegador especifico: IntraWeb detecta el navegador que se esta utilizando y devuelve un codigo JavaScript y archivos basicos JavaScript distintos.
. -
- .
- . --
--
..-
- ..
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:
La etiqueta f o r m contiene componentes especificos de la interfaz de usuario, como botones y cuadros de edicion:
<input type="TEXTV name="IWEDITl" size="17" value="fourl' id="IWEDITlW class="IWEDITlCSS"> < i n p u t v a l u e = " A d d I t e m " name=" I W B U T T O N l " t y p e = " b u t t o n " o n c l i c k = " r e t u r n IWBUTTONl-OnClick(this);" id="IWBUTTONl" class="IWBUTTONlCSS">
El formulario tiene tambien algunos componentes ocultos que IntraWeb utiliza para llevar informacion entre el cliente y el servidor, Sin embargo, el URL es el mod0 mas importante de pasar informacion en IntraWeb; en el programa tendra este aspecto:
La primera parte es la direccion IP y el puerto que suelen utilizarse para la aplicacion IntraWeb independiente (cambia cuando se usa una arquitectura distinta para desplegar el programa), seguida del comando EXEC, un numero de creciente peticion y un identificador de sesion. Ya hablaremos mas tarde de la sesion, pero por ahora bastara con saber que IntraWeb utiliza un elemento del URL en lugar de cookies para permitir el acceso a sus aplicaciones a pesar de las posibles configuraciones de 10s navegadores. Si se prefiere, se pueden utilizar cookies en lugar del URL, modificando la propiedad TrackMode en el controlador del servidor.
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:
Modo Standalone: Proporciona un servidor Web personalizado, como en el primer ejemplo creado. Resulta practico para depurar la aplicacion (ya que puede ejecutarse desde el IDE de Delphi y situar puntos de ruptura en cualquier parte del codigo). Tambien se puede usar este mod0 para desplegar aplicaciones en redes internas (intranets) y para permitir que 10s usuarios trabajen en mod0 desconectado en sus propios ordenadores, con una interfaz Web. Si se ejecuta un programa IntraWeb independiente con el indicador -inst a l l , se ejecutara como servicio y no aparecera el cuadro de dialogo. El mod0 Standalone ofrece un mod0 de desplegar un programs IntraWeb de mod0 Application mediante la propia IntraWeb como servidor Web. Modo Application: Permite desplegar una aplicacion IntraWeb en un servidor comercial, construido como modulo Apache o biblioteca ISS. El mod0 Application incluye gestion de sesiones y todas las caracteristicas de IntraWeb, y es el mod0 preferido de desplegar una aplicacion escalable para su uso en la Web. Para ser mas precisos, 10s programas IntraWeb en mod0 Application pueden desplegarse como programas independientes, bibliotecas ISAPI o modulos de Apache. Modo Page: Abre una via a la integracion de paginas IntraWeb en aplicaciones WebBroker y WebSnap. Se pueden aiiadir caracteristicas a programas ya existentes o confiar en otras tecnologias para partes de un sitio dinamico basado en paginas Web, mientras que se gestionan mediante IntraWeb las partes interactivas. El mod0 Page es la unica opcion para utilizar IntraWeb en aplicaciones CGI, per0 carece de las caracteristicas de gestion de sesiones. Los servidores IntraWeb independientes no soporta el mod0 Page.
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.
Si 10s elementos del menu controlan el evento Onclick en el codigo, se convertiran en enlaces en tiempo de ejecucion. Se puede ver un ejemplo de un menu en la figura 2 1.3. El segundo componente del ejemplo es una vista en arbol con un conjunto de nodos predefinidos. Este componente utiliza mucho codigo JavaScript para permitir la expansion y colapso de 10s nodos directamente en el navegador (sin tener que volver a acceder a1 servidor). A1 mismo tiempo, 10s
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;
Gracias a1 parecido de 10s componentes de IntraWeb con 10s componentes estandar de la VCL de Delphi, es facil leer y comprender este codigo.
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
AppI D del objeto global WebApp 1i c a t i o n . El segundo submenu, Tree 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;
--
procedure TformMain.IWAppFormCreate(Sender: TObject); var i: Integer; link: TIWURL; begin // fija 10s titulos de la cuadricula IWGridl.Cell [0, 0] .Text := 'Row'; IWGridl .Cell [0, 11 .Text : = 'Owner'; IWGridl .Cell [0, 2 ] .Text := 'Web Site'; / / fija el contenido de las celdas for i : = 1 to 1WGridl.RowCount - 1 do begin IWGridl .Cell [i,01 .Text : = 'Row ' + IntToStr (i+l); 1WGridl.Cell [i,l] .Text : = 'IWTwoForms by Marc0 Cantu'; link := TIWURL.Create (Self); link.Text := 'Click here '; link .URL : = 'http://www.marcocantu. corn'; IWGridl Cell [i,2 ] .Control : = link; end ; end ;
El efecto de este codigo se muestra en la figura 2 1.4. Ademas de la salida, hay que fijarse en unos cuantos detalles interesantes. En primer lugar, el componente de cuadricula utiliza 10s anclajes de Delphi (a False) para generar el codigo que lo mantiene centrado en la pagina, incluso aunque un usuario ajuste el tamaiio de la ventana del navegador. En segundo lugar, hemos aiiadido un componente IWURL a la tercera columna, per0 podria aiiadirse cualquier otro componente (incluidos botones y cuadros de edicion) a la cuadricula. La tercera (y mas importante) cuestion es que un IWGrid se traduce en una cuadricula HTML, con o sin marco alrededor. Este es un fragment0 del codigo HTML generado para una de las filas de la cuadricula:
<tr> <td valign="middle" align="leftW NOWRAP> <font style="font-size:lOpt;">Row 2</font> < / td> <td valign="middle" align="leftW NOWRAP> <font style="font-size: 10pt;">IWTwoForms by Marco Cantu</ font> </t& <td valign="middle" align="leftW NOWRAP> <font style="font-size: l0pt; "></font> <a href="#" onclick="parent .LoadURL ( 'http:/ / www.marcocantu.com')" id="TIWURLlr' name="TIWURLl" style="z-index:lOO;font-sty1e:normal;fontsize:lOpt;text-decoration:none;">
Click here</a>
blick here
1
I
bow
\ Flick here
Figura 21.4. El ejemplo IWTwoForms usa un componente IWGrid, texto incrustado y componentes IWURL.
TRUCO:En el listado anterior, hay que fijarse en que el UIU. vinculado se activa mediante JavaScript, y no directamente. Se hace asi porque todas las acciones de IntraWeb permiten operaciones adicionales de cliente, como validaciones, comprobaciones y envios. Por ejemplo, si se establece la propiedad Required de un componente, si el campo estiz vacio no se enviaI-o--r-r - - - - - - - a:--1-1ran 10s aaws, -.se Vera un mensaje ae error ae Javaacrlpr wersonallzaole y -mediante la propiedad descriptiva F r iendlyName).
-1..
> - 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;
Incluso aunque el programa llame a1 metodo Show, puede considerarse como una llamada a ShowModal, ya que IntraWeb considera las paginas visibles como una pila. La ultima pagina que se muestra esta en la parte superior de la pila y es la que muestra el navegador. A1 cerrar esta pagina (escondiendola o destruyendola), se vuelve a mostrar la pagina anterior. En el programa, las paginas secundarias se pueden cerrar cuando se llama a1 metodo R e l e a s e , que es (corno en la VCL) el mod0 correct0 de deshacerse de un formulario que se ejecuta en ese instante. Tambien se puede esconder el formulario secundario y volverlo a mostrar mas tarde, evitando volver a crearlo cada vez (en particular si hacer esto implica perder las operaciones de edicion del usuario).
- - -. - - - ADVERTENCIA: Hemos &dido en el pronrama un b o t h CIese en el - formulario principal. No deberia llamar a Release, she invocar en SU -
lugar a1 mCtodo Terminate del objeto WebApplication, pashdlola el mensaje de salida, como en WebApplication .Terminat e , ... . . . . .-.. .. .. [ ' tiooaoye ! ' ) . La aemostracion urillza una llamaaa alternauva: TerminateAndRedirect.
1
. 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);
Es algo distinto del archivo de proyecto estandar de Delphi, porque llama a una funcion global en lugar de aplicar un metodo a un objeto global que represente a la aplicacion. No obstante, el efecto es bastante parecido. Los dos parametros son las clases del formulario principal y del controlador IntraWeb, que maneja sesiones y otras caracteristicas, como veremos en breve. El formulario secundario del ejemplo IWTwoForms muestra otra caracteristica interesante de IntraWeb: su extenso soporte de graficos. El formulario tiene un componente grafico con la clasica imagen de Atenea de Delphi. Se consigue esto a1 cargar un mapa de bits en un componente IWImage: IntraWeb convierte el mapa de bits en un archivo JPEG, lo guarda en una carpeta cache creada dentro de la carpeta de la aplicacion y devuelve una referencia a ese archivo, con el siguiente codigo HTML:
La caracteristica adicional proporcionada por IntraWeb y aprovechada por el programa es que un usuario puede hacer clic sobre la imagen con el raton para modificar la imagen a1 ordenar la ejecucion de codigo de servidor. En este programa, el efecto es dibujar pequeiios circulos verdes.
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;
En el codigo de una aplicacion, se puede hacer referencia al objeto de sesion accediendo al campo Data de la variable global RWebApplication,utilizada para acceder a la sesion de usuario actual.
la unidad IWInit. Ofrece acceso a 10s datos de sesibn dc un mod0 seguro con respecto a 10s hilos: hay que tener un cuidado especial para acceder a ella incluso en un entorno rnultihilo. Esta variable puede utilizarse fhera de un formulario o control (que se basan de manera nativa en sesiones), que es por lo que se usa sobre todo en modulos de datos, rutinas globales y clases que no Sean de LntraWeb.
Una vez mas, la unidad ServerController predeterminada ofrece una funcion auxiliar que puede utilizarse:
function UserSession: TUserSession; begin Result := TUserSession(RWebApp1ication.Data); 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);
IWLabell-Text : = 'Global: ' + IntToStr (GlobalCount); IWLabel2 .Text : = 'Form: ' + IntToStr (FormCount); IWLabel3 .Text : = 'User: ' + IntToStr (UserSession.UserCount); end;
Fijese en que el programa utiliza la llamada Inter loc kedI ncrement de Windows para evitar el acceso concurrente a la variable global compartida por varios hilos. Entre las tecnicas alternativas se incluye el uso una seccion critica o de T i d T h r e a d S a f e I n t e g e r de Indy (que se encuentra en la unidad IdTrheadsafe). La figura 2 1.5 muestra el resultado del programa (con dos sesiones en ejecucion en dos navegadores distintos). El programa tambien tiene una casilla de verificacion que activa un temporizador. Aunque suene extraiio, en una aplicacion IntraWeb, 10s temporizadores funcionan casi del mismo mod0 que en Windows. Cuando expira el plazo del temporizador, se ejecuta un cierto codigo. En la Web esto significa refrescar la pagina lanzando una orden de refresco en el codigo JavaScript :
IWTIMERl=setTimeout ( ' S ~ b r n i t C l i c k ( ~ ~ ~ W T I M"",false) ' ,5000); ERl~~,
procedure TWebModulel.IWPageProducerlGetForm(ASender: TIWPageProducer; AWebApplication: TIWApplication; var VForm: TIWPageForm); begin VForm : = TformMain.Create(AWebApplication);
end;
Con esta sencilla linea de codigo (ademas de un componente IWModuleCon-troller en el modulo Web), la aplicacion WebBroker puede incrustarse en una pagina IntraWeb, como hace el programa CgiIntra. El componente IWModuleController proporciona servicios centrales para el soporte de IntraWeb. Debe existir un componente de este tip0 para que cada proyecto de IntraWeb funcione correctamente.
Global 24
Form 14
User. 14
Figura 21.5. La aplicacion IWSession tiene contadores globales y especlficos de sesion, como puede verse ejecutando dos sesiones en dos navegadores distintos (o incluso en el mismo navegador).
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
entrada de datos en una pagina HTML compleja? Es extrafio construir todo el contenido de una pagina mediante componentes IntraWeb, incluso aunque se pueda usar el componente IWText para incrustar una porcion de HTML personalizado en una pagina IntraWeb. La tecnica alternativa implica el uso de gestores de estructura de IntraWeb. En IntraWeb se usa de manera invariable un gestor de estructura; el predeterminado es el componente IWLayoutMgrForm. Las otras dos alternativas son componentes IWTemplateProcessorHTML para trabajar con un archivo de plantilla HTML externo e IWLayoutMgrHTML para trabajar con HTML interno. Este segundo componente incluye un potente editor HTML que puede usarse para preparar el HTML generic0 al igual que incrustar 10s componentes IntraWeb necesarios (algo que en ocasiones habra que hacer manualmente con un editor HTML esterno). Aun mas, cuando se selecciona un componente IntraWeb desde este editor (que se activa haciendo doble clic sobre un componente IWLayoutMgrHTML), se podra utilizar el Object Inspector de Delphi para personalizar las propiedades del componente. Como puede verse en la figura 2 1.6, el HTML Layout Editor disponible en IntraWeb es un potente editor HTML visual; el texto HTML que genera esta disponible en una pagina aparte. (El editor HTML se mejorara en una proxima actualization, y se arreglaran unos cuantos detalles.)
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:
TRUCQ:Fijese mqw cuando se u w w , los c&p.ona@s nn otilizan el posicioI3ami~ m ~ u t os b que ae ;listrihuyen acukdo con el IfIhlL. & de Por eso, el form'dari~ convierte hnipmente en up contenedor de composc
nentes, porque se ignofa la posicib d mulario. o de 10s companentes del for-
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;
Pero en lugar de acceder a un modulo de datos global, se utiliza el modulo de datos de la sesion actual. En el primer programa de ejemplo en el que se incluyen datos de una base de datos, llamado IWScrollData, hemos afiadido a1 modulo de datos un componente S i m p l e D a t a S e t y a1 formulario principal un componente IWDBGrid con la siguiente configuracion:
o b j e c t IWDBGridl: TIWDBGrid Anchors = [akLeft, akTop, akRight, akBottom] Bordersize = 1 Cellpadding = 0 CellSpacing = 0 Lines = t l R o w s UseFrame = False DataSource = DataSourcel FromStart = False Options = [dgShowTitles] RowAlternateColor = clSilver RowLimit = 10 RowCurrentColor = clTeal end
La configuracion mas importante es la eliminacion de un marco que albergue el control con sus propias barras de desplazamiento (la propiedad ~ s e ~ r a m e ) , el hecho de que 10s datos se muestren a partir de la posicion del conjunto de datos actual (la propiedad F r o m S t a r t ) y el numero de filas que se mostraran en el navegador (la propiedad RowLimi t ) . En la interfaz de usuario, hemos eliminado las lineas verticales y dado color a filas salteadas. Tambien hemos especificado un color para la fila actual (la propiedad R o w C u r r e n t C o l o r ) ; de no ser asi, 10s colores salteados no aparecerian correctamente, ya que la fila actual tiene el mismo color que las filas del fondo, sin importar su posicion (si se fija la propiedad R o w C u r r e n t C o l o r como c l N o n e se podra ver lo que queremos decir). Estos parametros producen el efecto que muestra la figura 2 1.7. Tambien puede verse si se ejecuta el ejemplo IWScrollData. El programa abre el conjunto de datos cuando se crea el formulario, utilizando el conjunto de datos enlazado con la fuente de datos actual.
procedure TformMain.IWAppFormCreate(Sender: begin DataSourcel.DataSet.0pen; 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;
do) se ha convertido en un hipervinculo; se pasa el numero de empleado como parametro a1 comando de continuar, como muestra la figura 2 1.8.
Figura 21.8. El formulario principal del ejemplo IWGridDemo utiliza una cuadricula con marco con hipervinculos hacia el formulario secundario.
El listado 2 1 . 1 , muestra un resumen de las propiedades clave de la cuadricula. Fijese en particular en la columna del apellido, que tiene un campo enlazado (lo que convierte a1 texto de la celda en un hipervinculo) y un controlador de evento que responde a su seleccion. En este metodo; el programa crea un formulario secundario mediante el cual el usuario puede editar 10s datos:
p r o c e d u r e TGridForm.IWDBGridlColurnns1C~ick(ASender: TObject; c o n s t AValue: String) ; begin w i t h TRecordForm.Create (WebApplication) d o begin S t a r t I D : = AValue; Show; end; end:
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 =
end item
DataField =
end item
DataField =
end item
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;
otras operaciones sobre la columna. El formulario secundario esta enlazado con el mismo modulo de datos que el formulario principal. Por eso; despues de actualizar 10s datos de la base de datos, se pueden ver en la cuadricula (pero las actualizaciones se guardan solo en memoria. porque el programa no realiza una llamada a ApplyUpdates). El formulario secundario utiliza unos cuantos controles de edicion y un navegador, proporcionado por IntraWeb. La figura 2 1.9 muestra este formulario en tiempo de ejecucion.
m a Last Name
Hire
estan
1 1 711 990 11
Figura 21.9. El formulario secundario del ejemplo IWGridDemo permite que un usuario edite 10s datos y explore 10s registros.
IWClientSideDataSet: Un conjunto de datos en memoria que se define fijando las propiedades ColumnName y Data en el codigo del programa. En futuras actualizaciones se podra editar datos en el cliente, ordenarlos, filtrarlos, definir estructuras maestro-detalle y muchas cosas mas.
IWClientSideDataSetDBLink: Un proveedor de datos que puede conectarse a cualquier conjunto de datos de Delphi, conectandolo con la propiedad Datasource.
IWDynGrid: Un componente de cuadricula dinamica conectado con uno de 10s dos componentes anteriores mediante la propiedad D a t a . Este componente lleva todos 10s datos a1 navegador y puede trabajar con ellos en el cliente mediante JavaScript.
Existen otros componentes de cliente en IntraWeb, como IWCSLabel, IWCSNavigator e IWDynamicChart (que solo funciona con Internet Explorer). Como un ejemplo del uso de esta tecnica, hemos construido el ejemplo IWClientGrid. El programa tiene poco codigo, per0 que hay mucho preparado para su uso en 10s componentes. Estos son 10s elementos centrales de su formulario principal:
object formMain: TformMain SupportedBrowsers = [brIE, brNetscape61 OnCreate = IWAppFormCreate object IWDynGridl: TIWDynGrid Align = alClient Data = IWClientSideDatasetDBLinkl end object DataSourcel: TDataSource Left = 72 Top = 8 8 end object IWClientSideDatasetDBLinkl: TIWClientSideDatasetDBLink Datasource = DataSourcel end end
El conjunto de datos procedente del modulo de datos se conecta con el Datasource cuando se crea el formulario. La cuadricula resultante, que muestra la figura 2 1.10, permite ordenar 10s datos en cualquier celda (mediante la pequeiia flecha que se encuentra despues del titulo de cada columna) y filtrar 10s datos mostrados seglin uno de 10s valores posibles del campo. Por ejernplo. en la figura se pueden ordenar 10s datos de empleado de acuerdo con el apellido y filtrarlos por pais y categoria laboral.
lC'
LI
TJ
Tcm Luke Carol Mary Leshe K J Randy Mchael
Green
Lcc
Leung
4 4 4
4
Yanowslu
4 4 4 4 4
Figura 21.10. La cuadricula del ejernplo IWClientGrid soporta la ordenacion y filtrado personalizados sin tener que volver a traer 10s datos desde el servidor Web.
Esta caracteristica es posible porque 10s datos se llevan a1 navegador dentro del codigo JavaScript. Este es un fragmente de uno de 10s guiones incrustados en el codigo HTML de la pagina:
< s c r i p t language="Javascriptl.Z"> var IWDYNGRIDl-Titlecaptions = [ "EMP-NO", "FIRST-NAME ", "LAS T-NAME", "PHONE-EXT", "DEPT NO " ,"JOB- CODE " ,"JOB-GRADE " ,"JOB- COUNTRY "1 ; var IWDYNGRIDl-Cellvalues = new Array(); IWDYNGRID1-CellValues[O] = [ Z , 'Robert', 'Nelson', '332', '600', 'VP',21 'USA'] ; IWDYNGRID1-CellValues[l] = [ 4 , 'Bruce', 'Young', '233', '621 ', ' E n g ' , 2 1' U S A ' ] ; IWDYNGRIDl-CellValues[Z] = [ 5 , 'Kim', 'Lambert ', '22', ' 1 3 O 1 ,' E n g 1 , 2 , 'USA']; IWDYNGRID1-Cellvalues [3] = [8, 'Leslie', 'Johnson', '410', ' 1 8 0 r , 'Mktg',3, 'USA'] ; IWDYNGRID1-CellValues[4] = [ 9 , 'Phil', 'Forest', '229', ' 6 2 Z 1 , M n g r ' , 3 1' U S A ' ] ; '
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
Crear aplicaciones para Internet significa usar protocolos y crear interfaces de usuario basadas en navegadores, como en 10s dos capitulos anteriores, per0 tambien abre una oportunidad para el intercambio de documentos de negocio electronicamente. Los estandares que surgen para este tip0 de actividad se centran en el formato de documento XML e incluyen el protocolo de transmision SOAP, 10s esquemas XML para la validacion de documentos y XSL para representar documentos como HTML. En este capitulo, comentaremos las principales tecnologias XML y el amplio soporte que Delphi les ofrece desde su version 6. Ya que el conocimiento sobre XML no esta muy extendido, vamos a ofrecer una pequeiia presentacion sobre cada tecnologia, per0 deberian consultarse libros dedicados especialmente a estas tecnologias para aprender mas. En el capitulo 23 nos centraremos de manera especifica en 10s servicios Web y SOAP. En este capitulo se tratan 10s siguientes temas: Presentacion de XML: Extensible Markup Language Trabajo con un DOM XML. Delphi y XML: interfaces y proyeccion. Procesamiento de XML con SAX.
Presentacion de XML
El lenguaje extensible de marcas (extens~ble Markup 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.
Los espacios en blanco (corno el caracter de espacio, el retorno de carro, el salto de linea y 10s tabuladores) generalmente se ignoran (corno en un documento HTML). Es importante dar formato a un documento XML para que resulte legible, per0 a 10s programas no les importara demasiado. Se pueden aiiadir comentarios dentro de las marcas < ! -- y -->, que, en esencia, ignoran 10s procesadores de XML. Existente tambien directivas e instrucciones de proceso, encerradas entre las marcas < ? y ? > . Existen unos pocos caracteres especiales o reservados que no pueden usarse en el texto. Los dos unicos simbolos que no pueden usarse jamas son el caracter menor que (<, usado para delimitar una marca), que se sustituye por & 1t ; y el caracter ampersand (&), que se sustituye por & ; (y es la evolucion grafica del et latino). Otros caracteres especiales optativos son & g t ; para el simbolo mayor que (>), & apo s ; para la comilla simple ( ' ) y " para la comilla doble ("). Para aiiadir contenido que no sea XML (por ejemplo, informacion binaria o un guion), se puede usar una seccion CDATA, delimitada por < ! [CDATA[ y ] I > . Todas las etiquetas se encuentran entre 10s simbolos menor y mayor que, < y >. Las marcas son sensibles a las mayusculas (no como en HTML). Por cada marca de apertura, debe existir una marca de cierre correspondiente, indicada por un caracter inicial de barra inclinada:
Las marcas no pueden solaparse: deben anidarse correctamente, como en la primera linea que se muestra (la segunda linea no es correcta):
<node>xx <nested> yy</nested> </node> <node>xx <nested> yy</node> </nested>
/ / correct0 / / erroneo
Si una marca no tiene contenido (pero su presencia resulta importante), pueden sustituirse las marcas de apertura y cierre por una marca unica que incluye una barra inclinada final: <node / >. Las marcas pueden tener atributos, usando varios nombres de atributos seguidos de un valor encerrado entre comillas:
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.
Entre las codificaciones posibles hay conjuntos de caracteres Unicode (como UTF-8, UTF- 16 y UTF-32) y algunas codificaciones I S 0 (como ISO- 10646-xxx o 1SO-8859-xss). El prologo tambien puede incluir declaraciones externas, el esquema usado para validar el documento, declaraciones de espacios de nombre, un archivo XSL asociado y algunas declaraciones de entidades internas. Consulte documentacion o libros sobre XML para conseguir mas informacion sobre estos temas. Un documento XML esta bien formado si tiene un prologo, tiene una sintaxis correcta (segun las reglas de la seccion anterior) y tiene un arb01 de nodos dentro de una raiz unica. La mayoria de las herramientas (como Internet Explorer) comprueban si un documento esta bien formado a1 cargarlo.
NOTA: XML es miis formal y precis0 que HTML.El W3C trabaja en un estandar XHTML que hara que los docurnentos HTML sean confonnes con XML, para que las herramientas XML 10s prowsen mejor. Esta implica muchos carnbios en un docurnento HTML tipico, d o evitar 10s atributos sin valores, a f d i r todas las marcas de cierie (wmo m </p> y </li>), ahdir la barra invertida para marcas independientes (cam0 <hr / > y br/ >), un anidado correcto y muchas cosas mais. El sitio Web de W3C alberga
u n cnnvercnr de HTMT, a XHTMT. Ilnmdn HTMI. Tidv en www w 3 mnl
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).
Figura 22.1. El ejemplo XmlEditOne permite escribir texto XML en un componente de memo, indicando 10s errores durante la escritura y mostrando el resultado en el navegador incluido.
-
. -
--
.-
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:
DOM: Carga un documento completo en un arbol jerarquico de nodos, lo que nos permite leerlos y manipularlos para modificar el documento. Por ello, el DOM es aconsejable para navegar por la estructura XML en memoria, editarla e incluso para crear documentos completamente nuevos.
SAX: Analiza sintacticamente el documento, lanzando un evento para cada elemento del documento sin crear ninguna estructura en memoria. Despues de que SAX haya analizado el documento, este se pierde, per0 este mod0 de funcionamiento suele ser mucho mas rapido que crear el arbol DOM. Usar SAX esta bien si el documento se va a leer de una vez, por ejemplo, si se busca una parte de sus datos. Existe una tercera posibilidad para manipular (y en concreto para crear) documentos XML: el manejo de cadenas. Crear un documento aiiadiendo cadenas es, sin duda alguna, la operacion mas rapida si podemos dar una sola pasada (y no necesitamos modificar 10s nodos ya generados). Incluso la lectura de documentos por medio de funciones de cadenas es muy rapida, per0 puede complicarse para estructuras complejas. Ademas de estos enfoques clasicos del procesamiento de XML, que tambien estan disponibles para otros lenguajes de programacion, Delphi 6 proporciona dos tecnicas mas que deberiamos tener en cuenta. La primera es la definicion de interfaces que proyectan la estructura del documento y que se utilizan para acceder a1 mismo en lugar de hacerlo a traves de la interfaz generica de DOM. Como veremos, este metodo contribuye a una codification mas rapida y aplicaciones mas solidas. La segunda tecnica es el desarrollo de transformaciones que nos permitan leer un documento XML generic0 dentro de un componente ClientDataSet o guardar el conjunto de datos en un archivo XML con una estructura dada (no en la estructura XML especifica que soporta nativamente el ClientDataSet o MyBase). No vamos a tratar de decidir que opcion es la que mejor se adapta a cada tip0 de documento y manipulacion, per0 resaltaremos algunas de las ventajas e inconvenientes mientras analizamos ejemplos de cada enfoque en las secciones siguientes. A1 final del capitulo, analizaremos la velocidad relativa de las tecnicas para el procesamiento de grandes archivos.
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.
I
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.
A un nivel superior, el componente XMLDocument implementa tambien la interfaz IXMLDocument. Se trata de una API personalizada del tip0 de DOM definida por Borland en la unidad XMLIntf y que incluye interfaces como IXMLNode, IXMLNodeList e IXMLNodeCollection. Esta interfaz de Borland simplifica algunas de las operaciones de DOM sustituyendo varias llamadas a metodos, que suelen repetirse a mod0 de secuencia, por una sola propiedad o metodo. En 10s siguientes ejemplos (sobre todo en el ejemplo DomCreate), utilizaremos ambos enfoques para dar una mejor idea de las diferencias practicas entre ambos.
// 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;
Este codigo es bastante interesante ya que resalta algunas de las operaciones que podemos realizar con un DOM. En primer lugar, cada nodo tiene una propiedad NodeType que podemos usar para determinar si el nodo es un elemento, un atributo, un nodo de testo o una entidad especial (como CDATA y otras). Ademas, no podemos acceder a la representacion textual del nodo, su Nodevalue, a menos que tenga un elemento de testo (el nodo de texto se omitira, como comprobacion inicial). Despues de mostrar el nombre del elemento y el valor del testo, si esta disponible, el programa muestra directamente el contenido de cada atributo y de cada subnodo llamando de manera recursiva a1 metodo DomToTree (vease figura 22.2).
a u h r = Canlu
i book 3
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
Figura 22.2. El ejernplo XmlDomTree puede abrir un documento XML generic0 y mostrarlo dentro de un control TreeView cornun.
Una vez que hayamos cargado el documento de muestra que acompaiia a1 programa XmlDomTree (mostrado en el listado 22.1) en el componente XMLDocument, podemos utilizar diversos metodos para acceder a nodos genericos, como en el anterior codigo de construccion del arbol, o buscar elementos especificos. Por ejemplo podemos obtener el valor del atributo t e x t del nodo raiz si escribimos:
XMLDocumentl.DocumentElement.Attributes
['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:
Para acceder a 10s nodos, utilizamos una tecnica similar, aprovechandonos posiblemente de la matriz ChildValues.Se trata de una extension de Delphi a DOM, que nos permite pasar como parametro el nombre del elemento o su posicion numerica:
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 tip0 de sangrado estqbleciendo la.propie* ..$ Node- - .-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);
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 value test3 = s~mple
E test
[color ="red"] test2 test4 lest3 = s~mole value
Figura 22.3. El ejemplo DomCreate puede generar diferentes tipos de documentos XML utilizando un DOM.
El segundo ejemplo de creacion de DOM tiene que ver con un conjunto de datos. Hemos aiiadido a1 formulario un componente de conjunto de datos dbExpress (pero habria semido cualquier otro conjunto de datos) y agregado tambien la llamada a1 procedimiento personalizado DataSetToDOM a un boton, de la siguiente manera:
DataSetToDOM
('customers', ' c u s t o m e r ' , XMLDoc,
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;
El codigo anterior utiliza las interfaces de acceso simplificado de DOM que proporciona Borland, que incluyen un nodo AddChi ld que crea el subnodo, y el acceso direct0 a la propiedad Text para definir un nodo hijo con contenido textual. Esta rutina extrae una representacion XML del conjunto de datos, ofreciendo muchas posibilidades para la publicacion Web, como veremos en la seccion sobre XSL. Otra interesante posibilidad es la generacion de documentos XML que describan objetos Delphi. El programa DomCreate tiene un boton que se utiliza para
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; (iNode: IXmlNode; Comp: TPersistent);
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 = Cl~enlHerghl 385 CfienlWdh = 563 Color = -16777201 Cnnslranls = 20255360
5n
FOIM
., .... ,,"".-.*-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~
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.
Veamos a continuacion unos fragmentos del codigo generado, disponible en la unidad XmlIntfDefinition del ejemplo Xml I n t e r face:
tYPe IXMLBooksType = interface (IXMLNode)
( Property Accessors
function Get-Text: WideString; function Get-Book: IXMLBookTypeList; function Get-Ebook: IXMLEbookTypeList; procedure Set-Text(Va1ue: Widestring);
( Methods 6 Properties )
property Text: WideString read Get-Text write Set-Text; property Book: IXMLBookTypeList read Get-Book; property Ebook: IXMLEbookTypeList read Get-Ebook; end; IXMLBookTypeList = interface(IXMLNodeCo11ection) [ ' {3449E8C4-3222-47B8-B2B2-38EE504 79OB6) ' 1
( Methods 6 Properties )
function Add: IXMLBookType; function Insert (const Index: Integer) : IXMLBookType; function Get-Item(1ndex: Integer): IXMLBookType; property Items [Index: Integer] : IXMLBookType read Get-Item; default; end; IXMLBookType = interface ( IXMLNode)
( Property Accessors
[ ' {26BF5CS1-9247-4DlA-8584-24AE68969935) )
'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;
Despues de generar estas interfaces utilizando el asistente en el ejemplo Xml Interface,hemos repetido el codigo de acceso a1 documento XML que es similar a1 del ejemplo XmlDomTree per0 mas facil de escribir (y leer). Por ejemplo, podemos obtener el atributo del nodo raiz escribiendo simplemente:
procedure TForml .btnAttrClick (Sender: TObject) ; var Books: IXMLBooksType; begin Books : = Getbooks (XmlDocumentl) ; ShowMessage (Books.Text) ; end;
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
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-
- - -.- - - -- NOTA: El editor de Delphi soporte la completitud de cbchgo para archives XML gracias a 10s DTD. Si se coloca un ar&vo DTD en el dir&to';io bin de Delphi y se hace referencia a 61 mediante una etiqaeta DOCTYPE, se habilitarh esta caracteristica, que brland no sa~orta h n a dfidd. dc
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
Start Element y EndElement . Delphi no incluye soporte especifico para la interfaz SAX per0 se puede conseguir facilmente importando el soporte XML de Microsoft (la biblioteca MSXML). En particular, para el ejemplo SasDemol, hemos utilizado la version 2 de MSXML, ya que se encuentra muy difundida. Hemos generado una unidad de importacion de biblioteca de tipos de Pascal para la biblioteca de tipos, y la unidad de importacion esta disponible dentro del codigo fuente del programa, per0 necesitamos que la biblioteca COM este registrada en nuestro ordenador para ejecutar el programa con exito.
I
-
NOTA:Otro ejcmploVhaciael final delcapituli(Lpgc~rnl) muestra, entis otras cosas, el uso de la API de SAX, incluyendselmotor OpenXml.
Para utilizar SAX, tenemos que instalar un controlador de eventos de SAX dentro de un lector SAX, y despues cargar un archivo y analizarlo sinticticamente. Hemos utilizado la interfaz de lectura de SAX proporcionada por MSXML para programadores de VB. La interfaz oficial (C++) tenia unos cuantos errores en su biblioteca de tipos que impedia que Delphi la pudiera importar de forma correcta. En el formulario principal del ejemplo SaxDemo 1 se declara:
sax: IVBSAXXMLReader;
El codigo tambien define un controlador de errores, que es una clase que implementa una interfaz especifica (IVBSAXErrorHandler)con tres metodos a 10s que se llama dependiendo de la gravedad del problema: error, fatalError e ignorablewarning. Simplificando un poco el codigo, el analizador sintactico SAX se activa al llamar a1 metodo parseURL despues de asignarle un controlador de contenido:
s a x - C o n t e n t H a n d l e r := TMySaxHandler-Create; s a x .parseURL (filename)
A1 final el codigo se encuentra en la clase TMySaxHandler, que es la que contiene 10s eventos SAX. Ya que en el ejemplo tenemos varios controladores de contenido SAX, hemos escrito una clase basica con el codigo principal y unas cuantas versiones especializadas para el procesamiento especifico. A continuacion veremos el codigo de la clase basica, que implementa l a interfaz I V B S A X C o n t e n t H a n d l e r y la interfaz I D i s p a t c h en que se basa
IVBSAXContentHandler:
type TMySaxHandler =class (TInterfacedObject, IVBSAXContentHandler)
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) ;
La clase TMyS impleSaxHand ler proporciona una implementation real que sobrescribe el evento st art E leme nt lanzado por cualquier nuevo nodo para enviar la posicion actual en el arbol, con la siguiente sentencia:
Log.Add (strLocalName +
') ') ;
El segundo metodo de la clase es el evento characters, que se provoca cuando se encuentra un valor de nodo (o un nodo de texto) y envia su contenido (como muestra la figura 22.6):
p r o c e d u r e TMySimpleSaxHandler.characters(var WideString) ; var str: WideString; begin inherited; s t r : = Removewhites (strchars); if (str <> " ) then L o g - A d d ('Text: ' + str); end; strchars:
pam~w
'
List
( '
I-
Paw TkJw
s ~ a l ~ ~ o c l m--- n t e bwkslbooksl book(books.book] lkle(books.book.tille] de Texl: La B~bha 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] de Texl. La Bibl~a 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.
Se trata de una operacion de analisis sintactico generica que afecta a todo el archivo XML. El segundo controlador de contenido SAX derivado se refiere a la estructura especifica del documento XML, extrayendo solamente nodos de un tip0 determinado. Concretamente, el programa busca 10s nodos de tipo title. Cuando
un nodo es de este tipo (en s t a r t E l e m e n t ) , la clase activa la variable booleana i s b o o k . El valor de texto del nodo se tiene en cuenta solo inmediatamente despues de encontrar un nodo de este tipo:
procedure TMyBooksListSaxHandler.startElement(var strNamespaceUR1, strLocalName, strQName: Widestring; const ~ A t t r i b u t e s : IVBSAXAttributes) ; begin inherited; isbook := (strLocalName = 'title ') ; end : procedure T ~ y B o o k s L i s t S a x H a n d l e r . c h a r a c t e r s ( v a r strchars: Widestring) ; var str: string; begin inherited; if isbook then begin s t r : = Removewhites (strchars); if (str <> 'I) then Log.Add (stack.CommaText + ': ' + s t r ) ; end ; end ;
Tmrh-mmon E w ~ ~ o ~ m d c v ~ u ~ l - ~ B d r ( , d a ar r i ~ d l
Figura 22.7. El XML Mapper muestra 10s dos extremos de una transforrnac~on para definir la proyeccion entre ellos (con las reglas indicadas en la parte central).
A la derecha se encuentra la seccion del paquete de datos: Muestra la informacion acerca de 10s metadatos en el paquete de datos, bien en la Field View (indicando la estructura del conjunto de datos) o en la Datapacket View (dandonos informacion sobre la estructura XML). Fi.jese en que el XML Mapper tambien puede abrir archivos en el formato original de ClientDataSet.
La parte central d e la ventana se utiliza para la proyeccion: Contiene a su vez dos paginas: Mapping, donde podemos ver las correspondencias entre 10s elementos seleccionados en ambos lados que formaran parte de la proyeccion; y Node Properties, donde podemos modificar 10s tipos de datos y otros detalles de cada posible proyeccion. La pagina Mapping del panel central alberga tambien el menu de metodo abreviado que se utiliza para generar la transformacion. El resto de 10s paneles y vistas tienen menus locales especificos, utilizados para realizar diversas acciones (ademas de unos cuantos comandos en el menu principal). Podemos utilizar XML Mapper para proyectar un esquema existente (o estraerlo a partir de un documento) sobre un paquete de datos nuevo, de un paquete de datos existente a un nuevo esquema o documento, o de un paquete de datos ekistente a un documento XML ya existente (si es razonable una cierta correspondencia). Ademas de convertir 10s datos de un archivo XML a un paquete de datos, tambien podemos convertirlos en un paquete delta del ClientDataSet. Esta tecnica es muy util para fusionar un documento con una tabla, como si un usuario hubiera insertado 10s registros modificados de la tabla. Concretamente, podemos transformar un documento XML en un paquete delta para modificar, borrar o insertar registros.
El resultado de usar el XML Mapper es uno o mas archivos de transformacion, cada uno de 10s cuales representa una conversion en sentido unico (necesitamos a1 menos dos archivos para hacer la conversion en ambos sentidos). Estos archivos de transformacion se utilizan en tiempo de diseiio y de ejecucion por 10s componentes XMLTransform, XMLTransformProvider y XMLTransformClient. A mod0 de ejemplo, hemos intentado abrir el documento XML de 10s libros, que tiene una estructura que no se corresponde facilmente con una tabla, ya que contiene dos listas de valores de distintos tipos. Despues de abrir el archivo sample. xml en la seccion XML Document, hemos utilizado su menu local para seleccionar todos sus elementos (Select All) y para crear el paquete de datos (Create Datapacket From XML). Esta operacion hace que el panel derecho se rellene automaticamente con el paquete de datos y la parte central con la transformacion propuesta. Tambien podemos ver inmediatamente su efecto en un programa de muestra haciendo clic sobre el boton Create and Test Transformation. Esto abre una aplicacion generica que permite cargar un documento en el conjunto de datos usando la transformacion creada. En este caso en concreto, podemos ver que el XML Mapper genera una tabla con dos campos de conjunto de datos: uno para cada una de las posibles listas de subelementos. Esta era la unica solucion estandar posible ya que las dos sublistas tienen estructuras diferentes, y es la unica solucion que permite editar 10s datos en la DBGrid conectada a1 ClientDataSet y guardarlos de nuevo en un archivo XML, tal y como se muestra en el ejemplo XmlMapping. Basicamente, este programa es un editor basado en Windows para un documento XML complejo. El ejemplo utiliza un componente TransformProvider con dos archivos de transformation aiiadidos para leer un documento XML y para que el ClientDataSet pueda disponer de el. Como sugiere su nombre, este componente es un proveedor de conjuntos de datos. Para construir la interfaz de usuario, no hemos conectado directamente el ClientDataSet a una cuadricula, ya que contiene un unico registro con un campo de texto y dos conjuntos de datos detallados. Por ello, hemos aiiadido a1 programa dos componentes ClientDataSet mas enlazados con 10s campos del conjunto de datos y conectados con 10s dos controles DBGrid. Probablemente sea mas facil entender esto si echamos un vistazo a la definicion de 10s componentes no visuales en el codigo fuente DFM en el siguiente fragmento, y a su salida en la figura 22.8.
object XMLTransformProviderl: TXMLTransformProvider TransforrnRead.TransformationFile = ' B o o k s D e f a u l t . x t r f TransformWrite.TransformationFile = 'BooksDefau1tToXml.xtr' XMLDataFile = 'Sanple.xml ' end object ClientDataSetl: TClientDataSet ProviderName = 'XMLTransformProviderl ' object ClientDataSetltext: TStringField object ClientDataSetlbook: TDataSetField object ClientDataSetlebook: TDataSetField end
o b j e c t ClientDataSet2: TClientDataSet DataSetField = ClientDataSetlbook end o b j e c t ClientDataSet3: TClientDataSet DataSetField = ClientDataSetlebook end
Harmon
Btuce
Can(u
hltg//ww.ma~cocantu.~m Cantu
II
Figura 22.8. El ejemplo XmlMapping utiliza un componente TransformProvider para permitir la edicion de un documento XML complejo dentro de varios componentes ClientDataSet.
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
legible que el que nos proporciona por defect0 el mecanismo de permanencia de ClientDataSet. Para construir el ejemplo MapTable, hemos colocado un componente SimpleDataSet de dbExpress en un formulario y le hemos conectado un DataSetProvider y un ClientDataSet a1 proveedor. Despues de abrir la tabla y el conjunto de datos de cliente, hemos guardado su contenido en un archivo XML. El siguiente paso ha sido abrir el XML Mapper, cargar el archivo del paquete de datos en el, seleccionar todos 10s nodos del paquete de datos (con el comando Select All de su menu local) y llamar a1 comando Create XML From
Datapacket.
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.
I
.............. .........
1231 Unirco 1351 S~ghl i w D 1354 Cayman Diverr World Llnhded 1356 Tom Swyer D i n g Cedre 1 3 0 Blw Jack Aqua Center 1384 VIP Divers Club
PO BoxZ-547 1 Neptune Lane PO Box 541 632.1 T hid Frydenhq 23-73 PaddnglonL a m 32 Main St.
Suhe 310
.
.
. .
.
.-...
Map ........,...
...
.......
-,
..........
.-
....
. .
- -:
O x m l vefs~on="l0"b < Docwnent~ <Cuttomef> ~Custl~lo>t221 ;ICustNo) <Compary)Kaua~ D~ve Shoppe</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.
servidor Web con una aplicacion personalizada y, finalmente, el navegador Web. Podemos colocar 10s componentes de acceso a la base de datos dentro de la misma aplicacion que maneja la peticion HTTP y que genera el resultado HTML, como en una solucion cliente/servidor. Incluso podemos acceder a una base de datos local o a un archivo XML, con una estructura de dos capas (el programa servidor y el navegador). Es decir, Internet Express es una tecnologia para crear clientes basados en un navegador, lo que nos permite enviar, junto con el HTML, todo el conjunto de datos a1 ordenador cliente. Tambien enviamos algo de codigo JavaScript para poder manipular el XML y mostrarlo dentro de la interfaz de usuario definida por el codigo HTML. El codigo JavaScript es lo que hace posible que el navegador pueda mostrar 10s datos e incluso manipularlos.
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.
El InetXPageProducer permite generar visualmente 10s formularios HTML a partir de 10s conjuntos de datos, de una forma similar a1 desarrollo de una interfaz de usuario AdapterPageProducer. En realidad, la arquitectura de Internet Express, las interfaces internas que utiliza y parte de su editor IDE pueden ser considerados como el progenitor de la arquitectura WebSnap. Con la notable diferencia de generar guiones que se ejecutan del lado del servidor y del cliente, ambos proporcionan un editor para colocar componentes visuales y generar estos guiones. Cabe destacar que el antiguo Internet Express se orienta mas a XML que el mas reciente WebSnap.
--
-,
~ s t o cornponent&~tienen propiedyad8- st yi&-e$ s las ~ i i para e definir el CSS y cada elemento visual time una propiedad 3t 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
Analizador sintactico XML compatible con DOM (para navegadores que carezcan de soporte nativo DOM). Clases JavaScript para 10s controles HTML. Clases JavaScript para enlace de datos XML con controles HTML. Clases para arreglar errores. Funciones JavaScript para mostrar datos y paquetes delta (cuya finalidad es la depuraci6n).
I x m l d b . js
Xm1disp.j~
Xrnlerrdisp js Xrn1Show.j~
Normalmente, las paginas HTML generadas por Internet Express incluyen referencias a estos archivos JavaScript, como en:
Podemos personalizar el codigo JavaScript aiiadiendo directamente codigo a las paginas HTML, o creando nuevos componentes de Delphi escritos para encajar con la arquitectura de Internet Express que produce el codigo JavaScript (posiblemente junto con codigo HTML). Como ejemplo, la clase TPromptQueryButton de muestra de InetXCustom genera el siguiente codigo HTML y JavaScript:
<script language=javascript type="text/javascript"> function PromptSetField (input, msg) ( v a r v = prompt (msg); i f ( V == null I I v == " " ) return false; input. value = v return true ; 1 var QueryForm3 = document.forms['QueryForm3']; </script> <input type=button value="Prornpt onclick="if (PromptSetField(PromptResult, 'Enter some t e x t \ n r ) ) QueryForm3. submit ( ) ;">
..."
TRUCOt Los mriipooentjq dcionales '&muestia.de ENetXCustom sw e de graa ayuda si tenaaos ktFncibn de u k I k e Express. E& c(mnmt ponentcn, estiin dirponibles en la Carpeta \ o dho s \ M i d a s \ ~ n t e r n e t ~ x ~ r e ~ s \~ e ttom.Siga 1% dew* st ~ ~ u s instrucciones del ar'chivo readma. t x t para ins'talar estos ~ ~que Borlands proporciona sin n&im tipo de sopork pmo que penniten afUadir muchias wra@terist.icas a Ias aplicaciones & m Eqress ccin hpxpdlo esfhrzo b & adicid.
Para utilizar esta arquitectura no necesitamos nada especial en el cliente, ya que puede usarse cualquier navegador que entienda el estandar HTML 4, en cualquier sistema operativo. Sin embargo, el servidor Web debe de ser un servidor de Win32 (esta tecnologia no esta disponible en Kylis) y hay que utilizarlo con bibliotecas DataSnap.
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 '
DataSet = ClientDataSetl
end o b j e c t XMLBrokerl: TXMLBroker
ProviderName = 'Da taSetProvider1 ' WebDispatch.MethodType = mtAny WebDispatch. PathInf o = 'XMLBrokerl ' ReconcileProducer = PageProducerl OnGetResponse = XMLBrokerlGetResponse
end
Se necesita la propiedad ReconcileProducer para mostrar un mensaje de error adecuado en caso de conflict0 de actualizacion. Uno de 10s programas de ejemplo de Delphi incluye un codigo personalizado, pero, para este caso, simplemente hemos conectado un componente Pageproducer tradicional con un mensaje de error HTML generico. Despues de preparar el XMLBroker, podemos aiiadir un InetXPageProducer al modulo de datos Web. Este componente tiene un esqueleto HTML estandar, que hemos personalizado para aiiadir un titulo, sin modificar las etiquetas especiales:
<HTML><HEAD> <title>IeFirst</title> </HEAD><BODY> <hl>Internet Express First Demo (IeFirst . exe)</hl> <#INCLUDES><#STYLES><#WARNINGS><#FORMS><#SCRIPT> </BODY>
Las etiquetas especiales se expanden automaticamente mediante 10s archivos JavaScript del directorio especificado en la propiedad Include Pat hURL. Es necesario establecer esta propiedad para que haga referencia al directorio del servidor Web donde residen estos archivos. Podemos encontrarlos en el directorio \ D e l p h i 7 \ S o u r c e \ W e b M i d a s . Las cinco etiquetas tiene el siguiente efecto:
<#INCLUDES>
Genera las instrucciones para incluir las bibliotecas JavaScript. Aiiade la hoja de definicion de estilo incrustada. Se utiliza en tiempo de diseiio para mostrar 10s errores en el editor InetXPageProducer. Genera el codigo HTML producido por 10s componentes de la pagina Web. Aiiade un bloque de JavaScript utilizado para iniciar el guion del lado del cliente.
<#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?,* -
E :
Salary SlalusColurnnl
Figura 22.10. El editor de InetXPageProducer nos permite crear visualmente complejos formularios HTML de una forma parecida al Adapterpageproducer.
El codigo DFM para el InetXPageProducer y sus componentes internos en el ejemplo se muestra a continuacion. Se pueden ver las configuraciones principales, ademas de algunas limitadas personalizaciones graficas:
object
InetXPageProducerl: TInetXPageProducer IncludePathURL = ' / j ssource/ ' HTMLDoc.Strings = ( . . . ) o b j e c t DataForml: TDataForm o b j e c t DataNavigatorl: TDataNavigator XMLComponent = DataGridl Custom = 'align="center" '
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...
El valor de estos componentes esta en el codigo HTML (y JavaScript) que generan y que podemos ver previamente a1 seleccionar la pestaiia H T M L del editor de InetXPageProducer. Veamos a continuacion un parte de las definiciones en el HTML para 10s botones, el encabezado de la cuadricula de datos y una de sus celdas:
/ / botones <table align="centerV> <tr><td colspan="2"> <input type="buttonW value=" 1 < " onclick='if (xml-ready) DataGridl-Disp. first ( ) ; ' > <input t ype="buttonW value="<< " onclick='if (xml-ready) DataGridl-Disp.pgup();'>
...
/ / titulo de cuadricula de datos
<tr> / / u n a celda d e datos <td><div> <input type="textW name="EmpNo" size="lO" ofcs'fxlrayDtGilnou=ioaard_ipxoip~ou(hs; nou=i(m~ed)aardofcs'fDtGilDs.fD~.fcsti)' onkeydown='if (xml-ready) Dii af~ top Ga rt iG dr li od nl k_ eD ys dp o. wk nD =. ' D keys (this); I > </div></td>
...
Despues de preparar el generador de HTML, podemos volver a1 modulo de datos Web, aiiadirle una accion y conectarla con el InetXPageProducer mediante la propiedad P r o d u c e r . Esto deberia bastar para que el funcione a travts-de un navegador, como muestra la fig& 22.1 1 .
programs
lelson
.
IRoberio
--
'oung
l~ruce
:uao
Figura 22.11. La aplicacion IeFirst envia al navegador algunos componentes HTML, un documento XML complete, y codigo JavaScript para mostrar 10s datos en 10s componentes visuales
Si miramos en el archivo HTML recibido por el navegador, encontraremos la tabla mencionada en la definition anterior, algo de codigo JavaScript y 10s datos de la base de datos en el formato XML del paquete de datos. Estos datos 10s controla el XMLBroker y se 10s pasa a1 componente productor para insertarlos en el archivo HTML. El numero de registros enviados al cliente depende del XMLBroker y no del numero de lineas de la cuadricula. Despues de que se envien 10s datos XML a1 navegador, podemos usar 10s botones del componente navegador para movernos por ellos sin necesidad de acceder a1 servidor para buscar mas. Esto difiere bastante del comportamiento de WebSnap. No queremos decir que un enfoque sea mejor que otro, sino que depende del tip0 de aplicacion que vayamos a construir.
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 contenido del archivo XSLT se basa en una o mas plantillas (o reglas o funciones) que procesara el motor. Su nodo es xsl : template,normalmente con un atributo match. En el caso mas sencillo, una plantilla funciona sobre nodos con un nombre determinado; se invoca la plantilla pasandole uno o mas nodos con una expresion XPath:
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
(
Una doble barra inclinada ( / / ) significa cualquier ruta. / /title nos indica todos 10s nodos title, cualesquiera que sean sus nodos padre y books/ /author nos indica cualquier nodo author bajo un nodo books sin tener en cuenta 10s nodos intermedios. El signo de arroba o a ( @ ) indica un atributo en lugar de un nodo, como t en el caso hipotetico de author/@lastname. Los corchetes cuadrados ( [ y ] ) pueden usarse para escoger solo 10s nodos o atributos que contengan un valor dado. Por ejemplo, author [ @name= "marco"] seleccionaria todos 10s autores con un atributo de nombre (name) dado (en este caso, marco). Hay muchos casos mas, per0 esta pequeiia introduccion a las reglas de XPath ayudaran con el comienzo y a comprender 10s ejemplos que siguen. Un documento XSLT es un documento XML que trabaja sobre la estructura de un documento
XML origen y que genera como salida otro documento XML, como un documento XHTML que podemos ver en un navegador Web.
NOTA: Entre 10s procesadores XSLT m h usados se incluyen MS-XML, Xalan del proyecto Apache XML (xml. apache. org) y Xt basado en Java de James Clarke. En Delphi tambih se puede usar el motor XSLT, incluido en XML Partner Pro de Turbopower (www.turbopower.com).
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 archi~~o XML. 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>
TRUCO: En Delphi 7, el editor proporciona la completitud de codigo para XSLT, que hace que la edicion de este tipo de c6digo en el editor sea tan potente como en algunos sofisticados editores especificos para XML.
Para que funcione este ejemplo, debemos proporcionar algunos datos al componente XSLPageProducer a traves de su propiedad XMLData. Esta propiedad puede conectarse a un XMLDocument o directamente a un componente XMLBroker, como hemos hecho en este caso. El XMLBroker toma 10s datos de un proveedor conectado a una tabla local, enlazada con el clasico componente de tabla customer. c d s de DBDEMOS. El efecto es que, con el siguiente XSL generado con Delphi, se consigue (incluso en tiempo de diseiio), el resultado que muestra la figura 22.12:
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>
NOTA: La ~lantilla XSL esthdar se ha &mPIiado desde D e l ~ h6. va aue i . las versiones originales no tenian en cuenta 10s campos nulos omitidos en el
T .
--.
- -
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 .J User
Tom
--
39 1
Insert
~ H T M ~ ~ P ~ Tree,&~~ ~ , $ M. L ~ ~ ~Tree/
'igura 22.12. El resultado de una transforrnacion XSL generada por el cornponente XSLPageProducer en el ejernplo XslCust.
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 +
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
end:
:=
HTMLDom.XML.Text;
El codigo usa el DOM para modificar el documento XSL para mostrar un unico registro, aiiadiendo la sentencia XPath para seleccionar el registro indicado por el campo de consulta i d . Este i d se aiiade a1 hipervinculo gracias a1 XSL con la lista de 10s registros, pero no vamos a listar mas archivos XSL, ya que podemos obtenerlos en la subcarpeta XSL de la carpeta de este ejemplo y analizarlos con mayor detenimiento.
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
de la carpeia XSL a partir del mmbbre deJpn$ramq dispuitible en el primer pariunetio ep b e a de comandos (yaque el orrjeto%pplication definido en la unidad Forms no es accesible por media & iia aplicacib CGI). ii
Despues de utilizar un grupo de botones de radio para definir la cantidad de datos que se desea procesar (algunas opciones pueden requerir minutos en un ordenador lento); 10s datos se duplican mediante este codigo:
while ClientDataSet1.RecordCount < nCount do begin SimpleDataSet1.RecNo : = Random (SimpleDataSet1.RecordCount) + 1; ClientDataSetl-Insert; ClientDataSetl. Fields [0].AsInteger := Random (10000); for I : = 1 to SimpleDataSetl.Fie1dCount - 1 do ClientDataSetl.Fields [i].AsString : = SimpleDataSetl.Fields [i].AsString; ClientDataSetl-Post; end:
archivo, consiguiendo asi un documento basado en atributos. Probablemente no sea este el formato que se desee, por lo que la segunda solucion es aplicar una transformacion de XmlMapper mediante un componente XMLTransf ormClient . La tercera solucion implica procesar directamente el conjunto de datos y escribir cada registro en un archivo:
procedure TForml.btnSaveCustomClick(Sender: TObject); var str: TFileStream; s: string; i: Integer; begin str : = TFileStream.Create ( 'data3.xmZ ', fmcreate) ; try ClientDataSet1.First; s : = '<?xmZ version="l 0" s tandalone="yes " ?><employee> '; str .Write (s[1] , Length (s)) ;
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
basado en el XmlMapper para un conjunto de datos grande, porque es varios cientos de veces mas lento, incluso para un conjunto de datos pequeiio (ni siquiera hemos podido probarlo con un conjunto de datos grande, porque, sencillamente, tarda demasiado). Por ejemplo, 10s 50 milisegundos que necesita un streaming personalizado para un conjunto de datos pequeiios se convierten en m b de 10 segundos cuando usamos la proyeccion, y el resultado es muy parecido.
'
4747 LesLe
-_
Nelson
332
937
G Ion
k
s -'
!Bddwn
Johnson h",bM
-
5 -
2
,7 -
__ 1410 1
'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;
Tambien es mas dificil invocar el motor de SAX, como se muestra en el codigo siguiente (del que hemos eliminado el codigo de creacion del conjunto de datos, la medida del tiempo y el registro):
p r o c e d u r e TForml.btnReadSaxOpenClick(Sender: TObject); var agent: TXmlStandardProcessorAgent; reader: TXmlStandardDocReader; filename: string; begin L o g : = memoLog. L i n e s ; filename := ExtractFilePath (Application.Exename) + ' d a t a 3 . xml '; agent : = TXmlStandardProcessorAgent.Create(ni1); reader:= TXmlStandardDocReader.Create ( n i l ) ; try reader.NextHandler : = TDataSaxHandler.Create (nil); / / our custom c l a s s agent.reader : = reader; agent.processFile(filename, filename); finally agent.free; reader.free; end; end;
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
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 es solventar este problema: el programa utilizado s 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. Sometido a1 s
W para su es n f C i m u c h &qxis&~ .Mgieron. co.apartilo cular 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 .NET Microsoft, asi como de las platafonnas aduales de Sun y Omclc. de
SOAP reemplazara, a1 menos entre ordenadores diferentes, el uso de COM. Del mismo modo, la definicion de un servicio SOAP en el formato Web Services Description Language (WSDL) sustituira a1 IDL y las bibliotecas de tipos utilizadas por COM y COM+. Los documentos WSDL son otro tipo de documentos XML que proporcionan la definicion de metadatos de una consulta SOAP. Cuando obtenemos un archivo en este formato (generalmente publicado para definir un servicio), podremos crear un programa para llamarlo. De manera especifica. Delphi proporciona una proyeccion bidireccional especifico entre WSDL y las interfaces. Esto significa que podemos coger un archivo WSDL y generar una interfaz para el. Podemos incluso crear un programa de cliente que incluya las consultas de SOAP mediante estas interfaces y utilizar un componente especial de Delphi que nos permita convertir las consultas de la interfaz local en llamadas SOAP (a menos que queramos generar manualmente el XML necesario para una consulta SOAP). En sentido inverso, podemos definir una interfaz (o utilizar una existente) y permitir que un componente Delphi genere una descripcion WSDL para ella. Otro componente nos proporciona una proyeccion de SOAP a Pascal para que a1 insertar este componente y un objeto que implemente la interfaz dentro del programa de servidor, consigamos en unos minutos poner en marcha un servicio Web.
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):
- - .- - - A n -
ll i
--
// omitido
initialization InvRegistry.RegisterInterface(TypeInfo(BabelFishPortType), ' urn:xmethodsBableFish ', ' ') ; 1nvRegistry.RegisterDefaultSOAPAction (TypeInfo (BabelFishPortType), ' urn:xmethodsBableFish#Babe1Fish ') ; end.
Observe que la interfaz hereda de la interfaz Ilnvokable. Esta interfaz no aiiade nada con respecto a 10s metodos de la interfaz base llnterface de Delphi,
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 no'sea M p b m e (es v paf'd 6 i a 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 ! ' )) ; (GetBabelFishPortType .BabelFish ( 'en-it'
'Hello,
Si se presta atencion a1 codigo de la funcion G e t B a b e l F i s h P o r t T y p e , se vera que crea una componente interno de invocacion de la clase THTTPRIO para procesar la Ilamada. Puede colocarse manualmente este componente en el formulario cliente (corno en el programa de ejemplo) para tener mas control sobre sus diversas variables (y controlar sus eventos). Este componente puede configurarse de dos maneras basicas: puede hacerse referencia a1 archivo o a1 URL WSDL, importarlo, y extraerlo a partir del URL de la llamada SOAP; o puede proporcionarse un URL direct0 para la Ilamada. El ejemplo tiene dos componentes que proporcionan 10s enfoques alternatives (que tienen exactamente el mismo efecto):
o b j e c t HTTPRIOI: T H T T P R I O WSDLLocation = 'C:\md6code\23\BdbelFish\Bdbe1FishService.~ml' Service = ' B a b e l F i s h ' Port = ' B a b e l F i s h P o r t ' end o b j e c t HTTPRI02: T H T T P R I O U R L = 'http://services. xrnethods. net :80/perl/sodplite. c g i ' end
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
i
.--.-.__I
len_de
Dud
Figura 23.2. Un ejemplo de la traducclon de ingles a alernan conseguida con BabelFish de AltaVista rnediante un serviclo W e b .
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.
Si definimos una interfaz directamente en el codigo, sin necesidad de utilizar una herramienta como el Type Library Editor, conseguimos una gran ventaja, ya que podemos crear facilmente una interfaz para una clase ya existente sin tener que aprender a utilizar una herramienta especifica para ello. Fijese en que le hemos dado un GUID a la interfaz, como es habitual, y que hemos utilizado la convencion de llamada stdcall,ya que el convertidor de SOAP no soporta la convencion de llamada predefinida, reg ister . En la misma unidad que define la interfaz del servicio, deberiamos ademas registrarlo. Esta operacion sera necesaria tanto en la parte del cliente como en la
del servidor, ya que podremos incluir la unidad de definicion de esta interfaz en ambos.
uses 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;
La implementacion de estas funciones, que llaman a1 codigo del sistema de conversion a1 euro del capitulo 3, no se cornentan aqui porque tiene poco que ver con el desarrollo del servicio. No obstante, es importante tener en cuenta que esta unidad de implementacion tambien tiene una llamada de registro en su seccion de inicializacion:
1nvRegistry.RegisterInvokableClass (TConvert);
NOT*: Aunclu6 otr& arcpitccturas de servicios Web proporcionan auto^^^ uu modo de el seretieio%bWd'e el navegador, e m th&aqsuele ~ e MA, 9orque utilizar seryiciqs web'kene sentido en r una arquitectQraen la qqs is.ieractbeadiSiqt8s aplicadbneg. Si todo l t p e u se necesita es mostrar datos ed pn aahgkdm. dskria creme un 'BitioWeb.
PortTypes:
Iconvert [WSDL]
0 0
0
0
0
0
WSIL:
Figura 23.3. La descripcion del servicio Web ConvertService proporcionada por componentes Delphi.
Esta caracteristica autodescriptiva no estaba disponible en 10s servicios Web creados en Delphi 6 (que solo proporcionaba un listado WSDL a bajo nivel), per0 es bastante sencilla de afiadir (o personalizar). Si se analiza el modulo Web SOAP de Delphi 7, ser vera una accion predefinida con un controlador para el evento OnAct i o n que invoca el siguiente comportamiento predefinido:
WSDLHTMLPublishl.ServiceInfo(Sender, Handled) ;
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.
procedure TForml.FormCreate(Sender: TObject); begin Invoker : = THTTPRio.Create(ni1); 1nvoker.URL : = 'http://localhost/scripts/ConvertService.exe/ soap/iconvert ' ; ConvIntf : = Invoker a s IConvert; end;
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 ; ' ,
Despues de seleccionar dos divisas, podemos realizar la conversion con este codigo (la figura 23.4 muestra el resultado):
procedure TForml.ButtonlClick(Sender: TObject); begin LabelResu1t.Caption : = Format ('tn', [(ConvIntf.ConvertCurrency( ComboBoxFrom.Text, ComboBoxTo-Text, StrToFloat(EditAmount.Text)))]); end;
-[
Fill List
DEM
"z
I
=I
POCOS
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.
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;
La implementacion del servicio Web recae en 10s dos metodos anteriores y algunas funciones auxiliares para gestionar 10s datos XML devueltos. Pero antes de llegar a la parte XML del ejemplo, vamos a analizar brevemente la seccion de acceso a la base de datos.
from
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;
function MakeXmlAttribute (attrName, attrvalue: string) : string; begin Result : = attrName + ' = " ' + attrvalue + ""; 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: programa establece cada vez un;a nueva conexion con I 1 :una operation bastante lenta); perco la pmte positiva es qu -'- - - - relmmnaan con el ilnn ne ilna -r------- milltihun --s e eiecil- - - -1 1 mln ---=- - --I--: nenwn a r ---- -- --- -- --- anlicacihn 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---
.. .
. . - .,
. .
Prestemos ahora atencion a1 segundo metodo, Get EmployeeData.Utiliza una consulta parametrica y da formato a 10s campos resultantes en nodos XML independientes (mediante otra funcion auxiliar, FieldsToXml):
function TSoapEmployee .GetEmployeeData (EmpID: string) : string; var dm: TDataModule3; begin dm : = TDataModule3 .Create (nil); try dm.dsEmpData.ParamByName('ID') .Asstring : = EmpId;
f u n c t i o n FieldsToXml (rootName: string; data: TDataSet): string; var i: Integer; begin Result : = ' < ' + rootName + ' > ' + sLineBreak;; f o r i : = 0 to data.FieldCount - 1 d o Result : = Result + ' ' + MakeXmlStr ( Lowercase (data.Fields [i] FieldName) , data. Fields [i] .Asstring) + sLineBreak; Result : = Result + ' < / I + rootName + ' > ' + sLineBreak;; end;
El componente Client D a t aSet no esta conectado a1 proveedor, ya que trataria de abrir el archivo de datos XML especificado por la transformacion. En este caso, 10s datos XML no se encuentran en un archivo, si no que se pasan a1 componente tras llamar a1 servicio Web. Por este motivo, el programa lleva directamente en el codigo 10s datos a1 ClientDataSet.
procedure TForml.btnGetListClick(Sender: TObject); var
strXml: string;
begin
Con este codigo, el programa puede mostrar la lista de empleados en una DBGrid, como muestra la figura 23.5. Cuando se consiguen 10s datos de un empleado especifico, el programa extrae el identificador del registro activo desde el ClientDataSet y muestra el XML resultante en un campo de memo:
procedure TForml.btnGetDetailsClick(Sender: TObject); begin Memo2.Lines.Text : = GetISoapEmployee.GetEmployeeData( 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:
String;
Content Type fext/xrnl User Agent Baland SOAP 1 2 Host Iocalhosl 1024 Contenc Length 508 Cormct~on Keep Alwe Cache Control n o a c h e
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 end ; initialization (out Obj : TObject) ;
Obj : = TMyImplClass.Create;
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.
El modulo de datos creado para un servidor DataSnap basado en SOAP define una interfaz personalizada (para que se le puedan aiiadir metodos) que hereda de IAppServerSOAP, que se define en una interfaz publicada (incluso aunque no herede de Ilnvokable).
para exponer'datos media& SOAP. ~ e l i h 7 ha py$t&do a ese ~ & r i predefmido mediante la interfaz beredada lAppSe;iu&fJ~I?. ,que es fbncionalmente identica pero permite que el eistam pueda determinar el tipo de llamada atendiendo a1 nombre de la interfaz. En breve, veremos c6mo llamar a una aplicaci6n antigua desde un cliente creado con Delphi 7, ya que este proceso no es autondtticol .
La clase de implementation, TSoapTes t Dm, es el modulo de datos, como en otros servidores DataSnap. Este es el codigo Delphi generado, con el aiiadido del metodo personalizado:
type ISampleDataModule = interface(IAppServerS0AP) [ ' {D47A293F-4024-4690-9915-8A68CB273D39) '1 f u n c t i o n GetRecordCount: Integer; stdcall; end; TSampleDataModule = class(TSoapDataModule, ISampleDataModule, IAppServerSOAP, IAppServer)
DataSetProviderl: TDataSetProvider; SQLConnectionl: TSQLConnection; SQLDataSetl: TSQLDataSet; public f u n c t i o n GetRecordCount: Integer; stdcall; end;
El TSoapDataModule base no hereda de T I n v o k a b l e C l a s s . No se trata de un problema siempre que se proporcione un procedimiento de creacion adicional para crear el objeto (que es lo que hace automaticamente la T I n v o k a b l e C l a s s ) y se aiiada el codigo de registro (como ya se comento anteriormente):
p r o c e d u r e TSampleDataModuleCreateInstance(out begin obj : = TSampleDataModule .Create (nil); end ; obj: TObject);
La aplicacion servidor tambien publica las interfaces IAppServerSOAP e IAppServer, gracias al codigo (breve) de la unidad SOAPMidas. Como comparacion, puede encontrarse el servidor DataSnap de SOAP creado con Delphi 6 en la carpeta SoapDataServer. El ejemplo sigue pudiendose compilar en Delphi 7, y funciona bien, per0 deberian escribirse 10s nuevos programas, con la estructura del nuevo; en la carpeta SoapDataServer7 se encuentra un ejemplo.
.
- -
- - -- -
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) ;
90020 Triggerfishy 90030 Snapper 90050 Wrasse 90070 Angelfish 9M80 Cod 90090 Scorpionlish
Clown Triggerf'ish Red Emperor G~anl m r ~ M Wrasse Blue Angellish Lunartail Rockead Fnefish
Figura 23.7. El ejemplo Fishclient recibe un ClientDataSet binario dentro de un adjunto de SOAP.
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 UDDI? e
La especificacion Universal Description, Discovery, and Integration (UDDI) es un esfuerzo para crear un catalog0 de servicios Web ofrecidos por empresas de
todo el mundo. El objetivo de esta iniciativa es crear un marco de trabajo abierto, global e independiente de plataformas para permitir que las entidades de negocio se encuentren entre si, definir como interactuan con la red Internet y compartir un registro de negocio global. Por supuesto, la idea es acelerar la adopcion del comercio electronico, en forma de aplicaciones B2B. Basicamente, UDDI es un registro de negocios global. Las empresas pueden registrarse en el sistema, describir su organizacion y 10s servicios Web que ofrecen (en UDDI, el termino "servicios Web" se usa en un sentido muy amplio, incluyendo direcciones de correo electronico y sitios Web). La informacion del registro de UDDI para cada empresa se divide en tres campos:
Paginas blancas: Incluyen informacion de contacto, direcciones y cosas similares. Paginas amarillas: Registran la compaiiia en una o mas taxonomias, incluyendo categorias industriales, productos vendidos por la empresa, information geografica y otras taxonomias (posiblemente personalizables). Paginas verdes: Proporcionan la lista de 10s sewicios Web ofrecidos por la empresa. Cada servicio se lista bajo un tip0 de servicio (llamado un tModel), que puede estar predefinido o un tip0 descrito especificamente por la empresa (por ejemplo en terminos de WSDL).
Tecnicamente, el registro UDDI deberia verse como un DNS actual, y deberia tener una naturaleza distribuida similar: varios servidores, reflejados y con almacenamiento de datos para acelerar 10s procesos. Los clientes pueden guardar en cache datos siguiendo unas reglas determinadas. UDDI define modelos de datos especificos para una entidad de negocio, un sewicio de negocio y una plantilla de enlace. El tipo BusinessEntity incluye informacion esencial sobre el negocio, como su nombre, la categoria a la que pertenece informacion de contacto. Soporta las taxonomias de las paginas amarillas, con informacion industrial, tipos de producto y detalles geograficos. El tipo Businessservice incluye descripciones de 10s servicios Web (usados por las paginas verdes). El tip0 principal es solo un contenedor para 10s servicios relacionados. Los s e ~ i c i o pueden estar ligados a una taxonomia (zona geografis ca, producto, etc.. .). Toda estructura BusinessService incluye una o mas BindingTemplates (la referencia a1 s e ~ i c i o )La BindingTemplate tiene un tModel. . El tModel incluye informacion sobre formatos, protocolos y seguridad, y referencias a especificaciones tecnicas (probablemente mediante el formato WSDL). Si varias empresas comparten un tModel, un programa puede interactuar con todas ellas con el mismo codigo. Un programa de negocio determinado, por ejemplo, puede ofrecer un tModel para que otros programas interactuen con el, sin importar la empresa que haya adoptado el software. La API de UDDI se basa en SOAP. Mediante SOAP, se pueden registrar datos y consultar un registro. Microsoft tambien ofrece un SDK basado en COM, e
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~ ,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 ) . u
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 t o r lmicro
Search by Name
Search by Category
] Description
Micro C, Inc. Micro Focus Micro Focus Micro lntormalica LLC MICRO MACHINES Micro Motion Inc. MicroApplications. Inc. rnrcrobizl We provide systems inte... Welcome to the future of ... Welcome to the future of ... This is a UDDI Business ... Plant and Machinery for ... Micro Motion manufactur ... informalion syslems dev... desc
1 BusinessKay
516ab96a-50f5-48b4-9fO4- ... 7e76378cfa28-47a2-b8a... 9566~530-7d59-11d6-8c3 ... dce959d-200d-4d9e-bee ... ca2551 cc088f-46b7-9cl ...
4
_1
87f5ta08-508e-4065-b379 ...
d4e4b830-fl9e-4edI-9144... a23~901e-834~-4b8~bf3...
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="Scott Witkin"> - .rd~scove~-vURLC>
Cuando arranca el programa, enlaza el componente HTTPRIO que alberga con la interfaz Inquiresoap de UDDI, definida en la unidad inquiry-vl que proporciona Delphi 7:
procedure TForml.FormCreate(Sender: TObject); begin httpriol-Url : = comboRegistry.Text; inftInquire := httpriol as Inquiresoap; end;
UDDI-necesitan muchos parhmetros, se ha importado empleando un 6nico parametro basado en un registro de tipo FindBusiness. Devuelve un objeto
businessList2:
procedure TForml.btnSearchClick(Sender: var findBusinessData: Findbusiness; businessListData: businessList2; begin httpriol.Ur1 : = cornboRegistry.Text;
TObject);
El objeto bus ines sList 2 es una lista que se procesa en el metodo bus ines sListToListView del formulario principal del programa, mostrando 10s detalles mas importantes en un componente de vista de lista:
procedure TForml.businessListToListView(businessList: businessList2); var i: Integer; begin ListViewl.Clear; for i : = 0 to businessList.businessInfos.Len do begin with ListViewl.Items.Add do begin Caption : = businesslist. businessInfos [i] .name; SubItems.Add (businessList.business1nfos [i].description); SubItems.Add (businessList.business1nfos [i].businessKey); end ; end ; 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
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 (disponino bles gratuitamente en www.marcocantu.com/cantoolsw) 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
menciona en el capitulo 4. Como realiza una operacion de busqueda y reemplazo en un archivo fuente base puede usarse siempre que necesitemos codigo repetido y el nombre de la clase (u otra entidad) cambie. O O P Form Wizard: Mencionado en el capitulo 4. Permite ocultar 10s componentes publicados de un formulario, haciendo el formulario m b orientad0 a objetos y ofreciendo un mejor mecanismo de encapsulacion. Debemos ejecutarlo cuando un formulario este activo y rellenara el controlador de eventos O n c r e a t e . Despues, tendremos que mover manualmente parte del codigo a la seccion de inicializacion de la unidad. Object Inspector Font Wizard: Permite cambiar el tip0 de letra del Object lnspector (algo especialmente util para presentaciones, ya que el tipo de letra del Object lnspector es demasiado pequeiio para mostrarse con facilidad en una pantalla de proyeccion). Otra opcion permite modificar una caracteristica interna del Object lnspector y mostrar 10s nombres de 10s tipos de letra (en la lista combinada desplegable de esa propiedad) usando un tipo de letra especifico. Rebuild Wizard: Permite reconstruir todos 10s proyectos de una subcarpeta determinada desputs de cargar cada uno de ellos secuencialmente en el IDE. Podemos usar este asistente para coger una serie de proyectos (como 10s de este libro) y abrir el que nos interesa haciendo clic en la lista:
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-
puesto). En ocasiones ocurren errores relacionados con el Portapapeles provocados por este asistente.
VCL Hierarchy Wizard: Muestra la jerarquia (casi) completa de VCL, incluyendo componentes de terceros que hayamos instalado, y permite buscar una clase y ver multiples detalles (clases basicas y subclases, propiedades publicadas, etc.). Hacer clic en el boton regenera tanto la lista como el arb01 (secuencialmente, por lo que la barra de progreso se muestra dos veces):
La lista de clases se genera usando un nucleo de clases predefinido (faltan algunas por lo que se aceptan sugerencias) y aiiadiendo cada componente de 10s paquetes instalados (10s de Delphi, 10s nuestros y 10s de terceros) junto con las clases de todas las propiedades publicadas que son de tip0 clase. A pesar de todo, las clases usadas solo como propiedades publicadas no se incluyen. Extended Database Forms Wizard: Hace muchas mas cosas que el D a t a b a s e Forms Wizard disponible en el IDE de Delphi, permitiendonos elegir 10s campos que queremos colocar en un formulario y usar conjuntos de datos diferentes de 10s basados en BDE. Multiline Palette Manager: Permite convertir l a c o m p o n e n t Palette de Delphi en un control de fichas con multiples lineas de fichas:
E !bwks\rnd7code\08\VI1\VI1dpr
E-\books\md7code\OB\Pol1Fo1m\PoliForrn dp E:\books\rnd7code\OBF1arnes2\F1arnes2 dp
-1%
Fums
-_-_I
El codigo fuente del programa esta disponible en la c a r p e t a T o o l s del codigo del libro. El programa VclToClx convierte nombres de unidades (basandose en un fichero de configuracion) y manipula 10s DFM renombrando 10s archivos DFM a XFM y corrigiendo las referencias en el codigo fuente. El programa no es sofisticado, no analiza el codigo pero busca apariciones del nombre de la unidad seguidos de una coma o un punto y coma, como ocurre en una sentencia u s e s . Tambien requiere que el nombre de la unidad vaya precedido por un espacio, pero podemos modificar el programa para que busque una coma. No debemos saltarnos esta comprobacion, jo la unidad Forms se convertira en QForms per0 la unidad QForms se reconvertira en QQForms!
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,
Borland present6 un Debug Inspector de tiempo de ejecucion, que tiene una interfaz similar y muestra informacion parecida. Antes de que Borland aiiadiera esta caracteristica, el autor implement6 una copia del Object Inspector de tiempo de ejecucion pensado para depurar programas:
DlapMode
Enabbd EutsmkdSckcl
+Font
M d TIW
Trw
[ O W 01343DF8)
Charsc(
Cdn
He* Nams Pilch S k
,, ;.
1 &ridowTed
. , l
T m t Nau Roman
IpDeld 0
SM
. -
Este programa ofrece acceso de lectura y escritura a todas las propiedades publicadas de un componente, y tiene dos cuadros combinados que permiten seleccionar un formulario y un componente dentro del formulario. Algunos de 10s tipos de propiedad tienen editores de propiedades a medida (listas y similares). Podemos situar el componente Ob je c t D e b b u g e r en el formulario principal de un programa (o podemos crearlo dinamicamente en el codigo): aparecera en su propia ventana. Puede mejorarse, per0 incluso en su forma actual esta herramienta es practica y tienen muchos usuarios. El codigo fuente de este componente esta disponible en la carpeta ~ o o l del s codigo del libro.
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: object: [TList - 161 buffer with heap pointer [00C032BO] string: [5-11 : Edit1 object: [TEdit - 5441 object: [TFont - 361 object: [TSizeConstraints - 321 object: [TBrush - 241 buffer with heap pointer [00C01FE4] buffer with heap pointer [00COlF18] string: [O-01 : dD string: [ll-21: c:\mman.log
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), que significa que pueden lo 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.
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.
le la base de datos de ejemplo; fonnamt parte da de Delphi. &En otros casosi es &esa@'la WSLOIJ ~lt: t;jernpw I I I L G ~ ~ S B EMPLOYEE.[ y tambih el sewidor & DWG merflase, pba sqguaeao).
<
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
identificador Delphi que este buscando (no es un completo motor de busqueda per0 se le acerca mucho). La estructura del directorio del codigo de ejemplo es bastante simple. Basicamente, cada capitulo del libro posee su propia carpeta y una subcarpeta para cada ejemplo (ej: 0 3 \ F i l e L i s t).En el texto, se hace referencia a 10s ejemplos solo por su nombre (ej: FileList). chivo Readrne.de 10s archlvos de c M g q r u , ~ que conuene Importanre informacion sobre el uso legal y efectivg ,, sdware.
En la carpeta Delphi7 del CD-ROM encontrara la version de prueba de Delphi 7, la edicion superior limitada en el tiempo. Para poder instalar esta version de Delphi 7, que le permitira seguir sin problemas todos 10s ejemplos descritos en el libro, es necesario que efectue una operacion de registro para obtener el numero de serie y la clave de activacion. No tiene mas que seguir las instrucciones de la utilidad de instalacion para completar el proceso de registro y activar su Delphi 7. Para ello necesitara disponer de una conexion a Internet y una cuenta de correo electronico. En el proceso tendra que crear una cuenta de registro en la pagina Web de Borland, responder a algunas preguntas y, finalmente, obtendra por correo electronico el numero de serie y la clave de activacion. Tambien encontrara en la carpeta PDF, anexos en 10s que le explican entre otras cosas, algunas de las tecnologias que conforman la iniciativa .NET, 10s cambios especificos que se realizaron en el lenguaje Delphi para hacerlo compatible con el Common Language Runtime, etc.
alfabetico
#O, 356#10, 141 #13#10, 141 $, 818 $00000000, 234 SOOFFFFFF, 234 $IFDEF LINUX, 228 $LIBPREFIX, 54 1 $LIBSUFFIX, 541 $LIBVERSION, 541 &, 255, 261, 1083 {$IF), 90 {$IFDEF LINUX), 140 {SIFDEF MSWINDOWS), 140 {$M+), 174, 176, 1133 {$Warn UNSAFE-CAST OFF), 73 {$Warn UNSAFE-CODE OFF), 73 {$Warn UNSAFE-TYPE OFF), 73 -DF, extension, 78 -DP, extension, 79 -PA, extension, 81 .NET, 657, 1051 .NET Framework Assembly Registration Utility, 657 arquitectura, 221, 493 COM y, 656 :=, 51 @, 1117 p d d R e f , 124, 600 declspec(dllexport), 533 -Release, 600, 6 10
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
Addchild, 276, 1092 AddNode, 280 Addobject, 269 Adjuntos, 1149 AdjustLineBreaks, 144 ADO, 803-805 Cursor Engine, 822 ADO.NET, 845 ADOCommand, 808 ADOConnection, 808, 81 1, 813, 821, 829-830 ADODataSet, 808, 837 ADODB, 812 ADOQuery, 808, 818 ADOStoredProc, 808 ADOTable, 808, 81 1, 816 ADOX, 814 adResyncUnderlyingValues, 840 ADTG, 844 AffectRecords, 840 Afterconnect, 869 AfterDelete, 865 AfterEdit, 687 AfterInsert, 787 Afteropen, 688 AfterPost, 687, 863, 865 Afterscroll, 866 AfterUpdateRecord, 873 Agente de conexion, 87 1 akBottom, 257 akRight, 257, 355 akTop, 355 alBottom, 1060 alclient, 1060 Alignment, 69 1, 862 AllocMemCount, 139 AllocMemSize, 139 AllowAl1Up, 3 17 AllowGrayed, 24 1 AlphaBlendValue, 366 Alto-bajo, tecnica de, 786 alTop, 260, 1060 1083 ampersand (t), Anchor, 285 Anchors, 257, 355 Anclajes, 257 Animate, 366 Animatewindow, 367 ANSI, 81 4 AnsiContainsText, 149 AnsiDequotedStr, 143 AnsiIndexText, 149 AnsiMatchText, 149 AnsiQuoteStr, 143 AnsiReplaceText, 149 AnsiResembleText, 149 AnsiString, 143
Apache, 1057, 1086 API de Windows, 234,265, 531 Aplicaciones Delphi, arquitectura, 403 AppID, 1060 Application, 108, 181, 262 ApplicationEvents, 128, 404 ApplyUpdates, 864,1041, 1075, 1148, 1106 AppServer, 869 Arb01 de datos, 275 Architect Studio, 36 ArrangeIcons, 424 Arrastre, 156 as, 120, 164, 195 ASI400, 807 ASCII, 244 archivo, 975 Asignacion de objetos, 105-107 Asociativas, listas, 198 Assign, 175 Assigned, 109 AssignTo, 175 Associate, 249, 377 11 17 at At, 22 1 Atozed Software, 1050-1051 Attributes, 830 AutoActivate, 633 Autocheck, 250 Autocomplete, 244 AutoCompleteOptions, 245 Autoconnect, 626 AutoHint, 302 AutoHotKey, 261 Automation, 612, 614 Automatization OLE, 6 12 interfaz de, 629 servidor de, 6 17 AutoPaletteScroll, 64 AutoPaletteSelect, 64 Autosnap, 259 AWpHORpNEGATIVE, 367 AW-HOR-POSITIVE, 367 AW-VER-NEGATIVE, 367 AW-VER-POSITIVE, 367 AxBorderStyle, 646
(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
tipo de, 823 ubicacion de, 822 CursorLocation, 834 CursorRect, 264 CurValue, 839 CustomConstraint, 86 1
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
Decision Cube. 173 Defittributes, 239 default, 100 DefaultColWidth, 890 DefaultDrawing, 892 DefaultExpression, 861 DefaultRowHeight, 896 DefaultTextLIneBreakStyle, 140 DefaultTimeout, 1043 DefineProperties, 175 Definepropertypage, 644 Definidores de estado, 250 Delete, 194, 1037 delete-xx, 1 153 Delphi ediciones de, 36 paquetes, 550 Delphi for .NET Preview, 656 Delphi Form Module (DFM), 224 DELPHI32.DCT, 6 6 DelphiMM, 154 Delta, 674 paquete, 865 DEM, extension, 82 Dephi ActiveX (DAX), 173 deprecate, 90 Depurador, 87 Derechos de acceso, 1047 DESC, 826-827 Descendiente, conversion, 1 19 Design Critic, 584 DesignIntf, 522 DesignOnly, 562 DesingEditors, 522 Destroy. 104, 108 DestroyComponents, 180 Destruccion de objetos, 108 Destructores, 104 DevelopMentor, 1 130 DFM, 174, 207, 224 archivos, 57, 83 extension, 7 8 DFN, extension, 79 Diagram Editor, 574 Diagram View, 54 Diagram, 54-55Diagramas de actividad, 574 de clase, 569 de colaboracion, 574 de componentes, 574 de dependencia de unidades, 574 de despliegue, 574 de estado, 574 de mapa mental, 574 de robustez, 574 de secuencia. 57 1
Difference, pestaiia, 583 Dinamica, biblioteca, 72 Dinamicas, propiedades, 8 12-8 13 Dinamico, enlace, 528 Dinamicos, mttodos, 1 17 Directorio en un conjunto de datos, 9 18-9 19 DisableCommit, 652 DisableIfNoHandler, 3 14 Disparador de servidor, 788 Disparadores, 79 1 dispinterface, 614, 61 6-617, 640 DisplayFormat, 862, 864, 992 DisplayLabel, 689, 862 DisplayName, 891 DisplayText, 891 DisplayType, 1039 DisplayValues, 862 Displaywidth, 818, 862 Distributed COM (DCOM), 8 5 1 Divisas, 158 DivMod, 146 DLL, 528-530, 531-532, 535, 544, 549-551, 648 en tiempo de ejecucion, 542 creacion de, 534-535 de C++, 532-534 extension, 79 normas de creacion, 530 dmAutomatic, 157, 277 DMT, extension, 82 DNS, 1 152 doAutoIndent, 1090 Document Import Signature, 586 Document Type Definition, 1098 Documentacion, 585 DOF archivo, 83 extension, 79 DOM, 1085 para creacion de documentos, 1090-1094 programacion con, 1085-1 086 DOMVendor, 1083 DoVerb, 631 DPK, extension, 79 DPKL, extension, 79 DPKW, extension, 79 DPR, 83 extension, 79, 581 DragMode, 157, 277 DragTree, 278 Draw, 431 Drawcell, 890 DrawText, 892 DRO, extension, 82 DropDownRows, 676 dsBrowse, 686 dsCalcFields, 686
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, 16 2
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
FilterGroup, 835-836, 839 Filtrado, 672 finally, 125-126 Find, 395 find-xx, 1153 Findclose, 162 Findcomponent, 184, 392 FindFist, 162 FindGlobalComponent, 185 FindNext, 162 FindNextPage, 288 FList, 201 FloatToCurr. 143 FloatToDateTime, 143 Flotantes, sugerencias. 262 fmOpenRead, 205 fmShareDenyWrite, 205 fmShareExclusive, 206 Foco, 188, 256 de entrada, 254 FocusControl, 54, 255 FollowChange, 674 FOnChange. 19 1 FOnCLick, I90 Font, 233, 896 FontDialog, 394 FontNamePropertyDisplayFontNames, 60 Footer, 992 ForEach, 199 Form Designer, 56, 62, 83 Formstyle, 423 Formularios dentro de paquetes, 553 formview, 1039 FRecordCount, 91 0 Free, 104, 108-109, 198 FreeAndNil, 108 FreeLibrary, 542, 556 FreeMem, 139 FreeNotification, 41 1 FriendlyName, 1062 FROM, clausula, 821 FromCommon, 161 FromStart, 1071 fsMDIChild, 423 fsMDIForm, 423 FTP, 982 Fuentes, 233 desplegables, 60 FullExpand, 277 function, 92
Generadores de 64 bits, 786 GeneratorField, 788 Gestion de archivos, 162 get-xx, 1 153 GetBufStart, 276 GetCanModify, 921 Getclass, 556 GetDataAsXml. 1 107 GetFileVersion, 144 GetInterfaceExternalName, 1 137 GetKeyState, 304 GetLocaleFormatSettings, 144 GetLongHint, 303 GetMem, 139 GetMemoryManager, 139 GetMethExternalName, 1 137 GetNamePath, 175 Getobjectcontext, 652 GetOleFont, 627 Getolestrings, 628 Getowner, 175 GetPackageDescription, 562 GetPackageInfo, 562, 564 GetPackageInfoTable, 139, 562 GetProcAddress, 542, 546 GetPropInfo, 178 GetPropValue, 178 GetShareData, 549 GetShortHint, 303 GetStrProp, 178 GetSystemMetrics, 355 GetTickCount, 624 GetVerb, 522 GetVerbCount, 522 GExperts, 4 1 glibc, 154 Global Desktop, 39 Google, 978 Goteo de memoria, 105 GreaterThanValue, 146 grEOF, 9 12 GroupBox, 181 , 2 4 1 Grouped, 3 17 GroupIndex, 3 17, 429, 6 3 1 GUID de Windows, 786 GUlD, 122, 657, 813, 1135 GuidAttribute, 657
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
IAppServer, 850-851, 1 146-1 148 IAppServerSOAP, 1 146-1 148 IBBackupService, 777 IBConfigService, 777 IBDatabase, 777 IBDatabaseInfo, 777 IBDataSet, 780, 792 IBEvents, 777 IBInstall, 777 IBLogService, 777 IBQuery, 780 IBRestoreService, 777 IBSecurityService, 777 IBServerProperties, 777 IBSQL, 777 IBSQLMonitor, 777, 783 IBStatisticalService, 777 IBUninstall, 777 IBUpdateSQL, 779-780 IBValidationService, 777 IBX, 777 ICO, extension, 77
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
LabeledEdit, 59, 238 LabelPosition, 238 Labelspacing, 238 Languaje Exceptions, 129 LargeImages, 270 LayoutChanged, 895 IbOwnerDrawFixed, 267 IbOwnerDrawVariable, 267 Left, 232 LessThanValue, 146 library, 90, 535 LIC, extension, 80, 640 Licencias, 1164 LIFO, 197 Ligeros, clientes, 850 like, 788 Linestart, 2 16 Linuy 162, 170, 200, 227, 932 List Template Wizard, 1159 Lista de referencias grafica, 270-275 Listas, 242 ListBox, 242, 267 listener, 189 Listview, 270 LIVE-SERVER-AT-DESIGNTIME, 626 LoadBalanced, 874 LoadFromFile, 193, 276, 844 LoadFromStream, 204 LoadLibrary, 542, 556 Loadpackage, 556 LocalConnection, 855 Locate, 673 Locked, 6 3 1 LockType, 831, 834, 840 LogChanges, 675 LoginFormAdapter, 1046 LongMonthNames, 110 IoPartialKey, 673 Lote, 839 Lotes, 834 Lotus 1-2-3, 815 ItBatchOptimistic, 831, 834, 837, 840 Itoptimistic, 83 1 ItPessimistic, 83 1-832 ItReadOnly, 83 1 ItUnspecified, 831
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, 5 1 2 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
Name mangling, 537, 552 Name, propiedad, 184 Namevalueseparator, 194 NegInfinity, 145 nerError, 152 nerLoose, 152 nerstrict, 152 NetCLX, 170, 173 New Items, cuadro de dialogo, 85 Newvalue, 839 nil, 104, 108-109, 187 No modal, cuadro de dialogo, 390 NodeIndentStr, 1090 NodeType, 1088 Nodevalue, 1088Nod0, 1088 de arbol, 280 Nombre-valor, pares, 194 Nombres de proyecto, cambio de, 540 Normalizacion, 795 Norrnalizacion, reglas de, 790-791 Notebook, 285 Notification, 4 1 1 null, 152 NullAsStringValue, 152 NullEqual~tyRule,152 NullMagnitudeRule, 152 NullStrictConvert. 152
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
OnDragOver, 157, 277 OnDrawItem, 265-266 OnDropDown, 244 OnEnter, 255-256 OnException. 128 OnExit, 255 OnFilterRecord, 686. 8 14 OnFormatCeII, 993 OnGetData, 875 OnGetDataSetProperties. 874 OnGetEditMask, 246 OnGetPickList, 247 OnGetValue, 1044 OnHelp, 353, 404 OnHint, 303, 404 OnIdle, 404 OnKeyDown, 236 OnKeyPress, 237, 356 OnMeasureItem, 265-266, 268 OnMessage, 404 OnMouseDown, 133,236,274 OnMouseMove, 236 OnPageAccessdenied, 1047 OnPaint, 236, 365 OnReceivingdata, 1 150 OnReconcileError, 853, 863, 865 OnRecordChangeComplete, 839 OnRecordsetCreate, 8 13 OnShorCut, 404 Options, 862, 872 Oracle, 807, 842 ORDER BY, clausula, 827 Orientado a objetos, enfoque, 135 out, 101 overload, 93 override, 1 14, 123 Owner, 182, 231 Owner-draw, 265
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
TableName, 816, 819, 9 18 Taborder, 254 Tabs, 64 TabSet, 285 Tabsheet, 284, 286 TabSheets, 285 TabStop, 254 TabVisible, 2 8 8 TActiveForm, 644 TADTField, 692 Tag, 188, 990 TAggregateField, 692 TAlignment, 3 17 TApplication. 130, 404 TArrayField, 692 TAutoIncField, 692 TBCDField, 693 tbHorizonta1, 424 TBitBtn, 499 TBitmap, 204 TBits, 216 TBlobFieId. 204 TBlobStream, 205 TBooleanField, 693 TBucketList, 198 TButton, 164, 166, 221 tbvertical, 424 TCompressStream, 2 13 TControl, 171. 219, 230, 283 TControlClass, 133 TConvTypeFactor, 16 1 TCPIIP, sockets, 852 TCurrencyField, 693 TCustomControl, 639 TCustomDBGrid, 895 TCustomEdit, 494 TCustomGrid, 889, 892 TCustomListBox, 244 TCustomMemoryStream, 204 TCustomVariantType, 15 1, 153 TDataEvent, 879 TDataLink, 878-879 TDataModule, 216, 855 TDataSet, 675, 897, 898, 902, 914, 919 TDatasetField, 870 TDataSetProvider, 863 TDateField, 690 TDateTime, 97, 139, 148, 916, 923 TDateTimeField, 693 TDBMS. 823 TDecompressionStream, 2 15 TDecompressStream, 2 13 TDump, 534 Teamsource, 75 Tema, 304 Template, 66
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
UDDI Browser, 1 153 UDDI, I151 en Delphi 7. 1153 UDL, extension, 8 1 UML, 567, 568-569, 572 UndoLastChange, 674 Union SQL, 833 Unique Table, propiedad, 834 Unit Code Editor, 580 Union interna, 796, 798 UnloadPackage, 556-557 Update, 365 UpdateBatch, 835 UpdateCriteria, 838 UpdateList, 202 UpdateOIeObject, 643 UpdatePropertyPage, 643 UpdateRegistry, 857, 874 UpdatesPending, 836 UpdateSql, 860 Updatestatus, 835 UpdateTarget, 5 12 UpDown, 249 upper, 788 UserFrame, 1071 uses, sentencias, 226, 229 UseSOAPAdapter, 1 148 Usuarios, 1043
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
Variants: 151, 153, 217 Varias paginas, aplicaciones de, 1060 VarUtils, 15 1, 21 7 VBX, 634 VCL Hierarchy Wizard, 1161 VCL, 38, 138, 170,202. 215 VclToClx, 1 I62 Ventanas hijo. 423 marco, 423 Verbos, 6 3 1 Verificacion, valor de, 198 VertScrolIBar, 249 ViewAccess, 1047 virtuaI, 1 14, 1 17 Virtuales metodos, 1 17 teclas, 3 13 Virtuals, 4 8 Visible, 233. 862 VisibleButtons, 676 VisibleRowCount, 896 Vista de diferencias, 582 virtual, 894 Vistas, 54 Visual Basic, 633 VisualCLX, 170, 172, 221 VCL frente a. 220 Visuales, controles, 2 19 Visualizador de registros, 889
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
W3C, 987, 1082, 113 1 Watch List, 87 WAV, archivos, 496 Web App Debugger, 75, 1068, 1143-1 144, 1146 Web Surface Designer, 1040, 1042 Web aplicaciones, 105 1 exponer como un servicio, 1144 servicio, 1 130, 1 134 WebApplication, 1060 WebBroker, 978, 1045, 1066, 1145 Webconnection, 855 WebDispatch, 1109 WebSnap, 48, 978, 1041-1042, 1066 WebUserList. 1045 while, 167 WideCompareStr, 143 WideCompareText, 143 WideFormat, 143 WideLowerCase, 143 WideSameStr. 143
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