Visual C++ Programación Avanzada en Win32 Fco. Javier Ceballos Sierra Profesor titular de la Escuela Politécnica Superior Universidad de Alcalá
http://www.fjceballos.es
Visual C++. Programación Avanzada en Win32 © Fco. Javier Ceballos Sierra © De la edición: RA-MA 1999 MARCAS COMERCIALES: Las designaciones utilizadas por las empresas para distinguir sus productos suelen ser marcas registradas. RA-MA ha intentado a lo largo de este libro distinguir las marcas comerciales de los términos descriptivos, siguiendo el estilo de mayúsculas que utiliza el fabricante, sin intención de infringir la marca y sólo en beneficio del propietario de la misma. RA-MA es una marca comercial registrada. Se ha puesto el máximo empeño en ofrecer al lector una información completa y precisa. Sin embargo, RA-MA Editorial no asume ninguna responsabilidad derivada de su uso, ni tampoco por cualquier violación de patentes ni otros derechos de terceras partes que pudieran ocurrir. Esta publicación tiene por objeto proporcionar unos conocimientos precisos y acreditados sobre el tema tratado. Su venta no supone para el editor ninguna forma de asistencia legal, administrativa ni de ningún otro tipo. Caso de precisarse asesoría legal u otra forma de ayuda experta, deben buscarse los servicios de un profesional competente. Reservados todos los derechos de publicación en cualquier idioma. Ninguna parte de este libro puede ser reproducida, grabada en sistema de almacenamiento o transmitida en forma alguna ni por cualquier procedimiento, ya sea electrónico, mecánico, reprográfico, magnético o cualquier otro, sin autorización previa y por escrito de RA-MA; según lo dispuesto en el artículo 534-bis del Código Penal vigente serán castigados con la pena de arresto mayor y multa quienes intencionadamente, reprodujeren o plagiaren, en todo o en parte, una obra literaria, artística o científica. Editado por: RA-MA Editorial Ctra. Canillas, 144 28043 MADRID Teléfono: 91 381 03 00 Telefax: 91 381 03 72 Correo electrónico:
[email protected] Servidor Web: http://www.ra-ma.es ISBN: 84-7897-344-3 Depósito Legal: Autoedición: Fco. Javier Ceballos Filmación e impresión: Albadalejo, S.L. Impreso en España Primera impresión: Enero 1999
ÍNDICE PRÓLOGO.............................................................................................................. XXI CAPÍTULO 1. AÑADIR CARACTERÍSTICAS A UNA APLICACIÓN ........
1
VENTANA DE PRESENTACIÓN .................................................................... CARGAR UNA APLICACIÓN UNA SOLA VEZ ............................................ INFORMACIÓN DEL SISTEMA ...................................................................... GetSystemInfo ............................................................................................... GetVersionEx ................................................................................................. GlobalMemoryStatus ..................................................................................... GetDiskFreeSpace .......................................................................................... GetSystemDirectory ....................................................................................... Acerca de........................................................................................................ FORMULARIOS FLOTANTES ........................................................................ SALIR DE WINDOWS DE UNA FORMA CONTROLADA ........................... EJECUTAR UNA APLICACIÓN WINDOWS O DE CONSOLA ................... ABRIR O IMPRIMIR UN DETERMINADO FICHERO .................................. AÑADIR UN ICONO A LA BARRA DE TAREAS ......................................... MENÚS CONTEXTUALES .............................................................................. AÑADIR UN SISTEMA DE AYUDA A UNA APLICACIÓN ........................ Soporte de ayuda proporcionado por AppWizard .......................................... Compilar los ficheros de ayuda ...................................................................... Diseñando el sistema de ayuda ...................................................................... Construir el fichero de ayuda ......................................................................... Ayuda sensible al contexto ............................................................................ Función WinHelp ........................................................................................... Propiedad Context help de las ventanas ......................................................... HTML Help Workshop ..................................................................................
1 5 9 9 12 13 15 15 16 20 23 24 30 31 36 38 39 40 41 46 48 49 49 49
VIII VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Convertir un fichero de ayuda WinHelp ........................................................ Vincular el sistema de ayuda HTML a una aplicación .................................. Ayuda HTML sensible al contexto ................................................................ DISTRIBUCIÓN DE UNA APLICACIÓN........................................................ Colocar el icono de la aplicación en el menú Inicio ...................................... Asignación de grupos a componentes ............................................................ Asignar componentes a cada tipo de instalación ............................................ Ventana de presentación y fichero leame.txt ................................................. Asignar ficheros a los grupos de ficheros ...................................................... Recursos ......................................................................................................... Construir las imágenes de los discos de distribución .....................................
50 51 52 54 58 59 60 60 61 62 62
CAPÍTULO 2. HILOS ...........................................................................................
65
CONCEPTO DE PROCESO .............................................................................. HILOS ................................................................................................................. Estados de un hilo .......................................................................................... Crear de un hilo .............................................................................................. CWinThread.............................................................................................. Finalizar un hilo ............................................................................................. Planificación de hilos ..................................................................................... Asignación de prioridades .............................................................................. Prioridad relativa de un hilo ...................................................................... HILOS UTILIZANDO LA BIBLIOTECA MFC ............................................... COMUNICACIÓN ENTRE HILOS ................................................................... Comunicación utilizando variables globales .................................................. Comunicación utilizando mensajes ................................................................ SINCRONIZACIÓN DE HILOS ........................................................................ Secciones críticas ........................................................................................... Creación de una sección crítica ................................................................. Exclusión mutua ............................................................................................. Semáforos....................................................................................................... Problema del productor-consumidor con semáforos................................. Eventos ........................................................................................................... Eventos con inicialización manual ............................................................ Eventos de inicialización automática ........................................................ Problema del productor-consumidor con eventos ..................................... Espera activa y pasiva .................................................................................... ELEGIR EL TIPO DE SINCRONIZACIÓN ......................................................
65 66 67 68 69 70 70 71 71 72 78 79 80 82 82 87 89 93 95 102 104 104 105 109 113
ÍNDICE
IX
CAPÍTULO 3. COMUNICACIONES .................................................................. 115 COMUNICACIONES POR EL PUERTO SERIE.............................................. Aplicación Win32 para comunicaciones vía RS232 ...................................... Registro de Windows ..................................................................................... Interfaz de comunicaciones ............................................................................ Función Iniciar .......................................................................................... Función Terminar...................................................................................... Función EstablecerConexion .................................................................... Función MensajeDeError .......................................................................... Función ConfigurarDisCom...................................................................... Controlar eventos ...................................................................................... Función LeerCaracteresPuerto .................................................................. Función EscribirCarsPuerto ...................................................................... Función CortarConexion ........................................................................... INTERFAZ DEL USUARIO .............................................................................. ENVIAR Y RECIBIR DATOS ........................................................................... CONTROL DE COMUNICACIONES............................................................... Tipo VARIANT ............................................................................................. Manipular las comunicaciones ....................................................................... Interfaz de comunicaciones ............................................................................ Función Iniciar .......................................................................................... Función Terminar...................................................................................... Función EstablecerConexion .................................................................... Función ConfigurarDisCom...................................................................... Controlar eventos ...................................................................................... Función LeerCaracteresPuerto .................................................................. Función EscribirCarsPuerto ...................................................................... Función CortarConexion ........................................................................... INTERFAZ DEL USUARIO .............................................................................. ENVIAR Y RECIBIR DATOS ...........................................................................
117 120 123 124 126 126 127 130 131 134 138 141 142 143 147 148 153 156 159 161 161 162 163 164 166 166 167 168 172
CAPÍTULO 4. CONTROLES ............................................................................... 175 CONTROL IMAGE LIST .................................................................................. Crear una lista de imágenes............................................................................ CONTROL LIST ................................................................................................ CListCtrl y CListView ................................................................................... Añadir elementos a un control list ................................................................. Añadir columnas a un control list .................................................................. Añadir los subelementos ................................................................................ Vistas del control list......................................................................................
177 177 178 179 182 184 185 185
X VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Iconos grandes .......................................................................................... Iconos pequeños ........................................................................................ Lista .......................................................................................................... Detalles ..................................................................................................... Elemento seleccionado ................................................................................... CONTROL TREE ............................................................................................... CTreeCtrl y CTreeView ................................................................................. Elementos de un control tree .......................................................................... Elementos padre e hijo ................................................................................... Añadir elementos a un control tree ................................................................ Seleccionar un elemento ................................................................................ Sincronización de los controles tree y list ......................................................
187 187 187 188 188 189 190 192 193 193 197 198
CAPÍTULO 5. COMPONENTES SOFTWARE ................................................. 203 COMPONENTES FRENTE A BIBLIOTECAS ................................................ MODELOS DE COMPONENTES ..................................................................... OLE ..................................................................................................................... COM.................................................................................................................... Fundamentos de los objetos COM ................................................................. DCOM ................................................................................................................. OLE 2 .................................................................................................................. ActiveX ............................................................................................................... CONTENEDOR ActiveX ................................................................................... Incrustar un objeto ......................................................................................... Vincular un objeto .......................................................................................... Esqueleto de la aplicación .............................................................................. Activar un objeto utilizando el ratón.............................................................. Eliminar un objeto ActiveX ........................................................................... SERVIDOR ActiveX .......................................................................................... Esqueleto de la aplicación .............................................................................. Completar el servidor ActiveX ...................................................................... SERVIDOR ActiveX AUTOMATIZADO ......................................................... Esqueleto de la aplicación .............................................................................. Completar el servidor ActiveX automatizado ................................................ Añadir una interfaz al servidor ....................................................................... CLIENTE AUTOMATIZADO ........................................................................... INTERFACES..................................................................................................... Interfaz personalizada .................................................................................... Interfaz de tipo dispinterface .......................................................................... Interfaz dual ................................................................................................... Añadir una interfaz dual .................................................................................
204 204 205 206 206 209 212 213 214 215 216 217 219 227 228 229 232 236 237 241 245 249 256 256 257 260 262
ÍNDICE
Implementar la interfaz dual .......................................................................... CONTROL ActiveX ........................................................................................... Crear un objeto ActiveX ................................................................................ Añadir una interfaz al control ActiveX .......................................................... Completar el control ActiveX ........................................................................ Propiedades de un control ActiveX................................................................ Añadir una propiedad normal ................................................................... Añadir una propiedad parametrizada ........................................................ Añadir una propiedad común .................................................................... Añadir una propiedad ambiental ............................................................... Hoja de propiedades .................................................................................. Añadir una nueva página de propiedades ................................................. Añadir una página de propiedades común ................................................ Añadir métodos .............................................................................................. Añadir eventos ............................................................................................... Persistencia..................................................................................................... CONTENEDOR PARA UN CONTROL ActiveX .............................................
XI
264 269 270 271 273 277 278 280 281 282 283 286 287 288 289 292 293
CAPÍTULO 6. ATL ................................................................................................ 297 QUÉ ES ATL ...................................................................................................... VISUAL C++ Y ATL ......................................................................................... CONTROL ActiveX ........................................................................................... Crear un proyecto para un control ActiveX ................................................... Crear un control ActiveX ............................................................................... Clase del control............................................................................................. Visualizar datos en el control ActiveX .......................................................... Añadir una interfaz al control ActiveX .......................................................... Completar el control ActiveX ........................................................................ Propiedades de un control ActiveX................................................................ Añadir una propiedad definida por el usuario ........................................... Añadir una propiedad común .................................................................... Añadir una propiedad ambiental ............................................................... Hoja de propiedades .................................................................................. Añadir una página de propiedades común ................................................ Añadir métodos .............................................................................................. Añadir eventos ............................................................................................... Persistencia..................................................................................................... CONTENEDOR PARA UN CONTROL ActiveX ............................................. PROGRAMACIÓN AVANZADA CON ATL ................................................... Propiedades asíncronas .................................................................................. Utilización del portapapeles ...........................................................................
297 298 300 300 301 304 305 309 313 317 318 322 324 324 330 330 332 336 336 338 338 345
XII VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Funcionalidad necesaria para utilizar el portapapeles ............................... El control pone datos en el portapapeles ................................................... El control obtiene datos del portapapeles ................................................. Arrastrar y soltar ............................................................................................ Control como fuente de datos ................................................................... Control como destino de los datos ............................................................ Controles con interfaz dual ............................................................................
348 353 355 359 359 361 365
CAPÍTULO 7. MULTIMEDIA ............................................................................. 367 ARQUITECTURA MULTIMEDIA ................................................................... TIPOS DE DATOS MULTIMEDIA .................................................................. MULTIMEDIA MCI........................................................................................... CD de audio ................................................................................................... Audio por forma de onda ............................................................................... Audio y vídeo entrelazado ............................................................................. Ejemplo de multimedia MCI .......................................................................... MULTIMEDIA UTILIZANDO LA API DE WINDOWS ................................. Servicios de audio .......................................................................................... Interfaz de control de medios ......................................................................... Dispositivos MCI ........................................................................................... Órdenes MCI .................................................................................................. Abrir un dispositivo ....................................................................................... Tipos de dispositivos ...................................................................................... Reproducir un fichero .................................................................................... Detener un dispositivo ................................................................................... Cerrar un dispositivo ...................................................................................... CD de audio ................................................................................................... Audio por forma de onda ............................................................................... Audio y vídeo entrelazado ............................................................................. PONER SONIDO A UNA APLICACIÓN ......................................................... HIPERMEDIA .................................................................................................... Cargar una imagen ......................................................................................... Establecer y probar zonas activas .................................................................. Guardar y recuperar las zonas activas ............................................................ ANIMACIÓN DE GRÁFICOS........................................................................... Un ejemplo de animación............................................................................... ANIMACIÓN CON CDIB.................................................................................. Un ejemplo de animación con CDIB .............................................................
368 369 370 375 381 383 386 393 393 396 397 398 399 399 400 400 400 400 407 409 412 415 420 422 427 431 433 439 441
ÍNDICE
XIII
CAPÍTULO 8. BIBLIOTECAS DINÁMICAS .................................................... 459 CREACIÓN DE UNA DLL EN Win32.............................................................. Fichero de cabecera (.h) ................................................................................. Fichero fuente (.c o .cpp) ............................................................................... Fichero de definición de módulos (.def) ........................................................ LLAMANDO A LAS FUNCIONES DE LA DLL ............................................. Enlace estático................................................................................................ Enlace dinámico ............................................................................................. RECURSOS EN UNA DLL ............................................................................... Acceso a los recursos en una DLL ................................................................. OBJETOS COM COMO ALTERNATIVA A LAS DLLs ................................. Esqueleto de la aplicación .............................................................................. Añadir métodos .............................................................................................. Utilización del servidor COM ........................................................................ Obtener un puntero a una interfaz ............................................................. Llamando a las funciones de la interfaz .................................................... Clase _com_ptr_t ...................................................................................... Directriz #import ....................................................................................... Añadir otra interfaz ................................................................................... Aplicación de tipo consola ........................................................................
460 462 462 465 465 467 468 471 474 480 481 482 484 485 488 489 491 494 499
CAPÍTULO 9. BASES DE DATOS ...................................................................... 503 CLASES ODBC PARA ACCESO A BASES DE DATOS ................................ ACCESO A UNA BASE DE DATOS UTILIZANDO ODBC .......................... Integridad referencial ..................................................................................... Características de la aplicación ...................................................................... Registrar la base de datos ............................................................................... Crear una aplicación con soporte ODBC para BD ......................................... Diseño de la plantilla de diálogo .................................................................... Asociar los controles con los campos del conjunto de registros .................... Ejecutar la aplicación ..................................................................................... Añadir otro conjunto de registros................................................................... Llenar la lista desplegable con la lista de idiomas ......................................... Establecer un filtro ......................................................................................... Establecer un parámetro ................................................................................. Editar, añadir y borrar registros ..................................................................... Editar un registro....................................................................................... Añadir un registro ..................................................................................... Borrar un registro ...................................................................................... Abandonar una operación de edición o de adición ...................................
504 510 511 511 512 513 517 518 519 520 521 524 525 528 529 530 533 533
XIV VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Registros borrados.......................................................................................... Conjunto de registros vacío............................................................................ OBJETOS ACTIVEX PARA ACCESO A DATOS ........................................... Modelo de objeto ADO .................................................................................. Acceso a los datos con ADO .......................................................................... ADO Comparado con RDO y DAO............................................................... ACCESO A UNA BASE DE DATOS UTILIZANDO ADO ............................. Crear una aplicación con soporte ADO para BD ........................................... Asistente ADO Data Bound Dialog .......................................................... Extensiones Visual C++ para ADO .......................................................... Diseño de la plantilla de diálogo ............................................................... Abrir una conexión ................................................................................... Moverse por la base de datos .................................................................... Ejecutar la aplicación ..................................................................................... Añadir nuevos controles al diálogo Recordset ............................................... Añadir otro conjunto de registros................................................................... Acceder a los campos de un registro .............................................................. Llenar la lista desplegable con la lista de idiomas ......................................... Establecer un filtro ......................................................................................... Habilitar o inhabilitar controles ..................................................................... Actualizar los datos ........................................................................................ Actualización inmediata ................................................................................. Actualización por lotes................................................................................... Cancelar modificaciones ................................................................................ Añadir un nuevo registro................................................................................ Borrar un registro ........................................................................................... Conjunto de registros vacío............................................................................ Características soportadas .............................................................................. Número de registros .......................................................................................
534 535 536 537 539 540 541 542 542 543 547 550 553 554 555 556 558 559 560 561 563 564 565 566 567 569 570 571 571
CAPÍTULO 10. INTERNET ................................................................................. 573 ¿QUÉ ES INTERNET? ....................................................................................... Intranet ........................................................................................................... Extranet .......................................................................................................... Terminología Internet .................................................................................... SERVICIOS EN INTERNET ............................................................................. Correo electrónico .......................................................................................... Conexión remota (telnet)................................................................................ Transferencia de ficheros (ftp) ....................................................................... Noticias (news) .............................................................................................. Conversaciones ..............................................................................................
573 574 574 574 577 578 579 580 582 583
ÍNDICE
Herramientas para búsqueda de información ................................................. World Wide Web (WWW) ....................................................................... Gopher ...................................................................................................... Archie........................................................................................................ La información en Internet ............................................................................. PÁGINAS WEB.................................................................................................. Qué es HTML ................................................................................................ Etiquetas básicas HTML ................................................................................ Etiquetas de formato de texto ......................................................................... URL................................................................................................................ Enlaces entre páginas ..................................................................................... Gráficos .......................................................................................................... Marcos............................................................................................................ Páginas dinámicas .......................................................................................... VBScript en una página Web .................................................................... Insertar un control ActiveX en una página Web ....................................... ActiveX Control Pad ...................................................................................... El editor de textos ..................................................................................... El editor de objetos ................................................................................... El asistente de VBScript o JScript ............................................................ El editor de plantillas ................................................................................ Distribución y licencia de ActiveX Control Pad ............................................ Controles ActiveX para páginas Web ............................................................ Control Marquee ....................................................................................... Control HotSpot ........................................................................................ Control ActiveMovie ................................................................................ Documentos ActiveX ..................................................................................... Objetos de Internet Explorer .......................................................................... Objeto window .......................................................................................... Objeto frames ............................................................................................ Objeto history ........................................................................................... Objeto navigator........................................................................................ Objeto location .......................................................................................... Objeto script .............................................................................................. Objeto document ....................................................................................... Objeto link ................................................................................................ Objeto anchor ............................................................................................ Objeto form ............................................................................................... Objeto element .......................................................................................... Microsoft FrontPage Express .........................................................................
XV
584 584 586 587 589 589 590 590 591 593 594 595 596 597 599 600 602 603 604 605 607 611 611 612 612 612 613 613 614 616 617 617 618 618 618 620 620 620 620 621
XVI VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
CAPÍTULO 11. VISUAL INTERDEV ................................................................. 623 ENTORNO DE PROGRAMACIÓN DE VISUAL INTERDEV ....................... Esquema html ................................................................................................. Cuadro de herramientas.................................................................................. Esquema de secuencias de comandos ............................................................ Vista Diseño ................................................................................................... Vista Código .................................................................................................. Vista rápida .................................................................................................... DISEÑO DE UN SITIO WEB ............................................................................ Trabajar sin conexión ..................................................................................... Crear un proyecto Web .................................................................................. Crear y organizar páginas .............................................................................. Incluir gráficos ............................................................................................... Establecer vínculos ........................................................................................ Tema y diseño ................................................................................................ Barra de exploración ...................................................................................... FrontPage y Visual InterDev.......................................................................... Distribución de la aplicación Web ................................................................. INTEGRAR MULTIMEDIA .............................................................................. PÁGINAS ASP ................................................................................................... Modelo ASP ................................................................................................... Secuencias de órdenes .................................................................................... Lenguajes de secuencias de órdenes .............................................................. Objetos ASP predefinidos .............................................................................. Objeto Response ....................................................................................... Objeto Request .......................................................................................... Objeto Server ............................................................................................ Objeto Session .......................................................................................... Objeto Application .................................................................................... Objeto ObjectContext ............................................................................... OBTENER INFORMACIÓN MEDIANTE FORMULARIOS .......................... Diseño de un formulario HTML .................................................................... Procesar la información del formulario en el cliente ..................................... Procesar formularios en el servidor................................................................ ACCESO A UNA BASE DE DATOS ................................................................ Diseño del acceso a bases de datos ................................................................
623 624 624 625 625 625 626 626 628 628 630 632 632 633 634 635 635 635 639 640 640 642 643 643 643 643 644 644 644 644 646 647 648 649 650
CAPÍTULO 12. APLICACIONES DE INTERNET ........................................... 655 CREAR UN EXPLORADOR WEB ................................................................... 655
ÍNDICE
WININET ............................................................................................................ Acceso a un servidor HTTP ........................................................................... Acceso a un servidor FTP .............................................................................. SOCKETS ........................................................................................................... WinSock ......................................................................................................... Comunicación orientada a conexión .............................................................. Servidor ..................................................................................................... Cliente ....................................................................................................... Enviar mensajes .............................................................................................
XVII
658 659 662 672 674 674 676 685 694
CAPÍTULO 13. DIRECTX.................................................................................... 701 COMPONENTES DE DIRECTX ....................................................................... DIRECTX Y COM.............................................................................................. CÓMO TRABAJA DIRECTX............................................................................ CREAR UNA APLICACIÓN DIRECTDRAW ................................................. Crear el objeto DirectDraw ............................................................................ Fijar el nivel de cooperación .......................................................................... Fijar la resolución de la tarjeta gráfica ........................................................... Crear las superficies de visualización ............................................................ Crear los objetos de recorte ............................................................................ Crear la paleta de colores ............................................................................... Asociar la paleta con las superficies de visualización.................................... Crear los sprites .............................................................................................. Acceso directo a la memoria de la superficie............................................ Emplear la GDI y hacer una copia desde un DIB ..................................... Bucle de animación ........................................................................................ Salir de la aplicación ...................................................................................... DIRECTDRAW Y LA BIBLIOTECA MFC ...................................................... APLICACIÓN DIRECTDRAW ......................................................................... Arquitectura de la aplicación ......................................................................... Crear superficie .............................................................................................. Cargar mapa de bits........................................................................................ Crear paleta de colores ................................................................................... Obtener la profundidad de color .................................................................... Objeto de recorte ............................................................................................ Creación del entorno DirectDraw .................................................................. Fijar el modo de vídeo ................................................................................... Mostrar la nueva escena generada.................................................................. Dibujar en una ventana .................................................................................. La ventana principal recibe el foco ................................................................ La paleta de colores cambió ...........................................................................
701 702 704 706 708 709 710 710 714 717 718 719 721 722 723 725 726 727 728 733 735 737 739 740 741 743 749 752 753 754
XVIII VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
La resolución de vídeo cambió ...................................................................... La posición de la ventana se modificó ........................................................... El tamaño de la ventana cambió..................................................................... Menús a pantalla completa ............................................................................. Destruir los objetos DirectDraw..................................................................... Cambiar a pantalla completa .......................................................................... Sincronización con el espacio vertical ........................................................... Cerrar la aplicación pulsando la tecla Esc ...................................................... Posición del ratón ........................................................................................... Animación de imágenes ................................................................................. Interfaz de programación ............................................................................... Variables y recursos .................................................................................. Funciones de la interfaz de programación ................................................ Función OnUsrCreaEscena ....................................................................... Función OnUsrCambiaResolucion ........................................................... Función OnUsrRestauraSuperficies .......................................................... Función OnUsrTick .................................................................................. Función OnUsrDestruyeEscena ................................................................ Función TamSuperficie ............................................................................. Función UsrDibujaMosaico ...................................................................... Función UsrDibuja .................................................................................... DIRECT3D ......................................................................................................... Interfaces Direct3DRM utilizadas.................................................................. Pasos para crear una aplicación Direct3DRM................................................ Objetos DirectDraw y Direct3DRM, superficies y modo de vídeo ............... Enumerar los drivers de dispositivo ............................................................... Crear el dispositivo y la superficie de proyección ......................................... Crear los objetos de la escena ........................................................................ Notificar mensajes a Direct3DRM ................................................................. Crear el bucle de animación ........................................................................... API de Direct3DRM ...................................................................................... Luces ......................................................................................................... Secuencias de animación .......................................................................... Marcos ...................................................................................................... Objetos visibles ......................................................................................... Crear objetos 3D ....................................................................................... Cargar un objeto 3D .................................................................................. Perspectivas............................................................................................... Transformaciones ...................................................................................... Clase Direct3DRMObject ......................................................................... Crear la escena ............................................................................................... DIRECTSOUND................................................................................................. Interfaces a utilizar .........................................................................................
754 755 756 758 761 761 763 763 764 764 766 766 767 768 769 770 771 773 773 774 775 775 776 778 779 783 788 792 796 798 801 801 802 802 803 803 805 806 806 809 809 816 817
ÍNDICE
XIX
Aplicación DirectSound ................................................................................. API de DirectSound ....................................................................................... Crear el objeto DirectSound...................................................................... Nivel de cooperación de la aplicación ...................................................... Crear un buffer de sonido ......................................................................... Efecto Doppler .......................................................................................... Reproducir un buffer de sonido ................................................................ Detener un sonido ..................................................................................... Volumen.................................................................................................... Balance...................................................................................................... Conos de sonido ........................................................................................ Crear los objetos de sonido para aplicación ................................................... Interfaz de programación ............................................................................... Cargar sonidos .......................................................................................... Reproducir un buffer de sonido ................................................................ Detener un sonido ..................................................................................... Sonidos utilizados ..........................................................................................
817 819 819 820 820 821 822 822 822 823 823 826 827 828 829 829 830
APÉNDICE A. CÓDIGOS DE CARACTERES.................................................. 835 UTILIZACIÓN DE CARACTERES ANSI CON WINDOWS .......................... JUEGO DE CARACTERES ANSI ..................................................................... UTILIZACIÓN DE CARACTERES ASCII ....................................................... JUEGO DE CARACTERES ASCII.................................................................... CÓDIGOS EXTENDIDOS ................................................................................. CÓDIGOS DEL TECLADO ...............................................................................
835 836 837 838 839 840
APÉNDICE B. ÍNDICE ALFABÉTICO .............................................................. 841
PRÓLOGO Para facilitar el desarrollo de aplicaciones para Windows escritas en C, Microsoft introduce en 1992, C/C++ 7.0 y la biblioteca de clases MFC 1.0. Las investigaciones demostraron que debido al nivel de dificultad de aprender y utilizar no sólo C++, sino el conjunto de clases de las MFC, muchos desarrolladores se quedaban en el intento de migrar a C++. Por este motivo fue creado en febrero de 1993 Visual C++ 1.0, para facilitar a los desarrolladores la migración a C++. Con Visual C++ se introdujo una tecnología de desarrollo innovadora a base de asistentes con una nueva versión de las MFC más potente; la 2.0. La biblioteca MFC 2.0, compatible con la versión anterior, permitió implementar una arquitectura de aplicación que facilitaba enormemente el desarrollo de aplicaciones. Los asistentes que permitían generar esta nueva arquitectura basada en las clases MFC, evitaban escribir muchas de las líneas de código necesarias para construir una aplicación. Esto es, los asistentes generaban aplicaciones para Windows, sin necesidad de escribir líneas y líneas de código. Por ello, Visual C++ se convirtió en el camino más corto para el desarrollo de aplicaciones C++ para Windows, combinando, además, un alto rendimiento con una comodidad en el uso. Posteriormente, en abril de 1993, Microsoft presentó OLE 2.0. Con OLE 2.0 los desarrolladores podían implementar objetos que interactuaban entre sí sin importar cómo actuaba cada objeto específicamente. Más aún, podían utilizarse aplicaciones enteras como componentes, lo que hacía más fácil la integración de aplicaciones y como consecuencia la combinación de información. No obstante, hasta que Visual C++ 1.5 y la biblioteca MFC 2.5 no estuvieron disponibles, no fue cómodo desarrollar aplicaciones OLE 2.0. A partir de este momento, los desarrolladores tuvieron asistentes para crear objetos OLE cliente, servidor o contenedor con pocos esfuerzos. Asimismo, esta biblioteca también incluía soporte
XXII VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
completo de ODBC para facilitar la programación y el acceso a bases de datos locales o remotas. El fuerte interés de los desarrolladores por crear aplicaciones de 32 bits (basadas en Win32 y OLE) así como tener flexibilidad para dirigirse a múltiples plataformas (Windows y Windows NT para Intel y RISC, Windows 9x y Macintosh) condujo a Microsoft a crear Visual C++ 2.0 y la biblioteca MFC 3.0 totalmente compatible con las versiones anteriores. De esta forma los desarrolladores podían continuar manteniendo sus aplicaciones de 16 bits con Visual C++ 1.5x y los desarrolladores de 32 bits las suyas con Visual C++ 2.x. El compilador C++ de Visual C++ 2.0 ya incorporaba las últimas características de C++; esto es, plantillas de C++ y los manipuladores de excepciones que sus antecesores incorporaban a base de macros. Visual C++ 2.0 aportaba entre otras características: la creación de aplicaciones de 32 bits, hilos (threads) y un espacio de memoria plana (eliminando los segmentos de 64K). Asimismo, la biblioteca MFC 3.0 añadía soporte OLE de 32 bits y ODBC. En realidad Visual C++ 2.0 fue un trampolín para sus predecesores (Visual C++ n.x, Visual C++ 6.0). Visual C++ 6.0 presenta la tecnología de compilación más avanzada para producir aplicaciones de 32 bits más rápidas y de menor tamaño. También incorpora las características y palabras clave más actuales del estándar ANSI. Las nuevas MFC&T combinan la fiabilidad y productividad de la biblioteca MFC con la biblioteca ATL (Active Template Library). Hace más fácil el desarrollo de software basado en componentes (soporte COM). Incorpora un gran número de nuevos elementos diseñados para explotar las tecnologías de Internet. Visual C++ 6.0 proporciona varias formas de trabajo con bases de datos. Por ejemplo, utilizando la biblioteca de clases MFC, podemos recurrir a las clases DAO (Data Access Objects - objetos de acceso a datos) o a las clases ODBC (Open DataBase Connectivity - conectividad abierta de bases de datos). Pero las tecnologías actuales de acceso a datos tienen que satisfacer los nuevos escenarios demandados por las empresas, tales como los sistemas de información basados en la Web. En esta línea, Microsoft ofrece utilizar OLE DB como un proveedor de datos y objetos ADO (ActiveX Data Objects - objetos ActiveX para acceso a datos), como tecnología de acceso a datos, argumentando que el acceso a datos basado en OLE DB y ADO es adecuado para una gama amplia de aplicaciones, desde pequeños procesos en estaciones de trabajo a aplicaciones Web a gran escala. OLE DB es un conjunto de interfaces COM que puede proporcionar acceso uniforme a los datos guardados en diversas fuentes de información. El modelo de objeto ADO define una colección de objetos programables, que soportan el modelo de objeto componente (COM) y la automatización OLE, que pueden interaccionar con la tecnología OLE DB. El modelo de objeto de ADO, comparado con
PRÓLOGO
XXIII
otros objetos de acceso a datos como RDO o DAO, tiene menos objetos y es más simple de utilizar. Este libro, escrito con la versión 6 de Visual C++, es continuación del publicado anteriormente, Visual C++ Aplicaciones para Win32. Por lo tanto, para abordar su contenido el autor ha supuesto que el lector conoce todo lo expuesto en el título anteriormente citado. Este libro trata temas más avanzados, como el sistema de ayuda HTML, hilos, comunicaciones RS-232, controles ActiveX, tecnología COM, biblioteca ATL, dispositivos MCI, bibliotecas dinámicas, acceso a bases de datos, Internet, Visual Interdev, aplicaciones de Internet, DirectDraw, Direct3DRM y DirectSound. Todos los temas se han documentando con abundantes ejemplos resueltos, lo que le facilitará el aprendizaje. Este libro es el cuarto de una colección de cuatro libros orientados al desarrollo de aplicaciones con C/C++. Entre los cuatro, y en el orden especificado, cubren los siguientes aspectos: programación con C, programación orientada a objetos con C++, desarrollo de aplicaciones para Windows basadas en objetos, y programación avanzada en Windows incluyendo Internet. El primero, Curso de programación C/C++, abarca todo lo relativo a la programación estructurada con C. También incluye diversos algoritmos de uso común así como estructuras dinámicas de datos. El segundo, Programación orientada a objetos con C++, estudia como su nombre indica el desarrollo de aplicaciones orientadas a objetos. Esta tecnología es imprescindible conocerla si queremos desarrollar aplicaciones utilizando bibliotecas de clases como las MFC&ATL de Microsoft Visual C++. El tercero, Visual C++ - Aplicaciones para Win32, le enseña fundamentalmente cómo desarrollar aplicaciones para Windows (aplicaciones con una interfaz gráfica basada en ventanas). Y éste, el cuarto, Visual C++ - Programación avanzada, complementa al libro anterior abordando temas más complejos, a los que me he referido anteriormente.
Agradecimientos He recibido ideas y sugerencias de algunas personas durante la preparación de este libro, entre las que se encuentran, cómo no, mis alumnos, que con su interés por aprender me hacen reflexionar sobre objetivos que a primera vista parecen inalcanzables, pero que una vez logrados sirven para que todos aprendamos; a todos ellos les estoy francamente agradecido.
XXIV VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
En especial, quiero expresar mi agradecimiento a Alfons González por sus ideas que siempre son bienvenidas, a Oscar García Población y Rafael Torcida por sus buenas recomendaciones y aportaciones, y a David Jurado González por su participación en la corrección de esta obra, por sus aportaciones a la misma y en especial, porque sin su empeño y colaboración este libro posiblemente no habría incluido DirectX. También, quiero agradecer a Microsoft Ibérica la cesión de los programas incluidos en el CD-ROM. Francisco Javier Ceballos Sierra http://www.fjceballos.es/
Faltan páginas...
CAPÍTULO 3
© F.J.Ceballos/RA-MA
COMUNICACIONES Este capítulo presenta técnicas de comunicación con otras máquinas utilizando el puerto serie. Antes de empezar el desarrollo de una aplicación que implemente comunicaciones serie, resulta útil hacer una breve descripción del funcionamiento básico de la propia interconexión RS-232. Las señales disponibles en un conector RS-232 están pensadas únicamente para asegurar la correcta transmisión y recepción de datos desde un equipo denominado DTE (Data Terminal Equipment - Equipo terminal de datos) a un DCE (Data Communication Equipment - Equipo de comunicación de datos). Un DTE es generalmente un ordenador y un DCE un módem. El enlace estándar entre un DTE y un DCE se puede ver en la figura siguiente.
116 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
La función de cada una de las señales es como sigue: Señal Nombre
Dirección
TxD RxD RTS CTS DSR
hacia DCE hacia DTE hacia DCE hacia DTE hacia DTE
Transmitted Data Received Data Request To Send Clear To Send Data Set Ready Signal Common DCD Data Carrier Detect DTR Data Terminal Ready RI Ring Indicator
Función
Salida de datos DTE Entrada de datos DTE DTE desea cambiar a modo transmisión DCE listo para transmitir DCE listo para comunicar con DTE Línea común del circuito (masa) hacia DTE Detectar si está conectado hacia DCE Pone a trabajar al módem hacia DTE Anuncia una llamada
TxD se encarga de transportar los datos serie hasta el módem. Para ello, han tenido que activarse RTS, CTS, DSR y DTR. RxD, recepción de datos, no depende de ninguna otra función RS-232. RTS tiene como misión conmutar un módem semi-duplex entre modos de recepción y transmisión. Cuando el DTE quiere transmitir, informa al módem de su deseo activando esta patilla. Cuando el módem conmuta para transmisión, lo informa al DTE activando la patilla CTS, indicando que ya puede enviar los datos. El módem origen no transmite ni activa su DSR hasta recibir el tono de respuesta del módem remoto. DCD, detección de señal de línea recibida, se activa cuando el módem recibe una portadora remota. En módems semi-duplex, DCD se activa únicamente en el módem receptor. Una vez que el módem esté conectado a la línea, DTR deberá permanecer activa mientras dure la conexión; si se inhibe, se produce la desconexión, interrumpiendo bruscamente el enlace. Además del enlace estándar, existen otros, como la conexión denominada módem nulo (cable de seis hilos), utilizada generalmente para transferir ficheros entre dos ordenadores. Esta conexión, como su nombre indica, no es en absoluto un módem, sino una conexión directa entre dos ordenadores (DTE) para comunicarse siguiendo las reglas lógicas del protocolo RS-232. Otra solución para la comunicación DTE-DTE más sencilla todavía, es la conexión de dos hilos (TxD y RxD).
CAPÍTULO 3: COMUNICACIONES
117
De lo expuesto puede deducirse que para que exista una comunicación entre dos equipos tiene que haber un acoplamiento entre ellos, y dicho acoplamiento puede realizarse por software o por hardware. El acoplamiento hardware sólo es posible si ambos equipos están físicamente conectados mediante un cable. Se suele realizar mediante las señales DTR/DSR o bien utilizando simplemente las señales secundarias RTS/CTS. El acoplamiento software no siempre es posible, ya que para que pueda darse, los equipos deben reconocer caracteres de control. En un acoplamiento software es el receptor el que controla el acoplamiento. Lo hace de la forma siguiente: •
Cuando su cola de entrada está llena, envía un carácter de desconexión (normalmente ASCII_XOFF - 0x13).
•
Cuando el transmisor recibe este carácter se detiene.
•
Cuando la cola de entrada del receptor puede recibir más caracteres, envía un carácter de conexión (normalmente ASCII_XON - 0x11).
•
Cuando el transmisor recibe este carácter reinicia el envío de caracteres.
COMUNICACIONES POR EL PUERTO SERIE La gestión de los puertos de comunicación no es una tarea fácil. Lo primero que hay que pensar es que los datos llegan a los puertos de forma asíncrona; es decir, su llegada es imprevisible. Esto sugiere que el dato que llega tiene que procesarse inmediatamente, puesto que pueden llegar otros datos. De esta tarea se encarga el hardware del PC, de forma que cuando detecta la llegada de un dato, interrumpe el flujo normal del proceso para ceder el control a la rutina de proceso de comunicaciones. Esta rutina tiene que ser una rutina de Windows en lugar de una rutina de la aplicación. Esto es así por dos razones: ### Windows debe mantener el control de la multitarea. En efecto, si la llegada de un dato hiciera que se transfiriera el control del procesador a su aplicación, Windows perdería su habilidad para gestionar la multitarea. Esto quiere decir que Windows tiene que estar entre la aplicación y el hardware. ### Windows no puede dirigir el dato recibido directamente a la aplicación. La razón es que los datos que se reciben en el puerto de comunicaciones no llegan con la identidad de la aplicación que los tiene que recibir. Por lo tanto, Windows tiene que guardar en un buffer los datos que llegan para una aplicación. Para qué aplicación, debe determinarse por adelantado; esto es, su apli-
118 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
cación debe haberle pedido a Windows la propiedad del puerto utilizando la función CreateFile. Cuando una aplicación solicita a Windows la propiedad de un puerto, Windows sólo se lo dará si ninguna otra aplicación lo tiene. Por el mismo motivo, mientras su aplicación tiene el control de un puerto, Windows se lo prohíbe a las demás aplicaciones que lo soliciten. Cuando su aplicación finalice la operación de E/S con un puerto, debe dejar el control del mismo para que otras aplicaciones puedan utilizarlo, lo cual requiere llamar a la función CloseHandle. Como ejemplo, vamos a realizar una aplicación que permita transferir datos entre dos ordenadores personales. Para probar la aplicación, debe conectar vía puerto de comunicaciones los dos ordenadores. Una vez realizada la conexión, asegúrese de que está bien hecha utilizando un paquete de comunicaciones comercial, como el programa HiperTerminal de Windows. Una forma de realizar esta aplicación sería utilizando las funciones de la API de Win32 que se indican en la tabla siguiente: Función
Descripción
CreateFile SetCommMask SetupComm PurgeComm GetCommState SetCommState ReadFile WriteFile CloseHandle
Abrir un puerto de comunicaciones Eventos que serán atendidos Tamaño de las colas de E/S Terminar operaciones pendientes y limpiar colas Obtener las características del puerto (estructura DCB) Establecer las características del puerto Leer datos Enviar datos Cerrar un puerto de comunicaciones
Otras funciones disponibles en la API de 32 bits son: SetCommTimeouts, EscapeCommFunction, WaitCommEvent, GetLastError, ClearCommError, BuildCommDCB, etc. No obstante, la forma más sencilla de trabajar con el puerto serie en aplicaciones de 32 bits es utilizando el control de comunicaciones mscomm32.ocx (Microsoft Communications Control), cuestión que veremos más adelante en este mismo capítulo. Para establecer una comunicación utilizando la API de 32 bits, siga estos pasos: 1. Abra el puerto de comunicación. Para realizar esta operación, llame a la función CreateFile con los argumentos: puerto de comunicaciones (COM1, COM2, etc.), modo de acceso (leer y/o escribir), modo de compartición, atributos de seguridad, acción a tomar tanto si existe el fichero como si no existe,
CAPÍTULO 3: COMUNICACIONES
119
y atributos del fichero. Esta función devuelve un handle que identifica el puerto de comunicaciones abierto, o el valor ERROR_FILE_NOT_FOUND si el puerto no está disponible. 2. Establezca la máscara de comunicaciones para especificar los eventos que serán atendidos. Para realizar esta operación, llame a la función SetCommMask. 3. Defina el tamaño de los buffers de las colas de entrada y salida. Utilice para ello la función SetupComm. No obstante, esta operación no siempre es necesaria, puesto que existen dos buffers definidos por omisión. A continuación, limpie estos buffers invocando a la función PurgeComm. 4. Construya una estructura de tipo DCB que especifique la configuración del puerto (DCB - device control block). Para ello, llame a la función GetCommState para obtener la configuración inicial del puerto y, a partir de estos valores iniciales, modifique los miembros de interés de la estructura DCB. Otra posibilidad es utilizar la función BuildCommDCB con los argumentos: definición del puerto y estructura DCB. La definición del puerto es una cadena de caracteres con un formato igual al utilizado por los argumentos de la orden mode (“com2:9600,n,8,1”). Por lo tanto esta función sólo modifica los miembros velocidad de transmisión, paridad, bits por carácter y bits de parada de la estructura DCB especificada. 5. Establezca la configuración del puerto. Para realizar esta operación, llame a la función SetCommState pasando como argumento la estructura DCB, en la que previamente se almacenó dicha configuración. 6. Cuando quiera enviar datos al puerto de comunicaciones, utilice la función WriteFile, que tiene los siguientes parámetros: el handle que identifica el dispositivo de comunicaciones (este valor es devuelto por la función CreateFile), una cadena de caracteres que contiene los caracteres enviados, el número de caracteres enviados, un puntero a una variable que almacena el número de caracteres escritos y un puntero a una estructura OVERLAPPED. La función WriteFile devuelve un valor de tipo BOOL; un valor FALSE significa que ha ocurrido un error. 7. Establezca un proceso que permita estar a la espera de los datos que nuestra aplicación espera recibir. Cuando se reciban datos, utilice la función ReadFile que tiene los siguientes parámetros: el handle que identifica el dispositivo de comunicaciones, una cadena de caracteres que almacenará los caracteres recibidos, el número de caracteres a leer, un puntero a una variable que almacena el número de caracteres leídos y un puntero a una estructura
120 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
OVERLAPPED. La función ReadFile devuelve un valor de tipo BOOL; un valor FALSE significa que ha ocurrido un error. 8. Cuando ocurre un error durante una operación de comunicaciones, Windows bloquea el puerto correspondiente, el cual permanecerá bloqueado hasta que se llame a la función ClearCommError. Los parámetros de esta función son: un handle al dispositivo de comunicaciones, un puntero a una variable que recibe el código de error y un puntero a una variable que recibe el estado del dispositivo de comunicaciones. 9. Utilice la función CloseHandle para cerrar el puerto de comunicaciones cuando éstas finalicen. Si ocurre un error, esta función devuelve un cero; invoque a GetLastError si quiere saber de qué error se trata.
Aplicación Win32 para comunicaciones vía RS232 Como ejemplo, cree una nueva aplicación SDI denominada Comm que utilice una caja de diálogo como ventana principal. Para ello, ejecute AppWizard y haga que la clase CCommView sea una clase derivada de CFormView. A continuación, abra el editor de recursos y sitúe sobre la plantilla de diálogo creada por omisión los controles con las propiedades que se especifican a continuación: Objeto Etiqueta Caja de texto
Etiqueta Caja de texto
Botón de pulsación
Propiedad ID Caption ID Multiline Vertical scroll Want return ID Caption ID Multiline Vertical scroll Want return ID Caption
Valor IDC_STATIC Texto a transmitir: IDC_TX Sí Sí Sí IDC_STATIC Texto recibido: IDC_RX Sí Sí Sí IDC_ENVIAR &Enviar
El resultado que obtendrá será similar al mostrado en la figura siguiente:
CAPÍTULO 3: COMUNICACIONES
121
Ejecute ClassWizard y vincule las cajas de texto con las variables miembro m_tx y m_rx de la clase CCommView, y el botón con la variable m_botonEnviar miembro de la misma clase. class CCommView : public CFormView { // ... public: //{{AFX_DATA(CCommView) enum { IDD = IDD_COMM_FORM }; CButton m_botonEnviar; CString m_rx; CString m_tx; //}}AFX_DATA // ... };
A continuación, modifique la barra de menús con los menús y las órdenes que se especifican en la tabla siguiente: Objeto Menú Conexión Orden Establecer Orden Cortar Separador Orden Salir
Propiedad Caption Popup ID Caption ID Caption Separator ID Caption
Valor Cone&xión Sí ID_CONEXION_ESTABLECER &Establecer ID_CONEXION_CORTAR &Cortar Sí ID_APP_EXIT &Salir
122 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Menú Configuración Orden Parámetros COM Menú Ayuda Orden Acerca de
Caption Popup ID Caption Caption Popup ID Caption
&Configuración Sí ID_CONFIG_PARAM &Parámetros COM &Ayuda Sí ID_APP_ABOUT &Acerca de Comm...
La orden Parámetros COM visualizará una caja de diálogo, que permitirá al usuario establecer las características bajo las que se realizará la comunicación. Según esto, vamos a diseñar una caja de diálogo (IDD_PARAMETROSCOM) con los controles que se indican en la tabla siguiente: Objeto Etiqueta Lista desplegable Etiqueta Lista desplegable Etiqueta Lista desplegable Etiqueta Lista desplegable Etiqueta Lista desplegable Etiqueta Lista desplegable
Botón de pulsación
Botón de pulsación Botón de pulsación
Propiedad Caption ID Items Caption ID Items Caption ID Items Caption ID Items Caption ID Items Caption ID Items ID Caption Default button ID Caption ID Caption
Valor Puerto: IDC_PUERTO COM1, COM2, COM3, COM4 Baudios: IDC_BAUDIOS 300, 600, 1200, 2400, ..., 256000 Paridad: IDC_PARIDAD Ninguna, Par, Impar, Marca, Espacio Bits por carácter: IDC_BITSCAR 4, 5, 6, 7, 8 Bits de parada: IDC_BITSPARADA 1, 1.5, 2 Control de flujo: IDC_CONTROLFLUJO Ninguno, Xon/Xoff, Hardware (DTR/DSR), Hardware (RTS/CTS) IDOK &Aceptar Sí IDCANCEL &Cancelar IDC_DEFAULT &Restaurar
CAPÍTULO 3: COMUNICACIONES
123
La caja de diálogo diseñada será similar a la siguiente:
A continuación, desde el editor de recursos, seleccione la caja de diálogo e invoque a ClassWizard. Esto le permitirá añadir a la aplicación una clase CParamCom derivada de CDialog, basada en la plantilla IDD_PARAMETROSCOM que acaba de diseñar. Guarde la declaración y la definición de esta clase en los ficheros paramcom.h y paramcom.cpp. Después, añada a la clase CParamCom las variables miembro m_nPuerto, m_nBaudios, m_nParidad, m_nBitsCar, m_nBitsParada y m_nControlFlujo vinculadas a cada una de las listas desplegables correspondientes. class CParamCom : public CDialog { // ... // Dialog Data //{{AFX_DATA(CParamCom) enum { IDD = IDD_PARAMETROSCOM }; int m_nBitsCar; int m_nBaudios; int m_nBitsParada; int m_nControlFlujo; int m_nPuerto; int m_nParidad; //}}AFX_DATA // ... };
Registro de Windows Vamos a almacenar en el registro de Windows las características por omisión bajo las que se realizará la comunicación. Lo que pretendemos es almacenar la última configuración utilizada, como configuración por omisión, para la siguiente vez
124 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
que se utilice la aplicación. La función SetRegistryKey de la clase CWinApp permite almacenar las características de la aplicación en el registro de Windows con la clave HKEY_CURRENT_USER\Software\... SetRegistryKey es llamada por la función InitInstance de la aplicación. Tiene un parámetro que permite especificar el nombre de la clave. Según lo expuesto, abra el fichero comm.cpp y modifique en la función InitInstance la llamada a SetRegistryKey como se indica a continuación: SetRegistryKey(_T("App Comm"));
Cuando posteriormente invoquemos a las funciones miembro GetProfileInt, WriteProfileInt, GetProfileString, y WriteProfileString de la clase CWinApp, éstas operaran sobre el registro de Windows en lugar de hacerlo sobre un fichero con extensión ini.
Interfaz de comunicaciones Para facilitar la implementación de las comunicaciones vía RS232, vamos a implementar una interfaz con las operaciones más comunes. Según hemos desarrollado nuestra aplicación, integraremos esta interfaz que resumimos en la tabla siguiente, en la clase de la vista: Función
Descripción
Iniciar Terminar
Lee del registro de Windows la configuración inicial. Guarda en el registro de Windows la configuración actual. Abre el puerto de comunicaciones. Establece los parámetros con los que se realizarán las comunicaciones. Lee un byte de la cola de entrada del puerto de comunicaciones. Escribe un byte en la cola de salida del puerto de comunicaciones. Cierra el puerto de comunicaciones. Convierte un código de error en el mensaje correspondiente. Hilo para notificar a la aplicación el evento que ha ocurrido sobre el puerto de comunicaciones.
EstablecerConexion ConfigurarDisCom LeerCaracteresPuerto EscribirCarsPuerto CortarConexion MensajeDeError ControlarEventos
CAPÍTULO 3: COMUNICACIONES
125
Añada la declaración de estas funciones y de las variables necesarias para su implementación a la declaración de la clase CCommView. #define WM_EVENTO_COM WM_USER + 100 // mensaje de notificación UINT ControlarEventos(LPVOID p); // hilo class CCommView : public CFormView { // ... // Operations public: ///////////////////////////////////////////////// // Interfaz para comunicaciones static int m_indPuerto; static int m_indBaudios; static int m_indParidad; static int m_indBitsCar; static int m_indBitsParada; static int m_indControlFlujo; HANDLE m_hDisCom; // handle OVERLAPPED m_sOverRead; OVERLAPPED m_sOverWrite; UINT m_wTablaBaudios[13]; BYTE m_TablaParidad[5]; BYTE m_TablaBitsParada[3]; BOOL m_ConexionEstablecida; BOOL m_bHiloActivo;
al // // // // // // //
dispositivo de comunicaciones utilizada en una entrada asíncrona utilizada en una salida asíncrona tabla de velocidades tabla de paridades tabla bits de parada TRUE si el puerto fue abierto TRUE si el hilo está activo
static void Iniciar(); static void Terminar(); BOOL EstablecerConexion(); BOOL ConfigurarDisCom(); BOOL CortarConexion(); int LeerCaracteresPuerto(BYTE *pBytesLeidos, int BloqueMax); BOOL EscribirCarsPuerto(BYTE *pBytesAEscribir, DWORD dwBytes); void MensajeDeError(DWORD nError); ///////////////////////////////////////////////// // ... };
Las variables m_ind... contienen el índice del elemento seleccionado de las listas de la caja de diálogo Configuración. A continuación escribiremos cada una de las funciones descritas. Antes, inicie las variables estáticas y defina las cadenas de caracteres necesarias para el registro de Windows en la implementación de la clase CCommView.
126 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 int int int int int int
CCommView::m_indPuerto CCommView::m_indBaudios CCommView::m_indParidad CCommView::m_indBitsCar CCommView::m_indBitsParada CCommView::m_indControlFlujo
static static static static static static static
char char char char char char char
szComu[] szPuerto[] szBaudios[] szParidad[] szBitsCar[] szBitsParada[] szControlFlujo[]
= = = = = = =
= = = = = =
1; 6; 0; 4; 0; 1;
// // // // // //
COM2 9600 ninguna 8 1 Xon/Xoff
"Comunicaciones"; "Puerto"; "Baudios"; "Paridad"; "BitsCar"; "BitsParada"; "ControlFlujo";
Finalmente, inicie la variable m_hDisCom a NULL y las variables m_ConexionEstablecida y m_bHiloActivo a FALSE, en el constructor de su clase.
Función Iniciar La función Iniciar obtiene del registro de Windows la configuración por omisión del dispositivo de comunicaciones. void CCommView::Iniciar() { CWinApp *pApp= AfxGetApp(); // Recuperar configuración del registro de Windows m_indPuerto = pApp->GetProfileInt(szComu, szPuerto, 1); m_indBaudios = pApp->GetProfileInt(szComu, szBaudios, 6); m_indParidad = pApp->GetProfileInt(szComu, szParidad, 0); m_indBitsCar = pApp->GetProfileInt(szComu, szBitsCar, 4); m_indBitsParada = pApp->GetProfileInt(szComu, szBitsParada, 0); m_indControlFlujo = pApp->GetProfileInt(szComu, szControlFlujo, 1); }
La función GetProfileInt de la clase CWinApp recupera del registro de Windows el entero asociado con la cadena sz... (segundo argumento) correspondiente a la sección szComu de la clave especificada por la función SetRegistryKey. Si la entrada especificada por sz... no se encuentra, la función devuelve el valor especificado por el argumento tercero.
Función Terminar La función Terminar será invocada cuando se corta la comunicación para guardar en el registro de Windows la configuración actual del dispositivo de comunicaciones.
CAPÍTULO 3: COMUNICACIONES
127
void CCommView::Terminar() { CWinApp *pApp= AfxGetApp(); // Guardar configuración en el registro de Windows pApp->WriteProfileInt(szComu, szPuerto, m_indPuerto); pApp->WriteProfileInt(szComu, szBaudios, m_indBaudios); pApp->WriteProfileInt(szComu, szParidad, m_indParidad); pApp->WriteProfileInt(szComu, szBitsCar, m_indBitsCar); pApp->WriteProfileInt(szComu, szBitsParada, m_indBitsParada); pApp->WriteProfileInt(szComu, szControlFlujo, m_indControlFlujo); }
La función WriteProfileInt de la clase CWinApp guarda en el registro de Windows el entero especificado por el argumento tercero, asociado con la cadena sz... (segundo argumento), en la sección szComu de la clave especificada por la función SetRegistryKey.
Función EstablecerConexion La función EstablecerConexion permite abrir el puerto de comunicaciones especificado por la variable miembro m_indPuerto. Para ello: 1. Invoca a la función CreateFile para abrir el puerto de comunicaciones. Hay dos formas de abrir un puerto de comunicaciones: solapada (overlapped) y no solapada (nonoverlapped). La documentación del SDK de Win32 utiliza los términos asíncrono y síncrono. Un puerto abierto para operaciones solapadas permite múltiples hilos realizando operaciones de E/S (ReadFile o WriteFile) simultáneas, así como ejecutar otra tarea mientras las operaciones estén pendientes. Además, el comportamiento de las operaciones solapadas permite a un único hilo realizar peticiones diferentes y ejecutar tareas en segundo plano mientras las operaciones estén pendientes. Si el puerto se abre para operaciones no solapadas, el hilo queda bloqueado mientras la operación de E/S solicitada no esté completada; una vez completada, el hilo puede seguir trabajando. En este caso, si un hilo está bloqueado esperando a que su operación de E/S finalice, cualquier otro hilo que requiera una operación de E/S quedará bloqueado. Esta última forma de abrir un puerto es útil cuando el sistema operativo no soporta operaciones de E/S solapadas. El código siguiente indica la forma apropiada de abrir un puerto de comunicaciones de forma solapada: m_hDisCom = CreateFile(szPuerto, GENERIC_READ | GENERIC_WRITE, 0, // acceso exclusivo NULL, // sin atributos de seguridad OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
128 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Cuando se especifica el parámetro FILE_FLAG_OVERLAPPED, las funciones ReadFile y WriteFile deben especificar una estructura OVERLAPPED. 2. Invoca a la función SetCommMask para especificar los eventos que serán atendidos. Por ejemplo: SetCommMask( m_hDisCom, EV_RXCHAR | EV_TXEMPTY | EV_RX80FULL | EV_ERR);
El parámetro m_hDisCom es un handle al dispositivo de comunicaciones devuelto por la función CreateFile. El otro parámetro especifica los eventos que son habilitados. Un valor cero inhabilita todos los eventos. Por ejemplo, EV_RXCHAR se produce cuando se recibe un carácter en la cola de entrada; EV_TXEMPTY se produce cuando se envía el último carácter de la cola de salida; EV_RX80FULL se produce cuando la cola de entrada está llena al 80%; EV_ERR sucede cuando se produce alguno de los siguientes errores: CE_FRAME, CE_OVERRUN, o CE_RXPARITY. 3. Invoca a la función SetupComm para especificar el tamaño en bytes de las colas de recepción y de transmisión. SetupComm(m_hDisCom, COLARX, COLATX);
4. Invoca a la función PurgeComm para terminar las operaciones de lectura y escritura pendientes y limpiar las colas de recepción y de transmisión. PurgeComm( m_hDisCom, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR );
5. Construye una estructura DCB e invoca a la función SetCommState para configurar el dispositivo de comunicaciones con los valores almacenados en dicha estructura. 6. Invoca a la función SetCommTimeouts para establecer los tiempos límites para las operaciones de recepción y transmisión. 7. Si la comunicación va a ser controlada por eventos, lanza un hilo dedicado a controlar cada evento de interés que se produzca en el puerto de comunicaciones. 8. Invoca a la función EscapeCommFunction para activar la señal DTR mientras dure la conexión. BOOL CCommView::EstablecerConexion() {
CAPÍTULO 3: COMUNICACIONES
129
char szPuerto[10]; BOOL bExito = FALSE; // Formar la cadena "COM" más el número de dispositivo wsprintf(szPuerto, "COM%d", m_indPuerto + 1); // Cerrar el puerto si estuviera abierto if (m_hDisCom) CloseHandle(m_hDisCom); // Abrir el puerto de comunicaciones m_hDisCom = CreateFile(szPuerto, GENERIC_READ | GENERIC_WRITE, 0, // acceso exclusivo NULL, // sin atributos de seguridad OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if( m_hDisCom == INVALID_HANDLE_VALUE ) { // Visualizar el error ocurrido. MensajeDeError( GetLastError() ); MessageBeep(0xFFFFFFFF); return FALSE; } // Especificar los eventos que serán atendidos SetCommMask( m_hDisCom, EV_RXCHAR | EV_TXEMPTY | EV_RX80FULL | EV_ERR); // Establecer el tamaño de las colas de recepción y de transmisión SetupComm(m_hDisCom, COLARX, COLATX); // Terminar las operaciones de lectura y escritura pendientes // y limpiar las colas Rx y Tx PurgeComm( m_hDisCom, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR ); // Establecer los parámetros de la comunicación bExito = ConfigurarDisCom(); if ( bExito ) { m_ConexionEstablecida = TRUE; // Crear un hilo secundario para ver qué evento ocurre if ( AfxBeginThread(ControlarEventos, this) == NULL ) { AfxMessageBox( "Error: No se puede iniciar el hilo", MB_OK | MB_ICONEXCLAMATION ); m_ConexionEstablecida = FALSE; CloseHandle(m_hDisCom); return FALSE;
130 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 } m_bHiloActivo = TRUE; // Enviar la señal DTR (data-terminal-ready). EscapeCommFunction(m_hDisCom, SETDTR); } else { AfxMessageBox( "Error: No se puede configurar el dispositivo", MB_OK | MB_ICONEXCLAMATION ); m_ConexionEstablecida = FALSE; CloseHandle(m_hDisCom); } return bExito; }
Defina las constantes COLARX y COLATX, que definen los tamaños de las colas de recepción y transmisión respectivamente, en el fichero commview.cpp: #define COLARX 4096 #define COLATX 4096
Función MensajeDeError La función MensajeDeError convierte un código de error en el correspondiente mensaje obtenido del sistema, y visualiza un diálogo con dicho mensaje. La conversión la hace invocando a la función FormatMessage de la API, que obtiene el mensaje de la tabla de mensajes del sistema y lo almacena en un buffer en memoria que crea automáticamente invocando a LocalAlloc. Una vez visualizado el mensaje, la función MensajeDeError invoca a LocalFree para liberar el buffer asignado por LocalAlloc desde FormatMessage (para obtener más información sobre esta función, consulte la ayuda en línea). void CCommView::MensajeDeError( DWORD nError ) { LPVOID lpMsg; ::FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, nError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsg, 0, NULL ); // Mostrar el error AfxMessageBox( (LPCTSTR)lpMsg, MB_OK | MB_ICONEXCLAMATION );
CAPÍTULO 3: COMUNICACIONES
131
// Liberar el buffer ::LocalFree( lpMsg ); }
Función ConfigurarDisCom Para construir la estructura DCB y configurar el dispositivo de comunicaciones así como para establecer los tiempos límites para las operaciones de recepción y de transmisión, EstablecerConexion invoca a la función ConfigurarDisCom. BOOL CCommView::ConfigurarDisCom() { BYTE bEstablecer; DCB dcb; dcb.DCBlength = sizeof(DCB); GetCommState(m_hDisCom, &dcb); dcb.BaudRate dcb.Parity dcb.ByteSize dcb.StopBits
= = = =
m_wTablaBaudios[m_indBaudios]; m_TablaParidad[m_indParidad]; (BYTE)(m_indBitsCar + 4); m_TablaBitsParada[m_indBitsParada];
// Establecer el control de flujo software bEstablecer = (BYTE)(m_indControlFlujo == 1); // Xon/Xoff dcb.fInX = dcb.fOutX = bEstablecer; dcb.XonChar = 0x11; // ASCII_XON dcb.XoffChar = 0x13; // ASCII_XOFF dcb.XonLim = 100; dcb.XoffLim = 100; // Establecer el control de flujo hardware bEstablecer = (BYTE)(m_indControlFlujo == 2); // DTR/DSR dcb.fOutxDsrFlow = bEstablecer; if (bEstablecer) dcb.fDtrControl = DTR_CONTROL_HANDSHAKE; else dcb.fDtrControl = DTR_CONTROL_ENABLE; bEstablecer = (BYTE)(m_indControlFlujo == 3); // RTS/CTS dcb.fOutxCtsFlow = bEstablecer; if (bEstablecer) dcb.fRtsControl = RTS_CONTROL_HANDSHAKE; else dcb.fRtsControl = RTS_CONTROL_ENABLE; // Otras especificaciones dcb.fBinary = TRUE; dcb.fParity = TRUE;
132 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
if(SetCommState(m_hDisCom, &dcb) == 0) { // Visualizar el error ocurrido. MensajeDeError( GetLastError() ); MessageBeep(0xFFFFFFFF); return FALSE; } // Establecer los tiempos límites para las operaciones de E/S COMMTIMEOUTS CommTimeOuts; CommTimeOuts.ReadIntervalTimeout = MAXDWORD; CommTimeOuts.ReadTotalTimeoutMultiplier = 0; CommTimeOuts.ReadTotalTimeoutConstant = 1000; // CBR_9600 es aproximadamente 1 byte/ms. Para nuestros // propósitos permitiremos un tiempo de espera por carácter // doble al necesario CommTimeOuts.WriteTotalTimeoutMultiplier = 2*CBR_9600/dcb.BaudRate; CommTimeOuts.WriteTotalTimeoutConstant = 0; SetCommTimeouts( m_hDisCom, &CommTimeOuts); return TRUE; }
La iniciación de un puerto se hace llamando a la función de la API de Windows SetCommState. El primer argumento hace referencia al puerto de comunicaciones y el segundo a una estructura de tipo DCB que almacena la configuración del puerto. DCB está definido en windows.h y recoge todos los parámetros relacionados con la configuración de un puerto. Si observa la definición de la función ConfigurarDisCom, verá que es necesario asignar valores a los arrays m_wTablaBaudios, m_TablaParidad y m_TablaBitsParada miembros de la clase CCommView. Realice esta asignación en el constructor de su clase, como se indica a continuación: CCommView::CCommView() : CFormView(CCommView::IDD) { m_wTablaBaudios[0] = CBR_110; m_wTablaBaudios[1] = CBR_300; m_wTablaBaudios[2] = CBR_600; m_wTablaBaudios[3] = CBR_1200; m_wTablaBaudios[4] = CBR_2400; m_wTablaBaudios[5] = CBR_4800; m_wTablaBaudios[6] = CBR_9600; m_wTablaBaudios[7] = CBR_14400; m_wTablaBaudios[8] = CBR_19200;
CAPÍTULO 3: COMUNICACIONES
m_wTablaBaudios[9] m_wTablaBaudios[10] m_wTablaBaudios[11] m_wTablaBaudios[12]
= = = =
CBR_38400; CBR_56000; CBR_128000; CBR_256000;
m_TablaParidad[0] m_TablaParidad[1] m_TablaParidad[2] m_TablaParidad[3] m_TablaParidad[4]
= = = = =
NOPARITY; EVENPARITY; ODDPARITY; MARKPARITY; SPACEPARITY;
133
m_TablaBitsParada[0] = ONESTOPBIT; m_TablaBitsParada[1] = ONE5STOPBITS; m_TablaBitsParada[2] = TWOSTOPBITS; //{{AFX_DATA_INIT(CCommView) m_rx = _T(""); m_tx = _T(""); //}}AFX_DATA_INIT // TODO: add construction code here m_hDisCom = NULL; m_pHiloEv = NULL; }
Finalmente hemos establecido los tiempos límite invocando a la función SetCommTimeouts. El primer argumento hace referencia al puerto de comunicaciones y el segundo a una estructura COMMTIMEOUTS que almacena los tiempos límites. El significado de cada uno de estos miembros se expone a continuación. ReadIntervalTimeout especifica el tiempo máximo, en milisegundos, que puede transcurrir entre la llegada de dos caracteres en la línea de comunicaciones. Durante una operación ReadFile, el lapso de tiempo empieza cuando se recibe el primer carácter. Si el intervalo de tiempo entre la llegada de dos caracteres cualesquiera excede esta cantidad, la operación ReadFile se da por finalizada devolviendo cualquier carácter que haya en la cola de entrada. Un valor cero indica que no se usan tiempos límites. Un valor MAXDWORD, combinado con valores cero para los miembros ReadTotalTimeoutConstant y ReadTotalTimeoutMultiplier, especifica que la operación de lectura tiene que retornar inmediatamente con los caracteres que ya se han recibido, aun cuando no se haya recibido ningún carácter. ReadTotalTimeoutMultiplier especifica el multiplicador, en milisegundos, utilizado para calcular el tiempo límite total para las operaciones de lectura. Para cada operación de lectura, este valor es multiplicado por el número de bytes que se quieren leer.
134 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
ReadTotalTimeoutConstant especifica la constante, en milisegundos, utilizada para calcular el tiempo límite total para las operaciones de lectura. Para cada operación de lectura, este valor se agrega al producto de ReadTotalTimeoutMultiplier por el número de bytes solicitados. Un valor cero para ReadTotalTimeoutMultiplier y ReadTotalTimeoutConstant indican que el tiempo límite total no se utilizará para operaciones de lectura. WriteTotalTimeoutMultiplier especifica el multiplicador, en milisegundos, utilizado para calcular el tiempo límite total para las operaciones de escritura. Para cada operación de escritura, este valor es multiplicado por el número de bytes que se quieren escribir. WriteTotalTimeoutConstant especifica la constante, en milisegundos, utilizada para calcular el tiempo límite total para operaciones de escritura. Para cada operación de escritura, este valor se agrega al producto de WriteTotalTimeoutMultiplier por el número de bytes escritos. Un valor cero para WriteTotalTimeoutMultiplier y WriteTotalTimeoutConstant indica que el tiempo límite total no se utilizará para operaciones de escritura. Si una aplicación pone ReadIntervalTimeout y ReadTotalTimeoutMultiplier a MAXDWORD y ReadTotalTimeoutConstant a un valor mayor que cero y menor que MAXDWORD, cuando la función ReadFile sea invocada, puede ocurrir que: •
Si hay caracteres en la cola de entrada, ReadFile retorna inmediatamente con los caracteres de la cola.
•
Si no hay caracteres en la cola de entrada, ReadFile espera hasta que llegue un carácter y entonces retorna inmediatamente.
•
Si ningún carácter llega dentro del tiempo especificado por ReadTotalTimeoutConstant, ReadFile retornará pasado el tiempo límite.
Controlar eventos Vimos que la función EstablecerConexion, después que abre el puerto de comunicaciones, lanza un hilo ControlarEventos. El hilo controlará los eventos que ocurren sobre el puerto invocando a la función WaitCommEvent. Esta función informa del evento ocurrido sobre el dispositivo de comunicaciones a través de su segundo argumento. Los eventos a controlar fueron establecidos por la función SetCommMask. También puede utilizar GetCommMask para ver los eventos que fueron establecidos. DWORD OVERLAPPED
dwMascEvt; sOver = {0, 0, 0, 0, 0};
CAPÍTULO 3: COMUNICACIONES
135
sOver.hEvent = CreateEvent( NULL, // sin seguridad TRUE, // iniciación manual FALSE, // inicialmente ocupado NULL ); // sin nombre // ... WaitCommEvent( m_hDisCom, &dwMascEvt, &sOver ); // ...
Si un proceso intenta cambiar la máscara de eventos del dispositivo utilizando SetCommMask mientras una operación WaitCommEvent está en curso, WaitCommEvent retorna inmediatamente y la variable dwMascEvt es puesta a 0. Si m_hDisCom no se abriera con FILE_FLAG_OVERLAPPED, WaitCommEvent no retorna hasta que ocurra uno de los eventos especificados, o un error. Si m_hDisCom se abre con FILE_FLAG_OVERLAPPED, el parámetro tercero de WaitCommEvent no debe ser NULL. Debe apuntar a una estructura OVERLAPPED válida que contenga un handle a un evento con iniciación manual. Si fuera NULL, la función puede informar incorrectamente de que la operación está finalizada. En este último caso, si una operación de E/S solapada no puede completarse inmediatamente, la función WaitCommEvent devuelve FALSE y la función GetLastError devuelve ERROR_IO_PENDING, indicando que la operación se está ejecutando en segundo plano. Cuando esto ocurre, el sistema pone el objeto referenciado por el miembro hEvent de la estructura OVERLAPPED en estado ocupado antes de que WaitCommEvent retorne. Cuando posteriormente ocurra un evento de los especificados o un error, el sistema liberará dicho objeto. El proceso que llama puede utilizar una de las funciones de espera (WaitForSingleObject, WaitForMultipleObjects, etc.) para determinar el estado del objeto evento y después invocar a GetOverlappedResult para determinar los resultados de la operación WaitCommEvent. GetOverlappedResult informa del éxito o fracaso de la operación, y la variable dwMascEvt indica el evento que ocurrió. UINT ControlarEventos(LPVOID p) { DWORD dwMascEvt; CCommView *const pView = (CCommView *)p; OVERLAPPED sOver = {0, 0, 0, 0, 0}; // Crear un evento de E/S utilizado para lecturas solapadas sOver.hEvent = CreateEvent( NULL, // sin seguridad TRUE, // iniciación manual FALSE, // inicialmente ocupado NULL ); // sin nombre
136 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 if (sOver.hEvent == NULL) { AfxMessageBox( "Fallo al crear el evento para el hilo", MB_OK | MB_ICONEXCLAMATION ); return 0; } // Restablecer los eventos por si hubieran cambiado if (!SetCommMask( pView->m_hDisCom, EV_RXCHAR | EV_TXEMPTY | EV_RX80FULL | EV_ERR )) return 0; while ( pView->m_ConexionEstablecida ) { dwMascEvt = 0; WaitCommEvent( pView->m_hDisCom, &dwMascEvt, &sOver ); if ((dwMascEvt & EV_RXCHAR) == EV_RXCHAR) ::PostMessage(pView->m_hWnd, WM_EVENTO_COM, EV_RXCHAR, 0); else if ((dwMascEvt & EV_TXEMPTY) == EV_TXEMPTY) ::PostMessage(pView->m_hWnd, WM_EVENTO_COM, EV_TXEMPTY, 0); else if ((dwMascEvt & EV_RX80FULL) == EV_RX80FULL) ::PostMessage(pView->m_hWnd, WM_EVENTO_COM, EV_RX80FULL, 0); else if ((dwMascEvt & EV_ERR) == EV_ERR) ::PostMessage(pView->m_hWnd, WM_EVENTO_COM, EV_ERR, 0); } pView->m_bHiloActivo = FALSE; // Liberar el manipulador del evento CloseHandle( sOver.hEvent ); return 1; }
Observe que cuando ocurre un evento sobre el dispositivo de comunicaciones, el hilo secundario se lo notifica al hilo principal (a la aplicación) enviándole un mensaje WM_EVENTO_COM. Este mensaje fue definido anteriormente en el fichero CommView.h. La información que acompaña a dicho mensaje permite conocer a la aplicación cuál fue el evento ocurrido. Por lo tanto, lo siguiente es añadir dicho mensaje al mapa de mensajes de la vista. Para ello, abra el fichero CommView.cpp, localice el mapa de mensajes y añada la línea que se muestra a continuación: BEGIN_MESSAGE_MAP(CCommView, CFormView) //{{AFX_MSG_MAP(CCommView) // ... //}}AFX_MSG_MAP ON_MESSAGE(WM_EVENTO_COM, OnEventoCom) END_MESSAGE_MAP()
En el capítulo de hilos vimos las posibles formas de comunicación entre hilos. No es correcto invocar desde un hilo a una función miembro de una clase de la
CAPÍTULO 3: COMUNICACIONES
137
aplicación. Por ejemplo, si procede de la forma siguiente, tendrá problemas, ya que un hilo, en nuestro caso ControlarEventos, además de no ser un miembro de una clase de la aplicación, se ejecuta con concurrentemente con ella (con el hilo principal): if ((dwMascEvt & EV_RXCHAR) == EV_RXCHAR) nBytes = pView->LeerCaracteresPuerto( BytesLeidos, BLOQUEMAX );
La respuesta al mensaje WM_EVENTO_COM es la función OnEventoCom, que realizará un proceso u otro en función del evento ocurrido. Por ejemplo, si el evento es que se recibió información en la cola de recepción del dispositivo de comunicaciones, esta función leerá dicha información y la procesará; en nuestro caso la visualizará en la caja de texto correspondiente. Por lo tanto, añada esta función a la clase CCommView y edítela como se indica a continuación: long CCommView::OnEventoCom(UINT wParam, long lParam) { BYTE BytesLeidos[BLOQUEMAX + 1]; int nBytes; // Mensajes recibidos desde el hilo switch (wParam) { case EV_RXCHAR: if (nBytes = LeerCaracteresPuerto( BytesLeidos, BLOQUEMAX )) OnVisualizarCars( BytesLeidos, nBytes ); break; case EV_TXEMPTY: // ... break; case EV_RX80FULL: // ... break; case EV_ERR: // ... break; } return 0; }
Vemos que si el evento que se produjo fue EV_RXCHAR, la función OnEventoCom invoca primero a la función LeerCaracteresPuerto para leer los caracteres recibidos y después a OnVisualizarCars para añadir dicha información a la caja de texto referenciada por m_rx de la interfaz de usuario.
138 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Función LeerCaracteresPuerto La función LeerCaracteresPuerto lee un bloque del puerto COM y lo almacena en un array. El primer parámetro es el array donde se almacenarán los bytes leídos y el segundo es el número máximo de bytes que se van a leer. int LeerCaracteresPuerto(BYTE *pBytesLeidos, int BloqueMax );
Defina la constante BLOQUEMAX en el fichero CommView.cpp, así: #define BLOQUEMAX 80
La función LeerCaracteresPuerto leerá como máximo BLOQUEMAX caracteres. Para ello invocará a la función ReadFile. bLeer = ReadFile( m_hDisCom, pBytesLeidos, dwNumBytes, &dwNumBytes, &m_sOverRead ); pBytesLeidos[dwNumBytes] = 0; // finalizar con el carácter nulo
La función ReadFile lee dwNumBytes caracteres del dispositivo de comunicaciones referenciado por m_hDisCom y los almacena en el array pBytesLeidos. Si m_hDisCom se abrió con FILE_FLAG_OVERLAPPED, el último parámetro de ReadFile no debe ser NULL; debe apuntar a una estructura OVERLAPPED válida que contenga un handle a un evento con iniciación manual. En este caso, ReadFile puede retornar antes de que la operación de lectura se haya completado, en cuyo caso devolverá FALSE y la función GetLastError ERROR_IO_PENDING. Esto permite continuar la ejecución del proceso que hizo la llamada mientras la operación de lectura finaliza. Cuando la operación finaliza, el evento especificado en la estructura OVERLAPPED, que está ocupado, se pone en el estado libre. Para obtener información del éxito o fracaso de la operación, podemos invocar a la función GetOverlappedResult; si esta función devuelve cero significa que la operación no se ha completado o que ha fracaso. Para obtener información de lo ocurrido, podemos invocar a la función GetLastError. Si el último parámetro de ReadFile fuera NULL, la función puede informar incorrectamente de que la operación está finalizada. La información obtenida a través de la función GetOverlappedResult corresponde a la última operación solapada sobre el dispositivo especificado, para la cual fue proporcionada la estructura OVERLAPPED especificada, y para la que los resultados de la operación estaban pendientes. if (!bLeer) {
CAPÍTULO 3: COMUNICACIONES
139
if (GetLastError() == ERROR_IO_PENDING) { while(!GetOverlappedResult( m_hDisCom, &m_sOverRead, &dwNumBytes, FALSE )) { dwError = GetLastError(); if(dwError == ERROR_IO_INCOMPLETE) // ... }
Si la función que inicia la operación devuelve FALSE y GetLastError devuelve ERROR_IO_PENDING, es porque esa operación queda pendiente. Cuando una operación de E/S está pendiente, la función que inició la operación pone el evento referenciado por el miembro hEvent de la estructura OVERLAPPED en el estado ocupado. Y cuando la operación pendiente finaliza, el sistema pone el objeto evento en el estado libre. Si el último parámetro de GetOverlappedResult es TRUE, la función no retorna hasta que la operación pendiente finalice. También, si posteriormente ocurre un evento de los especificados o un error, el sistema liberará el objeto evento. Si es FALSE y la operación está todavía pendiente, la función devuelve FALSE y GetLastError devuelve ERROR_IO_INCOMPLETE. int CCommView::LeerCaracteresPuerto(BYTE *pBytesLeidos, int BloqueMax) { BOOL bLeer; COMSTAT EstadoCom; DWORD dwCodsError; DWORD dwNumBytes; DWORD dwError; char szError[10]; // Leer cada vez como máximo BloqueMax caracteres ClearCommError( m_hDisCom, &dwCodsError, &EstadoCom ); dwNumBytes = min( (DWORD)BloqueMax, EstadoCom.cbInQue ); if (dwNumBytes > 0) { bLeer = ReadFile( m_hDisCom, pBytesLeidos, dwNumBytes, &dwNumBytes, &m_sOverRead ); pBytesLeidos[dwNumBytes] = 0; // finalizar con el carácter nulo if (!bLeer) { if (GetLastError() == ERROR_IO_PENDING) { // Tenemos que esperar a que la lectura se complete. Esta
140 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 // función será interrumpida cuando transcurra un tiempo // CommTimeOuts.ReadTotalTimeoutConstant. // Chequear errores en el puerto while(!GetOverlappedResult( m_hDisCom, &m_sOverRead, &dwNumBytes, FALSE )) { dwError = GetLastError(); // Si no terminó, dwError vale ERROR_IO_INCOMPLETE if(dwError == ERROR_IO_INCOMPLETE) continue; else { // Ocurrió un error, intentar recuperarlo MensajeDeError( dwError ); ClearCommError( m_hDisCom, &dwCodsError, &EstadoCom ); if ( dwCodsError > 0 ) { wsprintf( szError, "Com:
", dwCodsError ); // Los errores IE son < 0 (winbase.h) AfxMessageBox( szError, MB_OK | MB_ICONEXCLAMATION ); } break; } } // fin while } else // error distinto de ERROR_IO_PENDING { dwNumBytes = 0; ClearCommError( m_hDisCom, &dwCodsError, &EstadoCom ); if ( dwCodsError > 0 ) { wsprintf( szError, "Com: ", dwCodsError ); AfxMessageBox( szError, MB_OK | MB_ICONEXCLAMATION ); } } } } return ( dwNumBytes ); }
La función ClearCommError obtiene información sobre el error de comunicaciones ocurrido y sobre el estado del dispositivo de comunicaciones. Asimismo, desactiva cualquier indicador de error que haya sido activado para habilitar operaciones E/S adicionales.
CAPÍTULO 3: COMUNICACIONES
141
Función EscribirCarsPuerto La función EscribirCarsPuerto escribe un bloque en el puerto COM procedente de un array pasado como parámetro. El primer parámetro es el array donde se almacenarán los bytes leídos y el segundo es el número de bytes que se quieren escribir. Las explicaciones con respecto a algunas de las funciones utilizadas ya han sido descritas en apartados anteriores. BOOL CCommView::EscribirCarsPuerto(BYTE *pBytesAEscribir, DWORD dwBytes) { COMSTAT EstadoCom; BOOL bEscribir; DWORD dwNumBytes; DWORD dwCodsError; DWORD dwError; DWORD dwBytesEnviados=0; char szError[128]; bEscribir = WriteFile( m_hDisCom, pBytesAEscribir, dwBytes, &dwNumBytes, &m_sOverWrite ); // // // // // //
Normalmente el código siguiente no se ejecutará porque el driver cachea las operaciones de escritura. Por lo tanto, pequeñas peticiones de E/S (hasta algunos cientos de bytes) serán normalmente aceptadas inmediatamente y WriteFile devolverá TRUE aunque el puerto de comunicaciones permita operaciones solapadas.
if (!bEscribir) { if( GetLastError() == ERROR_IO_PENDING ) { // Debemos esperar a que la operación de escritura termine // para conocer el éxito o no de la misma. // // // //
Podría ser beneficioso colocar la operación de escritura en un hilo separado para que un bloqueo durante la realización de dicha operación no afecte negativamente la sensibilidad de la interfaz de usuario.
// // // //
Si la operación de escritura tarda demasiado en finalizar, esta función será interrumpida cuando transcurra un tiempo CommTimeOuts.WriteTotalTimeoutMultiplier. Este código toma nota de la interrupción pero no reintenta la escritura.
while(!GetOverlappedResult( m_hDisCom, &m_sOverWrite, &dwNumBytes, FALSE )) {
142 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 dwError = GetLastError(); // Si no terminó, dwError vale ERROR_IO_INCOMPLETE if(dwError == ERROR_IO_INCOMPLETE) { dwBytesEnviados += dwNumBytes; continue; } else { // ocurrió un error, intentar recuperarlo MensajeDeError( dwError ); ClearCommError( m_hDisCom, &dwCodsError, &EstadoCom ); if ( dwCodsError > 0 ) { wsprintf( szError, "Com: ", dwCodsError ); // Los errores IE son < 0 (winbase.h) AfxMessageBox( szError, MB_OK | MB_ICONEXCLAMATION ); } break; } } dwBytesEnviados += dwNumBytes; if( dwBytesEnviados != dwBytes ) wsprintf(szError,"\nProbablemente se ha sobrepasado el " "tiempo límite.\nBytes enviados %ld", dwBytesEnviados); else wsprintf(szError,"\n%ld bytes escritos", dwBytesEnviados); } else // error distinto de ERROR_IO_PENDING { ClearCommError( m_hDisCom, &dwCodsError, &EstadoCom ); if ( dwCodsError > 0 ) { wsprintf( szError, "Com: ", dwCodsError ); AfxMessageBox( szError, MB_OK | MB_ICONEXCLAMATION ); } return FALSE; } } return TRUE; }
Función CortarConexion La función CortarConexion cierra el puerto de comunicaciones. Para ello, pone la variable m_ConexionEstablecida a valor FALSE para que el hilo ControlarEventos finalice, inhabilita los eventos, desactiva la línea DTR, termina las operaciones de E/S, limpia las colas de recepción y de transmisión, y cierra el puerto de comunicaciones.
CAPÍTULO 3: COMUNICACIONES
143
BOOL CCommView::CortarConexion() { m_ConexionEstablecida = FALSE; // finaliza el hilo // Inhabilitar todos los eventos SetCommMask( m_hDisCom, 0 ); // Esperar hasta que el hilo finalice while( m_bHiloActivo ); // Desactivar DTR EscapeCommFunction( m_hDisCom, CLRDTR ); // Terminar las operaciones de lectura y escritura pendientes // y limpiar las colas Rx y Tx PurgeComm( m_hDisCom, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR ); CloseHandle( m_hDisCom ); m_hDisCom = NULL; return TRUE; }
INTERFAZ DEL USUARIO Cuando se ejecute la aplicación, una de las primeras tareas que hay que realizar es leer la configuración almacenada en el registro de Windows. Este proceso lo realizaremos desde la función OnInitialUpdate de la clase CCommView. void CCommView::OnInitialUpdate() { CFormView::OnInitialUpdate(); // Ajustar el tamaño de la ventana marco a la vista ResizeParentToFit( FALSE ); // Iniciar el puerto y los controles de la IU UpdateData(FALSE); m_botonEnviar.EnableWindow(FALSE); Iniciar(); // configuración inicial del puerto COM }
Asimismo, la función OnInitialUpdate, además de ajustar el marco de la ventana al tamaño de la vista, inhabilita el botón Enviar. Cuando se establece una conexión entre dos equipos, previamente hay que especificar los siguientes parámetros: puerto (COM1, COM2, COM3, COM4),
144 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
baudios (110, 300, 600, 1200, 2400, 4800, 9600, ...), paridad (par, impar, ninguna), bits de datos (normalmente 7 u 8 bits por carácter), bits de parada (1 o 2) y control de flujo (Xon/Xoff, hardware, ninguna). Para establecer los parámetros anteriormente especificados la aplicación proporciona la orden Parámetros COM. Cuando el usuario ejecute esta orden se visualizar la caja de diálogo Configuración que le permitirá establecer los parámetros con los que se realizarán las comunicaciones. La respuesta a esta acción del usuario será la función OnConfigParam. Ejecute ClassWizard, añada esta función y edítela como se indica a continuación: void CCommView::OnConfigParam() { // Crea el objeto de diálogo CParamCom dlg; // Actualizar los datos del diálogo dlg.m_nPuerto = m_indPuerto; dlg.m_nBaudios = m_indBaudios; dlg.m_nParidad = m_indParidad; dlg.m_nBitsCar = m_indBitsCar; dlg.m_nBitsParada = m_indBitsParada; dlg.m_nControlFlujo = m_indControlFlujo; // Mostrar el cuadro de diálogo y verificar el botón pulsado if (dlg.DoModal() != IDOK) return; // Actualizar la configuración m_indPuerto = dlg.m_nPuerto; m_indBaudios = dlg.m_nBaudios; m_indParidad = dlg.m_nParidad; m_indBitsCar = dlg.m_nBitsCar; m_indBitsParada = dlg.m_nBitsParada; m_indControlFlujo = dlg.m_nControlFlujo; if( EstablecerConexion() ) { m_ConexionEstablecida = TRUE; AfxMessageBox( "Puerto de comunicaciones abierto", MB_OK | MB_ICONEXCLAMATION ); m_botonEnviar.EnableWindow(TRUE); } }
Una vez visualizada la caja de diálogo Configuración, si el usuario hace clic en el botón Aceptar (IDOK) se actualizarán los parámetros de configuración con los valores seleccionados y se invocará a la función EstablecerConexion para abrir el puerto de comunicaciones especificado. Si este proceso se ejecuta satisfac-
CAPÍTULO 3: COMUNICACIONES
145
toriamente, ponemos la variable m_ConexionEstablecida a valor TRUE, variable que utilizaremos posteriormente para identificar si el puerto está o no abierto. Esta variable fue declarada anteriormente como miembro de la clase CCommView e iniciada en el constructor con el valor FALSE. Como la función anterior hace referencia a la clase CParamCom, es necesario añadir la línea siguiente al fichero CommView.cpp. #include "ParamCom.h"
Para no permitir modificar la configuración del puerto de comunicaciones cuando esté abierto, ejecute ClassWizard y añada la siguiente función miembro de CCommView, controladora del mensaje UPDATE_COMMAND_UI. void CCommView::OnUpdateConfigParam(CCmdUI* pCmdUI) { pCmdUI->Enable(!m_ConexionEstablecida); }
Si el usuario hace clic en el botón Cancelar (IDCANCEL) de la caja de diálogo Configuración, no se realizará ninguna acción. En cambio, si el usuario hace clic en el botón Restaurar (IDC_DEFAULT) se establecerán como valores por omisión los últimos valores que fueron almacenados en el registro de Windows. void CParamCom::OnPorOmision() { // Parámetros por defecto de la comunicación m_nPuerto = 1; // COM2 m_nBaudios = 6; // 9600 m_nParidad = 0; // Ninguna m_nBitsCar = 4; // 8 m_nBitsParada = 0; // 1 m_nControlFlujo = 1; // Xon/Xoff UpdateData(FALSE); // Refrescar las listas desplegables }
Ejecute ClassWizard y vincule la función OnPorOmision miembro de la clase CParamCom al botón Restaurar. La orden Establecer del menú Conexión tiene como función abrir el puerto de comunicaciones con los parámetros actualmente seleccionados. Cuando el usuario ejecute esta orden, como respuesta será invocada la función OnConexionEstablecer miembro de la clase CCommView, que a su vez invocará a la función Estable-
146 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
cerConexion. Por lo tanto, ejecute ClassWizard, vincule OnConexionEstablecer con la orden Establecer y edítela como se indica a continuación: void CCommView::OnConexionEstablecer() { if ( EstablecerConexion() ) { UpdateData(TRUE); m_botonEnviar.EnableWindow(TRUE); UpdateData(FALSE); } }
Si el dispositivo de comunicaciones se abre satisfactoriamente, la función OnConexionEstablecer, además, actualiza las variables miembro m_tx y m_rx ligadas con las cajas de texto de transmisión y de recepción, respectivamente, y habilita el botón Enviar. Para no permitir abrir el puerto de comunicaciones cuando ya esté abierto, ejecute ClassWizard y añada la siguiente función miembro de CCommView, controladora del mensaje UPDATE_COMMAND_UI. void CCommView::OnUpdateConexionEstablecer(CCmdUI* pCmdUI) { pCmdUI->Enable(!m_ConexionEstablecida); }
Para cerrar el puerto de comunicaciones, la interfaz de la aplicación proporciona la orden Cortar del menú Conexión. Para hacer operativa esta orden, vincúlela con la función OnConexionCortar y edítela así: void CCommView::OnConexionCortar() { Terminar(); // guardar la configuración CortarConexion(); m_botonEnviar.EnableWindow(FALSE); }
Observe que la función OnConexionCortar primero llama a la función Terminar para guardar la configuración actual en el registro de Windows, después invoca a la función CortarConexion y finalmente inhabilita el botón Enviar. Para no permitir cerrar el puerto de comunicaciones cuando no esté abierto, ejecute ClassWizard y añada la siguiente función miembro de CCommView, controladora del mensaje UPDATE_COMMAND_UI. void CCommView::OnUpdateConexionCortar(CCmdUI* pCmdUI)
CAPÍTULO 3: COMUNICACIONES
147
{ pCmdUI->Enable(m_ConexionEstablecida); }
Para prever el caso de que estando un dispositivo de comunicaciones abierto, el usuario cierre la aplicación sin haber ejecutado previamente la orden Cortar, defina el destructor de la clase CCommView así: CCommView::~CCommView() { if ( m_ConexionEstablecida ) CortarConexion(); }
ENVIAR Y RECIBIR DATOS Para enviar datos, el usuario arrancará la aplicación, establecerá las comunicaciones, escribirá el texto a enviar en la caja de Texto a transmitir y pulsará el botón Enviar. Por lo tanto, añada la función OnEnviar controladora del mensaje WM_COMMAND que Windows envía al hacer clic en el botón Enviar y edítela como se muestra a continuación: void CCommView::OnEnviar() { int vr, n; BYTE *pszBytes; UpdateData(TRUE); // Enviar los datos que hay en la caja transmisión if ( n = m_tx.GetLength() ) { pszBytes = (BYTE *)m_tx.GetBuffer(n + 1); vr = EscribirCarsPuerto(pszBytes, n ); m_tx.ReleaseBuffer(); // Eliminar los caracteres transmitidos if ( vr ) { m_tx = ""; UpdateData( FALSE ); } } }
La función OnEnviar envía la información a la cola de salida invocando a la función EscribirCarsPuerto y después limpia la caja Texto a transmitir.
148 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Cuando los caracteres enviados desde otra máquina se reciben en la cola de recepción, el dispositivo de comunicaciones lo notifica por medio del evento EV_RXCHAR. Este evento es capturado por el hilo ControlarEventos por medio de la función WaitCommEvent. Entonces, el hilo envía un mensaje WM_EVENTO_COM al hilo principal (a la aplicación) notificándole el evento ocurrido en el dispositivo de comunicaciones. Como respuesta a este mensaje se ejecuta la función OnEventoCom que en este caso invoca a la función LeerCaracteresPuerto para obtener los datos del puerto, y después a la función OnVisualizarCars para visualizarlos en la caja Texto recibido. Por lo tanto, ejecute ClassWizard, añada la función OnVisualizarCars como miembro de CCommView y edítela como se indica a continuación: void CCommView::OnVisualizarCars(BYTE *pszBytes, int nBytes) { m_rx += pszBytes; // añadir los caracteres recibidos a los ya existentes UpdateData( FALSE ); // visualizarlos GetDlgItem(IDC_TX)->SetFocus(); // enfocar la caja de transmisión }
La aplicación está finalizada. Ahora puede compilarla y ejecutarla. Para realizar las pruebas en un solo ordenador, puede unir los hilos numerados dos y tres de su puerto serie. De esta forma lo que trasmita, lo recibirá de nuevo. Otra solución, es conectar un módem. Si envía una orden ATZ más CR, el módem le devolverá OK. Esta aplicación no soporta la transmisión de ficheros binarios, sólo soporta la transmisión de ficheros de texto ASCII. Se deja como ejercicio para el lector modificar la aplicación para que soporte ficheros texto y binarios (observe que el problema deriva de haber utilizado para manipular la información, cadenas de caracteres finalizadas con el carácter ASCII nulo).
CONTROL DE COMUNICACIONES Visual C++ incluye un control personalizado, Microsoft Communications Control, que permite establecer una comunicación serie entre máquinas, basada en el estándar RS232, de una forma rápida y sencilla. Para poder utilizar este control en una aplicación, hay que añadir al proyecto el control ActiveX MSCOMM32.OCX para aplicaciones de 32 bits. Este control tiene los eventos y propiedades siguientes: Eventos OnComm Propiedades Break
CDHolding
CommEvent
CommID
CAPÍTULO 3: COMUNICACIONES
CommPort EOFEnable Index Name OutBufferSize PortOpen SThreshold
CTSHolding Handshaking Input NullDiscard Output RThreshold Tag
DSRHolding InBufferCount InputLen Object Parent RTSEnable
149
DTREnable InBufferSize InputMode OutBufferCount ParityReplace Settings
Para obtener una amplia información sobre cada una de estas propiedades, recurra a la ayuda en línea de Visual Basic. Como ejemplo, vamos a realizar la misma aplicación anterior, pero utilizando ahora un control de comunicaciones. Por lo tanto, cree una nueva aplicación SDI denominada ControlCom que utilice una caja de diálogo como ventana principal. Para ello, ejecute AppWizard y haga que la clase CControlComView sea una clase derivada de CFormView. A diferencia de la aplicación anterior, permita que ControlCom tenga una barra de estado que utilizaremos para visualizar mensajes. A continuación, abra el editor de recursos y sitúe sobre la plantilla de diálogo creada por omisión, los controles con las propiedades que se especificaron en la aplicación anterior. Añada ahora el control ActiveX, Microsoft Communications Control. El resultado que obtendrá será similar al mostrado en la figura siguiente:
Seleccione el control de comunicaciones (IDC_MSCOMM1), invoque a ClassWizard, y vincule con el control una variable m_MSComm1. En este instante será informado de que el control aún no ha sido insertado en el proyecto. Al pulsar el botón Aceptar, Developer Studio hará este trabajo automáticamente por usted insertando una o más clases que encapsulan el control; en este caso, añadirá al proyecto la clase CMSComm. Esta clase tiene el aspecto siguiente: class CMSComm : public CWnd { // ...
150 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 // Operations public: // ... void SetCommPort(short nNewValue); short GetCommPort(); // ... void SetPortOpen(BOOL bNewValue); BOOL GetPortOpen(); void SetRThreshold(short nNewValue); short GetRThreshold(); void SetRTSEnable(BOOL bNewValue); BOOL GetRTSEnable(); void SetSettings(LPCTSTR lpszNewValue); CString GetSettings(); void SetSThreshold(short nNewValue); short GetSThreshold(); void SetOutput(const VARIANT& newValue); VARIANT GetOutput(); void SetInput(const VARIANT& newValue); VARIANT GetInput(); void SetCommEvent(short nNewValue); short GetCommEvent(); // ... };
Observamos que la funcionalidad de la clase da acceso a cada una de las propiedades que expusimos anteriormente para este control. Por ejemplo, para establecer el número de puerto que deseamos utilizar escribiríamos: m_MSComm1.SetCommPort(2);
Si abre ClassWizard y selecciona IDC_MSCOMM1 (objeto control de comunicaciones) puede observar en la lista de mensajes el evento OnComm. El evento OnComm se genera siempre que cambia el valor de la propiedad CommEvent para indicar que se ha producido un evento o un error en la comunicación. La propiedad CommEvent contiene la constante numérica correspondiente al evento o al error que se ha generado. A continuación indicamos estas constantes. Constantes de eventos: Constante comEvSend comEvReceive comEvCTS
Valor 1 2 3
Descripción Evento “enviar datos”. Evento “recibir datos”. Cambio en la línea “preparado para enviar” (CTS).
CAPÍTULO 3: COMUNICACIONES
comEvDSR
4
comEvCD
5
comEvRing comEvEOF
6 7
151
Cambio en la línea “equipo de datos preparado” (DSR). Cambio en la línea “detección de portadora” (CD). Detección de llamada. Fin de fichero.
Constantes de errores: Constante comEventBreak comEventCTSTO
Valor 1001 1002
comEventDSRTO
1003
comEventFrame comEventOverrun comEventCDTO
1004 1006 1007
comEventRxOver comEventRxParity comEventTxFull comEventDCB
1008 1009 1010 1011
Descripción Señal de interrupción recibida. Tiempo de espera de “preparado para enviar” sobrepasado. Tiempo de espera de “equipo de datos preparado” sobrepasado. Error de trama. Pérdida de información en el puerto. Tiempo de espera de “detección de portadora” sobrepasado. Desbordamiento del buffer de recepción. Error de paridad. Buffer de transmisión lleno. Error inesperado al recuperar el “bloque de control de dispositivos” (DCB) para el puerto.
Constantes de InputMode: Constante comInputModeText
Valor 0
comInputModeBinary
1
Descripción (Predeterminado) Los datos se recuperan como texto mediante la propiedad Input. Los datos se recuperan como datos binarios mediante la propiedad Input.
Constantes de protocolos: Constante comNone comXonXoff comRTS
Valor 0 1 2
comRTSXOnXOff
3
Descripción Sin protocolo. Protocolo XON/XOFF. Protocolo RTS/CTS (Petición de envío/preparado para enviar). Ambos protocolos (RTS y XON/XOFF).
152 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Tenga en cuenta que si establece las propiedades RThreshold o SThreshold a 0 (valor predeterminado para ambas propiedades), se desactiva la interceptación de los eventos comEvReceive y comEvSend, respectivamente. Continuando con la aplicación, modifique la barra de menús con los menús y las órdenes que se especificaron en la aplicación anterior. Asimismo, implemente también la caja de diálogo Configuración exactamente igual que lo hizo en la aplicación anterior, excepto los datos de la caja de texto Control de flujo que ahora serán: Ninguno, Xon/Xoff, Hardware (RTS/CTS) y Ambos (RTS y XON/XOFF). Añada también una nueva lista desplegable identificada por IDC_INPUTMODE y asígnele los datos Modo texto y Modo binario.
Recuerde que tiene que añadir a la aplicación una clase CParamCom derivada de CDialog, basada en la plantilla IDD_PARAMETROSCOM que acaba de diseñar. Guarde la declaración y la definición de esta clase en los ficheros paramcom.h y paramcom.cpp. Después, añada a la clase CParamCom las variables miembro m_nPuerto, m_nBaudios, m_nParidad, m_nBitsCar, m_nBitsParada y m_nControlFlujo vinculadas a cada una de las listas desplegables del diálogo. Invoque a ClassWizard y añada a la clase CControlComView la función miembro OnInitialUpdate y edítela como se indica a continuación: void CControlComView::OnInitialUpdate() { CFormView::OnInitialUpdate(); // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout(); ResizeParentToFit( FALSE ); }
CAPÍTULO 3: COMUNICACIONES
153
Si ahora compila y ejecuta la aplicación, obtendrá un resultado similar al mostrado en la figura siguiente:
Tipo VARIANT Si nos fijamos en las funciones miembro de la clase CMSComm que encapsula la funcionalidad del control de comunicaciones, en muchas de ellas aparece un tipo identificado por VARIANT. El tipo VARIANT no es más que un tipo de datos genérico. Permite declarar variables capaces de almacenar datos de cualquier tipo predefinido. La declaración de este tipo se basa en una estructura en la que cabe destacar dos miembros: vt para almacenar el tipo del dato (por ejemplo, VT_I4, VT_BOOL, VT_BSTR) y una unión con todos los posibles tipos de datos que pueden ser descritos. typedef struct tagVARIANT { VARTYPE vt; // ... union { unsigned char bVal; short iVal; long lVal; float fltVal; double dblVal; VARIANT_BOOL boolVal; SCODE scode; CY cyVal; DATE date; BSTR bstrVal; // ... }; };
// // // // // // // // // //
VT_UI1. VT_I2. VT_I4. VT_R4. VT_R8. VT_BOOL. VT_ERROR. VT_CY. VT_DATE. VT_BSTR.
154 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
El siguiente ejemplo muestra como se utiliza un VARIANT para pasar valores a una función. #include // constantes y macros bool LongCadena(VARIANT *vLong, VARIANT vBstr) { // Validar los parámetros if ( vLong->vt != VT_I4 || vBstr.vt != VT_BSTR) return false; // Calcular la longitud V_I4(vLong) = ::SysStringByteLen(V_BSTR(&vBstr)); return true; } void MiFuncion() { bool vr; long lon; char *cad1 = "Cadena de caracteres", cad2[30]; BSTR bstrCadena; // un puntero de 32 bits a char bstrCadena = ::SysAllocString((const unsigned short *)cad1); // Declarar las variables VARIANT vLong; VARIANT vBstr; // Especificar los tipos vLong.vt = VT_I4; // long vBstr.vt = VT_BSTR; // Asignar valores. // La razón de utilizar un & en las líneas siguientes, // es porque las macros requieren punteros. V_I4(&vLong) = 0L; V_BSTR(&vBstr) = bstrCadena; // Llamar a la función vr = LongCadena(&vLong, vBstr); // ... // Acceder a los valores lon = V_I4(&vLong); strcpy(cad2, (const char *)V_BSTR(&vBstr)); // ... // Liberar la memoria asignada a la cadena ::SysFreeString(bstrCadena); }
CAPÍTULO 3: COMUNICACIONES
155
Como puede observar, hay que asignar el tipo de los datos manualmente y utilizar macros para asignar los datos. Cada tipo de datos tiene una macro asociada para acceder a los datos. Por ejemplo, el tipo VT_BSTR tiene asociada la macros V_BSTR. Visual C++ proporciona tres clases diseñadas para facilitar la utilización de datos de tipo VARIANT: COleVariant (MFC), CComVariant (ATL) y _variant_t. Esta última clase fue incorporada a Visual C++ a partir de la versión 5 y es mucho más funcional que las otras dos. Un objeto _variant_t encapsula el tipo de datos VARIANT. La clase maneja la asignación y desasignación del recurso, y llama a VariantInit (para iniciar vt a VT_EMPTY) y a VariantClear (para limpiar la estructura VARIANT) cuando es necesario. Esta clase, además de otras funciones, define varios constructores que permiten construir un objeto _variant_t a partir de cualquier dato de un tipo predefinido (incluyendo cadenas de caracteres), la función miembro SetString que permite asignar una cadena de caracteres a un objeto _variant_t y los operadores de asignación (=), de comparación (==, !=) y extractores u operadores de conversión de _variant_t a cada uno de los tipos predefinidos. Otra clase de interés es _bstr_t, que encapsula el tipo de datos BSTR. La clase maneja la asignación y desasignación del recurso llamando a SysAllocString y SysFreeString. Esta clase, además de otras funciones, define varios constructores que permiten construir un objeto _bstr_t a partir de: char *, BSTR, etc., la función miembro length que permite obtener la longitud del objeto BSTR encapsulado, y los operadores de asignación (=, +=), de concatenación (+, –), de negación (!) para verificar si el objeto BSTR encapsulado es NULL, de comparación (==, !=, <, >, <=, >=) y extractores u operadores de conversión de _bstr_t a wchar_t * y char *. Ambas clases utilizan el fichero de cabecera comdef.h. Veamos el ejemplo anterior utilizando ahora esta clase: #include // definiciones bool LongCadena(_variant_t *vLong, _variant_t vBstr) { // Validar los parámetros if ( vLong->vt != VT_I4 || vBstr.vt != VT_BSTR) return false; // Calcular la longitud _bstr_t bstr = vBstr; *vLong = (long)bstr.length();
156 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 return true; } void MiFuncion() { bool vr; long lon; char *cad1 = "Cadena de caracteres", cad2[30]; // Declarar las variables _variant_t vLong; _variant_t vBstr; // Asignar valores vLong = 0L; vBstr = cad1; // Llamar a la función vr = LongCadena(&vLong, vBstr); // ... // Acceder a los valores lon = vLong; strcpy(cad2, (_bstr_t)vBstr); // ... }
Manipular las comunicaciones El control de comunicaciones proporciona dos formas de manipular las comunicaciones: 1. Notificando cuándo ocurre un evento; por ejemplo, ha llegado un carácter o ha ocurrido un cambio en la línea DCD (detección de portadora) o RTS (petición de envío). Para manipular estos eventos y los posibles errores en las comunicaciones, implementaremos el procedimiento conducido por el evento OnComm del control de comunicaciones. 2. Verificando el valor de la propiedad CommEvent del control de comunicaciones después de cada función crítica en la aplicación, para saber qué evento se ha dado o qué error ha ocurrido. Esta alternativa es preferible cuando la aplicación es pequeña; por ejemplo, un marcador de llamadas telefónicas, ya que no tiene sentido generar un evento después de recibir cada carácter puesto que los únicos caracteres que se recibirán son las respuestas del módem.
CAPÍTULO 3: COMUNICACIONES
157
Cada control de comunicaciones que utilice se corresponde con un único puerto serie. Esto es, si necesitamos acceder a más de un puerto serie, hay que utilizar más de un control de comunicaciones. Por ejemplo, para establecer una comunicación a través del puerto COM2 utilizando un control de comunicaciones m_MSComm1, los pasos son los siguientes: 1. Especifique el puerto que va a abrir. Para realizar esta operación, asigne a la propiedad CommPort de m_MSComm1 el valor correspondiente a ese puerto. m_MSComm1.SetCommPort(2);
Puede asignar a la propiedad CommPort cualquier número entre 1 y 16 (el valor predeterminado es 1). Cualquier otro valor producirá un error. 2. Especifique las características de comunicación. Para ello, asigne a la propiedad Settings de m_MSComm1 los valores que definen las mismas: ' 19200 baudios, paridad ninguna, 8 bits por carácter ' y 1 bit de parada m_MSComm1.SetSettings("19200,N,8,1");
La propiedad Settings permite especificar la velocidad en baudios, la paridad y el número de bits de datos y de parada. De forma predeterminada, la velocidad en baudios es de 9600. La paridad sirve para la validación de los datos. Normalmente no se utiliza y se establece a “N”. El valor de bits de datos indica el número de bits que representan un bloque de datos. El bit de parada indica cuándo se ha recibido un bloque de datos. 3. Abra el puerto de comunicación. Para realizar esta operación, asigne el valor True a la propiedad PortOpen de m_MSComm1: m_MSComm1.SetPortOpen(true);
Una vez especificado el puerto que se desea abrir y la forma en que se realizará la transferencia de los datos, establecemos la conexión poniendo la propiedad PortOpen a true. No obstante, si la propiedad CommPort no se ha establecido correctamente o si el dispositivo no admite la configuración especificada, se producirá un error, o bien puede ocurrir que el dispositivo externo no funcione correctamente. 4. Cuando quiera enviar datos al puerto de comunicaciones, utilice la propiedad Output de m_MSComm1. Esta propiedad permite escribir caracteres en el buffer de transmisión:
158 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 CString str; // ... _variant_t var(str); m_MSComm1.SetOutput(var);
5. Para recibir datos a través del puerto, implemente la función que responda al evento OnComm y utilice la propiedad Input para obtener los datos. Esta propiedad retorna una cadena de caracteres tomados del buffer de recepción. Los caracteres leídos son eliminados automáticamente. void CControlComView::OnComm1() // responde al evento OnComm { // de m_MSComm1 _bstr_t bs; CString s; switch(m_MSComm1.GetCommEvent()) { case 1: // vbMSCommEvSend: // ... break; case 2: // vbMSCommEvReceive: bs = m_MSComm1.GetInput(); s = (char *)bs; AfxMessageBox(s); OnVisualizarCars(s); break; // ... } // ... }
La propiedad CommEvent retorna el evento o el error más reciente ocurrido durante un proceso de comunicaciones. 6. Utilice la propiedad PortOpen de m_MSComm1, para cerrar el puerto de comunicaciones cuando éstas finalicen. Para ello, asigne a esta propiedad el valor false. m_MSComm1.SetPortOpen(false);
Interfaz de comunicaciones Siguiendo los pasos de la aplicación anterior, vamos a implementar una interfaz con las operaciones más comunes. Dicha interfaz, que resumimos en la tabla siguiente, la integraremos en la clase CControlComView: Función
Descripción
CAPÍTULO 3: COMUNICACIONES
Iniciar Terminar EstablecerConexion ConfigurarDisCom LeerCaracteresPuerto EscribirCarsPuerto CortarConexion OnComm1
159
Lee del registro de Windows la configuración inicial. Guarda en el registro de Windows la configuración actual. Abre el puerto de comunicaciones. Establece los parámetros con los que se realizarán las comunicaciones. Lee un byte de la cola de entrada del puerto de comunicaciones. Escribe un byte en la cola de salida del puerto de comunicaciones. Cierra el puerto de comunicaciones. Función que responde a los eventos que ocurren sobre el puerto de comunicaciones.
Añada la declaración de estas funciones y de las variables necesarias para su implementación a la declaración de la clase CControlComView. class CControlComView : public CFormView { // ... // Operations public: ///////////////////////////////////////////////// // Interfaz para comunicaciones static int m_indPuerto; static int m_indBaudios; static int m_indParidad; static int m_indBitsCar; static int m_indBitsParada; static int m_indControlFlujo; static int m_indModoLectura; UINT BYTE BYTE bool
m_wTablaBaudios[13]; m_TablaParidad[5]; m_TablaBitsParada[3]; m_ConexionEstablecida;
// // // //
tabla de velocidades tabla de paridades tabla bits de parada true si el puerto fue abierto
static void Iniciar(); static void Terminar(); bool EstablecerConexion(); void ConfigurarDisCom(); bool CortarConexion(); int LeerCaracteresPuerto(_bstr_t *bstr); bool EscribirCarsPuerto(CString str); ///////////////////////////////////////////////// // ...
160 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 };
Las variables m_ind... contienen el índice del elemento seleccionado de las listas de la caja de diálogo Configuración. Inicie la variable m_ConexionEstablecida a false en el constructor de su clase. Asigne valores a los arrays m_wTablaBaudios, m_TablaParidad y m_TablaBitsParada miembros de la clase CControlComView. Realice esta asignación en el constructor de su clase, como se indica a continuación: CControlComView::CControlComView() : CFormView(CControlComView::IDD) { m_wTablaBaudios[0] = 110; m_wTablaBaudios[1] = 300; m_wTablaBaudios[2] = 600; m_wTablaBaudios[3] = 1200; m_wTablaBaudios[4] = 2400; m_wTablaBaudios[5] = 4800; m_wTablaBaudios[6] = 9600; m_wTablaBaudios[7] = 14400; m_wTablaBaudios[8] = 19200; m_wTablaBaudios[9] = 38400; m_wTablaBaudios[10] = 56000; m_wTablaBaudios[11] = 128000; m_wTablaBaudios[12] = 256000; m_TablaParidad[0] = 'N'; // ninguna m_TablaParidad[1] = 'E'; // par m_TablaParidad[2] = 'O'; // impar m_TablaParidad[3] = 'M'; // marca m_TablaParidad[4] = 'S'; // espacio //{{AFX_DATA_INIT(CControlComView) m_rx = _T(""); m_tx = _T(""); //}}AFX_DATA_INIT // TODO: add construction code here }
A continuación escribiremos cada una de las funciones descritas. Antes, inicie las variables estáticas y defina las cadenas de caracteres necesarias para el registro de Windows en la implementación de la clase CControlComView. int CControlComView::m_indPuerto int CControlComView::m_indBaudios int CControlComView::m_indParidad
= 1; // COM2 = 6; // 9600 = 0; // ninguna
CAPÍTULO 3: COMUNICACIONES
int int int int
CControlComView::m_indBitsCar CControlComView::m_indBitsParada CControlComView::m_indControlFlujo CControlComView::m_indModoLectura
static static static static static static static
char char char char char char char
szComu[] szPuerto[] szBaudios[] szParidad[] szBitsCar[] szBitsParada[] szControlFlujo[]
= = = = = = =
= = = =
4; 0; 1; 0;
// // // //
161
8 1 Xon/Xoff texto/binario
"Comunicaciones"; "Puerto"; "Baudios"; "Paridad"; "BitsCar"; "BitsParada"; "ControlFlujo";
Función Iniciar La función Iniciar obtiene del registro de Windows la configuración por omisión del dispositivo de comunicaciones. void CControlComView::Iniciar() { CWinApp *pApp = AfxGetApp(); // Recuperar configuración del registro de Windows m_indPuerto = pApp->GetProfileInt(szComu, szPuerto, 1); m_indBaudios = pApp->GetProfileInt(szComu, szBaudios, 6); m_indParidad = pApp->GetProfileInt(szComu, szParidad, 0); m_indBitsCar = pApp->GetProfileInt(szComu, szBitsCar, 4); m_indBitsParada = pApp->GetProfileInt(szComu, szBitsParada, 0); m_indControlFlujo = pApp->GetProfileInt(szComu, szControlFlujo, 1); }
El lugar de donde se obtiene esta información del registro de Windows se especifica de forma predeterminada en CControlCom.cpp así: SetRegistryKey(_T("Local AppWizard-Generated Applications"));
Función Terminar La función Terminar será invocada cuando se corta la comunicación para guardar en el registro de Windows la configuración actual del dispositivo de comunicaciones. Esta configuración es la que será utilizada por omisión la próxima vez que se ejecute la aplicación. void CControlComView::Terminar() { CWinApp *pApp= AfxGetApp(); // Guardar configuración en el registro de Windows pApp->WriteProfileInt(szComu, szPuerto, m_indPuerto); pApp->WriteProfileInt(szComu, szBaudios, m_indBaudios);
162 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 pApp->WriteProfileInt(szComu, szParidad, m_indParidad); pApp->WriteProfileInt(szComu, szBitsCar, m_indBitsCar); pApp->WriteProfileInt(szComu, szBitsParada, m_indBitsParada); pApp->WriteProfileInt(szComu, szControlFlujo, m_indControlFlujo); }
Función EstablecerConexion La función EstablecerConexion permite abrir el puerto de comunicaciones especificado por la variable miembro m_indPuerto. Según todo lo expuesto anteriormente, la función EstablecerConexion puede ser así: bool CControlComView::EstablecerConexion() { // Cerrar el puerto si estuviera abierto if (m_MSComm1.GetPortOpen()) m_MSComm1.SetPortOpen(false); // Especificar el puerto COM que se desea abrir m_MSComm1.SetCommPort(m_indPuerto + 1); // Establecer el tamaño de las colas de recepción y de transmisión m_MSComm1.SetInBufferSize(COLARX); m_MSComm1.SetOutBufferSize(COLATX); // Limpiar las colas Rx y Tx m_MSComm1.SetInBufferCount(0); m_MSComm1.SetOutBufferCount(0); // Establecer los parámetros de la comunicación ConfigurarDisCom(); // Caracteres que puede admitir el buffer de transmisión antes de que // el control genere el evento OnComm. Su valor predeterminado 0. m_MSComm1.SetSThreshold(1); // Caracteres que se van a recibir antes de que el control // genere el evento OnComm. Su valor predeterminado 0. m_MSComm1.SetRThreshold(1); // Abrir el puerto de comunicaciones m_MSComm1.SetPortOpen(true); if (!m_MSComm1.GetPortOpen()) { AfxMessageBox( "Error: No se puede abrir el puerto COM", MB_OK | MB_ICONEXCLAMATION ); MessageBeep(0xFFFFFFFF); return false; }
CAPÍTULO 3: COMUNICACIONES
163
m_ConexionEstablecida = true; return true; }
Defina las constantes COLARX y COLATX, que definen los tamaños de las colas de recepción y transmisión respectivamente, en el fichero ControlComView.cpp: #define COLARX 4096 #define COLATX 4096
Función ConfigurarDisCom Para construir el bloque de control del dispositivo (DCB) y configurar el dispositivo de comunicaciones, EstablecerConexion invoca a la función ConfigurarDisCom, que se muestra a continuación: void CControlComView::ConfigurarDisCom() { bool bEstablecer; char Settings[20]; char BitsParada[4]; switch(m_indBitsParada) { case 0: strcpy(BitsParada, "1"); break; case 1: strcpy(BitsParada, "1.5"); break; case 2: strcpy(BitsParada, "2"); break; } // Baudios, paridad, número de bits de datos y de parada wsprintf(Settings, "%ld,%c,%d,%s", m_wTablaBaudios[m_indBaudios], m_TablaParidad[m_indParidad], m_indBitsCar + 4, BitsParada); m_MSComm1.SetSettings(Settings); // Establecer el control de flujo software bEstablecer = (m_indControlFlujo == 1 || m_indControlFlujo == 3); // Xon/Xoff if (bEstablecer) m_MSComm1.SetHandshaking(1); // comXOnXOff // Establecer el control de flujo hardware bEstablecer = (m_indControlFlujo == 2 || m_indControlFlujo == 3); // RTS/CTS if (bEstablecer) m_MSComm1.SetHandshaking(2); // comRTS // Cómo se leerán los datos del puerto
164 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 m_MSComm1.SetInputMode(m_indModoLectura); // texto/binario }
Controlar eventos Vimos que la función EstablecerConexion, antes de abrir el puerto de comunicaciones, establece las propiedades SetSThreshold y SetRThreshold, que indican, respectivamente, los caracteres que puede admitir el buffer de transmisión antes de que el control genere el evento OnComm y los caracteres que se van a recibir antes de que el control genere el evento OnComm. El valor predeterminado para estas propiedades es cero. Si SetSThreshold es cero, no se generará el evento OnComm en la transmisión, y si SetRThreshold es cero, no se generará el evento OnComm en la recepción. Según lo expuesto, ejecute ClassWizard, seleccione el control IDC_MSCOMM1 y añada la función OnComm1 miembro de CControlCom que responderá al evento (mensaje) OnComm. OnComm1 será invocada automáticamente cada vez que se produzca sobre el puerto de comunicaciones un evento o un error de los expuestos anteriormente. Para saber de qué evento o error se trata y aplicar el tratamiento correspondiente, la función OnComm implementará básicamente una sentencia switch. Al mismo tiempo visualizará sobre la barra de estado el mensaje correspondiente al evento o error ocurrido. Estos mensajes los definiremos como recursos en la tabla de cadenas de caracteres de la aplicación. Por ejemplo, si el evento es que se recibió información en la cola de recepción del dispositivo de comunicaciones (comEvReceive), esta función leerá dicha información y la procesará; en nuestro caso la visualizará en la caja de texto correspondiente. Por lo tanto, edite esta función como se indica a continuación: void CControlComView::OnComm1() { CString strEvento, strError; _bstr_t bstrRecibida; // inluir switch(m_MSComm1.GetCommEvent()) { case 1: // comEvSend: strEvento.LoadString(IDS_COMEVSEND); break; case 2: // comEvReceive: strEvento.LoadString(IDS_COMEVRECEIVE); // Leer datos del puerto if ( LeerCaracteresPuerto(&bstrRecibida) ) OnVisualizarCars( (char *)bstrRecibida );
CAPÍTULO 3: COMUNICACIONES
break; case 3: // comEvCTS: strEvento.LoadString(IDS_COMMEVCTS); break; case 4: // comEvDSR: strEvento.LoadString(IDS_COMEVDSR); break; case 5: // comEvCD: strEvento.LoadString(IDS_COMEVCD); break; case 6: // comEvRing: strEvento.LoadString(IDS_COMEVRING); break; case 7: // comEvEOF: strEvento.LoadString(IDS_COMEVEOF); break; case 1001: // comErBreak: strError.LoadString(IDS_COMERBREAK); break; case 1002: // comErCTSTO: strError.LoadString(IDS_COMERCTSTO); break; case 1003: // comErDSRTO: strError.LoadString(IDS_COMERDSRTO); break; case 1004: // comErFrame: strError.LoadString(IDS_COMERFRAME); break; case 1006: // comErOverrun: strError.LoadString(IDS_COMEROVERRUN); break; case 1007: // comErCDTO: strError.LoadString(IDS_COMERCDTO); break; case 1008: // comErRxOver: strError.LoadString(IDS_COMERRXOVER); break; case 1009: // comErRxParity: strError.LoadString(IDS_COMERRXPARITY); break; case 1010: // comErTxFull: strError.LoadString(IDS_COMERTXFULL); break; } if (!strEvento.IsEmpty()) { CWnd *pMainWnd = AfxGetApp()->m_pMainWnd; CStatusBar *pStatusbar = (CStatusBar *)pMainWnd>GetDescendantWindow(AFX_IDW_STATUS_BAR);
165
166 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 pStatusbar->SetPaneText(0, strEvento); } else if (!strError.IsEmpty()) { MessageBeep(MB_ICONEXCLAMATION); strError += _T("\nAceptar para ignorar, Cancelar para salir"); int vr = AfxMessageBox(strError, MB_OKCANCEL | MB_ICONEXCLAMATION); if (vr == IDCANCEL) m_MSComm1.SetPortOpen(false); // cerrar el puerto } }
Vemos que si el evento que se produjo fue comEvReceive, la función OnComm invoca primero a la función LeerCaracteresPuerto para leer los caracteres recibidos y después a OnVisualizarCars para añadir dicha información a la caja de texto referenciada por m_rx de la interfaz de usuario.
Función LeerCaracteresPuerto La función LeerCaracteresPuerto lee datos del puerto COM, los almacena en un array de tipo _bstr_t y devuelve el número de caracteres leídos. int CControlComView::LeerCaracteresPuerto(_bstr_t *bstrRecibida) { *bstrRecibida = m_MSComm1.GetInput(); return bstrRecibida->length(); }
Función EscribirCarsPuerto La función EscribirCarsPuerto escribe un bloque en el puerto COM procedente de un array pasado como parámetro. bool CControlComView::EscribirCarsPuerto(CString str) { _variant_t var(str); m_MSComm1.SetOutput(var); return true; }
CAPÍTULO 3: COMUNICACIONES
167
Función CortarConexion La función CortarConexion cierra el puerto de comunicaciones. Antes de cerrarlo, verifica si quedan datos en la cola de transmisión. Si hay datos intenta enviarlos, y si no, cierra el puerto de comunicaciones. bool CControlComView::CortarConexion() { if (m_ConexionEstablecida) { // Establecer un periodo de 10 segundos a partir de la // hora actual bool bTiempoSobrepasado = false; CTime TiempoLimite = CTime::GetCurrentTime() + CTimeSpan(0,0,0,10); while (m_MSComm1.GetOutBufferCount()) { // Procesar todos los mensajes pendientes DoEvents(); if ( (CTime::GetCurrentTime() > TiempoLimite) || bTiempoSobrepasado) { int vr = AfxMessageBox("Datos no enviados", MB_ABORTRETRYIGNORE); switch (vr) { // Intentar enviar los datos durante otros 10 segs. case IDRETRY: TiempoLimite = CTime::GetCurrentTime() + CTimeSpan(0,0,0,10); break; // Ignorar el tiempo límite case IDIGNORE: bTiempoSobrepasado = true; break; // Abortar el intento case IDABORT: return false; } } } m_MSComm1.SetPortOpen(false); m_ConexionEstablecida = false; } return true; }
La función CortarConexión, puesto que establece un lazo que impediría que otras aplicaciones se ejecuten, invoca a la función DoEvents para permitir ejecutar cualquier otro mensaje que estuviera esperando en alguna cola de cualquier otra aplicación.
168 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 void CControlComView::DoEvents() { MSG msg; // ¿Hay mensajes esperando en alguna cola de mensajes? while (::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)) { // Procesar el mensaje. Si no es posible, retornar. if (!AfxGetThread()->PumpMessage()) return; } }
INTERFAZ DEL USUARIO Cuando se ejecute la aplicación, una de las primeras tareas que hay que hacer es leer la configuración almacenada en el registro de Windows. Este proceso lo realizaremos desde la función OnInitialUpdate de la clase CControlComView. void CControlComView::OnInitialUpdate() { CFormView::OnInitialUpdate(); // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout(); ResizeParentToFit( false ); // Iniciar el puerto y los controles de la IU UpdateData(false); m_botonEnviar.EnableWindow(false); Iniciar(); // configuración inicial del puerto COM }
Asimismo, la función OnInitialUpdate, además de ajustar el marco de la ventana al tamaño de la vista, inhabilita el botón Enviar. Cuando se establece una conexión entre dos equipos, previamente hay que especificar los parámetros bajos los cuales se realizará la misma. Para realizar esta operación la aplicación proporciona la orden Parámetros COM. Cuando el usuario ejecute esta orden se visualizará la caja de diálogo Configuración que le permitirá establecer los parámetros con los que se realizarán las comunicaciones. La respuesta a esta acción del usuario será la función OnConfigParam. Ejecute ClassWizard, añada esta función y edítela como se indica a continuación: void CControlComView::OnConfigParam() { // Crea el objeto de diálogo
CAPÍTULO 3: COMUNICACIONES
169
CParamCom dlg; // Actualizar los datos del diálogo dlg.m_nPuerto = m_indPuerto; dlg.m_nBaudios = m_indBaudios; dlg.m_nParidad = m_indParidad; dlg.m_nBitsCar = m_indBitsCar; dlg.m_nBitsParada = m_indBitsParada; dlg.m_nControlFlujo = m_indControlFlujo; dlg.m_nModoLectura = m_indModoLectura; // Mostrar el cuadro de diálogo y verificar el botón pulsado if (dlg.DoModal() != IDOK) return; // Actualizar la configuración m_indPuerto = dlg.m_nPuerto; m_indBaudios = dlg.m_nBaudios; m_indParidad = dlg.m_nParidad; m_indBitsCar = dlg.m_nBitsCar; m_indBitsParada = dlg.m_nBitsParada; m_indControlFlujo = dlg.m_nControlFlujo; m_indModoLectura = dlg.m_nModoLectura; if( EstablecerConexion() ) { m_ConexionEstablecida = true; AfxMessageBox( "Puerto de comunicaciones abierto", MB_OK | MB_ICONEXCLAMATION ); m_botonEnviar.EnableWindow(TRUE); } }
Una vez visualizada la caja de diálogo Configuración, si el usuario hace clic en el botón Aceptar (IDOK) se actualizarán los parámetros de configuración con los valores seleccionados y se invocará a la función EstablecerConexion para abrir el puerto de comunicaciones especificado. Como la función anterior hace referencia a la clase CParamCom, es necesario añadir la línea siguiente al fichero ControlComView.cpp. #include "ParamCom.h"
Para no permitir modificar la configuración del puerto de comunicaciones cuando esté abierto, ejecute ClassWizard y añada la siguiente función miembro de CControlComView, controladora del mensaje UPDATE_COMMAND_UI. void CControlComView::OnUpdateConfigParam(CCmdUI* pCmdUI) { pCmdUI->Enable(!m_ConexionEstablecida);
170 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 }
Si el usuario hace clic en el botón Cancelar (IDCANCEL) de la caja de diálogo Configuración, no se realizará ninguna acción. En cambio, si el usuario hace clic en el botón Restaurar (IDC_DEFAULT) se establecerán como valores por omisión los últimos que fueron almacenados en el registro de Windows. void CParamCom::OnPorOmision() { // Parámetros por defecto de la comunicación m_nPuerto = 1; // COM2 m_nBaudios = 6; // 9600 m_nParidad = 0; // Ninguna m_nBitsCar = 4; // 8 m_nBitsParada = 0; // 1 m_nControlFlujo = 1; // Xon/Xoff m_nControlFlujo = 0; // Texto/Binario UpdateData(false); // Refrescar las listas desplegables }
Ejecute ClassWizard y vincule la función OnPorOmision miembro de la clase CParamCom al botón Restaurar. La orden Establecer del menú Conexión tiene como función abrir el puerto de comunicaciones con los parámetros actualmente seleccionados. Cuando el usuario ejecute esta orden, como respuesta será invocada la función OnConexionEstablecer miembro de la clase CControlComView, que a su vez invocará a la función EstablecerConexion. Por lo tanto, ejecute ClassWizard, vincule OnConexionEstablecer con la orden Establecer y edítela como se indica a continuación: void CControlComView::OnConexionEstablecer() { if ( EstablecerConexion() ) { UpdateData(true); m_botonEnviar.EnableWindow(true); UpdateData(false); } }
Si el dispositivo de comunicaciones se abre satisfactoriamente, la función OnConexionEstablecer, además, actualiza las variables miembro m_tx y m_rx li-
CAPÍTULO 3: COMUNICACIONES
171
gadas con las cajas de texto de transmisión y de recepción, respectivamente, y habilita el botón Enviar. Para no permitir abrir el puerto de comunicaciones cuando ya esté abierto, ejecute ClassWizard y añada la siguiente función miembro de CControlComView, controladora del mensaje UPDATE_COMMAND_UI. void CControlComView::OnUpdateConexionEstablecer(CCmdUI* pCmdUI) { pCmdUI->Enable(!m_ConexionEstablecida); }
Para cerrar el puerto de comunicaciones, la interfaz de la aplicación proporciona la orden Cortar del menú Conexión. Para hacer operativa esta orden, vincúlela con la función OnConexionCortar y edítela así: void CControlComView::OnConexionCortar() { Terminar(); // guardar la configuración CortarConexion(); m_botonEnviar.EnableWindow(false); }
Observe que la función OnConexionCortar primero llama a la función Terminar para guardar la configuración actual en el registro de Windows, después invoca a la función CortarConexion y finalmente inhabilita el botón Enviar. Para no permitir cerrar el puerto de comunicaciones cuando no esté abierto, ejecute ClassWizard y añada la siguiente función miembro de CControlComView, controladora del mensaje UPDATE_COMMAND_UI. void CControlComView::OnUpdateConexionCortar(CCmdUI* pCmdUI) { pCmdUI->Enable(m_ConexionEstablecida); }
Si el usuario cierra la aplicación sin haber ejecutado previamente la orden Cortar, lógicamente estando el puerto abierto, el puerto es cerrado automáticamente cuando el control es destruido.
ENVIAR Y RECIBIR DATOS Para enviar datos, el usuario arrancará la aplicación, establecerá las comunicaciones, escribirá el texto a enviar en la caja de Texto a transmitir y pulsará el botón Enviar. Por lo tanto, añada la función OnEnviar controladora del mensaje
172 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
WM_COMMAND que Windows envía al hacer clic en el botón Enviar y edítela como se muestra a continuación: void CControlComView::OnEnviar() { int vr, n; UpdateData(true); // Enviar los datos que hay en la caja transmisión if ( n = m_tx.GetLength() ) { vr = EscribirCarsPuerto( m_tx ); // Eliminar los caracteres transmitidos if ( vr ) { m_tx = ""; UpdateData( false ); } } }
La función OnEnviar envía la información a la cola de salida invocando a la función EscribirCarsPuerto y después limpia la caja Texto a transmitir. Cuando los caracteres enviados desde otra máquina se reciben en la cola de recepción, el dispositivo de comunicaciones lo notifica por medio del mensaje OnComm. Como respuesta a este mensaje se ejecuta la función OnComm1, que en este caso, invoca a la función LeerCaracteresPuerto para obtener los datos del puerto, y después a la función OnVisualizarCars para visualizarlos en la caja Texto recibido. Por lo tanto, ejecute ClassWizard, añada la función OnVisualizarCars como miembro de CControlComView y edítela como se indica a continuación: void CControlComView::OnVisualizarCars(BYTE *pszBytes, int nBytes) { m_rx += strRecibida; // añadir los caracteres recibidos a los ya existentes UpdateData( false ); // visualizarlos GetDlgItem(IDC_TX)->SetFocus(); // enfocar la caja de transmisión }
La aplicación está finalizada. Ahora puede compilarla y ejecutarla. Para realizar las pruebas en un solo ordenador, puede unir los hilos numerados dos y tres de su puerto serie. De esta forma lo que transmita lo recibirá de nuevo. Otra solución, es conectar un módem. Si envía una orden ATZ más CR, el módem le devolverá OK.
CAPÍTULO 3: COMUNICACIONES
173
Como ejemplo, puede añadir a la aplicación un menú Utilidades con una orden Enviar fichero que permita transmitir ficheros de texto. A continuación se presenta un código que realiza esta operación: void CControlComView::OnEnviarFichero() { // Caja de diálogo Abrir CFileDialog DlgAbrir( TRUE, _T("txt"), NULL, OFN_HIDEREADONLY|OFN_PATHMUSTEXIST, _T("Ficheros de texto (*.txt)|*.txt|\ Todos (*.*)|*.*||"), this); if (DlgAbrir.DoModal() != IDOK) return; CFile FicheroTx( DlgAbrir.GetPathName(), CFile::modeRead ); int nTamBufTx = m_MSComm1.GetOutBufferSize(); int nTamFichTx = FicheroTx.GetLength(); char *pstrBuffer = new char[nTamBufTx+1]; // Leer/transmitir el fichero en bloques del tamaño del buffer Tx for (int n = 0; n < nTamFichTx/nTamBufTx; n++) { FicheroTx.Read( pstrBuffer, nTamBufTx ); pstrBuffer[nTamBufTx] = 0; // carácter nulo de terminación Transmitir( pstrBuffer, nTamBufTx ); } // Si el tamaño del fichero no es múltiplo de nTamBufTx, // enviar los datos restantes int nResto = nTamFichTx % nTamBufTx; if ( nResto ) { FicheroTx.Read( pstrBuffer, nResto ); pstrBuffer[nResto] = 0; // carácter nulo de terminación Transmitir( pstrBuffer, nResto ); } delete pstrBuffer; FicheroTx.Close(); GetDlgItem(IDC_TX)->SetFocus(); } void CControlComView::OnUpdateUtenviarfichero(CCmdUI* pCmdUI) { pCmdUI->Enable(m_ConexionEstablecida); } void CControlComView::Transmitir(char *pstrBuffer, int nTamBloque) {
174 VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32 LPWSTR pwstrDatos = new wchar_t[nTamBloque]; // Convertir el texto ASCII a UNICODE MultiByteToWideChar(CP_ACP, 0, pstrBuffer, nTamBloque, pwstrDatos, nTamBloque); _variant_t var(pstrBuffer); // Enviar los datos al puerto m_MSComm1.SetOutput(var); // Esperar a que el buffer Tx esté vacío while (m_MSComm1.GetOutBufferCount()) DoEvents(); delete pwstrDatos; }
La conversión del texto ASCII a UNICODE se ha hecho con fines didácticos. Si lo prefiere, puede enviar el texto directamente en ASCII. El tipo wchar_t (wide character - caracteres que utilizan dos bytes) es útil para escribir programas portables a nivel internacional. Este tipo se encuentra definido en stddef.h y stdlib.h.
Faltan páginas...
Faltan páginas...
CAPÍTULO 8 F.J.Ceballos/RA-MA
BIBLIOTECAS DINÁMICAS A pesar de la potencia de Visual C++, en algún momento se nos planteará algún problema que exija extendernos por encima de sus límites. Afortunadamente, Visual C++ no está limitado a sus capacidades internas. Como ya hemos visto, una aplicación Visual C++ puede utilizar una amplia variedad de funciones pertenecientes a la API de Windows. Si esto no es bastante, aún podemos ir más allá escribiendo bibliotecas dinámicas personalizadas. ¿Qué es una biblioteca dinámica? Una biblioteca dinámica, abreviadamente DLL (Dynamic Link Library), es un fichero ejecutable de funciones, o simplemente de recursos, tal como mapas de bits o definiciones de fuentes, que pueden ser llamadas por cualquier aplicación Windows. Una DLL personalizada es una biblioteca dinámica que nosotros mismos escribimos para satisfacer nuestras necesidades. Windows está en gran medida formado a partir de DLL, y si no, eche una mirada a su directorio system. Por ejemplo, los ficheros KRNL386.EXE, GDI.EXE y USER.EXE, así como KEYBOARD.DRV, SYSTEM.DRV y SOUND.DRV, son todos DLL. Los ficheros de fuentes, esto es, con extensión .FON, también son DLL. También encontrará cantidad de ficheros con extensión DLL; éstos también son DLL, muchas de ellas pertenecientes a aplicaciones Windows instaladas, como Excel o Visual Basic. Según esto, es fácil adivinar que una DLL puede tener cualquier extensión, aunque .DLL es la más estándar. De todas ellas, sólo las bibliotecas dinámicas con extensión .DLL son cargadas automáticamente por Windows, mientras que las DLL con otras extensiones tienen que ser cargadas explícitamente. Como todo, la utilización de las DLL tiene ventajas e inconvenientes. Por ejemplo, una función en una DLL está disponible para ser llamada por cualquier aplicación Windows. Las ventajas que esto supone son, por una parte, reducción
460
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
del código de la aplicación, al no tener que escribirla formando parte del código de la misma, lo que redundará en velocidad de compilación y de carga de la aplicación, así como en ahorro de espacio en el disco, ya que solamente existe una copia de la función. Y por otra parte, como están separadas de la aplicación, se pueden actualizar sin tener que tocar, y por lo tanto recompilar, las aplicaciones que las utilizan. Como inconvenientes, caben destacar la necesidad de estar presentes y el tiempo que se necesita para acceder a ellas cuando se ejecuta una aplicación que las utiliza. Sin embargo, cuando se utilizan bibliotecas estáticas, las funciones que la aplicación necesita se incluyen en la misma durante el proceso de enlace, por lo que ni se pierde tiempo en leerlas ni la biblioteca tiene que estar presente.
CREACIÓN DE UNA DLL EN Win32 El punto de entrada y de salida en una DLL en Win32 es una función denominada DllMain; ésta es una de las diferencias con respecto a Win16. Esta función es opcional; esto quiere decir que si no se escribe, el compilador asume una que no hace nada, simplemente retorna un valor TRUE. DllMain utiliza el convenio de llamada WINAPI para sus tres parámetros en lugar de FAR PASCAL que ha quedado obsoleto. El prototipo de esta función es el siguiente: BOOL WINAPI DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved);
y el esqueleto de la definición de la función es así: BOOL WINAPI DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved) { switch(dwReason) { case DLL_PROCESS_ATTACH: //... case DLL_THREAD_ATTACH: //... case DLL_THREAD_DETACH: //... case DLL_PROCESS_DETACH: //... } return TRUE; }
La función retorna el valor TRUE para indicar que se ha ejecutado satisfactoriamente. Si durante el proceso de iniciación la función retorna FALSE el sistema cancela el proceso.
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
461
El parámetro dwReason indica la razón por la que ha sido llamada la función DllMain: iniciación o terminación, por un proceso o por un hilo (thread). Para recordar las diferencias entre procesos e hilos, repase al capítulo “Hilos” incluido en esta obra. La siguiente tabla describe el significado de los posibles valores del parámetro dwReason: Valor de dwReason
Descripción
DLL_PROCESS_ATTACH Un nuevo proceso intenta acceder a la DLL; se asume un hilo. DLL_THREAD_ATTACH
Un nuevo hilo de un proceso existente intenta acceder a la DLL; esta llamada se hace a partir del segundo hilo de un proceso vinculado a la DLL.
DLL_PROCESS_DETACH Un proceso abandona la DLL. DLL_THREAD_DETACH
Uno de los hilos adicionales (no el primer hilo) de un proceso abandona la DLL.
El parámetro lpReserved se reserva para ser utilizado por el sistema. El parámetro hModule es el handle a un ejemplar de la DLL. Cuando necesite acceder a los parámetros emitidos en la línea de órdenes puede utilizar la función GetCommandLine de la API. Para crear una DLL, los pasos a seguir son similares a los ejecutados para escribir un programa C/C++. Escribimos los ficheros de cabecera (ficheros .h), los ficheros con la definición de las funciones (ficheros .c o .cpp), el fichero de definición de módulos (fichero .def), si es preciso, y los ficheros del proyecto para automatizar la construcción de la DLL; estos últimos, normalmente serán generados automáticamente por Visual C++. Dependiendo de la utilidad de la DLL, en ocasiones puede ser que necesitemos escribir un fichero de recursos (fichero .rc). Como ejemplo, vamos a desarrollar una DLL denominada strucdll32.dll que incluya dos funciones denominadas Sumar y Restar. Ambas funciones tendrán dos parámetros de tipo double y retornarán, respectivamente, un resultado también de tipo double que se corresponderá con la suma o la resta de los argumentos pasados en la llamada.
462
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Fichero de cabecera (.h) Los ficheros de cabecera contienen declaraciones y definiciones que el preprocesador de C incluye en el fichero fuente justo antes de la compilación. Para nuestro ejemplo, escribiremos un fichero strucdll32.h que contenga las declaraciones de las funciones Sumar y Restar: // ---------------------------------------------------// Nombre del fichero: STCDLL32.H // // Este fichero de cabecera contiene las funciones // prototipo para las funciones exportables por la // DLL denominada STCDLL32 // // Copyright (c) Fco. Javier Ceballos // ---------------------------------------------------// Variables globales // Funciones prototipo #ifdef __cplusplus //si los ficheros fuente son .cpp extern "C" { #endif double WINAPI Sumar( double Param1, double Param2 ); double WINAPI Restar( double Param1, double Param2 ); #ifdef __cplusplus //si el compilador es C++ ... } #endif
La macro WINAPI instruye al compilador para que interprete adecuadamente el convenio de llamada utilizado por la aplicación que invoca a las funciones de la DLL.
Fichero fuente (.c o .cpp) Fundamentalmente, el fichero fuente contiene la definición de las funciones. Para nuestro ejemplo, escribiremos un fichero strucdll32.cpp que contenga los ficheros de cabecera windows.h y strucdll32.h, la función de entrada y de salida de la DLL, DllMain, y las funciones que nosotros deseamos incluir en la biblioteca, Sumar y Restar:
// ---------------------------------------------------// Nombre del fichero: STCDLL32.CPP //
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
// Éste es el fichero fuente principal de la DLL, // el punto de entrada y de salida de la DLL // // Copyright (c) Fco. Javier Ceballos // ---------------------------------------------------#include #include "stcdll32.h" // ---------------------------------------------------// Función DllMain // // Éste es el punto de entrada y de salida de la DLL. // Esta función es llamada por Windows. Usted no tiene // que llamarla desde su aplicación. // // Parámetros: // hModule - el handle para un ejemplar de la DLL // dwReason - razón por la que ha sido llamada la DLL // lpReserved - reservado para uso del sistema. // // Valor retornado: // TRUE - indicando que la DLL se ha iniciado // satisfactoriamente. // ---------------------------------------------------BOOL WINAPI DllMain (HANDLE hModule, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: // Escriba aquí el código que escribía en LibMain (Win16). // Quizá tenga que hacer alguna modificación por el hecho de // que puede ser llamada más de una vez. // Retorne TRUE para salir de la DLL una vez cargada // o FALSE si la carga falla. break; case DLL_THREAD_ATTACH: // Escriba aquí el código de iniciación que se tiene // que ejecutar cada vez que se cree un hilo en un proceso // que ya tiene cargada esta DLL. break; case DLL_THREAD_DETACH: // Escriba aquí el código de terminación que se tiene // que ejecutar cada vez que un hilo en un proceso sale // de la DLL que dicho proceso ya tiene cargada. break; case DLL_PROCESS_DETACH: // Escriba aquí el código que escribía en WEP (Win16). // Este código quizá no sea necesario porque el
463
464
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
// sistema operativo ya se encarga de esta labor. break; } return TRUE; // DLL_PROCESS_ATTACH satisfactorio } // ---------------------------------------------------// Función Sumar // // Función que suma dos números reales. // // Parámetros: // Param1 - valor real. Primer sumando // Param2 - valor real. Segundo sumando. // Valor retornado: // valor real - Param1 + Param2 // ---------------------------------------------------double WINAPI Sumar( double Param1, double Param2 ) { return (Param1 + Param2); } // ---------------------------------------------------// Función Restar // // Función que suma dos números reales. // // Parámetros: // Param1 - valor real. Minuendo // Param2 - valor real. Sustraendo. // Valor retornado: // valor real - Param1 - Param2 // ---------------------------------------------------double WINAPI Restar( double Param1, double Param2 ) { return (Param1 - Param2); }
Fichero de definición de módulos (.def) El fichero de definición de módulos informa al enlazador (linker) sobre cómo crear el fichero ejecutable. Para nuestra DLL, puede ser el siguiente: ;-----------------------------------------------------; Nombre del fichero: STCDLL32.DEF ; ; Módulo de definición del fichero ; ; Copyright (c) Fco. Javier Ceballos
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
465
;-----------------------------------------------------LIBRARY STCDLL32 DESCRIPTION 'Ejemplo de creación de una DLL' EXPORTS Sumar @1 Restar @2
Observe que LIBRARY especifica el nombre de la biblioteca, y que EXPORTS define los nombres y los atributos de las funciones que explícitamente son puestas a disposición de otras aplicaciones y DLL; el valor ordinal a continuación del nombre de la función define la localización del nombre de la función en la tabla de nombres de la aplicación. La utilización del número de orden es más rápida y requiere menos espacio. Este fichero es necesario para que se genere stcdll32.lib. Si este fichero no se incluye en el proyecto sólo se generará stcdll32.dll. Para construir el proyecto que dará lugar a la DLL utilizando Visual C++ (32 bits), ejecute la orden New del menú File y elija la página Project. Después, elija el tipo de proyecto Win32 Dynamic-Link Library, ponga nombre al proyecto y pulse el botón OK. A continuación, si ya tiene editados los ficheros que van a formar parte del proyecto, añádalos al mismo utilizando la orden Files del submenú Add to Project del menú Project y compile el proyecto.
LLAMANDO A LAS FUNCIONES DE LA DLL A continuación, vamos a implementar una aplicación SDI que llame a las funciones de la DLL. Ejecute AppWizard y cree una aplicación denominada ApDll. Derive la clase CApDllView de la clase CFormView. Abra el editor de recursos y personalice los recursos de la aplicación. Edite la barra de menús para que sólo aparezcan los menús Fichero con la orden Salir y Ayuda con la orden Acerca de ApDll... Cree una plantilla de diálogo para que al ejecutar la aplicación se visualice una ventana como la siguiente:
466
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Cuando el usuario haga clic en el botón Sumar, aparecerá en la tercera caja la suma de las dos primeras, y cuando haga clic en Restar, aparecerá la diferencia. Para realizar la suma y la resta invocaremos a las funciones Sumar y Restar de la biblioteca dinámica stcdll32.dll. A continuación se expone el código correspondiente a esta aplicación. Lo primero que vamos a hacer es, utilizando ClassWizard, vincular la variable m_Operando1 con la caja de texto IDC_OPERANDO1, la variable m_Operando2 con la caja de texto IDC_OPERAND02 y la variable m_Resultado con la caja de texto IDC_RESULTADO, todas de tipo double. A continuación, añada la función OnInitialUpdate a la clase CApDllView y edítela para que permita ajustar el tamaño de la ventana marco a la vista. void CApDllView::OnInitialUpdate() { CFormView::OnInitialUpdate(); // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout(); ResizeParentToFit( false ); }
El siguiente paso es dar funcionalidad a los botones Sumar y Restar. Para ello, edite las funciones que se indican a continuación para que se ejecuten como respuesta al evento clic sobre cada uno de ellos. void CApDllView::OnSumar() { // Sumar UpdateData( true ); // actualizar variables miembro m_Resultado = Sumar( m_Operandol, m_Operando2 ); UpdateData( false ); // actualizar cajas de texto }
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
467
void CApDllView::OnRestar() { // Restar UpdateData( true ); // actualizar variables miembro m_Resultado = Restar( m_Operandol, m_Operando2 ); UpdateData( false ); // actualizar cajas de texto }
Observe que para realizar las operaciones de sumar y restar llamamos a las funciones de la biblioteca stcdll32.dll que hemos creado anteriormente. Antes de compilar el programa, hay que hacer todavía dos cosas: especificar los prototipos de las funciones de Sumar y Restar e indicar al compilador la biblioteca que tiene que utilizar para enlazar estas funciones.
Enlace estático Incluya en el directorio de la aplicación los ficheros stcdll32.h, stcdll32.dll y stcdll32.lib. Después, añada al fichero ApDllView.cpp la línea siguiente: #include "stcdll32.h" // prototipos de funciones
El enlace estático necesita de una biblioteca .lib que le informe acerca de los puntos de la DLL en los que se encuentran las funciones buscadas. Por eso, en nuestro proyecto tenemos que incluir la biblioteca stcdll32.lib. Para ello, ejecute la orden Settings del menú Project, elija la página Link y añada en Object/library modules el nombre stcdll32.lib.
Ahora guarde la aplicación, ejecútela y observe cómo funciona. Como ejercicio, puede modificar la aplicación y añadir otras funciones para otro tipo de operaciones matemáticas, financieras, estadísticas, etc.
468
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Enlace dinámico Aunque el enlace estático es el más sencillo de realizar, hay veces que es necesario realizar un enlace dinámico. Por ejemplo, cuando sólo disponemos del fichero DLL, cuando la aplicación no conoce el nombre de la DLL hasta la ejecución, etc. El enlace dinámico consiste en cargar la DLL en memoria durante la ejecución de la aplicación utilizando la función AfxLoadLibrary (o LoadLibrary) de la API de Windows y averiguar posteriormente cuál es el punto de entrada a la función que nos interesa utilizar. Para obtener el punto de entrada al que nos hemos referido, utilizaremos la función GetProcAddress. Cuando no utilice la DLL descárguela utilizando la función AfxFreeLibrary (o FreeLibrary). Las aplicaciones basadas en las MFC deberían utilizar las funciones Afx... en lugar de sus equivalentes puesto que han sido diseñadas para manipular la sincronización de hilos. HINSTANCE AFXAPI AfxLoadLibrary( LPCTSTR lpszModuleName ); BOOL AFXAPI AfxFreeLibrary( HINSTANCE hInstLib ); FARPROC GetProcAddress( HMODULE hModule, // handle al modulo DLL LPCSTR lpProcName // nombre de la función );
Para cargar una DLL dinámicamente, la aplicación debe realizar las siguientes operaciones:
Llamar a AfxLoadLibrary (o LoadLibrary) para cargar la DLL y obtener un handle al módulo que la define.
Llamar a GetProcAddress para obtener un puntero a cada una de las funciones exportadas que la aplicación necesita llamar.
Llamar a AfxFreeLibrary (o FreeLibrary) cuando finalice el trabajo con la DLL. Por ejemplo:
typedef UINT (CALLBACK* PFNDLLFUNC1)(DWORD,UINT); // ... HINSTANCE hDLL; // handle a la DLL PFNDLLFUNC1 pfnDllFunc1; // puntero a una función DWORD dwParam1; UINT uParam2, uValRet; hDLL = AfxLoadLibrary("MiDLL"); if (hDLL != NULL) { pfnDllFunc1 = (PFNDLLFUNC1)GetProcAddress(hDLL, "DLLFunc1");
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
469
if (!pfnDllFunc1) { // Manipular el error AfxFreeLibrary(hDLL); return CODIGO_DE_ERROR; } else { // Llamar a la función uValRet = pfnDllFunc1(dwParam1, uParam2); } }
Como ejemplo, reproduzca la misma aplicación anterior, pero ahora almacénela en el directorio ApDll2. Para este ejemplo sólo necesita incluir en el directorio de la aplicación el fichero stcdll32.dll. Añada a la declaración de la clase CApDllView las siguientes declaraciones y definiciones: typedef double (CALLBACK* PFNDLLFUNC1)(double, double); class CApDllView : public CFormView { private: HINSTANCE m_hDLL; // handle a la DLL PFNDLLFUNC1 m_pfnDllSumar; // puntero a la función Sumar PFNDLLFUNC1 m_pfnDllRestar; // puntero a la función Restar // ... };
A continuación, modifique la función OnInitialUpdate, OnSumar y OnRestar como se indica a continuación: void CApDllView::OnInitialUpdate() { CFormView::OnInitialUpdate(); // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout(); ResizeParentToFit( false ); // Cargar stcdll32.dll m_hDLL = AfxLoadLibrary("stcdll32"); if (m_hDLL != NULL) { m_pfnDllSumar = (PFNDLLFUNC1)GetProcAddress(m_hDLL, "Sumar"); if (!m_pfnDllSumar) { AfxFreeLibrary(m_hDLL); AfxMessageBox("Error al acceder a la función Sumar"); }
470
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
m_pfnDllRestar = (PFNDLLFUNC1)GetProcAddress(m_hDLL, "Restar"); if (!m_pfnDllRestar) { AfxFreeLibrary(m_hDLL); AfxMessageBox("Error al acceder a la función Restar"); } } else AfxMessageBox("No se puede cargar stcdll32.dll"); } void CApDllView::OnSumar() { // Sumar UpdateData( true ); // actualizar variables miembro m_Resultado = (*m_pfnDllSumar)( m_Operando1, m_Operando2 ); UpdateData( false ); // actualizar cajas de texto } void CApDllView::OnRestar() { // Restar UpdateData( true ); // actualizar variables miembro m_Resultado = (*m_pfnDllRestar)( m_Operando1, m_Operando2 ); UpdateData( false ); // actualizar cajas de texto }
Para decrementar el contador de referencias de la DLL cuando la aplicación finalice (cuando el contador de referencias sea cero, el sistema descargará la DLL de memoria), invoque a la función AfxFreeLibrary desde el destructor de la clase CApDllView, así: CApDllView::~CApDllView() { AfxFreeLibrary(m_hDLL); }
RECURSOS EN UNA DLL Los recursos que una aplicación necesita pueden ser aportados por la propia aplicación (fichero de recursos) o por una biblioteca dinámica. Esto es, además de funciones, una biblioteca dinámica puede contener también recursos, tal como mapas de bits o ficheros wav, que pueden ser utilizados por cualquier aplicación Windows que cargue esa biblioteca. Por ejemplo, supongamos una aplicación Windows que tiene que visualizar un mapa de bits adaptado a la resolución y número de colores que tenga el sistema. La solución puede ser crear distintos mapas de bits en función de la resolución de pantalla y del número de colores y que la aplicación cargue el mapa de
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
471
bits adecuado a nuestro sistema. Estos recursos, junto con otros, pueden ser almacenados en bibliotecas dinámicas para obtener un tiempo de ejecución satisfactorio. Como ejemplo, vamos a crear una DLL denominada recvga.dll con los recursos para un sistema VGA y recsvga.dll con los mismos recursos pero realizados para un sistema SVGA. Los contenidos de las DLL serán los siguientes: DLL recvga.dll recsvga.dll
Recursos bm4vga.bmp bm8vga.bmp mikeoldf.wav bm4svga.bmp bm8svga.bmp mikeoldf.wav
Descripción 640×480 - 16 colores 640×480 - 256 colores fichero de sonido 800×600 - 16 colores 800×600 - 256 colores fichero de sonido
Para construir el proyecto que dará lugar a la DLL recvga.dll utilizando Visual C++, ejecute la orden New del menú File y elija la página Project. Después, elija el tipo de proyecto Win32 Dynamic-Link Library, ponga el nombre recvga al proyecto y pulse el botón OK. A continuación edite los ficheros que van a formar del proyecto y añádalos al mismo. En este caso, crearemos los ficheros recvga.h (File - New - Files - C/C++ Header File), recvga.cpp (File - New - Files - C/C++ Source File), recvga.def (File - New - Files - Text File) y recvga.rc (File - New - Files - Resource Script). Para nuestros propósitos, el contenido de los ficheros recvga.def, recvga.h y recvga.cpp se reduce a sus esqueletos básicos: // ---------------------------------------------------// Nombre del fichero: RECVGA.H // // Este fichero de cabecera contiene las funciones // prototipo para las funciones exportables por la // DLL denominada RECVGA // // Copyright (c) Fco. Javier Ceballos // ---------------------------------------------------// Variables globales // Funciones prototipo #ifdef __cplusplus //si los ficheros fuente son .cpp extern "C" { #endif // Declaraciones de la funciones de la DLL #ifdef __cplusplus //si el compilador es C++ ... }
472
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
#endif // ---------------------------------------------------// Nombre del fichero: REGVGA.CPP // // Éste es el fichero fuente principal de la DLL, // el punto de entrada y de salida de la DLL // // Copyright (c) Fco. Javier Ceballos // ---------------------------------------------------#include #include "recvga.h" // ---------------------------------------------------// Función DllMain // // Éste es el punto de entrada y de salida de la DLL. // Esta función es llamada por Windows. Usted no tiene // que llamarla desde su aplicación. // // Parámetros: // hModule - el handle para un ejemplar de la DLL // dwReason - razón por la que ha sido llamada la DLL // lpReserved - reservado para uso del sistema. // // Valor retornado: // TRUE - indicando que la DLL se ha iniciado // satisfactoriamente. // ---------------------------------------------------BOOL WINAPI DllMain (HANDLE hModule, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: // Escriba aquí el código que escribía en LibMain (Win16). // Quizá tenga que hacer alguna modificación por el hecho de // que puede ser llamada más de una vez. // Retorne TRUE para salir de la DLL una vez cargada // o FALSE si la carga falla. break; case DLL_THREAD_ATTACH: // Escriba aquí el código de iniciación que se tiene // que ejecutar cada vez que se cree un hilo en un proceso // que ya tiene cargada esta DLL. break; case DLL_THREAD_DETACH: // Escriba aquí el código de terminación que se tiene // que ejecutar cada vez que un hilo en un proceso sale // de la DLL que dicho proceso ya tiene cargada.
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
473
break; case DLL_PROCESS_DETACH: // Escriba aquí el código que escribía en WEP (Win16). // Este código quizá no sea necesario porque el // sistema operativo ya se encarga de esta labor. break; } return TRUE; // DLL_PROCESS_ATTACH satisfactorio } // ---------------------------------------------------// Definición de las funciones de la DLL // ---------------------------------------------------;-----------------------------------------------------; Nombre del fichero: RECVGA.DEF ; ; Módulo de definición. Permite crear RECVGA.LIB ; ; Copyright (c) Fco. Javier Ceballos ;-----------------------------------------------------LIBRARY RECVGA DESCRIPTION 'DLL con recursos' EXPORTS ;-----------------------------------------------------; Funciones exportadas explícitamente ;------------------------------------------------------
Antes de editar recvga.rc, cree un directorio recvga\res para almacenar los recursos bm4vga.bmp, bm8vga.bmp y mikeoldf.wav correspondientes a esta DLL. Para construir los mapas de bits puede utilizar la utilidad Paint de Windows. Para editar recvga.rc, abra el editor de recursos y añada los recursos anteriormente especificados.
474
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Finalmente, compile el proyecto. Como resultado obtendrá el fichero recvga.lib y recvga.dll. Cuando otras aplicaciones utilicen los recursos proporcionados por recvga.dll, necesitarán conocer sus identificadores. Por lo tanto, cree un fichero idrecvga.h con dichos identificadores: // IDRECVGA.H. Identificadores de los recursos #define IDR_MIKEOLDF_WAV #define IDB_BM4VGA #define IDB_BM8VGA
101 102 103
Siguiendo los mismos pasos, cree otro proyecto recsvga que dé lugar a la DLL recsvga.dll. Cree también el fichero idrecsvga.h con los identificadores de los recursos proporcionados por recsvga.dll.
Acceso a los recursos en una DLL Para explicar cómo utilizar las DLLs que acabamos de construir, vamos a crear una aplicación, Recursos, que utilice los recursos de una u otra DLL en función de la resolución de nuestro monitor. Además, el fichero wav se ejecutará cuando se visualice el diálogo Acerca de. Para empezar, genere una nueva aplicación SDI. Después copie las bibliotecas anteriormente generadas en el directorio de la aplicación. Copie también los ficheros de cabecera idrecvga.h y idrecsvga.h. A continuación, añada al fichero Recursos.h las dos líneas siguientes: #include "idrecvga.h" #include "idrecsvga.h"
Cuando se ejecute la aplicación, lo primero que hay que hacer es obtener un handle a la biblioteca de recursos que se vaya a utilizar; esto dependerá del número de colores y de la resolución de pantalla. Para ello, en primer lugar, añada a la clase CRecursosApp las variables miembro m_hDll, m_BitsPorPixel, m_resx y m_resy: class CRecursosApp : public CWinApp { public: CRecursosApp(); HINSTANCE m_hDll; int m_BitsPorPixel, m_resx, m_resy; // ... };
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
475
Después, añada a la función miembro InitInstance de la clase aplicación el siguiente código: BOOL CRecursosApp::InitInstance() { AfxEnableControlContainer(); // Standard initialization // ... LoadStdProfileSettings(); // Comprobar qué DLL hay que cargar CString strDll; CDC dc; // Obtener información del contexto de dispositivo dc.CreateIC("DISPLAY", NULL, NULL, NULL); m_BitsPorPixel = dc.GetDeviceCaps( BITSPIXEL ); m_resx = dc.GetDeviceCaps( HORZRES ); m_resy = dc.GetDeviceCaps( VERTRES ); if ((m_resx >= 800) && (m_resy >= 600)) // SVGA strDll = ".\\recsvga.dll"; else strDll = ".\\recvga.dll"; // VGA // Cargar la biblioteca if ((m_hDll = AfxLoadLibrary(strDll)) == NULL) { char mensaje[80]; wsprintf(mensaje, "Error al cargar la DLL %s", strDll); AfxMessageBox( mensaje ); return FALSE; // Finalizar. Vuelve al S.O. } // ... return TRUE; }
Observe que la función InitInstance carga la biblioteca dinámica en función de la resolución. Si la biblioteca que se intenta cargar no se encuentra en el directorio actual de trabajo, la aplicación presenta mediante una caja de diálogo un mensaje de error y vuelve al sistema operativo. Cada vez que una aplicación carga una biblioteca, un contador asociado con la misma es incrementado en una unidad. Cuando la aplicación que ha cargado la biblioteca finalice, debe liberar la memoria asignada a la misma llamando a la función AfxFreeLibrary. Lo que hace esta función en realidad es decrementar en una unidad el contador asociado con la biblioteca. Cuando este contador alcanza el valor cero, la biblioteca es descargada de memoria; esto es, la memoria asignada a la biblioteca es liberada.
476
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Según lo expuesto añada a la clase CRecursosApp la función miembro ExitInstance para que invoque a la función AfxFreeLibrary. int CRecursosApp::ExitInstance() { if (m_hDll) AfxFreeLibrary( m_hDll ); return CWinApp::ExitInstance(); }
Una vez cargada la biblioteca el siguiente paso es ver cómo se accede a los recursos de la misma. Por ejemplo, para acceder al recurso de sonido identificado por la cadena de caracteres IDR_MIKEOLDF_WAV y ejecutar el sonido cuando se visualice el diálogo Acerca de ..., modifique en el fichero Recursos.cpp la función OnAppAbout así: void CRecursosApp::OnAppAbout() { static bool bError = false; BOOL bCorrecto = FALSE; if (!bError) { // Obtener el handle a los recursos de la aplicación HINSTANCE hRecsApp = AfxGetResourceHandle(); // Establecer como recursos de la aplicación los de la // biblioteca cargada en InitInstance AfxSetResourceHandle( ((CRecursosApp *)AfxGetApp())->m_hDll ); // Tocar el recurso de sonido bCorrecto = PlaySound(MAKEINTRESOURCE(IDR_MIKEOLDF_WAV), ((CRecursosApp *)AfxGetApp())->m_hDll, SND_MEMORY | SND_ASYNC | SND_NODEFAULT | SND_RESOURCE); if (!bCorrecto) { AfxMessageBox("No se puede activar el sonido.\n" "¿El driver es el adecuado?"); bError = true; } // Restablecer los recursos iniciales de la aplicación AfxSetResourceHandle( hRecsApp ); } // Visualizar el diálogo Acerca de ... CAboutDlg aboutDlg; if ( aboutDlg.DoModal() == IDOK ) // visualizar diálogo { if (bCorrecto) PlaySound(NULL, NULL, 0); }
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
477
}
La función anterior primero invoca a AfxGetResourceHandle para obtener un handle a los recursos actuales de la aplicación. A continuación, utilizando la función AfxSetResourceHandle, establece como nuevos recursos los proporcionados por la biblioteca cargada. Una vez hecho esto, utiliza los recursos requeridos de la biblioteca (en nuestro caso el recurso de sonido identificado por IDR_MIKEOLDF_WAV) y cuando termina de utilizarlos, restablece los recursos iniciales de la aplicación. Para reproducir el sonido proporcionado por IDR_MIKEOLDF_WAV la función OnAppAbout invoca a la función de la API PlaySound (esta función fue comentada en el capítulo “Multimedia”). Recuerde que para utilizar esta función tiene que incluir el fichero de cabecera mmsystem.h e indicar al enlazador que utilice la biblioteca winmm.lib. Para acceder a los mapas de bits IDB_BMxxx de la biblioteca dinámica, proceda de forma análoga. Por ejemplo, vamos a hacer que cuando se ejecute la aplicación, se cargue un mapa de bits y se visualice en la vista. El mapa de bits cargado será adecuado para la resolución y número colores establecidos en nuestro sistema. Para realizar este proceso declare, en primer lugar, las siguientes variables miembro de la clase CRecursosView: class CRecursosView : public CFormView { private: CDC *m_pMemDCPantalla; // DC del área de trabajo HBITMAP m_hBitmapAnterior; int m_nAncho, m_nAlto; // tamaño del mapa de bits // ... };
Después, añada a la clase CRecursosView la función miembro OnInitialUpdate. Esta función obtiene de los recursos almacenados en la biblioteca dinámica el mapa de bits adecuado a la resolución y número de colores del sistema para ser seleccionado por un contexto de dispositivo de memoria compatible con la vista; dicho contexto será utilizado posteriormente por la función OnDraw para pintar el mapa de bits en la vista. void CRecursosView::OnInitialUpdate() { CFormView::OnInitialUpdate(); BOOL bCorrecto = FALSE; // Crear un DC en memoria compatible con el área de trabajo CClientDC dc( this );
478
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
m_pMemDCPantalla = new CDC; m_pMemDCPantalla->CreateCompatibleDC( &dc ); // Obtener el handle a los recursos iniciales de la aplicación HINSTANCE hRecsApp = AfxGetResourceHandle(); // Establecer como recursos de la aplicación los de la biblioteca AfxSetResourceHandle( ((CRecursosApp *)AfxGetApp())->m_hDll ); // Cargar el mapa de bits que se va a visualizar CRecursosApp *pApp = (CRecursosApp *)AfxGetApp(); CBitmap *pBitmapAnterior, *pBitmapActual = new CBitmap; if ((pApp->m_resx >= 800) && (pApp->m_resy >= 600)) // SVGA if (pApp->m_BitsPorPixel >= 8) // 256 o más colores bCorrecto = pBitmapActual->LoadBitmap( IDB_BM8SVGA ); else bCorrecto = pBitmapActual->LoadBitmap( IDB_BM4SVGA ); else if (pApp->m_BitsPorPixel >= 8) // 256 o más colores bCorrecto = pBitmapActual->LoadBitmap( IDB_BM8VGA ); else bCorrecto = pBitmapActual->LoadBitmap( IDB_BM4VGA ); if (!bCorrecto) AfxMessageBox("No se puede cargar el mapa de bits"); // Seleccionar el mapa de bits para el DC en memoria pBitmapAnterior = m_pMemDCPantalla->SelectObject(pBitmapActual); // Guardar el handle del mapa de bits anterior m_hBitmapAnterior = (HBITMAP)pBitmapAnterior->GetSafeHandle(); // Restablecer los recursos propios de la aplicación AfxSetResourceHandle( hRecsApp ); // Dimensiones del mapa de bits fuente BITMAP bm; // estructura de datos BITMAP pBitmapActual->GetObject( sizeof(bm), &bm ); CRect rect(0, 0, bm.bmWidth, bm.bmHeight); dc.DPtoLP( &rect ); // tamaño del mapa de bits en unidades lógicas m_nAncho = rect.Width(); m_nAlto = rect.Height(); }
Como hemos dicho, la función OnDraw miembro de CRecursosView pintará el mapa de bits seleccionado en el contexto de dispositivo de memoria m_pMemDCPantalla cada vez que la ventana se repinte. void CRecursosView::OnDraw(CDC* pDC) { // Ver si hay un mapa de bits presente if (m_pMemDCPantalla == NULL) return; // No hay mapa de bits // Visualizar el mapa de bits
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
479
pDC->BitBlt( // DC destino 0, 0, // origen m_nAncho, // ancho m_nAlto, // alto m_pMemDCPantalla, // DC fuente 0, 0, // origen SRCCOPY ); // operación }
Finalmente, utilice el destructor de la clase CRecursosView para liberar los recursos asignados cuando la vista deje de existir. CRecursosView::~CRecursosView() { // Eliminar el mapa de bits if ( m_hBitmapAnterior ) { CBitmap *pbm = CBitmap::FromHandle( m_hBitmapAnterior ); delete m_pMemDCPantalla->SelectObject( pbm ); } // Eliminar el DC de memoria delete m_pMemDCPantalla; }
OBJETOS COM COMO ALTERNATIVA A LAS DLLs En un capítulo anterior expusimos cómo crear objetos COM utilizando la biblioteca ATL. En esa exposición vimos que Visual C++ proporciona un asistente, ATL COM AppWizard, que permite crear, entre otros, proyectos ATL de tipo DLL.
480
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
El proyecto creado por ATL COM AppWizard está inicialmente sin objetos COM. Para añadir un objeto COM utilizaremos la orden New ATL Object de ClassView. Esta orden abre un asistente que expone los distintos tipos de objetos que podemos añadir. Por ejemplo, en la categoría objetos hay un objeto COM con una funcionalidad mínima (Simple object) que puede ser el idóneo para presentar una alternativa a las DLL que se han expuesto anteriormente. Como ejemplo, vamos a construir un objeto COM como alternativa a la DLL stcdll32.dll que construimos al principio de este capítulo. Para ello: 1. Cree un nuevo proyecto de tipo ATL COM Wizard denominado stccom32. Este proyecto dará lugar al fichero stccom32.dll. 2. Seleccione como tipo de servidor, Dynamic Link Library (DLL). Una vez creado el proyecto, procedemos a añadir un objeto COM simple. Para ello, abra ATL Object Wizard desde ClassView o, ejecutando la orden New ATL Object del menú Insert de Developer Studio, seleccione Objects en el panel izquierdo de ATL Object Wizard y Simple Object en el panel derecho.
Después de pulsar el botón Next en el diálogo anterior, se muestra el diálogo de propiedades que nos permitirá definir el nombre del objeto, de la clase C++ y de la clase COM que soportarán el objeto. Seleccione la página Names y escriba en la caja Short Name el nombre MathCOM. El resto de las cajas se llenarán automáticamente.
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
481
Esqueleto de la aplicación El esqueleto de la aplicación que hemos generado a través de los asistentes ATL COM Wizard y ATL Object Wizard queda resumido en la figura siguiente:
Observamos tres partes bien diferenciadas: las funciones globales, la clase que encapsula el objeto COM y la interfaz que nos da acceso a la funcionalidad del objeto COM. Un objeto COM es parte de una biblioteca dinámica. Por lo tanto, primero habrá que escribir el código que dé lugar a la biblioteca dinámica y después añadiremos a la misma objetos con sus interfaces. Precisamente lo que hace ATL COM Wizard es generar los ficheros necesarios para construir la DLL; en nuestro caso, estos ficheros son básicamente: stccom32.def, stccom32.cpp, stccom32.idl y stccom32.rc. Stccom32.cpp es el fichero principal de la aplicación y contiene las funciones globales: entre ellas cabe destacar la función DllMain, que es el punto de entrada y de salida para la DLL; stccom32.def declara los parámetros del módulo stccom32.dll que se construirá finalmente; stccom32.idl describe las interfaces de los objetos utilizando el lenguaje IDL (Interface Definition Language); y stccom32.rc aporta los recursos para la biblioteca.
482
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Cuando compile el proyecto ATL, el compilador MIDL generará un fichero stccom32.h, el cual define, desde el punto de vista de C++, las interfaces y clases disponibles en el fichero stccom32.idl. Cuando añadimos un objeto COM a la biblioteca, se genera una clase que encapsula el objeto, así como una interfaz de acceso a la funcionalidad del objeto. Este trabajo es realizado por ATL Object Wizard. En nuestro caso fue añadida al proyecto la clase CMathCOM, cuyo código lo podemos localizar en los ficheros MathCOM.h y MathCOM.cpp. Inicialmente dicho objeto no aporta ninguna funcionalidad. Precisamente el trabajo que tenemos que realizar a continuación es proveer al objeto de la interfaz requerida para que cumpla el objetivo del diseño. Concretamente nuestro objetivo requiere los métodos Sumar y Restar.
Añadir métodos Para añadir un método al objeto COM, seleccione ClassView en la ventana WorkSpace, apunte con el ratón a la interfaz IMathCOM y haga clic con el botón derecho del ratón. En el menú contextual que se visualiza, seleccione la orden Add Method. Aparecerá un diálogo como el que se muestra a continuación que le permitirá introducir el nombre y los parámetros del método:
Observe que la función devuelve un valor de tipo HRESULT que generalmente se corresponde con un valor distinto de cero si la función se ejecuta con éxito, o cero en caso contrario. Por este motivo, nuestra función Sumar tiene tres parámetros, los dos primeros para los operandos y el tercero para el resultado. Una vez introducidos los datos que se muestran en la figura anterior, pulse el botón OK. En este instante acaba de añadir la función Sumar como miembro de la clase CMathCOM. Dicha función es accesible a través de la interfaz IMathCOM. A continuación, edite la función Sumar así: STDMETHODIMP CMathCOM::Sumar(double Param1, double Param2, double * Param3) { *Param3 = Param1 + Param2;
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
483
return S_OK; }
Según expusimos en el capítulo de ATL, es aconsejable realizar las siguientes modificaciones en el fichero stccom32.idl: // stccom32.idl // ... typedef enum prop_dispid { DISPID_SUMAR = 1, }PROP_DISPID; // ... interface IMathCOM : IDispatch { [id(DISPID_SUMAR), helpstring("method Sumar")] HRESULT Sumar(double Param1, double Param2, double *Param3); };
A continuación, procediendo de forma análoga, añada la función Restar. Añada también el identificador DISPID_RESTAR. STDMETHODIMP CMathCOM::Restar(double Param1, double Param2, double * Param3) { *Param3 = Param1 - Param2; return S_OK; }
Con esto, hemos finalizado la implementación de la biblioteca dinámica. Compile ahora el proyecto ATL para obtener el fichero stccom32.dll. Cuando Microsoft Developer Studio finaliza la compilación de la biblioteca dinámica, la registra en el registro de Windows. Posteriormente, cuando una aplicación utilice esa biblioteca, Windows recurrirá a su registro para saber en qué directorio se encuentra. Por lo tanto, no sirve cambiar la biblioteca de directorio; ni siquiera al directorio System. ¿Qué tiene que hacer para registrar la biblioteca en otra posición? Dos cosas: desregistrarla de la posición actual y volverla a registrar en la nueva posición, utilizando el programa regsvr32 que se encuentra en el directorio System. Por ejemplo, eligiendo la orden Ejecutar del menú Inicio, puede emitir las siguientes órdenes:
Desregistrar stccom32.dll del directorio actual: regsvr32 /u "C:\Ejemplos\stccom32\Release\stccom32.dll"
484
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
Registrar stccom32.dll en el directorio System. Primero copie stccom32.dll en el directorio System: regsvr32 "C:\Windows\System\stccom32.dll"
Utilización del servidor COM Una parte crítica de COM es cómo interactúan los clientes y servidores. Un servidor COM es cualquier objeto que proporciona servicios a los clientes. Estos servicios aparecen en forma de implementaciones de interfaces COM que pueden ser llamadas por cualquier cliente que puede conseguir un puntero a una de las interfaces en el objeto servidor. Hay dos tipos principales de servidores, in-process (se ejecuta en el espacio de proceso del controlador) y out-of-process (se ejecuta en su propio espacio de proceso). Los servidores in-process son implementados en una biblioteca dinámica (DLL), y los servidores out-of-process son implementados en un archivo EXE. Además, los servidores out-of-process pueden residir en una máquina local o remota. Como ejemplo de utilización del servidor stccom32.dll que acabamos de construir, vamos a reproducir la aplicación ApDll que realizamos al principio de este capítulo, y que ahora guardaremos en un directorio ApDll3. Incluya en el directorio de la aplicación los ficheros stccom32.h (definición de la interfaz IMathCOM) y stccom32_i.c (identificadores COM). En el capítulo de “Componentes Software” vimos que para que un cliente tuviera acceso a un componente software (anteriormente llamado componente OLE y ahora objeto COM), era necesario que dicho cliente iniciara la biblioteca dinámica OLE, para lo cual tenía que invocar a la función AfxOleInit, operación que realizaremos desde InitInstance así: BOOL CApDllApp::InitInstance() { AfxOleInit(); // necesaria para COM // ... }
Como ya sabemos, una aplicación cliente, como es ApDll, puede acceder a un objeto COM solamente a través de un puntero a una de sus interfaces, el cual, a su vez, permitirá al cliente llamar a cualquiera de los métodos que componen la interfaz. Nuestro objeto COM tiene una sola interfaz, IMathCOM. Por lo tanto, lo que tenemos que hacer ahora es obtener un puntero a esta interfaz.
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
485
Obtener un puntero a una interfaz Realmente, una instancia de la interfaz IMathCOM es un puntero a un array de punteros a los métodos especificados en dicha interfaz (para más detalles, consulte el capítulo de “Componentes Software”). Ya que COM no tiene un modelo de clase estricto, hay varias maneras de obtener un puntero a una interfaz de un objeto. Una de ellas puede ser llamar a una función de la API de la biblioteca COM que pueda crear un objeto en función de un identificador de clase (CLSID) y que devuelva un puntero a la interfaz solicitada. Por ejemplo: IMathCOM *m_pIMathCOM; CoCreateInstance(CLSID_MathCOM, NULL, CLSCTX_INPROC_SERVER, IID_IMathCOM, (void **)&m_pIMathCOM))
La función CoCreateInstance crea un objeto sin iniciar, de la clase que tiene el CLSID especificado, en el sistema local (para crear un único objeto en un sistema remoto hay que utilizar CoCreateInstanceEx, y para crear múltiples objetos con el mismo CLSID dispone de la función CoGetClassObject). El primer parámetro es el CLSID asociado con los datos y el código que sería utilizado para crear el objeto. El segundo parámetro, si es NULL indica que el objeto no se construye a partir de otro; si no es NULL, entonces es un puntero a la interfaz IUnknown del objeto agregado. El tercer parámetro indica el contexto en el que se ejecutará el código que manipula el nuevo objeto. El cuarto parámetro es el identificador de la interfaz utilizada para comunicar con el objeto. Y el quinto parámetro es la dirección de la variable puntero que almacenará el puntero a la interfaz requerida. Según esto, añada el siguiente código al fichero ApDllView.h: // ApDllView.h : interface of the CApDllView class // ... // Necesario para utilizar la biblioteca stccom32.dll #include "stccom32.h" // interfaz IMathCOM class CApDllView : public CFormView { // ... protected: IMathCOM *m_pIMathCOM; // ... };
486
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
A continuación, añada a la función OnInitialUpdate el código necesario para obtener el puntero m_pIMathCOM a la interfaz IMathCOM: void CApDllView::OnInitialUpdate() { CFormView::OnInitialUpdate(); m_pIMathCOM = 0; try { if (FAILED(CoCreateInstance(CLSID_MathCOM, NULL, CLSCTX_INPROC_SERVER, IID_IMathCOM, (void **)&m_pIMathCOM))) throw(_T("¿Tiene registrado el objeto COM?")); } catch(_com_error ErrorCom) { // Error COM. throw(ErrorCom.ErrorMessage()); } catch(TCHAR* pChar) { MessageBox( pChar, _T("Error en la aplicación"), MB_ICONERROR); GetParentFrame()->PostMessage(WM_CLOSE); } // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout(); ResizeParentToFit( false ); }
La función CoCreateInstance es una forma breve de conectar con un objeto de la clase asociada con el CLSID especificado, creando una instancia no iniciada, y liberando el objeto de la clase. Así que, encapsula la funcionalidad siguiente: IClassFactory *pCF; CoGetClassObject(CLSID_MathCOM, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pCF); HRESULT hresult = pCF->CreateInstance(NULL, IID_IMathCOM, (void **)&m_pIMathCOM); pCF->Release(); // decrementa el contador de referencias para // la interfaz IClassFactory
Para entender el código anterior tiene que saber que cada objeto COM de un servidor implementa automáticamente una interfaz IClassFactory. Esta clase es la responsable de crear instancias de la clase de objeto COM que la soporta (es
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
487
análoga al operador new de C++). IClassFactory está derivada de IUnknown y contiene los métodos: CreateInstance y LockServer. CreateInstance se utiliza para crear una instancia de una clase COM y LockServer incrementa o decrementa un contador de referencias dentro del servidor COM; cuando este contador es mayor que cero, el servidor no puede ser descargado de memoria. Según lo expuesto, es posible obtener un puntero a una interfaz del objeto COM a través del puntero a su interfaz IClassFactory así: 1. Determinar el identificador de la clase del objeto COM del cual se quiere crear una instancia. En nuestro caso CLSID_MathCOM. 2. Obtener el puntero a la interfaz IClassFactory para el CLSID especificado. IClassFactory *pCF; CoGetClassObject(CLSID_MathCOM, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pCF);
3. Crear una instancia no iniciada de la clase del objeto COM utilizando el método CreateInstance de IClassFactory. La interfaz requerida desde el objeto debe ser aquella que el cliente pide cuando crea una instancia de la clase del objeto COM. De esta forma se obtiene un puntero a dicha interfaz. HRESULT hresult = pCF->CreateInstance(NULL, IID_IMathCOM, (void **)&m_pIMathCOM); pCF->Release();
La macro FAILED permite verificar si existe algún fallo. Si CoCreateInstance devuelve un valor negativo es que ha ocurrido un fallo. Un objeto _com_error es una excepción detectada por los manipuladores de error en los ficheros de cabecera generados a partir de la biblioteca de tipos o por alguna de las clases que soportan COM. La clase _com_error está definida en comdef.h. Por lo tanto, debe incluir este fichero en ApDllView.cpp. Los identificadores de clase (CLSID) y de interfaz (IID) pasados como argumentos en la llamada a la función CoCreateInstance, están definidos en el fichero stccom32_i.c creado por el compilador MIDL cuando generamos la biblioteca dinámica stccom32.dll. Por lo tanto, incluya este fichero en ApDllView.cpp: // ApDllView.cpp : implementation of the CApDllView class // ... #include "ApDllDoc.h" #include "ApDllView.h" // Necesario para utilizar la biblioteca stccom32.dll
488
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
#include "stccom32_i.c" // identificadores COM #include "comdef.h" // necesario para _com_error
Llamando a las funciones de la interfaz Obtenido el puntero a la interfaz y suponiendo que ya ha reconstruido la aplicación ApDll para que muestre la siguiente interfaz gráfica, el siguiente paso es asignar funcionalidad a los botones Sumar y Restar.
Análogamente a como procedimos cuando desarrollamos esta aplicación al principio de este capítulo, edite las siguientes funciones que tienen que ejecutarse como respuesta al evento clic sobre cada uno de los botones. void CApDllView::OnSumar() { // Sumar UpdateData( true ); // actualizar variables miembro m_pIMathCOM->Sumar( m_Operando1, m_Operando2, &m_Resultado ); UpdateData( false ); // actualizar cajas de texto } void CApDllView::OnRestar() { // Restar UpdateData( true ); // actualizar variables miembro m_pIMathCOM->Restar( m_Operando1, m_Operando2, &m_Resultado ); UpdateData( false ); // actualizar cajas de texto }
Observe que para realizar las operaciones de sumar y restar, llamamos a las funciones Sumar y Restar de la biblioteca stccom32.dll a través del puntero m_pIMathCOM que acabamos de obtener. Los prototipos de estas funciones están declarados en el fichero stccom32.h. Ahora ya puede compilar la aplicación. En este caso no es necesario indicarle al enlazador la biblioteca que tiene que utilizar para enlazar estas funciones, porque, como dijimos anteriormente, esta información la obtiene el sistema del registro de Windows por tratarse de un objeto COM registrado.
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
489
Clase _com_ptr_t Otra alternativa para obtener un puntero a una interfaz de un objeto COM es utilizar la funcionalidad de la plantilla de clase _com_ptr_t definida en comdef.h. Un objeto _com_ptr_t encapsula un puntero a una interfaz COM (smart pointer). La plantilla _com_ptr_t manipula la asignación y liberación de los recursos necesarios, a través de llamadas a las funciones miembro de la interfaz IUnknown: QueryInterface, AddRef, y Release, lo cual significa que estamos liberados de realizar este tipo de llamadas. Un puntero de este tipo es un objeto de una clase, obtenida a partir de la plantilla _com_ptr_t, particularizada para una determinada interfaz COM. Esta clase se obtiene a través de la macro: _COM_SMARTPTR_TYPEDEF(IMiInterfaz, __uuidof(IMiInterfaz));
Esta macro toma como parámetros el nombre de la interfaz y su IID y construye una clase particularizada para dicha interfaz de nombre, el nombre de la interfaz más el sufijo Ptr. Por ejemplo, la línea anterior daría lugar a la clase IMiInterfazPtr. A su vez, la macro __uuidof recupera el GUID asociado con el parámetro especificado. Como ejemplo, vamos a modificar la aplicación ApDll creada en el ejemplo anterior para que ahora utilice la plantilla _com_ptr_t para obtener el puntero a la interfaz IMathCOM. Esta versión la guardaremos en el directorio ApDll4. En este caso procederemos así: 1. Asegúrese de que InitInstance invoca a AfxOleInit: BOOL CApDllApp::InitInstance() { AfxOleInit(); // necesaria para COM // ... }
2. Añada al fichero ApDllView.h el código indicado a continuación: // ApDllView.h : interface of the CApDllView class // ... #include "stccom32.h" // interfaz IMathCOM #include "comdef.h" // necesario para _com_ptr_t y _com_error // La siguiente macro define la clase IMathCOMPtr // a partir de la plantilla _com_ptr_t _COM_SMARTPTR_TYPEDEF(IMathCOM, __uuidof(IMathCOM));
490
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
class CApDllView : public CFormView { // ... protected: // m_pIMathCOM encapsula el puntero a la interfaz IMathCOM IMathCOMPtr m_pIMathCOM; // ... };
3. Añada al fichero ApDllView.cpp el código indicado a continuación: // ApDllView.cpp : implementation of the CApDllView class // ... #include "stccom32_i.c" // identificadores COM de IMathCOM // ... void CApDllView::OnInitialUpdate() { CFormView::OnInitialUpdate(); try { if ( FAILED(m_pIMathCOM.CreateInstance(CLSID_MathCOM))) throw(_T("¿Tiene registrado el objeto COM?")); } catch(_com_error ErrorCom) { // Error COM. throw(ErrorCom.ErrorMessage()); } catch(TCHAR* pChar) { MessageBox( pChar, _T("Error en la aplicación"), MB_ICONERROR); GetParentFrame()->PostMessage(WM_CLOSE); } // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout(); ResizeParentToFit( false ); }
4. Edite las funciones OnSumar y OnRestar de la misma forma que lo hizo anteriormente. La función CreateInstance miembro de _com_ptr_t llama a CoCreateInstance para crear una instancia de un objeto de la clase asociada con el CLSID especificado y obtener así el puntero a su interfaz IMathCOM, encapsulado en el objeto m_pIMathCOM de la clase IMathCOMPtr derivada de la plantilla
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
491
_com_ptr_t. También es llamada la función miembro Release para decrementar el contador de referencias del puntero previamente encapsulado. Esta rutina devuelve un valor HRESULT para indicar el éxito o fallo de la operación. Observe que las líneas: IMathCOMPtr m_pIMathCOM; m_pIMathCOM.CreateInstance(CLSID_MathCOM);
equivalen a: IMathCOMPtr m_pIMathCOM(CLSID_MathCOM);
Como m_pIMathCOM es un objeto de la clase IMathCOMPtr, una llamada de la forma: m_pIMathCOM->Sumar( m_Operando1, m_Operando2, &m_Resultado );
invoca a la función miembro operator->() (sobrecarga al operador ->) que devuelve el puntero a la interfaz, encapsulado en dicho objeto.
Directriz #import Otra alternativa para obtener un puntero a una interfaz de un objeto COM es utilizar la directriz #import. Esta directriz permite incorporar información de una biblioteca de tipos. El contenido de la biblioteca de tipos es convertido en clases C++ que describen las interfaces COM. Su sintaxis es de la forma siguiente: #import "fichero" [atributos] #import [atributos]
donde fichero es el nombre del fichero que contiene la información de la biblioteca de tipos. El fichero puede ser alguno de los tipos siguientes: Una biblioteca de tipos (fichero .TLB o .ODL). Un fichero ejecutable (.EXE). Una biblioteca dinámica que contenga los recursos de la biblioteca de tipos (tal como un .OCX o un .DLL). Un documento compuesto que posea la biblioteca de tipos. Cualquier otro formato de fichero que pueda ser admitido por la función de la API LoadTypeLib. Los atributos especificados en #import indican al compilador acciones especiales que debe tomar. Por ejemplo, los contenidos de la biblioteca de tipos importada a través de un fichero de cabecera son normalmente definidos en un espacio
492
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
de nombres (namespace) cuyo nombre se especifica en la sentencia library en el fichero IDL original. Para indicarle al compilador que no genere de nuevo este espacio de nombres, hay que especificar el atributo no_namespace: #import "stccom32.tlb" no_namespace
La directriz #import genera dos ficheros de cabecera, con el mismo nombre de la biblioteca y extensiones .TLH y .TLI, que reconstruyen la biblioteca de tipos en clases C++. Por ejemplo, la directriz anterior generaría stccom32.tlh y stccom32.tli. Estos ficheros pueden ser incluidos en la aplicación cliente, utilizando una directriz #include, en el lugar donde sean necesarios. El código del fichero .TLH de forma resumida hace lo siguiente:
Incluye el fichero de cabecera comdef.h que contiene declaraciones como las correspondientes a _com_ptr_t y _com_error.
Define las interfaces y clases de los objetos incluidas en el fichero IDL original. Por ejemplo IMathCOM y MathCOM.
Invoca a la macro _COM_SMARTPTR_TYPEDEF que permite generar una clase (de la que hemos hablado en el apartado anterior) a partir de la plantilla _com_ptr_t, particularizada para una determinada interfaz COM. Un objeto de esta clase envuelve un puntero a dicha interfaz.
Incluye el fichero .TLI que contiene las definiciones de las funciones miembro de las interfaces.
Ambos ficheros, .TLH y .TLI, una vez generados son leídos y compilados por el compilador como si se hubiera incluido en el código de la aplicación la directriz #include para el fichero .TLH. Por ejemplo: #include "stccom32.tlh"
Como ejemplo, vamos a modificar la aplicación ApDll creada en el ejemplo anterior para que ahora utilice la directriz #import para obtener el puntero a la interfaz IMathCOM. Esta versión la guardaremos en el directorio ApDll5. En este caso procederemos así: 1. Copie en el directorio de la aplicación cliente sólo el fichero stccom32.tlb. 2. Asegúrese de que InitInstance invoca a AfxOleInit: BOOL CApDllApp::InitInstance()
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
493
{ AfxOleInit(); // necesaria para COM // ... }
3. Añada al fichero ApDllView.h el código indicado a continuación: // ApDllView.h : interface of the CApDllView class // ... // Importar la biblioteca de tipos. Permite, a través de la // macro _COM_SMARTPTR_TYPEDEF, definir la clase IMathCOMPtr // a partir de la plantilla _com_ptr_t #import "stccom32.tlb" no_namespace class CApDllView : public CFormView { // ... protected: // m_pIMathCOM encapsula el puntero a la interfaz IMathCOM IMathCOMPtr m_pIMathCOM; // ... };
4. Añada al fichero ApDllView.cpp las mismas funciones OnInitialUpdate, OnSumar y OnRestar que utilizó en el apartado anterior y realice sobre la función OnInitialUpdate la siguiente modificación: sustituya CLSID_MathCOM por la expresión __uuidof(MathCOM). El resultado es el mismo, el CLSID de la clase pero sin tener que incluir otro fichero de cabecera. void CApDllView::OnInitialUpdate() { CFormView::OnInitialUpdate(); try { if ( FAILED(m_pIMathCOM.CreateInstance(__uuidof(MathCOM))) throw(_T("¿Tiene registrado el objeto COM?")); } catch(_com_error ErrorCom) { // Error COM. throw(ErrorCom.ErrorMessage()); } catch(TCHAR* pChar) { MessageBox( pChar, _T("Error en la aplicación"), MB_ICONERROR); GetParentFrame()->PostMessage(WM_CLOSE); } // Ajustar el tamaño de la ventana marco a la vista GetParentFrame()->RecalcLayout();
494
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
ResizeParentToFit( false ); }
Añadir otra interfaz En algunas ocasiones necesitaremos añadir una nueva interfaz a un objeto COM existente. En este apartado vamos a exponer los pasos que debe seguir para realizar este proceso. Vamos a realizar un ejemplo partiendo del objeto COM que creamos anteriormente y que almacenamos en la biblioteca stccom32.dll. Cargue, entonces, el proyecto stccom32 y abra el fichero stccom32.idl (en el disco que acompaña al libro, este proyecto está almacenado en el directorio interfaz2 de este capítulo). A continuación añada una nueva interfaz IMathExCOM derivada de IUnknown. Esto requiere generar un identificador global único para asignárselo al atributo uuid de la interfaz. Genere el UUID (universally unique identifier) utilizando el programa guidgen.exe proporcionado por Visual C++. Finalmente, añada a la definición coclass MathCOM de la sentencia library el nombre de la nueva interfaz. La sentencia library contiene toda la información que el compilador MIDL necesita para generar la biblioteca de tipos. // stccom32.idl : IDL source for stccom32.dll // // This file will be processed by the MIDL tool to // produce the type library (stccom32.tlb) and marshalling code. import "oaidl.idl"; import "ocidl.idl"; typedef enum prop_dispid { DISPID_SUMAR = 1, DISPID_RESTAR = 2, }PROP_DISPID; [ object, uuid(934D4B9F-1C0B-11D2-8197-896206EF2C3A), dual, helpstring("IMathCOM Interface"), pointer_default(unique) ] interface IMathCOM : IDispatch { [id(DISPID_SUMAR), helpstring("method Sumar")] HRESULT Sumar( double Param1, double Param2, double *Param3); [id(DISPID_RESTAR), helpstring("method Restar")] HRESULT Restar( double Param1, double Param2, double *Param3); };
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
495
[ object, uuid(AF9CD1C0-2023-11d2-8197-F60ED797973D), dual, helpstring("IMathExCOM Interface"), pointer_default(unique) ] interface IMathExCOM : IUnknown { }; [ uuid(934D4B92-1C0B-11D2-8197-896206EF2C3A), version(1.0), helpstring("stccom32 1.0 Type Library") ] library STCCOM32Lib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(934D4BA0-1C0B-11D2-8197-896206EF2C3A), helpstring("MathCOM Class") ] coclass MathCOM { [default] interface IMathCOM; interface IMathExCOM; }; };
El siguiente paso es editar el fichero MathCOM.h para especificar que la clase del objeto CMathCOM se derivará también de IMathExCOM. A su vez, sabemos que el mapa COM conecta la interfaz IUnknown con todas las interfaces soportadas por el objeto. Todos los objetos COM deben implementar una interfaz IUnknown para que a través de su función miembro QueryInterface podamos determinar qué otras interfaces soporta el control y obtener, cuando sea preciso, un puntero a ellas. Por lo tanto, debemos añadir también a este mapa una entrada que especifique la nueva interfaz. // MathCOM.h : Declaration of the CMathCOM #ifndef __MATHCOM_H_ #define __MATHCOM_H_ #include "resource.h"
// main symbols
496
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
////////////////////////////////////////////////////////////////// // CMathCOM class ATL_NO_VTABLE CMathCOM : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IMathExCOM { public: CMathCOM() { } DECLARE_REGISTRY_RESOURCEID(IDR_MATHCOM) BEGIN_COM_MAP(CMathCOM) COM_INTERFACE_ENTRY(IMathCOM) COM_INTERFACE_ENTRY(IMathExCOM) COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() // IMathExCOM public: // IMathCOM public: STDMETHOD(Restar)(double Param1, double Param2, double *Param3); STDMETHOD(Sumar)(double Param1, double Param2, double *Param3); }; #endif //__MATHCOM_H_
Con esto ha finalizado el proceso de añadir una nueva interfaz. Ahora, desde ClassView, puede añadir los métodos que crea necesarios, igual que hizo cuando añadió los métodos Sumar y Restar a la interfaz IMathCom. Como ejemplo, añada los métodos Multiplicar y Dividir que se muestran a continuación: typedef enum prop_dispid { DISPID_SUMAR = 1, DISPID_RESTAR = 2, DISPID_MULTIPLICAR = 3, DISPID_DIVIDIR = 4, }PROP_DISPID; // ... interface IMathExCOM : IUnknown { [id(DISPID_MULTIPLICAR), helpstring("method Multiplicar")] HRESULT Multiplicar( double Param1, double Param2, double *Param3); [id(DISPID_DIVIDIR), helpstring("method Dividir")]
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
497
HRESULT Dividir( double Param1, double Param2, double *Param3); };
A continuación, edite las funciones que acaba de añadir: STDMETHODIMP CMathCOM::Multiplicar(double Param1, double Param2, double * Param3) { *Param3 = Param1 * Param2; return S_OK; } STDMETHODIMP CMathCOM::Dividir(double Param1, double Param2, double * Param3) { *Param3 = Param1 / Param2; return S_OK; }
Después de esto, ha finalizado la implementación de la nueva interfaz. Ahora, puede compilar el proyecto. Para probar la nueva interfaz de nuestro objeto COM, vamos a modificar el proyecto ApDll anterior (en el disco que acompaña al libro, el proyecto resultante está almacenado en el directorio interfaz2\ApDll6 de este capítulo). La idea es añadir a la interfaz gráfica dos nuevos botones, Multiplicar y Dividir, y asociarles con las funciones manipuladoras correspondientes, para que utilizando los métodos Multiplicar y Dividir de la interfaz IMathExCOM realicen las operaciones esperadas. Según lo expuesto, siga los siguientes pasos:
Copie el fichero stccom32.tbl que acaba de generar en el proyecto stccom32, en el directorio ApDll6 de la aplicación.
Abra el editor de recursos y añada dos nuevos botones, Multiplicar y Dividir, a la interfaz gráfica.
Edite las funciones manipuladoras del evento clic para estos botones: void CApDllView::OnMultiplicar() { // Multiplicar UpdateData( true ); // actualizar variables miembro m_pIMathExCOM->Multiplicar( m_Operando1, m_Operando2, &m_Resultado ); UpdateData( false ); // actualizar cajas de texto } void CApDllView::OnDividir()
498
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
{ // Dividir UpdateData( true ); // actualizar variables miembro m_pIMathExCOM->Dividir( m_Operando1, m_Operando2, &m_Resultado ); UpdateData( false ); // actualizar cajas de texto }
Defina, análogamente a como definió el objeto m_pIMathCOM, el objeto m_pIMathExCOM como miembro de la clase CApDllView, para que a través de él podamos referenciar la nueva interfaz IMathExCOM del objeto COM. // Importar la biblioteca de tipos #import "stccom32.tlb" no_namespace class CApDllView : public CFormView { // ... protected: // m_pIMathCOM encapsula el puntero a la interfaz IMathCOM IMathCOMPtr m_pIMathCOM; IMathExCOMPtr m_pIMathExCOM; // puntero a la interfaz IMathExCOM // ... };
Inicie el objeto m_pIMathExCOM en la función OnInitialUpdate de la clase CApDllView para que permita acceder a la interfaz IMathExCOM. void CApDllView::OnInitialUpdate() { CFormView::OnInitialUpdate(); try { if ( FAILED(m_pIMathCOM.CreateInstance(__uuidof(MathCOM)))) throw(_T("¿Tiene registrado el objeto COM?")); m_pIMathExCOM = m_pIMathCOM; // llama a QueryInterface } // ... }
La sentencia m_pIMathExCOM = m_pIMathCOM invoca a la función miembro operator= de la clase de los objetos, que a su vez invoca a la función miembro QueryInterface que permite obtener en su segundo argumento un puntero a la interfaz identificada por su primer argumento. Para entenderlo mejor, el código que se muestra a continuación indica cómo utilizar QueryInterface para obtener un puntero a la interfaz IMathExCOM: IMathExCOM *p; m_pIMathCOM->QueryInterface(m_pIMathExCOM.GetIID(), (void **)(&p)); p->Dividir( m_Operando1, m_Operando2, &m_Resultado );
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
499
La función GetIID obtiene el identificador de la interfaz que representa el objeto m_pIMathExCOM.
Aplicación de tipo consola Seguramente que en alguna ocasión necesitará utilizar una biblioteca COM en una aplicación de tipo consola. La forma de proceder en estos casos es similar a la que acabamos de exponer para una aplicación basada en la biblioteca MFC. Como ejemplo, vamos a construir un proyecto denominado ApDll7 de tipo Win32 Console Application que utilice la biblioteca stccom32.dll para realizar las operaciones que ésta permite con la funcionalidad que expone a través de sus interfaces. Cuando haya construido el proyecto, añada un fichero .cpp denominado ApDll y edítelo como se indica a continuación: #include "iostream.h" void math(void); int menu(void); #import "stccom32.tlb" no_namespace int main() { OleInitialize(NULL); // iniciar la biblioteca COM math(); OleUninitialize(); // cerrar la biblioteca COM return 0; } void math() { // pIMathCOM encapsula el puntero a la interfaz IMathCOM IMathCOMPtr pIMathCOM(__uuidof(MathCOM)); // Obtener un puntero a la interfaz IMathExCOM IMathExCOMPtr pIMathExCOM = pIMathCOM; if (pIMathCOM == 0 || pIMathExCOM == 0) return; double dato1, dato2, resultado; int operacion; while(1) { operacion = menu(); if (operacion != 5) { cout << "dato 1: "; cin >> dato1; cout << "dato 2: ";
500
VISUAL C++. PROGRAMACIÓN AVANZADA EN WIN32
cin >> dato2; switch (operacion) { case 1: pIMathCOM->Sumar(dato1, dato2, &resultado); break; case 2: pIMathCOM->Restar(dato1, dato2, &resultado); break; case 3: pIMathExCOM->Multiplicar(dato1, dato2, &resultado); break; case 4: pIMathExCOM->Dividir(dato1, dato2, &resultado); break; } cout << "resultado: " << resultado << endl << endl; } else break; } } int menu(void) { int op; do { cout << "1. Sumar\n"; cout << "2. Restar\n"; cout << "3. Multiplicar\n"; cout << "4. Dividir\n"; cout << "5. Salir\n\n"; cout << "Seleccione la opción deseada: "; cin >> op; } while (op < 1 || op > 5); return op; }
Observe el código sombreado. Comprobará que la forma de proceder no difiere casi en nada de la expuesta en el apartado anterior. La directriz #import importa la biblioteca de tipos stccom32, las funciones OleInitialize y OleUninitialize hacen el trabajo que hacía AfxOleInit, y los punteros para acceder a las interfaces de nuestro objeto COM se han obtenido a partir de los objetos pIMathCOM y pIMathExCOM de las clase IMathCOMPtr e IMathExCOMPtr proporcionadas por #import. Compile la aplicación, ejecútela y compruebe que los resultados son los esperados.
CAPÍTULO 8: BIBLIOTECAS DINÁMICAS
501
Faltan páginas...
Faltan páginas...
Del mismo autor ● Curso de programación con PASCAL ● Curso de programación GW BASIC/BASICA ● Manual para TURBO BASIC Guía del programador ● Manual para Quick C 2 Guía del programador ● Manual para Quick BASIC 4.5 Guía del programador ● Curso de programación Microsoft COBOL ● Enciclopedia del lenguaje C ● Curso de programación QBASIC y MS-DOS 5 ● Curso de programación RM/COBOL-85 ● El abecé de MS-DOS 6 ● Microsoft Visual C ++ (ver. 1.5x de 16 bits) Aplicaciones para Windows ● Microsoft Visual C ++ Aplicaciones para Win32 (2ª edición) ● Microsoft Visual C ++ Programación avanzada en Win32 ● Visual Basic 6 Curso de programación (2ª edición) ● Enciclopedia de Microsoft Visual Basic 6 ● El lenguaje de programación Java ● El lenguaje de programación C#
ISBN: 978-84-86381-36-3 224 págs. ISBN: 978-84-86381-87-5 320 págs. ISBN: 978-84-86381-43-1 444 págs. ISBN: 978-84-86381-65-3 540 págs. ISBN: 978-84-86381-74-5 496 págs. ISBN: 978-84-7897-001-8 480 págs. ISBN: 978-84-7897-053-7 888 págs. ISBN: 978-84-7897-059-9 384 págs. ISBN: 978-84-7897-070-4 396 págs. ISBN: 978-84-7897-114-5 224 págs. ISBN: 978-84-7897-180-0 846 págs. + 2 disquetes ISBN: 978-84-7897-561-7 792 págs. + disquete ISBN: 978-84-7897-344-6 888 págs. + CD-ROM ISBN: 978-84-7897-357-6 528 págs. + disquete ISBN: 978-84-7897-386-6 1.072 págs. + CD-ROM ISBN: 978-84-7897-485-6 320 págs. + CD-ROM ISBN: 978-84-7897-500-6 320 págs. + CD-ROM
Del mismo autor ● El lenguaje de programación Visual Basic.NET ● Java 2 Lenguaje y aplicaciones ● Programación orientada a objetos con C ++ (4ª edición) ● C/C++ Curso de programación (3ª edición) ● Microsoft C# Lenguaje y aplicaciones (2ª edición) ● Java 2. Interfaces gráficas y aplicaciones para Internet (3ª edición) ● Aplicaciones .Net multiplataforma (Proyecto Mono) ● Enciclopedia del lenguaje C ++ (2ª edición) ● Enciclopedia de Microsoft Visual C# (3ª edición) ● Enciclopedia de Microsoft Visual Basic (2ª edición) ● Microsoft Visual Basic .NET Lenguaje y aplicaciones (3ª edición) ● Java 2 Curso de programación (4ª edición) ● Microsoft C# Curso de programación (2ª edición) ● Visual C#. Interfaces gráficas y aplicaciones para Internet con WPF, WCF y Silverlight ● Visual Basic. Interfaces gráficas y aplicaciones para Internet con WPF, WCF y Silverlight
ISBN: 978-84-7897-525-9 464 págs. + CD-ROM ISBN: 978-84-7897-745-1 392 págs. + CD-ROM ISBN: 978-84-7897-761-1 648 págs. + CD-ROM ISBN: 978-84-7897-762-8 708 págs. + CD-ROM ISBN: 978-84-7897-813-7 520 págs. + CD-ROM ISBN: 978-84-7897-859-5 718 págs. + CD-ROM ISBN: 978-84-7897-880-9 212 págs. + CD-ROM ISBN: 978-84-7897-915-8 902 págs. + CD-ROM ISBN: 978-84-7897-986-8 1.110 págs. + CD-ROM ISBN: 978-84-7897-987-5 1.090 págs. + CD-ROM ISBN: 978-84-9964-020-4 520 págs. + CD-ROM ISBN: 978-84-9964-032-7 820 págs. + CD-ROM ISBN: 978-84-9964-068-6 850 págs. + CD-ROM ISBN: 978-84-9964-203-1 956 págs. + CD-ROM ISBN: 978-84-9964-204-8 938 págs. + CD-ROM