Você está na página 1de 268

2011

C++ - Programacin GUI con Qt 4

Jasmin Blanchete & Mark Summerfield

Zona Qt
01/01/2011

6. Manejo de Layouts

Manejo de Layouts Esta es una traduccin libre y no oficial realizada por Zona Qt nicamente con6. fines educativos

C++ - Programacin GUI con Qt 4

Jasmin Blanchete Mark Summerfield

Esta es una traduccin libre y no oficial realizada por Zona Qt nicamente con fines educativos

6. Manejo de Layouts

Contenido
Parte II Qt Intermedio ................................................................................................................... 7 6. Manejo de Layouts ............................................................................................................ 8 Organizando Widgets en un Formulario ........................................................................ 8 Layouts Apilados .......................................................................................................... 13 Los Splitters (divisores o separadores) ........................................................................ 15 reas de Desplazamiento ............................................................................................ 18 Barras de Herramientas y Dock Widgets ..................................................................... 20 Interfaz de Mltiples Documentos .............................................................................. 22 7. Procesamiento de Eventos .............................................................................................. 30 Reimplementar Manejadores de Eventos ................................................................... 30 Instalar Filtros de Eventos ........................................................................................... 35 Evitar Bloqueos Durante Procesamientos Intensivos .................................................. 37 8. Grficos En 2 y 3 Dimensiones......................................................................................... 40 Dibujando con QPainter............................................................................................... 40 Transformaciones ........................................................................................................ 45 Renderizado de Alta Calidad con QImage ................................................................... 51 Impresin ..................................................................................................................... 53 Grficos con OpenGL ................................................................................................... 61 9. Arrastrar y Soltar ............................................................................................................. 66 Habilitando el Mecanismo de Arrastrar y Soltar (drag and drop) ............................... 66 Soporte de Tipos de Arrastre Personalizados .............................................................. 70 Manejo del Portapapeles ............................................................................................. 74 10. Clases para Visualizar Elementos (Clases Item View) .................................................... 76 Usando las Clases Item View de Qt.............................................................................. 77 Usando Modelos Predefinidos..................................................................................... 83 Implementando Modelos Personalizados ................................................................... 87 Implementando Delegados Personalizados ................................................................ 99 11. Clases Contenedoras ................................................................................................... 104 Contenedores Secuenciales ....................................................................................... 105 Contenedores Asociativos ......................................................................................... 112 Algoritmos Genricos ................................................................................................ 114 Cadenas de Textos, Arreglos de Bytes y Variantes (Strings, Byte Arrays y Variants) 116 12. Entrada/Salida ............................................................................................................. 122 Lectura y Escritura de Datos Binarios ........................................................................ 123 Lectura y Escritura de Archivos de Texto ................................................................... 127 Navegar por Directorios............................................................................................. 132

6. Manejo de Layouts

Incrustando Recursos ................................................................................................ 133 Comunicacin entre Procesos ................................................................................... 134 13. Bases de Datos............................................................................................................. 139 Conectando y Consultando ........................................................................................ 140 Presentando Datos en Forma Tabular ....................................................................... 145 Implementando Formularios Master-Detail .............................................................. 149 14. Redes ........................................................................................................................... 155 Escribiendo Clientes FTP ............................................................................................ 155 Escribiendo Clientes HTTP ......................................................................................... 163 Escribiendo Aplicaciones Clientes-Servidores TCP .................................................... 165 Enviando y Recibiendo Datagramas UDP .................................................................. 174 15. XML .............................................................................................................................. 178 Leyendo XML con SAX................................................................................................ 178 Leyendo XML con DOM ............................................................................................. 182 Ecribiendo XML .......................................................................................................... 185 16. Proporcionando Ayuda En Linea ................................................................................. 188 Ayudas: Tooltips, Status Tips, y Whats This? ........................................................ 188 Usando QTextBrowser como un Mecanismo de Ayuda ............................................ 190 Usando el Qt Assistant como una Poderosa Ayuda En Lnea .................................... 193 Parte III Qt Avanzado ................................................................................................................ 195 17. Internacionalizacin .................................................................................................... 196 Trabajando con Unicode ............................................................................................ 197 Haciendo Aplicaciones que Acepten Traducciones ................................................... 201 Cambio Dinmico del Lenguaje ................................................................................. 206 Traduciendo Aplicaciones .......................................................................................... 210 18. Multithreading ............................................................................................................. 214 Creando Threads ........................................................................................................ 214 Sincronizando Threads............................................................................................... 217 Comunicndose con el Thread Principal.................................................................... 223 Usando Clases Qt en Threads Secundarios................................................................ 227 19. Creando Plugins ........................................................................................................... 229 Extendiendo Qt con Plugins ....................................................................................... 229 Haciendo Aplicaciones que Acepten Plugins ............................................................. 237 Escribiendo Plugins para Aplicaciones ....................................................................... 240 20. Caractersticas Especficas de Plataformas .................................................................. 243 Creando Interfaces con APIs Nativas ........................................................................ 243 Usando Activex en Windows ..................................................................................... 247 Manejando la Administracin de Sesin en X11 ....................................................... 257

6. Manejo de Layouts

21. Programacin Embebida ............................................................................................. 264 Iniciando con Qtopia .................................................................................................. 264 Personalizando Qtopia Core ...................................................................................... 266 Glosario.............................................................................................................................. 268

6. Manejo de Layouts

Parte II Qt Intermedio

6. Manejo de Layouts

6. Manejo de Layouts

Organizando Widgets en un Formulario Layouts Apilados Los Splitters (divisores o separadores) reas de Desplazamiento Barras de Herramientas y Dock Widgets Interfaz de Mltiples Documentos

A cada widget que colocamos en un formulario se le debe dar un tamao y una posicin adecuada. Al proceso de organizar el tamao y la posicin de los widgets sobre un formulario se lo denomina en ingls "layout". Qt provee varias clases que nos ayudarn con esta tarea: QHBoxLayout, QVBoxLayout, QGridLayout y QStackLayout. Estas clases son cmodas y fciles de usar, al punto tal, que casi todos los desarrolladores las emplean, ya sea directamente en el cdigo fuente o a travs del diseador visual Qt Designer. Otra razn para usar estas clases, es que nos aseguran que los formularios se adaptarn automticamente a las diferentes fuentes, idiomas y plataformas usadas. Si el usuario cambia alguna configuracin de fuentes del sistema, los formularios de la aplicacin respondern inmediatamente, cambiando su tamao si es necesario. Y si se traduce la interfaz del programa a otros lenguajes, estas clases considerarn el contenido del texto traducido del widget para evitar su truncamiento. Otras clases que nos ayudan a organizar a los widgets son: QSplitter, QScrollArea, QMainWindow, y QWorkspace. Lo que tienen en comn estas clases es que proveen un mecanismo de disposicin flexible que el usuario puede manipular a su antojo. Por ejemplo QSplitter ofrece una barra divisora que se puede arrastrar para cambiar el tamao del widget y QWorkspace provee soporte para MDI (Interfaz de Mltiples Documentos), una manera de mostrar simultneamente varios documentos dentro de la ventana principal de la aplicacin. Estas clases se incluyen en este captulo porque se usan muy a menudo como alternativas a las propias clases de layout.

Organizando Widgets en un Formulario


Hay tres formas bsicas de organizar a los widgets sobre un formulario: el posicionamiento absoluto, el layout manual y los administradores de layout. Iremos repasando cada uno de estos enfoques por turnos, usando el dilogo Buscar Archivo mostrado en la Figura 6.1 como ejemplo.

6. Manejo de Layouts

Figura 6.1. El dilogo Buscar Archivo

El posicionamiento absoluto es la tcnica ms engorrosa para acomodar nuestros widgets. Esta tcnica se basa en asignar tamaos y posiciones fijas a los widgets y al formulario. El constructor de DialogoBuscarArchivo lucira de la siguiente manera: DialogoBuscarArchivo::DialogoBuscarArchivo(QWidget *parent) :QDialog(parent) { labelNombre->setGeometry(9, 9, 50, 25); lineEditNombre->setGeometry(65, 9, 200, 25); labelBuscarEn->setGeometry(9, 40, 50, 25); lineEditBuscarEn->setGeometry(65, 40, 200, 25); checkBoxSubdirectorios->setGeometry(9, 71, 256, 23); tableWidget->setGeometry(9, 100, 256, 100); labelMensaje->setGeometry(9, 206, 256, 25); botonBuscar->setGeometry(271, 9, 85, 32); botonParar->setGeometry(271, 47, 85, 32); botonCerrar->setGeometry(271, 84, 85, 32); botonAyuda->setGeometry(271, 199, 85, 32); setWindowTitle(tr("Buscar Archivos o Carpetas")); setFixedSize(365, 240); } El posicionamiento absoluto tiene varias desventajas: El usuario no puede cambiar el tamao de la ventana. Algunos textos pueden ser truncados si el usuario elige una fuente demasiado grande o si la aplicacin es traducida a otro lenguaje. El widget podra tener un tamao inapropiado en algunos estilos. Las posiciones y los tamaos deben ser calculados manualmente. Esto es tedioso, propenso a errores, y hace que el mantenimiento sea difcil. Una alternativa al posicionamiento absoluto es el layout manual. Con esta tcnica los widgets aun se colocan en posiciones absolutas, pero sus tamaos se mantienen proporcionales al tamao de la ventana en vez de ser invariantes. Para establecer los tamaos de los widgets del formulario, se re implementa la funcin resizeEvent() del mismo: DialogoBuscarArchivo::DialogoBuscarArchivo(QWidget *parent) :QDialog(parent) {

6. Manejo de Layouts

10

setMinimumSize(265, 190); resize(365, 240); } void DialogoBuscarArchivo::resizeEvent(QResizeEvent * /* event */) { int anchoExtra = width() - minimumWidth(); int altoExtra = height() - minimumHeight(); labelNombre->setGeometry(9, 9, 50, 25); lineEditNombre->setGeometry(65, 9, 100 + anchoExtra, 25); labelBuscarEn->setGeometry(9, 40, 50, 25); lineEditBuscarEn->setGeometry(65, 40, 100 + anchoExtra, 25); checkSubdirectorios->setGeometry(9, 71, 156 + anchoExtra, 23); tableWidget->setGeometry(9, 100, 156 + anchoExtra, 50 + altoExtra); labelMensaje->setGeometry(9, 156 + altoExtra, 156 + anchoExtra, 25); botonBuscar->setGeometry(171 + anchoExtra, 9, 85, 32); botonParar->setGeometry(171 + anchoExtra, 47, 85, 32); botonCerrar->setGeometry(171 + anchoExtra, 84, 85, 32); botonAyuda->setGeometry(171 + anchoExtra, 149 + altoExtra, 85, 32); } En el constructor de DialogoBuscarArchivo se establece el tamao mnimo del formulario a 265 x 190 y el tamao inicial a 365 x 240. En el manejador resizeEvent() asignamos una cantidad de espacio extra a los widgets que queremos que crezcan. Esto nos asegura que el formulario mantenga la forma cuando el usuario cambie su tamao. Al igual que en el posicionamiento absoluto, en el layout manual se requiere hacer algunos clculos por parte del programador. Escribir este tipo de cdigo es agotador, especialmente si el diseo del formulario cambia. Y todava existe el riesgo de que se trunquen los textos. Podemos evitar este riesgo tomando en cuenta los tamaos recomendados para los widgets del formulario, pero eso complicara el cdigo aun ms. Figura 6.2. Redimensionando un dialogo escalable

La solucin ms conveniente para organizar los widgets en un formulario es usar los administradores de layout provistos por Qt. Estos nos proporcionan parmetros por defecto para cada tipo de widget y toman en cuenta el tamao recomendado para cada widget, que a su vez, depende de la fuente, el estilo o el contenido del widget. Tambin respetan los tamaos mnimos y mximos establecidos, y automticamente ajustan el diseo en respuesta a cambios de: fuentes, contenido o tamao de la ventana.

6. Manejo de Layouts

11

Las tres clases ms importantes son: QHBoxLayout, QVBoxLayout y QGridLayout. Estas heredan de QLayout, la cual provee el marco bsico para las operaciones de layout. Las tres clases son totalmente soportadas por Qt Designer y tambin pueden ser usadas directamente en el cdigo. Este es el cdigo de DialogoBuscarArchivo usando administradores de layout: DialogoBuscarArchivo::DialogoBuscarArchivo(QWidget *parent) :QDialog(parent) { QGridLayout *layoutIzquierdo = new QGridLayout; layoutIzquierdo->addWidget(labelNombre, 0, 0); layoutIzquierdo->addWidget(lineEditNombre, 0, 1); layoutIzquierdo->addWidget(labelBuscarEn, 1, 0); layoutIzquierdo->addWidget(lineEditBuscarEn, 1, 1); layoutIzquierdo->addWidget(checkBoxSubdirectorios, 2, 0, 1, 2); layoutIzquierdo->addWidget(tableWidget, 3, 0, 1, 2); layoutIzquierdo->addWidget(labelMensaje, 4, 0, 1, 2); QVBoxLayout *layoutDerecho = new QVBoxLayout; layoutDerecho->addWidget(botonBuscar); layoutDerecho->addWidget(botonParar); layoutDerecho->addWidget(botonCerrar); layoutDerecho->addStretch(); layoutDerecho->addWidget(botonAyuda); QHBoxLayout *layoutPrincipal = new QHBoxLayout; layoutPrincipal->addLayout(layoutIzquierdo); layoutPrincipal->addLayout(layoutDerecho); setLayout(layoutPrincipal); setWindowTitle(tr("Buscar Archivos o Carpetas")); } La disposicin de los widgets est manejada por un objeto QHBoxLayout, un objeto QGridLayout y un objeto QVBoxLayout. El QGridLayout de la izquierda y el QVBoxLayout de la derecha, se colocan uno al lado del otro, envueltos por un QHBoxLayout. El margen alrededor del dilogo y el espacio entre los widgets estn establecidos a los valores por defecto basados en el estilo actual; estos se pueden modificar por medio de las funciones QLayout::setMargin() y QLayout::setSpacing(). Podramos crear visualmente el mismo dilogo en Qt Designer de la siguiente manera: colocamos los widgets en su posicin aproximada, seleccionamos aquellos que necesitemos que sean colocados juntos y luego hacemos clic en Form|Lay Out Horizontally, Form|Lay Out Vertically, o Form|Lay Out in a Grid, de acuerdo a lo que necesitemos. Hemos usado este enfoque en el Captulo 2 para crear los dilogos Ir-a-Celda y Ordenar de la aplicacin Hoja de Clculo. Utilizar QHBoxLayout y QVBoxLayout es bastante sencillo, pero usar QGridLayout es un poco ms complicado. QGridLayout trabaja como una cuadricula o rejilla bidimensional de celdas. El QLabel ubicado en la esquina superior izquierda ocupa la posicin (0,0), y su correspondiente QLineEdit ocupa la posicin (0,1). El QCheckBox se extiende por dos columnas, ocupando las posiciones (2,0) y (2,1). El QTreeWidget y el QLabel que est debajo tambin se extienden por dos columnas. El llamado a la funcin addWidget() tiene la siguiente sintaxis: layout->addWidget(widget, fila, columna, espacioFilas, espacioColumnas); En donde widget, es el widget del formulario que queremos incluir dentro del layout, ( fila, columna) es la celda superior izquierda en donde se posicionar el widget, espacioFilas es el numero de filas ocupadas por el widget, y espacioColumnas es la cantidad de columnas ocupadas por el widget. Si se omite, tanto el valor de espacioFilas como espacionColumnas se establecen, por defecto, a 1.

6. Manejo de Layouts

12

Figura 6.3. Organizacin de widgets del dilogo Buscar Archivo

La funcin addStretch() agrega un elemento de estiramiento en el lugar que le indiquemos, para proveer espacio en blanco. Al aadir un elemento de estiramiento, le estamos indicando al administrador de layout que coloque todo el espacio sobrante entre el botn Cerrar y el botn Ayuda. En Qt Designer, podemos obtener el mismo efecto insertando un espaciador. En Qt Designer, los espaciadores son representados como un resorte azul. Utilizar administradores de layout nos da beneficios adicionales a los que hemos discutido hasta ahora. Si agregamos o quitamos un widget, el layout se ajustar automticamente a la nueva situacin. Lo mismo ocurre cuando se llama a los mtodos hide() o show() de algn widget. Si el tamao recomendado de un widget cambia, entonces el layout se actualiza automticamente para ajustarse a la nueva situacin. Los administradores de layout pueden establecer automticamente el tamao mnimo para el formulario de manera general, basndose en los tamaos mnimos y recomendados de sus widgets. En los ejemplos presentados hasta ahora, simplemente hemos dispuesto los widgets dentro de layouts y hemos usado espaciadores (stretches) para consumir cualquier espacio sobrante. En algunos casos, esto no es suficiente para hacer que el formulario luzca exactamente como queremos. En estas situaciones, podemos ajustar la disposicin de los widgets cambiando las polticas de tamao y el tamao recomendado de los widgets. La poltica de tamao de un widget le dice al sistema de layout cmo este debera estirarse o encogerse. Qt provee polticas de tamaos predeterminadas para todos sus widgets, pero como un solo valor no puede servir para todas las situaciones posibles, es comn que los desarrolladores cambien las polticas de tamao de algn o algunos widgets del formulario. QSizePolicy tiene un componente vertical y uno horizontal. Estos seran los valores ms tiles: Fixed: el widget no puede agrandarse ni achicarse. Siempre permanecer con el tamao recomendado. Minimum: el tamao recomendado del widgets es su tamao mnimo. Su tamao no podr ser ms pequeo de lo que indique la propiedad de tamao recomendado (sizeHint), pero s podr crecer para ocupar el espacio disponible si es necesario. Maximum: el tamao recomendado del widget es su tamao mximo. Solo podr achicarse hasta su tamao mnimo. Preferred: el tamao recomendado es el tamao deseado. Puede crecer o achicarse si es necesario. Expanding: el widget puede estirarse o encogerse, pero est ms dispuesto a expandirse. La Figura 6.4 muestra el comportamiento de las diferentes polticas de tamao usando un QLabel con el texto Algn Texto como ejemplo.

6. Manejo de Layouts

13

Figura 6.4. El propsito de las diferentes polticas de tamao

Como se ve en la figura, tanto Preferred como Expanding parecen tener el mismo comportamiento. Se preguntarn: Qu es entonces lo que tienen de diferente? Cuando se cambia el tamao de un formulario que contiene widgets con Expanding y Preferred, el espacio sobrante siempre ser para los widgets con Expanding, mientras que los widgets con Preferred mantendrn su tamao. Hay otras dos polticas de tamao: MinimumExpanding e Ignored. El primero fue necesario en algunos casos muy raros en versiones anteriores de Qt, pero ahora no es muy til ya que un mejor mtodo es combinar Expanding con una re implementacin de minimumSizeHint(). El segundo es similar a Expanding, excepto que ignora tanto el tamao recomendado del widget, como su tamao mnimo. Adicionalmente a los componentes verticales y horizontales de las polticas de tamao, La clase QSizePolicy almacena dos factores de expansin: uno horizontal y otro vertical. Estos pueden ser usados para indicar cmo se deberan ajustar las proporciones de diferentes widgets cuando el formulario crezca. Por ejemplo, si tenemos un QTreeWidget sobre un QTextEdit y queremos que el segundo sea el doble de alto que el primero, podemos establecer el factor vertical del QTextEdit a 2 y el del QTreeWidget a 1. Otra manera de alterar un layout es establecer el tamao mnimo, el tamao mximo o un tamao fijo en los widgets. El administrador de layout respetar estas restricciones cuando ubique los widgets. Y si esto no es suficiente, siempre podemos crear una clase derivada del widget y re implementar sizeHint() para obtener el comportamiento que necesitemos.

Layouts Apilados
La clase QStackedLayout agrupa conjuntos de widgets en forma de pginas y muestra solo una a la vez, ocultando las otras de la vista del usuario. Esta clase es invisible y su edicin no proporciona ningn indicador visual. Las flechas y el recuadro, que se ven en la Figura 6.5, son provistos por Qt Designer para facilitar el trabajo de diseo de la interfaz. Qt tambin incluye QStackedWidget para proveer un widget con un paginado pre construido. La numeracin de las pginas comienza en 0. Para que un determinado widget sea visible, se debe llamar a setCurrentIndex() con el nmero de pgina a mostrar como argumento. Para obtener el nmero de pgina de un widget se usa indexOf().

6. Manejo de Layouts

14

Figura 6.5. QStackedLayout

Figura 6.6. Dos pginas del dialogo Preferencias

El dilogo Preferencias mostrado en la Figura 6.6 es un ejemplo del uso de QStackedLayout. Consiste de un QListWidget a la izquierda y de un QStackedLayout a la derecha. Cada tem en el QListWidget se corresponde con una pgina diferente del QStackedLayout. Este es el cdigo ms relevante del constructor del dilogo: DialogoPreferencias::DialogoPreferencias(QWidget *parent) : QDialog(parent) { widgetLista = new QListWidget; widgetLista->addItem(tr("Apariencia")); widgetLista->addItem(tr("Explorador Web")); widgetLista->addItem(tr("Correo y Noticias")); widgetLista->addItem(tr("Avanzado")); stackedLayout = new QStackedLayout; stackedLayout->addWidget(paginaApariencia); stackedLayout->addWidget(paginaExploradorWeb); stackedLayout->addWidget(paginaCorreoYNoticas); stackedLayout->addWidget(paginaAvanzado); connect(widgetLista, SIGNAL(currentRowChanged(int)), SLOT(setCurrentIndex(int))); widgetLista->setCurrentRow(0); }

stackedLayout,

6. Manejo de Layouts

15

Se crea un QListWidget y se rellena con los nombres de las pginas. Luego se crea el QStackedLayout y se agrega cada pgina con la funcin addWidget(). Conectamos la seal currentRowChanged(int) de la lista al slot setCurrentIndex(int) del layout para implementar el cambio de pginas, finalmente se llama a la funcin setCurrentRow() de la lista para seleccionar la pgina nmero 0. Este tipo de formularios son muy fciles de crear con Qt Designer: 7. Crear un nuevo formulario basado en la plantilla "Dialog" o "Widget" 8. Agregar un QListWidget y un QStackedWidget al formulario. 9. Rellenar cada pgina con los widgets necesarios y ajustar el layout. (Para crear una nueva pgina, solo basta con hacer clic con el botn derecho y elegir Insert Page; para cambiar de pgina haga clic en las flechas que se encuentran en la parte superior derecha del widget.) 10. Colocar los widget uno al lado del otro usando un layout horizontal. 11. Conectar la seal currentRowChanged(int) de la lista al slot setCurrentIndex(int) del stacked widget. 12. Establecer el valor de la propiedad currentRow de la lista a 0. Como hemos implementado el cambio de pginas usando seales y slots predefinidos, el dilogo se comportar correctamente cuando usemos la vista previa en Qt Designer.

Los Splitters (divisores o separadores)


Un QSplitter es un widget que contiene a otros widgets. Los widget contenidos estn separados por un divisor. Este nos permite modificar el tamao de un widget individual con solo arrastrarlo. Los splitters se suelen usar como una alternativa a los administradores de layouts, sobre todo si se desea darle ms control al usuario. Los widgets de un QSplitter se van colocando automticamente uno al lado del otro (o uno debajo del otro) a medida que van siendo creados, con una barra divisoria entre widgets adyacentes. Este el cdigo para crear el formulario mostrado en la Figura 6.7: int main(int argc, char *argv[]) { QApplication app(argc, argv); QTextEdit *editor1 = new QTextEdit; QTextEdit *editor2 = new QTextEdit; QTextEdit *editor3 = new QTextEdit; QSplitter separador(Qt::Horizontal); separadorsor.addWidget(editor1); separador.addWidget(editor2); separador.addWidget(editor3); separador.show(); return app.exec(); }

6. Manejo de Layouts

16

Figura 6.7. La aplicacin Separador

El ejemplo consta de tres QTextEdit dispuestos horizontalmente en un QSplitter. A diferencia de los administradores de layouts, los cuales no tienen una representacin visual, QSplitter hereda de QWidget, y por lo tanto puede ser usado como cualquier otro widget. Figura 6.8. Widgets de la aplicacin Separador

Combinando QSplitters horizontales y verticales podemos lograr interfaces bastante complejas. Por ejemplo, la aplicacin Cliente de Correo mostrada en la Figura 6.9 consiste de un QSplitter horizontal que contiene un QSplitter vertical en su lado derecho. Figura 6.9. Widgets de la aplicacin Cliente de Correo en Mac OS X

6. Manejo de Layouts

17

Este es el cdigo del constructor de la ventana principal de la aplicacin Cliente de Correo: ClienteCorreo::ClienteCorreo() { splitterDerecho = new QSplitter(Qt::Vertical); splitterDerecho->addWidget(treeWidgetMensajes); splitterDerecho->addWidget(textEdit); splitterDerecho->setStretchFactor(1, 1); splitterPrincipal = new QSplitter(Qt::Horizontal); splitterPrincipal->addWidget(treeWidgetCarpetas); splitterPrincipal->addWidget(splitterDerecho); splitterPrincipal->setStretchFactor(1, 1); setCentralWidget(splitterPrincipal); setWindowTitle(tr("Cliente de Correo")); leerConfiguraciones(); } Despus de crear los tres widgets que queremos mostrar, creamos el splitter vertical (llamado splitterDerecho) y le agregamos los dos widgets que queremos tener a la derecha. Luego creamos el splitter horizontal (llamado splitterPrincipal) y le agregamos primero el widget que queremos que se muestre a la izquierda de la ventana y posteriormente el splitter horizontal ( splitterDerecho) cuyos widgets se acomodar a la derecha. Luego transformamos a splitterPrincipal en el widget central de QMainWindow. Cuando el usuario modifica el tamao de la ventana, un QSplitter normalmente distribuye el espacio de manera tal que el tamao relativo de sus widget siga siendo el mismo. En el ejemplo no queremos este comportamiento, sino que queremos que el QTreeWidget y el QTableWidget mantengan su tamao, mientras QTextEdit consumir todo el espacio sobrante. Para lograr esto tenemos que usar la funcin setStretchFactor() de la clase QSplitter. El primer argumento es el ndice (basado en cero) del widget incluido en el splitter y el segundo argumento es el factor de crecimiento que le queremos dar al widget; por defecto este valor es 0. Figura 6.10. La indexacin de separadores de la aplicacin Cliente de Correo

La primera llamada a setStretchFactor() se realiza desde el splitter de la derecha (splitterDerecho) y establece que el widget que se encuentra en la posicin 1 ( textEdit) tenga un factor de crecimiento de 1. La segunda llamada se realiza desde el splitter principal(splitterPrincipal) y establece que el widget que se encuentra en la posicin 1 (splitterDerecho) tenga un factor de crecimiento de 1. Esto nos asegura que el QTextEdit tomar el espacio sobrante disponible. Cuando se inicia la aplicacin, QSplitter le asigna a cada widget un tamao apropiado basndose en el tamao inicial de cada uno (o en su tamao recomendado si no se especifica un tamao inicial). Podemos

6. Manejo de Layouts

18

mover los divisores mediante la funcin QSplitter::setSizes(). La clase QSplitter nos ofrece la posibilidad de guardar su estado y restablecerlo la prxima vez que ejecutemos la aplicacin. El siguiente cdigo muestra cmo la funcin guardarConfiguraciones() se encarga de guardar el estado de los widgets de la aplicacin Cliente de Correo: void ClienteCorreo::guardarConfiguraciones() { QSettings config("Software Inc.", "Cliente de Correo"); config.beginGroup("ventanaPrincipal"); config.setValue("tamao", size()); config.setValue("splitterPrincipal", splitterPrincipal->saveState()); config.setValue("splitterDerecho", splitterDerecho->saveState()); config.endGroup(); } Con la funcin leerConfiguraciones() recuperamos los estados previamente guardados: void ClienteCorreo::leerConfiguraciones() { QSettings config("Software Inc.", "Cliente de Correo"); config.beginGroup("ventanaPrincipal"); resize(config.value("tamao", QSize(480, 360)).toSize()); splitterPrincipal->restoreState(config.value(" splitterPrincipal").toByteArray()); splitterDerecho->restoreState(config.value(" splitterDerecho").toByteArray()); config.endGroup(); } QSplitter est totalmente soportado por Qt Designer. Para colocar widgets en un splitter, solo basta con ubicarlos aproximadamente en la posicin que deseamos, seleccionarlos y luego hacer clic en la opcin del men Form|Lay Out Horizontally in Splitter o Form|Lay Out Vertically in Splitter.

reas de Desplazamiento
La clase QScrollArea provee una vista desplazable con dos barras de desplazamiento. Si queremos agregar barras de desplazamiento a nuestros widgets, usar QScrollArea es ms simple que crear instancias de QScrollBars e implementar la funcionalidad de desplazamiento nosotros mismos. Figura 6.11. Widgets que constituyen un QScrollArea

La manera de usar QScrollArea es llamando a la funcin setWidget() pasndole como argumento el widget al que queremos dotar de barras de desplazamiento. QScrollArea automticamente cambia de padre al widget para hacerlo hijo del viewport o puerto de vista (accesible a travs de

6. Manejo de Layouts

19

QScrollArea::vewPort()), si es que ya no lo es. Por ejemplo, si queremos agregar barras de desplazamiento al widget IconEditor (desarrollado en el Captulo 5), escribimos el siguiente cdigo: int main(int argc, char *argv[]) { QApplication app(argc, argv); IconEditor *iconEditor = new IconEditor; iconEditor->setIconImage(QImage(":/imagenes/mouse.png")); QScrollArea scrollArea; scrollArea.setWidget(iconEditor); scrollArea.viewport()->setBackgroundRole(QPalette::Dark); scrollArea.viewport()->setAutoFillBackground(true); scrollArea.setWindowTitle(QObject::tr("Editor de Iconos")); scrollArea.show(); return app.exec(); } QScrollArea dibujar al widget con su tamao actual o usar el tamao recomendado si no se suministra informacin de tamao. Por medio de la funcin setWidgetResizable(true) podemos hacer que QScrollArea haga uso de cualquier espacio extra ms all de su tamao recomendado. Por defecto, las barras de desplazamiento solo aparecen cuando la vista es ms pequea que el widget. Podemos forzar a que se muestren siempre las barras de desplazamiento estableciendo las polticas de las mismas: scrollArea.setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOn); scrollArea.setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn); Figura 6.12. Redimensionando un QScrollArea

QScroolArea hereda mucho de su funcionalidad de QAbstractScrollArea. Las clases como QTextEdit y QAbstractItemView derivan de QAbstractScrollArea (la clase base de las clases de vista de tems en Qt), por lo que no es necesario utilizar QScrollArea para dotarlos de barras de desplazamiento.

6. Manejo de Layouts

20

Barras de Herramientas y Dock Widgets


Los dock widgets (widget acoplables) son aquellos que pueden ser anclados dentro de un QMainWindow o pueden ser ubicados de manera flotante como ventanas independientes. QMainWindow proporciona cuatro reas para widgets acoplables: una encima, una debajo, una a la izquierda y una a la derecha del widget central. Aplicaciones como Microsoft Visual Studio y Qt Linguist hacen un amplio uso de estos widgets para ofrecer una interfaz de usuario muy flexible. En Qt, los widgets acoplables son instancias de QDockWidget. Cada widget acoplable tiene su propia barra de ttulo, aun cuando est anclado. Se pueden mover de un rea a otra solo arrastrndolo desde su barra de ttulo. Tambin se puede separar del rea a donde est acoplado y dejarlo como una ventana independiente y flotante con solo arrastrarlo fuera de cualquier rea de anclaje. Las ventanas flotantes siempre se muestran encima de la ventana principal. Se pueden cerrar haciendo clic en el botn de cerrar que se encuentra en la barra de ttulo de la misma. Cualquier combinacin de estas caractersticas puede ser desactivada por medio de QDockWidget::setFeatures(). En versiones anteriores de Qt, las barras de herramientas eran tratadas como widgets acoplables y compartan las mismas reas. Desde la versin 4 de Qt, las barras de herramientas ocupan sus propias reas alrededor del widget central (como se muestra en la Figura 6.14) y no pueden ser desacopladas. Si se quiere tener una barra de herramientas flotante, simplemente se agrega dentro de un QDockWindow. Las esquinas indicadas con lneas punteadas pueden pertenecer a cualquiera de las dos reas contiguas. Por ejemplo, podramos hacer que la esquina superior izquierda perteneciera al rea de anclaje izquierda llamando al mtodoQMainWindow::setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea). Figura 6.13. Un QMainWindow con un dock widget

6. Manejo de Layouts

21

Figura 6.14. Dock area y toolbar area de QMainWindow

El siguiente fragmento de cdigo muestra cmo incluir un widget (en este caso un QTreeWidget) en un QDockWidget e insertar este en el rea de anclaje derecha de la ventana. QDockWidget *dockWidgetFormas = new QDockWidget( tr("Formas")); dockWidgetFormas->setWidget(treeWidget); dockWidgetFormas->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); addDockWidget(Qt::RightDockWidgetArea, dockWidgetFormas); La llamada a la funcin setAllowedAreas() especifica las restricciones sobre cules reas pueden aceptar una ventana acoplable. En este caso, solo se permite al usuario acoplar el widget en el rea izquierda o en el rea derecha, donde hay suficiente espacio para que el widget sea mostrado sin alteraciones. Si no se especifica ningn rea, se pueden usar cualquiera de las cuatro para acoplar el widget. A continuacin, se muestra cmo crear una barra de herramientas con un QComboBox, un QSpinBox y unos pocos QToolButtons: QToolBar *toolbarFuente = new QToolBar(tr("Fuentes")); toolbarFuente->addWidget(comboFamilia); toolbarFuente->addWidget(spinTamanio); toolbarFuente->addAction(actionNegrita); toolbarFuente->addAction(actionCursiva); toolbarFuente->addAction(actionSubrayado); toolbarFuente->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); addToolBar(toolbarFuente); Si queremos guardar la posicin de los widgets acoplables y las barras de herramientas para poder restablecer su ubicacin la prxima vez que ejecutemos la aplicacin, podemos usar un cdigo parecido al utilizado para guardar el estado de un QSplitter: void MainWindow::guardarConfiguraciones() { QSettings config("Software Inc.", "Editor de Iconos"); config.beginGroup("ventanaPrincipal"); config.setValue("tamao", size()); config.setValue("estado", saveState());

6. Manejo de Layouts

22

config.endGroup(); } void MainWindow::leerConfiguraciones() { QSettings config("Software Inc.", "Editor de Iconos"); config.beginGroup("ventanaPrincipal"); resize(config.value("tamao").toSize()); restoreState(config.value("estado").toByteArray()); config.endGroup(); } Por ltimo, QMainWindow proporciona un men contextual que muestra una lista de las barras de herramientas y widgets acoplables. Desde este men podemos cerrar y abrir los widget acoplables y mostrar u ocultar barras de herramientas. Figura 6.15. Un men contextual de QMainWindow

Interfaz de Mltiples Documentos


A las aplicaciones que pueden albergar varios documentos dentro de su rea central se las denomina aplicaciones de interfaz de mltiples documentos (o MDI para abreviar). En Qt, una aplicacin MDI se crea usando la clase QWorkspace como widget central y haciendo que cada documento sea hija de sta. Es una convencin que las aplicaciones MDI incluyan un men Ventana que incluya tanto una lista de las ventanas abiertas como una serie comandos para administrarlas. La ventana activa se identifica por medio de una marca o checkmark. El usuario puede activar cualquier ventana seleccionando la entrada del men con el nombre de la misma. En esta seccin desarrollaremos la aplicacin (tipo editor de texto) MDI Editor (Figura 6.16), para ejemplificar cmo se crea una aplicacin MDI y cmo implementar un men Ventana. La aplicacin consta de dos clases: MainWindow y Editor. Debido a que el cdigo es muy similar al cdigo de la aplicacin desarrollada en la Parte I, nos centraremos en el cdigo diferente. Empecemos por la clase MainWindow. MainWindow::MainWindow() { workspace = new QWorkspace; setCentralWidget(workspace); connect(workspace, SIGNAL(windowActivated(QWidget *)), this, SLOT(actualizarMenus())); crearAcciones(); crearMenus(); crearToolBars(); crearStatusBar(); setWindowTitle(tr("MDI Editor")); setWindowIcon(QPixmap(":/imagenes/icono.png"));

6. Manejo de Layouts

23

} Figura 6.16. La aplicacin MDI Editor

Figura 6.17. Mens de la aplicacin MDI Editor

En el constructor, creamos el widget QWorkspace y lo transformamos en el widget central. Conectamos la seal windowActivated() de QWorkspace al slot que usaremos para mantener el men actualizado. void MainWindow::nuevoArchivo() { Editor *editor = crearEditor(); editor->nuevoArchivo(); editor->show(); } El slot nuevoArchivo corresponde a la opcin de men Archivo|Nuevo. Este depende de la funcin privada crearEditor() para crear un widget Editor. Editor *MainWindow::crearEditor() { Editor *editor = new Editor; connect(editor, SIGNAL(copyAvailable(bool)), accionCortar,

6. Manejo de Layouts

24

SLOT(setEnabled(bool))); connect(editor, SIGNAL(copyAvailable(bool)), accionCopiar, SLOT(setEnabled(bool))); workspace->addWindow(editor); menuVentana->addAction(editor->accionMenuVentana()); actionGroupVentana->addAction(editor->accionMenuVentana()); return editor; } La funcin crearEditor() se encarga de crear un widget Editor y establecer la conexin de dos seales, que nos asegurarn que las acciones Edicin|Cortar y Edicin|Copiar se activarn o desactivarn dependiendo de si hay o no texto seleccionado. Como estamos usando MDI, es factible que haya varios editores abiertos al mismo tiempo. Esto presenta un pequeo inconveniente, ya que nos interesa que la seal copyAvailable(bool) solo provenga de la ventana activa, no de otras. Pero esta seal solo puede ser emitida por la ventana activa, as que en la prctica, no sera un problema. Una vez creado y configurado el Editor, agregamos un QAction que representa a la ventana en el men Ventana. La accin es provista por la clase Editor, la cual cubriremos en un momento. Tambin agregamos la accin a un objeto QActionGroup para asegurarnos que solo un tem del men Ventana est marcado a la vez. void MainWindow::abrir() { Editor *editor = crearEditor(); if (editor->abrir()) { editor->show(); } else { editor->close(); } } La funcin abrir() se corresponde a la opcin del men Archivo|Abrir. Aqu se crea un Editor para el nuevo documento y se llama a la funcin abrir() del mismo. Tiene ms sentido implementar las operaciones sobre archivos en la clase Editor que en la clase MainWindow, porque cada editor necesita mantener su propio estado independiente de los dems. Si esta funcin falla, simplemente cerramos el editor ya que el usuario ya ha sido notificado del error. No necesitamos eliminar explcitamente el objeto Editor, esto se realiza automticamente porque hemos activado el atributo Qt::WA_DeleteOnClose en el constructor del widget Editor. void MainWindow::guardar() { if (editorActivo()) editorActivo()->guardar(); } El slot guardar() llama a la funcin Editor::guardar() del editor activo, si es que hay uno. Nuevamente, el cdigo que realiza el verdadero trabajo, est en la clase Editor. Editor *MainWindow::editorActivo() { return qobject_cast<Editor *>(workspace->activeWindow()); } La funcin privada editorActivo() nos devuelve un puntero al objeto Editor de la ventana activa, o un puntero nulo si no hay ventana activa.

6. Manejo de Layouts

25

void MainWindow::cortar() { if (editorActivo()) editorActivo()->cortar(); } El slot cortar() llama a la funcin Editor::cortar() del editor activo. No se mostrarn los slots copiar() y pegar() porque tienen el mismo patrn. void MainWindow::actualizarMenus() { bool hayEditor = (editorActivo() != 0); bool haySeleccion = editorActivo() && editorActivo()->textCursor().hasSelection(); actionGuardar->setEnabled(hayEditor); actionGuardarComo->setEnabled(hayEditor); actionPegar->setEnabled(hayEditor); actionCortar->setEnabled(haySeleccion); actionCopiar->setEnabled(haySeleccion); actionCerrar->setEnabled(hayEditor); actionCerrarTodos->setEnabled(hayEditor); actionMosaico->setEnabled(hayEditor); actionCascada->setEnabled(hayEditor); actionSiguiente->setEnabled(hayEditor); actionAnterior->setEnabled(hayEditor); actionSeparador->setVisible(hayEditor); if (editorActivo()) editorActivo()->accionMenuVentana()->setChecked(true); } El slot actualizarMenus() es llamado cada vez que se activa una ventana (y cuando se cierra la ltima ventana) para actualizar el men, debido a que las conexiones las colocamos en el constructor de la clase MainWindow. La mayora de las opciones de men tienen sentido si hay una ventana activa, por lo tanto las desactivaremos si no hay ninguna ventana activa. Para finalizar, llamamos a setChecked() de un QAction que est representando a la ventana activa. Gracias al QActionGroup, no nos tenemos que preocupar por desmarcar la accin de la anterior ventana activa. void MainWindow::crearMenus() { menuVentana = menuBar()->addMenu(tr("&Ventana")); menuVentana->addAction(actionCerrar); menuVentana->addAction(actionCerrarTodos); menuVentana->addSeparator(); menuVentana->addAction(actionMosaico); menuVentana->addAction(actionCascada); menuVentana->addSeparator(); menuVentana->addAction(actionSiguiente); menuVentana->addAction(actionAnterior); menuVentana->addAction(actionSeparador); } La funcin privada crearMenus() se encarga de agregar las acciones al men Ventana. Estas son las acciones tpicas de este men y son implementadas a travs los slots closeActiveWindow(), closeAllWindows(), tile(), y cascade() de la clase QWorkspace.

6. Manejo de Layouts

26

Cada vez que se abre una nueva ventana, esta es agregada a la lista del men Ventana (esto se realiza en la funcin crearEditor()). Cuando se cierra una ventana, la opcin correspondiente es borrada (ya que el editor es el padre de la accin) y removida del men Ventana. void MainWindow::closeEvent(QCloseEvent *event) { workspace->closeAllWindows(); if (editorActivo()) { event->ignore(); } else { event->accept(); } } La funcin closeEvent() de la clase QMainWindow es re implementada para poder cerrar todas las ventanas, enviando a cada ventana abierta un evento close. Si alguno de los editores abiertos ignora el evento close (porque se cancel el mensaje "Hay cambios sin guardar"), se ignorar el evento close de MainWindow; en cualquier otro caso Qt termina cerrando la aplicacin entera. Si no implementramos closeEvent() en MainWindow, el usuario no dispondra de la oportunidad de guardar cambios pendientes en los documentos. Hemos terminado con la revisin de la clase MainWindow, ahora pasaremos a la implementacin de la clase Editor. Esta clase representa una ventana de edicin de documentos. Hereda de QTextEdit, la cual nos provee la funcionalidad de edicin de texto. Como cualquier otro widget puede ser usado como una ventana autnoma y por lo tanto tambin como una ventana hija de un MDI. Esta es la definicin de la clase Editor: class Editor : public QTextEdit { Q_OBJECT public: Editor(QWidget *parent = 0); void nuevoArchivo(); bool abrir(); bool abrirArchivo(const QString &nombreArchivo); bool guardar(); bool guardarComo(); QSize sizeHint() const; QAction *accionMenuVentana() const { return accion; } protected: void closeEvent(QCloseEvent *event); private slots: void documentoFueModificado(); private: bool okParaContinuar(); bool guardaArchivo(const QString &nombreArchivo); void setArchivoActual(const QString &nombreArchivo); bool leerArchivo(const QString &nombreArchivo); bool escribirArchivo(const QString &nombreArchivo); QString soloNombre(const QString &nombreArchivo); QString archivoActual; bool isSinTitulo; QString filtros; QAction *accion; };

6. Manejo de Layouts

27

Las funciones privadas okParaContinuar(), guardaArchivo(), setArchivoActual() y soloNombre(), estn presentes en la clase MainWindow de aplicacin Hoja de Clculo desarrollada en la Parte I, por lo que no se explicarn. Editor::Editor(QWidget *parent) : QTextEdit(parent) { accion = new QAction(this); accion->setCheckable(true); connect(accion, SIGNAL(triggered()), this, SLOT(show())); connect(accion, SIGNAL(triggered()), this, SLOT(setFocus())); isSinTitulo = true; filtros = tr("Archivos de Texto (*.txt)\n" "Todos los archivos (*)"); connect(document(), SIGNAL(contentsChanged()), this, SLOT(documentoFueModificado())); setWindowIcon(QPixmap(":/imagenes/documento.png")); setAttribute(Qt::WA_DeleteOnClose); } Primero creamos un QAction que representar al editor en el men Ventana de la aplicacin y la conectamos a los slots show() y setFocus(). Ya que permitimos crear la cantidad de ventanas que el usuario desee, debemos tener en cuenta que cada documento tenga un nombre distinto antes de que sean guardados por primear vez. El mtodo ms comn consiste en asignar un nmero al final de un nombre base (por ejemplo documento1.txt). Usaremos la variable isSinTitulo para distinguir entre los nombres suministrados por el usuario y los nombres creados por el programa. Conectamos la seal contentsChanged() del QTextDocument interno del editor al slot privado documentoFueModificado(). Este simplemente llama a setWindowModified(true). Finalmente establecemos el atributo Qt::WA_DeleteOnClose para prevenir fugas de memoria cuando se cierre una ventana de Editor. Despus del constructor, se espera una llamada a nuevoArchivo() o abrir(). void Editor::nuevoArchivo() { static int numeroDocumento = 1; archivoActual = tr("documento%1.txt").arg(numeroDocumento); setWindowTitle(archivoActual + "[*]"); accion->setText(archivoActual); isSinTitulo = true; ++numeroDocumento; } La funcin nuevoArchivo() genera un nombre para el nuevo documento (de tipo documento1.txt). El cdigo no se coloc en el constructor para no consumir nmeros si se desea abrir un archivo existente en vez de crear uno nuevo. Como definimos esttica a la variable numeroDocumento puede ser compartida por todos los objetos Editor creados. El marcador "[*]" en el ttulo de la ventana es un indicador que mostraremos cada vez que el documento contenga cambios sin guardar (excepto en MacOs). Hemos cubierto este tema en el Capitulo 3. bool Editor::abrir() {

6. Manejo de Layouts

28

QString nombreArchivo = QFileDialog::getOpenFileName(this, tr("Abrir"), ".", filtros); if (nombreArchivo.isEmpty()) return false; return abrirArchivo(nombreArchivo); } La funcin abrir() intenta abrir un archivo existente por medio de la funcin abrirArchivo(). bool Editor::guardar() { if (isSinTitulo) { return guardarComo(); } else { return guardaArchivo(archivoActual); } } La funcin guardar() determina por medio de la variable isSinTitulo si debera llamar a la funcin guardaArchivo() o a la funcin guardarComo(). void Editor::closeEvent(QCloseEvent *event) { if (okParaContinuar()) { event->accept(); } else { event->ignore(); } } La funcin closeEvent() es re implementada para permitirle al usuario guardar cambios pendientes. La lgica est codificada en la funcin okParaContinuar(), la cual muestra un mensaje preguntando si se desean guardar los cambios del documento. Si okParaContinuar() devuelve true, aceptamos el evento; de lo contrario lo ignoramos y abandonamos la ventana. void Editor::setArchivoActual(const QString &nombreArchivo) { archivoActual = nombreArchivo; isSinTitulo = false; accion->setText(soloNombre(archivoActual)); document()->setModified(false); setWindowTitle(soloNombre(archivoActual) + "[*]"); setWindowModified(false); } La funcin setArchivoActual() es llamada desde abrirArchivo() y guardaArchivo() para actualizar las variables isSinTitulo y archivoActual, establecer el ttulo de la ventana y el texto de la accin, y colocar en false la propiedad "modified" del documento. Cuando sea que el usuario modifique el texto, el QTextDocument subyacente emitir la seal contentsChanged() y establecer "modified" a true. QSize Editor::sizeHint() const { return QSize(72 * fontMetrics().width(x), 25 * fontMetrics().lineSpacing()); } La funcin sizeHint() devuelve un objeto QSize basado en el ancho de la letra "x" y el alto de una lnea de texto. Este es usado por QWorkspace para darle un tamao inicial a la ventana.

6. Manejo de Layouts

29

Este es el cdigo del archivo main.cpp: #include <QApplication> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QStringList args = app.arguments(); MainWindow mainWin; if (args.count() > 1) { for (int i = 1; i < args.count(); ++i) mainWin.abrirArchivo(args[i]); } else { mainWin.nuevoArchivo(); } mainWin.show(); return app.exec(); } Si el programa se ejecuta desde la lnea de comandos y se especifica nombres de archivo, se intentarn cargar. Si no se especifica nada, simplemente se iniciar la aplicacin con un documento en blanco. Las opciones de lnea de comandos especficas de Qt (como -style y - font) son automticamente quitadas de la lista de argumentos por el constructor de QApplication. Si ejecutamos el programa de la siguiente manera: mieditor -style motif leeme.txt desde la lnea de comandos, QApplication::arguments() nos devolver un QStringList conteniendo dos tems (mdieditor y leeme.txt) por lo que la aplicacin se iniciar y abrir el documento leeme.txt. MDI es una manera de manejar mltiples documentos al mismo tiempo. En MacOS X el enfoque preferido es utilizar ventanas de nivel superior. Este tema fue expuesto en la seccin "Documentos mltiples" del Captulo 3.

7. Procesamiento de Eventos

Reimplementar Manejadores de Eventos Instalando Filtros de Eventos Evitar Bloqueos Durante Procesamientos Intensivos

Los eventos son generados por el sistema de ventanas, o por Qt, para dar respuesta a distintos sucesos o acontecimientos. Cuando un usuario presiona o suelta una tecla o un botn del ratn, se genera un evento para dicha accin; cuando una ventana se muestra por primera vez se genera un evento de pintado para informarle a la nueva ventana que se tiene que dibujar por si misma. La mayora de los eventos son generados en respuesta a alguna accin del usuario, pero algunos, como los temporizadores, son generados independientemente por el sistema. Cuando programamos con Qt, rara vez necesitamos pensar en los eventos, porque utilizamos las seales que emiten los widgets cuando ocurre algo significativo. Los eventos se vuelven tiles cuando desarrollamos widgets propios o cuando queremos cambiar el comportamiento de uno existente. No debemos confundir los eventos con las seales. Se establece como regla que las seales son tiles cuando usamos un widget, mientras que los eventos son tiles cuando implementamos un widget. Por ejemplo, si trabajamos con un QPushButton, estaremos ms interesados en su seal clicked() que en los procesos de bajo nivel que causan su emisin. Pero si estamos desarrollando una clase que se comporta como un QPushButton, necesitaremos escribir el cdigo que controle los eventos del teclado y del ratn para poder emitir la seal clicked() cuando sea necesario.

Reimplementar Manejadores de Eventos


En Qt, un evento es un objeto derivado de QEvent. Existen ms de un centenar de tipos de eventos en Qt, cada uno identificado por un valor de una enumeracin. Podemos usar QEvent::type() para obtener el valor de la enumeracin que corresponde al evento emitido. Por ejemplo, QEvent::type() devolver QEvent::MouseButtonPress cuando se presiona un botn del ratn. Muchos tipos de eventos requieren ms informacin que puede ser guardada en un objeto QEvent; por ejemplo, los eventos de ratn necesitan guardar cul botn del ratn fue el que lo dispar, as como tambin necesitan guardar la posicin que tena el puntero del mouse cuando sucedi el evento. Esta informacin adicional es guardada en subclases de QEvent dedicadas, tal como QMouseEvent. Los eventos son notificados a los objetos a travs de la funcin event(), que se hereda de QObject. La implementacin de esta funcin en la clase QWidget re direcciona los tipos ms comunes de eventos a funciones que actan como manejadores de eventos especficos, tales como mousePressEvent(), keyPressEvent() y paintEvent().

7. Procesamiento de Eventos

31

Ya hemos visto varios manejadores de eventos cuando implementamos algunos ejemplos en captulos anteriores (MainWindow, EditorIcono y Plotter). Hay ms tipos de eventos enumerados en la documentacin de QEvent y tambin es posible crear nuestros propios tipos de eventos y emitirlos por nuestra cuenta. A continuacin, revisaremos los dos tipos de eventos que merecen una mayor explicacin: la presin de teclas y los temporizadores. Los eventos generados por la presin de teclas pueden ser controlados mediante la re implementacin de las funciones keyPressEvent() y keyReleaseEvent(). En la seccin dedicada al widget Plotter, se re implement este ltimo mtodo. Normalmente solo necesitaremos enfocarnos en keyPressEvent() ya que las nicas teclas que nos interesara saber cuando se soltaron son: Ctrl , Shift y Alt (tambin llamadas teclas modificadoras), y podemos obtener su estado por medio de QkeyEvent::modifiers(). Por ejemplo, si estamos desarrollando un widget EditorCodigo, y nos interesa distinguir entre la presin de la teclas Inicio y Ctr+Inicio, la re implementacin de keyPressEvent() se vera as: void EditorCodigo::keyPressEvent(QkeyEvent *evento) { switch (evento->key()) { case Qt::Key_Home: if (evento->modifiers() & Qt::ControlModifier){ irPrincipioDeDocumento(); } else { irPrincipioDeLinea(); } break; case Qt::Key_End: default: Qwidget::keyPressEvent(evento); } } La presin de las teclas Tab y Shift+Tab son casos especiales. Estas son manejadas por QWidget::event() pasando el enfoque al siguiente (o anterior) widget en el orden de tabulacin, antes de llamar a keyPressEvent(). Este es el comportamiento habitual que queremos, pero para el widget EditorCodigo, podramos preferir la tecla Tab idente una lnea. Aqu se muestra la re implementacin de event(): bool EditorCodigo::event(QEvent *evento) { if (evento->type() == QEvent::KeyPress) { QKeyEvent *eventoTecla = static_cast<QKeyEvent*>(evento); if (eventoTecla->key() == Qt::Key_Tab]) { insertarEnPosicionActual(\t); return true; } } return Qwidget::event(evento); } Si el evento fue emitido por la presin de una tecla, convertimos el objeto QEvent a QKeyEvent y verificamos qu tecla ha sido presionada. Si fue la tecla Tab, realizamos algn tipo de procesamiento y devolvemos true para comunicarle a Qt que el evento ya ha sido procesado. Si devolvemos false, Qt propagar el evento al widget padre. Un mtodo de alto nivel para implementar atajos de teclado es usar QAction. Por ejemplo, si las funciones irPrincipioDeLinea() e irPrincipioDeDocumento() son slots pblicos del widget EditorCodigo, y ste es usado como widget central en una clase MainWindow, podramos implementar atajos de teclados con el siguiente cdigo:

7. Procesamiento de Eventos

32

MainWindow::MainWindow() { editor = new EditorCodigo; setCentralWidget(editor); ActionIrPrincipioLinea = new QAction(tr(Ir al comienzo de la linea), this); ActionIrPrincipioLinea->setShortcut(tr(Home)); connect(ActionIrPrincipioLinea, SIGNAL(activated()), editor, SLOT(irPrincipioDeLinea())); ActionIrPrincipioDocumento = new QAction(tr(Ir al comienzo del documento), this); ActionIrPrincipioDocumento->setShortcut(tr(Ctrl+Home)); connect(ActionIrPrincipioDocumento, SIGNAL(activated()), editor, SLOT(irPrincipioDeDocumento())); } Esto hace que resulte fcil agregar los comandos al men o a la barra de herramientas, como se mostr en el Capitulo 3. Si el comando no tiene que aparecer en la interfaz de usuario, podramos reemplazar el objeto QAction con un QShortcut, la clase usada internamente por QAction para implementar atajos de teclado. De manera predeterminada, los atajos de teclado (ya sea usando QAction o QShortcut) estn habilitados siempre y cuando la ventana que contiene al widget est activa. Este comportamiento puede ser cambiado por medio de las funciones QAction::setShortcutContext() o QShortcut::setContext(). Otro tipo comn de evento es el emitido por los temporizadores. Mientras que la mayor parte de los eventos ocurre como resultado de una accin del usuario, los temporizadores permiten a las aplicaciones ejecutar procesos a intervalos de tiempo regulares. Se pueden usar para implementar el parpadeo del cursor y otras animaciones, o simplemente refrescar un rea de la ventana. Para demostrar el funcionamiento de los temporizadores, implementaremos el widget Ticker. Este widget mostrar un texto que se ir desplazando un pixel a la izquierda cada 30 milisegundos. Si el widget es ms ancho que el texto, este ltimo se repetir las veces que sea necesario hasta completar el ancho del widget. Figura 7.1 El widget Ticker

Este es el archivo de cabecera del widget: #ifndef TICKER_H #define TICKER_H #include <QWidget> class Ticker : public QWidget { Q_OBJECT Q_PROPERTY(QString texto READ texto WRITE setTexto) public: Ticker(QWidget *parent = 0); void setTexto(const QString &nuevoTexto); QString texto()const { return miTexto; } QSize sizeHint() const; protected: void paintEvent(QPaintEvent *evento);

7. Procesamiento de Eventos

33

void timerEvent(QTimerEvent *evento); void showEvent(QShowEvent *evento); void hideEvent(QHideEvent *evento); private: QString miTexto; int desplaz; int miTimerId; }; #endif Como apreciarn, re implementamos cuatro controladores de evento, tres de los cuales todava no hemos visto: timerEvent(), showEvent() y hideEvent(). Ahora revisemos la implementacin: #include <QtGui> #include ticker.h Ticker::Ticker(QWidget *parent) : QWidget(parent) { desplaz = 0; miTimerId = 0; } El constructor inicializa la variable desplaz a 0. Este valor nos servir para ir calculando la coordenada x en donde se dibujar el texto. Cada temporizador creado tiene un valor de tipo entero como identificador, estos son siempre distintos de cero, por lo que al usar un 0 para la variable miTimerId estamos indicando que no hay ningn temporizador activo. void Ticker::setTexto(const QString &nuevoTexto) { miTexto = nuevoTexto; update(); updateGeometry(); } La funcin setTexto() se encarga de establecer el texto a mostrar. La llamada a update() le indica al widget que se tiene que redibujar y updateGeometry() notifica al administrador de layout que el widget acaba de cambiar de tamao. QSize Ticker::sizeHint() const { return fontMetrics().size(0, texto()); } La funcin sizeHint() devuelve el tamao ideal del widget, que se calcula como el espacio necesario para el texto. QWidget::fontMetrics() nos devuelve un objeto QFontMetrics, el cual nos brinda informacin relativa a la fuente del widget. En este caso, obtenemos el espacio necesario para dibujar el texto, por medio de la funcin size(). El primer argumento es una bandera que no es necesario usar para cadenas simples, por lo que pasamos un 0. void Ticker::paintEvent(QPaintEvent * /* evento */) { QPainter painter(this); int anchoTexto = fontMetrics().width(texto()); if (anchoTexto < 1) return;

7. Procesamiento de Eventos

34

int x = -desplaz; while (x < width()) { painter.drawText(x, 0, anchoTexto, height(), Qt::AlignLeft | Qt::AlignVCenter, texto()); x += anchoTexto; } } La funcin paintEvent() dibuja el texto usando QPainter::drawText(). Usamos fontMetrics() para comprobar cuanto espacio horizontal requiere el texto, y luego lo dibujamos la cantidad de veces que sea necesario para cubrir el ancho del widget, tomando en cuenta a la variable desplaz. void Ticker::showEvent(QShowEvent * /* evento */) { miTimerId = startTimer(30); } La funcin showEvent() inicia un temporizador. Llama a QObject::startTimer(), la que nos devuelve un nmero ID, que luego nos servir para identificar al temporizador. QObject soporta varios temporizadores independientes, cada cual con su propio intervalo de tiempo. Despus de llamar a startTimer(), Qt generar un evento cada 30 milisegundos aproximadamente; la precisin depende del sistema operativo en donde se ejecute la aplicacin. Podramos haber llamado a startTimer() en el constructor de Ticker, pero al generar los temporizadores cuando el widget est visible logramos que Qt ahorre algunos recursos. void Ticker::timerEvent(QTimerEvent *evento) { if (evento->timerId() == miTimerId) { ++desplaz; if (desplaz >= fontMetrics().width(texto())) desplaz = 0; scroll(-1, 0); } else { QWidget::timerEvent(evento); } } La funcin timerEvent() es llamada a intervalos regulares por el sistema. Esta incrementa la variable desplaz en 1 para simular el movimiento del texto. Luego se desplaza el contenido del widget un pixel a la izquierda usando QWidget::scroll(). Hubiera sido suficiente llamar a update() en vez de scroll(), pero sta ltima es ms eficiente, ya que simplemente mueve los pixeles existentes en la pantalla y solo genera un evento de pintado para el rea revelada (en este caso un pixel de ancho). Si el evento del temporizador no es el que nos interesa, lo pasamos a la clase base. void Ticker::hideEvent(QHideEvent * /* evento */) { killTimer(miTimerId); } La funcin hideEvent() llama a QObject::killTimer() para detener el temporizador. Los eventos de temporizacin son de bajo nivel, y si necesitamos usar varios temporizadores al mismo tiempo, el seguimiento de todos los identificadores puede llegar a resultar demasiado engorroso y difcil de mantener. En tales situaciones, es ms fcil crear un QTimer por cada temporizador que necesitemos. Este

7. Procesamiento de Eventos

35

objeto emite la seal timeout() cuando se cumple el intervalo de tiempo establecido y tambin provee una interfaz conveniente para temporizadores que solo se disparen una vez.

Instalar Filtros de Eventos


Una caracterstica realmente poderosa del modelo de eventos de Qt, es que una instancia de QObject puede monitorear los eventos de otra instancia de QObject, incluso antes de que el ltimo objeto sea notificado de la existencia de los mismos. Supongamos que tenemos un widget DialogoInfoCliente compuesto de varios QLineEdit y que queremos usar la barra espaciadora para cambiar el enfoque al QLineEdit siguiente. Una solucin sencilla, para obtener este comportamiento no estndar, seria sub clasificar QLineEdit y re implementar keyPressEvent() para que llame a focusNextChild(), algo as: void MiLineEdit::keyPressEvent(QkeyEvent *event) { if (event->key() == Qt::Key_Space) { focusNextChild(); } else { QlineEdit::keyPressEvent(event); } } Este mtodo tiene una desventaja: si usamos un conjunto variado de widgets en el formulario (por ejemplo QComboBox y QSpinBox) adems, tendramos que hacer subclases de ellos para que tengan el mismo comportamiento que el MiLineEdit. Una solucin mucho mejor es hacer que DialogoInfoCliente vigile los eventos de teclados en sus widgets hijos e implemente el comportamiento requerido. Esto se logra usando filtros de eventos. La creacin de un filtro de eventos conlleva dos pasos: 1. Registrar el objeto que monitorea con el objeto de destino, llamando a la funcin installEventFilter() en el objeto destino. 2. Controlar los eventos emitidos por el objeto destino con la funcin eventFilter() del objeto monitor. El constructor de la clase es un buen lugar para registrar los filtros de eventos u objetos de monitoreo, como tambin se les llama. DialogoInfoCliente::DialogoInfoCliente(QWidget *parent): QDialog(parent) { editNombre->installEventFilter(this); editApellido->installEventFilter(this); editCiudad->installEventFilter(this); editTelefono->installEventFilter(this); } Una vez registrado el filtro de eventos, los eventos enviados a los widgets editNombre, editApellido, editCiudad y editTelefono sern enviados primero a la funcin eventFilter() de DialogoInfoCliente, antes que a su destino original. Aqu vemos la implementacin de la funcin eventFilter() que recibe los eventos: bool DialogoInfoCliente::eventFilter(QObject *target, QEvent *evento) { if (target == editNombre || target == editApellido || target == editCiudad || target == editTelefono) { if (evento->type() == QEvent::KeyPress) { QKeyEvent *eventoTecla =static_cast<QKeyEvent *> (evento);

7. Procesamiento de Eventos

36

if (eventoTecla->key() == Qt::Key_Space) { focusNextChild(); return true; } } } return QDialog::eventFilter(target, evento); } Primero, comprobamos si el widget destino es uno de los QLineEdit que nos interesa. Si el evento fue emitido por la presin de una tecla, convertimos evento a QKeyEvent y verificamos qu tecla fue presionada. Si fue la barra espaciadora, llamamos a focusNextChild() para pasar el enfoque al widget siguiente en el orden de tabulacin y devolvemos true para informarle a Qt que el evento ya ha sido procesado. Si devolvemos false, se enviar el evento al destino previsto, obteniendo como resultado un espacio en blanco agregado al QLineEdit. Si el widget destino no es un QLineEdit, o si el evento no fue lanzado por la presin de la barra espaciadora, pasamos el control del evento a la clase base. Esto lo hacemos porque un widget padre puede tener bajo vigilancia, por distintas razones, los eventos de sus widgets hijos. En Qt 4.1 esto no sucede con QDialog, pero si con otros widgets, como QScrollArea. Qt ofrece cinco niveles distintos para procesar y filtrar eventos: 1. Podemos re implementar un controlador de evento especfico. Re implementando los manejadores de eventos tales como mousePressEvent(), keyPressEvent() y paintEvent() est, por mucho, la manera mas comn de procesar eventos. Ya hemos visto varios ejemplos a lo largo del libro. 2. Podemos re implementar QObject::event(). Con esta tcnica podemos procesar los eventos antes que de que lleguen al controlador especifico. Se usa generalmente para anular o modificar el comportamiento que tiene por defecto la tecla Tab, como se mostr anteriormente. Tambin es usada para manejar tipos raros de eventos para los que no existen controladores especficos (por ejemplo, el evento QEvent::HoverEnter). Despus que re implementemos event(), debemos llamar a la funcin event() de la clase base para que se encargue de los casos que no controlamos explcitamente. 3. Podemos instalar filtros de eventos en un solo QObject. Una vez que el objeto haya sido registrado con la funcin installEventFilter(), todos los eventos destinados a ese objeto sern enviados primero a la funcin eventFilter() del objeto monitor. Si instalamos varios filtros en el mismo objeto, estos sern procesados por turnos, comenzando por el instalado ms recientemente hasta llegar al primer objeto instalado. 4. Podemos instalar filtros de eventos en el objeto QApplication. Una vez que el filtro ha sido registrado por qapp (recordemos que hay un solo objeto QApplication por programa), cada evento de cada objeto de la aplicacin ser enviado primero a la funcin eventFilter(), antes que a cualquier otro filtro de eventos. Esta tcnica suele ser til en el proceso de depuracin o para controlar eventos del ratn sobre widget desactivados, los cuales son normalmente descartados por QApplication. 5. Podemos subclasificar QApplication y re implementar notify(). Qt llama a QApplication::notify() para enviar un evento. Re implementando esta funcin es la nica manera de tener acceso a los eventos antes de que cualquier filtro de eventos los llegue a procesar. Los

7. Procesamiento de Eventos

37

filtros de eventos son generalmente ms tiles, porque podemos tener cualquier cantidad de filtros concurrentes, pero solo tendremos una funcin notify(). Muchos tipos de eventos, incluyendo los eventos de mouse y teclado, pueden ser propagados. Si un evento no ha sido controlado en el camino a su objeto destino o por el objeto destino mismo, se vuelve a emitir, pero esta vez con el objeto padre como nuevo destino. Esto continua, de padre a padre hasta que alguno controle el evento o se alcance el primer objeto de la jerarqua. Figura 7.2. Propagacin de evento en un dialogo

La Figura 7.2 muestra cmo, en un dialogo, un evento generado por la presin de una tecla es propagado desde un widgets hijo a los widgets padres. Cuando un usuario presiona una tecla, el evento es enviado primero al widget que tiene el enfoque, en este caso el QCheckBox que est en la parte inferior derecha del dialogo. Si ste no controla el evento, Qt se encarga de enviarlo al objeto QGroupBox y por ltimo al objeto QDialog.

Evitar Bloqueos Durante Procesamientos Intensivos


Cuando llamamos a QApplication::exec(), se inician los ciclos de eventos de Qt. Qt emite unos pocos eventos para mostrar y dibujar los widgets. Despus de esto, se ejecuta el ciclo principal de eventos en donde constantemente se verifica si ha ocurrido algn evento y de ser as, los enva a los objetos (QObjects) de la aplicacin. Mientras un evento est siendo procesado, los eventos adicionales que se generen sern agregados a una cola, en donde esperaran su turno. Si pasamos mucho tiempo procesando un evento en particular, la interfaz de usuario dejar de responder. Por ejemplo, cualquier evento generado por el sistema mientras la aplicacin guarda un archivo no ser procesado hasta que no se termine de guardar el archivo. Durante este tiempo la aplicacin no atender ningn requerimiento, ni siquiera la solicitud de re dibujado realizada por parte del sistema de ventanas. Una solucin para este caso seria usar varios hilos: uno para la interfaz grafica de la aplicacin y otro para el proceso que requiera demasiado tiempo de operacin (como el guardado de archivos). De esta manera, la interfaz de usuario podr recibir requerimientos mientras el archivo se guarda. Veremos cmo se hace esto en el Captulo 18. Una solucin ms simple es realizar llamadas frecuentes a QApplication::processEvents() en el cdigo donde realizamos el proceso intensivo (p. e., en el cdigo de guardado del archivo). La funcin processEvents() le dice a Qt que se encargue de cualquier evento pendiente en la cola y luego devuelva el control al procedimiento llamador. De hecho, QApplication::exec() es poco ms que un ciclo mientras (while) envolviendo llamadas peridicas a la funcin processEvents(). Aqu presentamos un ejemplo de esta tcnica, basndonos en el cdigo de guardado de archivo de la aplicacin Hoja de Clculo:

7. Procesamiento de Eventos

38

bool HojaCalculo::guardaArchivo(const QString &nombreArchivo) { QFile archivo(nombreArchivo); for (int fila = 0; fila < CantidadFilas; ++fila) { for (int columna = 0; columna < CantidadColumnas; ++columna) { QString str = formula(fila, columna); if (!str.isEmpty()) out << quint16(fila) << quint16(columna) << str; } qApp->processEvents(); } return true; } Un peligro que presenta este enfoque es que el usuario podra cerrar la ventana principal mientras la aplicacin aun se encuentra realizando el guardado del archivo o volver a activar otra vez la misma accin desde el men Archivo|Guardar, obteniendo un comportamiento indefinido e inesperado como resultado. La solucin ms fcil a este problema es reemplazar qApp->processEvents(); Con qApp->processEvents(QEventLoop::ExcludeUserInputEvents); De esta manera le informamos a Qt que ignore los eventos del teclado y del ratn. Mientras se est ejecutando una operacin larga, es comn querer mostrar el progreso de la misma. Para esto utilizamos QProgressDialog; esta clase posee una barra que mantiene al usuario informado sobre el avance del proceso que est realizando la aplicacin y un botn que permite abortar la operacin en cualquier momento. Este sera el cdigo para guardar un archivo de la aplicacin Hoja de Clculo usando QProgressDialog y processEvents(): bool HojaCalculo::guardaArchivo(const QString &nombreArchivo) { QFile archivo(nombreArchivo); QProgressDialog progreso(this); progreso.setLabelText(tr(Guardando %1).arg(nombreArchivo)); progreso.setRange(0, CantidadFilas); progreso.setModal(true); for (int fila = 0; fila < CantidadFilas; ++fila) { progreso.setValue(fila); qApp->processEvents(); if (progreso.wasCanceled()) { archivo.remove(); return false; } for (int columna = 0; columna < CantidadColumnas; ++columna) { QString str = formula(fila, columna); if (!str.isEmpty()) out<<quint16(fila)<<quint16(columna)<<str; } } return true; }

7. Procesamiento de Eventos

39

Creamos un QProgressDialog con la variable CantidadFilas como el nmero total de pasos a ejecutar. Entonces, por cada paso, llamamos a setValue() para actualizar la barra de progreso. El objeto QProgressDialog automticamente calcula el porcentaje dividiendo el valor actual por la cantidad total de pasos. Luego llamamos a QApplication::processEvents() para procesar los eventos de redibujado o cualquier otro evento (por ejemplo, permitirle al usuario presionar el botn Cancelar). Si el usuario decide cancelar, abortamos el guardado y borramos el archivo. No llamamos a la funcin show() del QProgressDialog porque este lo hace por s mismo. QProgressDialog puede detectar si la operacin resultar demasiado corta, ya sea porque el archivo es pequeo o el equipo es demasiado rpido, y no se muestra. Aparte de las tcnicas de hilos mltiples, processEvents() y QProgressDialog, hay otra manera completamente diferente para tratar con operaciones largas: en vez de realizar el procesamiento cuando el usuario lo requiere, podemos postergarlo hasta que la aplicacin est inactiva. Esto puede funcionar si el procesamiento puede ser interrumpido sin perjuicio alguno y reanudado, ya que no sabemos cunto tiempo estar inactiva la aplicacin. En Qt, este tcnica puede ser implementada usando un temporizador de 0 milisegundos. Iremos realizando el proceso en cada evento del temporizador siempre y cuando no haya eventos pendientes. Presentamos un ejemplo de la funcin timerEvent() implementando esta tcnica: void HojaCalculo::timerEvent(QTimerEvent *evento) { if (evento->timerId() == miTimerId) { while (paso < CantidadPasos && !qApp->hasPendingEvents()) { ejecutarPaso(paso); ++paso; } } else { QTableWidget::timerEvent(evento); } } Si hasPendingEvents() devuelve true, paramos el procesamiento y le damos el control a Qt. El procesamiento continuar cuando no haya ms eventos pendientes.

8. Grficos En 2 y 3 Dimensiones

40

8. Grficos En 2 y 3 Dimensiones

Dibujando con QPainter Transformaciones Renderizado de Alta Calidad con QImage Impresin Grficos con Open GL

El motor para generar grficos en dos dimensiones (2D) de Qt se basa principalmente en la clase QPainter. Esta clase puede dibujar figuras geomtricas bsicas (puntos, lneas, rectngulos, elipses, arcos, lneas curvas, segmentos circulares, polgonos y curvas Bezier), as como tambin imgenes y texto. Aun ms, QPainter soporta caractersticas avanzadas tales como antialiasing (para textos y bordes de figuras), transparencias, rellenos con degradados y trazados vectoriales. Tambin soporta transformaciones, las cuales posibilitan el dibujo de grficos 2D independientes de la resolucin del dispositivo. QPainter puede ser usado para dibujar sobre cualquier dispositivo de dibujo, en concreto, cualquier clase que herede de QPaintDevice, como QWidget, QPixmap o QImage. Este es muy til cuando escribimos clases de widgets propios o de un tem, con una apriencia personalizada. Tambin puede ser usada en conjuncin con QPrinter para realizar impresiones o generar archivos PDF. Esto nos posibilita, a menudo, usar el mismo cdigo ya sea para mostrar datos por pantalla o producir reportes impresos. Una alternativa a QPainter es usar OpenGL. Esta es una librera estndar para generacin de grficos en dos o tres dimensiones. El mdulo QtOpenGL hace que sea fcil integrar cdigo OpenGL en aplicaciones realizadas con Qt.

Dibujando con QPainter


Para comenzar a trabajar sobre un dispositivo de dibujo (tpicamente un widget), simplemente creamos un objeto QPainter pasndole el puntero al dispositivo donde se quiere dibujar. Por ejemplo: void MiWidget::paintEvent(QPaintEvent *evento) { QPainter painter(this); ... } Podemos dibujar varias figuras usando las funciones draw...() de QPainter. La Figura 8.1 muestra algunas de las ms importantes. La clase QPainter contiene una serie de propiedades que determinan la manera en que se realiza el dibujado de las figuras. Algunas de estas son adoptadas desde el dispositivo de dibujo y otras son inicializadas con valores predeterminados. Las tres propiedades principales son pen(), brush() y font(): pen(): propiedad de tipo QPen. Es usado para dibujar lineas y los bordes de figura. Consiste en un color, un ancho, un estilo de linea, un estilo de cubierta y un estilo de unin.

8. Grficos En 2 y 3 Dimensiones

41

brush(): propiedad de tipo QBrush. Es un patrn usado para rellenar figuras geomtricas. Normalmente consta de un color y un estilo, pero tambin puede ser una textura (una imagen que se repite infinitamente) o un degradado. font(): propiedad de tipo QFont, es usada para dibujar texto. La fuente tiene muchos atributos, incluyendo una familia y un tamao de punto.

Estas propiedades pueden ser modificadas en cualquier momento usando las funciones setPen(), setBrush() y setFont(). Figura 8.1. Las funciones draw() ms usadas de QPainter

8. Grficos En 2 y 3 Dimensiones

42

Figura 8.2. Estilos de cubierta y de unin

Figura 8.3. Estilos de pluma

Figura 8.4. Estilos de pinceles predeterminados

Figura 8.5. Ejemplos de figuras geomtricas

8. Grficos En 2 y 3 Dimensiones

43

Veamos algunos ejemplos prcticos. Este es el cdigo necesario para dibujar la elipse mostrada en la Figura 8.5(a) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 12, Qt::DashDotLine, Qt::RoundCap)); painter.setBrush(QBrush(Qt::green, Qt::SolidPattern)); painter.drawEllipse(80, 80, 400, 240); La funcin setRenderHint() permite activar el antialiasing, haciendo que QPainter use distintas intensidades de color al dibujar los bordes de las figuras para reducir la distorsin visual que normalmente ocurre cuando una figura se convierte a pixeles. De esta manera se obtienen bordes suaves, siempre y cuando la plataforma y el dispositivo soporten dicha caracterstica. Este es el cdigo para dibujar el segmento circular mostrado en la Figura 8.5(b): QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(Qt::black, 15, Qt::SolidLine, Qt::RoundCap, Qt::MiterJoin)); painter.setBrush(QBrush(Qt::blue, Qt::DiagCrossPattern)); painter.drawPie(80, 80, 400, 240, 60 * 16, 270 * 16); Los ltimos dos argumentos a drawPie() son expresados en un dieciseisavo (1/16) de una porcin. Este es el cdigo para dibujar la curva Bezier mostrada en la Figura 8.5(c) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QPainterPath figura; figura.moveTo(80, 320); figura.cubicTo(200, 80, 320, 80, 480, 320); painter.setPen(QPen(Qt::black, 8)); painter.drawPath(figura); La clase QPainterPath puede generar figuras vectoriales arbitrarias mediante la conexin de elementos grficos bsicos: lneas rectas, crculos, polgonos, arcos, curvas Bezier (cuadrticas o cubicas) y otras figuras vectoriales. Las figuras vectoriales son la primitiva de dibujo definitiva en el sentido de que cualquier otra figura o combinacin de figuras puede ser representada mediante esta. Una figura se compone de un contorno y de un rea, encerrada por el contorno, que puede ser pintada con un pincel. En el ejemplo de la Figura 8.5(c), al no establecer un pincel, solo se dibuja el borde. Los tres ejemplos anteriores usan patrones de pinceles preestablecidos: Qt::SolidPattern, Qt::DiagCrossPattern, y Qt::NoBrush. En las aplicaciones modernas, el relleno mediante degradados est ganando terreno, dejando de lado a los patrones monocromaticos. Los degradados realizan una interpolacin entre dos o ms colores para obtener suaves transiciones. Son usados frecuentemente para producir efectos de tres dimensiones; por ejemplo el estilo Plastique usa degradados para dibujar los botones. Qt soporta tres tipos de degradados: lineal, cnico y radial. El ejemplo Temporizador de Horno presentado en la siguiente seccin combina los tres tipos en un solo widget para lograr un aspecto real.

8. Grficos En 2 y 3 Dimensiones

44

Figura 8.6. Pinceles de degradado de QPainter

Degradado lineal: esta definido por dos puntos de control y por una serie de puntos (denominados "color stops") ubicados sobre la linea que conecta los dos puntos de control. El degradado usado en la Figura 8.6 es creado usando el siguiente cdigo: QLinearGradient gradiente(50, 100, 300, 350); gradiente.setColorAt(0.0, Qt::white); gradiente.setColorAt(0.2, Qt::green); gradiente.setColorAt(1.0, Qt::black); Especificamos tres colores en tres posiciones diferentes entre los dos puntos de control. Las posiciones se especifican como valores de punto flotante entre 0 y 1, donde el cero corresponde al primer punto de control y el uno al segundo punto de control. Los colores establecidos sern interpolados para formar el degradado.

Degradado radial: esta definido por un punto central (x, y), un radio r, un punto focal (xf, yf), adicionalmente a los "stops colors". El punto central ms el radio forman un circulo. Los colores se esparcirn desde el punto focal (el cual puede ser cualquier punto dentro del crculo) hacia el exterior. Degradado cnico: esta definido por un punto central (x, y) y un ngulo . Los colores se esparcirn alrededor del punto central siguiendo la direccin de las agujas del reloj.

Hasta ahora solo hemos usado solo tres propiedades de QPainter (pen(), brush() y font()). QPainter posee ms propiedades que influencian la manera en que son dibujadas las figuras: background(): es un propiedad de tipo QBrush que es usada para pintar el fondo de las figuras, textos o imgenes cuando se establece la propiedad backgroundMode a Qt::OpaqueMode (por defecto es Qt::TransparentMode). brushOrigin(): es una propiedad de tipo QPoint que indica el punto desde donde se comienza a aplicar un patrn de relleno. clipRegion(): es una propiedad de tipo QRegion que marca el rea del dispositivo que puede ser dibujada. Todo lo que se realice fuera de esta no tendr ningn efecto.

8. Grficos En 2 y 3 Dimensiones

45

viewport(), window() y worldTransform(): estas tres propiedades determinan cmo QPainter mapea las coordenadas lgicas a las coordenadas fsicas del dispositivo. Por defecto, estas coinciden. El sistema de coordenadas ser analizado en la prxima seccin. compositionMode(): es una enumeracin que especifica cmo los nuevos pixeles dibujados debern interactan con los ya existentes. Por defecto est en "source over", en donde los pixeles se dibujan encima de los existentes. Esta caracterstica no est soportada por todos los dispositivos y la veremos ms adelante en este captulo.

En cualquier momento, podemos guardar el estado de nuestro objeto QPainter en una pila interna llamando a la funcin save() y restablecerlo ms tarde con la funcin restore(). Esto puede ser til si queremos cambiar temporalmente alguna configuracin, como veremos en la prxima seccin.

Transformaciones
En el sistema predeterminado de coordenadas de QPainter, el punto (0,0) se encuentra en la esquina superior izquierda del dispositivo de dibujo; las coordenadas x crecen hacia la derecha y las coordenadas y hacia abajo. Cada pixel ocupa un rea de tamao 1x1. Algo importante que debemos entender es que el centro del pixel se encuentra en la coordenada "medio pixel". Por ejemplo, el pixel de la esquina superior izquierda cubre un rea que va desde el punto (0,0) al punto (1,1), y su centro se encuentra en (0.5, 0.5). Si le pedimos a QPainter que dibuje un pixel, digamos en el punto (100,100), este desplazar las coordenadas 0.5 en cada direccin, obteniendo como resultado un pixel dibujado en (100.5, 100.5). Esto les podr parecer bastante acadmico al principio, pero tiene consecuencias importantes en la prctica. Primero, el desplazamiento solo ocurrir si el antialiasing esta desactivado (esto es por defecto); si esta activado y dibujamos un pixel negro en el punto (100,100), QPainter adems pintar cuatro pixeles (99.5, 99.5), (99.5, 100.5), (100.5, 99.5) y (100.5, 100.5) de color gris claro, para dar la impresin de un pixel que se extiende desde el centro hacia los cuatro puntos. Si no deseamos este efecto, podemos evitarlo especificando las coordenadas de "medio pixel", por ejemplo (100.5, 100.5). Cuando dibujamos figuras como lneas, rectngulo o elipses se aplica una regla parecida. La Figura 8.7 muestra el resultado de cmo vara la llamada a drawRect(2, 2, 6, 5) de acuerdo al ancho del lpiz, con el antialiasing desactivado. Es importante observar que un rectngulo de 6x5 dibujado con un lpiz de 1 pixel de ancho, cubre un rea efectiva de 6x7 pixeles. Este comportamiento es diferente en versiones anteriores de Qt, pero es esencial para poder lograr grficos vectoriales realmente escalables e independientes de la resolucin. Figura 8.7. Dibujando un rectngulo 6x5 sin atialiasing

Ahora que hemos entendido cmo funciona el sistema de coordenadas predeterminado, podemos adentrarnos en la forma como este puede ser modificado. Primero daremos algunas definiciones tiles en este contexto: Viewport: el viewport y el window, estn muy relacionados. El viewport es un rectngulo arbitrario especificado en coordenadas fsicas. Window: es el mismo rectngulo que "viewport" solo que especificado en coordenadas lgicas.

8. Grficos En 2 y 3 Dimensiones

46

Cuando damos una orden para realizar un dibujo especificamos la ubicacin de los puntos en coordenadas lgicas, estas son transformadas en coordenadas fsicas de manera algebraica, basndose en la configuracin del viewport de la ventana actual. Por defecto, "viewport" y "window" se establecen al rectngulo del dispositivo, coincidiendo el sistema de coordenadas fsico con el lgico. Por ejemplo, si tenemos un widget de 320x200, el "viewport" y el "window" tendrn este tamao. La conjuncin entre "viewport" y "window" hacen posible la realizacin de dibujos independiente del tamao o de la resolucin del dispositivo destino. Por ejemplo, si queremos que las coordenadas lgicas se extiendan desde el punto (-50,-50) hasta el punto (+50,+50) teniendo como centro el punto (0,0) podemos hacer lo siguiente: painter.setWindow(-50, -50, 100, 100); Los primeros dos valores especifican el origen, y el tercer y cuarto valor establecen el ancho y el alto respectivamente. Con esto, nos aseguramos que la coordenada lgica (-50,-50) ahora corresponda a la coordenada fsica (0,0) y la coordenada lgica (50,50) se corresponda a la coordenada fsica (320,200). En este ejemplo no hemos cambiado el "viewport". Figura 8.8. Convirtiendo coordenadas lgicas a coordenadas fsicas

Ahora nos dedicaremos a la "world matrix". Esta es una matriz de transformacin que se aplica adicionalmente a la conversin entre "window"-"viewport". Nos permite trasladar, escalar, rotar, o cizallar los tems que dibujamos. Por ejemplo, si queremos dibujar un texto en un ngulo de 45, podramos usar este cdigo: QMatrix matriz; matriz.rotate(45.0); painter.setMatrix(matriz); painter.drawText(rect, Qt::AlignCenter, tr("Ingresos")); La coordenada lgica que le pasamos a drawText() primero es transformada por la "world matrix", y luego mapeada a coordenadas fsicas usando las configuraciones window-viewport. Si especificamos varias transformaciones, estas sern aplicadas en el orden en que las fuimos creando. Por ejemplo: si queremos usar el punto (10,20) como punto pivote de rotacin, podemos primero mover el "window", realizar la rotacin y luego volver el "window" a su posicin original. QMatrix matriz; matriz.translate(-10.0, -20.0); matriz.rotate(45.0); matriz.translate(+10.0, +20.0); painter.setMatrix(matriz); painter.drawText(rect, Qt::AlignCenter, tr("Ingresos")); Una manera ms simple de realizar transformaciones es usar las siguientes funciones de QPainter: translate(), scale(), rotate() y shear(): painter.translate(-10.0, -20.0);

8. Grficos En 2 y 3 Dimensiones

47

painter.rotate(45.0); painter.translate(+10.0, +20.0); painter.drawText(rect, Qt::AlignCenter, tr("Ingresos")); Pero si queremos aplicar varias veces la misma transformacin, es mas eficiente almacenarla en un objeto QMatrix y asignarla a QPainter cada vez que la necesitemos. Figura 8.9. El widget TempHorno

Para ilustrar el uso de transformaciones, revisaremos el cdigo del widget "TempHorno" mostrado en la Figura 8.9. Este widget sigue el modelo de los temporizadores de cocina que se usaban antes de que fuera comn la incorporacin de relojes en los hornos. El usuario puede hacer click sobre una marca para establecer la duracin del temporizador. La rueda girar automticamente hasta alcanzar el cero, en este punto, emitir la seal tiempoTerminado(). class TempHorno : public QWidget { Q_OBJECT public: TempHorno(QWidget *parent = 0); void setDuracion(int segs); int duracion() const; void dibujar(QPainter *painter); signals: void tiempoTerminado(); protected: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); private: QDateTime horaFin; QTimer *timerActualizar; QTimer *timerFin; }; La clase TempHorno hereda de QWidget y reimplementa dos funciones virtuales: paintEvent() y mousePressEvent(). const const const const const double GradosPorMinuto = 7.0; double GradosPorSegundo = GradosPorMinuto / 60; int MaxMinutos = 45; int MaxSegundos = MaxMinutos * 60; int IntervaloActualizacion = 1;

Comenzamos definiendo unas cuantas constantes que controlarn la apariencia del widget.

8. Grficos En 2 y 3 Dimensiones

48

TempHorno::TempHorno(QWidget *parent) : QWidget(parent) { horaFin = QDateTime::currentDateTime(); timerActualizar = new QTimer(this); connect(timerActualizar, SIGNAL(timeout()), this, SLOT(update())); timerFin = new QTimer(this); timerFin->setSingleShot(true); connect(timerFin, SIGNAL(tiempoTerminado()), this, SIGNAL(tiempoTerminado())); connect(timerFin, SIGNAL(tiempoTerminado()), timerActualizar, SLOT(stop())); } En el constructor, creamos dos objetos QTimer: timerActualizar es usado para refrescar la apariencia del widget cada 1 segundo, y timerFin emite la seal tiempoTerminado() cuando el contador llega a 0. Como este ltimo solo necesita emitir la seal una vez, establecemos setSingleShot(true); ya que por defecto los temporizadores emiten seales repetidamente hasta que son detenidos o destruidos. La ultima llamada a connect() es una optimizacin para dejar de actualizar el widget cuando est inactivo. void TempHorno::setDuracion(int segs) { if (segs > MaxSegundos) { segs = MaxSegundos; } else if (segs <= 0) { segs = 0; } horaFin = QDateTime::currentDateTime().addSecs(segs); if (segs > 0) { timerActualizar->start(IntervaloActualizacion* 1000); timerFin->start(segs * 1000); } else { timerActualizar->stop(); timerFin->stop(); } update(); } La funcin setDuracion() establece la duracin del temporizador a un nmero de segundos dado. Calculamos la hora de finalizacin agregando la duracin a la hora actual (obtenida a travs de QDateTime::currentDateTime()) y la almacenamos en la variable privada horaFin. Por ltimo, llamamos a update() para que el widget se vuelva a dibujar. La variable horaFin es de tipo QDateTime. Ya que este tipo de datos puede contener tanto fecha como hora, evitamos el error que se ocasionara si estableciramos el tiempo actual antes de medianoche y que el conteo finalizara despus de esta. int TempHorno::duracion() const { int segs = QDateTime::currentDateTime().secsTo(horaFin); if (segs < 0) segs = 0; return segs; }

8. Grficos En 2 y 3 Dimensiones

49

La funcin duracion() devuelve la cantidad de segundos restantes para que el temporizador finalice. Si el temporizador est inactivo devuelve 0. void TempHorno::mousePressEvent(QMouseEvent *event) { QPointF point = event->pos() - rect().center(); double theta = atan2(-point.x(), -point.y()) * 180 / 3.14159265359; setDuracion(duracion() + int(theta / GradosPorSegundo)); update(); } Cuando el usuario realiza un click sobre el widget, buscamos la marca ms cercana utilizando una simple, pero eficaz, formula matemtica, y usamos el resultado obtenido como la nueva duracin y actualizamos el widget. La marca que el usuario presion ahora estar arriba de todo y se ira moviendo, mientras el tiempo transcurra, en sentido contrario a las agujas del reloj hasta llegar a cero. void TempHorno::paintEvent(QPaintEvent * /* event */) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); int lado = qMin(width(), height()); painter.setViewport((width() - lado) / 2, (height() - lado) / 2, lado, lado); painter.setWindow(-50, -50, 100, 100); dibujar(&painter); } En la funcin paintEvent(), configuramos un "viewport" del tamao del cuadrado ms grande que entre en el widget, y configuramos un "window" de tamao 100x100, que va desde el punto(-50,-50) hasta el punto (50,50). Usamos qMin para obtener el menor de dos argumentos, y as establecer el valor lado del cuadrado. Despus de esto llamamos a la funcin dibujar() que se encargara de dibujar el widget. Si no establecemos el "viewport" a un cuadrado, el widget se transformara en una elipse cuando, por cambios de tamao, este no tenga el mismo alto que ancho. Para evitar esta deformacin, debemos establecer el "vieport" y el "window" con la misma relacin de aspecto. Figura 8.10. El widget TempHomo en tres tamaos distintos

8. Grficos En 2 y 3 Dimensiones

50

Ahora nos centraremos en el cdigo que dibuja el widget: void TempHorno::dibujar(QPainter *painter) { static const int triangulo[3][2] = {{ -2, -49 }, { +2, -49 }, { 0, -47 } }; QPen penGrueso(palette().foreground(), 1.5); QPen penFino(palette().foreground(), 0.5); QColor azulLindo(150, 150, 200); painter->setPen(penFino); painter->setBrush(palette().foreground()); painter->drawPolygon(QPolygon(3, &triangulo[0][0])); Comenzamos dibujando el pequeo tringulo que marca la posicin cero en la parte superior del widget. El tringulo es definido por tres coordenadas fijas y dibujado por medio de la funcin drawPolygon(). Aqu vemos que una de las ventajas del mecanismo "windowviewport" es que, por ms que dibujemos el tringulo en coordenadas fijas, vamos a obtener un buen resultado cuando se cambie el tamao del widget. QConicalGradient gradienteConico(0, 0, -90.0); gradienteConico.setColorAt(0.0, Qt::darkGray); gradienteConico.setColorAt(0.2, azulLindo); gradienteConico.setColorAt(0.5, Qt::white); gradienteConico.setColorAt(1.0, Qt::darkGray); painter->setBrush(gradienteConico); painter->drawEllipse(-46, -46, 92, 92); Ahora dibujamos el crculo exterior y lo pintamos con un degradado cnico. El punto central del degradado est localizado en (0,0) y el ngulo utilizado es -90. QRadialGradient gradienteCirc(0, 0, 20, 0, 0); gradienteCirc.setColorAt(0.0, Qt::lightGray); gradienteCirc.setColorAt(0.8, Qt::darkGray); gradienteCirc.setColorAt(0.9, Qt::white); gradienteCirc.setColorAt(1.0, Qt::black); painter->setPen(Qt::NoPen); painter->setBrush(gradienteCirc); painter->drawEllipse(-20, -20, 40, 40); El circulo interior lo pintaremos con un degradado radial. Ubicamos el punto central y el punto focal en la coordenada (0,0) y usamos un radio de 20. QLinearGradient gradientePerilla(-7, -25, 7, -25); gradientePerilla.setColorAt(0.0, Qt::black); gradientePerilla.setColorAt(0.2, azulLindo); gradientePerilla.setColorAt(0.3, Qt::lightGray); gradientePerilla.setColorAt(0.8, Qt::white); gradientePerilla.setColorAt(1.0, Qt::black); painter->rotate(duracion() * GradosPorSegundo); painter->setBrush(gradientePerilla); painter->setPen(penFino); painter->drawRoundRect(-7, -25, 14, 50, 150, 50); for (int i = 0; i <= MaxMinutos; ++i) { if (i % 5 == 0) {

8. Grficos En 2 y 3 Dimensiones

51

painter->setPen(penGrueso); painter->drawLine(0, -41, 0, -44); painter->drawText(-15, -41, 30, 25, Qt::AlignHCenter | Qt::AlignTop,QString::number(i)); } else { painter->setPen(penFino); painter->drawLine(0, -42, 0, -44); } painter->rotate(-GradosPorMinuto); } } Con la funcin rotate() giramos el sistema de coordenadas. En el anterior sistema de coordenadas, la marca de 0 minutos estaba en la parte superior: ahora se mueve al lugar apropiado para marcar el tiempo restante. Dibujamos la perilla rectangular despus de la rotacin, ya que su inclinacin depende del ngulo de rotacin. En el ciclo for, dibujamos las marcas sobre el borde del circulo exterior y los nmeros para cada mltiplo de 5 minutos. El texto se dibuja en un rectngulo invisible debajo de la marca. Al final de cada iteracin, rotamos el lienzo 7 en contra de las agujas del reloj (lo que corresponde a un minuto). La prxima vez que dibujemos una marca, estar en una posicin distinta alrededor del circulo, aun cuando le pasemos las mismas coordenadas a las funciones drawLine() y drawText(). El cdigo del ciclo for tiene un pequeo defecto, el cual podra volverse aparente si realizramos demasiadas iteraciones. Cada vez que llamamos a rotate(), se genera una nueva "world matrix" mediante una rotacin. Al irse acumulando los errores de redondeo asociados a las operaciones aritmticas en punto flotante, producen una "world matrix" inexacta. Una manera de evitar esto es reescribir el cdigo usando las funciones save() y restore() para guardar y restablecer la matriz de transformacin original en cada iteracin. for (int i = 0; i <= MaxMinutos; ++i) { painter->save(); painter->rotate(-i * GradosPorMinuto); if (i % 5 == 0) { painter->setPen(penGrueso); painter->drawLine(0, -41, 0, -44); painter->drawText(-15, -41, 30, 25, Qt::AlignHCenter | Qt::AlignTop, QString::number(i)); } else { painter->setPen(penFino); painter->drawLine(0, -42, 0, -44); } painter->restore(); } Otra manera de evitar este error es calcular por nuestra cuenta las posiciones (x, y) usando sin() y cos() para dar con las posiciones a lo largo del crculo. Pero todava tendramos la necesidad de usar las operaciones de translacin y rotacin para dibujar el texto inclinado.

Renderizado de Alta Calidad con QImage


Cuando dibujamos, podemos encontrarnos con el compromiso de tener que elegir entre velocidad o precisin. Por ejemplo, en sistemas X11 y Mac OS X, las operaciones de dibujo sobre un QWidget o un QPixmap se basan en el sistema de dibujo nativo de la plataforma. En X11, esto nos asegura que la comunicacin con el servidor X se mantiene al mnimo; solo los comandos de dibujo son enviados en vez de los datos de la imagen actual. El principal inconveniente de esto es que Qt est limitado a las capacidades que soporta el sistema nativo:

8. Grficos En 2 y 3 Dimensiones

52

En X11, algunas caractersticas, como son antialiasing y soporte para coordenadas fraccionales, estn disponibles solo si la extensin X Render est presente en el servidor X. En MacOs X, el motor grfico de antialiasing usa diferentes algoritmos que X11 y Windows para dibujar polgonos, obteniendo resultados ligeramente diferentes.

Cuando la precisin es ms importante que la eficiencia, podemos dibujar en un QImage y copiar el resultado a la pantalla. Esta tcnica siempre usa el motor de dibujo interno de Qt, obteniendo resultados idnticos en todas las plataformas. La nica restriccin es que el objeto QImage que usemos para dibujar debe ser creado con alguno de los siguientes argumentos: 1) QImage::Format_RGB32 2) QImage::Format_ARGB32_Premultiplied. El formato ARGB32 premultiplicado es casi idntico al formato convencional ARGB32 (0xaarrggbb), la diferencia reside en que los canales rojo, verde y azul son "premultiplicados" con el valor del canal alfa. Esto hace que el valor RGB, el cual normalmente tiene un rango que va desde 0x00 a 0xFF, ahora posea una escala de 0x00 hasta el valor del canal alfa. Por ejemplo, el color azul con 50% de transparencia en el formato ARGB32 tiene un valor de 0x7F0000FF, mientras que en ARGB32 premultiplicado es de 0x7F00007F. Supongamos que queremos usar antialiasing para dibujar un widget, y queremos obtener un buen resultado aun cuando el sistema X11 no posea la extensin X Render. El controlador original del evento paintEvent(), el cual se basa en X Render para lograr el antialiasing, podra verse de esta manera: void MiWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); dibujar(&painter);//dibujar() ser la sustituta de draw() } A continuacin mostramos cmo quedara el cdigo anterior si utilizamos el motor grfico de Qt: void MiWidget::paintEvent(QPaintEvent *event) { QImage imagen(size(), QImage::Format_ARGB32_Premultiplied); QPainter imagenPainter(&imagen); imagenPainter.initFrom(this); imagenPainter.setRenderHint(QPainter::Antialiasing, true); imagenPainter.eraseRect(rect()); dibujar(&imagenPainter); imagenPainter.end(); QPainter widgetPainter(this); widgetPainter.drawImage(0, 0, imagen); } Creamos un objeto QImage en formato ARGB32 premultiplicado del mismo tamao que el widget, y un objeto QPainter para dibujar sobre la imagen. La llamada a initFrom() inicializa los valores de pen(), brush() y font() de QPainter con los valores del widget. El dibujo es realizado usando QPainter como siempre, y al final copiamos la imagen resultante encima del widget. Este mtodo produce resultados de alta calidad idnticos en todas las plataformas, con la excepcin del dibujado de las fuentes, que depende de las fuentes instaladas en el sistema. Una caracterstica realmente poderosa del motor grfico de Qt es el soporte para modos de composicin. Cada uno de estos modos especifica cmo los pixeles de origen y destino sern mezclados cuando se dibujen. El modo predeterminado es QImage::CompositionMode_SourceOver, el cual hace que el pixel que se va a dibujar sea mezclado con el pixel existente de manera tal que el componente alfa del origen defina la

8. Grficos En 2 y 3 Dimensiones

53

transparencia. La Figura 8.11 muestra el resultado de dibujar una mariposa semitransparente sobre un patrn verificador utilizando cada uno de los distintos modos. Los modos de composicin se establecen por medio de QPainter::setCompositionMode(). Por ejemplo, esta seria la forma de crear un objeto QImage que contenga una operacin XOR entre la mariposa y el patrn verificador: QImage imagenResultado = imagenPatron; QPainter painter(&imagenResultado); painter.setCompositionMode(QPainter::CompositionMode_Xor); painter.drawImage(0, 0, imagenMariposa); Figura 8.11. Modos de composicin de QPainter

Una caracterstica a tener en cuenta de la operacin QImage::CompositionMode_Xor es que tambin se aplica al canal alfa. Esta hace que si se aplica a dos puntos blancos (0xFFFFFFFF), obtengamos un punto transparente (0x00000000) en vez de uno negro (0xFF000000).

Impresin
La impresin en Qt es muy similar a realizar operaciones de dibujo sobre QWidget, QPixmap o QImage. Consta de los siguientes pasos: 1) Crear un objeto QPrinter que sirva como dispositivo de dibujo. 2) Mostrar un QPrintDialog, para permitirle al usuario seleccionar la impresora y otras opciones de impresin. 3) Crear un objeto QPainter que opere con el objeto QPrinter. 4) Dibujar sobre la pgina usando el objeto QPainter. 5) Llamar a la funcin QPrinter::newPage() para pasar a la siguiente pagina. 6) Repetir el paso 4 y el 5 hasta que se hayan impreso todas las pginas. En Windows y MacOs X, QPrinter usa los drivers de impresin del sistema. En Unix, se genera un PostScript y se enva a lp o lpr (o el programa establecido mediante QPrinter::setPrintProgram()). El objeto QPrinter tambin puede ser usado para generar archivos PDF con solo llamar a setOutputFormat(QPrinter::PdfFormat).

8. Grficos En 2 y 3 Dimensiones

54

Figura 8.12. Imprimiendo un objeto QImage

Comenzaremos con ejemplos simples que impriman solo en una hoja. El primer ejemplo muestra cmo imprimir un objeto QImage: void VentanaImpresion::imprimirImagen(const QImage &imagen) { QPrintDialog dialogoImpresion(&impresora, this); if (dialogoImpresion.exec()) { QPainter painter(&impresora); QRect rect = painter.viewport(); QSize tam = imagen.size(); tam.scale(rect.size(), Qt::KeepAspectRatio); painter.setViewport(rect.x(), rect.y(), tam.width(), tam.height()); painter.setWindow(imagen.rect()); painter.drawImage(0, 0, imagen); } } Asumimos que la clase VentanaImpresion tiene una variable de tipo QPrinter llamada impresora. Simplemente podramos haber creado el objeto QPrinter local al mtodo imprimirImagen(), pero no tendramos forma de recordar las preferencias del usuario entre distintas impresiones.

8. Grficos En 2 y 3 Dimensiones

55

Creamos un objeto QPrintDialog y lo mostramos por medio de la funcin exec(). Esta devuelve true si el usuario presiona el botn Aceptar del dialogo, sino devuelve false. Despus de la llamada a exec(), el objeto QPrinter est listo para usarse, aunque tambin es posible realizar una impresin sin usar un QPrintDialog, solamente establecemos las preferencias de impresin por medio de las funciones miembro del objeto QPrinter. A continuacin, creamos un objeto QPainter que nos permitir dibujar sobre el objeto QPrinter. Establecemos el "viewport" a un rectngulo con la misma relacin de aspecto que la imagen y el "window" al tamao de la imagen, luego dibujamos la imagen en la posicin (0,0). Predeterminadamente, el "window" de QPainter es inicializado de manera tal que la impresora parezca tener una resolucin similar a la pantalla (usualmente un valor entre 72 y 100 puntos por pulgadas), haciendo que sea fcil reutilizar el cdigo de dibujado del widget para imprimir. En el ejemplo, esto no importa porque hemos establecido nuestro propio "window". La impresin de items que no sobrepasan una pgina es muy simple, pero la mayora de las aplicaciones necesitan imprimir datos en varias pginas. Para esto, se dibuja el contenido de una pgina por vez intercalando un llamado a newPage() cada vez que se quiera pasar a una nueva. El problema que surge es que debemos determinar cunta informacin va a ser impresa en cada pgina. En Qt hay dos enfoques principales para manejar la impresin de documentos de varias pginas: Podemos convertir los datos a HTML e imprimirlos mediante el motor de texto enriquecido de Qt (QTextDocument). Podemos realizar el dibujo de las pginas manualmente.

Revisaremos cada enfoque por turnos. Como un ejemplo, imprimiremos una gua de flores compuesta por una lista de nombres y descripciones. Cada entrada es almacenada como una cadena de caracteres en formato "nombre: descripcin", por ejemplo: Miltonopsis santanae: Una especie de orqudea muy peligrosa. Ya que cada tem de la gua de flores es una cadena de caracteres, usaremos una QStringList para representarla. Esta es la funcin que imprime la lista de flores usando el motor de texto enriquecido de Qt: void VentanaImpresion::imprimirGuiaFlores(const QStringList &entradas) { QString html; foreach (QString entrada, entradas) { QStringList campos = entrada.split(": "); QString titulo = Qt::escape(campos[0]); QString desc = Qt::escape(campos[1]); html += "<table width=\"100%\" border=1 cellspacing=0>\n" "<tr><td bgcolor=\"lightgray\"><font size=\"+1\">" "<b><i>" + titulo + "</i></b></font>\n<tr><td>" + desc + "\n</table>\n<br>\n"; } imprimeHtml(html); } El primer paso es convertir la lista en HTML. Cada tem de la lista se transforma en una tabla con dos celdas. Usamos Qt::escape() para reemplazar los caracteres especiales &, <, > con las correspondientes entidades HTML(&amp;, &lt;,&gt;). Luego imprimimos el resultado por medio de la funcin imprimeHtml(): void VentanaImpresion::imprimeHtml(const QString &html) { QPrintDialog dialogoImpresion(&impresora, this);

8. Grficos En 2 y 3 Dimensiones

56

if (dialogoImpresion.exec()) { QPainter painter(&impresora); QTextDocument documentoTexto; documentoTexto.setHtml(html); documentoTexto.print(&impresora); } } La funcin imprimeHtml() muestra un QPrintDialog y se encarga de imprimir el documento. La misma puede ser reutilizada "tal como est" en cualquier otra aplicacin para imprimir cualquier pgina HTML. Figura 8.13. Imprimiendo una gua de flores usando un objeto QTextDocument

Convertir un documento a HTML y usar QTextDocument para imprimirlo es, por lejos, la mejor alternativa para imprimir informes y otros documentos complejos. En casos donde necesitemos ms control, podemos establecer el diseo y dibujo de la pgina a mano. Veamos cmo podemos usar este enfoque para imprimir la gua de flores: void VentanaImpresion::imprimirGuiaFlores( const QStringList &entradas) { QPrintDialog dialogoImpresion(&impresora, this); if (dialogoImpresion.exec()) { QPainter painter(&impresora); QList<QStringList> paginas; paginar(&painter, &paginas, entradas); imprimirPaginas(&painter, paginas); } } Despus de configurar la impresora y crear el objeto QPainter, llamamos a la funcin de soporte paginar() para que determine las entradas que deberan aparecer en cada pagina. El resultado de esto es una lista de objetos QStringLists que contiene los datos a imprimir en cada pgina. A esta lista la pasamos a la funcin imprimirPaginas().

8. Grficos En 2 y 3 Dimensiones

57

Por ejemplo, supongamos que la lista de flores est formada por 6 entradas, a las cuales nos referiremos como A, B, C, D, E y F. Ahora supongamos que hay espacio solo para A y B en la primera pgina; D, C y E en la segunda y F en la tercera. La lista de pginas debera contener una lista con los objetos [A, B] en la posicin 0, la lista [C, D, E] en la posicin 1 y la lista [F ] en la posicin 2. void VentanaImpresion::paginar(QPainter *painter, QList<QStringList> *paginas, const QStringList &entradas) { QStringList paginaActual; int altoPagina = painter->window().height() 2 * espacioGrande; int y = 0; foreach (QString entrada, entradas) { int alto = altoEntrada(painter, entrada); if (y + alto > altoPagina && !paginaActual.empty()) { paginas->append(paginaActual); paginaActual.clear(); y = 0; } paginaActual.append(entrada); y += alto + espacioMediano; } if (!paginaActual.empty()) paginas->append(paginaActual); } La funcin paginar() distribuye las entradas en cada pgina. Esta se basa en la funcin altoEntrada(), la cual calcula el alto de una entrada. Tambin toma en cuenta los espacios verticales vacos al principio y al final pgina, cuyo tamao est almacenado en la variable espacioGrande. Iteramos sobre las entradas y las vamos agregando a la pgina actual hasta que lleguemos a una entrada que no entre en el espacio en blanco de la pgina, entonces anexamos la pgina actual a la lista de pginas y comenzamos a trabajar en una nueva. int VentanaImpresion::altoEntrada(QPainter *painter, const QString &entrada) { QStringList campos = entrada.split(": "); QString titulo = campos[0]; QString desc = campos[1]; int anchoTexto = painter->window().width() 2 * espacioChico; int altoMax = painter->window().height(); painter->setFont(fuenteTitulo); QRect recTitulo = painter->boundingRect(0, 0, anchoTexto, altoMax, Qt::TextWordWrap, titulo); painter->setFont(fuenteDesc); QRect rectDesc = painter->boundingRect(0, 0, anchoTexto, altoMax, Qt::TextWordWrap, desc); return recTitulo.height() + rectDesc.height() + 4 * espacioChico; } La funcin altoEntrada() usa QPainter::boundingRect() para calcular el espacio vertical necesario para una entrada, La Figura 8.14 muestra el layout de un tem de la gua de flores y la representacin de la constantes espacioChico y espacioMediano.

8. Grficos En 2 y 3 Dimensiones

58

Figura 8.14. Layout de un tem de la gua de flores

void VentanaImpresion::imprimirPaginas(QPainter *painter, const QList<QStringList> &paginas) { int primeraPagina firstPage = impresora.fromPage() - 1; if (primeraPagina >= paginas.size()) return; if (primeraPagina == -1) primeraPagina = 0; int ultimaPagina = impresora.toPage() - 1; if (ultimaPagina == -1 || ultimaPagina >= paginas.size()) ultimaPagina = paginas.size() - 1; int cantidadPaginas = ultimaPagina - primeraPagina + 1; for (int i = 0; i < impresora.numCopies(); ++i) { for (int j = 0; j < cantidadPaginas; ++j) { if (i != 0 || j != 0) impresora.newPage(); int indice index; if (impresora.pageOrder() == QPrinter::FirstPageFirst) { indice = primeraPagina + j; } else { indice = ultimaPagina - j; } imprimirPagina(painter, paginas[indice], indice + 1); } } } El rol de la funcin imprimirPaginas() es enviar cada pagina a la impresora en el orden y cantidad de veces correcta. Usando la clase QPrintDialog, el usuario podra requerir la impresin de varias copias, un rango de pginas o que se imprima en orden inverso. Es nuestra responsabilidad respetar estas opciones o desactivarlas por medio de QPrintDialog::setEnabledOptions().

8. Grficos En 2 y 3 Dimensiones

59

Comenzamos por determinar el rango a imprimir. Esto lo hacemos obteniendo los valores de las funciones fromPage() y toPage() del objeto QPrinter, que devuelven el nmero de pgina de inicio y fin del rango de impresin seleccionado por el usuario o cero si no se escogi un rango. Como el ndice de la lista de pginas esta basado en cero, tenemos que restar uno a los valores de las pginas seleccionadas. Despus imprimimos cada pgina. El primer ciclo itera las veces necesarias para producir la cantidad de copias requeridas por el usuario. La mayora de los drivers de impresoras soportan copias mltiples, es por esto que QPrinter::numCopies() siempre devuelve 1. Si el driver de la impresora no puede manejar varias copias, numCopies() si devolver la cantidad de copias y ser la aplicacin la encargada de imprimirlas (en la impresin de QImage que vimos anteriormente en este capitulo, ignoramos la cantidad de copias por cuestiones de simplicidad). Figura 8.15. Imprimiendo una gua de flores usando un objeto QPainter

El ciclo for interno itera a travs de las pginas. Si la pgina actual no es la primera, llamamos a newPage() para limpiar la pgina anterior y empezar a trabajar con una nueva. Mediante la funcin imprimirPagina() cada pgina es enviada a la impresora. void VentanaImpresion::imprimirPagina(QPainter *painter, const QStringList &entradas, int numeroPagina) { painter->save(); painter->translate(0, espacioGrande); foreach (QString entrada, entradas) { QStringList campos = entrada.split(": "); QString titulo = campos[0]; QString desc = campos[1]; imprimirRecuadro(painter, titulo, fuenteTitulo, Qt::lightGray); imprimirRecuadro(painter, desc, fuenteDesc, Qt::white); painter->translate(0, espacioMediano); } painter->restore(); painter->setFont(fuentePie);

8. Grficos En 2 y 3 Dimensiones

60

painter->drawText(painter->window(), Qt::AlignHCenter | Qt::AlignBottom, QString::number(numeroPagina)); } La funcin imprimirPagina() recorre la gua de flores e imprime cada entrada mediante dos llamadas a la funcin imprimirRecuadro(); una para el ttulo y otra para la descripcin. Tambin se encarga de dibujar el nmero de cada pgina. Figura 8.16. Layout de pgina de la gua de flores

void VentanaImpresion::imprimirRecuadro(QPainter *painter, const QString &str, const QFont &fuente, const QBrush &pincel) { painter->setFont(fuente); int anchoCaja = painter->window().width(); int anchoTexto = anchoCaja - 2 * espacioChico; int altoMax = painter->window().height(); qglClearColor(Qt::black); QRect rectTexto = painter->boundingRect(espacioChico, espacioChico, anchoTexto, altoMax, Qt::TextWordWrap, str); int altoCaja = rectTexto.height() + 2 * espacioChico; painter->setPen(QPen(Qt::black, 2, Qt::SolidLine)); painter->setBrush(pincel); painter->drawRect(0, 0, anchoCaja, altoCaja); painter->drawText(rectTexto, Qt::TextWordWrap, str); painter->translate(0, altoCaja); } La funcin imprimirRecuadro() dibuja el borde de un recuadro y el texto dentro de este.

8. Grficos En 2 y 3 Dimensiones

61

Grficos con OpenGL


OpenGL es una API estndar para la generacin de grficos en dos y tres dimensiones. Las aplicaciones realizadas con Qt pueden dibujar grficos en 3D usando el mdulo QtOpenGL, el cual se basa en las libreras OpenGL instaladas en el sistema. En esta seccin se asume que el lector tiene conocimientos previos sobre la utilizacin de OpenGL. Si este es un mundo nuevo para usted, un buen lugar para comenzar a aprender es http://www.opengl.org/. Figura 8.17. La aplicacin Tetraedro

Generar grficos mediante OpenGL en un programa realizado con Qt es sencillo. Debemos subclasificar la clase QGLWidget, reimplementar algunas funciones virtuales y enlazar la aplicacin con la librera OpenGL (mediante el mdulo QtOpenGL). Ya que QGLWidget hereda de QWidget, podemos aplicar la mayor parte de lo que hemos visto hasta aqu. La principal diferencia radica en que se usan las funciones de OpenGL para realizar los dibujos en vez de QPainter. Para mostrar cmo trabaja, revisaremos el cdigo de la aplicacin Tetraedro mostrada en la Figura 8.17. La aplicacin presenta un tetraedro en tres dimensiones, con cada cara pintada de un color diferente. El usuario puede rotar la figura con solo arrastrar el puntero del ratn mientras mantiene presionado el botn del mismo. Para cambiar el color de una cara, basta con realizar un doble click y seleccionar el color del QColorDialog mostrado. class Tetraedro : public QGLWidget { Q_OBJECT public: Tetraedro(QWidget *parent = 0); protected: void initializeGL(); void resizeGL(int width, int height); void paintGL(); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseDoubleClickEvent(QMouseEvent *event); private: void dibujar(); int caraEnPosicion(const QPoint &pos); GLfloat rotacionX; GLfloat rotacionY; GLfloat rotacionZ; QColor colores[4];

8. Grficos En 2 y 3 Dimensiones

62

QPoint ultPos; }; La clase Tetraedro hereda de QGLWidget. Las funciones initializeGL(), resizeGL() y paintGL() son reimplementadas da la clase QGLWidget. Tetraedro::Tetraedro(QWidget *parent) : QGLWidget(parent) { setFormat(QGLFormat(QGL::DoubleBuffer | QGL::DepthBuffer)); rotacionX = -21.0; rotacionY = -57.0; rotacionZ = 0.0; colores[0] = Qt::red; colores[1] = Qt::green; colores[2] = Qt::blue; colores[3] = Qt::yellow; } En el constructor llamamos a la funcin QGLWidget::setFormat() para especificar las caractersticas del contexto e inicializamos las variables privadas de la clase. void Tetraedro::initializeGL() { glShadeModel(GL_FLAT); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); } La funcin initializeGL() es llamada solo una vez, antes de la llamada a paintGL(). Este es el lugar en donde podemos configurar el contexto de renderizado de OpenGL, definiendo la lista de pantallas y realizando otras inicializaciones. Todo el cdigo est compuesto de llamadas a funciones de OpenGL, excepto por qglClearColor(). Si queremos mantener todo en OpenGL estndar, podramos llamar a glClearColor() si trabajamos en modo RGBA y glClearIndex() en modo color indexado. void Tetraedro::resizeGL(int width, int height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); GLfloat x = GLfloat(width) / height; glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0); glMatrixMode(GL_MODELVIEW); } La funcin resizeGL() es llamada antes que se llame por primera vez a paintGL(), pero despus de la llamada a initializeGL(). sta tambin es llamada cada vez que el widget cambia de tamao. Este es el lugar en donde podemos configurar el "viewport" de OpenGL, las proyecciones y cualquier otro valor que dependa del tamao del widget. void Tetraedro::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); dibujar(); } La funcin paintGL() es llamada cada vez que el widget necesite redibujarse. Esto es muy parecido a usar QWidget::paintEvent(). El dibujo es realizado por la funcin privada dibujar().

8. Grficos En 2 y 3 Dimensiones

63

void Tetraedro::dibujar() { static const GLfloat static const GLfloat static const GLfloat static const GLfloat

P1[3] P2[3] P3[3] P4[3]

= = = =

{ { { {

0.0, -1.0, +2.0 }; +1.73205081, -1.0, -1.0 }; -1.73205081, -1.0, -1.0 }; 0.0, +2.0, 0.0 };

static const GLfloat * const coords[4][3] = { { P1, P2, P3 }, { P1, P3, P4 }, { P1, P4, P2 }, { P2, P4, P3 } }; glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -10.0); glRotatef(rotacionX, 1.0, 0.0, 0.0); glRotatef(rotacionY, 0.0, 1.0, 0.0); glRotatef(rotacionZ, 0.0, 0.0, 1.0); for (int i = 0; i < 4; ++i) { glLoadName(i); glBegin(GL_TRIANGLES); qglColor(colores[i]); for (int j = 0; j < 3; ++j) { glVertex3f(coords[i][j][0], coords[i][j][1], coords[i][j][2]); } glEnd(); } } En la funcin dibujar() realizamos el dibujado del widget tomando en cuenta las rotaciones de los ejes x, y, z y los colores almacenados en el vector colores. Todo el cdigo esta formado por llamadas a funciones de OpenGL, excepto por qglColor(). En vez de sta, podramos haber usado alguna de la funciones glColor3d() o glIndex(), dependiendo del modo de trabajo elegido.

void Tetraedro::mousePressEvent(QMouseEvent *event) { ultPos = event->pos(); } void Tetraedro::mouseMoveEvent(QMouseEvent *event) { GLfloat dx = GLfloat(event->x() - ultPos.x()) / width(); GLfloat dy = GLfloat(event->y() - ultPos.y()) / height(); if (event->buttons() & Qt::LeftButton) { rotacionX += 180 * dy; rotacionY += 180 * dx; updateGL(); } else if (event->buttons() & Qt::RightButton) { rotacionX += 180 * dy; rotacionZ += 180 * dx; updateGL(); } ultPos = event->pos(); }

8. Grficos En 2 y 3 Dimensiones

64

Las funciones mousePressEvent() y mouseMoveEvent() se reimplementan de QWidget para permitirle al usuario rotar la figura. El botn izquierdo del ratn permite rotar la figura sobre el eje x y el eje y, mientras que el botn derecho lo hace sobre el eje x y el eje z. Despus de modificar cualquiera de las variables rotacin, rotacin y rotacin llamamos a la funcin updateGL() para que actualice la escena. void Tetraedro::mouseDoubleClickEvent(QMouseEvent *event) { int cara = caraEnPosicion(event->pos()); if (cara != -1) { QColor color = QColorDialog::getColor(colores[cara], this); if (color.isValid()) { colores[cara] = color; updateGL() } } } La funcin mouseDoubleClickEvent() responde a la realizacin de un doble click sobre la figura y permite establecer el color de una cara de la misma. Por medio de caraEnPosicion() determinamos cul cara est situada bajo el cursor. El color lo obtenemos llamando a QColorDialog::getColor(), asignamos el color seleccionado al vector colores y llamamos a updateGL() para redibujar la escena. int Tetraedro::caraEnPosicion(const QPoint &pos) { const int TamMax = 512; GLuint buffer[TamMax]; GLint vista[4]; glGetIntegerv(GL_VIEWPORT, vista); glSelectBuffer(TamMax, buffer); glRenderMode(GL_SELECT); glInitNames(); glPushName(0); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); gluPickMatrix(GLdouble(pos.x()), GLdouble(vista[3] pos.y()), 5.0, 5.0, vista); GLfloat x = GLfloat(width()) / height(); glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0); dibujar(); glMatrixMode(GL_PROJECTION); glPopMatrix(); if (!glRenderMode(GL_RENDER)) return -1; return buffer[3]; } La funcin caraEnPosicion() devuelve el numero de cara que se encuentra en una posicin determinada o -1 si o hay nada en dicha posicin. El cdigo para determinar esto en OpenGL es un poco complicado. Esencialmente, lo que hacemos es dibujar la escena en modo GL_SELECT para aprovechar las capacidades de seleccin de OpenGL y as poder devolver el nmero de cara.

8. Grficos En 2 y 3 Dimensiones

65

ste es el archivo main.cpp: #include <QApplication> #include <iostream> #include "tetraedro.h" using namespace std; int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!QGLFormat::hasOpenGL()) { cerr<<"OpenGL no esta instalado en su sistema." <<endl; return 1; } Tetraedro tetraedro; tetraedro.setWindowTitle(QObject::tr("Tetraedro")); tetraedro.resize(300, 300); tetraedro.show(); return app.exec(); } Si OpenGL no se encuentra instalado en el sistema, mostramos un mensaje de error por consola y terminamos el programa inmediatamente. Para enlazar el mdulo QtOpenGL de la aplicacin con las libreras de OpenGL, necesitamos agregar la siguiente entrada al archivo .pro QT += opengl Con esto completamos la aplicacin Tetraedro. Para obtener mas informacin sobre el mdulo QtOpenGL, consulte la documentacin de referencia de QGLWidget, QGLFormat, QGLContext, QGLColormap y QGLPixelBuffer.

9. Arrastrar y Soltar

66

9. Arrastrar y Soltar

Habilitando el Mecanismo de Arrastrar y Soltar (drag and drop) Soporte de Tipos de Arrastre Personalizados Manejo del Portapapeles

Arrastrar y soltar es una forma moderna e intuitiva de transferir informacion dentro de una aplicacin o entre diferentes aplicaciones. Es, adems, a menudo provisto como soporte del portapapeles para mover y copiar datos. En este captulo, veremos cmo agregar soporte a una aplicacin para arrastrar y soltar y cmo manejar formatos personalizados. A continuacin vamos a mostrar la forma de reutilizar el cdigo de arrastrar y soltar para aadir soporte al portapapeles. Esta reutilizacin de cdigo es posible debido a que ambos mecanismos se basan en QMimeData, una clase que puede proveer datos en varios formatos.

Habilitando el Mecanismo de Arrastrar y Soltar (drag and drop)


Arrastrar y soltar implica dos acciones distintas: arrastre y liberacin. Los widgets de Qt pueden servir como sitios de arrastre, como sitios para soltar, o como ambos. Nuestro primer ejemplo muestra cmo hacer una aplicacin que acepte un arrastre iniciado por otra aplicacin. La aplicacin es una ventana principal con un QTextEdit como su widget central. Cuando el usuario arrastra un archivo de texto desde el escritorio o desde un explorador de archivos y lo suelta dentro de la aplicacin, la aplicacin carga el archivo dentro del QTextEdit. Aqu est la definicin de la clase del ejemplo MainWindow class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); private: bool readFile(const QString &fileName); QTextEdit *textEdit; }; La clase MainWindow reimplementa dragEnterEvent() y dropEvent() de QWidget. Puesto que el propsito del ejemplo es mostrar cmo arrastrar y soltar, gran parte de la funcionalidad que esperaramos que est en una clase de ventana principal se ha omitido.

9. Arrastrar y Soltar

67

MainWindow::MainWindow() { textEdit = new QTextEdit; setCentralWidget(textEdit); textEdit->setAcceptDrops(false); setAcceptDrops(true); setWindowTitle(tr("Text Editor")); } En el constructor, creamos un QTextEdit y lo establecemos como widget central. Por defecto, QTextEdit acepta arrastre de texto desde otras aplicaciones, y si el usuario suelta un archivo sobre l, se insertar el nombre del archivo en el texto. Dado que los eventos de soltar son propagados de hijo a padre, desactivando la opcion de soltar en el QTextEdit y habilitndola en la ventana principal, obtenemos los eventos de soltar para toda la ventana en el MainWindow. void MainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat("text/uri-list")) event->acceptProposedAction(); } La funcion dragEnterEvent() es llamada cada vez que el usuario arrastra un objeto dentro de un widget. Si llamamos la funcin acceptProposedAction() en el evento, indicamos que el usuario puede soltar el objeto arrastrado en este widget. Por defecto, el widget no aceptaria el arrastre. Qt automaticamente cambia el cursor para indicar al usuario si el widget es un sitio legtimo para soltar. Aqu queremos que al usuario se le permita arrastrar archivos y nada ms. Para ello, comprobamos el tipo MIME del arrastre. El tipo MIME text/uri-list se utiliza para almacenar una lista de identificadores de recursos universal (URI por sus siglas en ingles), que pueden ser nombres de archivos, direcciones URL (como rutas HTTP o FTP), u otros identificadores de recursos globales. El estandar de tipos MIME son definidos por la Autoridad de Numeros de Internet Asignados (IANA por sus siglas en ingles). Se componen de un tipo y de un subtipo, separados por una barra. Los tipos MIME son usados por el portapapeles y por el sistema de arrastrar y soltar para identificar diferentes tipos de datos. La lista oficial de los tipos MIME est disponible en http://www.iana.org/assignments/media-types/. void MainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urls = event->mimeData()->urls(); if (urls.isEmpty()) return; QString fileName = urls.first().toLocalFile(); if (fileName.isEmpty()) return; if (readFile(fileName)) setWindowTitle(tr("%1 - %2").arg(fileName) .arg(tr("Drag File"))); } La funcion dropEvent() es llamada cuando el usuario suelta un objeto sobre el widget. Hacemos un llamado a QMimeData::urls() para obtener una lista de QUrls. Normalmente, los usuarios slo arrastran un archivo a la vez, pero es posible que arrastren varios archivos mediante una seleccin. Si hay ms de un URL, o si la URL no es un nombre de archivo local, retornamos inmediatamente. QWidget tambin proporciona las funciones dragMoveEvent() y dragLeaveEvent(), pero la mayora de las aplicaciones no necesitan ser reimplementadas. El segundo ejemplo muestra cmo iniciar un arrastre y aceptarlo al ser soltado. Vamos a crear una subclase QListWidget que soporta arrastrar y soltar, y lo utilizan como un componente en la aplicacin Selector de Proyecto (Project Chooser) que se muestra en la Figura 9.1.

9. Arrastrar y Soltar

68

Figura 9.1. La aplicacin Selector de Proyecto

La aplicacin Selector de Proyecto le presenta al usuario dos widgets de listas, llenada con nombres. Cada list widget representa un proyecto. El usuario puede arrastrar y soltar los nombres del widget de listas para mover a una persona de un proyecto a otro. El cdigo de arrastrar y soltar est ubicado en la subclase QListWidget. Aqu esta la definicin de la clase: class ProjectListWidget : public QListWidget { Q_OBJECT public: ProjectListWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void dragEnterEvent(QDragEnterEvent *event); void dragMoveEvent(QDragMoveEvent *event); void dropEvent(QDropEvent *event); private: void startDrag(); QPoint startPos; }; La clase ProjectListWidget reimplementa cinco manejadores de eventos declarados en QWidget. ProjectListWidget::ProjectListWidget(QWidget *parent) : QListWidget(parent) { setAcceptDrops(true); } En el constructor, habilitamos la opcin de soltar en el widget de listas. void ProjectListWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) startPos = event->pos(); QListWidget::mousePressEvent(event); } Cuando el usuario presiona el botn izquierdo del ratn, almacenamos la posicin del ratn en la variable privada startPos. Llamamos a la implementacin de mousePressEvent() perteneciente a

9. Arrastrar y Soltar

69

QListWidget para asegurar que QListWidget tenga la oportunidad de procesar los eventos del ratn como de costumbre. void ProjectListWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) startDrag(); } QListWidget::mouseMoveEvent(event); } Cuando el usuario mueve el cursor del ratn mientras mantiene pulsado el botn izquierdo del ratn, se considera que se ha iniciado un arrastre. Calculamos la distancia entre la posicin actual del ratn y la posicn en la que se ha pulsado el botn izquierdo del ratn. Si la distancia es ms grande que la distancia de inicio de arrastre recomendada por QApplication (normalmente 4 pixeles), llamamos a la funcion privada startDrag() para iniciar el arrastre. Esto evita iniciar un arrastre slo porque la mano del usuario se sacude. void ProjectListWidget::startDrag() { QListWidgetItem *item = currentItem(); if (item) { QMimeData *mimeData = new QMimeData; mimeData->setText(item->text()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); drag->setPixmap(QPixmap(":/images/person.png")); if (drag->start(Qt::MoveAction) == Qt::MoveAction) delete item; } } En startDrag(), creamos un objeto de tipo QDrag con ProjectListWidget como su padre. Los objetos QDrag almacenan los datos en un objeto QMimeData. Para este ejemplo, proporcionamos los datos como una cadena de texto plano usando QMimeData::setText(). QMimeData proporciona muchas funciones para la manipulacion de los tipos de arrastre ms comunes (imgenes, URLs, colores, etc.) y puede manejar tipos MIME arbitrarios representados como QByteArrays. La llamada a QDrag::setPixmap() establece el cono que sigue el cursor mientras que el arrastre est teniendo lugar. La llamada QDrag::start() inicia la operacin de arrastre y bloquea hasta que el usuario suelte o cancele el arrastre. Se necesita una combinacin de las "acciones de arrastre" soportadas como argumento (Qt::CopyAction, Qt::MoveAction y Qt::LinkAction) y retorna la accin de arrastre que fue ejecutada (o Qt::IgnoreAction si ninguna fue ejecutada). La ejecucin de una accin depender de lo que el widget fuente soporte, de lo que el widget de destino soporte y de las teclas modificadoras que son presionadas cuando se produce la liberacin. Despues de la llamada a start(), Qt toma posesin del objeto de arrastre y lo borrar cuando ya no sea necesario. void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event) { ProjectListWidget *source = qobject_cast<ProjectListWidget *>(event->source()); if (source && source != this) { event->setDropAction(Qt::MoveAction); event->accept(); } }

9. Arrastrar y Soltar

70

El widget ProjectListWidget no slo origina el arrastre, tambien acepta el arrastre si proviene de otro ProjectListWidget en la misma aplicacin. QDragEnterEvent::source() retorna un puntero al widget que inici el arrastre si ese widget es parte de la misma aplicacin; de otro modo, retorna un puntero nulo. Usamos qobject_cast<T>() para asegurarnos que el arrastre proviene de un ProjectListWidget. Si todo es correcto, le decimos a Qt que estamos listos para aceptar la accin como una accin de movimiento. void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event) { ProjectListWidget *source = qobject_cast <ProjectListWidget *>(event->source()); if (source && source != this) { event->setDropAction(Qt::MoveAction); event->accept(); } } El codigo en dragMoveEvent() es idntico al que hicimos en dragEnterEvent(). Es necesario debido a que necesitamos reemplazar la implementacin de la funcion de QListWidget (en realidad, es a la de QAbstractItemView) void ProjectListWidget::dropEvent(QDropEvent *event) { ProjectListWidget *source = qobject_cast<ProjectListWidget *>(event->source()); if (source && source != this) { addItem(event->mimeData()->text()); event->setDropAction(Qt::MoveAction); event->accept(); } } En dropEvent(), recuperamos el texto arrastrado con QMimeData::text() y creamos un item con ese texto. Tambien necesitamos aceptar el evento como una "accin de movimiento" para decirle al widget fuente que ahora puede remover la version original del item arrastrado. Arrastrar y soltar es un mecanismo poderoso para transferir datos entre aplicaciones. Pero en algunos casos, es posible implementar arrastrar y soltar sin usar el mecanismo de arrastrado y soltado que Qt nos facilita. Si todo lo que queremos es mover datos entre un widget en la aplicacin, a menudo podemos simplemente reimplementar mousePressEvent() y mouseRelaseEvent().

Soporte de Tipos de Arrastre Personalizados


En los ejemplos vistos hasta ahora, nos hemos basado en el soporte de QMimeData para tiposde datos MIME comunes. Por ello, usamos QMimeData::setText() para crear un arrastre de texto, y se utiliz QMimeData:urls() para recuperar el contenido de un arrastre de tipo text/uri-list. Si queremos arrastrar texto plano, texto HTML, imgenes, URLs o colores, podemos utilizar QMimeData sin formalidad. Pero si queremos arrastrar datos personalizados, debemos elegir entre las siguientes alternativas: 1. Podemos proporcionar datos arbitrarios en forma de un QByteArray QMimeData::setData() y extraerlo ms adelante con el QMimeData::data(). utilizando

2. Podemos hacer una subclase QMimeData y reimplementar los mtodos formats() y retrieveData() para manejar nuestros tipos de datos personalizados. 3. Para las operaciones de arrastrar y soltar dentro de una sola aplicacin, podemos hacer una subclase de QMimeData y almacenar los datos con cualquier estructura de datos que queramos.

9. Arrastrar y Soltar

71

La primera opcin no implica ninguna subclasificacion, pero tiene algunos inconvenientes: Tenemos que convertir nuestra estructura de datos a un QByteArray aunque el arrastre no sea finalmente aceptado, y si queremos ofrecer varios tipos MIME para interactuar bien con una amplia gama de aplicaciones, tenemos que guardar los datos varias veces (una vez por cada tipo MIME). Si los datos son grandes, esto puede ralentizar la aplicacin innecesariamente. Las otras dos opciones pueden evitar o minimizar estos problemas. Nos dan un control completo y se pueden utilizar juntas. Para mostrar cmo estos mtodos funcionan, vamos a mostrar cmo agregar capacidades de arrastrar y soltar a un QTableWidget. El arrastre soportar los siguientes tipos MIME: text/plain, text/html, y el text/csv. Usando la primera opcin, el mtodo para iniciar un arrastre lucira de esta forma: void MyTableWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) startDrag(); } QTableWidget::mouseMoveEvent(event); } void MyTableWidget::startDrag() { QString plainText = selectionAsPlainText(); if (plainText.isEmpty()) return; QMimeData *mimeData = new QMimeData; mimeData->setText(plainText); mimeData->setHtml(toHtml(plainText)); mimeData->setData("text/csv", toCsv(plainText).toUtf8()); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); if (drag->start(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction) deleteSelection(); } La funcin privada startDrag() se llama desde mouseMoveEvent() para empezar a arrastrar una seleccin rectangular. Establecemos tipos text/plain y text/html utilizando setText() y setHtml(), y establecemos el tipo text/csv utilizando setData(), el cual recibe un tipo MIME arbitrario y un QByteArray. El cdigo para selectionAsString() es ms o menos lo mismo que la funcin Spreadsheet::copy() vista en el Captulo 4. QString MyTableWidget::toCsv(const QString &plainText) { QString result = plainText; result.replace("\\", "\\\\"); result.replace("\"", "\\\""); result.replace("\t", "\", \""); result.replace("\n", "\"\n\""); result.prepend("\""); result.append("\""); return result; } QString MyTableWidget::toHtml(const QString &plainText) { QString result = Qt::escape(plainText); result.replace("\t", "<td>"); result.replace("\n", "\n<tr><td>"); result.prepend("<table>\n<tr><td>"); result.append("\n</table>"); return result;

9. Arrastrar y Soltar

72

} Las funciones toCsv() y toHTML() convierten una cadena de "etiquetas y saltos de lnea" en un archivo CSV (valores separados por comas) o una cadena HTML. Por ejemplo, los datos: Red Green Blue Cyan Yellow Magenta Son convertidos en: "Red", "Green", "Blue" "Cyan", "Yellow", "Magenta" O a: <table> <tr><td>Red<td>Green<td>Blue <tr><td>Cyan<td>Yellow<td>Magenta </table> La conversin se realiza en la forma ms sencilla posible, usando QString::replace(). Para evitar los caracteres especiales de HTML, podemos usar Qt::escape(). void MyTableWidget::dropEvent(QDropEvent *event) { if (event->mimeData()->hasFormat("text/csv")) { QByteArray csvData = event->mimeData()->data("text/csv"); QString csvText = QString::fromUtf8(csvData); event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/plain")) { QString plainText = event->mimeData()->text(); event->acceptProposedAction(); } } Aunque se incluyen los datos en tres formatos diferentes, aceptamos solamente dos de ellos en dropEvent(). Si el usuario arrastra las celdas de un QTableWidget a un editor HTML, queremos que las celdas se conviertan en una tabla HTML. Pero si el usuario arrastra HTML arbitrario en un QTableWidget, no vamos a querer aceptarlo. Para que este ejemplo funcione, tambin tenemos que llamar a setAcceptDrops(true) y setSelectionMode(ContiguousSelection) en el constructor de MyTableWidget. A continuacin, vamos a realizar nuevamente el ejemplo, pero esta vez vamos a hacer una subclase de QMimeData para posponer o evitar las conversiones potencialmente costosas entre QTableWidgetItems y QByteArray. Aqu est la definicin de nuestra subclase: class TableMimeData : public QMimeData { Q_OBJECT public: TableMimeData(const QTableWidget *tableWidget, const QTableWidgetSelectionRange &range); const QTableWidget *tableWidget() const { return myTableWidget; } QTableWidgetSelectionRange range() const { return myRange; } QStringList formats() const; protected: QVariant retrieveData(const QString &format, QVariant::Type preferredType) const; private: static QString toHtml(const QString &plainText);

9. Arrastrar y Soltar

73

static QString toCsv(const QString &plainText); QString text(int row, int column) const; QString rangeAsPlainText() const; const QTableWidget *myTableWidget; QTableWidgetSelectionRange myRange; QStringList myFormats; }; En lugar de almacenar los datos reales, almacenamos un QTableWidgetSelectionRange que especifica qu celdas estn siendo arrastradas y mantiene un puntero al QTableWidget. Las funciones formats() y retrieveData() son reimplementaciones de QMimeData. TableMimeData::TableMimeData(const QTableWidget *tableWidget, const QTableWidgetSelectionRange &range) { myTableWidget = tableWidget; myRange = range; myFormats << "text/csv" << "text/html" << "text/plain"; } En el constructor, inicializamos las variables privadas. QStringList TableMimeData::formats() const { return myFormats; } La funcin formats() devuelve una lista de tipos MIME proporcionada por el objeto tipo MIME. El orden preciso de los formatos suele ser irrelevante, pero es una buena prctica poner los "mejores" formatos primero. Las aplicaciones que admiten muchos formatos algunas veces usarn el primero que coincida. QVariant TableMimeData::retrieveData(const QString &format, QVariant::Type preferredType) const { if (format == "text/plain") { return rangeAsPlainText(); } else if (format == "text/csv") { return toCsv(rangeAsPlainText()); } else if (format == "text/html") { return toHtml(rangeAsPlainText()); } else { return QMimeData::retrieveData(format, preferredType); } } La funcin retrieveData() devuelve los datos para un tipo MIME dado como QVariant. El valor del parmetro formato es normalmente una de las cadenas devueltas por formats(), pero no podemos asumir eso, dado que no todas las aplicaciones comprueban el tipo MIME en por medio de formats(). Las funciones text(), html(), urls(), imageData(), colorData() y data() proporcionadas por QMimeData se implementan en trminos de retrieveData(). El parmetro preferredType nos da una pista sobre qu tipo hay que poner en el QVariant. Aqu, lo ignoramos y confiamos en que QMimeData convertir el valor de retorno en el tipo deseado, si es necesario. void MyTableWidget::dropEvent(QDropEvent *event) { const TableMimeData *tableData = qobject_cast<const TableMimeData *> (event->mimeData()); if (tableData) {

9. Arrastrar y Soltar

74

const QTableWidget *otherTable = tableData->tableWidget(); QTableWidgetSelectionRange otherRange = tableData->range(); event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/csv")) { QByteArray csvData = event->mimeData()->data("text/csv"); QString csvText = QString::fromUtf8(csvData); event->acceptProposedAction(); } else if (event->mimeData()->hasFormat("text/plain")) { QString plainText = event->mimeData()->text(); event->acceptProposedAction(); } QTableWidget::mouseMoveEvent(event); } La funcin dropEvent() es similar a la que haba anteriormente en esta seccin, pero esta vez la optimizamos comprobando primero si podemos convertir con seguridad el objeto QMimeData a un TableMimeData. Si la instruccin qobject_cast <T>() funciona, significa que el arrastre se origin por un objeto de tipo MyTableWidget en la misma aplicacin, y podemos acceder directamente a los datos de tabla en vez de ir a travs de la API de QMimeData. Si la instruccin falla, extraemos los datos de la forma estndar. En este ejemplo, codificamos el texto CSV con la codificacin UTF-8. Si queremos estar seguros de utilizar la codificacin correcta, podramos utilizar el parmetro charset del tipo MIME text/plain para especificar una codificacin explcita. Aqu estn algunos ejemplos: text/plain;charset=US-ASCII text/plain;charset=ISO-8859-1 text/plain;charset=Shift_JIS text/plain;charset=UTF-8

Manejo del Portapapeles


La mayora de las aplicaciones hacen uso del manejo del portapapeles integrado de Qt de un modo u otro. Por ejemplo, la clase QTextEdit proporciona los slots cut(), copy () y paste() as como atajos de teclado, de manera que, poco o ningn cdigo adicional se requiera. Al escribir nuestras propias clases, podemos acceder al portapapeles a travs QApplication::clipboard(), que devuelve un puntero al objeto QClipboard de la aplicacin. El manejo del portapapeles del sistema es fcil: Llamar a setText(), setImage(), o setPixmap() para poner los datos en el portapapeles, y llamar a text(), image(), o pixmap() para recuperar datos desde el portapapeles. Ya hemos visto ejemplos de uso del portapapeles en la aplicacin de hoja de clculo en el Captulo 4. Para algunas aplicaciones, la funcionalidad integrada podra no ser suficiente. Por ejemplo, podramos proporcionar datos que no son slo texto o imagen, o queremos proporcionar datos en muchos formatos diferentes para la mxima interoperabilidad con otras aplicaciones. La cuestin es muy similar a lo que nos encontramos antes con arrastrar y soltar, y la respuesta tambin es similar: Podemos hacer una subclase de QMimeData y reimplementar unas pocas funciones virtuales. Si nuestra aplicacin es compatible con arrastrar y soltar a travs de una subclase de QMimeData, simplemente podemos volver a utilizar la subclase QMimeData y ponerla en el portapapeles con la funcin setMimeData(). Para recuperar los datos, podemos llamar a mimeData() en el portapapeles. En X11, por lo general es posible pegar una seleccin haciendo clic en el botn central del ratn (para uno de tres botones). Esto se hace utilizando un portapapeles de "seleccin" separado. Si quieres que tus widgets soporten este tipo de portapapeles, de la misma manera que con el tipo estandar, debes pasar QClipboard::Selection como un argumento adicional a las diferentes llamadas al portapapeles. Por

9. Arrastrar y Soltar

75

ejemplo, aqu est la forma en que se reimplementara mouseReleaseEvent() en un editor de texto para soportar el pegado con el botn central del ratn: void MyTextEditor::mouseReleaseEvent(QMouseEvent *event) { QClipboard *clipboard = QApplication::clipboard(); if (event->button() == Qt::MidButton && clipboard->supportsSelection()) { QString text = clipboard->text(QClipboard::Selection); pasteText(text); } } En X11, la funcin supportsSelection() devuelve true. En otras plataformas, devuelve false. Si deseamos que se nos notifique cada vez que el contenido del portapapeles cambie, podemos conectar la seal QClipboard::dataChanged() a un slot personalizado.

10. Clases para Visualizar Elementos (Clases Item View)

76

10. Clases para Visualizar Elementos (Clases Item View)

Usando las Clases Item View de Qt Usando Modelos Predefinidos Implementando Modelos Personalizados Implementando Delegados Personalizados

Muchas aplicaciones dejan que el usuario vea, busque, y edite elementos individuales pertenecientes a un conjunto de datos. Dichos datos pueden provenir de un archivo, de una base de datos o de un servidor en la red. El enfoque tradicional para trabajar con conjuntos de datos es usar las clases que Qt provee para visualizar elementos (denominadas tem view classes en ingls). En versiones anteriores de Qt, el contenido de datos en los widgets visualizadores de items era cargado completamente del conjunto de datos que iba a mostrar; el usuario poda realizar las operaciones de bsqueda y edicin directamente sobre los elementos contenidos en este, y en algn punto los cambios eran enviados al origen de datos. Aunque esta tcnica es simple de entender y aplicar, no escala bien cuando el conjunto de datos es demasiado grande y no se presta a situaciones donde queremos usar varias vistas para mostrar los mismos datos en dos o mas widgets distintos. El lenguaje Smalltak populariz una tcnica flexible para visualizar grandes cantidades de datos: el modelo vista-controlador (MVC por sus siglas en ingles: Model View Controller). En esta tcnica, el modelo representa el conjunto de datos y es responsable de recuperar aquellos que necesita mostrar la vista y de guardar los cambios realizados en ellos. Cada tipo de conjunto de datos tiene su propio modelo, aunque la API que proporciona el modelo a las vistas es uniforme sin importar el tipo de datos subyacente. La vista se encarga de presentar los datos al usuario. Cuando el conjunto de datos es grande, la vista solo muestra una pequea parte de ellos a la vez, de manera tal que el modelo nicamente tiene que recuperar una pequea porcin de datos del origen. El controlador es un mediador entre el usuario y la vista, convirtiendo las acciones del usuario en requerimientos de navegacin o edicin, los cuales la vista transmite al modelo cuando sea necesario. Figura 10.1. Arquitectura del enfoque modelo/vista de Qt

Qt provee una arquitectura modelo/vista inspirada en el enfoque MVC. En Qt, el modelo se comporta igual que en el enfoque clsico. Pero, en lugar del controlador, se usa una abstraccin ligeramente diferente: el delegado. El delegado es usado para proveer un control fino sobre el dibujado y edicin de los elementos. Qt proporciona un delegado por defecto para cada tipo de vista. Esto es suficiente para la mayora de las aplicaciones, por lo que usualmente no necesitamos preocuparnos de ellos. Usando la arquitectura modelo/vista provista por Qt, podemos usar modelos que solo obtienen los datos que la vista necesita mostrar. Esto hace que la manipulacin de grandes cantidades de datos sea rpida y con un consumo de memoria menor que el proceso de cargar todos los datos a la vez. Y mediante el registro de un

10. Clases para Visualizar Elementos (Clases Item View)

77

modelo en dos o ms vistas, podemos dar al usuario la oportunidad de ver e interactuar con los mismos datos de diferentes maneras, con una pequea sobrecarga. Qt se encarga de mantener las vistas sincronizadas, reflejando los cambios realizados en una a las dems. Un beneficio adicional de esta arquitectura es que, si decidimos modificar el tipo de almacenamiento de los datos, solo necesitamos cambiar el modelo; las vistas continuarn comportndose correctamente. Figura 10.2. Un modelo puede proporcionar datos a mltiples vistas

En muchas situaciones, solo necesitamos presentar una cantidad relativamente pequea de datos al usuario. Para estos casos, podemos usar las clases de Qt que nos simplifican el trabajo: QListWidget, QTableWidget y QTreeWidget, y llenarlas con tems directamente. Estas se comportan de una manera similar a las clases para visualizar elementos provistas en versiones anteriores de Qt. Almacenan los datos en "items" (por ejemplo, un QTableWidget contiene mltiples QTableWidgetItems). Internamente usan modelos personalizados que le permiten mostrar los elementos en la vista. Para grandes cantidades de datos, por lo general, duplicarlos no es una buena opcin. En estos casos, podemos usar una conjuncin entre una vista (QListView, QTableView y QTreeView) y un modelo de datos, el cual puede ser un modelo propio o uno de los predefinidos proporcionados por Qt. Por ejemplo, si el conjunto de datos est almacenado en una base de datos, podemos combinar un QTableView con un QsqlTableModel.

Usando las Clases Item View de Qt


Utilizar estas clases es por lo general ms simple que definir un modelo propio y es apropiado cuando no necesitamos los beneficios de separar la vista de los datos a mostrar. Hemos usado esta tcnica en el Captulo 4 cuando subclasificamos QTableWidget y QTableWidgetItem para implementar la funcionalidad de la hoja de clculo. En esta seccin mostraremos como utilizar estas clases para presentar un conjunto de elementos al usuario. En el primer ejemplo crearemos un QListWidget de solo lectura, en el segundo un QTableWidget editable y en el tercer ejemplo un QTreeWidget de solo lectura. Comenzaremos con un dialogo simple que le permitir al usuario seleccionar un smbolo para diagramas de flujo de una lista. Cada elemento est formado por un icono, un texto descriptivo y un identificador nico. Empecemos con un extracto del archivo cabecera del dialogo: class SeleccionaSimboloDiagramaFlujo : public QDialog { Q_OBJECT public: SeleccionaSimboloDiagramaFlujo(const QMap<int, QString> &mapSimbolo, QWidget *parent = 0); int idSeleccionado() const { return id; } void done(int result); };

10. Clases para Visualizar Elementos (Clases Item View)

78

Figura 10.3. La aplicacin Seleccionador de Simbolos

Al constructor del dialogo le debemos pasar un objeto QMap<int,QString>. Podemos recuperar el identificador de un elemento por medio de la funcin idSeleccionado() (que devolver -1 si no hay ninguno seleccionado). SeleccionaSimboloDiagramaFlujo::SeleccionaSimboloDiagramaFlujo( const QMap<int, QString> &mapSimbolo, QWidget *parent): QDialog(parent) { id = -1; widgetLista = new QListWidget; widgetLista->setIconSize(QSize(60, 60)); QMapIterator<int, QString> it(mapSimbolo); while (it.hasNext()) { it.next(); QListWidgetItem *item = new QListWidgetItem( it.value(), widgetLista); item->setIcon(iconoDeSimbolo(it.value())); item->setData(Qt::UserRole, it.key()); } } Comenzamos por inicializar la variable id (que nos indica el ultimo identificador seleccionado) a -1. A continuacin creamos un QListWidget. Mediante un iterador recorremos los elementos de la lista de smbolos incluidos en el QMap creando un QListWidgetItem para cada uno. El constructor de QListWidgetItem toma como argumento un QString que representa el texto a mostrar, seguido por el QListWidget padre. Despus establecemos el icono del QListWidgetItem y llamamos a setData() para agregarle un identificador al mismo. La funcin privada iconoDeSimbolo() devuelve un objeto QIcon perteneciente a un elemento determinado. La clase QListWidgetItem tiene varios roles, cada uno de los cuales tiene un dato asociado de tipo QVariant. Los roles ms comunes son Qt::DisplayRole, Qt::EditRole y Qt::IconRole, y cada uno de estos tiene funciones de lectura y escritura propias (como setText(), setIcon(), etc), pero adems de estos existen otros roles. Tambin podemos definir roles personales especificando un valor numrico mayor o igual a Qt::UserRole. En nuestro ejemplo usamos Qt::UserRole para almacenar el identificador de cada elemento.

10. Clases para Visualizar Elementos (Clases Item View)

79

Omitimos el cdigo del constructor en el cual se crean los botones, se ubican los widgets y se establece el titulo de la ventana. void SeleccionaSimboloDiagramaFlujo::done(int result) { id = -1; if (result == QDialog::Accepted) { QListWidgetItem *item = widgetLista->currentItem(); if (item) id = item->data(Qt::UserRole).toInt(); } QDialog::done(result); } A la funcin done() la reimplementamos de la clase QDialog, y es llamada cuando el usuario presiona el botn OK o el botn Cancelar. Si se presiona OK, obtenemos el id del elemento seleccionado por medio de la funcin data(). Si estuviramos interesados en el texto del elemento, podramos obtenerlo por medio de item->data(Qt::DisplayRole).toString() o, lo que es ms conveniente, item->text(). Por defecto, la clase QListWidget presenta datos en modo de solo lectura. Si queremos que el usuario edite los datos mostrados, podramos establecer el disparador de edicin por medio de QAbstractItemView::setEditTriggers(); por ejemplo, al usar QAbstractItemView::AnyKeyPressed, el usuario puede editar los datos de un elemento con solo empezar a escribir. Por otro lado, tambin podramos proveer un botn Editar (y por supuesto Agregar y Eliminar) y conectarlos a los slots que manejaran las operaciones de edicin programticamente. Ahora que ya hemos visto como mostrar y seleccionar elementos, pasaremos a un ejemplo en donde podamos editar los datos. De nuevo usamos un dialogo, pero esta vez mostraremos un conjunto de coordenadas (x,y) que el usuario puede modificar. Figura 10.4. La aplicacin Configurador de Coordenadas

Como en el ejemplo anterior, solo nos centraremos en el cdigo relevante sobre el manejo de los elementos a mostrar, comenzando con el constructor: ConjuntoCoordenadas::ConjuntoCoordenadas(QList<QPointF> *coords, QWidget *parent) : QDialog(parent) { coordenadas = coords; widgetTabla = new QTableWidget(0, 2); widgetTabla->setHorizontalHeaderLabels(QStringList() << tr("X") << tr("Y")); for (int fila = 0; fila < coordenadas->count(); ++fila) {

10. Clases para Visualizar Elementos (Clases Item View)

80

QPointF punto = coordenadas->at(fila); agregarFila(); widgetTabla->item(fila, 0)-> setText(QString::number(punto.x())); widgetTabla->item(fila, 1)-> setText(QString::number(punto.y())); } } El constructor de QTableWidget toma la cantidad inicial de filas y columnas a mostrar. Cada elemento es representado por un objeto QTableWidgetItem, incluyendo los encabezados horizontales y los verticales. La funcin setHorizontalHeaderLabels() se encarga de establecer el texto de cada encabezado de columna, utilizando para ello la lista de cadenas de caracteres que le hemos pasado como parmetro. Por defecto, QTableWidget etiqueta los encabezados verticales con el nmero de fila, comenzando por el nmero 1, por lo que no debemos preocuparnos de establecerlos manualmente. Una vez que tenemos creado y centrado el texto de las columnas, recorremos el conjunto de datos que contiene las coordenadas. Para cada par, creamos dos objetos QTableWidgetItems, uno para la coordenada x y otro para la coordenada y. Los elementos generados se agregan a QTableWidget por medio de la funcin setItem(), a la cual hay que indicarle la posicin de insercin (nmero de fila y de columna). La clase QTableWidget siempre permite la edicin de los elementos. El usuario puede modificar cualquier celda seleccionada con solo presionar F2 o simplemente comenzando a escribir. Los cambios realizados en la vista sern automticamente reflejados en el QTableWidgetItem apropiado. Si queremos prevenir la edicin debemos llamar a setEditTriggers(QAbstractItemView::NoEditTriggers). void ConjuntoCoordenadas::agregarFila() { int fila = widgetTabla->rowCount(); widgetTabla->insertRow(fila); QTableWidgetItem *item0 = new QTableWidgetItem; item0->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); widgetTabla->setItem(fila, 0, item0); QTableWidgetItem *item1 = new QTableWidgetItem; item1->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); widgetTabla->setItem(fila, 1, item1); widgetTabla->setCurrentItem(item0); } El slot agregarFila() es invocado cada vez que el usuario presiona el botn Agregar Fila. Para eso utilizamos la funcin insertRow(). Si el usuario intenta modificar alguna celda de la nueva fila, QTableWidget automticamente crear un nuevo objeto QTableWidgetItem vaco. void ConjuntoCoordenadas::done(int result) { if (result == QDialog::Accepted) { coordenadas->clear(); for (int fila = 0; fila < widgetTabla->rowCount(); ++fila) { double x = widgetTabla->item(fila, 0) ->text().toDouble(); double y = widgetTabla->item(fila, 1) ->text().toDouble(); coordenadas->append(QPointF(x, y));

10. Clases para Visualizar Elementos (Clases Item View)

81

} } QDialog::done(result); } Por ltimo, cuando el usuario presiona el botn OK, borramos la lista de coordenadas recibidas y creamos una nueva basada en los elementos contenidos en el QtableWidget. En nuestro tercer ejemplo, veremos algunos fragmentos de una aplicacin que nos ilustrarn en el uso de la clase QTreeWidget. Esta, predeterminadamente, presenta datos de solo lectura. Figura 10.5. La aplicacin Visor de Configuraciones

Aqu mostramos un extracto del cdigo del constructor: VisorConfiguraciones::VisorConfiguraciones(QWidget *parent): QDialog(parent) { organizacion = "Trolltech"; aplicacion = "Designer"; widgetArbol = new QTreeWidget; widgetArbol->setColumnCount(2); widgetArbol->setHeaderLabels(QStringList() << tr("Clave") << tr("Valor")); widgetArbol->header()->setResizeMode(0, QHeaderView::Stretch); widgetArbol->header()->setResizeMode(1, QHeaderView::Stretch); setWindowTitle(tr("Visor de Configuraciones")); leerConfiguraciones(); } Para acceder a las configuraciones de la aplicacin, debemos crear un objeto QSettings pasndole el nombre de la organizacin y el de la aplicacin como parmetros, para esto utilizamos las variables organizacion y aplicacion. Luego creamos un nuevo QTreeWidget y llamamos a la funcin leerConfiguraciones(). void VisorConfiguraciones::leerConfiguraciones() { QSettings config(organizacion, aplicacion);

10. Clases para Visualizar Elementos (Clases Item View)

82

widgetArbol->clear(); agregarConfiguraciones(config, 0, ""); widgetArbol->sortByColumn(0); widgetArbol->setFocus(); setWindowTitle(tr("Visor de Configuraciones - %1 by %2") .arg(aplicacion).arg(organizacion)); } Los valores de configuracin de una aplicacin son guardados en una jerarqua de claves y valores. La funcin privada agregarConfiguraciones() recibe un objeto QSettings, un QTreeWidgetItem padre y el grupo actual. Un grupo de configuraciones es el equivalente QSettings a un directorio. Esta funcin recorre una estructura de rbol arbitraria llamndose as misma recursivamente. La llamada inicial desde leerConfiguraciones() pasa un cero como padre para representar el elemento raz. void VisorConfiguraciones::agregarConfiguraciones (QSettings &config, QTreeWidgetItem *parent, const QString &grupo) { QTreeWidgetItem *item; config.beginGroup(grupo); foreach (QString clave, config.childKeys()) { if (parent) { item = new QTreeWidgetItem(parent); } else { item = new QTreeWidgetItem(widgetArbol); } item->setText(0, clave); item->setText(1, config.value(clave).toString()); } foreach (QString grupo, config.childGroups()) { if (parent) { item = new QTreeWidgetItem(parent); } else { item = new QTreeWidgetItem(widgetArbol); } item->setText(0, grupo); agregarConfiguraciones(config, item, grupo); } config.endGroup(); } En la funcin agregarConfiguraciones() creamos los objetos QTreeWidgetItems a mostrar. Primero recorremos todas las claves del nivel actual, creando un QTableWidgetItem por cada una. Si parent es cero, creamos el elemento como hijo del QTreeWidget (transformndolo en un elemento de ms alto nivel); de otra manera lo creamos como hijo de parent. La primera columna mostrar el nombre de la clave y la segunda columna el valor correspondiente. A continuacin, recorremos los grupos de cada nivel. Por cada grupo, creamos un nuevo QTreeWidgetItem, colocando el nombre del grupo en la primera columna. La funcin se vuelve a llamar as misma para cargar los elementos que contiene el grupo. Los widgets mostrados en esta seccin permiten usar un estilo de programacin muy similar al usado en versiones anteriores de Qt: se lee enteramente un conjunto de datos y se carga en la vista, usando objetos para representar los elementos y (si se permite la edicin) escribirlo al origen de datos. En la siguiente seccin iremos ms all de este simple enfoque y mostraremos la ventaja real y completa de la arquitectura modelo/vista de Qt.

10. Clases para Visualizar Elementos (Clases Item View)

83

Usando Modelos Predefinidos


Qt provee varios modelos predefinidos para utilizar con las vistas: QStringListModel QStandardItemModel QDirModel QSqlQueryModel QSqlTableModel QSqlRelationalTableModel QSortFilterProxyModel Almacena una lista de cadenas de caracteres Almacena datos jerrquicos de cualquier tipo Encapsula el acceso al sistema de archivos locales Encapsula el acceso a bases de datos SQL Encapsula una tabla SQL Encapsula el acceso a tablas SQL con claves forneas Ordena y/o filtra los datos contenidos en otros modelos

En esta seccin, veremos como usar las clases QStringListModel, QDirModel y QSortFilterProxyModel. Los modelos para el manejo de datos SQL se cubrirn en el Captulo 13. Comenzaremos con un dialogo simple en el cual se puede agregar, editar y eliminar datos de una clase QStringList, donde cada cadena representa un lder de un equipo. Figura 10.6. La aplicacin Lderes de Equipos

A continuacin mostramos un extracto del constructor: DialogoLiderEquipo::DialogoLiderEquipo(const QStringList &lideres, QWidget *parent) :QDialog(parent) { modelo = new QStringListModel(this); modelo->setStringList(lideres); viewLista = new QListView; viewLista->setModel(modelo); viewLista->setEditTriggers(QAbstractItemView::AnyKeyPressed | QAbstractItemView::DoubleClicked); } Comenzamos por crear y rellenar un QStringList. A continuacin creamos un QListView y establecemos su modelo. Tambin activamos algunos disparadores de edicin que le permitirn al usuario

10. Clases para Visualizar Elementos (Clases Item View)

84

modificar un valor de la lista simplemente comenzando a escribir o por medio de un doble click. Por defecto, no hay ningn disparador de edicin establecido en el objeto QListView. void DialogoLiderEquipo::insertar() { int fila = viewLista->currentIndex().row(); modelo->insertRows(fila, 1); QModelIndex indice = modelo->index(fila); viewLista->setCurrentIndex(indice); viewLista->edit(indice); } Cuando el usuario hace click en el botn Insertar, se invoca al slot insertar(). Este obtiene el nmero de fila del elemento seleccionado en la lista. Cada dato en el modelo se corresponde con un "ndice de modelo", el cual es representado por un objeto QModelIndex. Examinaremos los ndices con mas detalle en la prxima seccin, por ahora bastar con saber que un ndice tiene tres componentes principales: un nmero de fila, un nmero de columna y un puntero al modelo que pertenece. Para un modelo unidimensional la columna siempre es 0. Una vez que tenemos el nmero de fila, insertamos una nueva en dicha posicin. La insercin es realizada sobre el modelo, y el modelo automticamente actualiza la vista. Luego establecemos el ndice actual del modelo al de la fila que acabamos de agregar. Finalmente colocamos la nueva fila en modo edicin si el usuario ha presionado una tecla o realizado un doble click. void DialogoLiderEquipo::borrar() { modelo->removeRows(viewLista->currentIndex().row(), 1); } En el constructor, la seal clicked() del botn Borrar es conectada al slot borrar(). Ya que solo estamos borrando la fila actual, podemos llamar a removeRows() con el ndice del elemento seleccionado y la cantidad de 1. Al igual que en la insercin, el modelo se encargar de actualizar la vista. QStringList DialogoLiderEquipo::lideres() const { return modelo->stringList(); } Por ultimo, la funcin lideres() proporciona una manera de obtener las lista de cadenas editadas cuando cerremos el dialogo. Este ejemplo podra fcilmente convertirse en un editor genrico de listas de cadenas de caracteres con solo parametrizar el titulo de la ventana. Otro dialogo genrico que es muy requerido es aquel que presenta una lista de archivos y directorios al usuario. En el prximo ejemplo usaremos la clase QDirModel, la cual encapsula el acceso al sistema de archivos de la computadora y es capaz de mostrar (y ocultar) varios de sus atributos. Este modelo puede aplicar un filtro para restringir el conjunto de archivos y carpetas mostrados y puede ordenar los datos de varias maneras. Empezremos con la creacin y configuracin del modelo y de la vista en el constructor del dialogo. VisorDirectorios::VisorDirectorios(QWidget *parent) :QDialog(parent) { modelo = new QDirModel; modelo->setReadOnly(false); modelo->setSorting(QDir::DirsFirst | QDir::IgnoreCase | QDir::Name);

10. Clases para Visualizar Elementos (Clases Item View)

85

viewArbol = new QTreeView; viewArbol->setModel(modelo); viewArbol->header()->setStretchLastSection(true); viewArbol->header()->setSortIndicator(0, Qt::AscendingOrder); viewArbol->header()->setSortIndicatorShown(true); viewArbol->header()->setClickable(true); QModelIndex indice = modelo->index(QDir::currentPath()); viewArbol->expand(indice); viewArbol->scrollTo(indice); viewArbol->resizeColumnToContents(0); } Figura 10.7. La aplicacin Visor de Directorios

Una vez que hemos construido el modelo, lo hacemos editable y establecemos algunos atributos de ordenacin. Luego creamos el objeto QTreeView que se encargar de mostrar los datos aportados por el modelo. Los encabezados de columna del objeto QTreeView pueden ser usados para proveer ordenacin controlada por el usuario. La llamada a setClickable(true) hace que los encabezados de columna respondan a los clicks del ratn emitiendo la seal sectionClicked(). El usuario puede seleccionar el orden de los datos con solo presionar sobre un encabezado de columna; si repite los clicks, se alterna entre orden ascendente y descendente. Luego de esto, establecemos el ndice del modelo al directorio actual y nos aseguramos que el directorio sea totalmente visible (por medio de la funcin expand()) y nos desplazamos hasta el usando scrollTo(). Despus, hacemos que la primera columna sea lo bastante ancha como para mostrar los datos sin recortarlos. En la parte del cdigo del constructor que no mostramos aqu, conectamos los botones Crear Directorio y Remover a los slots que se encargan de realizar dichas acciones. No necesitamos un botn Renombrar ya que el usuario puede modificar un elemento con solo presionar F2 y comenzar a escribir. void VisorDirectorios::creaDirectorio() { QModelIndex indice = viewArbol->currentIndex(); if (!indice.isValid()) return; QString nombreDir = QInputDialog::getText(this, tr("Crear Directorio"),tr("Nombre del directorio")); if (!nombreDir.isEmpty()) { if (!modelo->mkdir(indice, nombreDir).isValid())

10. Clases para Visualizar Elementos (Clases Item View)

86

QMessageBox::information(this, tr("Crear Directorio"), tr("No se pudo crear el directorio")); } } Si el usuario ingresa un nombre en el dialogo de entrada, intentamos crear un nuevo directorio (hijo del directorio actual) con dicho nombre. La funcin QDirModel::mkdir() toma como argumentos el ndice del directorio padre y el nombre del nuevo directorio y devuelve el ndice del directorio creado. Si la operacin falla, esta devuelve un ndice invlido. void VisorDirectorios::borrar() { QModelIndex indice = viewArbol->currentIndex(); if (!indice.isValid()) return; bool ok; if (modelo->fileInfo(indice).isDir()) { ok = modelo->rmdir(indice); } else { ok = modelo->remove(indice); } if (!ok) QMessageBox::information(this, tr("Borrar"), tr("No se pudo borrar %1") .arg(modelo->fileName(indice))); } Si el usuario presiona el botn Remover intentamos borrar el archivo o directorio asociado con el ndice actual. Para realizar esta tarea podemos usar la clase QDir, pero QDirModel ofrece una prctica funcin que trabaja con los ndices del modelo. El ltimo ejemplo de esta seccin muestra cmo usar QSortFilterProxyModel. Al contrario que los otros modelos predefinidos, ste encapsula un modelo existente y manipula los datos que pasan entre el modelo y la vista. En nuestro ejemplo, el modelo subyacente es un QStringListModel cargado con una lista de nombres de colores reconocidos por Qt (obtenidos por medio de QColor::colorNames()). El usuario puede ingresar una expresin de filtro en un QLineEdit y especificar como dicha cadena tiene que ser interpretada (como una expresin regular, comodines o una cadena fija) por medio de un combobox. Figura 10.8. La aplicacin Nombres de Colores

Aqu presentamos una parte del cdigo del constructor de la clase DialogoNombreClores:

10. Clases para Visualizar Elementos (Clases Item View)

87

DialogoNombreColores::DialogoNombreColores(QWidget *parent) : QDialog(parent) { modeloOrigen = new QStringListModel(this); modeloOrigen->setStringList(QColor::colorNames()); modeloProxy = new QSortFilterProxyModel(this); modeloProxy->setSourceModel(modeloOrigen); modeloProxy->setFilterKeyColumn(0); viewLista = new QListView; viewLista->setModel(modeloProxy); comboSintaxis = new QComboBox; comboSintaxis->addItem(tr("Expresion Regular"), QRegExp::RegExp); comboSintaxis->addItem(tr("Comodines"), QRegExp::Wildcard); comboSintaxis->addItem(tr("Cadena Fija"), QRegExp::FixedString); } El objeto QStringListModel es creado y rellenado de la manera habitual. Es seguido por la construccin del QSortFilterProxyModel. Asignamos el modelo a usar por medio de setSourceModel() y le decimos que el filtro lo aplique sobre la columna 0 del modelo original. La funcin QComboBox::addItem() acepta un argumento opcional "data" de tipo QVariant; usamos este para almacenar el valor QRegExp::PatternSyntax que corresponde a cada elemento del mismo. void DialogoNombreColores::reaplicarFiltro() { QRegExp::PatternSyntax sintaxis = QRegExp::PatternSyntax (comboSintaxis->itemData(comboSintaxis->currentIndex()).toInt()); QRegExp regExp(lineEditFiltro->text(), Qt::CaseInsensitive, sintaxis); modeloProxy->setFilterRegExp(regExp); } El slot reaplicarFiltro() es invocado cada vez que el usuario modifica la cadena de filtro o el elemento seleccionado en el combo. Creamos un objeto QRegExp usando el texto contenido en el QLineEdit. Luego establecemos su patrn de sintaxis al valor seleccionado en el combo. Cuando llamamos a setFilterRegExp(), el nuevo filtro se transforma en activo y la vista es actualizada automticamente.

Implementando Modelos Personalizados


Los modelos predefinidos ofrecen una manera cmoda de manipular y mostrar datos. Como es normal, algunos orgenes de datos no pueden ser manejados eficientemente por estos modelos, y para estas situaciones es necesario crear un modelo optimizado para dicho origen de datos. Antes que nos embarquemos en la creacin de un modelo propio, revisaremos primero algunos conceptos claves usados en la arquitectura modelo/vista de Qt. Cada elemento de datos tiene un ndice de modelo y un conjunto de atributos, llamado roles, que pueden tomar valores arbitrarios. Decamos anteriormente en este captulo que los roles usados ms frecuentemente son Qt::DisplayRole y Qt::EditRole. Otros roles son usados para datos suplementarios (como pueden ser Qt::ToolTipRole, Qt::StatusTipRole y Qt::WhatsThisRole), y otros para controlar atributos de presentacin de los datos (tales como Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole y Qt::BackgroundColorRole).

10. Clases para Visualizar Elementos (Clases Item View)

88

Figura 10.9. Vista esquemtica de los modelos de Qt

Para un modelo de tipo lista, el nico componente relevante del ndice es el nmero de fila, asequible por medio de QModelIndex::row(). Para un modelo de tipo tabla, en cambio los componentes relevantes del ndice son el nmero de fila y el de columna, dados por fromQModelIndex::row() y QModelIndex::column() respectivamente. Para ambos tipos de modelos, el padre de cada elemento es el elemento raz, el cual es representado por medio de un ndice invlido. Los dos primeros ejemplos de esta seccin muestran cmo crear y utilizar modelos de tipo tabla. Un modelo de tipo rbol es similar a uno tipo tabla con las siguientes diferencias: Al igual que en las tablas, el padre de los elementos de ms alto nivel es el elemento raz, pero cada padre de los restantes elementos es un elemento vlido en la jerarqua. El padre de cada elemento es asequible por medio de QModelIndex::parent(). Cada elemento tiene aparte de sus datos, cero o ms hijos. Dado que cada elemento es capaz de tener otros elementos como hijos, es posible representar estructuras de datos recursivas, como mostraremos en el ejemplo final de esta seccin. Nuestro primer ejemplo es un modelo de solo lectura que muestra una tabla con las monedas del mundo y la relacin de cambio que existe entre las mismas.

Figura 10.10. La aplicacin Monedas Circulantes

La aplicacin podra implementarse fcilmente usando una simple tabla, pero queremos usar un modelo propio aprovechando ciertas propiedades para minimizar los datos almacenados. Si almacenramos los 162 valores monetarios actualmente negociados en una tabla, necesitaramos usar 26244 valores (162 x 162); mientras que con el modelo que implementaremos solo necesitaremos guardar 162 valores (uno por cada tipo de cambio en relacin con el dolar norteamericano).

10. Clases para Visualizar Elementos (Clases Item View)

89

La clase ModeloMonetario ser usada con un QTableView comn. El modelo almacena los valores en un QMap<QString,double>; cada clave es un cdigo monetario y cada valor corresponde al tipo de cambio de la moneda con respecto al dolar norteamericano. El siguiente fragmento de cdigo muestra cmo se cargan los datos en el QMap y cmo se usa el modelo: QMap<QString, double> mapMonedas; mapMonedas.insert("AUD", 1.3259); mapMonedas.insert("CHF", 1.2970); mapMonedas.insert("SGD", 1.6901); mapMonedas.insert("USD", 1.0000); ModeloMonetario modeloMonedas; modeloMonedas.setMonedas(mapMonedas); QTableView ViewTabla; ViewTabla.setModel(&modeloMonedas); ViewTabla.setAlternatingRowColors(true); Ahora revisaremos la implementacin del modelo, comenzando por el archivo cabecera: class ModeloMonetario : public QAbstractTableModel { public: ModeloMonetario(QObject *parent = 0); void setMonedas(const QMap<QString, double> &valores); int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int seccion, Qt::Orientation orientacion, int rol) const; private: QString monedaAt(int desp) const; QMap<QString, double> mapMonedas; }; Hemos escogido que nuestro modelo herede de QAbstractTableModel, ya que su comportamiento est muy cerca del que buscamos. Qt provee varios modelos bsicos, incluyendo QAbstractListModel, QAbstractTableModel y QAbstractItemModel. La clase QAbstractItemModel es usada como soporte de varios tipos de modelos, incluyendo aquellos basados en datos recursivos, mientras que las clase QAbstractListModel y QAbstractTableModel son ms convenientes para conjuntos de datos unidimensionales o bidimensionales. Figura 10.11. Arbol de herencia de las clases de modelos abstractos

Para un modelo de solo lectura, debemos reimplementar tres funciones: rowCount(), columnCount() y data(). En este caso, tambin reimplementamos headerData(), y agregamos una funcin para inicializar los datos (setMonedas()). ModeloMonetario::ModeloMonetario(QObject *parent) : QAbstractTableModel(parent) { }

10. Clases para Visualizar Elementos (Clases Item View)

90

No necesitamos hacer nada en el constructor, excepto pasar el parmetro padre a la clase base int ModeloMonetario::rowCount(const QModelIndex & /* parent */) const { return mapMonedas.count(); } Para este modelo, la cantidad de filas (y tambin de columnas) est dada por la cantidad de entradas en el mapa de monedas. El parmetro parent no se usa en este modelo; esta ah porque tanto rowCount() como columnCount() se han heredado de la clase QAbstractItemModel, la cual soporta estructuras jerrquicas. QVariant ModeloMonetario::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role == Qt::TextAlignmentRole) { return int(Qt::AlignRight | Qt::AlignVCenter); } else if (role == Qt::DisplayRole) { QString filaMoneda = monedaAt(index.row()); QString columnaMoneda = monedaAt(index.column()); if (mapMonedas.value(filaMoneda) == 0.0) return "####"; double importe = mapMonedas.value(columnaMoneda) / mapMonedas.value(filaMoneda); return QString("%1").arg(importe, 0, f, 4); } return QVariant(); } La funcin data() devuelve el valor para cualquier rol de un elemento. El elemento es especificado por un QModelIndex. Para un modelo de tabla, los componentes interesantes de un objeto QModelIndex son su nmero de fila y su nmero de columna, disponibles a travs de las funciones row() y column() respectivamente. Si el rol es Qt::TextAlignmentRole, devolvemos una alineacin adecuada para nmeros. Si el rol es Qt::DisplayRole, buscamos el valor de cada moneda y calculamos el tipo de cambio. Podramos devolver el valor calculado como un double, pero no tendramos control sobre la cantidad de dgitos decimales que se mostraran (a menos que utilicemos un delegado propio). En vez, devolvemos el valor como una cadena de caracteres formateada como queremos que se muestre. QVariant ModeloMonetario::headerData(int section, Qt::Orientation /* orientation */, int role) const { if (role != Qt::DisplayRole) return QVariant(); return monedaAt(section); } La funcin headerData() es llamada por la vista para establecer el ttulo de los encabezados horizontales y verticales. El parmetro section indica el nmero de fila o columna (dependiendo de la orientacin). Ya que las filas y columnas tienen el mismo cdigo monetario, no tenemos que preocuparnos por la orientacin y simplemente retornamos el cdigo de acuerdo al nmero de seccin.

10. Clases para Visualizar Elementos (Clases Item View)

91

void ModeloMonetario::setMonedas(const QMap<QString, double> &valores) { mapMonedas = valores; reset(); } Se puede cargar o cambiar los valores monetarios por medio de la funcin setMonedas(). La llamada a QAbstractItemModel::reset() le indica a las vistas que estn usando un modelo cuyos datos son invlidos; esto fuerza a refrescar todos los elementos visibles. QString ModeloMonetario::monedaAt(int desp) const { return (mapMonedas.begin() + desp).key(); } La funcin monedaAt() devuelve la clave monetaria que se encuentra en la posicin marcada por el parmetro desp. Usamos un iterador de tipo STL para encontrar el elemento y luego llamamos a key(). Como acabamos de ver, no es difcil crear modelos de solo lectura, y dependiendo de la naturaleza de los datos a acceder, podemos obtener altas prestaciones y ahorro de memoria al utilizar un modelo bien diseado. En el prximo ejemplo, la aplicacin Ciudades es tambin un modelo de tipo tabla, pero esta vez los datos van a ser ingresados por el usuario. Esta aplicacin es usada para almacenar valores que indican la distancia entre dos ciudades. Como en el ejemplo anterior, podramos simplemente usar un QTableWidget y almacenar un elemento por cada par de ciudades. Pero un modelo propio podra ser ms eficiente porque la distancia de una ciudad A a cualquier ciudad B es la misma si vamos de A a B o al revs, por lo tanto los elementos estn espejados a lo largo de la diagonal principal. Para ver la comparacin entre el modelo propio con una simple tabla, asumiremos que tenemos tres ciudades: A, B y C. Si almacenramos los valores para cada combinacin necesitaramos nueve valores. Un modelo cuidadosamente diseado solo requiere tres elementos (A, B), (A, C) y (B, C). Figura 10.12. La aplicacin Ciudades

Aqu mostramos cmo configurar y usar el modelo: QStringList ciudades; ciudades << "Arvika" << "Boden" << "Eskilstuna" << "Falun" << "Filipstad" << "Halmstad" << "Helsingborg" <<"Karlstad"<< "Kiruna" << "Kramfors" << "Motala" << "Sandviken"<< "Skara" << "Stockholm" << "Sundsvall" << "Trelleborg"; ModeloCiudad modelo; modelo.setCiudades(ciudades);

10. Clases para Visualizar Elementos (Clases Item View)

92

QTableView ViewTabla; ViewTabla.setModel(&modelo); ViewTabla.setAlternatingRowColors(true); Debemos reimplementar las mismas funciones que en el ejemplo anterior. Sumado a esto, debemos tambin reimplementar las funciones setData() y flags() para que el modelo permita la edicin de los datos. Esta es la definicin de la clase: class ModeloCiudad : public QAbstractTableModel { Q_OBJECT public: ModeloCiudad(QObject *parent = 0); void setCiudades(const QStringList &nombreCiudades); int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int seccion, Qt::Orientation orientacion, int role) const; Qt::ItemFlags flags(const QModelIndex &index) const; private: int posicionDe(int fila, int columna) const; QStringList ciudades; QVector<int> distancias; }; Para este modelo usaremos dos estructuras de datos: un QStringList para almacenar los nombres de las ciudades y un QVector<int> para almacenar las distancias entre cada par de ciudades. ModeloCiudad::ModeloCiudad(QObject *parent) : QAbstractTableModel(parent) { } Nuevamente, en el constructor no hacemos nada ms all de pasar el padre a la clase base. int ModeloCiudad::rowCount(const QModelIndex & /* parent */) const { return ciudades.count(); } int ModeloCiudad::columnCount(const QModelIndex & /* parent */) const { return ciudades.count(); } Ya que tenemos una grilla cuadrada de ciudades, la cantidad de filas y columnas es igual a la cantidad de ciudades en la lista. QVariant ModeloCiudad::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role == Qt::TextAlignmentRole) { return int(Qt::AlignRight | Qt::AlignVCenter); } else if (role == Qt::DisplayRole) {

10. Clases para Visualizar Elementos (Clases Item View)

93

if (index.row() == index.column()) return 0; int pos = posicionDe(index.row(), index.column()); return distancias[pos]; } return QVariant(); } La funcin data() es similar a la construida en la clase ModeloMonetario. Devuelve 0 si la fila y columna son iguales, porque esto correspondera al caso donde origen y destino son la misma ciudad; de otra manera, busca la entrada para la fila y columna dada en el vector de distancias y devuelve el valor. QVariant ModeloCiudad::headerData(int section, Qt::Orientation /* orientation */, int role) const { if (role == Qt::DisplayRole) return ciudades[section]; return QVariant(); } La funcin headerData() es simple porque tenemos una tabla cuadrada donde cada encabezado de fila y de columna se corresponde a un elemento de la lista. Simplemente devolvemos el nombre de la ciudad ubicada en la posicin requerida de la lista. bool ModeloCiudad::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && index.row() != index.column() && role == Qt::EditRole) { int pos = posicionDe(index.row(), index.column()); distancias[pos] = value.toInt(); QModelIndex indiceTranspuesto = createIndex(index.column(), index.row()); emit dataChanged(index, index); emit dataChanged(indiceTranspuesto, indiceTranspuesto); return true; } return false; } La funcin setData() se ejecuta cada vez que el usuario edita un elemento. Al proveer un ndice valido, las dos ciudades son diferentes, y si el rol a modificar es Qt::EditRole, la funcin almacena el valor que ingres el usuario en el vector de distancias. La funcin createIndex() es usada para generar un ndice de modelo. Necesitamos obtener el ndice de un elemento ubicado del otro lado de la diagonal principal que se corresponde con el elemento que est siendo modificado, ya que ambos deben mostrar el mismo valor. Esta funcin toma como argumento la fila antes que la columna, pero invertimos los parmetros para obtener el ndice del elemento diagonalmente opuesto al que es especificado por index. Emitimos la seal dataChanged() con el ndice del elemento que fue modificado. La razn de que esta seal tome dos ndices de modelos como argumentos es que es posible un cambio que afecte a una regin rectangular de ms de una fila y una columna, por lo tanto los ndices indican el elemento superior izquierdo y el inferior derecho de la regin afectada. Tambin emitimos la seal dataChanged() para el ndice transpuesto para asegurarnos de que la vista refrescar dicho elemento. Finalmente devolvemos verdadero o falso para indicar si la edicin se complet o no satisfactoriamente.

10. Clases para Visualizar Elementos (Clases Item View)

94

Qt::ItemFlags ModeloCiudad::flags(const QModelIndex &index) const { Qt::ItemFlags flags = QAbstractItemModel::flags(index); if (index.row() != index.column()) flags |= Qt::ItemIsEditable; return flags; } La funcin flags() es usada por el modelo para comunicar lo que se puede hacer con el elemento (por ejemplo, si este es editable). La implementacin por defecto de QAbstractTableModel devuelve Qt::ItemIsSelectable|Qt::ItemIsEnabled. Nosotros le agregamos ItemIsEditable a todos los elementos, excepto aquellos ubicados en la diagonal principal. void ModeloCiudad::setCiudades(const QStringList &nombreCiudades) { ciudades = nombreCiudades; distancias.resize(ciudades.count() * (ciudades.count() - 1) / 2); distancias.fill(0); reset(); } Si recibimos una nueva lista de ciudades, pasamos la QStringList privada llamada ciudades a la nueva lista y limpiamos el vector de distancias, luego llamamos a QAbstractItemModel::reset() para notificar a las vistas que los elementos que estn mostrando deben ser actualizados. int ModeloCiudad::posicionDe(int fila, int columna) const { if (fila < columna) qSwap(fila, columna); return (fila * (fila - 1) / 2) + columna; } La funcin privada posicionDe() calcula el ndice de un par de ciudades en el vector distancias. Por ejemplo, si tenemos las ciudades A, B, C y D y el usuario actualiza la fila 3, columna 1 (B a D), esta funcin devolvera 3 (3 - 1)/2 + 1 = 4. Por otro lado, si el usuario en actualiza la fila 1, columna 3 (D a B), gracias a qSwap(), realizamos el mismo clculo y por lo tanto obtenemos el mismo resultado. El ltimo ejemplo de esta seccin es un modelo que muestra el rbol de sintaxis de una expresin regular. Una expresin regular consiste en uno o mas trminos, separados por el carcter |. De este modo la cadena alpha|bravo|charlie contiene tres trminos. Cada trmino es una secuencia de uno o ms factores; por ejemplo, el trmino "bravo" consiste en cinco factores (cada letra es un factor). Los factores pueden ser descompuestos en una unidad atmica ms un calificador opcional, tal como '*' o '+'. Ya que las expresiones regulares pueden tener subexpesiones entre parntesis, pueden generar arboles de anlisis recursivos. La expresin regular mostrada en la Figura 10.14, ab|(cd)?e, busca un carcter 'a' seguido de una 'b', o alternativamente una 'c' seguida de una 'd' seguida de una 'e', o solo una 'e'. Por lo tanto esta expresin encontrar trminos como ab y cde, pero no bc o cd. Figura 10.13. Las estructuras de datos ciudades y distancias junto con el table model

10. Clases para Visualizar Elementos (Clases Item View)

95

La aplicacin Analizador Expreg consta de cuatro clases: VentanaExpReg: es una ventana que le permite al usuario ingresar una expresin regular y muestra el correspondiente rbol de anlisis. AnalizadorExpReg: genera el rbol de anlisis a partir de una expresin regular. ModeloExpReg: es un modelo jerrquico que encapsula el rbol de anlisis. Nodo: representa un elemento del rbol de anlisis. Figura 10.14. La aplicacin Analizador Expreg

Empecemos con la clase Nodo: class Nodo { public: enum Tipo { RegExp, Expression, Term, Factor, Atom, Terminal }; Nodo(Tipo tipo, const QString &str = ""); ~Nodo(); Tipo tipo; QString str; Nodo *padre; QList<Nodo *> hijos; }; Cada nodo tiene un tipo, una cadena de caracteres (la cual puede estar vaca), un padre (el cual puede ser cero), y una lista de nodos hijos (la cual puede estar vaca). Nodo::Nodo(Tipo tipo, const QString &str) { this->tipo = tipo; this->str = str; padre = 0; } El constructor simplemente inicializa el tipo y la cadena del nodo. Como todos los datos son pblicos, el cdigo que haga uso de la clase Nodo puede manipular el tipo, la cadena, el padre y los hijos directamente. Nodo::~Nodo() { qDeleteAll(hijos);

10. Clases para Visualizar Elementos (Clases Item View)

96

} La funcin qDeleteAll recorre todos los punteros de un contenedor y llama a delete para cada uno. Esta no establece el puntero a 0, por lo tanto si es usada fuera del destructor es comn que le siga una llamada a clear(). Ahora que tenemos definidos nuestros elementos de datos (cada uno representado por un objeto Nodo), estamos listos para crear el modelo: class ModeloExpReg : public QAbstractItemModel { public: ModeloExpReg(QObject *parent = 0); ~ModeloExpReg(); void setNodoRaiz(Nodo *nodo); QModelIndex index(int row, int column, const QModelIndex &parent) const; QModelIndex parent(const QModelIndex &child) const; int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const; private: Nodo *nodoDesdeIndice(const QModelIndex &indice) const; Nodo *nodoRaiz; }; Esta vez tenemos que heredar de QAbstractItemModel en vez de QAbstractTableModel, porque queremos crear un modelo jerrquico. Las funciones esenciales que debemos reimplementar siguen siendo las mismas, adems de index() y parent(). Para establecer los datos del modelo tenemos a la funcin setNodoRaiz(), a la cual se le debe pasar el nodo raz del rbol de anlisis de la expresin regular. ModeloExpReg::ModeloExpReg(QObject *parent) : QAbstractItemModel(parent) { nodoRaiz = 0; } En el constructor del modelo, solo necesitamos establecer el nodo raz a un puntero nulo y pasar el padre a la clase base. ModeloExpReg::~ModeloExpReg() { delete nodoRaiz; } En el destructor borramos el nodo raz, Si el nodo raz tiene hijos, cada uno de ellos ser eliminado y sus hijos tambin, por el destructor de la clase Nodo. void ModeloExpReg::setNodoRaiz(Nodo *nodo) { delete nodoRaiz; nodoRaiz = nodo; reset(); } Cuando recibimos un nuevo nodo raz, comenzamos por borrar el anterior. Luego establecemos el nuevo nodo raz y llamamos a reset() para notificar a las vistas que deben refrescar los elementos visibles.

10. Clases para Visualizar Elementos (Clases Item View)

97

QModelIndex ModeloExpReg::index(int row, int column, const QModelIndex &parent) const { if (!nodoRaiz) return QModelIndex(); Nodo *nodoPadre = nodoDesdeIndice(parent); return createIndex(row, column, nodoPadre->hijos[row]); } A la funcin index() la reimplementamos de la clase QAbstractItemModel. Esta es llamada cada vez que el modelo o la vista necesitan crear un objeto QModelIndex para un hijo en particular (o para un elemento de nivel superior si el padre es un QModelIndex invlido). Para un modelo de tipo tabla o lista, no necesitamos reimplementar esta funcin, porque la implementacin por defecto de QAbstractListModel y QAbstractTableModel es suficiente. En nuestra funcin index(), si el modelo no tiene datos devolvemos un QModelIndex invlido. De cualquier otra manera, creamos un QModelIndex con la fila y la columna indicadas y un puntero al nodo requerido. Para modelos jerrquicos, conocer la fila y la columna de un elemento relativo a su padre no es suficiente para poder identificarlo: debemos conocer quin es el padre. Para resolver esto, podemos almacenar un puntero al nodo en el ndice de modelo. El objeto QModelIndex nos da la opcin de guardar un void * o un entero adems del nmero de fila y de columna. El nodo padre es extrado del ndice usando la funcin privada nodoDesdeIndice(). El puntero al nodo es obtenido a travs de la lista de nodos del padre. Nodo *ModeloExpReg::nodoDesdeIndice(const QModelIndex &index)const { if (index.isValid()) { return static_cast<Nodo *>(index.internalPointer()); } else { return nodoRaiz; } } La funcin nodoDesdeIndice() convierte el ndice pasado a un puntero tipo Nodo, o devuelve el nodo raz si el ndice es invlido, ya que un ndice no vlido es usado para representar el elemento raz de un modelo. int ModeloExpReg::rowCount(const QModelIndex &parent) const { Nodo *nodoPadre = nodoDesdeIndice(parent); if (!nodoPadre) return 0; return nodoPadre->hijos.count(); } La cantidad de filas de un elemento est dada simplemente por cuantos hijos tenga. int ModeloExpReg::columnCount(const QModelIndex & /* parent */) const { return 2; } Establecemos la cantidad de columnas a 2. La primera columna muestra el tipo de nodo y la segunda el valor del mismo. QModelIndex ModeloExpReg::parent(const QModelIndex &child) const { Nodo *nodo = nodoDesdeIndice(child);

10. Clases para Visualizar Elementos (Clases Item View)

98

if (!nodo) return QModelIndex(); Nodo *nodoPadre = nodo->parent; if (!nodoPadre) return QModelIndex(); Nodo *nodoAbuelo = nodoPadre->parent; if (!nodoAbuelo) return QModelIndex(); int fila = nodoAbuelo->hijos.indexOf(nodoPadre); return createIndex(fila, child.column(), nodoPadre); } Obtener el ndice del padre desde un elemento hijo es un poco ms complicado que buscar un hijo desde el padre. Podemos fcilmente recuperar el nodo padre usando nodoDesdeIndice() y subiendo en la jerarqua por medio del puntero que cada nodo tiene a su padre, pero para obtener el nmero de fila, necesitamos llegar hasta el nodo abuelo y buscar el ndice que indica la posicin del padre en su lista de nodos. QVariant ModeloExpReg::data(const QModelIndex &index, int role) const { if (role != Qt::DisplayRole) return QVariant(); Nodo *nodo = nodoDesdeIndice(index); if (!nodo) return QVariant(); if (index.column() == 0) { switch (nodo->tipo) { case Nodo::RegExp: return tr("Expresin Regular"); case Nodo::Expression: return tr("Expresin"); case Nodo::Term: return tr("Termino"); case Nodo::Factor: return tr("Factor"); case Nodo::Atom: return tr("Unidad Atmica"); case Nodo::Terminal: return tr("Terminal"); default: return tr("Desconocido"); } } else if (index.column() == 1) { return nodo->str; } return QVariant(); } En la funcin data() obtenemos un puntero a un objeto Nodo para el elemento requerido y accedemos a los datos subyacentes. Si el llamador quiere un valor para un rol distinto a Qt::DisplayRole o si no podemos acceder al ndice, devolvemos un QVariant no vlido. Si la columna es 0, devolvemos el nombre del tipo de nodo; si la columna es 1, devolvemos el valor del nodo (su cadena). QVariant ModeloExpReg::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role ==

10. Clases para Visualizar Elementos (Clases Item View)

99

Qt::DisplayRole) { if (section == 0) { return tr("Nodo"); } else if (section == 1) { return tr("Valor"); } } return QVariant(); } En la reimplementacin de headerData(), devolvemos la etiqueta del encabezado horizontal o vertical apropiada. La clase QTreeView, la cual es usada para la visualizacin de modelos jerrquicos no tiene encabezados verticales, por lo tanto ignoramos esa posibilidad. Ahora que tenemos cubierto las clases Nodo y ModeloExpReg, veremos cmo crear el nodo raz cuando el usuario modifica el texto en el editor: void VentanaExpReg::CambioExpReg(const QString &regExp) { AnalizadorExpReg analizador; Nodo *nodoRaiz = analizador.analizar(regExp); regExpModel->setNodoRaiz(nodoRaiz); } Cuando el usuario cambia el texto que se encuentra en el line edit de la aplicacin, el slot CambioExpReg() de la ventana principal es invocado. En este slot, el texto del usuario es analizado y el analizador retorna un puntero al nodo raz del rbol analizador. No hemos mostrado la clase AnalizadorExpReg porque no es relevante para la interfaz o para la programacin modelo/vista. En esta seccin, hemos visto cmo crear tres modelos diferentes. Muchos modelos son mucho ms simples que los que hemos mostrado aqu, con una correspondencia de uno-a-uno entre los tems y los ndices de los modelos. Ms ejemplos modelo/vista son provistos junto con Qt, e incluyen una documentacin extensa para entenderlos lo mejor posible.

Implementando Delegados Personalizados


Cada tem de las vistas es dibujado y editado usando delegados. En la mayora de los casos, usar el delegado predeterminado de una vista es suficiente. Si lo que deseamos es tener un control ms preciso sobre el dibujado de los tems, podemos lograr lo que queremos manejando los atributos Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole y Qt::BackgroundColorRole que son usados por el delegado predeterminado. Por ejemplo, en los ejemplos de Ciudades y Monedas Circulantes mostrados anteriormente, manejamos el atributo Qt::TextAlignmentRole para alinear los nmeros a la derecha. Si deseamos un control aun mayor, podemos crear nuestra propia clase delegada y establecerla en las vistas que queremos que hagan uso de ella. El dialogo Editor de Pistas mostrado a continuacin hace uso de un delegado personalizado. Muestra los ttulos de las msicas y su duracin. Los datos contenidos por el modelo sern simplemente datos tipo QString (los titulos) e int (segundos), pero la duracin ser separada en minutos y segundos y ser un campo editable usando un QTimeEdit. El dialogo Editor de Pistas usa un QTableWidget, una subclase de una de las clases tem/view de Qt que opera sobre un QTableWidgetItems. Los datos son proporcionados como una lista de Pistas: class Pista { public: Pista(const QString &titulo = "", int duracion = 0); QString titulo;

10. Clases para Visualizar Elementos (Clases Item View)

100

int duracion; }; Figura 10.15. El dialogo Editor de Pistas

Aqu hay un extracto del constructor que muestra la creacin y el llenado de un table widget: EditorPistas::EditorPistas(QList<Pista> *pistas, QWidget *parent): QDialog(parent) { this->pistas = pistas; tableWidget = new QTableWidget(pistas->count(), 2); tableWidget->setItemDelegate(new PistaDelegate(1)); tableWidget->setHorizontalHeaderLabels( QStringList() << tr("Pista") << tr("Duracion")); for (int fila = 0; fila < filas->count(); ++fila) { Pista pista = pistas->at(fila); QTableWidgetItem *item0 = new QTableWidgetItem(pista.titulo); tableWidget->setItem(fila, 0, item0); QTableWidgetItem *item1 = new QTableWidgetItem (QString::number(pista.duracion)); item1->setTextAlignment(Qt::AlignRight); tableWidget->setItem(fila, 1, item1); } } El constructor crea un table widget, y en lugar de usar el delegado predeterminado de este, establecemos nuestro propio PistaDelegate, pasndolo en la columna que contiene el dato de los tiempos de duracin. Comenzamos estableciendo los encabezados de las columnas, luego iteramos sobre los datos, llenando las filas con el nombre y la duracin de cada pista. El resto del constructor y el resto del dialogo EditorPista no contiene ninguna sorpresa, as que veremos ahora la clase PistaDelegate que maneja el dibujado y la edicin de los datos de una pista. class PistaDelegate : public QItemDelegate { Q_OBJECT public: PistaDelegate(int columnaDuracion, QObject *parent = 0); void paint(QPainter *painter,const QStyleOptionViewItem &option, const QModelIndex &index) const;

10. Clases para Visualizar Elementos (Clases Item View)

101

QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; private slots: void commitYCerrarEditor(); private: int columnaDuracion; }; Usamos QItemDelegate como nuestra clase base, de manera que aprovechemos la implementacin que tiene el delegado predeterminado. Pudimos haber usado tambin QAbstractItemDelegate como clase base, si queramos empezar desde cero. Para darle la capacidad a un delegado de que pueda editar datos, debemos implementar las funciones createEditor(), setEditorData() y setModelData(). Tambin debemos implementar una funcin de dibujo, que por defecto es llamada paint(), para cambiar el dibujado de la columna duracin. PistaDelegate::PistaDelegate(int columnaDuracion, QObject *parent): QItemDelegate(parent) { this->columnaDuracion = columnaDuracion; } El parmetro columnaDuracion pasado al constructor le dice al delegado cul columna contiene la duracin de la pista. void PistaDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == columnaDuracion) { int sgundos = index.model()->data(index, Qt::DisplayRole).toInt(); QString texto = QString("%1:%2").arg(segundos / 60, 2, 10, QChar(0)).arg(segundos % 60, 2, 10, QChar(0)); QStyleOptionViewItem myOption = option; myOption.displayAlignment = Qt::AlignRight | Qt::AlignVCenter; drawDisplay(painter, myOption, myOption.rect, texto); drawFocus(painter, myOption, myOption.rect); } else{ QItemDelegate::paint(painter, option, index); } } Ya que queremos dibujar la duracin de cada pista en la forma minutos:segundos, hemos reimplementado la funcin paint(). Los llamados a arg() toman como parmetros un entero para dibujar como una cadena, la cantidad de caracteres que debe tener, la base del entero (10 por decimal) y un carcter de relleno. Para alinear el texto a la derecha, copiamos las opciones actuales de estilo y sobreescribimos el alineamiento predeterminado. Luego llamamos a QItemDelegate::drawDisplay() para dibujar el texto, seguido de una llamada a QItemDelegate::drawFocus(), el cual dibujar un rectngulo de enfoque o seleccin si el tem est seleccionado, y en otro caso, no har nada. Usar drawDisplay() es muy conveniente, especialmente cuando lo usamos con nuestras propias opciones de estilo. Es importante recalcar, que pudimos haber dibujado usando el atributo painter directamente.

10. Clases para Visualizar Elementos (Clases Item View)

102

QWidget *PistaDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == columnaDuracion) { QTimeEdit *timeEdit = new QTimeEdit(parent); timeEdit->setDisplayFormat("mm:ss"); connect(timeEdit, SIGNAL(editingFinished()), this, SLOT(commitYCerrarEditor())); return timeEdit; } else { return QItemDelegate::createEditor(parent, option, index); } } Nosotros solo queremos controlar la edicin del campo duracin de cada pista, dejndole la edicin de los nombre al delegado predeterminado. Podemos lograr eso verificando cul columna es requerida por el delegado para proporcionar un editor para esta. Si es la columna duracin la que se requiere, creamos un QTimeEdit, establecemos el formato de visualizacin apropiadamente, y conectamos la seal editingFinished() del QTimeEdit a nuestro slot commitYCerrarEditor(). Para cualquier otra columna, pasamos el manejo de edicin al delegado predeterminado. void PistaDelegate::commitYCerrarEditor() { QTimeEdit *editor = qobject_cast<QTimeEdit *>(sender()); emit commitData(editor); emit closeEditor(editor); } Si el usuario presiona Enter o mueve el foco de seleccin fuera del QTimeEdit (pero no si presiona Escape), la seal editingFinished() es emitida y el slot commitYCerrarEditor() es llamado. Este slot emite la seal commitData() para informarle a la vista que existen datos editados y que debe reemplazar los datos que se estn mostrando en ella. Tambin emite la seal closeEditor() para notificarle a la vista que este editor no ser requerido de ah en adelante, y en tal punto, el modelo lo eliminar. El editor es recuperado usando QObject::sender(), que retorna el objeto que emiti la seal que activ el slot. Si el usuario cancela (presionando Escape), la vista simplemente eliminar el editor. void PistaDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == columnaDuracion) { int segundos = index.model()->data(index, Qt::DisplayRole).toInt(); QTimeEdit *timeEdit = qobject_cast <QTimeEdit *>(editor); timeEdit->setTime(QTime(0, segundos / 60, segundos % 60)); } else { QItemDelegate::setEditorData(editor, index); } } Cuando el usuario comienza a editar, la vista llama a la funcin createEditor() para crear un editor, y luego llama a setEditorData() para inicializar el editor con los datos actuales del tem. Si el editor es para la columna duracin, extraemos la duracin de la pista en segundos y establecemos el tiempo del QTimeEdit al nmero correspondiente de minutos y segundos; de otra forma, dejamos que el delegado predeterminado maneje la inicializacion. void PistaDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { if (index.column() == columnaDuracion) {

10. Clases para Visualizar Elementos (Clases Item View)

103

QTimeEdit *timeEdit = qobject_cast <QTimeEdit *>(editor); QTime tiempo = timeEdit->time(); int segundos = (tiempo.minute() * 60) + tiempo.second(); model->setData(index, segundos); } else { QItemDelegate::setModelData(editor, model, index); } } Si el usuario completa la edicin (por ejemplo, presionando el click izquierdo afuera del widget editor o presionando Enter o Tabulacin) y no la cancela, el modelo debe ser actualizado con los datos del editor. Si la duracin fue editada, entonces extraemos los minutos y los segundos del QTimeEdit y establecemos los datos al nmero de segundos correspondientes. Aunque no es necesario en este caso, es totalmente posible crear un delegado personalizado que controle finamente la edicin y el dibujado de cualquier tem en un modelo. Hemos elegido tomar el control de una columna en especfico, pero como QModelIndex es pasado a todas las funciones de QItemDelegate que hemos reimplementado, podemos tomar el control por columna, fila, regin rectangular, padre, o icluso cualquier combinacin de estas, hasta para tems individuales si se requiere. En este capitulo, hemos presentado un amplio vistazo de la arquitectura modelo/vista (model/view) de Qt. Hemos mostrado cmo usar las subclases convenientes de la vista, cmo usar los modelos predefinidos de Qt y cmo crear modelos y delegados propios. Pero la arquitectura modelo/vista es tan basta y amplia, que no hemos tenido el espacio para cubrir todas las posibilidades que nos brinda. Por ejemplo, podramos crear una vista propia que no dibuje sus tems como una lista, tabla o rbol. Esto se hace en el ejemplo llamado Chart que se encuentra en los ejemplos de Qt (en el directorio examples/itemview/chart), el cual muestra una vista personalizada que dibuja los datos del modelo en un grfico tipo torta. Es totalmente posible usar varias vistas para visualizar el mismo modelo sin ninguna formalidad. Cualquier edicin realizada a travs de una de las vistas ser automtica e inmediatamente reflejada en las dems. Este tipo de funcionalidad es particularmente til para la visualizacin de conjuntos de datos muy extensos donde el usuario desea solamente visualizar una parte de estos. La arquitectura tambin soporta selecciones: donde dos o ms vistas estn usando el mismo modelo, cada vista puede configurarse para tener sus propias selecciones independientes o para que las selecciones sean compartidas en las vistas. La documentacin online de Qt proporciona amplia cobertura al tema de la programacin para visualizar elementos y las clases que lo implementan. Visite http://doc.trolltech.com/4.1/model-view.html para obtener una lista de todas las clases relevantes, y http://doc.trolltech.com/4.1/model-viewprogramming.html para obtener informacin adicional y enlaces a ejemplos relevantes incluidos en Qt.

11. Clases Contenedoras

104

11. Clases Contenedoras

Contenedores Secuenciales Contenedores Asociativos Algoritmos Genricos Cadenas de Textos, Arreglos de Bytes y Variantes(Strings, Byte Arrays y Variants)

Las clases contenedoras son clases tipo plantillas de propsitos generales que guardan en memoria elementos de un determinado tipo. El lenguaje C++ ya ofrece muchos contenedores como parte de la Librera de Plantilla Estndar, conocida en ingls como Standard Template Library (STL), la cual est incluida en las libreras estndares de C++. Qt proporciona sus propias clases contenedoras, de manera que para los programas realizados en Qt podemos usar tanto los contenedores STL como los contenedores de Qt. Las principales ventajas de usar los contenedores de Qt son que estas tienen el mismo comportamiento en todas las plataformas y adems todas son implcitamente compartidas. El compartimiento implcito, o copy on write, es una optimizacin que hace posible pasar contendores enteros como valores sin ningn coste de rendimiento significante. Los contenedores de Qt tambin presentan clases iteradoras fciles de usar inspiradas por el lenguaje Java, estas pueden ser puestas en funcionamiento usando la clase QDataStream, y generalmente resultan en menos cdigos en el ejecutable que en los contenedores STL correspondientes. Finalmente, en algunas plataformas hardware soportadas por Qtopia Core (la versin de Qt para dispositivos mviles), los contenedores de Qt son los nicos que se encuentran disponibles. Qt ofrece contenedores secuenciales como QVector<T>, QLinkedList<T> y QList<T>. Tambin ofrece contenedores asociativos como QMap<K, T> y QHash<K, T>. Conceptualmente, los contenedores secuenciales guardan los elementos uno tras otro, mientras que los asociativos los guardan como pares del tipo clave-valor. Qt tambin proporciona algoritmos genricos que realizan ciertas operaciones en los contenedores. Por ejemplo, El algoritmo qSort() ordena un contenedor secuencial, y qBinaryFind() realiza una bsqueda binaria en un contenedor ordenado secuencialmente. Estos algoritmos son similares a aquellos ofrecidos por STL. Si ya ests familiarizado con los contenedores STL y los tienes disponibles en la plataforma para la que desarrollas, tal vez quieras usarlas en lugar de usar las de Qt, o usarlas en adicin a las de Qt. Para ms informacin acerca de las clases STL y las funciones, un buen lugar para empezar es el sitio web de SGI: http://www.sgi.com/tech/stl/. En este captulo, tambin veremos las clases QString, QByteArray y QVariant, ya que estas tienen mucho en comn con los contenedores. QString es una cadena Unicode de 16.bits usada en toda la API de Qt. QByteArray es un arreglo de chars de 8-bits muy til para guardar datos binarios. QVariant es un tipo de dato que puede alojar la mayora de los tipos de datos Qt y C++.

11. Clases Contenedoras

105

Contenedores Secuenciales
Un QVector<T> es una estructura de datos parecida a un arreglo que aloja en memoria sus elementos en posiciones adyacentes. Lo que distingue a un vector de un arreglo C++ comn es que un vector posee su propio tamao y puede ser redimensionado. Anexar elementos extras al final de un vector resulta ser muy eficiente, mientras que la insercin de elementos al principio o en el medio de un vector puede ser algo costoso. Figura 11.1. Un vector de doubles 0 937.81 1 25.984 2 308.74 3 310.92 4 40.9

Si sabemos de antemano cuntos elementos vamos a necesitar, podemos darle al vector un tamao inicial cuando lo definamos y usamos el operador [ ] para asignar un valor a los elementos; de otra forma, debemos redimensionar tambin el vector o anexar elementos. Aqu est un ejemplo donde se especifica el tamao inicial: QVector<double> vect(3); vect[0] = 1.0; vect[1] = 0.540302; vect[2] = -0.416147; Aqu est el mismo ejemplo, pero esta vez comenzando con un vector vaco y usando la funcin append() para anexar elementos al final: QVector<double> vect; vect.append(1.0); vect.append(0.540302); vect.append(-0.416147); Tambin podemos usar el operador << en lugar de usar el mtodo append(): vect << 1.0 << 0.540302 << -0.416747; Una manera de iterar sobre los elementos del vector es usar los operadores [ ] y el mtodo count(): double suma = 0.0; for (int i = 0; i < vect.count(); ++i) suma += vect[i]; Las entradas de los vectores que son creadas sin asignarles un valor explicito son inicializadas usando el constructor por defecto de la clase. Los tipos bsicos y los tipos de punteros son inicializados en cero. La insercin de elementos al principio o en el medio de un QVector<T>, o la remocin de estos de esas posiciones, puede ser ineficiente para vectores grandes. Por esta razn, Qt tambin ofrece la clase QLinkedList<T>, una estructura de datos que guarda sus tems en una ubicacin no adyacente de la memoria. A distincin de los vectores, las listas enlazadas (linked lists) no soportan el acceso aleatorio, pero si proporcionan inserciones y remociones de tiempo constante. Figura 11.2. Una lista enlazada de doubles

11. Clases Contenedoras

106

Las listas enlazadas no proporcionan el operador [ ], de manera que los iteradores deben ser usados para recorrer sus elementos. Los iteradores tambin son usados para especificar la posicin de elementos. Por ejemplo, el siguiente cdigo inserta la cadena Tote Hosen entre las cadenas Clash y Ramones: QLinkedList<QString> lista; lista.append("Clash"); lista.append("Ramones"); QLinkedList<QString>::iterator i = lista.find("Ramones"); lista.insert(i, "Tote Hosen"); Haremos una revisin ms detallada a los iteradores ms adelante en esta seccin. El contenedor secuencial QList<T> es una lista de arreglos que combina en una sola clase los beneficios ms importantes de QVector<T> y QLinkedList<T>. Esta soporta el acceso aleatorio, y su interfaz est basada en ndices, como lo es QVector. Las operaciones de insercin y remocin de un elemento en ambos extremos de un QList<T> son muy rpidas, y la insercin en el medio es tambin rpida para aquellas listas no ms de mil elementos. A menos que lo que queramos sea realizar inserciones en el medio de una lista enorme o necesitemos que los elementos de la lista ocupen direcciones de memoria consecutivas, QList<T> es usualmente la clase ms apropiada para usar. La clase QStringList es una subclase de QList<QString> que es muy usada en la API de Qt. Adicionalmente a las funciones que esta hereda de su clase base, esta proporciona algunas funciones extras que hacen de la clase algo ms verstil para el manejo de cadenas de texto. Lo que tiene que ver con la clase QStringList es discutido en la ltima seccin de este captulo. Las clases QStack<T> y QQueue<T> poseen dos ejemplos ms de subclases convenientes. QStack<T> es un vector que proporciona los mtodos push(), pop() y top(). QQueue<T> es una lista que proporciona los mtodos enqueue(), dequeue y head(). Para todas las clases contenedoras que hemos visto hasta ahora, el tipo de valor T puede ser: 1. Un tipo primitivo como int o double. 2. Un tipo puntero. 3. O una clase que posea un constructor por defecto (un constructor que no tiene ningn argumento), un constructor copia y un operador de asignacin. Entre las clases que cumplen con esos criterios se incluyen: QByteArray, QDateTime, QRegExp, QString y QVariant. Las clases Qt que heredan de QObject no califican; ya que estas no poseen constructores copia ni operadores de asignacin. En la prctica, esto no es ningn problema, ya que podemos guardar los punteros que apuntan a los tipos de objetos QObject y no a los objetos en s. El tipo de valor T tambin puede ser un contenedor, caso en el cual debemos recordar separar el smbolo mayor que (>) con espacios; ya que de no hacerlo, el compilador podra generar un error al tomarlos como un operador >>. Por ejemplo: QList<QVector<double> > list; //manera correcta QList<QVector<double>> list; //manera incorrecta Adems de los tipos ya mencionados, un tipo de valor de un contenedor puede ser cualquier clase personalizada que cumpla con los criterios descritos anteriormente. Aqu est un ejemplo de una clase que se ajuste a dichos criterios: class Pelicula { public: pelicula(const QString &titulo = "", int duracion = 0); void setTitulo(const QString &titulo) { miTitulo = titulo;} QString titulo() const { return miTitulo; }

11. Clases Contenedoras

107

void setDuracion(int duracion) { miDuracion = duracion; } QString duracion() const { return miDuracion; } private: QString miTitulo; int miDuracion; }; Como podemos ver, la clase posee un constructor que no requiere de argumentos (sin embargo puede tener mximo dos argumentos); ya que solo posee argumentos opcionales. Esta tambin posee un constructor copia y un operador de asignacin, ambos provistos por C++ implcitamente. Para esta clase, copiar miembro por miembro es suficiente, as que no necesitamos implementar nuestro propio constructor copia ni nuestro operador de asignacin. Qt proporciona dos categoras de iteradores para recorrer los elementos alojados en un contenedor: los iteradores al estilo Java y los iteradores al estilo STL. Los iteradores al estilo Java son fciles de usar, mientras que los de estilo STL pueden combinarse, para ser ms poderosos, con los algoritmos genricos de STL pertenecientes a Qt. Para cada clase contenedora, existen dos tipos de iteradores estilo Java: un iterador de solo lectura y uno de lectura- escritura. Las clases iteradoras de solo lectura son QVectorIterator<T>, QLinkedListIterator<T> y QListIterator<T>. Los iteradores de lectura-escritura correspondientes poseen la palabra Mutable en su nombre (por ejemplo, QMutableVectorIterator<T>). En esta parte, nos concentraremos en los iteradores de QList. Los iteradores para listas enlazadas y vectores tienen la misma API. Lo primero que hay que tener en mente cuando usamos iteradores estilo Java es que estos no apuntan directamente a sus elementos. En lugar de eso, estos pueden ubicarse antes del primer elemento, despus del ltimo elemento o entre dos elementos. Un ciclo de iteracin tpico, lucira algo como esto: QList<double> lista; ... QListIterator<double> i(lista); while (i.hasNext()) { Sequential Containers 255 do_something(i.next()); } Figura 11.3. Posiciones vlidas para iteradores estilo Java

El iterador es inicializado con el contenedor a recorrer. En este punto, el iterador est ubicado justo antes del primer elemento. El llamado a hasNext() retorna true si hay un elemento a la derecha del iterador. La funcin next() retorna el elemento a la derecha del iterador y avanza el iterador de la siguiente posicin valida. Iterar en reversa es algo similar, excepto que primero debemos llamar a la funcin toBack() para posicionar el iterador despus del ltimo elemento: QListIterator<double> i(lista); i.toBack(); while (i.hasPrevious()) { do_something(i.previous()); } La funcin hasPrevious() retorna true si existe un elemento a la izquierda del iterador; previous() retorna el elemento a la izquierda del iterador y lo mueve una posicin hacia atrs. Otra manera de pensar acerca de los iteradores next() y previous() es que estos pueden retornar el elemento que el iterador tiene justo despus de l, bien sea para adelante o para atrs.

11. Clases Contenedoras

108

Figura 11.4. Efecto de los iteradores estilo Java previous() y next()

Los iteradores Mutables proporcionan funciones para insertar, modificar y remover elementos mientras se itera. El siguiente ciclo remueve todos los nmeros negativos de una lista: QMutableListIterator<double> i(lista); while (i.hasNext()) { if (i.next() < 0.0) i.remove(); } La funcin remove() siempre opera en el ltimo elemento al que se ha saltado. Tambin funciona cuando se itera en reversa: QMutableListIterator<double> i(lista); i.toBack(); while (i.hasPrevious()) { if (i.previous() < 0.0) i.remove(); } Similarmente, los iteradores mutables estilo Java proporcionan una funcin llamada setValue() que modifica el ltimo elemento al que se ha saltado. Aqu se muestra la manera de cmo reemplazar nmeros negativos con su valor absoluto: QMutableListIterator<double> i(lista); while (i.hasNext()) { int val = i.next(); if (val < 0.0) i.setValue(-val); } Tambin es posible insertar un elemento en la posicin actual de un iterador llamando al mtodo insert(). Entonces el iterador es movido para que apunte entre el nuevo elemento y el siguiente elemento. Adicionalmente a los iteradores estilo Java, cada clase secuencial contenedora C<T> posee dos tipos de iteradores de estilo STL: C<T>::iterator y C<T>::const_iterator. La diferencia entre los dos es que const_iterator no nos permite modificar los datos. Una funcin del contenedor llamada begin() retorna un iterador estilo STL que hace referencia al primer elemento en el contenedor (por ejemplo, lista[0]), mientras que end() retorna un iterador al la posicin siguiente del ltimo elemento (por ejemplo, lista[5] para una lista de tamao 5). Si el contenedor est vaco, la funcin begin() y la funcin end() retornan lo mismo. Esto puede ser usado para ver si el contenedor no posee ningn tem, aunque usualmente es ms conveniente llamar a isEmpty() para ese propsito. Figura 11.5. Posiciones vlidas para iteradores estilo STL

Podemos usar los operadores ++ y -- para movernos al elemento siguiente o previo, y el operador unario * para recuperar el elemento actual. Para QVector<T>, los tipos iterator y const_iterator son

11. Clases Contenedoras

109

meramente typedefs para T * y const T* (Esto es posible porque QVector<T> guarda sus elementos en ubicaciones consecutivas de memoria.) El siguiente ejemplo reemplaza cada valor en una QList<double> con su valor absoluto: QList<double>::iterator i = lista.begin(); while (i != lista.end()) { *i = qAbs(*i); ++i; } Pocas funciones en Qt retornan un contenedor. Si queremos iterar sobre el valor retornado de una funcin usando un iterador estilo STL, debemos hacer una copia del contenedor e iterar sobre ella. Por ejemplo, el siguiente cdigo muestra la manera correcta de iterar sobre el QList<int> retornado por QSplitter::sizes(): QList<int> lista = splitter->sizes(); QList<int>::const_iterator i = lista.begin(); while (i != list.end()) { do_something(*i); ++i; } El siguiente cdigo muestra la manera incorrecta: // MAL QList<int>::const_iterator i = splitter->sizes().begin(); while (i != splitter->sizes().end()) { hacer_algo(*i); ++i; } Esto es as, porque QSplitter::sizes() retorna un nuevo QList<int> por cada valor, cada vez que es llamada. Si nosotros no guardamos el valor retornado, C++ lo destruir automticamente antes de que hayamos empezado a iterar, dejndonos con un iterador inservible. Para complicarlo ms aun, cada vez que el ciclo se ejecute, QSplitter->sizes() debe generar una nueva copia de la lista por el llamado que se le hace a splitter->sizes().end(). En resumen: Cuando usemos iteradores estilo STL, debemos iterar siempre sobre una copia de un contenedor retornado por un valor. Con los iteradores estilo Java de solo lectura, no necesitamos hacer una copia. El iterador toma una copia por nosotros, asegurando as que iteraremos siempre sobre los datos que la funcin retorno primero. Por ejemplo: QListIterator<int> i(splitter->sizes()); while (i.hasNext()) { hacer_algo(i.next()); } Hacer una copia de un contenedor como este suena algo costoso, pero no lo es, gracias a una optimizacin llamada comparticin implcita (implicit sharing). Esto quiere decir que copiar un contenedor Qt es casi tan rpido como copiar un puntero. Solo si una de las copias ha cambiado sus datos actualmente copiados y todo esto es manejado automticamente. Por esta razn, la comparticin implcita es llamada algunas veces copiar sobre escritura (copy on write). La belleza de la comparticin implcita radica en que esta es una optimizacin en la cual no necesitamos pensar; simplemente funciona, sin requerir ninguna intervencin por parte del programador. Al mismo tiempo, la comparticin implcita promueve un estilo de programacin limpio donde los objetos son retornados por valor. Considere la siguiente funcin: QVector<double> tablaDelSeno() { QVector<double> vect(360);

11. Clases Contenedoras

110

for (int i = 0; i < 360; ++i) vect[i] = sin(i / (2 * M_PI)); return vect; } El llamado a esta funcin se vera as: QVector<double> tabla = tablaDelSeno();

STL, en comparacin, nos motiva a pasar el vector como una referencia no constante (non-const) para evitar la copia que tiene lugar cuando el valor retornado por la funcin es guardado en una variable: using namespace std; void tablaDelSeno(vector<double> &vect) { vect.resize(360); for (int i = 0; i < 360; ++i) vect[i] = sin(i / (2 * M_PI)); } Luego, el llamado a la funcin se hace ms tedioso para escribirlo y menos claro de leer: vector<double> tabla; tablaDelSeno(tabla); Qt usa la comparticin implcita para todos sus contenedores y para muchas otras clases, incluyendo QByteArray, QBrush, QFont, QImage, QPixmap y QString. Esto hace que estas clases sean muy eficientes para pasar por valor, tanto parmetros de funciones como valores retornados. La comparticin implcita es una garanta de que los datos no sern copiados si no los modificamos. Para obtener lo mejor de la comparticin implcita, podemos adoptar, como programadores, un par de nuevos hbitos de programacin. Un hbito es usar la funcin at() y no el operador [ ], para los casos de acceso de solo lectura sobre un vector o lista (no constante). Ya que los contenedores de Qt no pueden decir si el operador [ ] aparece en el lado izquierdo de una asignacin o no, estos asumen lo peor y fuerzan a que ocurra una copia dado que at() no est permitida en el lado izquierdo de una asignacin. Algo similar sucede cuando iteramos sobre un contenedor con iteradores de estilo STL. En cualquier momento que llamemos a begin() o a end() en un contenedor no constante, Qt fuerza a que ocurra una copia si los datos son compartidos. Para evitar y prevenir esta ineficiencia, la solucin es usar const_iterator, constBegin() y constEnd() cuando sea posible. Qt proporciona un ltimo mtodo para iterar sobre elementos en un contenedor secuencial: el ciclo foreach. Este se ve as: QLinkedList<Pelicula> lista; ... foreach (Pelicula pelicula, lista) { if (pelicula.titulo() == "Citizen Kane") { cout << "Encontrado Citizen Kane" << endl; break; } } La palabra reservada foreach est implementada en trminos del for estndar. En cada iteracin del ciclo, la variable de iteracin (pelicula) se establece a un nuevo elemento, empezando en el primer elemento en el contenedor y avanza hacia adelante. El ciclo foreach hace una copia del contenedor cuando se comienza, y por esta razn el ciclo no se ve afectado si el contenedor es modificado durante la iteracin.

11. Clases Contenedoras

111

Cmo Funciona La Comparticin Implcita (Implicit Sharing)


La comparticin implcita trabaja automticamente tras bastidores, de manera que no tenemos que hacer nada en nuestro cdigo para hacer que esta optimizacin ocurra. Pero, como siempre es bueno saber cmo funcionan las cosas, estudiaremos un ejemplo y veremos qu sucede debajo del cap. El ejemplo usa la clase QString, una de las muchas clases implcitas compartidas de Qt. QString str1 = "Humpty"; QString str2 = str1; Hacemos que str1 sea igual a Humpty y as str2 es igual a str1. En este momento, ambos objetos QString apuntan a la misma estructura de datos en la memoria. Junto con los datos de caracteres, la estructura de datos contiene un contador de referencia que indica cuantos QStrings apuntan a la misma estructura de datos. Ya que str1 y str2 apuntan a los mismos datos, el contador de referencia es 2. str2[0] = D; Cuando modificamos a la variable str2, este primero hace una copia de los datos, para asegurarse de que str1 y str2 apunten a diferentes estructuras de datos, y luego aplica el cambio a su propia copia de los datos. El contador de referencia de los datos de str1 (Humpty) se hace 1 y el contador de referencia de los datos de str2 (Dumpty) son establecidos a 1. Un contador de referencia de 1 significa que los datos no son compartidos. str2.truncate(4); Si modificamos nuevamente a str2, no se lleva a cabo ninguna copia porque el contador de referencia de los datos de str2 es 1. La funcin truncate() opera directamente sobre los datos de str2, dando como resultado la cadena de texto Dump . El contador de referencia sigue siendo 1. str1 = str2; Cuando asignamos str1 a str2, el contador de referencia para los datos de str1 se hace 0, lo cual significa que ya ningn QString est usando el dato Humpty. Los datos, desde luego, son liberados de la memoria. Ambos QStrings apuntan a Dump, el cual tiene ahora un contador de referencia igual a 2. El compartimiento de datos es, a menudo, dejado de lado en aquellos programas que son de mltiples hilos, a raz de las condiciones de rapidez en el sistema de contadores de referencia. Con Qt, esto no es un problema. Internamente, las clases contenedoras usan instrucciones en lenguaje ensamblador para realizar contabilizaciones de referencias atmicas. Esta tecnologa se encuentra disponible para los usuarios de Qt a travs de las clases QSharedData y QSharedDataPointer. Las palabras reservadas para los ciclos break y continue estn soportadas. Si el cuerpo consta de una sola sentencia, las llaves son innecesarias. Como sucede con una sentencia for, la variable de iteracin puede ser definida fuera del ciclo, as: QLinkedList<Pelicula> lista; Pelicula pelicula; ... foreach (pelicula, lista) { if (pelicula.titulo() == "Citizen Kane") { cout << "Encontrado Citizen Kane" << endl; break; } } Definir la variable de iteracin fuera del ciclo es la nica opcin para aquellos contenedores que contienen una coma (por ejemplo, QPair<QString, int>).

11. Clases Contenedoras

112

Contenedores Asociativos
Un contenedor asociativo contiene un nmero arbitrario de elementos del mismo tipo, indexados por una clave. Qt proporciona dos clases contenedoras asociativas principales: QMap<K, T> y QHash<K, T>. Un QMap<K, T> es una estructura de datos que aloja un par conformado por una clave y un valor, ordenados ascendientemente por el campo clave. Esta distribucin hace posible obtener un buen performance en las tareas bsquedas e insercin y tambin en la iteracin in-orden. Internamente, QMap<K, T> est implementada como una lista de saltos (skip-list). Figura 11.6. Mapa de QString a int Mexico City 22 350 000

Seoul

22 050 000

Tokyo

34 000 000

Una manera sencilla de insertar elementos en un mapa es llamando al mtodo insert(): QMap<QString, int> mapa; mapa.insert("eins", 1); mapa.insert("sieben", 7); mapa.insert("dreiundzwanzig", 23); Alternativamente, simplemente podemos asignar un valor a una determinada clave: mapa["eins"] = 1; mapa["sieben"] = 7; mapa["dreiundzwanzig"] = 23; El operador [ ] puede ser usado tanto para insertar como para recuperar. Si el operador [ ] es usado para recuperar un valor para una clave no existente en un mapa no constante, un nuevo elemento ser creado con la clave dada y un valor vacio. Para evitar la creacin accidental de valores, podemos usar la funcin value() para recuperar elementos en lugar de usar el operador [ ]. int valor = mapa.value("dreiundzwanzig"); Si la clave no existe, un valor por defecto es retornado usando el constructor por defecto del tipo del valor, y ningn elemento nuevo ser creado. Para los tipos bsicos y punteros, retorna cero. Podemos especificar tambin cualquier otro valor por defecto como un segundo argumento a la funcin value(), por ejemplo: int segundos = mapa.value("delay", 30); Esto equivale a: int segundos = 30; if (mapa.contains("delay")) segundos = mapa.value("delay");

Los tipos de dato K y T de un QMap<K, T> pueden ser tipos de datos bsicos como int o double, tipos punteros, o clases que tengan un constructor por defecto, un constructor copia y un operador de asignacin. Adems, el tipo de dato K se debe proporcionar un operator<() (menor que) ya que QMap<K, T> usa este operador para aguardar los elementos en orden ascendiente (ordenados por el campo clave).

11. Clases Contenedoras

113

QMap<K, T> posee una pareja de funciones convenientes: keys() y values(), que son especialmente tiles cuando tratamos con pequeos conjuntos de datos. Estas retornan QLists de las claves y valores de un mapa. Los mapas son normalmente mono valuados: si un nuevo valor es asignado a una clave existente, el valor viejo es reemplazado por el nuevo, asegurando que dos elementos no compartan la misma clave. Es posible tener mltiples pares de clave-valor con la misma clave usando la funcin insertMulti() o la subclase conveniente QMultiMap<K, T>. La clase QMap<K, T> tiene una funcin sobrecargada llamada values(const K &) que retorna un QList de todos los valores para una clave dada. Por ejemplo: QMultiMap<int, QString> multiMapa; multiMapa.insert(1, "one"); multiMapa.insert(1, "eins"); multiMapa.insert(1, "uno"); QList<QString> valores = multiMapa.values(1); Un QHash<K, T> es una estructura de datos que guarda un par de clave-valor en una tabla hash. Su interfaz es casi idntica a la de QMap<K, T>, pero posee requerimientos diferentes para el tipo de dato K y usualmente proporciona operaciones de bsqueda mucho ms rpidas que las que puede lograr QMap<K, T>. Otra diferencia es que, en QHash<K, T>, los elementos estn desordenados. En adicin a los requerimientos estndares sobre cualquier tipo de valor alojado en un contenedor, el tipo de dato K de QHash<K, T> necesita proporcionar un operador == () y debe ser soportado por una funcin global llamada qHash() que retorna un valor hash para una clave determinada. Qt ya provee funciones qHash() para tipos de datos enteros, tipos punteros, QChar, QString y QByteArray. QHash<K, T> aloja automticamente un nmero primo de revisin para sus tablas hash internas y las redimensiona tantas veces como tems son insertados o removidos de estas. Tambin es posible hacer pequeos ajustes de rendimiento llamando a la funcin reserve() para especificar el nmero esperado de tems que se esperan alojar en el hash. Tambin se usa squeeze() para encoger la tabla hash basndose en el nmero actual de tems. Un habito suele ser llamar a reserve() con el nmero mximo de tems que esperamos guardar, luego insertar los datos, para finalmente, llamar a squeeze() para minimizar el uso de memoria si haban menos tems de los que se esperaban. Los hashes son normalmente mono valuados, pero cuando se requieran de varios valores, estos pueden ser asignados a alguna clave usando la funcin insertMulti() o la subclase conveniente QMultiHash<K, T>. A parte de QHash<K, T>, Qt tambin proporciona la clase QCache<K, T> que puede ser usada para alojar en cach los objetos, asocindolos con una clave, y un contenedor tipo QSet<K> que guarde nicamente las claves. Internamente, ambos se apoyan en QHash<K, T> y ambos tienen los mismos requerimientos para el tipo de dato K como los tiene QHash<K, T>. La manera ms fcil de iterar a travs de todos los pares clave-valor guardados en un contenedor asociativo es usar un iterador estilo Java. Ya que los iteradores deben dar acceso tanto al valor como a la clave, los iteradores estilo Java para contenedores asociativos trabajan algo diferente a su contraparte secuencial (los iteradores de los contenedores secuenciales). Las principales diferencias son que las funciones next() y previous() retornan un objeto que representa un par clave-valor, y no nicamente un valor. La componente clave y el componente valor son accesibles desde este objeto a travs de las funciones key() y value() respectivamente. Por ejemplo: QMap<QString, int> mapa; ... int suma = 0; QMapIterator<QString, int> i(mapa); while (i.hasNext()) suma += i.next().value();

11. Clases Contenedoras

114

Si necesitamos tener acceso tanto a la clave como al valor, simplemente podemos ignorar el valor de retorno de las funciones next() o previous() y usar las funciones key() y value() del iterador, el cual opera en el ltimo elemento en el que se ha ubicado. QMapIterator<QString, int> i(mapa); while (i.hasNext()) { i.next(); if (i.value() > valorMasAlto) { claveMasAlta = i.key(); valorMasAlto = i.value(); } } Los iteradores mutables poseen una funcin setValue() que modificaa el valor asociado con el elemento actual: QMutableMapIterator<QString, int> i(mapa); while (i.hasNext()) { i.next(); if (i.value() < 0.0) i.setValue(-i.value()); } Los iteradores estilo STL tambin proporcionan las funciones key() y value(). Con los tipos de iteradores no constantes, la funcin value() retorna una referencia no constante, permitindonos cambiar el valor as como lo iteramos. Nota, que aunque estos iteradores son llama dos de estilo STL, estos se desvan significantemente de los iteradores map<K, T> de STL, los cuales estn basados en la plantilla pair<K, T>. El ciclo foreach tambin funciona sobre contenedores asociativos, pero solamente en el componente valor del par compuesto por la clave y el valor (clave-valor). Si necesitamos la componente clave y tambin la componente valor, podemos llamar las funciones keys() y values(cosnt K &) en ciclos foreach anidados: QMultiMap<QString, int> mapa; ... foreach (QString clave, map.keys()) { foreach (int valor, map.values(clave)) { do_something(clave, valor); } }

Algoritmos Genricos
La cabecera <QtAlgorithms> declara una serie de funciones de plantillas globales que implementan algoritmos bsicos en los contenedores. La mayora de estas funciones operan en iteradores estilo STL. La cabecera STL <algorithm> proporciona un conjunto ms completo de algoritmos genricos. Estos algoritmos pueden ser usados en los contenedores de Qt, as como tambin en los contenedores STL. Si las implementaciones STL estn disponibles en todas tus plataformas (para las que desarrollas), probablemente no haya razones para no usar los algoritmos STL cuando Qt carezca de alguno de esos algoritmos. El algoritmo qFind() busca un valor en particular dentro de un contenedor. Este recibe un iterador de inicio y un iterador de fin y retorna un iterador apuntando al primer elemento que coincida, o finaliza si no lo encuentra. En el siguiente ejemplo, la variable i se hace igual a la operacin lista.begin() + 1, mientras que j se hace igual a lista.end(). QStringList lista; lista << "Emma" << "Karl" << "James" << "Mariette"; QStringList::iterator i = qFind(lista.begin(), lista.end(), "Karl");

11. Clases Contenedoras

115

QStringList::iterator j = qFind(lista.begin(), lista.end(), "Petra"); El algoritmo qBinaryFind() realiza una bsqueda de la misma manera que lo hace qFind(), excepto que qBinaryFind() asume que los elementos a buscar ya estn ordenados de manera ascendiente y usa su rpido mtodo de bsqueda binaria en lugar de la bsqueda lineal que lleva a cabo qFind(). El algoritmo qFill() llena un contenedor con valores particulares: QLinkedList<int> lista(10); qFill(lista.begin(), lista.end(), 1009); Al igual que los otros algoritmos basados en iteradores, podemos usar qFill() en una porcin del contenedor variando los argumentos. El siguiente segmento de cdigo inicializa los primeros cinco elementos de un vector en 1009 y los dos ltimos elementos en 2013. QVector<int> vect(10); qFill(vect.begin(), vect.begin() + 5, 1009); qFill(vect.end() - 5, vect.end(), 2013); El algoritmo qCopy() copia los valores desde un contenedor a otro: QVector<int> vect(lista.count()); qCopy(lista.begin(), lista.end(), vect.begin()); qCopy() tambin puede ser usado para copiar valores dentro del mismo contenedor, siempre y cuando el rango de fuente y el rango de destino no se solapen. En el siguiente segmento de cdigo, los usamos para sobre escribir los dos ltimos elementos de una lista con los primeros dos elementos: qCopy(lista.begin(), lista.begin() + 2, lista.end() - 2); El algoritmo qSort() ordena los elementos del contenedor en ascendientemente: qSort(lista.begin(), lista.end()); Por defecto, qSort() usa el operador < para comparar los elementos. Para ordenar los elementos en orden descendiente, hay que pasar como tercer argumento a qGreater<T> (donde T es el tipo del valor): qSort(lista.begin(), lista.end(), qGreater<int>()); Podemos usar el tercer argumento para definir el criterio de ordenacin. Por ejemplo, aqu se muestra una comparacin menor que que compara dos QStrings de manera no sensible a las maysculas: bool menorQue(const QString &str1, const QString &str2) { return str1.toLower() < str2.toLower(); } El llamado a qSort() luego se transforma en: QStringList lista; ... qSort(lista.begin(), lista.end(), menorQue); El algoritmo qStableSort() es similar a qSort(), con la diferencia de que este garantiza que los elementos que compare como iguales aparezcan en el mismo orden que tenan antes de la ordenacin. Esto es especialmente til si el criterio de ordenamiento solamente debe tomar en cuenta partes del valor y el resultado es visible al usuario. Nosotros hemos usado este mtodo anteriormente en el Captulo 4 para implementar el ordenamiento en la aplicacin Hoja de Clculo.

11. Clases Contenedoras

116

El algoritmo qDeleteAll() llama a la palabra reservada delete en cada puntero guardado en un contenedor. Hacer esto solo tiene sentido en aquellos contenedores cuyo tipo de valor es un puntero. Despus de la llamada a esta funcin, aun debe hacerse el llamado a la funcin clear(). Por ejemplo: qDeleteAll(lista); ista.clear(); El algoritmo qSwap() intercambia el valor de dos variables. Por ejemplo: int x1 = linea.x1(); int x2 = linea.x2(); if (x1 > x2) qSwap(x1, x2); Finalmente, la cabecera <QtGlobal>, la cual es incluida en otras cabeceras de Qt, proporciona muchas definiciones tiles, incluyendo la funcin qAbs(), que retorna el valor absoluto de sus argumentos, y las funciones qMin() y qMax(), que retornan el mnimo y el mximo de dos valores.

Cadenas de Textos, Arreglos de Bytes y Variantes (Strings, Byte Arrays y Variants)


QString, QByteArray y QVariant son tres clases que tienen mucho en comn con los contenedores y que pueden ser usadas como alternativas a estos, en algunos contextos. As mismo, al igual que los contenedores, estas clases usan comparticin implcita como una optimizacin de memoria y de velocidad. Empezaremos con la clase QString. Las cadenas de texto, es decir las strings, son usadas en todos los programas con interfaz grafica (y de consola tambin), no solamente para la interfaz de usuario sino tambin como estructuras de datos. El lenguaje C++ proporciona nativamente dos tipos de cadenas: las cadenas tradicionales de C, que poseen el carcter de finalizacin de cadenas \0 y la clase std::string. A distincin de estas, QString contiene valores Unicode de 16-bits. Unicode contiene ASCII y Latin-1 como un conjunto, con sus valores numricos usuales. Pero ya que QString es de 16-bits, este puede representar miles de otros caracteres para escribir la mayora de los lenguajes del mundo. Vea el Capitulo 17 para ms informacin acerca de Unicode. Cuando usamos QString, no tenemos que preocuparnos por detalles como reservar suficiente memoria o asegurarnos de que las cadenas terminen en \0. Conceptualmente, los tipos de datos QString pueden considerarse como un vector de QChars. Un QString puede incrustar caracteres \0. La funcin length() retorna el tamao de toda la cadena, incluyendo los caracteres \0 incrustados. QString provee un operador binario + para concatenar dos cadenas y un operador += para anexar una cadena a otra. Ya que QString asigna espacio de memoria al final de los datos de la cadena, construir una cadena a travs de la repeticin de anexar caracteres es muy rpido. Aqu est un ejemplo que muestra cmo trabajar con los operadores + y +=: QString str = "Usuario: "; str += nombreUsuario + "\n"; Tambin existe una funcin QString::append() que hace lo mismo que el operador +=: str = "Usuario: "; str.append(nombreUsuario); str.append("\n"); Una manera totalmente diferente de combinar cadenas es usar la funcin de QString llamada sprintf(): str.sprintf("%s %.1f%%", "competicin perfecta", 100.0);

11. Clases Contenedoras

117

Esta funcin soporta los mismos especificadores de formato que la funcin sprintf() de C++. En el ejemplo anterior, a la cadena str le es asignada la cadena competicin perfecta 100.0%. Todava hay otra manera de construir una cadena a partir de otras cadenas o de nmeros, la funcin arg(): str = QString("%1 %2 (%3-%4)") .arg("sociedad").arg("indulgente").arg(1950).arg(1970); En este ejemplo, el %1 es reemplazado por sociedad, el %2 es reemplazado por indulgente, el %3 es reemplazado por 1950 y el %4 es reemplazado por 1970. El resultado es la cadena sociedad indulgente (1950-1970). Existen varias sobrecargas de arg() para manejar varios tipos de datos. Algunas sobrecargas poseen parmetros extras para controlar el ancho del campo, la base numrica, o la precisin de los punto flotantes. En general, arg() es una solucin mucho mejor que sprintf(), porque esta es segura cuando el tipo de dato a incrustar es importante, soporta totalmente Unicode y permite usar intrpretes para reordenar los parmetros %n. QString puede convertir nmeros en cadenas usando la funcin esttica QString::number(): str = QString::number(59.6); O usando la funcin setNum(): str.setNum(59.6); La conversin inversa, de cadena de texto a nmero, se puede lograr usando las funciones respectivas toInt(), toLongLong(), toDouble() y as sucesivamente. Por ejemplo: bool ok; double d = str.toDouble(&ok); Estas funciones aceptan un puntero opcional a una variable booleana y establece la variable a true o false, dependiendo del xito de la operacin de conversin. Si la conversin falla, estas funciones retornan cero. Una vez que obtengamos una cadena, muy a menudo querremos extraer partes de esta. La funcin mid() retorna la subcadena que empieza en una posicin dada (el primer argumento) y extendindose hasta otra posicin (el segundo argumento). Por ejemplo, el siguiente cdigo imprime pagar en la consola: * QString str = "hay que pagar la renta"; qDebug() << str.mid(8, 4); Si omitimos el segundo argumento, la funcin mid() retorna la subcadena que se forma al empezar en la posicin dada (8) y al terminar en el final de la cadena. Por ejemplo, el sigui ente cdigo imprime pagar la renta en la consola: QString str = "hay que pagar la renta"; qDebug() << str.mid(8); Tambin estn las funciones left() y right() que realizan un trabajo similar. Ambas aceptan un numero de caracteres, n, y retornan los primeros o ltimos n caracteres de la cadena. Por ejemplo, el siguiente cdigo imprime hay renta en la consola: QString str = "hay que pagar la renta"; qDebug() << str.left(3) << " " << str.right(5); Si queremos encontrar si una cadena contiene un carcter, una subcadena o una expresin regular en particular, podemos usar una de las funciones indexOf de QString: QString str = "la mitad"; int i = str.indexOf("mitad"); * La sintaxis conveniente de QDebug << arg usada aqu requiere la inclusin del archivo de cabecera <QtDebug>,
mientras que la sintaxis (,arg) est disponible en cualquier archivo que incluya al menos una cabecera de Qt.

11. Clases Contenedoras

118

El cdigo anterior har que la variable i valga 3. La funcin indexOf() retorna -1 cuando falla, y acepta una posicin de inicio opcional y una bandera de case-sensivity. Si solo queremos verificar si una cadena empieza o termina con algo, podemos usar las funciones startsWith() y endsWith(): if (url.startsWith("http:") && url.endsWith(".png")) ... Lo anterior es igual de simple y rpido que esto: if (url.left(5) == "http:" && url.right(4) == ".png") ... Las comparaciones de cadenas con el operador == son case-sensitive. Si estamos comparando cadenas visibles al usuario, la funcin localeAwareCompare() es usualmente la eleccin ms indicada, y si queremos que las comparaciones sean case-sensitive, podemos usar los mtodos toUpper() y toLower(). Por ejemplo: if (nombreArchivo.toLower() == "leeme.txt") ... Si queremos reemplazar cierta parte de una cadena con otra cadena, podemos usar la funcin replace(): QString str = "un da nublado"; str.replace(7, 7, "soleado"); El resultado es un da soleado. El cdigo puede reescribirse para usar las funciones remove() e insert(): str.remove(7, 7); str.insert(7, "soleado"); Primero, removimos siete caracteres empezando en la posicin 7, quedando la cadena un da, luego insertamos soleado en la posicin 7. Existen versiones sobrecargadas de la funcin replace() que reemplazan todas las ocurrencias de su primer argumento con su segundo argumento. Por ejemplo, aqu est la manera de reemplazar todas las ocurrencias de & con &amp; en una cadena: str.replace("&", "&amp;"); Una necesidad muy frecuente, es la de extraer o eliminar los espacios en blanco (como los espacios, tabulaciones o saltos de lnea) de una cadena. QString posee una funcin que elimina esos espacios blancos de ambos extremos de una cadena: QString str = " BOB \t EL \nPERRO \n"; qDebug() << str.trimmed(); La cadena str puede ser representada de esta manera: B O B \t E L \n P E R R O \n

La cadena retornada por la funcin trimmed() es: B O B \t E L \n P E R R O

Cuando manejamos la entrada del usuario, a menudo, queremos reemplazar cada secuencia de una o ms caracteres en blanco internos con un solo espacio, adicionalmente a quitar los espacios en blanco de los extremos. Esto es lo que hace precisamente la funcin simplified():

11. Clases Contenedoras

119

QString str = " BOB \t EL \nPERRO \n"; qDebug() << str.simplified(); La cadena retornada por simplified() es: B O B Una cadena puede ser QString::split(): dividida en E L un P E R R O formado por subcadenas usando

QStringList

QString str = "hay que pagar la renta"; QStringList palabras = str.split(" "); En el ejemplo anterior, lo que hicimos fue dividir la cadena hay que pagar la renta en cinco subcadenas: hay, que, pagar, la, renta. La funcin split() posee un tercer argumento opcional que especifica si se debe quedar con subcadenas vacas (la opcin por defecto) o estas deben ser descartadas. Los elementos en un QStringList deben ser unidos para formar una sola cadena usando el mtodo join(). El argumento que se le enva join() es insertado entre cada par de cadenas unidas. Por ejemplo, aqu est la manera de crear una sola cadena que est compuesta de todas las cadenas contenidas en un QStringList ordenado alfabticamente y separados por saltos de lnea: palabras.sort(); str = palabras.join("\n"); Cuando se trata con cadenas de texto, regularmente solemos necesitar determinar si una cadena est vaca o no. Esto se hace llamando al mtodo isEmpty() o verificando si el mtodo length() es igual a 0. La conversin de cadenas const char * a cadenas QString es una operacin automtica en la mayora de los casos. Por ejemplo: str += " (1870)"; Aqu agregamos un const char * a un QString sin ningn protocolo. Para convertir explcitamente un const char * a un QString, simplemente debemos usar un cast de QString, o llamar a la funcin fromASCii() o a la funcin fromLatin1() (Vea el Capitulo 17 para una explicacin de manejo de cadenas literales en otras codificaciones). Para hacer una conversin de QString a const char *, debemos usar las funciones toAscii() o toLatin1(). Estas funciones retornan un QByteArray, el cual puede ser convertido a un cont char * usando QByteArray::data() o QByteArray::constData(). Por ejemplo: printf("Usuario: %s\n", str.toAscii().data()); Por conveniencia, Qt provee toAscii().constData(): el macro qPrintable() que realiza lo mismo que

printf("Usuario: %s\n", qPrintable(str)); Cuando llamamos a data() o a constData() en un QByteArray, la cadena retornada pasa a ser poseda por el objeto QByteArray. Esto significa que no necesitamos preocuparnos de los famosos goteos de memorias o, en ingls, memory leaks. Qt reclamar la memoria necesaria por nosotros. Por otra parte, debemos ser muy cuidadosos de no usar el puntero por mucho tiempo. Si el QByteArray no es guardado en una variable, este ser automticamente eliminado al final de la declaracin. La clase QByteArray posee una API muy similar a la que tiene QString. Funciones como left(), right(), mid(), toLower(), toUpper(), trimmed() y simplified() existen tambin en QByteArray con las misma semntica que tiene QString. QByteArray es til para guardar datos binarios en crudo y cadenas de texto de 8-bits. En general, recomendamos usar QString cuando se trata de guardar textos y no usar QByteArray para eso, porque QString soporta Unicode.

11. Clases Contenedoras

120

Por comodidad, QByteArray se asegura automticamente de que el ltimo byte pasado el ultimo carcter de la cadena, sea \0, facilitando el pase de un QByteArray a una funcin que recibe un const char *. QByteArray tambin soporta caracteres \0 incrustados, permitindonos usarlo para guardar datos binarios a placer. En algunas situaciones, necesitamos guardar datos de tipos diferentes en la misma variable. Un mtodo consiste en codificar los datos como un QByteArray o un QString. Por ejemplo, una cadena puede contener un valor textual o un valor numrico en forma de cadena. Estos mtodos nos proporcionan completa flexibilidad, pero suprimen algunos de los beneficios del C++, en particular la eficiencia y la seguridad con los tipos de datos. Qt proporciona una manera mucho ms limpia de manejar variables que pueden contener diferentes tipos de datos: QVariant. La clase QVariant puede contener valores de muchos tipos Qt, incluyendo QBrush, QColor, QCursor, QDateTime, QFont, QKeySequence, QPalette, QPen, QPixmap, QPoint, QRect, QRegion, QSize y QString, as como tambin los tipos de datos numricos bsicos del C++, como double e int. La clase QVariant tambin puede alojar contenedores: QMap<QString,QVariant>, QStringList y QList<QVariant>. Las variantes son usadas extensivamente por las clases de visualizacin de elementos (clases tem view), los mdulos de bases de datos y QSettings, permitindonos leer y escribir los datos de un elemento, datos de una base de datos y preferencias del usuario para cualquier tipo de dato compatible con QVariant. Ya hemos visto un ejemplo de esto en el Captulo 3, donde pasamos un QRect, un QStringList y un par de variables tipo bool como variantes a QSettings::setValue() y los recuperamos luego como variantes. Es posible crear estructuras de datos complejas y arbitrarias usando QVariant a travs del anidamiento de valores de los tipos de contenedores: QMap<QString, QVariant> mapaPera; mapaPera ["Estandar"] = 1.95; mapaPera ["Organica"] = 2.25; QMap<QString, QVariant> mapaFruta; mapaFruta ["Naranja"] = 2.10; mapaFruta ["Pia"] = 3.85; mapaFruta ["Pera"] = mapaPera; Ac hemos creado un mapa con cadenas de texto como claves (nombres de productos) y valores que son tanto nmeros de punto flotante (precios) como mapas. El mapa de primer nivel contiene tres claves: Naranja, Pera y Pia. El valor asociado con la clave Pera es un mapa que contiene dos claves (Estandar y Organica). Cuando se itera sobre un mapa que contiene valores variantes, necesitamos usar la funcin type() para verificar el tipo que una variante contiene o aloja de manera que podamos responder apropiadamente. Crear estructuras de datos como esta puede ser algo muy atractivo ya que podemos organizar los datos de la manera que queramos. Pero la comodidad de QVariant implica sacrificar algo de eficiencia y legibilidad. Como una regla, vale ms la pena definir nuestra propia clase C++ para guardar nuestros datos siempre que sea posible. QVariant es usada por el sistema de meta-objetos de Qt y por lo tanto es parte del mdulo QtCore. No obstante, cuando enlazamos con el modulo QtGui, QVariant puede alojar tipos de datos relacionados a la interfaz de usuario (GUI-related) tales como QColor, QFont, QIcon, QImage y QPixmap: QIcon icono("abrir.png"); QVariant variant = icono; Para recuperar el valor de uno de estos tipos desde una QVariant, podemos usar la funcin miembro tipo plantilla QVariant::value<T>(): QIcon icono = variant.value<QIcon>();

11. Clases Contenedoras

121

La funcin value<T> () tambin funciona para convertir entre tipos de datos no relacionados a la interfaz de usuario (non-GUI data types) y QVariant, pero en la prctica lo que usamos normalmente son las funciones de conversin to() (por ejemplo, toQString()) para los tipos de datos no relacionados a la interfaz de usuario. QVariant puede usarse tambin para alojar tipos de datos propios, asumiendo que estos ya tienen un constructor por defecto y un constructor copia. Para que esto funcione, primero debemos registrar el tipo de dato usando el macro Q_DECLARE_METATYPE(), generalmente en un archivo de cabecera arriba de la definicin de la clase: Q_DECLARE_METATYPE(TarjetaDeNegocios) Esto nos permite escribir el cdigo de esta manera: TarjetaDeNegocios tarjetaNegocios; QVariant variant = QVariant::fromValue(tarjetaNegocios); ... if (variant.canConvert< TarjetaDeNegocios >()) { TarjetaDeNegocios tarjeta = variant.value< tarjetaNegocios >(); ... } Debido a las limitaciones de un compilador, estas funciones miembros tipo plantilla no estn disponibles para MSVC 6. Si necesitas usar este compilador, debes usar las funciones globales qVariantFromValue(), qVariantValue<T> () y qVariantCanConvert<T> (). Si los tipos de datos propios poseen los operadores << y >> para escribir y leer desde un QDataStream, podemos registrarlos usando QRegisterMetaTypeStreamOperators<T> (). Esto hace posible guardar las preferencias de los tipos de datos propios usando QSettings, entre otras cosas. Por ejemplo: qRegisterMetaTypeStreamOperators<TarjetaDeNegocios>("TarjetaDeNegocios"); Este captulo se ha enfocado en los contenedores de Qt, as como tambin en las clases QString, QByteArray y QVariant. Adicionalmente a estas clases, Qt tambin proporciona otros contenedores. Uno es QPair<T1, T2>, el cual simplemente guarda dos valores y es similar a std::pair<T1, T2>. Otro es QBitArray, el cual usaremos en la primera seccin del Captulo 19. Finalmente, est QVarLengthArray<T, Prealloc>, una alternativa a QVector<T> pero de bajo nivel. Debido a que este reserva espacio de memoria en la pila y no est compartida implcitamente, su costo operativo es menor que la de QVector<T>, hacindolo ms apropiado para ciclos fuertes. Los algoritmos de Qt, incluyendo unos pocos que no se han cubierto aqu como lo son qCopyBackward() y qEqual(), son descritos en la documentacin de Qt en http://doc.trolltech.com/4.1/algorithms.html. Y para mas detalles de los contenedores de Qt, incluyendo informacin acerca de su complejidad y estrategias de desarrollo, visite http://doc.trolltech.com/4.1/containers.html.

12. Entrada/Salida

122

12. Entrada/Salida

Lectura y Escritura de Datos Binarios Lectura y Escritura de Archivos de Texto Navegar por Directorios Incrustando Recursos Comunicacin entre Procesos

La lectura o escritura de archivos u otros dispositivos es una necesidad comn en casi todas las aplicaciones. Qt provee un excelente soporte para las operaciones de E/S a travs de QIODevice, una poderosa abstraccin que encapsula "dispositivos" que pueden leer y escribir bloques de bytes. Qt incluye las siguientes clases derivadas de QIODevice: QFile QTemporaryFile QBuffer QProcess QTcpSocket QUdpSocket Permite el acceso a archivos del sistema local y recursos embebidos. Crea y accede archivo temporales en el sistema. Realiza operaciones de lectura y escritura de datos sobre QByteArray. Ejecuta programas externos y maneja comunicacin entre procesos. Transfiere un flujo de datos sobre una red usando TCP. Enva o recibe datagramas UDP sobre la red.

QProcess, QTcpSocket y QUdpSocket son dispositivos secuenciales, por lo que los datos pueden ser accedidos solo una vez, comenzando por el primer byte y progresando serialmente hasta el ultimo. QFile, QTemporaryFile y QBuffer son dispositivos de acceso aleatorio, por lo que los bytes pueden ser ledos cualquier cantidad de veces desde cualquier posicin; estas cuentan con la funcin QIODevice::seek() para posicionar el puntero del archivo. Sumado a las clases de dispositivos, Qt tambin provee dos clases de alto nivel que pueden ser usadas para leer o escribir cualquier dispositivo de E/S: QDataStream para datos binarios y QTextStream para texto. Estas clases se encargan de cuestiones tales como ordenacin de bytes y codificacin de textos, asegurando que las aplicaciones ejecutadas en diferentes plataformas o en diferentes pases puedan leer y escribir dichos archivos. Esto hace que las clase para manejo de E/S proporcionadas por Qt sean mucho ms prcticas que las correspondientes clases estndares de C++, las cuales dejan que el programador de la aplicacin se encargue de estas cuestiones. QFile facilita el acceso a archivos individuales, ya sea si se encuentran en el sistema o embebidos en el ejecutable como un recurso. Para aquellas aplicaciones que necesiten identificar un conjunto de archivos, Qt provee las clases QDir y QFileInfo, las cuales manejan directorios y proveen informacin sobre los archivos que se encuentran dentro de estos. La clase QProccess permite lanzar programas externos y comunicarse con estos a travs de los canales estndares de entrada, salida y de error (cin, cout y cerr). Tambin podemos establecer el valor de las variables de entorno que la aplicacin externa usar y su directorio de trabajo. Por defecto, la comunicacin con el proceso es asncrona, es decir, que no bloquea nuestra aplicacin, pero es posible bloquearla en ciertas ocasiones.

12. Entrada/Salida

123

El trabajo en red y el manejo de archivos XML son temas tan importantes que sern cubiertos separadamente en captulos exclusivos (Captulo 14 y Captulo 15).

Lectura y Escritura de Datos Binarios


La manera ms simple de cargar y guardar datos binarios es instanciar la clase QFile, para abrir el archivo, y acceder a sus datos a travs de un objeto QDataStream. Esta clase provee un formato de almacenamiento independiente de la plataforma que soporta los tipos bsicos de C++ como int y double, pero adems tambin incluye soporte para QByteArray, QFont, QImage, QPixmap, QString y QVariant, as como tambin clases contenedoras como QList<T> y QMap<K,T>. Aqu mostramos cmo podramos almacenar un entero, un QImage y un QMap<QString, QColor> en un archivo llamado facts.dat: QImage imagen("foto.png"); QMap<QString, QColor> mapa; mapa.insert("rojo", Qt::red); mapa.insert("verde", Qt::green); mapa.insert("azul", Qt::blue); QFile archivo("facts.dat"); if (!archivo.open(QIODevice::WriteOnly)) { cerr << "No se pudo abrir el archivo: " << qPrintable(archivo.errorString()) << endl; return; } QDataStream salida(&archivo); salida.setVersion(QDataStream::Qt_4_1); salida << quint32(0x12345678) << imagen << mapa; Si no podemos abrir el archivo, informamos al usuario de la situacin y salimos. La macro qPrintable() devuelve un const char * para un QString dado. (Otro enfoque hubiese sido usar QString::toStdString(), la cual retorna un std::string, para el cual <iostream> tiene un operador << sobrecargado para este tipo de dato). Si el archivo es abierto satisfactoriamente, creamos un QDataStream y establecemos el nmero de versin que se utilizar. Este es un entero que indica la forma de representar los tipos de datos de Qt (los tipos bsicos de C++ son representados siempre de la misma manera). En Qt 4.1, el formato ms completo es la versin 7. Podemos establecer el valor a mano o usar un nombre simblico (en este caso QDataStream::Qt_4_1). Para asegurarnos de que el nmero 0x12345678 sea escrito como un entero de 32 bits sin signo en todas las plataformas, lo convertimos a quint32, un tipo de dato que nos garantiza 32 bits exactamente. Para asegurar la interoperabilidad, QDataStream estandariza a "big endian" (vase Endianness en el Glosario de terminos) por defecto, esto puede modificarse por medio de setByteOrder(). No necesitamos cerrar explcitamente el archivo ya que esto se realiza automticamente cuando la variable QFile queda fuera de mbito. Si queremos verificar que los datos hayan sido escritos, podemos llamar a la funcin flush() y verificar su valor de retorno (true si no hubo errores). El cdigo para leer los datos guardados en el archivo fact.dat es: quint32 nun; QImage imagen; QMap<QString, QColor> mapa; QFile archivo("facts.dat"); if (!archivo.open(QIODevice::ReadOnly)) { cerr << "No se pudo abrir el archivo: " << qPrintable(archivo.errorString()) << endl;

12. Entrada/Salida

124

return; } QDataStream entrada(&archivo); entrada.setVersion(QDataStream::Qt_4_1); entrada >> num >> imagen >> mapa; La versin de QDataStream que usamos para leer los datos es la misma que la usada para escribirlos. Siempre debe ser as. Estableciendo la versin por cdigo, nos aseguramos que la aplicacin siempre pueda leer los datos (asumiendo que sta fue compilada con QT 4.1 o cualquier versin posterior). La clase QDataStream almacena los datos de manera tal que puedan ser recuperados sin problemas. Por ejemplo, un QByteArray es representado como una cantidad de 32 bytes seguida por los bytes de datos. QDataStream tambin puede ser usada para leer y escribir bytes sin formato usando las funciones readRawBytes() y writeRawBytes(), obviando los encabezados que indican la cantidad de bytes guardados. El manejo de errores, cuando recuperamos datos con QDataStream, es bastante fcil. El flujo tiene un estado (valor devuelto por status()), que puede tomar cualquiera de los siguientes valores: QDataStream::Ok, QDataStream::ReadPastEnd o QDataStream::ReadCorruptData. Una vez que haya ocurrido un error, el operador >> devolver siempre cero o valores vacos. Esto posibilita que podamos simplemente leer el archivo completo sin preocuparnos por los errores potenciales y verificar el valor de estado al final para ver si los datos son vlidos. QDataStream soporta una variedad de datos de C++ y de Qt: la lista completa est disponible en http://doc.trolltech.com/4.1/datastreamformat.html. Podemos agregar soporte para nuestros propios tipos de datos con solo sobrecargar los operadores << y >>. Aqu hay una definicin de un tipo de dato que puede ser usado con QDataStream: class Cuadro { public: Cuadro() { miFecha = 0; } Cuadro(const QString &titulo, const QString &artista, int fecha) { miTitulo = titulo; miArtista = artista; miFecha = fecha; } void setTitulo(const QString &titulo) {miTitulo = titulo;} QString titulo() const { return miTitulo; } ... private: QString miTitulo; QString miArtista; int miFecha; }; QDataStream &operator<<(QDataStream &salida, const Cuadro &cuadro); QDataStream &operator>>(QDataStream &entrada, Cuadro &cuadro); De esta manera debemos implementar el operador <<: QDataStream &operator<<(QDataStream &salida, const Cuadro &cuadro) { salida << cuadro.titulo() << cuadro.artista() << quint32(cuadro.fecha()); return salida; }

12. Entrada/Salida

125

Para guardar una clase Cuadro, simplemente guardamos dos QString y un quint32. Al final de la funcin, devolvemos el objeto QDataStream. Esto es algo comn en C++, que nos permite encadenar operadores << con un solo flujo de salida. Por ejemplo: salida << cuadro1 << cuadro2 << cuadro3;. La implementacin del operador >> es similar al de <<: QDataStream &operator>>(QDataStream &entrada, Cuadro &cuadro) { QString titulo; QString artista; quint32 fecha; entrada >> titulo >> artista >> fecha; cuadro = Cuadro(titulo, artista, fecha); return entrada; } Proveer operadores de E/S para tipos de datos propios nos aporta varios beneficios. Uno de ellos es que permitir usarlo en operaciones de E/S con contenedores. Por ejemplo: QList<Cuadro> cuadros = ...; salida << cuadros; Podemos cargar el contenedor de una manera muy fcil: QList<Cuadro> cuadros; entrada >> cuadros; Este cdigo generara un error de compilacin si la clase Cuadro no tuviese soporte para << o >>. Otro beneficio de proporcionar operadores de flujos para tipos de datos propios es que podemos almacenar valores de estos tipos como QVariants, lo cual los hace ms utilizables, por ejemplo con la clase QSetting. Esto funciona siempre que registremos el tipo usando qRegisterMetaTypeStreamOperators<T>() por adelantado, como se explic en el Capitulo 11. Cuando usamos QDataStream, Qt se encarga de leer y escribir cada tipo, incluyendo contenedores con una cantidad arbitraria de elementos. Esto nos alivia de la necesidad de estructurar los datos que vamos a escribir y de realizar cualquier tipo de anlisis de los datos que queremos leer. Nuestra nica obligacin es asegurarnos que leemos los datos en el mismo orden que los hemos escrito, dejando que Qt se encargue de los detalles. QDataStream sirve tanto para formatos propio de una aplicacin como para formatos binarios estndares. Podemos leer y escribir formatos binarios estndares usando los operadores de flujo sobre tipos bsicos (como quint16 o float) o usando las funciones readRawBytes() y writeRawBytes(). Si estamos usando QDataStream para leer y escribir exclusivamente tipos de datos bsicos de C++, no debemos llamar nunca a setVersion(). Hasta el momento, hemos cargado y guardado datos con la versin del flujo establecida a QDataStream::Qt_4_1. Este enfoque es simple y seguro, pero tiene un pequeo inconveniente: no podemos aprovechar mejoras o nuevos formatos. Por ejemplo, si en una versin posterior de Qt se agrega un nuevo atributo a la clase QFont y nosotros hemos establecido el numero de versin a Qt_4_1, este atributo no podra ser ni guardado ni cargado. Para este problema tenemos dos posibles soluciones. La primera sera integrar el nmero de versin en el archivo: QDataStream salida(&archivo); salida << quint32(NumeroMagico) << quint16(salida.version()); (NumeroMagico es una constante que identifica inequvocamente el tipo de archivo). Eso nos asegura que siempre escribamos los datos usando la versin ms reciente de QDataStream, cualquiera que sta sea. Cuando vamos a cargar los datos desde el archivo, primero leemos la versin utilizada:

12. Entrada/Salida

126

quint32 magico; quint16 version; QDataStream entrada(&archivo); entrada >> magico >> version; if (magico != NumeroMagico) { cerr << "El archivo no es reconocido por la aplicacin" << endl; } else if (version > entrada.version()) { cerr << "El archivo fue creado con una versin ms reciente de la aplicacin" << endl; return false; } entrada.setVersion(version); Podemos leer los datos del archivo siempre que la versin del flujo sea menor o igual a la versin usada por la aplicacin; si esto no se cumple, reportamos el error. Si el formato del archivo contiene un nmero de versin propio, podemos usarlo para deducir la versin del flujo usada, en vez de almacenarlo explcitamente. Por ejemplo, supongamos que la versin del formato de archivo de nuestra aplicacin sea 1.3. Podemos escribir los datos as: QDataStream salida(&archivo); salida.setVersion(QDataStream::Qt_4_1); salida << quint32(NumeroMagico) << quint16(0x0103); Cuando leamos estos datos, determinaremos la versin de QDataStream usada en base al nmero de versin de la aplicacin: QDataStream entrada(&archivo); entrada >> magico >> appVersion; if (magico != NumeroMagico) { cerr << "El archivo no es reconocido por la aplicacin" << endl; return false; } else if (appVersion > 0x0103) { cerr << "El archivo fue creado con una versin ms reciente de la aplicacin"<< endl; return false; } if (appVersion < 0x0103) { entrada.setVersion(QDataStream::Qt_3_0); } else { entrada.setVersion(QDataStream::Qt_4_1); } En este ejemplo, especificamos que cualquier archivo guardado con una versin anterior a 1.3 de la aplicacin usa la versin 4 (Qt_3_0), y que los archivos guardados con la versin 1.3 usan la versin 7 (Qt_4_1). En resumen, hay tres polticas para manejar versiones de QDataStream: establecer a mano el nmero de versin, escribirlo y leerlo explcitamente y usar diferentes versiones de acuerdo a la versin de la aplicacin. Cualquiera de estas pueden usarse para asegurar que los datos escritos en una versin anterior de una aplicacin puedan ser cargados en una nueva versin, aun si sta se ha compilado con una versin posterior de Qt. Una vez que hayamos escogido una poltica para manejar la versin de QDataStream, el proceso de leer y escribir datos binarios con Qt es simple y confiable. Si queremos leer o escribir un archivo en un solo paso, podemos evitar el uso de QDataStream y usar las funciones write() y readAll() de QIODevice. Por ejemplo:

12. Entrada/Salida

127

bool copiarArchivo(const QString &origen, const QString &destino) { QFile archivoOrigen(origen); if (!archivoOrigen.open(QIODevice::ReadOnly)) return false; QFile archivoDestino(destino); if (!archivoDestino.open(QIODevice::WriteOnly)) return false; archivoDestino.write(archivoOrigen.readAll()); return archivoOrigen.error() == QFile::NoError && archivoDestino.error() == QFile::NoError; } En la linea donde llamamos a readAll(), el contenido completo del archivo es cargado dentro de un QByteArray, el cual luego es pasado a la funcin write() para que lo guarde en otro archivo. Tener todos los datos en un QByteArray requiere ms memoria que ir leyendo los elementos uno a uno, pero esto ofrece algunas ventajas. Por ejemplo, podemos usar qCompress() y qUncompress() para comprimir y descomprimir datos. Hay otros escenarios en donde acceder a QIODevice directamente es ms apropiado que usar QDataStream. La clase QIODevice provee la funcin peek() que devuelve el siguiente byte de datos sin efectos secundarios mientras que la funcin getChar() transforma el byte en un carcter. Esto funciona para dispositivos de acceso aleatorio (como archivos) as coma tambin para dispositivos secuenciales (como sockets de red). Tambin disponemos de la funcin seek() que establece la posicin del dispositivo, para aquellos que soporten acceso aleatorio. Los archivos binarios proveen las forma ms verstil y compacta de almacenar datos, y QDataStream hace que el acceso a datos binarios sea muy fcil. Adems de los ejemplos dados en esta seccin, hemos visto el uso de QDataStream en el Capitulo 4 para leer y escribir los archivos de la hoja de calculo y lo veremos de nuevo en el Capitulo 19 para cargar y guardar cursores de Windows.

Lectura y Escritura de Archivos de Texto


Mientras que los datos binarios son tpicamente ms compactos que los formatos basados en texto, no son legibles ni editables para el humano. En casos donde esto es un problema, podemos guardar los datos en formato de texto. Qt provee la clase QTextStream para leer y escribir archivos de textos planos y archivos que usen otros formatos de texto como HTML, XML y cdigo fuente. El manejo de archivos XML se cubre debidamente en el Capitulo 15. QTextStream se encarga de convertir entre Unicode y la codificacin usada en el sistema o cualquier otra codificacin, y maneja de manera transparente los diferentes caracteres de fin de linea usados por los distintos sistemas operativos (/r/n en Windows y /n en Unix y MacOS X). Su unidad de datos fundamental es el tipo de 16 bits QChar. Adems soporta los tipos numricos bsicos de C++, los cuales puede convertir a cadenas y viceversa. Por ejemplo. el siguiente cdigo guarda la cadena ThomasM.Disch: 334 /n en el archivo sf-book.txt: QFile archivo("sf-book.txt"); if (!archivo.open(QIODevice::WriteOnly)) { cerr << "No se pudo abrir el archivo: " << qPrintable(archivo.errorString()) << endl; return; } QTextStream salida(&archivo); salida << "Thomas M. Disch: " << 334 << endl; Guardar texto es realmente fcil, pero cargarlo puede ser difcil, porque los datos de tipo texto son ambiguos. Consideremos el siguiente ejemplo: salida << "Noruega" << "Suecia";

12. Entrada/Salida

128

Si el objeto salida es un QTextStream, el valor que se guardar ser "NoruegaSuecia". No podemos esperar que el siguiente cdigo vuelva a leer los datos correctamente: entrada >> str1 >> str2; De hecho, lo que sucede es que str1 toma el valor de la palabra completa "NoruegaSuecia" y str2 queda vaco. Este problema no lo tenemos con QDataStream porque este almacena el largo de cada cadena antes de los caracteres de datos. Para formatos de archivo complejos, podramos necesitar un verdadero analizador. Este se encargara de cargar caracter por caracter usando el operador >> con un QChar, o linea a linea usando QTextStream::readLine(). Al final de esta seccin, presentaremos dos pequeos ejemplos, uno que lee un archivo linea a linea y otro que lo hace caracter por caracter. Para analizadores que trabajan sobre el texto completo, podemos cargar el contenido del archivo con QTextStream::readAll() si no estamos preocupados por el consumo de memoria, o si sabemos que el archivo siempre ser pequeo. Por defecto, QTextStream usa la codificacin de caracteres del sistema local (por ejemplo, ISO 8859-1 o ISO 8859-15 en Amrica y muchas partes de Europa) para leer y escribir. Esto puede modificarse por medio de setCodec(), algo as: stream.setCodec("UTF-8"); La codificacin UTF-8 es muy popular y compatible con ASCII, puede representar el conjunto completo de caracteres Unicode. Para ms informacin sobre Unicode y el soporte de QTextStream para codificaciones ver el Capitulo 17 (Internacionalizacin). QTextStream tiene varias opciones de modelado aparte de las ofrecidas por <iostream>. Estas pueden establecerse pasando objetos especiales, denominados 'manipuladores de flujo', al flujo abierto para alterar su estado. El siguiente ejemplo muestra el uso de las opciones showbase, upperCasedigits, y hex, antes de guardar el valor entero 12345678, produciendo como resultado el texto "0xBC614E": salida << showbase << uppercasedigits << hex << 12345678; Las opciones tambin pueden usarse a travs de funciones miembro: salida.setNumberFlags(QTextStream::ShowBase| QTextStream::UppercaseDigits); salida.setIntegerBase(16); salida << 12345678; Figura 12.1. Funciones para configurar las opciones de QTextStream setIntegerBase(int) 0 2 8 10 16 setNumberFlags(NumberFlags) showBase ForceSign ForcePoint UppercaseBase Muestra un prefijo esi la base es 2 (0b), 8 (0) o 16 (0x) Siempre muestra el signo en nmeros reales Siempre coloca el separador decimal en nmeros Usa versiones en maysculas de base prefijada (0X, 0B) Auto-detectccion basada en prefijo (cuando lee) Binary Octal Decimal Hexadecimal

12. Entrada/Salida

129

UppercaseDigits

Usa letras maysculas en nmeros hexadecimales

setRealNumberNotation(RealNumberNotation) FixedNotation ScientificNotation SmartNotation setRealNumberPrecision(int) Establece el nmero mximo de dgitos que deben ser generados (6 por defecto) setFieldWidth(int) Establece el tamao mnimo de un campo (0 por defecto) setFieldAlignment(FieldAlignment) AlignLeft AlignRight AlignCenter AlignAccountingStyle setPadChar(QChar) Establece el caracter usado para los campos de relleno (espacio por defecto) Al igual que QDataStream, QTextStream opera sobre una subclase de QIODevice, pudiendo ser QFile, QTemporaryFile, QBuffer, QProcess, QTcpSocket, o QUdpSocket. Adems puede ser usada directamente sobre un objeto QString, por ejemplo: QString str; QTextStream(&str) << oct << 31 << " " << dec << 25 << endl; Esto hace que el contenido de la cadena sea "37 25/n", ya que el numero decimal 31 es expresado como 37 en octal. En este caso, no necesitamos establecer la codificacin del flujo, ya que QString siempre trabaja en Unicode. Veamos un ejemplo simple de un formato de archivo basado en texto. En la aplicacin Hoja de Calculo descrita en la Parte I, usamos un formato binario para almacenar los datos. Estos consistan en una secuencia de fila, columna, formula, uno por cada celda no vaca. Guardar estos datos como texto es sencillo; aqu vemos un extracto de una versin modificada del mtodo para guardar el archivo de la aplicacin Hoja de Calculo, en el mtodo writeFile(): QTextStream salida(&archivo); for (int fila = 0; fila < RowCount; ++fila) { for (int columna = 0; columna < ColumnCount; ++columna) { QString str = formula(fila, columna); if (!str.isEmpty()) salida << fila << " " << columna << " " << str << endl; } } Rellena el lado derecho del campo Rellena el lado izquierdo del campo Rellena ambos lados del campo Rellena entre el signo y el nmero Notacin de Punto-fijo (p. e., 0.000123) Notacin cientfica (p. e., 1.234568e-04) Notacin de punto-fijo o cientfica, la que sea ms compacta

12. Entrada/Salida

130

Hemos usado un formato simple, en donde cada linea representa una celda y con espacios entre los datos de fila, columna y frmula. La frmula puede contener espacios en blanco, pero podemos asumir que no contiene un carcter de fin de lnea (\n). Ahora veamos el cdigo correspondiente a la lectura de dichos datos: QTextStream entrada(&archivo); while (!entrada.atEnd()) { QString linea = entrada.readLine(); QStringList campos = linea.split( ); if (campos.size() >= 3) { int fila = campos.takeFirst().toInt(); int columna = campos.takeFirst().toInt(); setFormula(fila, columna, campos.join( )); } } Leemos los datos de una linea por vez. La funcin readLine() quita los caracteres de fin de linea. QString::split() divide una cadena en donde se encuentra el caracter separador y nos devuelve una lista de cadenas. Por ejemplo la linea 5 19 Total value resultar en la siguiente lista de cuatro elementos [5, 19, Total, value]. Si tenemos al menos tres campos, estamos listos para extraer los datos. La funcin QStringList::takeFirst() quita el primer elemento de la lista y lo devuelve. Usamos esta para extraer el nmero de fila y de columna. No hemos realizado ninguna comprobacin de errores; si leemos un valor no entero como nmero de fila o columna, QString::toInt() devolver cero. Cuando llamamos a setFormula(), debemos concatenar los campos restantes para obtener la cadena de la formula, al ponerlos en una sola cadena. En el segundo ejemplo de QTextStream, usaremos la tcnica que consiste en cargar caracter por caracter un archivo de texto, pero que lo guarde quitando los espacios sobrantes y reemplazando los tabuladores por espacios. Todo el trabajo del programa es realizado por la funcin ordenarArchivo(): void ordenarArchivo(QIODevice *dispEntrada, QIODevice *dispSalida) { QTextStream entrada(dispEntrada); QTextStream salida(dispSalida); const int TamTab = 8; int cantidadFinl = 0; int cantidadEspacios = 0; int columna = 0; QChar ch; while (!entrada.atEnd()) { entrada >> ch; if (ch == \n) { ++cantidadFinl; cantidadEspacios = 0; columna = 0; } else if (ch == \t) { int tam = TamTab - (columna % TamTab); cantidadEspacios += tam; columna += tam; } else if (ch == ) { ++cantidadEspacios; ++columna; } else { while (cantidadFinl > 0) { salida << endl; --cantidadFinl; columna = 0;

12. Entrada/Salida

131

} while (cantidadEspacios > 0) { salida << ; --cantidadEspacios; ++columna; } salida << ch; ++columna; } } salida << endl; } Creamos un QTextStream de entrada y uno de salida basados en los objetos QIODevice que recibe la funcin. Mantenemos tres elementos de estado: un contador de nuevas lineas, un contador de espacios y uno que marca la posicin de la columna actual en la linea, que nos sirve para convertir un tabulador en el nmero de espacios correctos. El anlisis se realiza mediante un ciclo while que recorre, uno por vez, cada carcter presente en el archivo de entrada. El cdigo es un poco sutil en algunos lugares. Por ejemplo, aunque establezcamos el tamao del tabulador (la variable TamTab) a 8, reemplazamos a estos con los espacios suficientes para rellenar hasta el siguiente tabulador, en vez de directamente reemplazar cada uno con ochos espacios. Si llegamos a una nueva linea, tabulador o espacio en blanco, simplemente actualizamos el estado del dato. Solo cuando obtenemos otro tipo de caracter producimos una salida, y antes de escribir los caracteres debemos escribir cualquier nueva linea o espacio pendiente (para respetar las lineas en blanco y preservar la indentacin) y actualizamos el estado. int main() { QFile archivoEntrada; QFile archivoSalida; archivoEntrada.open(stdin, QFile::ReadOnly); archivoSalida.open(stdout, QFile::WriteOnly); ordenarArchivo(&archivoEntrada, &archivoSalida); return 0; } Para este ejemplo, no necesitamos de un objeto QApplication, porque solo estamos usando clases independientes de la interfaz grfica. Consulte http://doc.trolltech.com/4.1/tools.html para obtener una lista completa de estas clases. Asumimos que leprograma es usado como filtro, por ejemplo: tidy < cool.cpp > cooler.cpp El programa debera ser fcil de extender para que sea capaz de manejar nombres de archivos dados desde la linea de comandos, y filtrar cin y cout por otra parte. Ya que es una aplicacin de consola, el archivo .pro presenta pequeas diferencias con el de una aplicacin GUI. TEMPLATE = app QT = core CONFIG += console CONFIG -= app_bundle SOURCES = tidy.cpp Solo vinculamos a QtCore, ya que no necesitamos usar ninguna funcionalidad del mdulo QtGui. Despus especificamos que queremos habilitar la salida por consola en Windows y que no queremos que la aplicacin viva en un paquete sobre Mac OS X. Para leer y escribir archivos planos ASCII o ISO 8859-1 (Latin-1), es posible usar la API de QIODevice directamente en vez de usar QTextStream. Rara vez es conveniente hacer esto, ya que la mayora de las

12. Entrada/Salida

132

aplicaciones necesitan soportar otros tipos de codificacin en algn que otro punto y solo QTextStream provee apoyo para esto. Si aun quiere escribir texto directamente a un QIODevice, debe especificar explcitamente la bandera QIODevice::Text al usar la funcin open(), por ejemplo: archivo.open(QIODevice::WriteOnly | QIODevice::Text); Cuando se guardan los datos, esta bandera le indica a QIODevice que convierta los caracteres '/n' en la secuencia '/r/n' si estamos en Windows. Cuando se leen los datos, esta bandera indica que se deben ignorar los caracteres '/r' en todas las plataformas. Entonces podemos asumir que el caracter de fin de linea es '/n', ms all de la convencin de fin de lnea usada por el sistema operativo.

Navegar por Directorios


La clase QDir provee un medio independiente de plataforma para navegar por los directorios y obtener informacin sobre los archivos. Para ver cmo se usa esta clase, escribiremos una pequea aplicacin de consola que calcula el espacio consumido por todas las imgenes que hay en un directorio particular y en todos sus subdirectorios. El corazn de la aplicacin es la funcin espacioImagen(), la cual se encarga de calcular recursivamente el tamao ocupado por todas las imgenes del directorio. qlonglong espacioImagen(const QString &ruta) { QDir dir(ruta); qlonglong tam = 0; QStringList filtros; foreach (QByteArray formato, QImageReader::supportedImageFormats()) filtros += "*." + formato; foreach (QString archivo, dir.entryList(filtros, QDir::Files)) tam += QFileInfo(dir, archivo).size(); foreach (QString subDir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) tam += espacioImagen(ruta + QDir::separator() + subDir); return tam; } Creamos un objeto QDir pasndole el directorio de trabajo, el cual puede ser relativo al directorio actual o absoluto. Le pasamos a la funcin entryList() dos parmetros. El primero es una lista con los nombres de archivos a procesar. Esta puede contener caracteres comodines como '*' o '?'. En este ejemplo, el filtro incluye solo formatos de archivos que la clase QImage pueda leer. El segundo argumento especifica qu tipo de entradas procesaremos (archivos normales, directorios, unidades, etc). Recorremos la lista de archivos, acumulando sus tamaos. La clase QFileInfo nos permite acceder a varios atributos de los archivos, tales como el tamao del mismo, permisos de seguridad, propietarios y marcas de tiempo. La segunda llamada a entryList() nos devuelve todos los subdirectorios del directorio. Los recorremos (excluyendo los caracteres: . y ..) y llamamos recursivamente a espacioImagen() para determinar el tamao de sus imgenes. Para crear cada ruta de subdirectorios, combinamos la ruta del directorio actual con el nombre del subdirectorio, separndolo con una barra. QDir trata al carcter el carcter '/' como el separador de directorios en todas las plataformas, adicionalmente al reconocido carcter \ que se usa en Windows. Cuando tenemos que mostrar las rutas al usuario, podemos usar la funcin esttica

12. Entrada/Salida

133

QDir::convertSeparators() para convertir el separador de directorios al utilizado en cada plataforma. Agreguemos un funcin main() a nuestro pequeo programa: int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); QString ruta = QDir::currentPath(); if (args.count() > 1) ruta = args[1]; cout << "El espacio ocupado por las imgenes en " <<qPrintable(ruta)<<" y en sus subdirectorios es " << (espacioImagen(ruta) / 1024)<< " KB" << endl; return 0; } Usamos QDir::currentPath() para inicializar la ruta de trabajo al directorio actual. Como una alternativa, podramos usar QDir::homePath() para utilizar el directorio del usuario como directorio de trabajo. Si el usuario a especificado un directorio en la linea de comando, usamos este. Finalmente, llamamos a la funcin espacioImagen() para que realice el clculo del tamao que ocupan las imgenes. La clase QDir provee otras funciones para trabajar con archivos y directorios, incluyendo a entryInfoList(), la cual devuelve una lista de objetos QFileInfo, rename(), exists(), mkdir() y rmdir(). Tambin incluye algunas funciones estticas como remove() o exists().

Incrustando Recursos
Hasta el momento, en este capitulo, hemos hablado sobre acceder datos en dispositivos externos, pero tambin es posible incluir datos binarios o de texto dentro del ejecutable de la aplicacin. Esto se hace por medio del sistema de recursos de Qt. En otros captulos, hemos usados archivos de recursos para incluir imgenes en el ejecutable, pero tambin es posible agregar cualquier tipo de archivo. Los archivos embebidos o incrustados, pueden ser ledos usando QFile como cualquier archivo normal del sistema. Los recursos son convertidos a cdigo C++ por rcc, el compilador de recursos de Qt. Le podemos indicar a qmake que incluya reglas especiales para ejecutar rcc agregando estas lineas al archivo .pro: RESOURCES = myresourcefile.qrc myresourcefile.qrc es un archivo XML que lista todos los archivos a incluir en el ejecutable. Imaginemos que tenemos que escribir una aplicacin que retenga informacin de contactos. Para comodidad de nuestros usuarios, queremos incluir los cdigos de marcado internacionales en el ejecutable. Si el archivo se encuentran en el subdirectorio datos, dentro del directorio donde es contruida la aplicacin, el archivo de recursos podra verse algo as: <!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>datos/cod-telefono.dat</file> </qresource> </RCC> Desde la aplicacin, los recursos son identificados por el prefijo ':/'. En este ejemplo, los cdigos de marcado tienen la ruta :/datos/cod-telefono.dat y puede ser ledo como cualquier otro archivo usando QFile.

12. Entrada/Salida

134

Incluir datos en el ejecutable tiene la ventaja de que no se pueden perder y nos dan la posibilidad de crear ejecutables independientes (si es compilado estticamente). Esta tcnica tiene dos desventajas: si necesitamos hacer cambios en algn dato embebido, debemos cambiar tambin el ejecutable, y el tamao del ejecutable ser ms grande porque debe alojar los datos incrustados. El sistema de recursos de Qt provee ms caractersticas que las presentadas en este ejemplo, incluye soporte para alias de archivos y para localizacin. Todo esto est documentado en http://doc.trolltech.com/4.1/resources.html.

Comunicacin entre Procesos


La clase QProcess nos permite ejecutar programas externos e interactuar con ellos. Trabaja asncronamente, realizando el trabajo en segundo plano para no bloquear la interfaz de usuario. Emite seales para notificarnois cuando el proceso externo tenga datos o haya finalizado. Revisaremos el cdigo de una pequea aplicacin que permite convertir un imagen a travs de otro programa. Para este ejemplo, contamos con el programa ImageMagick, el cual est disponible libremente en todas las plataformas. Figura 12.2. La aplicacin Convertidor de Imagen

La interfaz de usuario fue creada con Qt Designer. Nos centraremos en la clase que hereda de Ui::DialogoConvertir (que fue creada por uic), comenzando por el archivo cabecera: #ifndef DIALOGOCONVERTIR_H #define DIALOGOCONVERTIR_H #include <QDialog> #include <QProcess> #include "ui_dialogoconvertir.h" class DialogoConvertir : public QDialog, public Ui::DialogoConvertir { Q_OBJECT public: DialogoConvertir(QWidget *parent = 0); private slots: void on_botonBuscar_clicked(); void on_botonConvertir_clicked(); void actualizarTextoSalida(); void procesoTerminado(int codigoSalida, QProcess::ExitStatus estadoSalida);

12. Entrada/Salida

135

void procesoError(QProcess::ProcessError error); private: QProcess proceso; QString archivoDestino; }; #endif Esta sigue el patrn de todas las clases generadas desde un formulario de Qt Designer. Gracias al mecanismo de conexin automtica, los slots on_botonBuscar_clicked() y on_botonConvertir_clicked() ya estn conectados a las seales clicked() de los botones Buscar y Convertir respectivamente. DialogoConvertir::DialogoConvertir(QWidget *parent) : QDialog(parent) { setupUi(this); connect(&proceso, SIGNAL(readyReadStandardError()), this, SLOT(actualizarTextoSalida())); connect(&proceso, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(procesoTerminado(int, QProcess::ExitStatus))); connect(&proceso, SIGNAL(error(QProcess::ProcessError)), this, SLOT(procesoError(QProcess::ProcessError))); } La llamada a setupUi() crea y acomoda los widgets en el formulario, establece la conexin entre seales y slots necesarias. Despus de esto, conectamos manualmente tres seales del objeto QProcess a tres slots privados. Siempre que el proceso externo publique datos o errores (a travs de cerr), lo manejaremos en el slot actualizarTextoSalida(). void DialogoConvertir::on_botonBuscar_clicked() { QString nombreInicial = sourceFileEdit->text(); if (nombreInicial.isEmpty()) nombreInicial = QDir::homePath(); QString nombreArchivo = QFileDialog::getOpenFileName(this, tr("Elija el archivo"), nombreInicial); nombreArchivo = QDir::convertSeparators(nombreArchivo); if (!nombreArchivo.isEmpty()) { sourceFileEdit->setText(nombreArchivo); botonConvertir->setEnabled(true); } } La seal clicked() del botn Buscar es conectada automticamente al slot on_botonBuscar_clicked() en la funcin setupUi(). Si el usuario previamente a seleccionado un archivo, inicializamos el dialogo de seleccin de archivo con ese nombre; de otra manera, usamos el directorio del usuario. void DialogoConvertir::on_botonConvertir_clicked() { QString archivoOrigen = sourceFileEdit->text(); archivoDestino = QFileInfo(archivoOrigen).path() + QDir::separator() + QFileInfo(archivoOrigen).baseName() + "." + targetFormatComboBox->currentText().toLower(); botonConvertir->setEnabled(false); outputTextEdit->clear(); QStringList args; if (enhanceCheckBox->isChecked()) args << "-acentuar";

12. Entrada/Salida

136

if (monocromoCheckBox->isChecked()) args << "-monocromo"; args << archivoOrigen << archivoDestino; proceso.start("convertir", args); } Cuando el usuario presiona el botn Convertir, copiamos el nombre del archivo y cambiamos la extensin para que coincida con el formato de destino. Usamos el separador de directorio especifico de la plataforma (por medio de QDir::separator()) en vez de establecerlo a mano porque el nombre de archivo ser visible al usuario. Despus desactivamos el botn Convertir para prevenir el lanzamiento accidental de varias conversiones, y limpiamos el editor de texto que usamos para mostrar informacin de estado. Para iniciar el proceso externo, llamamos a QProcess::start() con el nombre del programa a ejecutar ms cualquier argumento que este requiera. En este caso le pasamos las banderas acentuar y monocromo si el usuario marca las opciones apropiadas, seguido del nombre del archivo de origen y del archivo de destino. El programa de conversin infiere el tipo de conversin requerida por la extensin de los archivos. void DialogoConvertir::actualizarTextoSalida() { QByteArray newData = proceso.readAllStandardError(); QString texto = outputTextEdit->toPlainText() + QString::fromLocal8Bit(newData); outputTextEdit->setPlainText(text); } Siempre que el programa externo mande algn dato a cerr, el slot actualizarTextoSalida() es llamado. Leemos el texto de error publicado y lo mostramos. void DialogoConvertir::procesoTerminado(int codigoSalida, QProcess::ExitStatus estadoSalida) { if (estadoSalida == QProcess::CrashExit) { outputTextEdit->append(tr("El programa de conversin ha finalizado inesperadamente")); } else if (codigoSalida != 0) { outputTextEdit->append(tr("No fue posible realizar la conversin")); } else { outputTextEdit->append(tr("El archivo %1 fue creado exitosamente").arg(archivoDestino)); } botonConvertir->setEnabled(true); } Cuando el proceso finaliza, le hacemos conocer el resultado al usuario y activamos el botn Convertir. void DialogoConvertir::procesoError(QProcess::ProcessError error) { if (error == QProcess::FailedToStart) { outputTextEdit->append(tr("El programa de conversin no fue encontrado")); botonConvertir->setEnabled(true); } } Si el proceso no se puede iniciar, QProcess emite la seal error() en vez de finished(). Reportamos cualquier error y habilitamos el botn Convertir.

12. Entrada/Salida

137

En este ejemplo, hemos realizado la conversin asncronamente, o sea que le decimos a QProcess que ejecute el programa de conversin y devuelva el control a la aplicacin inmediatamente. Esto no bloquea la interfaz mientras el programa se ejecuta (en segundo plano). Pero en algunas situaciones necesitamos que el proceso externo se complete antes de poder hacer cualquier otra cosa en nuestra aplicacin, en esos casos necesitamos que QProcess opere de manera sncrona. Un ejemplo comn en donde es deseable para la aplicacin que el proceso externo se ejecute de manera sncrona es la edicin de texto con el editor preferido por el usuario. Esto es fcil de implementar usando QProcess. Por ejemplo, asumamos que tenemos un texto plano en un QTextEdit, y proveemos un botn Editar conectado a un slot editar(). void ExternalEditor::editar() { QTemporaryFile archivoSalida; if (!archivoSalida.open()) return; QString nombreArchivo = archivoSalida.nombreArchivo(); QTextStream salida(&archivoSalida); salida << textEdit->toPlainText(); archivoSalida.close(); QProcess::execute(editor, QStringList() << options << nombreArchivo); QFile archivoEntrada(nombreArchivo); if (!archivoEntrada.open(QIODevice::ReadOnly)) return; QTextStream entrada(&archivoEntrada); textEdit->setPlainText(entrada.readAll()); } Usamos QTemporaryFile para crear un archivo temporal vaco. No especificamos ningn argumento a QTemporaryFile::open() ya que por defecto abre un archivo en modo lectura/escritura. Escribimos el contenido del QTextEdit en el archivo temporal y luego cerramos el archivo porque algunos editores no pueden trabajar con archivos abiertos. La funcin esttica QProcess::execute() ejecuta un proceso externo y bloquea la aplicacin hasta que este finaliza. El argumento editor es un QString que tiene el nombre del ejecutable (por ejemplo, gvim). El segundo argumento es un QStringList que nos sirve para pasarle argumentos opcionales al programa (por ejemplo el nombre del archivo que tiene que abrir). Despus que el usuario cierra el editor, el proceso finaliza y libera a la aplicacin. Ahora podemos abrir el archivo temporal y cargar su contenido dentro del QTextEdit. QTemporaryFile elimina automticamente el archivo creado cuando el objeto sale del mbito. Cuando usamos QProcess sncronamente, no necesitamos realizar conexiones entre seales y slots. Si necesitamos un control ms fino que el que provee la funcin execute() podemos usar una tcnica alternativa. Esta consta de crear un objeto QProcess y llamar a start(), y luego forzar su bloqueo llamando primero a QProcess::waitForStarted(), y si se inicia correctamente llamar a QProcess::waitForFinished(). Vea la documentacin de referencia de QProcess para obtener un ejemplo que ilustra el uso de esta tcnica. En esta seccin, hemos usado QProcess para obtener acceso a funcionalidades pre-existentes. Utilizar aplicaciones que ya existen puede ahorrar tiempo de desarrollo y puede librarnos de detalles que no son de inters para el propsito de nuestra aplicacin. Otra manera de acceder a funcionalidades pre-existentes es vincular nuestro programa con una librera que la provea. Pero donde no es factible que exista una librera, envolver una aplicacin de consola con QProcess puede funcionar muy bien.

12. Entrada/Salida

138

Otro uso que le podemos dar a esta clase es lanzar otras aplicaciones GUI, tales como un navegador web o un cliente de correo. Sin embargo, si nuestro objetivo es mantener la comunicacin entre varias aplicaciones en vez de ejecutarlas, podra ser mejor comunicarse directamente con ellas, usando las clases de trabajo en red de Qt o la extensin ActiveQt si estamos trabajando en Windows.

13. Bases de Datos

139

13. Bases de Datos

Conectando y Consultando Presentando Datos en Forma Tabular Implementando Formularios Master-Detail

El modulo QtSql proporciona una interfaz independiente de la plataforma y de la base de datos utilizada para acceder a bases de datos SQL. Esta interfaz es soportada por un conjunto de clases que usan la arquitectura modelo/vista de Qt para proporcionar una integracin con bases de datos a la interfaz de usuario. Este captulo est vinculado con las clases de modelo/vista de Qt cubierto en el Captulo 10. Una conexin de base de datos es representada a travs de un objeto QSqlDatabase. Qt usa controladores para comunicarse con las distintas APIs de bases de datos. La Edicin de Escritorio de Qt incluye los siguientes controladores:

Debido a las restricciones que imponen las licencias, no todos los controladores son proporcionados por la edicin Open Source de Qt. Cuando configuramos Qt, podemos elegir entre incluir los controladores SQL dentro del mismo Qt y construirlos como plugins. Qt est equipado con la base de datos SQLite, una base de datos dentro-de-proceso de dominio pblico. Para los usuarios que se sienten cmodos con la sintaxis de SQL, la clase QSqlQuery proporciona un medio para ejecutar directamente sentencias SQL arbitrarias y manejar sus resultados. Para aquellos usuarios quienes prefieren una interfaz de base de datos de ms alto nivel, que se salte la sintaxis de SQL, QSqlTableModel y QSqlRelationalTableModel proporcionan abstracciones adecuadas. Estas clases representan una tabla SQL de la misma manera en que lo hacen las clases de otros modelos de Qt (cubiertas en el Capitulo 10). Estas pueden ser usadas como stand-alone para recorrer y editar datos en el cdigo, o pueden ser adjuntadas a vistas a travs de las cuales los usuarios finales pueden ver y editar los datos ellos mismos. Qt tambin hace fcil la programacin en los mosdismos de bases de datos ms comunes, como el masterdetail y el dril-down, como lo demostrarn algunos ejemplos en este captulo.

13. Bases de Datos

140

Conectando y Consultando
Para ejecutar una consulta SQL, primero debemos establecer una conexin con la base de datos. Como es tpico, las conexiones a las bases de datos son establecidas en una funcin separada que llamaremos al inicio de la aplicacin. Por ejemplo: bool crearConexion() { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("mozart.konkordia.edu"); db.setDatabaseName("musicdb"); db.setUserName("gbatstone"); db.setPassword("T17aV44"); if (!db.open()) { QMessageBox::critical(0, QObject::tr("Error de Base de Datos"), db.lastError().text()); return false; } return true; } Primero llamamos a QSqlDatabase::addDatabase() para crear un objeto QSqlDatabase. El primer argumento a addDatabase() especifica cul controlador de base de datos debe usar Qt para acceder a la base de datos. En este caso, usamos MySQL. Lo siguiente que se hace es establecer el nombre de host de la base de datos, el nombre de la base de datos, el nombre de usuario y la contrasea, para luego abrir la conexin. Si open() falla, mostraremos un mensaje de error. Tpicamente, llamaramos a crearConexion() en la funcin main(): int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!crearConexion()) return 1; return app.exec(); } Una vez que una conexin es establecida, podemos usar QSqlQuery para ejecutar cualquier consulta SQL que la base de datos usada soporte. Por ejemplo, aqu se muestra como ejecutar una sentencia SELECT: QSqlQuery query; query.exec("SELECT titulo, ao FROM cd WHERE ao >= 1998"); Despus del llamado a exec(), podemos navegar a travs del conjunto de resultados de la consulta: while (query.next()) { QString titulo = query.value(0).toString(); int ao = query.value(1).toInt(); cerr << qPrintable(titulo) << ": " << ao << endl; } Llamamos a next() una vez para posicionar el QSqlQuery en el primer registro del conjunto de resultados. Llamadas subsecuentes a next() avanzan el puntero un registro a la vez, hasta que el final sea alcanzado, en donde next() retorna false. Si el conjunto de resultados est vaco (o si la consulta falla), el primer llamado a next() retornar false.

13. Bases de Datos

141

La funcin value() retorna un valor de un campo como una QVariant. Los campos son enumerados desde 0 en el orden dado en la sentencia SELECT. La clase QVariant puede contener muchos tipos de valores C++ y Qt, incluyendo int y QString. Los diferentes tipos de datos que puede ser guardados en la base de datos son asignados o convertidos a los correspondientes tipos de datos C++ y Qt, y pueden ser guardados en QVariants. Por ejemplo, VARCHAR es representado como un QString y un DATETIME como un QDateTime. QSqlQuery provee algunas funciones para navegar a travs del conjunto de resultados: first(), last(), previous() y seek(). Estas funciones son convenientes, pero para algunas bases de datos pueden llegar a ser lentas y ms consumidoras de memoria que el mtodo next(). Para una fcil optimizacin cuando se trabaja con un nmero muy grande de resultados, podemos llamar a QSqlQuery::setForwardOnly(true) antes de llamar a next(), y luego solo usar next() para moverse por el conjunto de resultados. Anteriormente especificamos la consulta SQL como un argumento a QSqlQuery::exec(), pero tambin podemos pasrselo directamente al constructor, el cual lo ejecuta inmediatamente: QSqlQuery query("SELECT titulo, ao FROM cd WHERE ao >= 1998"); Podemos verificar si ocurri algn error llamando a isActive() sobre la consulta o query: if (!query.isActive()) QMessageBox::warning(this, tr("Error de Base de Datos"), query.lastError().text()); Si no ocurre ningn error, el query se volver activo y podremos usar next() para navegar a travs de los resultados. Hacer un INSERT es casi tan fcil como realizar un SELECT: QSqlQuery query("INSERT INTO cd (id, artistaid, titulo, ao) " "VALUES (203, 102, Living in America, 2002)"); Despus de esto, numRowsAffected() retorna el nmero de filas que fueron afectadas por la sentencia SQL (o -1 si ocurre un error). Si necesitamos insertar una gran cantidad de registros, o si queremos evadir la conversin de valores a strings (y escapar de ellos correctamente), podemos usar prepare() para armar una consulta que contenga sostenedores de espacio y luego sustituir o ubicar los valores que queramos insertar en esos contenedores. Qt soporta la sintaxis de sostenedores de espacio al estilo Oracle y al estilo ODBC para todas las bases de datos, usando soporte nativo donde ste est disponible y, simulndolo si no lo est. Aqu est un ejemplo que usa la sintaxis al estilo Oracle con sostenedores de espacio nombrados (con nombres): QSqlQuery query; query.prepare("INSERT INTO cd (id, artistaid, titulo, ao) " "VALUES (:id, :artistaid, :titulo, :ao)"); query.bindValue(":id", 203); query.bindValue(":artistaid", 102); query.bindValue(":titulo", "Living in America"); query.bindValue(":ao", 2002); query.exec(); Aqu est el mismo ejemplo usando los sostenedores de espacio posicionales, al estilo ODBC: QSqlQuery query; query.prepare("INSERT INTO cd (id, artistaid, titulo, ao) " "VALUES (?, ?, ?, ?)"); query.addBindValue(203); query.addBindValue(102); query.addBindValue("Living in America"); query.addBindValue(2002); query.exec();

13. Bases de Datos

142

Despus del llamado a exec(), podemos llamar a bindValue() o a addBindValue() para sustituir nuevos valores, luego llamar a exec() de nuevo para ejecutar el query con los nuevos valores. Los sostenedores de espacio son usados muy a menudo para especificar datos binarios o cadenas que contengan caracteres que no sean ASCII o que no sean Latin-1. Tras bastidores, Qt usa el formato Unicode con aquellas bases de datos que soporten lo soporten, y para aquellas donde no, Qt convierte las cadenas a la codificacin apropiada transparentemente. Qt soporta transacciones SQL sobre las bases de datos donde stas estn disponibles. Para iniciar una transaccin, llamamos al mtodo transaction() sobre el objeto que represente la conexin a la base de datos. Para finalizar la transaccin, llamamos a commit() o a rollback(). Por ejemplo, aqu se muestra cmo haramos para buscar una clave fornea y ejecutar una sentencia INSERT dentro de una transaccin: QSqlDatabase::database().transaction(); QSqlQuery query; query.exec("SELECT id FROM artista WHERE nombre = Gluecifer"); if (query.next()) { int artistaId = query.value(0).toInt(); query.exec("INSERT INTO cd (id, artistaid, titulo, ao) " "VALUES (201, " + QString::number(artistaId) + ", Riding the Tiger, 1997)"); } QSqlDatabase::database().commit(); La funcin QSqlDatabase::database() retorna un objeto QSqlDatabase representando la conexin que hemos creado en crearConexion(). Si la transaccin no pudo iniciarse, QSqlDatabase::transaction() retorna false. Algunas bases de datos no soportan las transacciones. Para ellas, las funciones transaction(), commit() y rollback() no hacen nada. Podemos probar si una base de datos soporta transacciones usando hasFeature() sobre el QSqlDriver asociado con la base de datos: QSqlDriver *driver = QSqlDatabase::database().driver(); if (driver->hasFeature(QSqlDriver::Transactions)) Las caractersticas de muchas otras bases de datos pueden ser probadas, incluyendo si la base de datos soporta BLOBs (Binary Large Objects), Unicode y consultas preparadas. En los ejemplos que hemos visto hasta ahora hemos asumido que la aplicacin est usado una sola conexin a la base de datos. Si queremos crear mltiples conexiones, podemos pasar un nombre como segundo argumento a addDatabase(). Por ejemplo: QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL", "OTHER"); db.setHostName("saturn.mcmanamy.edu"); db.setDatabaseName("starsdb"); db.setUserName("hilbert"); db.setPassword("ixtapa7"); Luego podemos recuperar un puntero QSqlDatabase::database(): al objeto QSqlDatabase pasndole el nombre a

QSqlDatabase db = QSqlDatabase::database("OTRA"); Para ejecutar consultas usando la otra conexin, pasamos el objeto QSqlDatabase al constructor de QSqlQuery: QSqlQuery query(db); query.exec("SELECT id FROM artista WHERE nombre = Mando Diao");

13. Bases de Datos

143

Mltiples conexiones son tiles si queremos realizar ms de una transaccin a la vez, ya que cada conexin solamente puede manejar una sola transaccin activa. Cuando usamos mltiples conexiones a bases de datos, todava podemos tener una conexin sin nombre, y QSqlQuery usar esa conexin si ninguna conexin es especificada. Adicionalmente a QSqlQuery, Qt proporciona la clase QSqlTableModel como una interfaz de ms alto nivel, permitindonos evitar el uso de SQL crudo para realizar la operaciones SQL ms comunes ( SELECT, INSERT, UPDATE y DELETE). La clase puede ser usada de manera stand-alone (autnoma o independiente) para manipular una base de datos sin participacin de ninguna IGU (Interfaz Grfica de Usuario o GUI en ingls), o puede ser usado como datos fuentes para QListView o QTableView. Aqu est un ejemplo que usa QSqlTableModel para realizar un SELECT: QSqlTableModel modelo; modelo.setTable("cd"); modelo.setFilter("ao >= 1998"); modelo.select(); este es el equivalente a la consulta SELECT * FROM cd WHERE ao >= 1998 Navegar a travs del conjunto de resultados se hace recuperando un registro dado usando el mtodo QSqlTableModel::record() y accediendo a los campos individuales usando el mtodo value(): for (int i = 0; i < modelo.rowCount(); ++i) { QSqlRecord registro = modelo.record(i); QString titulo = registro.value("titulo").toString(); int ao = regitro.value("ao").toInt(); cerr << qPrintable(titulo) << ": " << ao << endl; } La funcin QSqlRecord::value() toma tambin un nombre de un campo o un ndice de campo. Cuando se trabaja con grandes conjuntos de datos, es recomendable que los campos se especifiquen por sus ndices. Por ejemplo: int indiceTitulo = modelo.record().indexOf("titulo"); int indiceAo = modelo.record().indexOf("ao"); for (int i = 0; i < modelo.rowCount(); ++i) { QSqlRecord registro = modelo.record(i); QString titulo = registro.value(indiceTitulo).toString(); int ao = registro.value(indiceAo).toInt(); cerr << qPrintable(titulo) << ": " << ao << endl; } Para insertar un registro en una tabla de la base de datos, usamos el mismo mtodo que usaramos si insertramos en cualquier modelo bidimensional: Primero, llamamos a insertRow() para crear una nueva fila vaca (registro), y luego usamos setData() para establecer los valores de cada columna (campo). QSqlTableModel modelo; modelo.setTable("cd"); int fila = 0; modelo.insertRows(fila, 1); modelo.setData(modelo.index(fila, modelo.setData(modelo.index(fila, modelo.setData(modelo.index(fila, modelo.setData(modelo.index(fila, modelo.submitAll();

0), 1), 2), 3),

113); "Shanghai My Heart"); 224); 2003);

13. Bases de Datos

144

Despus de llamar a submitAll(), el registro puede ser movido a una posicin de fila diferente, dependiendo de cmo est ordenada la tabla. El llamado a submitAll() retornar false si la insercin falla. Una diferencia importante entre un modelo SQL y un modelo estndar es que para un modelo SQL debemos llamar a submitAll() para escribir cualquier cambio en la base de datos. Para actualizar un registro, primero debemos posicionar el QSqlTableModel en el registro que queremos modificar (por ejemplo, usando select()). Luego extraemos el registro, actualizamos los campos que queremos cambiar, y escribimos nuestros cambios nuevamente a la base de datos: QSqlTableModel modelo; modelo.setTable("cd"); modelo.setFilter("id = 125"); modelo.select(); if (modelo.rowCount() == 1) { QSqlRecord registro = modelo.record(0); registro.setValue("titulo", "Melody A.M."); registro.setValue("ao", registro.value("ao").toInt() + 1); modelo.setRecord(0, registro); modelo.submitAll(); } Si hay un registro que coincida con el filtro especificado, lo recuperamos usando QSqlTableModel::record(). Aplicamos nuestros cambios y sobre escribimos el registro original con nuestro registro modificado. Tambin es posible realizar una actualizacin usando setData(), como si lo haramos para un modelo que no sea SQL. El modelo indexa lo que recuperamos para una fila y columna dada: modelo.select(); if (modelo.rowCount() == 1) { modelo.setData(modelo.index(0, 1), "Melody A.M."); modelo.setData(modelo.index(0, 3), modelo.data(modelo.index(0, 3)).toInt() + 1); modelo.submitAll(); } Para borrar un registro, el proceso es muy similar al de actualizar: modelo.setTable("cd"); modelo.setFilter("id = 125"); modelo.select(); if (modelo.rowCount() == 1) { modelo.removeRows(0, 1); modelo.submitAll(); } El llamado a removeRows() toma el nmero de la fila del primer registro a eliminar y el nmero de registros a eliminar. El siguiente ejemplo elimina todos los registros que coinciden con el filtro: modelo.setTable("cd"); modelo.setFilter("ao < 1990"); modelo.select(); if (modelo.rowCount() > 0) { modelo.removeRows(0, modelo.rowCount()); modelo.submitAll();

13. Bases de Datos

145

} Las clases QSqlQuery y QSqlTableModel proporcionan una interfaz entre Qt y una base de datos SQL. Usando estas clases, podemos crear formularios que presenten datos al usuario y que le permitan insertar, actualizar y eliminar registros.

Presentando Datos en Forma Tabular


En muchos casos, es muy simple presentar a los usuarios una vista tabular de un conjunto de datos. En esta seccin y en la siguiente, presentamos una sencilla aplicacin llamada CD Collection que usa QSqlTableModel y su subclase QSqlRelationalTableModel para permitirle a los usuarios ver e interactuar con los datos guardados en la base de datos. El formulario principal muestra una vista master-detail de CDs y de pistas que se encuentran en el CD seleccionado, como se muestra en la Figura 13.1. La aplicacin usa tres tablas, mostradas en la Figura 13.2 y definidas como se muestra a continuacin: CREATE TABLE artista ( id INTEGER PRIMARY KEY, nombre VARCHAR(40) NOT NULL, pais VARCHAR(40)); CREATE TABLE cd ( id INTEGER PRIMARY KEY, titulo VARCHAR(40) NOT NULL, artistaid INTEGER NOT NULL, ao INTEGER NOT NULL, FOREIGN KEY (artistaid) REFERENCES artista); CREATE TABLE pista ( id INTEGER PRIMARY KEY, titulo VARCHAR(40) NOT NULL, duracion INTEGER NOT NULL, cdid INTEGER NOT NULL, FOREIGN KEY (cdid) REFERENCES cd); Algunas bases de datos no soportan claves forneas. Para esos casos, debemos remover las clausulas FOREIGN KEY. El ejemplo seguir funcionando, pero la base de datos no forzar la integridad referencial. En esta seccin, escribiremos un dialogo que permita al usuario editar una lista de artistas usando un formulario tabular simple, como puede verse en la Figura 13.3. El usuario puede insertar o eliminar artistas usando los botones del formulario. Las actualizaciones pueden ser aplicadas directamente, simplemente editando las celdas de texto. Los cambios son aplicados a la base de datos cuando el usuario presione Enter o se mueva a otro registro. Aqu est la definicin de la clase para el dialogo ArtistForm: class ArtistForm : public QDialog { Q_OBJECT public: ArtistForm(const QString &nombre, QWidget *parent = 0); private slots: void agregarArtista(); void eliminarArtista(); void antesDeIsertarArtista(QSqlRecord &registro); private:

13. Bases de Datos

146

enum { Id_Artista = 0, Nombre_Artista = 1, Pais_Artista = 2 }; QSqlTableModel *modelo; QTableView *tableView; QPushButton *botonAgregar; QPushButton *botonEliminar; QPushButton *botonCerrar; }; Figura 13.1. La aplicacin CD Collection

Figura 13.2. Tablas de la aplicacin CD Collection

Figura 13.3. El dialogo ArtistForm

13. Bases de Datos

147

El constructor es muy similar a uno que se usara para crear un formulario basado en un modelo que no sea SQL: ArtistForm::ArtistForm(const QString &nombre, QWidget *parent) : QDialog(parent) { modelo = new QSqlTableModel(this); modelo->setTable("artista"); modelo->setSort(Nombre_Artista, Qt::AscendingOrder); modelo->setHeaderData(Nombre_Artista, Qt::Horizontal, tr("Nombre")); modelo->setHeaderData(Pais_Artista, Qt::Horizontal, tr("Pais")); modelo->select(); connect(modelo, SIGNAL(beforeInsert(QSqlRecord &)), this, SLOT(AntesDeInsertarArtista (QSqlRecord &))); tableView = new QTableView; tableView->setModel(modelo); tableView->setColumnHidden(Id_Artista, true); tableView->setSelectionBehavior (QAbstractItemView::SelectRows); tableView->resizeColumnsToContents(); for (int fila = 0; fila < modelo->rowCount(); ++fila) { QSqlRecord registro = modelo->record(fila); if (registro.value(Nombre_Artista).toString() == nombre) { tableView->selectRow(fila); break; } } } Comenzamos el constructor con la creacin de un QSqlTableModel. Pasamos el puntero this como padre para darle propiedad del formulario. Hemos elegido ordenar por la columna 1 (especificado por la constante Nombre_Artista), la cual corresponde al campo nombre. Si no especificamos las cabeceras de las columnas, los nombres de los campos sern usados. Preferimos darle nombres nosotros mismos para asegurarnos que stas sean capitalizadas apropiadamente e internacionalizadas. Ahora, creamos un QSqlTableModel para visualizar el modelo. Ocultamos el campo id y establecemos el ancho de la columna para acomodarse a su contenido sin la necesidad de mostrar puntos suspensivos. El constructor de ArtistForm toma el nombre del artista que debe ser seleccionado cuando el dialogo aparezca. Iteramos sobre los registros de la tabla artista y seleccionamos al artista especificado. El resto del cdigo del constructor es usado para crear y conectar los botones y para ubicar los widgets hijos. void ArtistForm::agregarArtista() { int fila = modelo->rowCount(); modelo->insertRow(fila); QModelIndex indice = modelo->index(fila, Nombre_Artista); tableView->setCurrentIndex(indice); tableView->edit(indice); } Para agregar un nuevo artista, insertamos una sola fila vaca al fondo del QTableView. Ahora el usuario puede ingresar un nuevo nombre de artista y un nuevo nombre de pas. Si el usuario confirma la insercin

13. Bases de Datos

148

presionando Enter, la seal beforeInsert() es emitida y luego el nuevo registro es insertado en la base de datos. void ArtistForm::antesDeInsertarArtista(QSqlRecord &registro) { registro.setValue("id", generarId("artista")); } En el constructor, conectamos la seal beforeInsert() del modelo a este slot mostrado arriba. Hemos pasado una referencia no constante al registro justo antes de que sea insertado en la base de datos. En este punto, llenamos su campo id. Ya que necesitaremos al mtodo generarId() algunas que otras veces, los definimos inline en un archivo de cabecera y lo incluimos cada vez que lo necesitemos. Aqu est una manera muy rpida (e ineficiente) de implementarlo: inline int generarId(const QString &tabla) { QSqlQuery query; query.exec("SELECT MAX(id) FROM " + tabla); int id = 0; if (query.next()) id = query.value(0).toInt() + 1; return id; } La funcin generarId() solamente puede garantizar su buen funcionamiento si es ejecutada dentro del contexto de la misma transaccin como la sentencia INSERT correspondiente. Algunas bases de datos soportan los campos auto generados, y usualmente es mucho mejor, por lejos, usar el soporte especifico de la base de datos para esta operacin. La ltima posibilidad que ofrece el dialogo ArtistForm() es la de eliminar. En lugar de realizar eliminaciones en cascada (a ser cubierto pronto), hemos elegido permitir solamente las eliminaciones de artistas que no tengan CDs en la coleccin. void ArtistForm::eliminarArtista() { tableView->setFocus(); QModelIndex indice = tableView->currentIndex(); if (!indice.isValid()) return; QSqlRecord registro = modelo->record(indice.row()); QSqlTableModel cdModel; cdModel.setTable("cd"); cdModel.setFilter("artistaid = " + record.value("id").toString()); cdModel.select(); if (cdModel.rowCount() == 0) { modelo->removeRow(tableView->currentIndex().row()); } else { QMessageBox::information(this,tr("Eliminar Artista"), tr("No se puede eliminar %1 porque hay Cds asociados " "con este artista en la coleccin.") .arg(registro.value("nombre").toString())); } } Si hay un registro seleccionado, verificamos para ver si el artista posee algn CD, y si no lo tiene, lo eliminamos inmediatamente. De otra forma, mostramos un mensaje explicando por qu la eliminacin no fue realizada. Estrictamente hablando, debimos haber usado una transaccin, porque como se encuentra el cdigo, es posible para un CD tener su artista establecido al que estamos eliminando en-entre los llamados a

13. Bases de Datos

149

cdModel.select() y modelo->removeRow(). Mostraremos una transaccin en la siguiente seccin para cubrir este caso.

Implementando Formularios Master-Detail


Ahora vamos a revisar el formulario principal, el cual toma un papel de master-detail. La vista maestra o master view en ingles, es una lista de CDs. La vista de detalle o detail view en ingles, es una lista de pistas para el CD actual. Este formulario es la ventana principal de la aplicacin CD Collection que se muestra en la Figura 13.1. class MainForm : public QWidget { Q_OBJECT public: MainForm(); private slots: void agregarCd(); void eliminarCd(); void agregarPista(); void eliminarPista(); void editarArtista(); void cdActualCambiado(const QModelIndex &indice); void antesDeInsertarCd(QSqlRecord &registro); void antesDeInsertarPista(QSqlRecord &registro); void refrescarCabeceraVistaPista (); private: enum { Cd_Id = 0, Cd_Titulo = 1, Cd_ArtistaId = 2, Cd_Ao = 3 }; enum { Pista_Id = 0, Pista_Titulo = 1, Pista_Duracion = 2, Pista_CdId = 3 }; QSqlRelationalTableModel *cdModel; QSqlTableModel *pistaModel; QTableView *cdTableView; QTableView *trackTableView; QPushButton *botonAgregarCd; QPushButton *botonEliminarCd; QPushButton *botonAgregarPista; QPushButton *botonEliminarPista; QPushButton *botonEditarArtista; QPushButton *botonQuitar; }; Usamos un QSqlRelationalTableModel para la tabla cd en lugar de usar un QSqlTableModel plano porque necesitamos manejar claves forneas. Ahora revisaremos cada funcin en turno, comenzando con el constructor, el cual veremos por secciones porque es algo largo. MainForm::MainForm() { cdModel = new QSqlRelationalTableModel(this);

13. Bases de Datos

150

cdModel->setTable("cd"); cdModel->setRelation(Cd_ArtistaId, QSqlRelation("artista", "id", "nombre")); cdModel->setSort(Cd_Titulo, Qt::AscendingOrder); cdModel->setHeaderData(Cd_Titulo, Qt::Horizontal, tr("Titulo")); cdModel->setHeaderData(Cd_ArtistaId, Qt::Horizontal, tr("Artista")); cdModel->setHeaderData(Cd_Ao, Qt::Horizontal, tr("Ao")); cdModel->select(); El constructor comienza configurando el QSqlRelationalTableModel que maneja la tabla cd. El llamado a setRelation() le dice al modelo que su campo artistaid (cuyo campo ndice esta dado por Cd_ArtistaId) contiene la clave fornea id de la tabla artista, y eso debe mostrar el contenido de los campos nombre correspondientes en lugar de los IDs. Si el usuario elige editar este campo (por ejemplo, presionando F2), el modelo automticamente presentar un combobox con los nombres de todos los artistas, y si el usuario elige un artista diferente, se actualizar la tabla cd. cdTableView = new QTableView; cdTableView->setModel(cdModel); cdTableView->setItemDelegate(new QSqlRelationalDelegate(this)); cdTableView->setSelectionMode(QAbstractItemView::SingleSelection); cdTableView->setSelectionBehavior(QAbstractItemView::SelectRows); cdTableView->setColumnHidden(Cd_Id, true); cdTableView->resizeColumnsToContents(); Configurar la vista para la tabla cd es nuevamente similar a lo que ya hemos visto. La nica diferencia significante es que en lugar de usar el delegado por defecto de la vista, usamos QSqlRelationalDelegate. Es ste delegado el que hace que la clave fornea sea manejable. trackModel = new QSqlTableModel(this); trackModel->setTable("pista"); trackModel->setHeaderData(Pista_Titulo, Qt::Horizontal, tr("Titulo")); trackModel->setHeaderData(Pista_Duracion, Qt::Horizontal,tr("Duracion")); trackTableView = new QTableView; trackTableView->setModel(trackModel); trackTableView->setItemDelegate(new PistaDelegate(Pista_Duracion, this)); trackTableView->setSelectionMode(QAbstractItemView::SingleSelection); trackTableView->setSelectionBehavior(QAbstractItemView::SelectRows); Para las pistas, solamente vamos a mostrar sus nombres y duraciones, as que un QSqlTableModel es suficiente. (los campos id y cdid estn ocultados en el slot cdActualCambiado() mostrado ms adelante). El nico aspecto notable de esta parte del cdigo es que usamos el PistaDelegate desarrollado en el Capitulo 10 para mostrar tiempos de pistas en formato minutos:segundos y para permitirles ser editados usando un QTimeEdit adecuado. La creacin, conexin y ubicacin de las vistas y botones no contiene ninguna sorpresa, as que la nica parte del constructor que mostraremos son unas cuantas conexiones no tan obvias. connect(cdTableView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(cdActualCambiado(const QModelIndex &))); connect(cdModel, SIGNAL(beforeInsert(QSqlRecord &)), this, SLOT(antesDeInsertarCd(QSqlRecord &))); connect(trackModel, SIGNAL(beforeInsert(QSqlRecord &)), this, SLOT(antesDeInsertarPista(QSqlRecord &))); connect(trackModel, SIGNAL(rowsInserted(const QModelIndex &,

13. Bases de Datos

151

int,int)), this, SLOT(refrescarCabeceraVistaPista())); } La primera conexin es inusual, ya que en vez de conectar un widget, conectamos a un modelo de seleccin. La clase QItemSelectionModel es usada para mantener las selecciones de pistas en vistas. Por ser conectado al modelo de seleccin de la vista de tabla, nuestro slot cdActualCambiado() ser llamado cuando sea que el usuario navegue de un registro a otro. void MainForm::cdActualCambiado(const QModelIndex &indice) { if (indice.isValid()) { QSqlRecord registro = cdModel->record(indice.row()); int id = registro.value("id").toInt(); trackModel->setFilter(QString("cdid = %1").arg(id)); } else { trackModel->setFilter("cdid = -1"); } trackModel->select(); refrescarCabeceraVistaPista(); } Este slot es llamado cuando sea que el CD actual cambie. Esto ocurre cuando el usuario navega a otro CD (haciendo clic o usando las teclas Arriba y Abajo). Si el CD es invalido (por ejemplo, si no hay CDs o uno nuevo est siendo insertado, o el actual ha sido borrado), establecemos la propiedad cdid de la tabla pista en -1 (un ID invalido que sabemos, no arrojar registros). Luego, habiendo establecido el filtro, seleccionamos los registros de pistas coincidentes. La funcin refrescarCabeceraVistaPista() ser explicada en un momento. void MainForm::agregarCd() { int fila = 0; if (cdTableView->currentIndex().isValid()) fila = cdTableView->currentIndex().row(); cdModel->insertRow(fila); cdModel->setData(cdModel->index(fila, Cd_Ao), QDate::currentDate().year()); QModelIndex indice = cdModel->index(fila, Cd_Titulo); cdTableView->setCurrentIndex(indice); cdTableView->edit(indice); } Cuando el usuario haga clic en el botn agregar CD, una nueva fila en blanco es insertada en el cdTableView y se entra en modo de edicin. Tambin establecemos un valor por defecto para el campo ao. En este punto, el usuario puede editar el registro, llenando los campos en blanco y seleccionando un artista del combobox con una lista de artistas que es automticamente proporcionada por el QSqlRelationalTableModel gracias al llamado a setRelation(), y edita el ao si el que se proporcion por defecto no era apropiado. Si el usuario confirma la insercin presionando Enter, el registro es insertado. El usuario puede cancelar presionando Escape. void MainForm::antesDeInsertarCd(QSqlRecord &registro) { registro.setValue("id", generarId("cd")); }

13. Bases de Datos

152

Este slot es llamado cuando el cdModel emite su seal beforeInsert(). Nosotros lo usamos para llenar el campo id justo como lo hicimos para insertar nuevos artistas, y la misma advertencia aplica: Debe hacerse dentro del bucle de una transaccin, e idealmente el mecanismo especfico para crear IDs de la base de datos usada (por ejemplo, IDs auto generados) debe ser usado en lugar del mtodo anterior. void MainForm::eliminarCd() { QModelIndex indice = cdTableView->currentIndex(); if (!indice.isValid()) return; QSqlDatabase db = QSqlDatabase::database(); db.transaction(); QSqlRecord registro = cdModel->record(indice.row()); int id = registro.value(Cd_Id).toInt(); int pistas = 0; QSqlQuery query; query.exec(QString("SELECT COUNT(*) FROM pista WHERE cdid = %1").arg(id)); if (query.next()) pistas = query.value(0).toInt(); if (pistas > 0) { int r = QMessageBox::question(this, tr("Eliminar CD"), tr("Desea eliminar el CD \"%1\" y todas sus pistas?").arg(record. value(Cd_ArtistaId).toString()), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape); if (r == QMessageBox::No) { db.rollback(); return; } query.exec(QString("DELETE FROM pista WHERE cdid = %1").arg(id)); } cdModel->removeRow(indice.row()); cdModel->submitAll(); db.commit(); cdActualCambiado(QModelIndex()); } Si el usuario hace clic en el botn Eliminar Cd, este slot es llamado. Si hay un CD actual, encontramos cuntas pistas tiene ste. Si existe al menos una pista, le preguntamos al usuario que confirme la eliminacin, y si hace clic en Yes, eliminamos todos los registros de pistas, y luego el registro del CD. Todo esto se hace dentro del bucle de una transaccin, as que la eliminacin en cascada o fallar por completo o resultar por completo asumiendo que la base de datos usada soporte transacciones. El manejo de los datos de una pista es muy similar al manejos de los datos de un CD. Las actualizaciones pueden realizarse simplemente editando las celdas (por parte del usuario). En el caso de las duraciones de las pistas nuestro PistaDelegate asegura que los tiempos son mostrados en un formato agradable y son fcilmente editables usando un QTimeEdit. void MainForm::agregarPista() { if (!cdTableView->currentIndex().isValid()) return;

13. Bases de Datos

153

int fila = 0; if (trackTableView->currentIndex().isValid()) fila = trackTableView->currentIndex().row(); trackModel->insertRow(fila); QModelIndex indice = trackModel->index(fila, Pista_Titulo); trackTableView->setCurrentIndex(indice); trackTableView->edit(indice); } Esto funciona de la misma manera que lo hace agregarCd(), con una nueva fila en blanco siendo insertada dentro de la vista. void MainForm::antesDeInsertarPista(QSqlRecord &registro) { QSqlRecord registroCd = cdModel->record(cdTableView-> currentIndex() .row()); registro.setValue("id", generarId("pista")); registro.setValue("cdid", registroCd.value(Cd_Id).toInt()); } Si el usuario confirma la insercin inicializada por agregarPista(), esta funcin es llamada para llenar los campos id y cdid. La advertencia mencionada anteriormente sigue siendo aplicable, por supuesto. void MainForm::eliminarPista() { trackModel->removeRow(trackTableView-> currentIndex().row()); if (trackModel->rowCount() == 0) trackTableView->horizontalHeader()-> setVisible(false); } Si el usuario hace clic en el botn Eliminar Pista, eliminamos la pista sin formalidad alguna. Sera ms fcil usar un mensaje con las opciones Si/No si preferimos las eliminaciones previa confirmacin por parte del usuario. void MainForm::refrescarCabeceraVistaPista() { trackTableView->horizontalHeader()->setVisible( trackModel->rowCount() > 0); trackTableView->setColumnHidden(Pista_Id, true); trackTableView->setColumnHidden(Pista_CdId, true); trackTableView->resizeColumnsToContents(); } El slot refrescarCabeceraVistaPista() es invocado desde varios lugares para asegurarse de que la cabecera horizontal de la vista de pistas es mostrada si, y slo si, existen pistas a ser mostradas. Este tambin esconde los campos id y cdid y redimensiona las columnas visibles de la tabla basado en el contenido actual de la tabla. void MainForm::editarArtistas() { QSqlRecord registro = cdModel->record(cdTableView->currentIndex() .row()); ArtistForm artistForm(registro.value(Cd_ArtistaId).toString(), this); artistForm.exec(); cdModel->select(); }

13. Bases de Datos

154

Este slot es llamado si el usuario hace clic en el botn Editar Artistas. Este proporciona dril-down en el artista del CD actual, invocando el ArtistForm cubierto en la seccin anterior y seleccionando el artista apropiado. Si no existen registros actuales, un registro vaco seguro es retornado por record(), y esto, inofensivamente, no encontrar coincidencias de (y por lo tanto no selecciona) ningn artista en el formulario de artistas. Lo que sucede actualmente es que cuando llamamos a registro.value(Cd_ArtistaId), ya que estamos usando un QSqlRelationalTableModel que convierte los IDs de los artistas a nombres de artistas, el valor que es retornado es el nombre del artista (el cual ser una cadena vaca si el registro est vaco). Al final, obtenemos el cdModel para re seleccionar sus datos, lo que causa que el cdTableView refresque sus celdas visibles. Esto se hace para asegurar que los nombres de artistas son mostrados correctamente, ya que alguno de ellos podra haber sido cambiado por el usuario en el dialogo ArtistForm. Para proyectos que usen clases SQL, debemos agregar la siguiente lnea QT += sql

A los archivos .pro; esto asegurar que la aplicacin sea enlazada nuevamente con la librera QtSql. Este captulo ha mostrado que las clases de modelo/vista de Qt hacen que la visualizacin y la edicin de datos en base de datos SQL sea tan fcil como sea posible. En casos donde las claves forneas refieran a tablas con demasiados registros (digamos, unos mil o ms), es probablemente mejor crear nuestro propio delegado y usarlo para presentar un formulario de lista de valores con capacidad de bsqueda antes que usar los comboboxes por defecto del QSqlRelationalTableModel. Y en situaciones donde queramos presentar registros usando un formulario de vista, debemos manejar esto nosotros mismos: usando un QSqlQuery o QSqlTableModel para manejar la interaccin con la base de datos, y convirtiendo o mapeando el contenido de los widgets de la interfaz de usuario que queramos usar para presentar y editar los datos de la base de datos usada en nuestro propio cdigo.

14. Redes

155

14. Redes

Escribiendo Clientes FTP Escribiendo Clientes HTTP Escribiendo Aplicaciones Clientes-Servidores TCP Enviando y Recibiendo Datagramass UDP

Qt provee las clases QFtp y QHttp para trabajar con FTP y HTTP. Estos protocolos son fciles de usar para la descarga y subida de archivos y, en el caso de HTTP, para enviar solicitudes a servidores web y obtener resultados. Qt tambin proporciona las clases de bajo nivel QTcpSocket y QUdpSocket, las cuales implementan los protocolos de transporte TCP y UDP. TCP es un confiable protocolo orientado a conexines que opera en trminos de flujos de datos (data streams) transmitidos entre los nodos de red, mientras que UDP es un protocolo sin conexin desconfiable que est basado en paquetes discretos de datos que son enviados entre los nodos de la red. Ambos pueden ser usados para crear aplicaciones de red tanto clientes como servidores. Para los servidores, necesitamos tambin la clase QTcpServer para manejar las conexiones TCP entrantes.

Escribiendo Clientes FTP


La clase QFtp implementa el lado del cliente con el protocolo FTP en Qt. Este ofrece varias funciones para realizar las operaciones FTP ms comunes y permitirnos ejecutar comandos FTP arbitrarios. La clase QFtp trabaja asncronamente. Cuando llamamos a una funcin como get() o put(), esta retorna inmediatamente y la transferencia de datos sucede cuando el control pasa de vuelta al ciclo de eventos de Qt. Esto nos asegura que la interfaz de usuario permanezca activa mientras los comandos FTP son ejecutados. Comenzaremos con un ejemplo que muestra cmo recuperar un archivo usando la funcin get(). El ejemplo es una aplicacin de consola llamada ftpget que descarga el archivo remoto especificado en la lnea de comando. Comencemos con la funcin main(): int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); if (args.count() != 2) { cerr << "Uso: ftpget url" << endl << "Ejemplo:" << endl << " ftpget ftp://ftp.trolltech.com/mirrors" << endl; return 1; } FtpGet getter; if (!getter.obtenerArchivo(QUrl(args[1]))) return 1;

14. Redes

156

QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); }

Creamos un objeto QCoreApplication en lugar de su subclase QApplication para evitar tener que enlazar con la librera QtGui (ya que esta es una aplicacin de consola). La funcin QCoreApplication::arguments() retorna los argumentos de la lnea de comando como una QStringList, siendo el primer tem el nombre con que fue invocado el programa, y cualquier argumento especifico de Qt como -style. El corazn de la funcin main() es la construccin del objeto FtpGet y el llamado a la funcin obtenerArchivo(). Si el llamado es realizado con xito, dejamos correr el ciclo de eventos hasta que la descarga finalice. Todo el trabajo es realizado por la subclase de FTpGet, la cual se define como sigue a continuacin: class FtpGet : public QObject { Q_OBJECT public: FtpGet(QObject *parent = 0); bool obtenerArchivo(const QUrl &url); signals: void done(); private slots: void ftpHecho(bool error); private: QFtp ftp; QFile archivo; }; La clase posee una funcin pblica, obtenerArchivo(), que recupera el archivo especificado por una URL. La clase QUrl proporciona una interfaz de alto nivel para extraer las diferentes partes de una URL, como el nombre del archivo, la ruta o path, el protocolo y el puerto. La clase FtpGet tambin posee un slot privado. Entonces conectamos la seal QFtp::done(bool) a nuestro slot privado ftpHecho(bool). QFtp emite la seal done(bool) cuando este ha finalizado la descarga del archivo. Tambin tiene dos variables privadas: La variable ftp, de tipo QFtp, encapsula la conexin a un servidor FTP, y la variable archivo que es usada para la escritura en disco del archivo descargado. FtpGet::FtpGet(QObject *parent) : QObject(parent) { connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpHecho(bool))); } En el constructor, conectamos la seal QFtp::done(bool) al slot privado ftpHecho(bool). QFtp emite la seal done(bool) cuando se ha finalizado el procesamiento de todas las solicitudes. El parmetro bool indica si ha ocurrido un error o no. bool FtpGet::obtenerArchivo(const QUrl &url) { if (!url.isValid()) { cerr << "Error: URL invalida" << endl; return false; } if (url.scheme() != "ftp") { cerr << "Error: La URL debe iniciar con ftp:" << endl;

14. Redes

157

return false; } if (url.path().isEmpty()) { cerr << "Error: La URL no tiene una ruta" << endl; return false; } QString localNombreArchivo = QFileInfo(url.path()).fileName(); if (localNombreArchivo.isEmpty()) localNombreArchivo = "ftpget.out"; archivo.setFileName(localNombreArchivo); if (!archivo.open(QIODevice::WriteOnly)) { cerr << "Error: No se puede abrir " << qPrintable(archivo.fileName()) << " para escribir: " << qPrintable(archivo.errorString()) << endl; return false; } ftp.connectToHost(url.host(), url.port(21)); ftp.login(); ftp.get(url.path(), &archivo); ftp.close(); return true; } La funcin obtenerArchivo() empieza con la revisin de la URL que le fue pasada. Si se encuentra un problema, la funcin imprime un mensaje de error con el comando cerr y retorna false para indicar que la descarga ha fallado. En lugar de obligar al usuario a colocar un nombre de archivo, intentamos crear un nombre usando la misma URL, y si falla, le damos como nombre ftpget.out. Si fallamos al intentar abrir el archivo, imprimimos un mensaje de rror y retornamos false. Lo que sigue, es ejecutar una secuencia de comandos FTP usando nuestro objeto QFtp. La llamada a url.port(21) retorna el nmero del puerto especificado en la URL, o el puerto 21 si no se especific en la URL. Como ningn nombre de usuario o contrasea han sido dados a la funcin login(), se intenta iniciar un login annimo. El segundo argumento a get() especifica el dispositivo de salida. Los comandos FTP son puestos en cola y se van ejecutando en el ciclo de eventos de Qt. La seal done(bool) de QFtp indica la completacin de todos los comandos. Esta seal fue la que conectamos al slot ftpHecho() en el constructor anteriormente. void FtpGet::ftpHecho(bool error) { if (error) { cerr << "Error: " << qPrintable(ftp.errorString()) << endl; } else { cerr << "Archivo descargado como " << qPrintable(archivo.fileName()) << endl; } archivo.close(); emit done(); } Una vez que los comandos FTP han sido ejecutados en su totalidad, cerramos el archivo y emitimos nuestra seal done(). Puede parecer extrao que cerremos el archivo aqu, en lugar de cerrarlo despus del llamados a ftp.close() al final de la funcin obtenerArchivo(), pero recordemos que los comandos FTP son jecutados asncronamente y pueden seguir perfectamente en proceso aun despus que la

14. Redes

158

funcin obtenerArchivo() retorne. Solo cuando la seal done() del objeto QFtp es emitida, sabremos que la descarga ha finalizado y ser seguro cerrar el archivo. QFtp proporciona muchos comandos FTP, incluyendo connectToHost(), login(), close(), list(), cd(), get(), put(), remove(), mkdir(), rmdir() y rename(). Todas estas funciones lo que hacen es programar (ponen en lista) la ejecucin de un comando FTP y retornan un nmero de ID que identifica el comando. Tambien es posible controlar el modo de trasferencia (que por defecto es pasivo) y el tipo de transferencia (que por defecto es binario). Los comandos FTP arbitrarios pueden ser ejecutados usando rawCommand(). Por ejemplo, aqu est la manera de ejecutar el comando SITE CHMOD: ftp.rawCommand("SITE CHMOD 755 fortune"); QFtp emite la seal commandStarted(int) cuando se inicia la ejecucin de un comando, y este emite la seal commandFinished(int, bool) cuando la ejecucin del comando es finalizada. El parmetro int es el numero de ID que identifica al comando. Si estamos interesados en el destino de comandos individuales, podemos guardar los nmeros de ID cuando se programen los comandos. Seguir el rastro de los nmeros de ID nos permite proporcionar un feedback detallado al usuario. Por ejemplo: bool FtpGet::obtenerArchivo(const QUrl &url) { ... idConexion = ftp.connectToHost(url.host(), url.port(21)); idLogin = ftp.login(); idGet = ftp.get(url.path(), &file); idCierre = ftp.close(); return true; } void FtpGet::ftpComandoIniciado(int id) { if (id == idConexion) { cerr << "Conectando..." << endl; } else if (id == idLogin) { cerr << "Logueando..." << endl; ... } Otra manera de proporcionar un feedback es conectar la seal stateChanged() de QFtp, la cual es emitida si la conexin cambia de estado (QFtp::Connecting, QFtp::Connected, QFtp::LoggedIn, etc.). En la mayora de las aplicaciones, solamente nos interesar el destino de las secuencias de comandos como un conjunto y no de comandos en particular. Para esos casos, simplemente se conecta la seal done(bool), que se emite si la cola de comandos se queda vaca. Cuando ocurre un error, QFtp limpia la cola de comandos automticamente. Esto quiere decir que si la conexin o el login fallan, los comandos que sigan en la cola no sern ejecutados. Si programamos la ejecucin de nuevos comandos despus de que ocurre un error usando el mismo objeto QFtp, estos comandos sern puestos en cola y ejecutados. En el archivo .pro de la aplicacin, necesitamos la siguiente lnea para enlazar con la librera QtNetwork: QT += network Ahora vamos a examinar un ejemplo algo ms avanzado. El programa de lnea de comando llamado spider descarga todos los archivos localizados en un directorio FTP. Recursivamente va descargando todos los subdirectorios del directorio inicial. La lgica de red est localizada en la clase spider:

14. Redes

159

class Spider : public QObject { Q_OBJECT public: Spider(QObject *parent = 0); bool obtenerDirectorio(const QUrl &url); signals: void done(); private slots: void ftpHecho(bool error); void ftpInfoLista(const QUrlInfo &urlInfo); private: void procesarSiguienteDir(); QFtp ftp; QList<QFile *> archivosAbiertos; QString directorioActual; QString directorioLocalActual; QStringList directoriosPendientes; }; El directorio inicial es especificado obtenerDirectorio(). como un QUrl y se establece usando la funcin

Spider::Spider(QObject *parent) : QObject(parent) { connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpHecho(bool))); connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)), this, SLOT(ftpInfoLista(const QUrlInfo &))); } En el constructor, establecemos dos conexiones. La seal listInfo(const QUrlInfo &) es emitida por QFtp cuando solicitamos un listado de directorios (en obtenerDirectorio()) para cada archivo que se recupera. Esta seal est conectada a un slot llamado ftpInfoLista(), el cual se encarga de descargar el archivo asociado con la URL que se le pasa. bool Spider::obtenerDirectorio(const QUrl &url) { if (!url.isValid()) { cerr << "Error: URL Invalida" << endl; return false; } if (url.scheme() != "ftp") { cerr << "Error: La URL de empezar con ftp:" << endl; return false; } ftp.connectToHost(url.host(), url.port(21)); ftp.login(); QString ruta = url.path(); if (ruta.isEmpty()) ruta = "/"; directoriosPendientes.append(ruta); procesarSiguienteDir(); return true; }

14. Redes

160

Cuando la funcin obtenerDirectorio() es llamada, esta hace varias revisiones de integridad y si todo est bien, intenta establecer una conexin FTP. Esta mantiene el rastro de las rutas (paths) que debe procesar y llama a procesarSiguienteDir() para empezar a descargar desde el directorio raz. void Spider::procesarSiguienteDir() { if (!directoriosPendientes.isEmpty()) { directorioActual = directoriosPendientes.takeFirst(); directorioLocalActual = "downloads/" + directorioActual; QDir(".").mkpath(directorioLocalActual); ftp.cd(directorioActual); ftp.list(); } else { emit done(); } } La funcin procesarSiguienteDir() toma el primer directorio remoto de la lista de directorios pendientes (directoriosPendientes()) y crea un directorio correspondiente en el sistema de archivo local. Luego le dice al objeto QFtp que cambie el directorio al directorio tomado y liste sus archivos. Por cada archivo que la funcin list() procese, emite un seal listInfo() que hace que el slot ftpInfoLista() sea llamado. Si ya no hay mas directorios por procesar, la funcin emite la seal done() para indicar que la descarga se ha completado. void Spider::ftpInfoLista(const QUrlInfo &urlInfo) { if (urlInfo.isFile()) { if (urlInfo.isReadable()) { QFile *archivo = new QFile(directorioLocalActual + "/" + urlInfo.name()); if (!archivo->open(QIODevice::WriteOnly)) { cerr<< "Advertencia: No se puedo abrir el archivo" << qPrintable(QDir::convertSeparators (archivo->fileName())) << endl; return; } ftp.get(urlInfo.name(), archivo); archivosAbiertos.append(archivo); } } else if (urlInfo.isDir() && !urlInfo.isSymLink()) { directoriosPendientes.append(directorioActual + "/" + urlInfo.name()); } } El parmetro urlInfo del slot ftpInfoLista() provee informacin detallada acerca de un archivo remoto. Si el archivo es un archivo normal (y no un directorio) y adems es legible, llamamos a get() para descargarlo. El objeto QFile usado para descargar es localizado usando un puntero hacia su ubicacin en la lista archivosArbiertos. Si el objeto QUrlInfo contiene los detalles de un directorio remoto que no es un enlace simblico, agregamos este directorio a la lista directoriosPendientes. Tenemos que obviar los enlaces simblicos porque estos pueden inducir a una recursin infinita. void Spider::ftpHecho(bool error) { if (error) {

14. Redes

161

cerr << "Error: " << qPrintable(ftp.errorString()) << endl; } else { cout << "Descargado " << qPrintable(directorioActual) << " a " << qPrintable(QDir::convertSeparators (QDir(directorioLocalActual).canonicalPath())); } qDeleteAll(archivosAbiertos); archivosAbiertos.clear(); procesarSiguienteDir(); } El slot ftpHecho() es llamado cuando todos los comandos FTP han finalizado o si ocurre un error. Lo que sigue es borrar los objetos QFile para evitar goteos de memoria (memory leaks) y tambin debemos cerrar cada archivo que fue abierto. Finalmente, llamamos a procesarSiguienteDir(). Si existe algn direcotorio restante, el proceso entero comenzar nuevamente con el directorio que siga en la lista; de otra manera, la descarga parar y la seal done() ser emitida. Si no hay errores, la secuencia de comandos FTP y de la emisin de las seales ser de esta forma: connectToHost(host, puerto) login() cd(directorio_1) list() emit listInfo(archivo_1_1) get(archivo_1_1) emit listInfo(archivo_1_2) get(archivo_1_2) ... emit done() ... cd(directorio_N) list() emit listInfo(archivo_N_1) get(archivo_N_1) emit listInfo(archivo_N_2) get(archivo_N_2) ... emit done() Si un archivo es, de hecho, un directorio, este ser agregado a la lista directoriosPendientes, y cuando el ltimo archivo del comando list() ha sido descargado, se ejecuta un nuevo comando cd(), seguido de un nuevo comando list() lista de comandos con el siguiente directorio pendiente, y todo el proceso empieza de nuevo pero con el nuevo directorio. Esto se repite con los nuevos archivos descargados, y con los nuevos directorios agregados a la lista directoriosPendientes, hasta que cada archivo haya sido descargado de cada directorio, punto en el cual la lista directoriosPendientes estar vacia. Si ocurre un error de red cuando se est descargando, por ejemplo, el quinto archivo de un total de veinte archivos existentes en el directorio, entonces los archivos restantes no sern descargados. Si queremos descargar tantos archivos como sea posible, una solucin podra ser programar las operaciones GET una a la vez y esperar por la seal done(bool) antes de programar la siguiente. En listInfo(), simplemente anexaramos el nombre del archivo a un QStringList, en lugar de llamar a get() en seguida, y en donde va done(bool) llamariamos a get() con el siguiente archivo a descargar en el QStringList. La secuencia de ejecucin sera como esta: connectToHost(host, puerto) login()

14. Redes

162

cd(directorio_1) list() ... cd(directorio_N) list() emit listInfo(archivo_1_1) emit listInfo(archivo_1_2) ... emit listInfo(archivo_N_1) emit listInfo(archivo_N_2) ... emit done() get(archivo_1_1) emit done() get(archivo_1_2) emit done() ... get(archivo_N_1) emit done() get(archivo_N_2) emit done() ... Otra solucin podra ser usar un objeto QFtp por archivo. Esto nos permitira descargar los archivos en paralelo, a travs de conexiones FTP separadas. int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); if (args.count() != 2) { cerr << "Uso: url spider" << endl << "Ejemplo:" << endl << " spider ftp://ftp.trolltech.com/freebies/leafnode"<< endl; return 1; } Spider spider; if (!spider.obtenerDirectorio(QUrl(args[1]))) return 1; QObject::connect(&spider, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); } La funcin main() completa el programa. Si el usuario no especifica una URL en la lnea de comandos, mandamos un mensaje de error y terminamos la ejecucin del programa. En ambos ejemplos, los datos recuperados usando get() fueron escritos a un QFile. Claro est, que este no tiene por qu ser el caso. Si queremos los datos en memoria en lugar de escribirlos en disco, podriamoshaber usado un QBuffer, la subclase de QIODevice que envuelve un QByteArray. Por ejemplo: QBuffer *buffer = new QBuffer; buffer->open(QIODevice::WriteOnly);

14. Redes

163

ftp.get(urlInfo.name(), buffer); Tambien podramos omitir el argumento buffer a get() o pasar un puntero nulo. La clase QFtp emitir luego una seal readyRead() cada vez que nuevos datos estn disponibles, para que los datos puedan ser leidos usando read() o readAll().

Escribiendo Clientes HTTP


La clase QHttp implementa lo que es la parte cliente del protocolo HTTP en Qt. Esta provee varias funciones para realizar las operaciones HTTP ms comunes, incluyendo get() y post(), y proporciona unos mtodos para enviar solicitudes HTTP. Si has ledo la seccin anterior acerca de QFtp, te dars cuenta que existen muchas similitudes entre las clases QFtp y QHttp. La clase QHttp trabaja asncronamente. Cuando llamamos a una funcin como get() o post(), la funcin retorna inmediatamente, y la transferencia de datos se realiza despus, cuando el control vuelva al ciclo de eventos de Qt. Esto asegura que la interfaz de usuarios de la aplicacin permanezca activa mientras se procesan solicitudes de tipo HTTP. Ahora vamos a examinar una aplicacin de consola como ejemplo que es llamada httpget, que muestra cmo descargar un archivo usando el protocolo HTTP. Es muy similar al ejemplo ftpget, mostrado en la seccin anterior, tanto en funcionalidad como en implementacin, as que no mostraremos lo que es el archivo de cabecera. HttpGet::HttpGet(QObject *parent) : QObject(parent) { connect(&http, SIGNAL(done(bool)), this, SLOT(httpHecho(bool))); } En el constructor, conectamos la seal done(bool) del objeto QHttp al slot privado httpHecho(bool). bool HttpGet::obtenerArchivo(const QUrl &url) { if (!url.isValid()) { cerr << "Error: URL Invalida" << endl; return false; } if (url.scheme() != "http") { cerr << "Error: La URL debe comenzar con http:" << endl; return false; } if (url.path().isEmpty()) { cerr << "Error: La URL no tiene una ruta" << endl; return false; } QString localNombreArchivo = QFileInfo(url.path()).fileName(); if (localNombreArchivo .isEmpty()) localNombreArchivo = "httpget.out"; archivo.setFileName(localNombreArchivo ); if (!archivo.open(QIODevice::WriteOnly)) { cerr << "Error: No se puede abrir " << qPrintable(archivo.fileName()) << " para escritura: " << qPrintable(archivo.errorString()) << endl; return false; }

14. Redes

164

http.setHost(url.host(), url.port(80)); http.get(url.path(), &archivo); http.close(); return true; } La funcin obtenerArchivo() realiza el mismo tipo de comprobaciones de errores que se realizaron en QFtp::obtenerArchivo, mostrada anteriormente, y usa el mismo mtodo para darle al archivo un nombre local. Cuando recuperamos datos desde sitios web, ningn login es necesario, asi que podemos simplemente configurar el host y el puerto (usando el puerto 80, que es el puerto predeterminado del protocolo HTTP, si es que no se especifica ninguno en la URL) y descargamos los datos en un archivo, ya que el segundo argumento a QHttp::get() especifica el dispositivo de salida a ser usado. Las solicitudes HTTP son puestas en cola y ejecutadas asncronamente en el ciclo de eventos de Qt. La completacion de las solicitudes se indica por medio de la seal done(bool) de QHttp, la cual hemos conectado con httpHecho(bool) en el constructor. void HttpGet::httpHecho(bool error) { if (error) { cerr << "Error: " << qPrintable(http.errorString()) << endl; } else { cerr << "Archvo descargado como " << qPrintable(archivo.fileName()) << endl; } Archivo.close(); emit done(); } Una vez que las solicitudes HTTP han finalizado, cerramos el archivo, notificndole al usuario si ha ocurrido un error. La funcin main() es muy similar a la que hemos usado en ftpget: int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); if (args.count() != 2) { cerr << "Uso: httpget url" << endl << "Example:" << endl << " httpget http://doc.trolltech.com/qq/index.html" << endl; return 1; } HttpGet getter; if (!getter.getFile(QUrl(args[1]))) return 1; QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); } La clase QHttp provee de muchas operaciones, incluyendo setHost(), get(), post() y head(). Si un sitio requiere autenticaciones, setUser() puede ser usada para dar un nombre de usuario y una contrasea. QHttp puede usar un socket proporcionado por el programador en lugar de usar el QTcpSocket interno. Esto hace posible usar un QtSslSocket seguro para conseguir un metodo HTTP basado en SSL. Para enviar una lista de pares nombre=valor a un script CGI, podemos usar post():

14. Redes

165

http.setHost(www.example.com); http.post(/cgi/algunscript.py/, x=200&y=320, &archivo); Podemos pasar los datos como una cadena de texto de 8-bit o como un QIODevice abierto, como lo es un QFile. Para ms control, podemos usar la funcin request(), la cual acepta datos y una cabecera HTTP arbitraria. Por ejemplo: QHttpRequestHeader cabecera("POST", "/search.html"); cabecera.setValue("Host", "www.trolltech.com"); cabecera.setContentType("application/x-www-form-urlencoded"); http.setHost("www.trolltech.com"); http.request(cabecera, "qt-interest=on&search=opengl"); QHttp emite la seal requestStarted(int) cuando comienza a ejecutar una solicitud, y emite la seal requestFinished(int, bool) cuando la solicitud ha finalizado. El parmetro int es un numero de ID que identifica a la solicitud. Si estamos interesados en el curso que siguen solicitudes individuales, podemos guardar esos nmeros ID cuando programemos las solicitudes. Siguiendo el rastro de los nmeros de ID, podremos proporcionar un feedback con informacin detallada al usuario. En la mayora de las aplicaciones, solamente queremos saber si toda la secuencia de solicitudes es completada satisfactoriamente o no. Esto es fcil de hacer a travs de la conexin de la seal done(bool), la cual es emitida cuando la cola de solicitudes se queda vacia. Cuando un ocurre un error, La cola de solicitudes es automticamente limpiada. Pero si programamos nuevas solicitudes despus de que haya ocurrido un error usando el mismo objeto QHttp, dichas solicitudes sern puestas en cola y eviandas, como es usual. Al igual que QFtp, QHttp provee una seal readyRead() y tambin las funciones read() y readAll(), que podemos usar en lugar de especificar un dispositivo de E/S.

Escribiendo Aplicaciones Clientes-Servidores TCP


Las clases QTcpSocket y QTcpServer pueden ser usadas para implementar clientes y servidores TCP. TCP es un protocolo de transporte que forma la base de la mayora de los niveles de aplicacin de los protocolos de internet, incluyendo FTP y HTTP, y puede ser usado tambin para crear protocolos propios. TCP es un protocolo orientado a flujos (stream-oriented). Para las aplicaciones, los datos parecen ser un gran flujo, y no un gran archivo plano. Los protocolos de alto nivel construidos sobre TCP son, generalmente, tanto orientados a lneas como orientados a bloques. Los protocolos orientados a lneas transfieren los datos como lneas de texto, cada una terminada por una nueva lnea. Los protocolos orientados a bloques tranfieren los datos como bloques de datos binarios. Cada bloque consta de una campo tamao seguido de los datos en si.

QTcpSocket hereda de QIODevice a travs de QAbstractSocket, de manera que puede ser ledo y escrito para usar un QDataStream o un QTextStream. Una diferencia notable cuando leemos datos desde una red comparado con la lectura de un archivo es que debemos asegurarnos de que hemos recibido la data suficiente desde el proveedor antes de usar el operador >>. Si no se hace esto, puede resultar un comportamiento no definido. En esta seccin, vamos a revisar el cdigo de un cliente y un servidor que usan un protocolo propio orientado a bloques. El cliente es llamado Planeador de Viaje y permite al usuario planear su prximo viaje en tren. El servidor es llamado Servidor de Viaje y provee la informacin de viaje al cliente. Comenzaremos con la escritura del cliente Planeador de Viaje.

14. Redes

166

El Planeador de Viaje provee un campo Desde, un campo Hacia, un campo Fecha y un campo Tiempo Aproximado, adems de dos radio buttons para seleccionar si el tiempo aproximado es de partida o de llegada. Cuando el usuario hace click en Buscar, la aplicacin enva una solicitud al servidor, el cual responde con una lista de viajes de trenes que coincidan con el criterio del usuario. La lista es mostrada en un QTableWidget en la ventana del Planeador de Viaje. La parte ms baja de la ventana est ocupada por un QLabel que muestra el estado de la ltima operacin y un QProgressBar. Figura 14.1. La aplicacin Planeador de Viaje

La interfaz de usuario de Planeador de Viaje fue creada con Qt Designer en un archivo llamado planeadorviaje.ui. Aqu, nos concentraremos en el cdigo fuente de la subclase de QDialog que implementa la funcionalidad de la aplicacin: #include "ui_planeadorviaje.h" class PlaneadorViaje : public QDialog, public Ui::PlaneadorViaje { Q_OBJECT public: PlaneadorViaje(QWidget *parent = 0); private slots: void conectarAServidor(); void enviarSolicitud(); void actualizarTableWidget(); void pararBusqueda(); void ConexionCerradaPorServidor(); void error(); private: void cerrarConexion(); QTcpSocket tcpSocket; quint16 siguienteTamaoBloque; }; La clase PlaneadorViaje hereda de Ui::PlaneadorViaje (el cual es generado por uic a partir del archivo planeadorviaje.ui) y de QDialog. La variable miembro tcpSocket encapsula la conexin TCP. La variable siguienteTamaoBloque es usada cuando se analizan los bloques recibidos desde el servidor.

PlaneadorViaje::PlaneadorViaje(QWidget *parent) : QDialog(parent) { setupUi(this);

14. Redes

167

QDateTime dateTime = QDateTime::currentDateTime(); dateEdit->setDate(dateTime.date()); timeEdit->setTime(QTime(dateTime.time().hour(), 0)); progressBar->hide(); progressBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored); tableWidget->verticalHeader()->hide(); tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(botonBuscar, SIGNAL(clicked()), this, SLOT(connectToServer())); connect(botonParar, SIGNAL(clicked()), this, SLOT(pararBusqueda())); connect(&tcpSocket, SIGNAL(connected()), this, SLOT(enviarSolicitud())); connect(&tcpSocket, SIGNAL(disconnected()), this, SLOT(conexionCerradaPorServidor())); connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(actualizarTableWidget())); connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error())); } En el constructor, inicializamos los editores de fecha y de tiempo basados en la fecha y hora actuales. Tambin escondemos la barra de progreso, porque solo queremos que aparezca cuando una conexin es activada. En Qt Designer, las propiedades minimun y mximum de la barra de progreso fueron establecidas a cero. Esto le dice al QProgressBar que se comporte como un idicador de estado ocupado en lugar de comportarse como una barra de progreso estndar basada en porcentajes. En el constructor tambin conectamos las seales connected(), disconnected(), readyRead() y error (QAbstract::SocketError) pertenceientes a QTcpSocket a distintos slots privados. void PlaneadorViaje::conectarAServidor() { tcpSocket.connectToHost("tripserver.zugbahn.de", 6178); tableWidget->setRowCount(0); botonBuscar->setEnabled(false); botonParar->setEnabled(true); labelEstado->setText(tr("Conectando con el servidor...")); progressBar->show(); siguienteTamaoBloque = 0; } El slot conectarAServidor() es ejecutado cuando el usuario hace click en Buscar para empezar una bsqueda. Llamamos a connectToHost() desde el objeto QTcpSocket para conectar con el servidor, el cual asumimos es accesible por el puerto 6178 en el host ficticio tripserver.zugbahn.de (si quieres intentar este ejemplo en tu propia mquina, reemplaza el nombre del servidor con QHostAddress::LocalHost). El llamado a connectToHost() es asncrono; siempre retorna inmediatamente. La conexin se establece, generalmente, un tiempo despus. El objeto QTcpSocket emite la seal connected() cuando la conexin se establece y se encuentra activa., o emite la seal de error (QAbstractSocket::SocketError) si la conexin falla. Ahora, podemos actualizar la interfaz de usuario, haciendo la barra de progreso visible.

14. Redes

168

Finalmente, hacemos a la variable siguienteTamaoBloque igual a cero. Esta variable guarda el tamao del siguiente bloque recibido desde el servidor. Hemos elegido usar el valor de 0 para indicar que no sabemos todava el tamao del siguiente bloque. void PlaneadorViaje::enviarSolicitud() { QByteArray blocque; QdataStream salida(&bloque, QIODevice::WriteOnly); salida.setVersion(QdataStream::Qt_4_1); salida << quint16(0) << quint8(S) << fromComboBox->currentText() << haciaComboBox->currentText() << dateEdit->date() << timeEdit->time(); if (dePartidaRadioButton->isChecked()) { salida << quint8(P); } else { salida << quint8(L); } salida.device()->seek(0); salida << quint16(bloque.size() sizeof(quint16)); tcpSocket.write(bloque); labelEstado->setText(tr(Enviando solicitud)); } El slot enviarSolicitud() es ejecutado cuando el objeto QTcpSocket emite la seal connected(), indicando que una conexin ha sido establecida. La tarea del slot es generar una solicitud al servidor, con toda la informacin introducida por el usuario. La solicitud es un bloque binario con el siguiente formato: quint16 quint8 QString QString QDate QTime quint8 Tamao del bloque en bytes (excluyendo este campo) Tipo de solicitud (siempre S) Ciudad de partida Ciudad de destino Fecha del viaje Tiempo aproximado del viaje El tiempo es de partida (P) o de llegada (L)

Lo primero que hacemos es escribir los datos a un QByteArray llamado bloque. No podemos escribir los datos directamente al QTcpSocket porque no sabemos el tamao que tendr el bloque de datos, el cual debe ser enviado primero, hasta despus que hayamos puesto todos los datos en el bloque. Inicialmente hacemos cero al tamao del bloque, seguido por el resto de los datos. Luego llamamos a seek(0) sobre el dispositivo de E/S (un QBuffer creado por QDataStream ocultamente) para ir al principio del byte array nuevamente, y sobreescribir el cero inicial con el tamao de los datos del bloque. El tamao es calculado tomando el tamao del bloque y restando el resultado de sizeof(quint16) (que es 2) para excluir el campo tamao de la cantidad de bytes total calculada (ya que este campo debe excluirse, como ya se dijo). Despues de eso, llamamos a write() desde el objeto QTcpSocket para enviar el bloque de datos al servidor. void PlaneadorViaje::actualizarTableWidget() { QDataStream entrada(&tcpSocket); entrada.setVersion(QDataStream::Qt_4_1); forever {

14. Redes

169

int fila = tableWidget->rowCount(); if (siguienteTamaoBloque == 0) { if (tcpSocket.bytesAvailable() < sizeof(quint16)) break; entrada >> siguienteTamaoBloque ; } if (siguienteTamaoBloque == 0xFFFF) { cerrarConexion(); labelEstado->setText(tr("Viajes Encontrados") .arg(fila)); break; } if (tcpSocket.bytesAvailable() < siguienteTamaoBloque ) break; QDate fecha; QTime tiempoPartida; QTime tiempoLlegada; quint16 duracion; quint8 cambios; QString tipoTren; entrada >> fecha >> tiempoPartida >> duracion >> cambios >> tipoTren; tiempoLlegada = tiempoPartida.addSecs(duracion * 60); tableWidget->setRowCount(fila + 1); QStringList campos; fields << fecha.toString(Qt::LocalDate) << tiempoPartida.toString(tr("hh:mm")) << tiempoLlegada.toString(tr("hh:mm")) << tr("%1 hr %2 min").arg(duracion / 60) .arg(duracion % 60) << QString::number(cambios) << tipoTren; for (int i = 0; i < campos.count(); ++i) tableWidget->setItem(fila, i, new QTableWidgetItem (campos[i])); siguienteTamaoBloque = 0; } } El slot actualizarTableWidget() est conectado a la seal readyRead() de QTcpSocket, el cual es emitido si el QTcpSocket ha recibido nuevos datos desde el servidor. El servidor nos enva una lista de posibles viajes de trenes que coincida con el criterio del usuario. Cada viaje coincidente es enviado como un nico bloque, y cada bloque empieza con un tamao. El ciclo forever es necesario porque no obtenemos un bloque de datos a la vez desde el servidor necesariamente.* Tal vez podramos haber recibido un bloque entero, o solo parte de un bloque, o uno y medio, o incluso todos los bloques de una sola vez. Figura 14.2. Los bloques del Servidor de Viajes

* La palabra clave forever es provista por Qt. Esta simplemente expande el for (;;)

14. Redes

170

Y cmo funciona el ciclo forever? Si la variable siguienteTamaoBloque es 0, quiere decir que no hemos ledo el tamao del siguiente bloque. Tratamos de leerlo (asumiendo que existen al menos 2 bytes de espacio disponible para la lectura). El servidor usa un valor para el tamao de 0xFFFF para indicar que no hay ms datos a ser recibidos, de manera que si leemos este valor, sabremos que hemos alcanzado el final. Si el tamao del bloque de datos no es 0xFFFF, tratamos de leer el siguiente bloque. Primero, verificamos para ver si existen bytes de tamao de bloque para ser leidos. Si no existe ninguno, paramos all mientras tanto con el comando break. La seal readyRead() ser emitida nuevamente cuando estn disponibles ms datos, y lo intentaremos de nuevo luego. Una vez que estemos seguros de que un bloque completo ha llegado, podemos usar seguramente el operador >> en el QDataStream para extraer la informacin relativa a un viaje, y creamos varios QTableWidgetItem con esa informacin. Un bloque recibido desde el servidor posee el siguiente formato: quint16 QDate QTime quin16 quint8 QString Tamao del bloque en bytes (excluyendo este campo) Fecha de salida Tiempo de salida Duracion (en minutos) Numero de cambios Tipo de tren

Al final, reestablecemos la variable siguienteTamaoBloque a cero para indicar que el siguiente tamao del bloque es desconocido y necesita ser ledo. void PlaneadorViaje::cerrarConexion() { tcpSocket.close(); botonBuscar->setEnabled(true); botonParar->setEnabled(false); progressBar->hide(); } La funcion privada cerrarConexion() cierra la conexin con el servidor TCP y actualiza la interfaz de usuario. Esta es llamada desde el slot actualizarTableWidget() cuando el valor 0xFFFF es ledo y tambin es llamada desde otros slots, los cuales que veremos en seguida. void PlaneadorViaje::pararBusqueda() { labelEstado->setText(tr("Busqueda detenida")); cerrarConexion(); } El slot pararBusqueda() est conectado a la seal clicked() del botn Parar. En esencia, este slot solamente se encarga de llamar a la funcin cerrarConexion(). void PlaneadorViaje::ConexionCerradaPorServidor() { if (siguienteTamaoBloque != 0xFFFF) labelEstado->setText( tr("Error: Conexin cerrada por el servidor")); cerrarConexion(); } El slot conexionCerradaPorServidor() est conectado con la seal disconnected() de la clase QTcpSocket. Si el servidor cierra la conexin y aun no hemos recibido el marcador de fin 0xFFFF, le

14. Redes

171

decimos al usuario que ha ocurrido un error. Llamamos a cerrarConexion() como es de costumbre para actualizar la interfaz de usuario. void PlaneadorViaje::error() { labelEstado->setText(tcpSocket.errorString()); cerrarConexion(); } El slot error() se encuentra conectado con la seal error(QAbstractSocket::SocketError) de QTcpSocket. Ignoramos el cdigo de error y usamos QTcpSocket::errorString(), la cual retorna un mensaje de error legible por el usuario, correspondiente al ltimo error ocurrido. Esto es todo lo que tiene que ver con la clase PlaneadorViaje. La funcin main() para la aplicacion Planeador de Viaje se ve de esta manera: int main(int argc, char *argv[]) { QApplication app(argc, argv); PlaneadorViaje planeadorViaje; planeadorViaje.show(); return app.exec(); } Ahora vamos a implementar el servidor. El servidor consta de dos clases: ServidorViaje y ClienteSocket. La clase ServidorViaje hereda de QTcpServer, una clase que nos permite aceptar conexiones TCP entrantes. La clase ClienteSocket reimplementa QTcpSocket y maneja una sola conexin. Existen tantos objetos ClienteSocket en memoria como nmero clientes siendo servidos. class ServidorViaje : public QTcpServer { Q_OBJECT public: ServidorViaje(QObject *parent = 0); private: void conexionEntrante(int socketId); }; La clase ServidorViaje reimplementa la funcin incomingConnection() de QTcpServer, pero aqu la llamamos conexionEntrante(). Esta funcin es llamada si un cliente intenta conctarse al puerto en el que el servidor est escuchando. ServidorViaje:: ServidorViaje (QObject *parent) : QTcpServer(parent) { } El constructor de ServidorViaje es algo trivial. Alli no es necesario hacer nada. void ServidorViaje::conexionEntrante(int socketId) { ClienteSocket *socket = new ClienteSocket(this); socket->setSocketDescriptor(socketId); } En conexionEntrante(), creamos un objeto ClienteSocket como hijo del objeto ServidorViaje, y configuramos su descriptor de socket al nmero proporcionado por nosotros. El objeto ClienteSocket se borrar a si mismo automticamente cuando la conexin se termine. class ClienteSocket : public QTcpSocket {

14. Redes

172

Q_OBJECT public: ClienteSocket(QObject *parent = 0); private slots: void leerCliente(); private: void generateRandomTrip(const QString &desde, const QString &hacia, const QDate &fecha, const QTime &tiempo); quint16 siguienteTamaoBloque; }; La clase ClienteSocket hereda de QTcpSocket y encapsula el estado de un solo cliente. ClienteSocket::ClienteSocket(QObject *parent) : QTcpSocket(parent) { connect(this, SIGNAL(readyRead()), this, SLOT(leerCliente())); connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater())); siguienteTamaoBloque = 0; } En el constructor, establecemos las conexiones necesarias, y hacemos que la variable siguienteTamaoBloque sea igual a cero para indicar que todava no sabemos el tamao del bloque de datos que ha sido enviado por el cliente. La seal disconnected() est conectada con deleteLayer, un objeto una funcin heredada de QObject que elimina los objetos cuandop el control retorna al ciclo de eventos de Qt. Esto asgura que el objetoClienteSocket es eliminado cuando el socket de conexin sea cerrado. void ClienteSocket::leerCliente() { QDataStream entrada(this); entrada.setVersion(QDataStream::Qt_4_1); if (siguienteTamaoBloque == 0) { if (bytesAvailable() < sizeof(quint16)) return; entrada >> siguienteTamaoBloque; } if (bytesAvailable() < nextBlockSize) return; quint8 tipoSolicitud; QString desde; QString hacia; QDate fecha; QTime hora; quint8 flag; entrada >> tipoSolicitud; if (tipoSolicitud == S) { entrada >> desde >> hacia >> fecha >> hora >> flag; srand(desde.length() * 3600 + to.length() * 60 + hora.hour()); int numViajes = rand() % 8; for (int i = 0; i < numViajes; ++i) generarViajeAleatorio(desde, hacia, fecha, hora); QDataStream salida(this); salida << quint16(0xFFFF); }

14. Redes

173

close(); } El slot leerCliente() est conectado a la seal readyRead() de QTcpSocket. Si la variable siguienteTamaoBloque es igual a 0, empezamos con la lectura del tamao del bloque; de otra forma, quiere decir que ya lo hemos ledo, y en lugar de calcularlo verificamos si el bloque ha llegado completo. Una vez que un bloque completo se encuentra listo para ser ledo, lo leemos. Usamos el QDataStream directamente en el QTcpSocket (el objeto this) y leemos los archivos usando el operador >>. Una vez que hayamos ledo la solicitud del cliente, estaremos listos para generar una respuesta. Si esta fuera una aplicacin real, buscaramos la informacin en una respectiva base de datos y trataramos de encontrar viajes de trenes. Pero aqu nos ser suficiente con una funcin llamada generarViajeAleatorio() que generar un viaje aleatorio. Llamamos a la funcin un nmero aleatorio de veces, y luego enviamos el valor 0xFFFF para indicar el final de los datos. Al final, cerramos la conexin. void ClienteSocket::generarViajeAleatorio(const QString & /* desde */, const QString & /* hacia */, const QDate &fecha, const QTime &hora) { QByteArray bloque; QDataStream salida(&bloque, QIODevice::WriteOnly); salida.setVersion(QDataStream::Qt_4_1); quint16 duracion = rand() % 200; salida << quint16(0) << fecha << hora << duracion << quint8(1) << QString("InterCity"); salida.device()->seek(0); salida << quint16(bloque.size() - sizeof(quint16)); write(bloque); } La funcin generarViajeAleatorio() muestra cmo enviar un bloque de datos sobre conexiones TCP. Esto es muy similar a lo que hicimos en el cliente en la funcin enviarSolicitud(). Una vez ms, escribimos el bloque de datos a un QByteArray para que podamos determinar su tamao antes de enviarlo usando la funcin write(). int main(int argc, char *argv[]) { QApplication app(argc, argv); ServidorViaje servidor; if (!servidor.listen(QHostAddress::Any, 6178)) { cerr << "Fallo al enlazar con el puerto" << endl; return 1; } QPushButton botonQuitar(QObject::tr("&Quitar")); botonQuitar.setWindowTitle(QObject::tr("Servidor de Viaje")); QObject::connect(&botonQuitar, SIGNAL(clicked()),&app, SLOT(quit())); botonQuitar.show(); return app.exec(); } En el main(), creamos un objeto ServidorViaje y un QPushButton que le permitir al usuario detener el servidor. Iniciamos el servidor llamando a QTcpSocket::listen(), la cual toma la direccin IP y el nmero del puerto en el cual queremos aceptar conecciones. La direccin especial 0.0.0.0 (QHostAddress::Any) significa cualquier interfaz presente en el host local. Esto completa nuestro ejemplo de clientes-servidores. En este caso, hemos usado un protocolo orientado a bloques que nos permite usar la clase QDataStream para la lectura y la escritura. Si lo que queramos era usar un protocolo orientado a lneas, el mtodo ms simple podra haber sido usar las funciones

14. Redes

174

canReadLine() y readLine() de la clase QTcpSocket en un slot conectado a la seal readyRead(): QStringList lineas; while (tcpSocket.canReadLine()) lineas.append(tcpSocket.readLine()); Luego procesaramos cada lnea que ha sido leida. En lo que respecta a el envio de datos, este puede hacerse usando un QTextStream en el QTcpSocket. La implementacin del servidor que hemos usado no es muy escalable cuando existen demasiadas conexiones. El problema es que mientras estamos procesando una solicitud, no manejamos las otras conexiones. Un procedimiento ms escalable sera iniciar un nuevo hilo para cada conexin. El ejemplo Threaded Fortune Server ubicado en el directorio examples/network/threadedfortuneserver de Qt se ilustra cmo hacerlo.

Enviando y Recibiendo Datagramas UDP


La clase QUdpSocket puede ser usada para enviar y recibir datagramas UDP. UDP es un protocolo voluble orientado a datagramas. Algunos protocolos a nivel de aplicaciones usan UDP porque es ms ligero que TCP. Con UDP, los datos son enviados como paquetes (datagramas) de un host a otro. All, no existe el concepto de conexin, y si un paquete UDP no es entregado satisfactoriamente, ningn error es reportado al remitente. Veremos cmo usar UDP desde una aplicacin Qt a travs de los ejemplos Globo Climatolgico y Estacin Climatolgica. La aplicacin Globo Climatolgico imita a un globo climatolgico que enva datagramas UDP (presumiblemente usando una conexin inalmbrica) cada 2 segundos conteniendo las condiciones atmosfricas. La aplicacin Estacin Climatolgica recibe estos datagramas y los muestra en pantalla. Primero, comenzaremos revisando el cdigo de la aplicacin Globo Climtolgico. class GloboClimatologico : public QPushButton { Q_OBJECT public: GloboClimatologico(QWidget *parent = 0); double temperatura() const; double humedad() const; double altitud() const; private slots: void enviarDatagrama(); private: QUdpSocket udpSocket; QTimer cronometro; }; La clase GloboClimatologico hereda de QPushButton. Esta usa su variable privada QUdpSocket para comunicarse con la Estacin Climatolgica (la otra aplicacin). GloboClimatologico:: GloboClimatologico(QWidget *parent) : QPushButton(tr("Quitar"), parent) { connect(this, SIGNAL(clicked()), this, SLOT(close())); connect(&cronometro, SIGNAL(timeout()), this, SLOT(enviarDatagrama())); cronometro.start(2 * 1000); setWindowTitle(tr("Globo Climatolgico")); }

14. Redes

175

En el constructor, iniciamos un QTimer para invocar al slot enviarDatagrama() cada 2 segundos. void GloboClimatologico::enviarDatagrama() { QByteArray datagrama; QDataStream salida(&datagrama, QIODevice::WriteOnly); salida.setVersion(QDataStream::Qt_4_1); out << QDateTime::currentDateTime() << temperatura() << humedad() << altitude(); udpSocket.writeDatagram(datagrama, QHostAddress::LocalHost, 5824); } En enviarDatagrama(), generamos y enviamos un datagrama conteniendo la fecha, hora, temperatura, humedad y altitud actuales: QDateTime double double double Fecha y hora de medicin Temperatura en C Humedad en % Altitud en metros

El datagrama es enviado usando QUdpSocket::writeDatagram(). El segundo y tercer argumento a writeDatagram() son la direccin IP y el numero del puerto del peer (la Estacin Climatolgica) respectivamente. Para este ejemplo, asumimos que la Estacin Climatolgica est corriendo en la misma mquina que Globo Climatolgico, as que usamos una IP de 127.0.0.1 (QHostAddress::LocalHost), una direccin especial que designa el host local. A diferencia de las subclases de QAbstractSocket, QUdpSocket no acepta nombres de host, solo direcciones de host. Si queramos determinar la direccin IP a partir del nombre de host, tenemos dos opciones: si estamos preparados para bloquear la interfaz mientras la bsqueda se hace, podemos usar la funcin esttica QHostInfo::fromName(). De otra manera, podemos usar la funcin esttica QHostInfo::lookupHost(), la cual retorna inmediatamente y llama al slot que le es pasado con un objeto QHostInfo conteniendo las direcciones correspondientes cuando la bsqueda est completa. int main(int argc, char *argv[]) { QApplication app(argc, argv); GloboClimatologico globo; globo.show(); return app.exec(); } La funcin main() sencillamente crea un objeto GloboClimatologico, el cual sirve tanto como peer UDP como un QPushButton en pantalla. Haciendo click en el QPushButton, el usuario puede quitar la aplicacin. Ahora revisemos el cdigo fuente para el cliente Estacin Climatolgica. class EstacionClimatologica : public QDialog { Q_OBJECT public: EstacionClimatologica(QWidget *parent = 0); private slots: void procesarDatagramasPendientes(); private: QUdpSocket udpSocket; QLabel *labelFecha;

14. Redes

176

QLabel *labelHora; QLineEdit *lineEditAltitud; } La clase EstacionClimatologica hereda de QDialog. Esta escucha en un puerto UDP particular, analiza cada datagrama entrante (desde Globo Climatologico), y muestra su contenido en cinco QLineEdits de solo lectura. La nica variable privada de inters aqu es udpSocket de tipo QUdpSocket, la cual usaremos para recibir datagramas. EstacionClimatologica:: EstacionClimatologica (QWidget *parent) : QDialog(parent) { udpSocket.bind(5824); connect(&udpSocket, SIGNAL(readyRead()),this, SLOT(procesarDatagramasPendientes())); }

Figura 14.3. La aplicacin Estacin Climatolgica

En el constructor, comenzamos con la vinculacin del QUdpSocket al puerto en el que el globo climatolgico est transmitiendo. Ya que no hemos especificado una direccin de host, el socket aceptar datagramas enviados a cualquier direccin IP que pertenezca a la mquina en donde la aplicacin Estacin Climatologica se est ejecutando. Luego, conectamos la seal readyRead() del socket al slot privado procesarDatagramasPendientes() que extrae y muestra los datos. void EstacionClimatologica::procesarDatagramasPendientes() { QByteArray datagrama; do { datagrama.resize(udpSocket.pendingDatagramSize()); udpSocket.readDatagram(datagrama.data(), datagrama.size()); } while (udpSocket.hasPendingDatagrams()); QDateTime dateTime; double temperatura; double humedad; double altitud; QDataStream entrada(&datagrama, QIODevice::ReadOnly); entrada.setVersion(QDataStream::Qt_4_1); entrada >> dateTime >> temperatura >> humedad >> altitud; lineEditFecha->setText(dateTime.date().toString());

14. Redes

177

lineEditHora->setText(dateTime.time().toString()); lineEditTemperatura->setText(tr("%1 C").arg(temperatura)); lineEditHumedad->setText(tr("%1%").arg(humedad)); lineEditAltitud->setText(tr("%1 m").arg(altitud)); } El slot procesarDatagramasPendientes() es llamado cuando un datagrama ha llegado. QUdpSocket pone en cola los datagramas entrantes y nos permite acceder a ellos uno a la vez. Normalmente, Debera haber solo un datagrama, pero no podemos excluir la posibilidad de que el remitente podra enviar unos cuantos datagramas en una fila antes de que la seal readyRead() sea emitida. En ese caso, podemos ignorar todos los datagramas exceptuando al ltimo, ya que el anterior contiene condiciones atmosfricas obsoletas. La funcin pendingDatagramSize() retorna el tamao del primer datagrama pendiente. Desde el punto de vista de la aplicacin, los datagramas son enviados y recibidos siempre como una sola unidad de datos. Esto significa que si cualquier cantidad de bytes se encuentra disponible, un datagrama completo puede ser ledo. La llamada a readDatagram() copia el contenido del primer datagrama pendiente a un buffer char * especificado (truncando los datos si el buffer es muy pequeo) y avanza hasta el siguiente datagrama pendiente. Una vez que hemos ledo todos los datagramas, descomponemos el ultimo (el nico con las medidas atomosfericas ms recientes) en sus partes y llenamos los QLineEdits con los nuevos datos. int main(int argc, char *argv[]) { QApplication app(argc, argv); EstacionClimatologica estacion; estacion.show(); return app.exec(); } Finalmente en el main(), creamos y mostramos el objeto EstacionClimatologica. Ahora hemos terminado nuestro emisor y receptor UDP. Las aplicaciones son tan sencillas como es posible, con la operacin de envio de datagramas que hace la aplicacin Globo Climatolgico y la recepcin de estos por parte de la aplicacin Estacin Climatolgica. En la mayora de las aplicaciones del mundo real, ambas aplicaciones necesitaran la lectura y la escritura en su socket. A la funcin QUdpSocket::writeDatagram() se le puede pasar una direccin de host y un nmero de puerto, para que QUdpSocket pueda leer desde el host y el puerto recibido usando bind(), y escribir en algn otro host y puerto.

15. XML

178

15. XML

Leyendo XML con SAX Leyendo XML con DOM Escribiendo XML

XML (Extensible Markup Lenguage/ Lenguaje de Marcado Extendible) es un formato de archivo de texto de propsitos generales que es popular en el intercambio y almacenamiento de datos. Qt provee dos APIs diferentes para la lectura de documentos XML como parte de el modulo QtXml: SAX (Simple API for XML/ API Sensilla para XML) reporta eventos de anlisis de texto directamente a la aplicacin a travs de funciones virtuales. DOM (Document Object Model/ Modelo de Objeto de Documento) convierte un documento XML en tres estructuras, las cuales pueden ser navegadas por la aplicacin.

Existen muchos factores que deben tomarse en cuenta cuando se elige entre DOM y SAX para una aplicacin en particular. SAX es de ms bajo nivel y por tanto usualmente ms rpido, lo cual lo hace especialmente apropiado tanto para para tareas simples (como buscar todas las ocurrencias de una etiqueta dada en documento XML) y para leer archivos muy grandes que no quepan en memoria. Pero para muchas aplicaciones, la comodidad ofrecida por DOM es de mucho ms peso que las velocidad potencial y los beneficios de memoria de SAX. Para escribir archivos XML, tenemos dos opciones disponibles: Podemos generar el XML a mano, o podemos representar los datos como un rbol DOM en memoria y pedirle al rbol que se escriba en un archivo.

Leyendo XML con SAX


SAX es una API estndar, de dominio publico de hecho, para leer documentos XML. Las clases SAX de Qt son modeladas siguiendo de la implementacin de SAX2 en Java, con algunas diferencias en el nombrado para que encajen con las convenciones de Qt. Para ms informacin acerca de SAX, visite http://www.saxproject.org/. Qt proporciona un parseador (analizador sintctico o analizador de texto) XML sin validaciones basado en SAX llamado QXmlSimpleReader. Este analizador reconoce cdigo XML sano y soporta namespaces XML. Cuando el analizador recorrer el documento, llama a funciones virtuales en clases manejadoras registradas para indicar eventos de parseo (Estos eventos de parseo no estn relacionados con los eventos de Qt, como los eventos de teclado y ratn). Por ejemplo, supongamos que el parseador se encuentra analizando el siguiente documento XML: <doc> <quote>Ars longa vita brevis</quote> </doc>

15. XML

179

El parseador llamara a los siguientes manejadores de eventos de parseo: inicioDocumento() elementoInicio("doc") elementoInicio("quote") caracteres("Ars longa vita brevis") elementoFin("quote") elementoFin("doc") finDocumento() Las funciones mostradas anteriormente son declaradas en la clase QXmlContenthandler. Por simplicidad, hemos omitido algunos argumentos a elementoInicio() y elementoFin(). QXmlContentHandler es solo una de muchas clases que pueden ser usadas en conjunto con QXmlSimpleReader. Las otras son QXmlEntityResolver, QXmlDTDHandler, QXmlErrorHandler, QXmlDeclHandler y QXmlLexicalHandler. Estas clases solamente declaran funciones virtuales y proporcionan informacin acerca de los diferentes eventos de parseo. Para la mayora de las aplicaciones, QXmlContentHandler y QXmlErrorHandler son las nicas que se necesitarn. Por comodidad, Qt tambin provee la clase QXmlDefaultHandler, una clase que hereda de todos las clases manejadoras y que provee implementaciones triviales para todas las funciones. Este diseo, con muchas clases manejadoras abstractas y una subclase trivial, es inusual en Qt; esta fue adoptada para seguir, lo ms cerca posible, el modelo de la implementacin en Java. Veremos ahora un ejemplo que muestra cmo usar QXmlSimpleReader y QXmlDefaultHandler para parsear un formato de archivo XML y dibujar su contenido en un QTreeWidget. La subclase de QXmlDefaultHandler es llamada SaxHandler, y el formato que maneja es el de un ndice de un libro, con entradas y subentradas. Figura 15.1. Arbol de herencia de SaxHandler

Aqu se est el archivo ndice que se muestra en el QTreeWidget de la Figura 15.2: <?xml version="1.0"?> <inidicelibro> <entrada termino=espaciado"> <pagina>10</pagina> <pagina>34-35</pagina> <pagina>307-308</pagina> </entrada> <entrada termino=substraccion"> <entrada termino="de imagenes"> <pagina>115</pagina> <pagina>244</pagina> </entrada> <entrada termino="de vectores"> <pagina>9</pagina> </entrada> </entrada> </indicelibro>

15. XML

180

Figura 15.2. Un archivo de ndice de libro mostrado en un QTreeWidget

El primer paso para implementar el parseador es subclasificar a QXmlDefaultHandler: class SaxHandler : public QXmlDefaultHandler { public: SaxHandler(QTreeWidget *arbol); bool elementoInicio(const QString &namespaceURI, const QString &nombreLocal, const QString &qnombre, const QXmlAttributes &attributes); bool elementoFinal(const QString &namespaceURI, const QString &nombreLocal, const QString &qNombre); bool caracteres(const QString &str); bool fatalError(const QXmlParseException &excepcion); private: QTreeWidget *treeWidget; QTreeWidgetItem *itemActual; QString textoActual; }; El constructor de SaxHandler acepta el objeto QTreeWidget que queremos llenar con la informacin guardada en el archivo XML. bool SaxHandler::elementoInicio(const QString&/* namespaceURI*/, const QString & /*nombreLocal*/, const QString &qNombre, const QXmlAttributes &atributos) { if (qNombre == "entrada") { if (itemActual) { itemActual = new QTreeWidgetItem(itemActual); } else { itemActual = new QTreeWidgetItem(treeWidget); } itemActual->setText(0, attributes.value("termino")); } else if (qNombre == "pagina") { textoActual.clear(); } return true; } La funcin elementoInicio() es lamada cuando el lector encuentra una etiqueta de inicio. El tercer parmetro en el nombre de la etiqueta (o ms precisamente, su nombre calificativo). El cuarto parmetro es la lista de atributos. En este ejemplo, ignoramos los parmetros primero y segundo. Estos son utiles para aquellos archivos XML que usan el mecanismo de namespace de XML, una materia que se discute en detalle en la documentacin de referencia. En la etiqueta <entrada>, creamos un nuevo tem dentro del QTreeWidget. Si la etiqueta es anidada dentro de otra etiqueta <entrada>, la nueva etiquetas define una subentrada en el ndice, y el nuevo QTreeWidgetItem se crea como hijo del QTreeWidgetItem que representa la entrada superior o

15. XML

181

anidadora. Si la etiqueta no est anidada, entonces creamos el QTreeWidgetItem con el QTreeWidget como padre, hacindolo un tem de alto nivel. Llamamos a setText() desde el objeto itemActual para establecer el texto a ser mostrado en la columna 0 al valor del atributo termino de la etiqueta <entrada>. Si la etiqueta resulta ser <pagina>, hacemos que textoActual sea una cadena de texto vaca. La variable textoActual sirve como un acumulador para el texto ubicado entre la etiqueta <pagina> y la etiqueta </pagina>. Al final, retornamos true para decirle a SAX que continue analizando el archivo. Si queramos reportar errores al encontrar etiquetas desconocidas, podramos haber retornado false para esos casos. Luego reimplementariamos la funcin errorString() desde QXmlDefaultHandler para retornar el mensaje de error apropiado. bool SaxHandler::elementoFin(const QString & /* namespaceURI */, const QString & /* nombreLocal */, const QString &qNombre) { if (qNombre == "entrada") { itemActual = itemActual->parent(); } else if (qNombre == "pagina") { if (itemActual) { QString todasPaginas = itemActual->text(1); if (!todasPaginas.isEmpty()) todasPaginas += ", "; todasPaginas += textoActual; itemActual->setText(1, todasPaginas); } } return true; } La funcin elementoFin() es llamada cuando el lector encuentra una etiqueta de cierre. Como sucede con elementoInicio(), el tercer parmetro es el nombre de la etiqueta. Si la etiqueta encontrada es </entrada>, actualizamos la variable privada itemActual para que apunte al padre del QTreeWidgetItem actual. Esto nos asegura que la variable itemActual es restaurada al valor que tenia antes de que la correspondiente etiqueta <entrada> fue leida. La funcin fatalError() se llama cuando el lector falla al parsear el archivo XML. Si esto llegara a ocurrir, simplemente mostramos un mensaje, dando el numero de lnea, el numero de columna y el texto del error producido por el parseador. Esto completa la implementacin de la clase SaxHandler. Ahora veamos cmo podemos hacer uso de ella: bool parseArchivo(const QString &nombreArchivo) { QStringList labels; labels << QObject::tr("Terminos")<< QObject::tr("Paginas"); QTreeWidget *treeWidget = new QTreeWidget; treeWidget->setHeaderLabels(labels); treeWidget->setWindowTitle(QObject::tr("SAX Handler")); treeWidget->show(); QFile archivo(nombreArchivo); QXmlInputSource entradaFuente(&archivo); QXmlSimpleReader lector; SaxHandler handler(treeWidget); lector.setContentHandler(&handler); lector.setErrorHandler(&handler);

15. XML

182

return lector.parse(inputSource); } Primero, configuramos un QTreeWidget con dos columnas. Luego creamos un objeto QFile para el archivo que va a ser ledo y un QXmlSimpleReader para parsear el archivo. No necesitamos abrir el QFile por nuestra cuenta; ya que QXmlImputSource lo hace automticamente. Finalmente, creamos un objeto SaxHandler, lo instalamos sobre el lector como manejador de contenido y como manejador de errores, y llamamos a la funcin parse() sobre el leector para realizar el parseo. En lugar de pasar simplemente un objeto de archivo a la funcin parse(), pasamos un QXmlInputSource. Esta clase abre el archivo que se le da, lo lee (tomando en cuenta cualquier codificacin de caracteres especificada en la declaracin <?xml?>), y proporciona una interfaz a travs de la cual el parseador lee el archivo. En la clase SaxHandler, solamente reimplementamos las funciones de las clases QXmlContentHandler y QXmlErrorHandler. Si hubiramos implementado las funciones de otras clases manejadoras, habriamos necesitado llamar a sus correspondientes funciones seteadoras sobre el lector. Para enlazar la aplicacin con la librera QtXml, debemos agregar esta lnea al archivo .pro: QT += xml

Leyendo XML con DOM


DOM es una API estndar para el parseo (anlisis gramtico o sintctico) de XML desarrollado por el World Wide Web Consortium (W3C). Qt proporciona una implementacin sin validaciones de DOM Level 2 para la lectura, manipulacin y escritura de documentos XML. DOM representa un archivo XML como un rbol en la memoria. Podemos navegar a travs del rbol DOM tanto como queramos, y podemos modificarlo y guardarlo como un archivo XML. Consideremos el siguiente archivo XML: <doc> <cita>Ars longa vita brevis</cita> <traduccion>El arte es larga, la vida corta</traduccion> </doc> Este corresponde al siguiente rbol DOM: Document o Element (doc) Element (cita) Text (Ars longa vita brevis) Element (traduccion) Text (El arte es larga, la vida corta) El rbol DOM contiene nodos de diferentes tipos. Por ejemplo, un nodo Element corresponde a una etiqueta de inicio y su etiqueta de cierre correspondiente. El material que falla entre las etiquetas aparece como nodos hijos del nodo Element.

15. XML

183

En Qt, los tipos de nodo (como todas las otras clases relacionadas con DOM) tienen un prefijo QDom. As, QDomElement representa un nodo Element, y un QDomText representa un nodo Text. Los diferentes tipos de nodos pueden tener diferentes tipos de nodos hijos. Por ejemplo, un nodo Element puede contener otros nodos Element, y tambin nodos EntityReference, Text, CDATASection, ProcessingInstruction y Comment. La Figura 15.3 muestra cules nodos pueden tener cules tipos de nodos hijos. Los nodos en gris no pueden tener ningn hijo de su propio tipo. Figura 15.3. Relacion padre-hijo entre nodos DOM

Para ilustrar cmo usar DOM para la lectura de archivos XML, escribiremos un parseador para el formato del archivo de ndice del libro descrito en la seccin previa. class DomParser { public: DomParser(QIODevice *device, QTreeWidget *arbol); private: void parseEntry(const QDomElement &elemento, QTreeWidgetItem *parent); QTreeWidget *treeWidget; }; Definimos una clase llamada DomParser que ser la que realizar el parseo a un documento XML del ndice del libro y mostrar el resultado en un QTreeWidget. Esta clase no hereda de ninguna otra clase. DomParser::DomParser(QIODevice *device, QTreeWidget *arbol) { treeWidget = arbol; QString errorStr; int lineaError; int columnaError; QDomDocument doc; if (!doc.setContent(device, true, &errorStr, &lineaError, &columnaError)) { QMessageBox::warning(0, QObject::tr("DOM Parser"), QObject::tr("Parse error at line %1, " "column %2:\n%3").arg(lineaError) .arg(columnaError).arg(errorStr)); return; } QDomElement root = doc.documentElement();

15. XML

184

if (root.tagName() != "indicelibro") return; QDomNode nodo = root.firstChild(); while (!nodo.isNull()) { if (nodo.toElement().tagName() == "entrada") parsearEntrada(nodo.toElement(), 0); nodo = nodo.nextSibling(); } } En el constructor, creamos un objeto QDomDocument y llamamos a su mtodo setContent() para leer el documento XML proporcionado por el objeto QIODevice. La funcin setContent() abre automticamente el dispositivo (device) si este no ha sido abierto. Luego llamamos a documentElement() en el QDomDocument para obtener su nico hijo de tipo QDomElement, y verificamos que este sea un elemento <indicelibro>. Iteramos sobre todos los nodos hijos, y si el nodo es un elemento <entrada>, llamamos a parsearEntrada() para parsearlo. La clase QDomNode puede alojar cualquier tipo de nodo. Si queremos procesar un nodo adicional, primero debemos convertirlo al tipo de dato adecuado. En este ejemplo, solamente nos ocupamos de los nodos tipo Element, de manera que llamamos a toElement() en el QDomNode para convertirlo a QDomElement y luego llamar a tagName() para recuperar el nombre de la etiqueta del elemento. Si el nodo no es de tipo Element, la funcin toElement() retornar un objeto QDomElement nulo, con un nombre de etiqueta vaco. void DomParser::parsearEntrada(const QDomElement &elemento, QTreeWidgetItem *parent) { QTreeWidgetItem *item; if (parent) { item = new QTreeWidgetItem(parent); } else { item = new QTreeWidgetItem(treeWidget); } item->setText(0, elemento.attribute("termino")); QDomNode nodo = elemento.firstChild(); while (!nodo.isNull()) { if (nodo.toElement().tagName() == "entrada") { parsearEntrada(nodo.toElement(), item); } else if (nodo.toElement().tagName() == "pagina") { QDomNode nodoHijo = nodo.firstChild(); while (!nodoHijo.isNull()) { if (nodoHijo.nodeType() == QDomNode::TextNode) { QString pagina = nodoHijo.toText().data(); QString todasPaginas = item->text(1); if (!todasPaginas.isEmpty()) todasPaginas += ", "; todasPaginas += pagina; item->setText(1, todasPaginas); break; } nodoHijo = nodoHijo.nextSibling(); } } nodo = nodo.nextSibling(); } }

15. XML

185

En la funcin parsearEntrada(), creamos un tem QTreeWidget. Si la etiqueta est anidada dentro de otra etiqueta <entrada>, la nueva etiqueta define una subentrada en el ndice, y creamos el QTreeWidgetItem como un hijo del QTreeWidgetItem que representa a la entrada contenedora (en otras palabras, la entrada padre). Si la etiqueta no se encuentra anidada, creamos el QTreeWidgetItem con el QTreeWidget como su padre, hacindolo un tem de primer nivel. Luego llamamos a setText() para establecer el texto mostrado en la columna 0 para el valor del atributo termino de la etiqueta <entrada>. Una vez que hayamos inicializado el QTreeWidgetItem, iteramos sobre los nodos hijos del nodo QDomElement correspondiente a la etiqueta <entrada> actual. Si el elemento es <entrada>, llamamos a parsearEntrada() pasndole como segundo argumento el tem actual. El nuevo QTreeWidgetItem de la entrada ser creado con el QTreeWidgetItem que lo encierra, como su padre. Si el elemento es <pagina>, navegamos a travs de la lista de elementos hijos para buscar un nodo Text. Una vez que lo hayamos encontrado, llamamos a toText() para convertirlo a un objeto tipo QDomText y llamamos a data() para extraer el texto como un QString. Luego agregamos el texto a la lista de nmeros de pginas (que se encuentran separados por comas) en la columna 1 del QTreeWidgetItem. Ahora veamos cmo podemos usar la clase DomParser para parsear un archivo: void parsearArchivo(const QString &nombreArchivo) { QStringList labels; labels << QObject::tr("Terminos") << QObject::tr("Paginas"); QTreeWidget *treeWidget = new QTreeWidget; treeWidget->setHeaderLabels(labels); treeWidget->setWindowTitle(QObject::tr("DOM Parser")); treeWidget->show(); QFile archivo(nombreArchivo); DomParser(&archivo, treeWidget); } Comenzamos con la configuracionde un QTreeWidget. Luego creamos un QFile y un DomParser. Cuando el DomParser es construido, este parsea el archivo y llena el tree widget. Como en el ejemplo anterior, necesitamos la siguiente lnea en el archivo .pro de la aplicacin para enlazarlo con la librera QtXml: QT += xml Como ilustra el ejemplo, navegar a travs de un rbol DOM puede ser engorroso. Simplemente extrayendo el texto entre <pagina> y </pagina> requiere que la iteraccin sobre una lista de QDomNodes usando firstChild() y nextSibling(). Los programadores que usan mucho DOM tienden a escribir sus propias funciones de alto nivel para simplificar las operaciones ms ncesitadas y comunes, como la extraccin del texto entre las etiquetas de inicio y de cierre.

Ecribiendo XML
Existen, bsicamente, dos mtodos para generar archivos XML desde una aplicacin Qt: Podemos construir un rbol DOM y llamar al mtodo save() desde este. Podemos generar el XML a mano.

La eleccin entre alguno de estos dos mtodos muchas veces depende de si usamos SAX o DOM para la lectura de documentos XML.

15. XML

186

Aqu se encuentra un fragmento de cdigo que muestra cmo podemos crear un rbol DOM y escribirlo usando un QTextStream: const int Indent = 4; QDomDocument doc; QDomElement root = doc.createElement("doc"); QDomElement cita = doc.createElement("cita"); QDomElement traduccion = doc.createElement("traduccion"); QDomText latin = doc.createTextNode("Ars longa vita brevis"); QDomText espanol = doc.createTextNode("El arte es vida, la vida corta"); doc.appendChild(root); root.appendChild(cita); root.appendChild(traduccion); quote.appendChild(latin); translation.appendChild(espanol); QTextStream salida(&archivo); doc.save(salida, Indent); El segundo argumento a save() es el tamao de la identacion a usar. Un valor distinto de cero hace que el archivo sea ms fcil de leer. Aqu est la salida del archivo XML: <doc> <cita>Ars longa vita brevis</cita> <traduccion>El arte es larga, la vida corta</traduccion> </doc> Otro caso pudiera darse en aquellas aplicaciones que usan un rbol DOM como su estructura de datos principal. Estas aplicaciones normalmente leern los archivos XML usando DOM, luego modificarn el rbol DOM en memoria y finalmente llamarn a save() para convertir el rbol nuevamente en XML. Por defecto, QDomDocument::save() usa la codificacin UTF-8 para generar el archivo. Podemos usar cualquier otra codificacin colocando en el inicio del rbol DOM una declaracin XML como esta: <?xml version="1.0" encoding="ISO-8859-1"?> El siguiente segmento de cdigo nos muestra cmo hacerlo: QTextStream salida(&archivo); QDomNode xmlNode = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"ISO-8859-1\""); doc.insertBefore(xmlNode, doc.firstChild()); doc.save(salida, Indent); Generar los archivos XML a mano no es ms difcil que usar DOM. Podemos usar QTextStream y escribir las cadenas como lo haramos con cualquier otro archivo de texto. La parte ms tramposa y delicada es escapar de los caracteres especiales en los valores del texto y de los atributos. La funcin Qt::escape() escapa los caracteres <, > y &. Aqu est un cdigo que hace uso de ella: QTextStream salida(&archivo); salida.setCodec("UTF-8"); salida << "<doc>\n" << " <cita>" << Qt::escape(textoCita) << "</cita>\n" << " <traduccion>" << Qt::escape(textoTraduccion) << "</traduccion>\n" << "</doc>\n"; El articulo de Qt Quarterly llamado Generating XML, disponible en http://doc.trolltech.com/qq/qq05generating-xml.html, presenta una clase muy simple que facilita la generacin de archivos XML. Esta clase se ocupa de los detalles tales como caracteres especiales, indentacion, y asuntos de codificacin,

15. XML

187

permitindonos concentrarnos en el XML que queremos generar. La clase fue diseada para trabajar con Qt 3 pero es inecesesario portarlo a Qt 4.

16. Proporcionando Ayuda En Linea

188

16. Proporcionando Ayuda En Linea

Ayudas: Tooltips, Status Tips y Whats This? Usando QTextBrowser Como un Mecanismo de Ayuda Usando Qt Assistant como una Poderosa Ayuda En Linea

La mayora de las aplicaciones proveen a los usuarios de ayuda en lnea. Algunas ayudas son cortas, como son los tooltips, status tips y Whats This?. Naturalmente, Qt soporta todas estas. Otras ayudas pueden ser mucho ms extensas, involucrando varias pginas de texto. Para este tipo de ayudas, usted puede usar el QTextBrowser como un simple buscador de ayuda en lnea, o puede invocar el Qt Assistant o un Navegador HTML desde su aplicacin.

Ayudas: Tooltips, Status Tips, y Whats This?


Un tooltip es un pequeo fragmento de texto que aparece cuando el puntero del mouse pasa encima de un widget por un cierto periodo de tiempo. Los Tooltips son representados con el texto en negro sobre un fondo amarillo. Su uso fundamental es proveer una descripcin textual de botones en una barra de herramientas. Podemos agregar un tooltip a cualquier widget en el cdigo usando QWidget::setToolTip(). Por ejemplo: botonBuscar->setToolTip(tr("Buscar siguiente")); Para ponerle un tooltip a un QAction que pueda ser aadido a un men o una barra de herramientas, podemos simplemente llamar el mtodo setToolTip() de la accin. Por ejemplo: accionNuevo = new QAction(tr("&Nuevo"), this); accionNuevo->setToolTip(tr("Nuevo documento")); Si no le ponemos explcitamente un tooltip, el QAction usar el texto de la accin. Un status tip es tambin un corto fragmento descriptivo de texto, usualmente un poco ms largo que un tooltip. Cuando el puntero del mouse pasa encima de un botn de una barra de herramientas o una opcin de un men, un status tip aparece justo en la barra de estado. Para agregar un status tip a una accin o un widget use el mtodo setStatusTip(): accionNuevo->setStatusTip(tr("Crear un nuevo documento")); En algunas situaciones, es deseable brindar ms informacin acerca de un widget de la que puede ser dada por tooltips y status tips. Por ejemplo, se podra querer mostrar un dilogo complejo con texto que explique cada campo sin forzar al usuario a invocar una ventana de ayuda por separado. El modo Whats This? es la solucin ideal para esto. Cuando una ventana est en el modo Whats This?, el cursor cambia a y el usuario puede hacer click en cualquier componente de la interfaz de usuario para obtener su texto de ayuda.

16. Proporcionando Ayuda En Linea

189

Para entrar en el modo Whats This? el usuario puede hacer click en el botn ? en la barra de ttulo del dilogo (en Windows y KDE) o presionar Shift+F1. Figura 16.1. Una aplicacin mostrando un tooltip y un status tip

Este es un ejemplo de un texto Whats This? aplicado a un dilogo: dialog->setWhatsThis(tr("<img src=\":/images/icono.png\">" "&nbsp;El contenido del campo Origen depende " "del campo Tipo:" "<ul>" "<li><b>Libros</b> tienen una Editorial" "<li><b>Articulos</b> tienen un nombre de Diario con " "numero de volumen y asunto" "<li><b>Tesis</b> tienen un nombre de Institucin " "y un nombre de Departamento" "</ul>")); Podemos usar etiquetas HTML para dar formato al texto de un Whats This?. En el ejemplo, incluimos una imagen (la cual estaba listada en el archivo de recursos de la aplicacin), una lista con vietas, y algn texto en negrita. Las etiquetas y atributos que Qt soporta estn especificadas en http://doc.trolltech.com/4.1/richtext-html-subset.html. Cuando le ponemos un texto Whats This? a una accin, el texto ser mostrado cuando el usuario haga click en el elemento del men o botn de la barra de herramientas o presione el mtodo abreviado estando en modo Whats This?. Cuando los componentes de la ventana principal de una aplicacin proveen el texto Whats This?, puede ser incluida la opcin Whats This? en el men de Ayuda y su correspondiente botn en la barra de herramientas. Esto puede ser hecho creando una accin Whats This? usando la funcin esttica QWhatsThis::createAction() y agregando la accin que retorna al men de Ayuda y a la barra de herramientas. La clase QWhatsThis, adems, presenta funciones estticas para entrar y salir del modo Whats This?.

16. Proporcionando Ayuda En Linea

190

Figura 16.2. Un dialogo mostrando un texto de ayuda Whats This?

Usando QTextBrowser como un Mecanismo de Ayuda


Las aplicaciones grandes pueden necesitar de ms ayuda en lnea que la ayuda que los tooltips, status tips y Whats This? pueden mostrar. Una simple solucin a esto es brindar un buscador de ayuda. Las aplicaciones que incluyen un buscador de ayuda tpicamente tienen un vnculo a la Ayuda en el men Ayuda en la ventana principal y un botn de Ayuda en cada dilogo. En esta seccin, presentaremos un simple buscador de ayuda que se muestra en la Figura 16.3 y explicaremos cmo puede ser usado dentro de una aplicacin. La ventana usa un QTextBrowser para mostrar las pginas de ayuda que estn usando una sintaxis basada en HTML. Un QTextBrowser puede manipular una gran cantidad de etiquetas HTML, por eso es ideal para este propsito. Empezaremos con el archivo de cabecera: #include <QWidget> class QPushButton; class QTextBrowser; class BuscadorAyuda : public QWidget { Q_OBJECT public: BuscadorAyuda(const QString &ruta, const QString &pagina, QWidget *parent = 0); static void mostrarPagina(const QString &page); private slots: void actualizarTituloVentana(); private: QTextBrowser *textoBuscador; QPushButton *botonInicio; QPushButton *botonAtras; QPushButton *botonCerrar; };

16. Proporcionando Ayuda En Linea

191

La clase BuscadorAyuda provee una funcin esttica que puede ser llamada desde cualquier parte de la aplicacin. Esta funcin crea una ventana BuscadorAyuda y muestra la pgina dada. Figura 16.3. El widget BuscadorAyuda

Aqu est el principio de la implementacin: #include <QtGui> #include "buscadorayuda.h" BuscadorAyuda::BuscadorAyuda(const QString &path, const QString &page,QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_GroupLeader); textoBuscador = new QTextBrowser; botonInicio = new QPushButton(tr("&Inicio")); botonAtras = new QPushButton(tr("&Atras")); botonCerrar = new QPushButton(tr("Cerrar")); botonCerrar->setShortcut(tr("Esc")); QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addWidget(botonInicio); buttonLayout->addWidget(botonAtras); buttonLayout->addStretch(); buttonLayout->addWidget(botonCerrar); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addLayout(buttonLayout); mainLayout->addWidget(textoBuscador); setLayout(mainLayout); connect(botonInicio, SIGNAL(clicked()), textoBuscador, SLOT(home())); connect(botonAtras, SIGNAL(clicked()),textoBuscador, SLOT(backward())); connect(botonCerrar, SIGNAL(clicked()), this, SLOT(close())); connect(textoBuscador, SIGNAL(sourceChanged(const QUrl &)),

16. Proporcionando Ayuda En Linea

192

this, SLOT(actualizarTituloVentana())); textoBuscador->setSearchPaths(QStringList()<< ruta<< ":/imagenes"); textoBuscador->setSource(pagina); } Ponemos el atributo Qt::WA_GroupLeader porque queremos mostrar la ventana BuscadorAyuda desde dilogos modales adems de la ventana principal. Los dilogos modales normalmente previenen la interaccin del usuario con cualquier otra ventana en la aplicacin. Sin embargo, despus de llamar la ayuda, obviamente se le debe permitir al usuario interactuar con el dilogo modal y con el buscador de ayuda. Activar el atributo Qt::WA_GroupLeader hace esta interaccin posible. Proveemos dos rutas de bsqueda, la primera, una ruta en el sistema de archivos que contiene la documentacin de la aplicacin, y la segunda, la localizacin de los recursos de imgenes. El HTML puede incluir referencias a imgenes en el sistema de archivos en la forma normal y adems referencias a recursos de imgenes usando una ruta que comienza con :/ (dos punto slash). El parmetro pagina, es el nombre del archivo de documentacin, con un ancla HTML opcional. void BuscadorAyuda::actualizarTituloVentana() { setWindowTitle(tr("Ayuda: %1"). arg(textoBuscador-> documentTitle())); } Siempre que la pgina fuente cambia, el slot actualizarTituloVentana() es llamado. La funcin documentTitle() retorna el texto especificado en la etiqueta de la pgina <title>. void BuscadorAyuda::mostrarPagina(const QString &pagina) { QString ruta = QApplication::applicationDirPath() + "/doc"; BuscadorAyuda *buscador = new BuscadorAyuda(ruta, pagina); buscador->resize(500, 400); buscador->show(); } En la funcin esttica mostrarPagina(), creamos la ventana BuscadorAyuda y luego la mostramos. La ventana ser destruida automticamente cuando el usuario la cierre, al ser puesto el atributo Qt::WA_DeleteOnClose en el constructor de BuscadorAyuda. Para este ejemplo, asumimos que la documentacin se encuentra en el subdirectorio doc del directorio que contiene el ejecutable de la aplicacin. Todas las pginas pasadas a la funcin mostrarPagina() sern tomadas desde ese subdirectorio. Ahora estamos listos para invocar el buscador de ayuda desde la aplicacin. En la ventana principal de la aplicacin, podemos crear una accin Ayuda y conectarla a un slot ayuda() que se muestra a continuacin: void MainWindow::ayuda() { BuscadorAyuda::mostrarPagina("index.html"); } Esto asume que el archivo principal de la ayuda es llamado index.html. Para los dilogos, podemos conectar el botn Ayuda al slot ayuda() de la siguiente forma: void DialogoEntrada::help() { BuscadorAyuda::mostrarPagina("forms.html#editando"); }

16. Proporcionando Ayuda En Linea

193

Aqu podemos buscar en diferentes archivos de ayuda, forms.html, y navegar en el QTextBrowser hasta el ancla editando, que lleva al la seccin de edicin.

Usando el Qt Assistant como una Poderosa Ayuda En Lnea


El Qt Assistant es una aplicacin redistribuible de ayuda en lnea suministrada por Trolltech. Sus principales virtudes son que puede indexar, hacer bsqueda de texto y puede manejar conjuntos de documentacin para mltiples aplicaciones. Para hacer uso del Qt Assistant, debemos incorporar el cdigo necesario en nuestra aplicacin, y debemos responsabilizar al Qt Assistant de nuestra documentacin. La comunicacin entre una aplicacin de Qt y el Qt Assistant es manejada por la clase QAssistantClient, la cual se encuentra en una librera por separado. Para enlazar esta librera con una aplicacin, debemos aadir la siguiente lnea al archivo .pro de la aplicacin: CONFIG += assistant Ahora veamos el cdigo de una nueva clase BuscadorAyuda que usa el Qt Assistant. #ifndef BUSCADORAYUDA_H #define BUSCADORAYUDA_H class QAssistantClient; class QString; class BuscadorAyuda { public: static void mostrarPagina(const QString &pagina); private: static QAssistantClient *asistente; }; #endif Aqu est el nuevo archivo buscadorayuda.cpp: #include <QApplication> #include <QAssistantClient> #include "buscadorayuda.h" QAssistantClient *BuscadorAyuda::asistente = 0; void BuscadorAyuda::mostrarPagina(const QString &pagina) { QString ruta = QApplication::applicationDirPath() + "/doc/" + pagina; if (!asistente) asistente = new QAssistantClient(""); asistente->showPage(ruta); } El constructor de QAssistantClient acepta una cadena ruta como primer argumento, el cual es usado para localizar el ejecutable del Qt Assistant. Pasando una ruta vaca, significa que QAssistantClient debe buscar el ejecutable en la variable de entorno PATH. QAssistantClient tiene una funcin showPage() que acepta un nombre de una pgina con un ancla HTML opcional.

16. Proporcionando Ayuda En Linea

194

El prximo paso es preparar la tabla de contenidos y un ndice para la documentacin. Esto se hace creando un perfil Qt Assistant y escribiendo un archivo .dcf que provee informacin acerca de la documentacin. Todo eso es explicado en la documentacin en lnea del Qt Assistant, as que no vamos a duplicar esa informacin aqu. Una alternativa a usar QTextBrowser o Qt Assistant es usar mtodos especficos de cada plataforma para crear ayudas en lnea. Para las aplicaciones de Windows, puede ser deseable crear archivos de ayuda HTML de Windows para proveer acceso a ellos usando el Microsoft Internet Explorer. Usted puede usar la clase QProcess o el framework ActiveQt para esto. Para aplicaciones X11, un mtodo adecuado sera proveer archivos HTML y ejecutar un navegador web usando QProcess. En Mac OS X, Apple Help provee funcionalidades similares al Qt Assistant. Hemos alcanzado el final de la Parte II. Los captulos que siguen en la Parte III cubren caractersticas ms avanzadas y especializadas de Qt. El C++ y el cdigo de Qt que se presentan all no es ms difcil que el visto en la Parte II, pero algunos de los conceptos e ideas pueden ser ms desafiantes en aquellas reas que sean nuevas para usted.

195

Parte III Qt Avanzado

17. Internacionalizacin

196

17. Internacionalizacin

Trabajando con Unicode Haciendo Aplicaciones que Acepten Traducciones Cambio Dinmico del Lenguaje Traduciendo Aplicaciones

Adicionalmente al alfabeto Latino usado para el ingls y para muchos otros lenguajes europeos, Qt 4 tambin provee soporte para el resto de los sistemas de escritura del mundo: Qt usa el Unicode en toda la API e internamente. No importa qu lenguaje usemos para la interfaz de usuario, la aplicacin puede soportar a todos los usuarios del mismo modo. Los prximos motores de Qt pueden manejar la gran mayora de los sistemas de escrituras no Latinos, incluyendo el rabe, Chino, Cirlico, Hebreo, Japons, Coreano, Tailands y los lenguajes ndicos. Los motores de Layout de Qt soportan la distribucin de los elementos de la aplicacin de derecha a izquierda para lenguajes como el rabe y el hebreo. Ciertos lenguajes requieren mtodos de entrada especiales para introducir texto. Los widgets de edicin, como el QLineEdit y el QTextEdit trabajan bien con cualquier mtodo de entrada instalado en el sistema del usuario.

A menudo, no basta solo con permitirles a los usuarios ingresar texto en su idioma nativo; toda la interfaz de usuario debe estar traducida igualmente. Qt facilita esta tarea: Simplemente envuelve todas las cadenas de texto con la funcin tr() (como lo hemos hecho en captulos anteriores) y usa las herramientas de soporte para preparar los archivos de traduccin en el lenguaje requerido. Qt provee una herramienta GUI llamada Qt Linguist para ser usada por traductores. Qt Linguist est complementado por dos programas de comandos, que son: lupdate y lrelease, los cuales son generalmente ejecutados por los desarrolladores de la aplicacin. Para la mayora de las aplicaciones, un archivo de traduccin es cargado al iniciar, basado en las configuraciones locales del usuario. Pero en unos cuantos casos, es tambin necesario para los usuarios, que puedan cambiar el lenguaje de la aplicacin en tiempo de ejecucin. Esto es perfectamente posible hacerlo con Qt, aunque requiera un poquito ms de trabajo. Y gracias al sistema de Layouts de Qt, los distintos componentes de la interfaz de usuario se ajustarn automticamente para hacer espacio al texto traducido, cuando este sea ms largo que el texto original.

17. Internacionalizacin

197

Trabajando con Unicode


Unicode es un estndar de codificacin de caracteres que soporta la mayora de los sistemas de escritura del mundo. La idea original detrs de Unicode es que, se usen 16 bits para almacenar caracteres en vez de 8 bits, eso hara posible codificar alrededor de 65000 caracteres en vez de solo 256. [*] Unicode contiene la ASCII y la ISO 8856-1 (Latin-1) como subconjuntos en las mismas posiciones de cdigo. Por ejemplo, el caracter A tiene un valor 0x41 en ASCII, Latin-1 y Unicode, y el caracter tiene un valor 0xD1 en ambos: en Latin1 y en Unicode.
[*] Las versiones recientes del estndar Unicode, asigna valores por encima de 65535 a los caracteres. Esos caracteres pueden ser representados usando secuencias de dos valores de 16-bits llamados surrogate pairs, que en espaol significa pares sustitos.

La clase QString de Qt, aloja cadenas de caracteres como Unicode. Cada caracter en un QString es un QChar de 16-bits, en lugar de un char de 8-bits. Aqu hay dos maneras de configurar el primer caracter de una cadena de caracteres con el caracter A: str [0] = A; str [o] = QChar (0x41); Si el archivo fuente est codificado en Latin-1, especificar caracteres Latin-1 es as de fcil: str [0] = ; Y si el archivo fuente posee otra codificacin, el valor numrico funciona bien: str [0] = QChar (0xD1); Podemos especificar cualquier caracter Unicode por su valor numrico. Por ejemplo, aqu est cmo especificar la letra capital griega sigma () y el smbolo del euro (): str [0] = QChar (0x03A3); str [0] = QChar (0x20AC); Los valores numricos de todos los caracteres soportados por Unicode estn listados en la siguiente direccin web: http://www.unicode.org/standar/. Si raramente necesitas caracteres que no sean Latin-1, mirar los caracteres va online es suficiente; pero Qt provee maneras ms convenientes de introducir cadenas de texto Unicode en un programa hecho con Qt, como veremos ms tarde en esta seccin. El motor de texto de Qt 4 soporta los siguientes sistemas de escritura en todas las plataformas: aravico, chino, cirlico, griego, hebreo, japons, coreano, lao, latn, tailands y vietnamita. Tambin soporta todos los scripts Unicode 4.1 que no requieren ningn procesamiento especial. Adicionalmente, los siguientes sistemas de escrituras son soportados en X11 con Fontconfig y en las versiones recientes de Windows: bengal, devanagari, gujarati, gurumuji, canars, jemer, malabar, syriac, tamil, telugu, thaana (dhivelhi), y el tibetano. Finalmente, el oriya es soportado en X11, y el mongol y el sinhala estn soportados en Windows XP. Asumiendo que los mtodos apropiados estn instalados, los usuarios sern capaces de ingresar textos que usen alguno de estos sistemas de escritura en sus aplicaciones Qt. Programar con un QChar es un tanto diferente a programar con un char. Para obtener el valor numrico de un QChar, se llama a la funcin unicode() perteneciente a este. Para obtener el valor ASCII o Latin-1 de un QChar (como un char), se llama a la funcin toLatin1(). Para caracteres que no son del sistema Latin-1, la funcin toLatin1() retorna el caracter \0. Si sabemos que todas las cadenas de texto en un programa son ASCII, podemos usar las funciones estndar de la cabecera <cctype> como la funcin isalpha(), isdigit(), y isspace() en el valor de retorno de toLatin1(). Sin embargo, generalmente es bueno usar funciones miembros QChar para

17. Internacionalizacin

198

realizar con eficiencia estas operaciones, dado que ellas funcionarn para cualquier caracter Unicode. Las funciones que provee QChar incluyen: isPrint(), isPunct(), isSpace(), isMark(), isLetter(), isNumber(), isLetterOrNumber(), isDigit(), isSymbol(), isLower() e isUpper(). Por ejemplo, aqu se muestra una manera de chequear si un caracter es un dgito o una letra mayscula: if(ch.isDigit()||ch.isUpper()) El cdigo de arriba funciona para cualquier alfabeto donde se distinga entre maysculas y minsculas, incluyendo el latn, el griego y el cirlico. Una vez que tengamos una cadena de texto Unicode, podemos usarla en cualquier parte de la API de Qt donde se espere un QString. Es entonces, responsabilidad de Qt el mostrarla apropiadamente y convertirla en las codificaciones relevantes al comunicarse con el sistema operativo. Se necesita especial atencin y cuidado cuando leamos y escribamos archivos de texto. Los archivos de texto pueden usar una gran variedad de codificaciones, y es a menudo imposible adivinar o suponer la codificacin de un archivo de texto por su contenido. Por defecto, QTextStream usa la codificacin de 8-bits local del sistema (disponible como QTextCodec::codecForLocale()) tanto para la lectura como para la escritura. Para las localidades Americanas y de Europa Occidental, esto representa, usualmente, el Latin-1. Si diseamos nuestro propio formato de archivo y queremos ser capaces de leer y escribir arbitrariamente caracteres Unicode, podemos guardar los datos como Unicode llamando a las funciones stream.setCodec(UTF-16); stream.setGenerateByOrderMark(true); antes de empezar a escribir al QTextStream. Los datos sern por lo tanto guardados en formato UTF16, un formato que requiere de dos bytes por caracter, y tendr como prefijo un valor especial de 16-bit (la marca de orden de bytes de Unicode, 0xFFFE) identificando que el archivo est en Unicode y tambin si los bytes estn en orden llittle endian o en orden big endian (vase Endiannes en el Glosario de Trminos). El formato UTF-16 es idntico a la representacin de memoria de un QString, as que, leer y escribir cadenas de texto Unicode en UTF-16, puede ser muy rpido. Por otro lado, existe una sobrecarga o saturacin inherente cuando se guardan datos en ASCII puro en formato UTF-16, puesto que este guarda dos bytes por cada caracter, en lugar de solo uno. Otras codificaciones pueden ser especificadas mediante la llamada a setCodec() con un QTextCodec apropiado. Un QTextCodec es un objeto que hace conversiones entre Unicode y una codificacin dada. Qt usa QTextCodecs en una variedad de contextos. Internamente, son usados para dar soporte a las fuentes, a mtodos de entrada, al portapapeles, al arrastrar y soltar y en nombres de archivos. Pero estn de igual forma disponibles para nosotros cuando programemos nuestras aplicaciones con Qt. Al momento de leer un archivo de texto, si el archivo empieza con la marca de orden de bytes, QTextStream detecta el formato UTF-16 automticamente. Esta funcionalidad o comportamiento puede ser desactivada a travs de la llamada a setAutoDetectUnicode(false). Si los datos estn en formato UTF-16, pero no puede sobreentenderse con la marca de orden de bytes al iniciar, resulta mejor llamar a setCodec() con UTF-16 antes de leer. Otras codificaciones que soportan en toda su extensin a Unicode es el formato UTF-8. Su principal ventaja sobre UTF-16 es que es un sper conjunto del sistema ASCII. Cualquier caracter en el rango 0x00 hasta 0x7F es representado como un solo byte. Otros caracteres, incluyendo caracteres Latin-1 por encima de 0x7F, son representados por medio de secuencias de bytes mltiples. Para textos que son mayormente ASCII, el formato UTF-8 ocupa aproximadamente la mitad del espacio consumido por el formato UTF-16.

17. Internacionalizacin

199

Para usar el formato UTF-8 con QTextStream, hay que llamar a setCodec() con UTF-8 como el nombre del cdec antes de leer o escribir Si siempre queremos leer y escribir en Latn-1, independientemente de la configuracin regional del usuario, podemos establecer el cdec ISO 8859-1 en el QTextStream. Por ejemplo: QTextStream in(&archivo); in.setCodec(ISO 8859-1); Algunos formatos de archivos especifican su codificacin en sus cabeceras. Generalmente, la cabecera est en ASCII puro, para asegurarse de que el archivo es ledo correctamente sin importar qu codificacin es usada (asumiendo que es un sper conjunto de ASCII). Con respecto a esto, el formato de archivo XML es un ejemplo interesante. Los archivos XML normalmente son codificados como UTF-8 o UTF-16. La manera apropiada para leerlos es llamar a la funcin setCodec() con UTF-8 como argumento. Si el formato es UTF-16, QTextStream lo detectar automticamente y se ajustar por s mismo. La cabecera <?xml?> de un archivo XML algunas veces contiene un argumento de codificacin, por ejemplo: <?xml versin=1.0 encoding=EUC-KR?> Ya que QTextStream no permite que modifiquemos la codificacin una vez que se ha iniciado la lectura, la forma correcta de respetar una codificacin explicita es empezar a leer el archivo renovado, usando el cdec correcto (obtenido de QTextCodec::codecForName()). En el caso de XML, podemos evitar tener que manejar nosotros mismos la codificacin mediante el uso de las clases XML de Qt, que se describen en el Captulo 15. Otro uso para QTextCodec es especificar la codificacin de cadenas de texto que se producen en el cdigo fuente. Consideremos, por ejemplo, un equipo de programadores japoneses quienes se encuentran escribiendo una aplicacin dirigida, principalmente, al mercado domstico de Japn. Estos programadores pueden escribir su cdigo fuente en un editor de texto que use una codificacin como EUC-JP o Shift-JIS. De manera que, un editor les permite escribir en caracteres japoneses a la perfeccin para que puedan escribir cdigo como este: QPushButton *button = new QPushButton (tr( ));

Por defecto, Qt interpreta los argumentos de tr() como Latin-1. Para cambiar esto, hay que hacer un llamado a la funcin esttica QTextCodec::setCodecForTr(). Por ejemplo: QTextCodec::setCodecForTr(QTextCodec::codecForName(EUC-JP)); Esto debe hacerse antes de la primera llamada a tr(). Tpicamente, haremos esto en el main(), inmediatamente despus de que el objeto QCoreApplication o QApplication sea creado. Otras cadenas de texto especificadas en el programa sern interpretadas como cadenas Latin-1. Si el programador quiere ingresar caracteres Japoneses en estas, de igual forma pueden convertirlas explcitamente a Unicode usando un QTextCodec: QString text = japaneseCodec->toUnicode( );

Alternativamente, tambin pueden decirle a Qt que use un cdec especifico cuando se hace una conversin entre const char* y QString a travs de la llamada a QtextCodec::setCodecForCStrings(): QTextCodec::setCodecForCStrings(QTextCodec::codecForName("EUC-JP")); Las tcnicas descritas aqu pueden ser aplicadas a cualquier lenguaje que no sea Latin-1, incluyendo el chino, el griego, el coreano y el ruso. Aqu hay una lista de las codificaciones soportadas por Qt 4: Apple Roman Big5 Big5-HKSCS EUC-JP

17. Internacionalizacin

200

EUC-KR GB18030-0 IBM 850 IBM 866 IBM 874 ISO 2022-JP ISO 8859-1 ISO 8859-2 ISO 8859-3 ISO 8859-4 ISO 8859-5 ISO 8859-6 ISO 8859-7 ISO 8859-8 ISO 8859-9 ISO 8859-10 ISO 8859-13 ISO 8859-14 ISO 8859-15 ISO 8859-16 Iscii-Bng Iscii-Dev Iscii-Gjr Iscii-Knd Iscii-Mlm Iscii-Ori Iscii-Pnj Iscii-Tlg Iscii-Tml JIS X 0201 JIS X 0208 KOI8-R KOI8-U MuleLao-1 ROMAN8 Shift-JIS TIS-620 TSCII UTF-8 UTF-16 UTF-16BE UTF-16LE Windows-1250 Windows-1251 Windows-1252 Windows-1253 Windows-1254 Windows-1255 Windows-1256 Windows-1257 Windows-1258 WINSAMI2 Para todas estas, QTextCodec::codecForName() siempre retornar un puntero vlido. Otras codificaciones pueden soportarse usando una subclase de QTextCodec.

17. Internacionalizacin

201

Haciendo Aplicaciones que Acepten Traducciones


Si queremos hacer que nuestras aplicaciones estn disponibles en mltiples lenguajes, debemos hacer dos cosas: Asegurarnos de que cada cadena de texto visible para el usuario pase por la funcin tr(). Leer un archivo de traduccin (.qm) al iniciar la aplicacin.

Ninguno de estos pasos es necesario para las aplicaciones que nunca necesitarn ser traducidas. Sin embargo, usar la funcin tr() casi no requiere esfuerzo y deja la posibilidad de hacer traducciones a la aplicacin en otro momento. La funcin tr() es una funcin esttica definida en QObject y sobrepuesta en cada subclase definida con el macro Q_OBJECT. Cuando se escribe cdigo dentro de una subclase de QObject, podemos llamar a la funcin tr() sin formalidad. Una llamada a tr() retorna una traduccin del texto, si se encuentra disponible; de otra forma, el texto original es retornado. Para preparar los archivos de traduccin, debemos ejecutar la herramienta de Qt lupdate. Esta herramienta, extrae todas las cadenas de texto que aparecen en la las llamadas a la funcin tr() y produce archivos de traduccin que contienen todas esas cadenas de texto listas para ser traducidas. Luego, los archivos pueden ser enviados a un traductor para tener las traducciones aadidas. Este proceso es explicado en la seccin Traduciendo Aplicaciones, ms adelante en este captulo. Una llamada a la funcin tr() tiene la siguiente sintaxis general: Context::tr(textoFuente, comentario) La parte que dice Context, es el nombre de una subclase QObject definida con el macro Q_OBJECT. No necesitamos identificarlo si llamamos a la funcin tr() desde una funcin miembro de la clase en cuestin. La parte que dice textoFuente es la cadena de texto que necesita ser traducida. La parte que dice comentario es opcional; puede ser usada para proveer informacin adicional al traductor. Aqu estn unos cuantos ejemplos: RockyWidget::RockyWidget(QWidget *parent) : QWidget(parent) { QString str1 = tr("Carta"); QString str2 = RockyWidget::tr("Carta"); QString str3 = SnazzyDialog::tr("Carta"); QString str4 = SnazzyDialog::tr("Carta", " tamao de papel"); } Las primeras dos llamadas a tr() tienen el texto RockyWidget como su contexto, y las ltimas dos llamadas tienen el texto SnazzyDialog. Las cuatro tienen el texto Carta como texto fuente. La ltima llamada tambin tiene un comentario para ayudar al traductor a entender el significado del texto fuente. Las cadenas de texto en contextos diferentes (clases), son traducidas independientemente de las dems. Los traductores trabajan tpicamente con un solo contexto a la vez, a menudo con la aplicacin ejecutndose y mostrando el widget o el dilogo a ser traducido. Cuando llamamos a tr() desde una funcin global, debemos especificar el contexto explcitamente. Cualquier subclase de QObject en la aplicacin puede ser usada como el contexto. Si ninguna es apropiada, siempre podemos optar por usar el mismo QObject. Por ejemplo:

17. Internacionalizacin

202

int main(int argc, char *argv[]) { QApplication app(argc, argv); ... QPushButton boton(QObject::tr("Hola Qt!")); boton.show(); return app.exec(); } En cada ejemplo que hemos visto hasta ahora, el contexto ha sido el nombre de una clase. Esto resulta conveniente, porque, casi siempre, podemos omitirlo, pero este no tiene que ser el caso. La manera ms general de traducir una cadena de texto en Qt, es usar la funcin QCoreApplication::translate(), la cual acepta hasta tres argumentos: el contexto, el texto a traducir (texto fuente), y el comentario opcional. Por ejemplo, he aqu otra forma de traducir Hola Qt!: QCoreApplication::translate("Cosas Globales", "Hola Qt!") Esta vez, colocamos el texto en el contexto Cosas Globales. La funciones tr() y translate()tienen un doble uso: sirven de marcadores que lupdate usa para encontrar las cadenas de texto visibles al usuario, y al mismo tiempo son funciones de C++ que traducen texto. Esto tiene un gran impacto en cmo escribimos nuestro cdigo. Por ejemplo, el cdigo siguiente no funcionar: // MAL const char *NombreApp = "OpenDrawer 2D"; QString traducido = tr(NombreApp); El problema aqu es que lupdate no ser capaz de extraer la cadena textual OpenDrawer 2D, porque no aparece dentro de la llamada a tr(). Esto quiere decir que el traductor no tendr la oportunidad de traducir la cadena de texto. Este error suele presentarse cuando se manejan cadenas de texto dinmicas. // MAL statusBar()->showMessage(tr("Host "+ NombreHost +" encontrado")); Aqu, la cadena que pasamos a tr() vara dependiendo del valor de NombreHost, de manera que no podemos esperar que tr() lo traduzca correctamente. La solucin es usar QString::arg(): statusBar()->showMessage(tr("Host %1 encontrado").arg(NombreHost)); Ntese cmo trabaja: La cadena de texto Host %1 encontrado es pasada a tr(). Asumiendo que se ha cargado un archivo de traduccin al francs, tr() retornara algo como "Hte %1 trouv". Luego el parmetro %1 es reemplazado por el contenido de la variable NombreHost. Aunque generalmente es poco aconsejable llamar a tr() delante de una variable, es posible hacer que funcione. Debemos usar el macro QT_TR_NOOP() para marcar las cadenas de texto para la traduccin despus de que las asignemos a una variable. Esto es til mayormente para arreglos estticos de cadena de texto. Por ejemplo: void FormOrdenar::init()//formulario ordenar { static const char * const flores[] = { QT_TR_NOOP("Medio Tallo Rosas Rosadas"), QT_TR_NOOP("Una Docena Rosas Embaladas"), QT_TR_NOOP("Orqudea Calipso"), QT_TR_NOOP("Buqu Rosas Rojas Secas"), QT_TR_NOOP("Buqu Peonias Mixtas"), 0 };

17. Internacionalizacin

203

for (int i = 0; flores[i]; ++i) comboBox->addItem(tr(flores[i])); } El macro QT_TR_NOOP() simplemente retorna sus argumentos. Pero lupdate extraer todas las cadenas de texto envueltas en el QT_TR_NOOP() de modo que puedan ser traducidas. Cuando se use la variable luego, podemos llamar a tr() para realizar la traduccin como de costumbre. Aun cuando pasemos una variable a tr(), la traduccin seguir funcionando. Existe tambin el macro QT_TRANSLATE_NOOP() que funciona como el macro QT_TR_NOOP() pero adems soporta un contexto. Este macro es muy til cuando inicializamos variables afuera de una clase: static const char * const flores[] = { QT_TRANSLATE_NOOP("FormOrdenar", "Medio Tallo Rosas Rosadas"), QT_TRANSLATE_NOOP("FormOrdenar", "Una Docena Rosas Embaladas"), QT_TRANSLATE_NOOP("FormOrdenar", "Orqudea Calipso"), QT_TRANSLATE_NOOP("FormOrdenar", "Buqu Rosas Rojas Secas"), QT_TRANSLATE_NOOP("FormOrdenar", "Buqu Peonias Mixtas"), 0 }; El argumento contexto debe ser el mismo que el contexto que le demos luego en tr() o translate(). Cuando comenzamos a usar la funcin tr() en una aplicacin, es muy fcil olvidarse de encerrar algunas cadenas de texto visibles al usuario con una llamada a tr(), especialmente cuando solo estamos empezando a usarlo. Estas llamadas perdidas a tr() son eventualmente descubiertas por el traductor o, en el peor de los casos, por usuarios que usen la aplicacin traducida, cuando algunas cadenas de texto aparezcan en el lenguaje original. Para evitar este problema, podemos decirle a Qt que prohba conversiones implcitas de const char * a QString. Podemos hacer esto definiendo el smbolo preprocesador QT_NO_CAST_FROM_ASCII antes de incluir cualquier cabecera de Qt. La manera ms fcil de asegurarse de que este smbolo este establecido, es aadir la siguiente lnea al archivo de proyecto ( .pro) de la aplicacin: DEFINES += QT_NO_CAST_FROM_ASCII Esto fuerza a cada cadena textual a ser envuelta por la funcin tr() o por QLatin1String(), dependiendo de si sta debe ser traducida. Las cadenas que no son debidamente envueltas producirn un error de compilacin, y esto, por consecuencia, nos obligar a aadir las cadenas de texto que se hayan olvidado envolver con las funciones tr() o QLatin1String(). Una vez que hemos envuelto cada cadena de texto visible al usuario con una llamada a tr(), lo nico que resta por hacer para permitir la traduccin, es cargar un archivo de traduccin. Tpicamente, haramos esto en la funcin main() de la aplicacin. Por ejemplo, aqu puede apreciarse cmo se tratara de cargar un archivo de traduccin dependiendo de la localidad del usuario: int main(int argc, char *argv[]) { QApplication app(argc, argv); ... QTranslator Traductorapp; Traductorapp.load("miApp_" + QLocale::system().name(), qmPath); app.installTranslator(&Traductorapp); ... return app.exec(); } La funcin QLocale::system() retorna un objeto QLocale que proporciona informacin con respecto a la localidad del usuario. Tradicionalmente, usamos el nombre de la localidad como parte del nombre del archivo .qm. Los nombres de las localidades pueden ser ms o menos precisos; por ejemplo, fr especifica una localidad de lenguaje francs, fr_CA especifica una localidad Francesa-Canadiense y

17. Internacionalizacin

204

fr_CA.ISO8859-15 especifica una localidad francesa con la codificacin ISO 8859-15 (una codificacin que soporta los caracteres , ', y ). Suponiendo que la localidad es fr_CA.ISO8859-15, la funcin QTranslator::load() trata primero de leer el archivo miApp_fr_CA.ISO8859-15.qm. Si este archivo no existe, la funcin load() trata luego con miApp_fr_CA.qm, luego miApp_fr.qm y finalmente intenta con miApp.qm, antes de darse por vencida. Normalmente, solo proporcionaramos el archivo miApp.qm, conteniendo una traduccin francesa estndar, pero si necesitamos un archivo diferente para el habla francesa de Canad, podemos de igual forma proporcionar el archivo miApp_fr_CA.qm y ser usado para las localidades fr_CA. El segundo argumento a QTranslator::load() es el directorio donde queremos que load() busque el archivo de traduccin. En este caso, asumiremos que los archivos de traduccin estn localizados en el mismo directorio que el ejecutable. Las libreras de Qt contienen unas cuantas cadenas de texto que necesitan ser traducidas. Trolltech proporciona traducciones Francesas, Alemanas y Chinas en el directorio de traducciones (translations) de Qt. Otros pocos lenguajes son provistos tambin, adems de los anteriores, pero estos son contribuciones de los usuarios de Qt y no son soportados oficialmente. Los archivos de traduccin de las libreras Qt deben ser cargados igualmente: QTranslator Traductorqt; Traductorqt.load("qt_" + QLocale::system().name(), qmPath); app.installTranslator(&Traductorqt); Un objeto QTranslator puede contener solamente un archivo de traduccin por vez, as que usamos un QTranslator separado de los dems, para la traduccin de Qt. Tener solo un archivo por traductor no es un problema puesto que se pueden instalar tantos traductores como necesitemos. Al final, QCoreApplication los usar a todos ellos cuando se busque una traduccin. En algunos lenguajes como el rabe y el hebreo, la escritura es de derecha a izquierda en vez de izquierda a derecha. Para este tipo de lenguajes, absolutamente todo el mapeo de la aplicacin debe ser revertido o modificado, y esto se puede hacer llamando a QApplication::setLayoutDirection(Qt::RightToLeft). Los archivos de traduccin para Qt contienen un marcador especial llamado LTR que le dice a Qt si el lenguaje es de izquierda a derecha o al contrario, de derecha a izquierda, de manera que normalmente no es necesario que llamemos a setLayouDirection() por nuestra cuenta. Puede resultar ms conveniente para nuestros usuarios si proveemos a nuestra aplicacin con los archivos de traduccin integrados en el ejecutable, usando el sistema de recursos de Qt. Hacer esto, no solo reducira el numero de archivos distribuidos como parte de nuestro producto sino tambin evitara el riesgo de prdidas de los archivos de traduccin o de que estos sean eliminados accidentalmente. Suponiendo que los archivos .qm estn ubicados en el subdirectorio traducciones en el rbol de cdigos fuente, tendramos un archivo llamado miApp.qrc con el siguiente contenido: <RCC> <qresource> <file>traducciones/miApp_de.qm</file> <file>traducciones/miApp_fr.qm</file> <file>traducciones/miApp_zh.qm</file> <file>traducciones/qt_de.qm</file> <file>traducciones/qt_fr.qm</file> <file>traducciones/qt_zh.qm</file> </qresource> </RCC> El archivo .pro contendra la siguiente entrada: RESOURCES += miApp.qrc

17. Internacionalizacin

205

Finalmente, en el main(), debemos especificar :/traducciones como el patch para los archivos de traduccin. Los dos puntos al principio indican que el patch hace referencia a un recurso en contraposicin a un archivo en el sistema de archivos. Hasta este momento hemos cubierto todo lo que se requiere para hacer una aplicacin disponible para trabajar usando traducciones a otros lenguajes. Pero el lenguaje y el sistema de direccin de escritura no son el nico aspecto que vara entre pases y culturas. Un programa internacionalizado debe tener en cuenta el formato de fecha y hora local, formatos monetarios, formatos numricos y el orden de comparacin de cadenas de texto. Qt incluye una clase llamada QLocale que provee de formatos localizados de fecha/hora y numricos. Para consultar cualquier otra informacin con respecto a la localidad, podemos usar las funciones estndar de C++ setlocale() y localeconv(). Algunas clases y funciones de Qt adaptan su funcionamiento a la localidad: QString::localeAwareCompare() compara dos cadenas de texto de una manera localdependiente. Esto es realmente til para la clasificacin de los tems visibles al usuario. La funcin toString() proporcionada por QDate, QTime y QDateTime retorna una cadena de texto en un formato local cuando se llamada con Qt::LocaleDate como argumento. Por defecto, los widgets QDateEdit y QDateTimeEdit presentan fechas en el formato local. Finalmente, una aplicacin traducida puede necesitar diferentes iconos en ciertas situaciones en preferencia a los iconos originales. Por ejemplo, las flechas izquierda y derecha en navegador web, referentes a los botones de navegacin atrs y adelante, deben ser intercambiados cuando se est tratando con un lenguaje de derecha a izquierda. Podemos hacer esto, como se muestra a continuacin: if (QApplication::isRightToLeft()) { accionAtras->setIcon(iconoAlante); accionAlante->setIcon(iconoAtras); } else { accionAtras->setIcon(iconoAtras); accionAlante->setIcon(iconoAlante); } Los iconos que contengan caracteres alfabticos muy comnmente necesitan ser traducidos. Por ejemplo, la letra I en una barra de herramientas asociada con la opcin de Itlica de un procesador de texto, debe ser reemplazada por una C en el espaol (Cursivo) y por una K en el Dans, Holands, Alemn, Noruego y Suizo (Kursiv). Aqu est una manera sencilla de hacerlo: if (tr("Italic")[0] == 'C') { accionCursiva->setIcon(iconoC); } else if (tr("Italic")[0] == 'K') { accionCursiva->setIcon(iconoK); } else { accionCursiva->setIcon(iconoI); } Una alternativa es usar el soporte para mltiples localidades del sistema de recursos. En el archivo .qrc, podemos especificar una localidad para un recurso usando el atributo lang. Por ejemplo: <qresource> <file>italic.png</file> </qresource> <qresource lang="es"> <file alias="italic.png">cursivo.png</file> </qresource> <qresource lang="sv"> <file alias="italic.png">kursiv.png</file> </qresource>

17. Internacionalizacin

206

Si la localidad del usuario es es (Espaol), :/italic.png se convierte en una referencia a la imagen cursivo.png. Si la localidad es sv (Sueco), la imagen kursiv.png es usada. Para otras localidades, la imagen italic.png ser usada.

Cambio Dinmico del Lenguaje


Para la mayora de las aplicaciones, detectar el lenguaje preferido por el usuario en el main() y cargar los archivos .qm apropiados es un mtodo totalmente satisfactorio para sus fines. Pero existen algunas situaciones donde el usuario puede necesitar la capacidad de poder cambiar el lenguaje de la aplicacin dinmicamente. Una aplicacin que es usada continuamente por diferentes personas, puede necesitar que se pueda cambiar el lenguaje sin tener que reiniciar la aplicacin. Por ejemplo, las aplicaciones usadas por los operadores de los Centros de llamadas, las aplicaciones usadas por varios traduc tores al mismo tiempo, y las aplicaciones usadas por operadores de registro de efectivo computarizado, muy a menudo requieren de la capacidad de poder cambiar el lenguaje de la aplicacin sin reiniciar. Hacer una aplicacin capaz de cambiar el lenguaje dinmicamente requiere un poco ms de trabajo que cuando se lee un nico archivo de traduccin al inicio de la aplicacin, pero no es difcil. Lo que se debe hacer es: Proporcionar un mtodo mediante el cual el usuario pueda cambiar el lenguaje. Para cada widget o dilogo, colocar todas sus cadenas de texto traducibles en una funcin separada (a menudo llamada retraducirUi()) y llamar a dicha funcin cuando el lenguaje sea cambiado.

Vamos a repasar las partes ms relevantes del cdigo fuente de una aplicacin de un Centro de llamadas. La aplicacin provee un men de lenguaje (mostrado en la Figura 17.1), para permitir al usuario establecer el lenguaje en tiempo de ejecucin. El lenguaje por defecto es el ingls. Puesto que no sabemos cul lenguaje quiere usar el usuario cuando se inicie la aplicacin, ya no cargaremos las traducciones en la funcin main(). En lugar de eso, las cargaremos dinmicamente cuando sean necesitadas, de forma que todo el cdigo al que necesitemos aplicar traducciones debe ir en la ventana principal y en las clases de dilogos. Echemos un vistazo a la subclase QMainWindow de la aplicacin. MainWindow::MainWindow() { journalView = new JournalView; setCentralWidget(journalView); qApp->installTranslator(&Traductorapp); qApp->installTranslator(&Traductorqt); qmPath = qApp->applicationDirPath() + "/traducciones"; crearAcciones(); crearMenus(); retraducirUi(); } Figura 17.1. Men de lenguaje dinmico

17. Internacionalizacin

207

En el constructor, colocamos al widget central para que sea un objeto tipo JournalView, que es una subclase de QTableWidget. Despus, colocamos unas cuantas variables miembros relacionadas a la traduccin: La variable Traductorapp es un objeto QTranslator usado para guardar las traducciones actuales de la aplicacin. La variable Traductorqt es un objeto QTranslator usado para guardar las traducciones de Qt. La variable qmPath es un QString que especifica el path del directorio que contiene los archivos de traduccin de la aplicacin.

Lo que hacemos es instalar dos objetos QTranslators en el QApplication: el objeto Traductorapp guarda la traduccin actual de la aplicacin, y el objeto Traductorqt guarda las traducciones de Qt. Al final, llamamos a las funciones privadas crearAcciones() y crearMenus() para crear el men del sistema, y llamamos a retraducirUi() (tambin es una funcin privada) para colocar por primera vez las cadenas de texto visibles al usuario. void MainWindow::crearAcciones() { accionNuevo = new QAction(this); connect(accionNuevo, SIGNAL(triggered()), this, SLOT(newFile())); ... accionSobreQt = new QAction(this); connect(accionSobreQt, SIGNAL(triggered()), qApp, SLOT(aboutQt())); } La funcin crearAcciones() crea los objetos QAction como es de costumbre, pero sin establecer ninguno de los textos o teclas de atajos. Esto se har en la funcin retraducirUi(). void MainWindow::crearMenus() { menuArchivo = new QMenu(this); menuArchivo->addAction(accionNuevo); menuArchivo->addAction(accionAbrir); menuArchivo->addAction(accionGuardar); menuArchivo->addAction(accionSalir); menuEditar = new QMenu(this); ... crearMenuLenguaje(); menuAyuda = new QMenu(this); menuAyuda->addAction(accionSobre); menuAyuda->addAction(accionsobreQt); barraMenu()->addMenu(menuArchivo); barraMenu ()->addMenu(menuEditar); barraMenu()->addMenu(menuReportes); barraMenu()->addMenu(menuLenguaje); barraMenu()->addMenu(menuAyuda); } La funcin crearMenus() crea los mens, pero no les da ningn ttulo. Nuevamente, esto se har en la funcin retraducirUi(). A mitad de funcin, llamamos a la funcin crearMenuLenguaje() para llenar el men Lenguaje con la lista de los lenguajes soportados. Estudiaremos su cdigo fuente en un momento. Primero veamos el cdigo de la funcin retraducirUi(): void MainWindow::retraducirUi() { accionNuevo->setText(tr("&Nuevo")); accionNuevo->setShortcut(tr(Ctrl+N));

17. Internacionalizacin

208

accionNuevo->setStatusTip(tr("Crear una publicacin nueva")); ... accionSobreQt->setText(tr("Sobre &Qt")); accionSobreQt->setStatusTip(tr("Mostrar el cuadro Sobre las Librerias de Qt")); accionSalir->setText(tr("&Salir")); accionSalir->setShortcut(tr("Ctrl+S")); ... menuArchivo->setTitle(tr("&Archivo")); menuEditar->setTitle(tr("&Editar")); menuReportes->setTitle(tr("&Reportes")); menuLenguaje->setTitle(tr("&Lenguaje")); menuAyuda->setTitle(tr("&Ayuda")); setWindowTitle(tr("Centro de Llamadas")); } La funcin retraducirUi() es donde se hacen todas las llamadas a tr() para la clase MainWindow. Esta es llamada al final del constructor y cada vez que el usuario cambia el lenguaje de la aplicacin usando el men Lenguaje. Establecimos el texto de cada QAction y el de su status tip, y el atajo para aquellas acciones que no tienen atajos estndar. Establecimos tambin el titulo de cada QMenu, as como tambin el titulo de la ventana. La funcin crearMenus() presentada previamente llam a la funcin crearMenuLenguaje() para llenar el men Lenguaje con una lista de lenguajes: Vista del cdigo: void MainWindow::crearMenuLenguaje() { menuLenguaje = new QMenu(this); actionGroupLenguaje = new QActionGroup(this); connect(actionGroupLenguaje, SIGNAL(triggered(QAction*)), this, SLOT(cambiarLanguage(QAction *))); QDir qmDir(qmPath); QStringList nombreArchivos = qmDir.entryList(QStringList("centrodellamadas_*.qm")); for (int i = 0; i < nombreArchivos.size(); ++i) { QString localidad = nombreArchivos[i]; localidad.remove(0, localidad.indexOf('_') + 1); localidad.truncate(locale.lastIndexOf('.')); QTranslator traductor; traductor.load(nombreArchivos[i], qmPath); QString lenguaje = traductor.translate("MainWindow", "Espaol"); QAction *accion = new QAction(tr("&%1 %2").arg(i +1) .arg(lenguaje), this); accion->setCheckable(true); accion->setData(localidad); menuLenguaje->addAction(accion); actionGroupLenguaje->addAction(accion); if (lenguaje == "Espaol") accion->setChecked(true); } }

17. Internacionalizacin

209

En lugar de codificar internamente los lenguajes soportados por la aplicacin, creamos una entrada de men por cada archivo .qm localizado en el directorio traducciones de la aplicacin. Para mayor sencillez, suponemos que el lenguaje Espaol posee tambin un archivo .qm. Una alternativa podra haber sido llamar a la funcin clear() en los objetos QTranslator cuando el usuario eligiera Espaol como lenguaje. Una dificultad muy particular es presentar un nombre agradable para el lenguaje provisto por cada archivo .qm. Solamente mostrando en para English o de para Deutsch, basado solo en el nombre del archivo .qm, lucira muy rudimentario y confundira a algunos usuarios. La solucin usada en crearMenuLenguaje() es revisar la traduccin de la cadena de texto Espaol en el contexto MainWindow. La cadena de texto debe ser traducida a Deutsch en una traduccin alemana, a Franais en una traduccin francesa, y a en una traduccin japonesa. Hemos creado un QAction chequeable para cada lenguaje y aloja el nombre de la localidad en el tem data de la accin. Los aadimos a un objeto QActionGroup para asegurar que solo un tem del men Lenguaje es chequeado por vez. Cuando el usuario elija una accin del grupo, el QActionGroup emite la seal triggered(QAction *), que est conectada a la funcin cambiarLenguaje(). void MainWindow::cambiarLenguaje(QAction *accion) { QString localidad = accion->data().toString(); Traductorapp.load("Centrodellamadas_" + localidad, qmPath); Traductorqt.load("qt_" + localidad, qmPath); retraducirUi(); } El slot cambiarLenguaje() es llamado cuando el usuario elije un lenguaje del men Lenguaje. Cargamos los archivos de traduccin pertinentes para la aplicacin y para Qt, y llamamos a retraducirUi() para re-traducir todas las cadenas de texto para el main window. En Windows, una alternativa para suministrar un men de Lenguaje es responder a los eventos de LocaleChange, una especie de evento emitido por Qt cuando detecta cambios en la localidad del entorno. Este tipo de evento, existe en todas las plataformas soportadas por Qt, pero actualmente, es generada solamente en Windows, cuando el usuario cambia las configuraciones locales (en la seccin Configuracin Regional y de Idioma del Panel de Control). Para manejar los eventos de LocaleChange, podemos re implementar QWidget::changeEvent() de la siguiente manera: void MainWindow::changeEvent(QEvent *evento) { if (evento->type() == QEvent::LocaleChange) { Traductorapp.load("centrodellamadas_"+ QLocale::system().name(), qmPath); Traductorqt.load("qt_" + QLocale::system().name(), qmPath); retranslateUi(); } QMainWindow::changeEvent(evento); } Si el usuario cambia la localidad mientras la aplicacin est siendo ejecutada, intentaremos cargar los archivos de traduccin correctos para la nueva localidad y llamamos a retraducirUi() para actualizar la interfaz de usuario. En todos los casos, pasamos el evento la funcin de la clase base changeEvent(), ya que a la clase base puede serle de importancia el LocaleChange o algn otro evento. Hemos terminado nuestra revisin del cdigo del MainWindow. Ahora veremos el cdigo para una clase de uno de los widget de la aplicacin, la clase JournalView, para observar qu cambios son necesarios hacerle para que soporte traducciones dinmicas.

17. Internacionalizacin

210

JournalView::JournalView(QWidget *parent) : QTableWidget(parent) { ... retraducirUi(); } La clase JournalView es una subclase de QTableWidget. Al final del constructor, llamamos a la funcin privada retraducirUi() para colocar las cadenas de texto de los widgets. Es similar a lo que hicimos para el MainWindow. void JournalView::changeEvent(QEvent *evento) { if (evento->type() == QEvent::LanguageChange) retraducirUi(); QTableWidget::changeEvent(evento); } Igualmente, re implementamos la funcin changeEvent() para llamar a retraducirUi() cuando hayan eventos LanguageChange. Qt genera un evento LanguageChange cuando el cambia contenido de un objeto QTranslator instalado en QCoreApplication. En nuestra aplicacin, esto sucede cuando llamamos a la funcin load() en los objetos traductores Traductorapp o Traductorqt, cualquiera de MainWindow::cambiarLanguage() o de MainWindow::changeEvent(). Los eventos LanguageChange no deben ser confundidos con los eventos LocaleChange. Los eventos LocaleChange son generados por el sistema y le dicen a la aplicacin: Quiz debas cargar una nueva traduccin. Los eventos LanguageChange, por otro lado, son generados por el mismo Qt, y le dicen a los widgets de la aplicacin: Tal vez deberan re traducir todas sus cadenas de texto. Cuando implementamos el MainWindow, no necesitamos responder a los eventos de LanguageChange. En vez de eso, simplemente llamamos a retraducirUi() en cualquier momento que llamemos a load() en cualquier objeto QTranslator. void JournalView::retraducirUi() { QStringList etiquetas; etiquetas << tr("Hora") << tr("Prioridad") << tr("Nmero de Telfono")<< tr("Asunto"); setHorizontalHeaderLabels(etiquetas); } La funcin retraducirUi() actualiza la columnas de cabeceras con nuevos textos traducidos, completando el cdigo relacionado a una traduccin de un widget escrito a mano. Para los widgets y dilogos desarrollados con Qt Designer, la herramienta uic genera automticamente una funcin similar a nuestra funcin retraducirUi() que es automticamente llamada en respuesta a los eventos LangageChange.

Traduciendo Aplicaciones
Traducir aplicaciones hechas en Qt que contienen llamadas a tr() es un proceso que involucra tres pasos bsicos:

1. Ejecutar lupdate para extraer todas las cadenas de texto visibles al usuario del cdigo fuente de la
aplicacin.

2. Traducir la aplicacin usando Qt Linguist

17. Internacionalizacin

211

3. Ejecutar lrelease para generar los archivos binarios .qm que la aplicacin puede cargar usando
QTranslator. Los pasos 1 y 3 son realizados por los desarrolladores de la aplicacin. El paso 2 es manejado por los traductores. Este ciclo puede repetirse cada vez que sea necesario durante el desarrollo de la aplicacin y en toda su vida til. Como ejemplo, mostraremos cmo traducir la aplicacin de hoja de clculo hecha en el Capitulo 3. La aplicacin ya contiene las llamadas a tr() alrededor de cada cadena de texto visible al usuario. Primero, debemos modificar un poco el archivo .qm de la aplicacin para especificar a cules lenguajes queremos dar soporte. Por ejemplo, si queremos dar soporte al idioma alemn y al francs, adems del espaol, aadiramos al archivo spreadsheet.pro la siguiente entrada: TRANSLATIONS = spreadsheet_de.ts \ spreadsheet_fr.ts

Aqu, especificamos que se usarn dos archivos de traduccin: uno para el alemn y otro para el francs. Estos archivos sern creados la primera vez que ejecutamos lupdate y es actualizado cada vez que ejecutemos lupdate. Estos archivos poseen normalmente una extensin .ts. Estos estn en un formato XML limpio y claro y no son tan compactos como los archivos .qm que son binarios y comprendidos por QTranslator. Es tarea de lrelease convertir los archivos .ts legibles por el humano a archivos .qm que representan mucha ms eficiencia para la mquina. Curiosamente, la extensin .ts hace alusin a translation source (que en espaol sera fuente de traduccin o recurso de traduccin) y la extensin .qm hace alusin a un archivo Qt message (cuya equivalente al espaol seria mensaje de Qt). Suponiendo que estamos ubicados en el directorio que contiene el cdigo fuente de la aplicacin Spreadsheet, podemos ejecutar lupdate sobre el archivo spreadsheet.pro desde la lnea de comandos, como se muestra a continuacin: lupdate -verbose spreadsheet.pro La opcin verbose le dice a lupdate que proporcione ms retroaccin de lo normal. Vea ac la salida que esperamos ver: Updating 'spreadsheet_de.ts'... Found 98 source texts (98 new and 0 already existing) Updating 'spreadsheet_fr.ts'... Found 98 source texts (98 new and 0 already existing) Cada cadena de texto que aparezca dentro de una llamada a tr() en el cdigo fuente de la aplicacin es guardada en los archivos .ts, junto con una traduccin vaca. Las cadenas de texto que aparezcan en los archivos .ui de la aplicacin son incluidas tambin. La herramienta lupdate asume por defecto que el argumento a tr() es una cadena de texto Latin-1. Si este no es el caso, nosotros mismos debemos aadir una entrada CODECFORTR al archivo .pro. Por ejemplo: CODECFORTR = EUC-JP Esto debe hacerse adicionalmente a la llamada a QTextCodec::setCodecForTr() desde la funcin main() de la aplicacin. Las traducciones, luego, necesitan ser agregadas spreadsheet_fr.ts, usando Qt Linguist. a los archivos spreadsheet_de.ts y

Para ejecutar Qt Linguist en Windows, dirjase a Qt by Trolltech v4.x.y/Linguist. Para ejecutarlo en distribuciones Linux, escriba linguist en la lnea de comandos. En Mac puede usar el Mac Os X Finder y

17. Internacionalizacin

212

hacer doble clic a Linguist. Para empezar a aadir traducciones al archivos .ts, haga clic en File/Open y elija el archivo a traducir. La parte izquierda de la ventana principal de Qt Linguist muestra una vista de rbol (tree view). Los tems de la parte superior son los contextos de la aplicacin a ser traducidos. Para la aplicacin Spreadsheet, estos son FindDialog, GotoCellDialog, MainWindow, SortDialog y SpreadSheet. La parte superior es la lista de textos fuentes para el contexto actual. Cada texto fuente es mostrado junto con una traduccin y un indicador Done que indica si est hecha la traduccin o no. El area central derecha es donde podemos ingresar una traduccin para el tem actual. El rea inferior derecha, es un cuadro llamado Warnings donde se muestran las advertencias y recomendaciones proporcionadas automticamente por Qt Linguist. Una vez que hayamos traducido los archivos .ts, necesitaremos convertirlos a archivos binarios .qm para que sean usables por QTranslator. Para hacer esto desde Qt Linguist, hacemos clic en File/Release. Tpicamente, empezaremos por traducir solo unas cuantas cadenas de texto y ejecutamos la aplicacin con el archivo .qm para asegurarnos de que todo funciona apropiadamente. Si queremos regenerar los archivos .qm de todos los archivos .ts, podemos usar la herramienta lrelease como se muestra a continuacin: lrelease -verbose spreadsheet.pro Suponiendo que hemos traducido 19 cadenas de texto al francs y que hemos hecho clic en el flag Done para 17 de ellas, lrelease produce la siguiente salida: Updating 'spreadsheet_de.qm'... Generated 0 translations (0 finished and 0 unfinished) Ignored 98 untranslated source texts Updating 'spreadsheet_fr.qm'... Generated 19 translations (17 finished and 2 unfinished) Ignored 79 untranslated source texts Las cadenas de texto no traducidas sern mostradas en su lenguaje original cuando ejecutemos la aplicacin. El flag Done es ignorado por lrelease; este puede ser usado por los traductores para identificar cuales traducciones estn finalizadas y cules deben ser revisadas. Figura 17.2 Qt Linguist en accin

17. Internacionalizacin

213

Cuando modificamos el cdigo fuente de la aplicacin, los archivos de traduccin pueden quedar obsoletos o desactualizados. La solucin es ejecutar lupdate nuevamente, proporcionando las traducciones para las nuevas cadenas de texto, y regenerar luego los archivos .qm. Algunos equipos de desarrollo encuentran muy til ejecutar lupdate frecuentemente en el proceso de desarrollo, mientras que otros prefieren esperar hasta que la aplicacin este casi lista para su lanzamiento. Las herramientas lupdate y Qt Linguist son muy inteligentes. Las traducciones que no sern ms usadas, son dejadas en los archivos .ts, en caso de que puedan necesitarse luego. Cuando se actualizan los archivos .ts, lupdate usa un algoritmo inteligente de asociacin que puede ahorrar un tiempo considerable a los traductores con todos aquellos textos que son iguales o similares pero en diferentes contextos. Para ms informacin acerca de Qt Linguist, lupdate y lrelease, dirjase al manual de Qt Linguist en la siguiente direccin web: http://doc.trolltech.com/4.1/linguist-manual.html. El manual contiene una explicacin completa de la interfaz de usuario de Qt Linguist y tutoriales paso a paso para programadores.

18. Multithreading

214

18. Multithreading

Creando Threads (hilos) Sincronizando Threads Comunicndose con el Thread Principal Usando Clases Qt en Threads Secundarios

Las aplicaciones GUI convencionales poseen un hilo (conocido como thread en ingls) de ejecucin y realizan una operacin a la vez. Si el usuario invoca una operacin que toma mucho tiempo desde la interfaz de usuario, la interfaz tpicamente se congelar mientras la operacin est en progreso. El Capitulo 7 (Procesamiento de Eventos) presenta soluciones a este problema. El multithreading es otra solucin. En una aplicacin multi hilo o multi tarea (multithreading), la interfaz grfica se ejecuta o corre en su propio hilo y el procesamiento es llevado a cabo en uno ms hilos adicionales. Esto da como resultado aplicaciones que poseen GUIs fluidas durante el tratamiento intensivo o el procesamiento intensivo. Otro beneficio del multithreading es que los sistemas multiprocesadores pueden ejecutar varios hilos simultneamente en diferentes procesadores, obteniendo as mejor rendimiento. En este captulo, empezaremos con mostrar cmo hacer subclases de QThread y cmo usar QMutex, QSemaphore, y QWaitCondition para sincronizar varios hilos. Luego veremos cmo comunicarnos con el hilo principal desde hilos secundarios mientras el ciclo de evento se est ejecutando. Finalmente, terminaremos haciendo una revisin de las clases de Qt que pueden ser usadas en hilos secundarios y cules no. El multithreading es un tema bastante largo con muchos libros dedicados exclusivamente a la materia. Aqu, se asume que ya has entendido los fundamentos de la programacin multithreading, de manera que nos enfocaremos ms en explicar cmo desarrollar aplicaciones Qt con multithreading que en el tema mismo de los hilos.

Creando Threads
Proporcionar mltiples hilos en una aplicacin Qt es fcil de hacer: simplemente hacemos una subclase de QThread y re implementamos la funcin run(). Para mostrar cmo funciona, comenzaremos revisando el cdigo para una subclase muy pequea y sencilla que imprima repetidamente en una consola una cadena de texto que le fue pasada. class Hilo : public QThread { Q_OBJECT public: Hilo(); void establecerMensaje(const QString &mensaje); void detener(); protected: void run();

18. Multithreading

215

private: QString mensajeStr; volatile bool detenido; }; La clase Hilo hereda de QThread y re implementa la funcin run(). Tambin proporciona dos funciones adicionales: establecerMensaje() y detener(). La variable detenido es una variable declarada volatile porque es accedida desde diferentes hilos y queremos estar seguros de que sta sea leda lo ms actualizada y fresca posible cada vez que se necesite. Si omitimos la clave volatile, el compilador puede optimizar el acceso a la variable, posiblemente induciendo a obtener resultados incorrectos. Hilo::Hilo() { detenido = false; } En este constructor, Inicializamos en false la variable detenido. void Hilo::run() { while (!detenido) cerr << qPrintable(mensajeStr); detenido = false; cerr << endl; } La funcin run() es llamada para empezar a ejecutar el hilo. Mientras la variable detenido sea false, la funcin sigue imprimiendo el mensaje que se le ha pasado en la consola. El hilo termina cuando control deja la funcin run(). void Hilo::detener() { detenido = true; } La funcin detener() le da el valor true a la variable detenido, y por consiguiente es como que le dijera a run() que pare de imprimir texto en la consola. Esta funcin puede ser llamada desde cualquier hilo en cualquier momento. Para los propsitos de este ejemplo, asumimos que la asignacin de un bool es una operacin atmica. Esta es una suposicin lgica, considerando que un bool solamente puede tener dos estados. Ms adelante en esta seccin veremos cmo usar QMutex para garantizar que la asignacin a una variable es una operacin atmica. QTread provee una funcin llamada terminate() que termina la ejecucin del hilo mientras ste est ejecutndose. Usar terminate() no es recomendable, ya que sta puede detener el hilo en cualquier punto y no le da al hilo ningn chance de limpiarse despus. Siempre es ms seguro usar una variable detenido y una funcin detener() como lo hicimos aqu. Figura 18.1 La aplicacin Hilos

Ahora veremos cmo usar la clase Hilo en una pequea aplicacin que use dos hilos, A y B, adicionalmente al hilo principal.

18. Multithreading

216

class DialogoHilo : public QDialog { Q_OBJECT public: DialogoHilo (QWidget *parent = 0); protected: void eventoCerrar(QCloseEvent *evento); private slots: void iniciarODetenerHiloA(); void iniciarODetenerHiloB(); private: Hilo hiloA; Hilo hiloB; QPushButton *botonHiloA; QPushButton *botonHiloB; QPushButton *botonQuitar; }; La clase DialogoHilo declara dos variables de tipo Hilo y algunos botones para proporcionar una interfaz de usuario bsica. DialogoHilo::DialogoHilo(QWidget *parent) : QDialog(parent) { hiloA.establecerMensaje("A"); hiloB.establecerMensaje("B"); botonHiloA = new QPushButton(tr("Iniciar A")); botonHiloB = new QPushButton(tr("Iniciar B")); botonQuitar = new QPushButton(tr("Quitar")); botonQuitar->setDefault(true); connect(botonHiloA,SIGNAL(clicked()),this, SLOT(iniciarODetenerHiloA())); connect(botonHiloB, SIGNAL(clicked()),this, SLOT(iniciarODetenerHiloB())); } En el constructor llamamos a establecerMensaje() para hacer que el primer hilo imprima repetidamente el texto A y el segundo hilo imprima B. void DialogoHilo::iniciarODetenerA() { if (HiloA.isRunning()) { HiloA.detener(); botonHiloA->setText(tr("Iniciar A")); } else { HiloA.start(); botonHiloA->setText(tr("Detener A")); } } Cuando el usuario haga click en el botn para el hilo A, la funcin iniciarODetenerHiloA() detiene el hilo si ste estaba ejecutndose, sino estaba ejecutndose entonces lo inicia. Esta funcin tambin actualiza el texto del botn. void DialogoHilo::iniciarODetenerHiloB() { if (hiloB.isRunning()) { hiloB.detener(); botonHiloB->setText(tr("Iniciar B")); } else {

18. Multithreading

217

hiloB.start(); botonHiloB->setText(tr("Detener B")); } } El cdigo para iniciarODetenerHiloB() es muy similar. void DialogoHilo::eventoCerrar(QCloseEvent *evento) { hiloA.detener(); hiloB.detener(); hiloA.wait(); hiloB.wait(); evento->accept(); } Si el usuario hace click en Quitar o cierra la ventana, detendremos cualquier hilo que se est ejecutando y esperaremos por ellos para finalizar (usando QThread::wait()) antes de llamar a QCloseEvent::accept(). De esta manera, nos aseguramos de que la aplicacin se cierre de una manera limpia, aunque esto no importe tanto para este ejemplo. Si ejecutas la aplicacin y haces click en Iniciar A, la consola se rellenar con muchas A. Si haces click en Iniciar B, entonces se alternar la secuencia para rellenar la consola con letras A y letras B. Si presionas Detener A, ahora solamente imprimir letras B.

Sincronizando Threads
Un requisito comn para las aplicaciones multi hilos es que stas sincronicen varios de ellos. Qt proporciona las siguientes clases de sincronizacin: QMutex, QReadWriteLock, QSemaPhore y QWaitCondition. La clase QMutex provee una manera de proteger una variable o una pieza de cdigo que solamente pueda ser accedida vez por vez (vase MUTEX en el Glosario de terminos). La clase proporciona igualmente una funcin lock() que bloquea o cierra el mutex. Si el mutex esta desbloqueado, el hilo actual lo agarra inmediatamente y lo bloquea; de otra manera, el hilo actual es bloqueado hasta que el hilo que contiene el mutex lo desbloquee. De cualquier manera, cuando el llamado a lock() retorna, el hilo actual contiene el mutex hasta que este llame a unlock(). La clase QMutex tambin proporciona una funcin tryLock() que retorna inmediatamente si el mutex ya est bloqueado. Por ejemplo, supongamos que queramos proteger con un QMutex la variable detenido de la clase Hilo de la seccin anterior. Entonces lo que haramos seria aadir el siguiente dato miembro a la clase Hilo: private: QMutex mutex; }; La funcin run() se cambiara por esta: void Hilo::run() { porsiempre { mutex.lock(); if (detenido) { detenido = false; mutex.unlock(); break; }

18. Multithreading

218

mutex.unlock(); cerr << qPrintable(mensajeStr); } cerr << endl; } La funcin detener() se transformara en esto: void Hilo::detener() { mutex.lock(); detenido = true; mutex.unlock(); } Bloquear y desbloquear un mutex en funciones complejas, o funciones que usen excepciones C++, pueden ser un proceso propenso a errores. Qt ofrece la conveniente clase QMutexLocker para simplificar el manejo de los mutex. El constructor de QMutexLocker acepta un QMutex como argumento y lo bloquea. El destructor de QMutexLocker desbloquea el mutex. Por ejemplo, podramos reescribir las funciones previas run() y detener() como sigue a continuacin: void Hilo::run() { porsiempre { { QMutexLocker bloqueador(&mutex); if (detenido) { detenido = false; break; } } cerr << qPrintable(mensajeStr); } cerr << endl; } void Hilo::detener() { QMutexLocker bloqueador(&mutex); detenido = true; } Un hecho particular cuando se usan varios mutex es que solamente un solo hilo puede acceder a una misma variable a la vez. En programas con muchos hilos tratando de leer la misma variable simultneamente (sin modificarla), el mutex puede transformarse en un verdadero cuello de botella, perjudicial para el rendimiento. En estos casos, podemos usar QReadWriteLock, una clase de sincronizacin que permite accesos simultneos de slo lectura sin comprometer el rendimiento. En la clase Hilo, no tendra sentido remplazar QMutex con QReadWriteLock para proteger a la variable detenido, porque, a lo sumo, solamente un hilo intentar leer la variable en cualquier momento dado. Un ejemplo ms apropiado incluira uno o ms hilos lectores accediendo a algn dato compartido y uno o varios hilos escritores modificando ese dato. Por ejemplo: MiDato dato; QReadWriteLock bloquear; void HiloLector::run() { ... bloquear.lockForRead();

18. Multithreading

219

acceder_a_dato_sin_modificarlo(&dato); bloquear.unlock(); ... } void HiloEscritor::run() { ... bloquear.lockForWrite(); modificar_dato(&dato); bloquear.unlock(); ... } Por conveniencia, podemos usar las clases QReadLocker y QWriteLocker para bloquear y desbloquear un dato ReadWriteLock. QSemaphore es otra generalizacin de los mutex, pero a distincin de los bloqueadores de lectura/escritura, los semforos (tipo de dato QSemaphore) pueden ser usados para salvaguardar cierto nmero de recursos idnticos. Los siguientes dos segmentos de cdigo muestran la correspondencia entre QSemaphore y QMutex: QSemaphore semaforo(1); semaforo.acquire(); semaforo.release(); QMutex mutex; mutex.lock(); mutex.unlock();

Pasando el numero 1 al constructor; le decimos al semforo que controla un nico recurso. La ventaja de usar un semforo es que podemos pasar otros nmeros distintos a 1 al constructor y luego llamar a la funcin acquire() mltiples veces para adquirir muchos recursos. Una aplicacin tpica de los semforos se hace cuando se transfiere cierta cantidad de datos (TamaoDeDatos) entre dos hilos usando un buffer circular de un cierto tamao (TamaoDeBuffer): const int TamaoDeDatos = 100000; const int TamaoDeBuffer = 4096; char buffer[TamaoDeBuffer]; El hilo productor escribe los datos en el buffer hasta que alcance el fin y luego reinicia desde el comienzo, sobre escribiendo los datos existentes. El hilo consumidor lee los datos como son generados. La Figura 18.2 ilustra esto, asumiendo un pequeo buffer de 16-bytes. Figura 18.2 El modelo productor-consumidor

La necesidad de sincronizar en el ejemplo de productor-consumidor es doble: si el productor genera los datos demasiado rpido, este sobre escribir datos que el consumidor no haya ledo todava; si el consumidor lee los datos muy rpido, este pasar al productor y leer basura. Una manera de solventar este problema es hacer que el productor llene el buffer, y luego esperar hasta que el consumidor haya ledo el buffer completo, y as sucesivamente. Sin embargo, en maquinas con varios procesadores, esto no es tan rpido como dejar a los hilos productor y consumidor operar en diferentes partes del buffer al mismo tiempo. Una manera de resolver este problema satisfactoriamente involucra dos semforos:

18. Multithreading

220

QSemaphore espacioLibre(TamaoDeBuffer); QSemaphore espacioUsado(0); El semforo espacioLibre maneja la parte del buffer que el productor puede llenar con datos. El semforo espacioUsado maneja el rea que el consumidor puede leer. Estas dos reas son complementarias. El semforo espacioLibre es inicializado con el TamaoDeBuffer en 4096, significando eso que ste tiene esos grandes recursos que pueden ser adquiridos. Cuando la aplicacin comience, el hilo lector empezar adquiriendo bytes libres y convirtindolos a bytes usados. El semforo espacioUsado es inicializado con 0 para asegurarse de que consumidor no lea basura al iniciar. Para este ejemplo, cada byte cuenta como un recurso. En una aplicacin del mundo real, tendramos que operar probablemente en largas unidades (por ejemplo, 64 o 256 bytes por vez) para reducir la sobrecarga asociada con el uso de semforos. void Productor::run() { for (int i = 0; i < TamaoDeDatos; ++i) { espacioLibre.acquire(); buffer[i % TamaoDeBuffer]= "ACGT"[uint(rand()) % 4]; espacioUsado.release(); } } En el productor, cada iteracin empieza adquiriendo un byte libre. Si el buffer est lleno de datos que el consumidor no haya ledo, la llamada a acquire() se bloquear hasta que el consumidor empiece a consumir los datos. Una vez que hayamos adquirido el byte, lo llenaremos con algn dato aleatorio (A, C, G o T) y liberaremos el byte como usado, para que pueda ser ledo por el hilo consumidor. void Consumidor::run() { for (int i = 0; i < TamaoDeDatos; ++i) { espacioUsado.acquire(); cerr << buffer[i % TamaoDeBuffer]; espacioLibre.release(); } cerr << endl; } En el consumidor, empezamos adquiriendo un byte usado. Si el buffer no contiene ningn dato para leer, el llamado a acquire() se bloquear hasta que el productor haya producido algn dato. Una vez que hayamos adquirido el byte, lo imprimimos y liberamos el byte como libre, hacindole posible al productor llenarlo con datos nuevamente. int main() { Producer productor; Consumer consumidor; productor.start(); consumidor.start(); productor.wait(); consumidor.wait(); return 0; } Finalmente, en el main(), iniciamos los hilos productor y consumidor. Lo que sucede despus es que el productor convierte algn espacio libre en espacio usado, y el consumidor luego puede convertirlo a espacio libre nuevamente.

18. Multithreading

221

Cuando ejecutamos el programa, ste escribe una secuencia aleatoria de 100.000 caracteres A, C, G y T a la consola y luego termina. Para entender lo que est pasando realmente, podemos deshabilitar o desactivar la impresin de salida y, en lugar de eso, imprimir P cada vez que el productor genera un byte y c cada vez que el consumidor lee un byte. Y para hacer las cosas tan fciles de entender como sea posible, podemos usar valores pequeos para los buffers TamaoDeDatos y TamaoDeBuffer. Por ejemplo, aqu est una posible corrida con un TamaoDeDatos de 10 y un TamaoDeBuffer de 4: PcPcPcPcPcPcPcPcPcPc. En este caso, el consumidor lee los bytes tan pronto como sean generados por el productor; los dos hilos son ejecutados con la misma velocidad. Otra posibilidad es que el productor llene todo el buffer antes de que el consumidor empiece a leerlos, incluso: PPPPccccPPPPccccPPcc. Hay muchas otras posibilidades. Los semforos dan un montn de alcances al programador del sistema de hilos, el cual puede estudiar el comportamiento de los hilos y elegir una poltica de programacin adecuada. Un mtodo diferente al problema de sincronizar un productor y un consumidor es usar QWaitCondition y QMutex. Un QWaitCondition permite que un hilo despierte a otros hilos cuando alguna condicin se haya cumplido. Esto da un margen de mayor precisin en el control del que es posible con el mtodo de usar solamente varios mutex. Para mostrar cmo funciona, vamos a rehacer el ejemplo de productor-consumidor usando condiciones de espera (wait conditions). const int TamaoDeDatos = 100000; const int TamaoDeBuffer = 4096; char buffer[TamaoDeBuffer]; QWaitCondition bufferNoEstaLLeno; QWaitCondition bufferNoEstaVacio; QMutex mutex; int espacioUsado = 0; Adems del buffer, declaramos dos QWaitConditions, un QMutex, y una variable que alojar cuntos bytes, en el buffer, son bytes usados. void Productor::run() { for (int i = 0; i < TamaoDeDatos; ++i) { mutex.lock(); while (espacioUsado == TamaoDeBuffer) bufferNoEstaLLeno.wait(&mutex); buffer[i % TamaoDeBuffer]= "ACGT"[uint(rand()) % 4]; ++espacioUsado; bufferNoEstaVacio.wakeAll(); mutex.unlock(); } } En el productor, comenzamos con chequear si el buffer est lleno. Si lo est, esperamos a que se cumpla la condicin buffer no est lleno. Cuando esa condicin se cumpla, escribimos un byte al buffer, incrementamos la variable espacioUsado, y activamos cualquier hilo que espere que la condicin de buffer no est vaco sea activada. Usamos un mutex para proteger todos los accesos a la variable espacioUsado. La funcin QWaitCondition::wait() puede tomar un mutex bloqueado como su primer argumento, al cual desbloquea antes de bloquear al hilo actual y luego lo bloquea antes de retornar. Para este ejemplo, pudimos haber reemplazado el ciclo entero while (espacioUsado == TamaoDeBuffer) bufferNoEstaLleno.wait(&mutex); Por esta otra operacin: if (espacioUsado == TamaoDeBuffer) {

18. Multithreading

222

mutex.unlock(); bufferNoEstaLleno.wait(); mutex.lock(); } Sin embargo, esto se rompera tan pronto como permitamos ms de un hilo productor, ya que otro productor podra tomar al mutex inmediatamente despus del llamado a wait() y hacer que la condicin buffer no est lleno sea falsa nuevamente. void Consumidor::run() { for (int i = 0; i < TamaoDeDatos; ++i) { mutex.lock(); while (espacioUsado == 0) bufferNoEstaVacio.wait(&mutex); cerr << buffer[i % TamaoDeBuffer]; --espacioUsado; bufferNoEstaLleno.wakeAll(); mutex.unlock(); } cerr << endl; } El consumidor hace exactamente lo opuesto que el productor: espera a que la condicin buffer no est lleno se cumpla y activa cualquier hilo en espera de la condicin buffer no est lleno. En todos los ejemplos vistos hasta ahora, nuestros hilos han accedido a la misma variable global. Pero algunas aplicaciones que usan hilos necesitan tener una variable global guardando diferentes valores en diferentes hilos. Esto es llamado, a menudo, alojamiento local en hilo o dato especifico de hilo. Nosotros podemos simularlo usando un mapa con claves basadas en los IDs de los hilos (retornadas por QThread::currentThread()), pero un mtodo muy adecuado es usar la clase QThreadStorage <T>. Un uso comn para QThreadStorage<T> es para las cachs. Teniendo una cach separada en diferentes hilos, podemos evitarnos la sobrecarga de bloquear, desbloquear y posiblemente esperar por un mutex. Por ejemplo: QThreadStorage<QHash<int, double> *> cache; void insertarEnCache(int id, double value) { if (!cache.hasLocalData()) cache.setLocalData(new QHash<int, double>); cache.localData()->insert(id, value); } void removerDeCache(int id) { if (cache.hasLocalData()) cache.localData()->remove(id); } La variable cache guarda un puntero a QMap<int, double> por cada hilo (Debido a problemas con algunos compiladores, el tipo de dato de la plantilla en QThreadStorage<T> debe ser un puntero). La primera vez que usamos la cach en un hilo particular, la funcin hasLocalData() retorna false y creamos el objeto QHash<int,double>. Adicionalmente a usar cach, QThreadStorage<T> puede usarse para variables globales de estado de errores (similares a errno) para asegurarnos de que las modificaciones en un hilo no afecten al otro hilo.

18. Multithreading

223

Comunicndose con el Thread Principal


Cuando una aplicacin Qt se ejecuta, solamente un hilo empieza a correr el hilo principal. Este es el nico hilo al que le es permitido la creacin del objeto QCoreApplication y de llamar a exec() con ste. Despus del llamado a exec(), ste hilo tambin espera por un evento o por el procesamiento de un evento. El hilo principal puede iniciar nuevos hilos a travs de la creacin de objetos de una subclase de QObject, como lo hicimos en la seccin anterior. Si esos nuevos hilos necesitan comunicarse entre ellos, pueden usar variables compartidas junto con los mutex, bloqueadores de lectura/escritura, semforos o condiciones de espera. Pero ninguna de estas tcnicas puede usarse para comunicarse con el hilo principal, puesto que se podra bloquear el ciclo de evento y congelar la interfaz de usuario. La solucin para comunicarse desde un hilo secundario con el hilo principal es usar las conexiones de signals y slots entre los hilos. Normalmente, el mecanismo de signals y slots opera sincronizadamente, lo cual significa que los slots que estn conectados a una seal son invocados inmediatamente cuando la seal es emitida, usando un llamado directo a una funcin. Sin embargo, cuando conectamos objetos que habitan en diferentes hilos, el mecanismo se vuelve asncrono (Este comportamiento puede cambiarse a travs de un quito parmetro opcional a QObject::connect()). Detrs de escenas, esas conexiones son implementadas por medio del aviso de un evento. Luego, los slots son llamados por el ciclo de evento del hilo en donde el objeto receptor existe. Por defecto, un QObject existe en el hilo en el cual fue creado; esto puede cambiar en cualquier momento al llamar a QObject::moveToThread(). Figura 18.3 La aplicacin Image Pro

Para ilustrar cmo funcionan las conexiones de signals y slots entre hilos, vamos a revisar el cdigo de la aplicacin Image Pro, una aplicacin de procesamiento de imagen muy bsica que le permite al usuario rotar, redimensionar o cambiar la profundidad de color de una imagen. La aplicacin usa un hilo secundario para realizar operaciones sobre imgenes sin bloquear el ciclo de evento. Esto hace una diferencia significante cuando se procesan imgenes muy grandes. El hilo secundario posee una lista de tareas, o transacciones, para realizarlas y enviar eventos a la ventana principal para reportar el progreso. VentanaImagen::VentanaImagen() { imagenLabel = new QLabel; imagenLabel->setBackgroundRole(QPalette::Dark);

18. Multithreading

224

imagenLabel->setAutoFillBackground(true); imagenLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); setCentralWidget(imagenLabel); crearAcciones(); crearMenus(); statusBar()->showMessage(tr("Listo"), 2000); connect(&thread,SIGNAL(transaccionIniciada(const QString &)), statusBar(),SLOT(showMessage (const QString &))); connect(&thread, SIGNAL(finished()), this, SLOT(todasTransaccionesHechas())); establecerArchivoActual(""); } La parte interesante del constructor de VentanaImagen son las dos conexiones de signal y slot. Ambas incluyen seales emitidas por el objeto HiloTransaccion, el cual revisaremos en un momento. void VentanaImagen::voltearHorizontalmente() { agregarTransaccion(new TransaccionVoltear(Qt::Horizontal)); } El slot voltearHorizontalemente() crea una transaccin de rotacion y la registra usando la funcin privada agregarTransaccion(). Las funciones voltearVerticalmente, redimensionarImagen(), convertirA32Bit(), convertirA8Bit() y convertirA1Bit son similares. void VentanaImagen::agregarTransaccion(Transaccion *transac) { thread.agregarTransaccion(transac); accionAbrir->setEnabled(false); accionGuardar->setEnabled(false); accionGuardarComo->setEnabled(false); } La funcin agregarTransaccion() agrega una transaccin a la lnea de transacciones del hilo secundario y deshabilita las acciones Abrir, Guardar y Guardar Como mientras las transacciones estn siendo procesadas. void VentanaImagen::todasTransaccionesHechas() { accionAbrir->setEnabled(true); accionGuardar->setEnabled(true); accionGuardarComo->setEnabled(true); imagenLabel->setPixmap(QPixmap::fromImage (thread.imagen())); setWindowModified(true); statusBar()->showMessage(tr("Listo"), 2000); } El slot todasTransaccionesHechas() es llamado cuando la lnea de transacciones de TransacctionThread queda vaca. Ahora, vamos a ver la clase HiloTransaccion: class HiloTransaccion : public QThread {

18. Multithreading

225

Q_OBJECT public: void agregarTransaccion(Transaccion *transac); void ponerImagen(const QImage &imagen); QImage imagen(); signals: void transaccionIniciada(const QString &mensaje); protected: void run(); private: QMutex mutex; QImage imagenActual; QQueue<Transaccion *> transacciones; }; La clase HiloTransaccion mantiene una lista de transacciones para procesarlas y ejecutarlas una despus de la otra en el segundo plano. void HiloTransaccion::agregarTransaccion(Transaccion *transac) { QMutexLocker locker(&mutex); transacciones.enqueue(transac); if (!isRunning()) start(); } La funcin agregarTransaccion() agrega una transaccin a la lnea de transacciones e inicia el hilo de transaccin en caso de que no se haya iniciado ya. Todos los accesos a la variable miembro transacciones son protegidos por un mutex, porque el hilo principal puede modificarlas a travs de agregarTransaccion() al mismo tiempo que el hilo secundario itera sobre transacciones. void HiloTransaccion::ponerImagen(const QImage &imagen) { QMutexLocker locker(&mutex); imagenActual = imagen; } QImage HiloTransaccion::imagen() { QMutexLocker locker(&mutex); return imagenActual; } Las funciones ponerImagen() e imagen() le permiten al hilo principal establecer la imagen sobre la cual realizar las transacciones y recuperar la imagen resultante una vez que todas las transacciones sean hechas. Nuevamente, protegeremos el acceso a una variable miembro usando un mutex. void HiloTransaccion::run() { Transaccion *transac; forever { mutex.lock(); if (transacciones.isEmpty()) { mutex.unlock(); break; } QImage imagenAnterior = imagenActual; transac = transacciones.dequeue(); mutex.unlock();

18. Multithreading

226

emit transaccionIniciada(transac->mensaje()); QImage nuevaImagen=transac->aplicar(imagenAnterior); delete transac; mutex.lock(); imagenActual = nuevaImagen; mutex.unlock(); } } La funcin run() revisa a travs de la lnea de transaccin y ejecuta cada transaccin en turno mediante la llamada al mtodo aplicar() de cada transaccin. Cuando una transaccin es iniciada, emitimos la seal transaccionIniciada() con un mensaje para mostrarlo en la barra de estatus de la aplicacin. Cuando todas las transacciones se hayan procesado, la funcin run() retorna y QThread emite la seal finished(). class Transaccion { public: virtual ~Transaccion() { } virtual QImage aplicar(const QImage &imagen) = 0; virtual QString mensaje() = 0; }; La clase Transaccion es una clase base abstracta para las operaciones que el usuario puede realizar sobre una imagen. El destructor virtual es necesario porque necesitamos eliminar las instancias de las subclases de Transaccion a travs de un puntero Transaccion (De todas formas, si omitimos este paso, alguno compiladores emitirn una advertencia). La clase Transaccion tiene tres subclases concretas: TransaccionVoltear, TransaccionRedimencionar y TransaccionConvertirProfundidad. Nosotros solamente vamos a revisar la subclase TransaccionVoltear; las otras dos clases son similares. class TransaccionVoltear : public Transaccion { public: TransaccionVoltear(Qt::Orientation orientacion); QImage aplicar(const QImage &imagen); QString mensaje(); private: Qt::Orientation orientacion; }; El constructor de TransaccionVoltear toma un parmetro que especifica la orientacin de la rotacin (horizontal o vertical). QImage TransaccionVoltear::aplicar(const QImage &imagen) { return imagen.mirrored(orientacion == Qt::Horizontal, orientacion == Qt::Vertical); } La funcin aplicar() llama a la funcin QImage::mirrored() sobre el objeto QImage que recibe como parmetro y retorna el QImage resultante. QString TransaccionVoltear::mensaje() { if (orientacion == Qt::Horizontal) { return QObject::tr("Volteando imagen horizontalmente..."); } else {

18. Multithreading

227

return QObject::tr("Volteando imagen verticalmente..."); } } La funcin mensaje() retorna el mensaje a mostrar en la barra de estado mientras la operacin est en progreso. Esta funcin es llamada en HiloTransaccion::run() cuando se emite la seal transaccionIniciada().

Usando Clases Qt en Threads Secundarios


Se dice que una funcin es thread-safe (segura en el uso de hilos) cuando esta puede ser llamada desde diferentes hilos simultneamente de una manera segura. Si dos hilos seguros son llamados desde hilos diferentes sobre los mismos datos compartidos, el resultado est siempre definido. Por extensin, puede decirse que una clase es thread-safe cuando todas sus funciones pueden ser llamadas desde diferentes hilos simultneamente sin interferir con los dems, aun cuando se est operando sobre el mismo objeto. Las clases de Qt que son thread-safe son QMutex, QMutexLocker, QReadWriteLock, QReadLocker, QWriteLocker, QSemaphore, QThreadStorage<T>, QWaitCondition y partes de la API de QThread. Adicionalmente, muchas funciones son thread-safe, incluyendo QObject::connect(), QObject::disconnect(), QCoreApplication::postEvent(),CoreApplication::removePostedEvent() y QCoreApplication::removePostedEvents(). La mayora de las clases de Qt que no son de interfaz grafica cumplen un requerimiento menos estricto: Son reentrant (que permite muchas entradas [puede ser leda simultneamente por varios usuarios o varias veces por el mismo usuario]). Una clase es reentrant si diferentes instancias de la clase pueden ser usadas simultneamente en diferentes hilos. No obstante, acceder al mismo objeto reentrant en mltiples hilos simultneamente no es seguro, y dichos accesos deberan ser protegidos con un mutex. Tpicamente, cualquier clase C++ que no se referencie a datos globales u otra forma de datos compartidos, es reentrant. QObject es una clase reentrant, pero existen tres restricciones que se deben tener en mente:

Los hijos tipo QObject deben ser creados en sus hilos padres.
En particular, esto quiere decir que los objetos creados en un hilo secundario nunca deben ser creados con el objeto QThread como padre, porque ese objeto fue creado en otro hilo (tambin el hilo principal o un hilo secundario diferente).

Debemos eliminar todos los QObjects creados en un hilo secundario antes de eliminar el objeto QThread correspondiente.
Esto puede hacerse creando objetos en la pila; o stack, en QThread::run().

Los QObjects deben ser eliminados en el hilo que los cre.


Si necesitamos eliminar un QObject que existe en hilo diferente, debemos llamar a la funcin thread-safe QObject::deleteLater(), la cual informa sobre un evento de borrado diferido. Las subclases de QObject que no son GUI como QTimer, QProcces y las clases de red son reentrant. Podemos usarlas en cualquier hilo, siempre y cuando el hilo tenga un ciclo de eventos. Para hilos secundarios, el ciclo de eventos es iniciado llamando a QThread::exec() o llamando a funciones convenientes como QProcces::waitFinished() y QAbstractSocket::waitForDisconnected(). Debido a las limitaciones heredadas de las libreras de bajo nivel en las cuales el soporte GUI de Qt es construido, la clase QWidget y sus subclases no son reentrant. Una consecuencia de esto, es que no

18. Multithreading

228

podemos llamar directamente a funciones sobre un widget desde un hilo secundario. Si queremos hacerlo, digamos que, queremos cambiar el texto de un QLabel desde un hilo secundario, podemos emitir una seal conectada a QLabel::setText() o llamar a QMetaObject::invokeMethod() desde ese hilo. Por ejemplo: void MiThread::run() { ... QMetaObject::invokeMethod(label, SLOT(setText(const QString &)), Q_ARG(QString, "Hola")); ... } Muchas de las clases no GUI de Qt, incluyendo QImage, QString y el contenedor de clases, usan compartimiento implcito como una tcnica de optimizacin. Mientras que sta optimizacin usualmente hace a una clase no reentrant, en Qt esto no un problema porque Qt usa instrucciones atmicas en lenguaje ensamblador para implementar contadores referenciales y que sean thread-safe, haciendo que las clases implcitamente compartidas de Qt sean reentrant. El modulo SQL de Qt puede usarse tambin en aplicaciones multi hilos, pero este tiene sus propias restricciones, que varan de base de datos en base de datos. Para detalles al respecto, visite http://doc.trolltech.com/4.1/sql-driver.html. Para una lista completa de advertencias sobre el multithreading, visite http://doc.trolltech.com/4.1/threads.html.

19. Creando Plugins

229

19. Creando Plugins

Extendiendo Qt con Plugins Haciendo Aplicaciones que Acepten Plugins Escribiendo Plugins para Aplicaciones

Las libreras dinmicas (tambin llamadas libreras compartidas o DLLs) son mdulos independientes que son guardados en un archivo separado en el disco y pueden ser utilizados por mltiples aplicaciones. Los programas especifican, usualmente, las libreras dinmicas que necesitan en tiempo de enlazado, caso donde las libreras son cargadas automticamente cuando la aplicacin comienza. Este mtodo a menudo implica tener que aadir la librera y posiblemente su directorio de include al archivo .pro de la aplicacin e incluir las cabeceras correspondientes en los archivos fuentes. Por ejemplo: LIBS += -ldb_cxx INCLUDEPATH += /usuario/local/BerkeleyDB.4.2/include La alternativa es cargar dinmicamente la librera cuando sea requerido, y luego determinar los smbolos que queremos usar de ella. Qt proporciona la clase QLibrary para lograr esto de una manera eficaz independientemente de la plataforma en que se trabaje. Dado un segmento del nombre de una librera, QLibrary busca la librera en las locaciones estndares de la plataforma, buscando un archivo apropiado. Por ejemplo, dando el nombre mimetype, sta buscar a mimetype.dll en Windows o mimetype.so en Linux, y mimetype.dylib en Mac OS X. Las aplicaciones GUI modernas frecuentemente pueden ser extendidas mediante el uso de pluings. Un plugin es una librera dinmica que implementa una interfaz particular para proporcionar una funcionalidad opcional extra. Por ejemplo, en el Capitulo 5, creamos un plugin para integrar un widget personalizado con Qt Designer. Qt reconoce sus propios sets de plugins para varios campos, incluyendo formato de imgenes, drivers de bases de datos, estilos de widgets, codificaciones de texto, y accesibilidad. La primera seccin de este captulo muestra cmo extender Qt con plugins de Qt. Tambin es posible crear plugins especficos para aplicaciones Qt particulares. Qt hace fcil la tarea de escribir tales plugins a travs de su framework para plugins, el cual agrega seguridad de colisin y comodidad para trabajar con QLibrary. En las ltimas dos secciones de este captulo, mostramos cmo hacer una aplicacin que soporte plugins y cmo crear plugins personalizados para una aplicacin.

Extendiendo Qt con Plugins


Qt puede ser extendido con una variedad de plugins de diferentes tipos, los ms comunes son driver de bases de datos, formatos de imagen, estilos y cdecs de texto. Para cada tipo de plugin, normalmente necesitamos al menos dos clases: una clase que sea contenedora del plugin que implemente las funciones genricas de la API del plugin, y una o ms clases manejadoras que implementen la API para un tipo particular de plugin. Los manejadores son accedidos a travs de la clase contenedora. Estas clases se muestran en la Figura 19.1.

19. Creando Plugins

230

Figura 19.1 Plugin Qt y clases manejadoras

Para demostrar cmo extender Qt con plugins, vamos a implementar un plugin que puede leer archivos de cursores monocromticos de Windows (archivos .cur). Estos archivos pueden contener muchas imgenes del mismo cursor en diferentes tamaos. Una vez que el plugin de cursor es construido e instalado, Qt ser capaz de leer archivos .cur y acceder a cursores individuales (p. e., a travs de QImage, QImageReader o QMovie), y ser capaz de escribir los cursores en otros formatos de archivos de imagen como BMP, JPEG y PNG. El plugin tambin podra mostrarse con aplicaciones Qt ya que estas automticamente verifican las locaciones estndar para los plugins de Qt y leen cualquier cosa que encuentren. Los nuevos contenedores de plugins de formato de imagen deben subclasificar a QImageIOPlugin y re implementar unas cuantas funciones virtuales: class CursorPlugin : public QImageIOPlugin { public: QStringList claves() const; Capabilities capacidades(QIODevice *device, const QByteArray &formato) const; QImageIOHandler *crear(QIODevice *device, const QByteArray &formato) const; }; La funcin claves() retorna una lista de formatos de imagen que el plugin soporta. El parmetro formato de las funciones capacidades() y crear() puede ser adoptado para obtener un valor de esa lista. QStringList CursorPlugin::claves() const { return QStringList() << "cur"; } Nuestro plugin soporta solamente un formato de imagen, as que retorna una lista con un solo nombre. Idealmente, el nombre debe ser la extensin de archivo usada para el formato. Cuando tratemos con formatos con muchas extensiones (como .jpg y .jpeg para el formato JPEG), podemos retornar una lista con varias entradas para el mismo formato, uno por cada extensin. QImageIOPlugin::Capabilities CursorPlugin::capacidades(QIODevice *device, const QByteArray &formato) const { if (formato == "cur") return CanRead; if (formato.isEmpty()) { ManejadorCursor manejador;

19. Creando Plugins

231

manejador.setDevice(device); if (manejador.canRead()) return CanRead; } return 0; } La funcin capacidades() retorna lo que el manejador de imagen es capaz de hacer con el formato de imagen dado. Existen tres capacidades (CanRead, CanWrite y CanReadIncremental) y el valor de retorno es un OR en bits de aquellos en que aplique. Si el formato es cur, nuestra implementacin retorna CanRead. Si ningn formato de imagen es suministrado, creamos un manejador de cursor y verificamos si es capaz de leer los datos desde el objeto (device) dado. La funcin canRead() solamente echa un vistazo en los datos, viendo si reconoce el archivo, sin cambiar el puntero del archivo. Una capacidad de 0 significa que el archivo no puede ser ledo o escrito por este manejador. QImageIOHandler *CursorPlugin::crear(QIODevice *device, teArray &formato) const { ManejadorCursor *manejador = new ManejadorCursor; manejador->setDevice(device); manejador->setFormat(formato); return manejador; } Cuando el archivo de cursor es abierto (p. e., por QImageReader), la funcin crear() del contenedor de plugin ser llamada con el puntero al objeto (device) y con cur como formato. Creamos una instancia de ManejadorCursor y la configuramos con el objeto (device) y formato especificado. El llamador toma dominio del manejador y lo borrar cuando ya no sea necesitado ms. Si varios archivos tienen que ser ledos, un manejador nuevo ser creado por cada uno. Q_EXPORT_PLUGIN2(cursorplugin, CursorPlugin) Al final del archivo .cpp, usamos el macro Q_EXPORT_PLUGIN2() para asegurarnos de que Qt reconozca el plugin. El primer parmetro es un nombre arbitrario que le queramos dar al plugin. El segundo parmetro es el nombre de la clase del plugin. Hacer una subclase de QImageIOPlugin es algo sencillo. El verdadero trabajo del plugin se hace en el manejador. Los manejadores de formatos de imagen deben ser subclases de QImageIOHandler y deben re implementar algunas o todas sus funciones pblicas. Comencemos con la cabecera: class ManejadorCursor : public QImageIOHandler { public: ManejadorCursor(); bool canRead() const; bool leer(QImage *imagen); bool SaltarASiguienteImagen(); int NumeroDeImagenActual() const; int ContarImagenes() const; private: enum Estado { CabeceraAnterior, ImagenAnterior, DespuesDeUltimaImagen, Error }; void LeerCabeceraSiEsNecesario() const; QBitArray leerBitmap(int ancho, int alto, QDataStream &in) const; void entrarEstadoError() const; mutable Estado estado; mutable int NumImagenActual;

19. Creando Plugins

232

mutable int numImagenes; }; Las signaturas de todas las funciones pblicas estn fijas. Hemos omitido muchas funciones que no vamos a necesitar para re implementar un manejador de solo lectura, en particular write(). Las variables miembros son declaradas con la palabra clave mutable porque estas son modificadas dentro de las funciones constantes. ManejadorCursor::ManejadorCursor() { estado = CabeceraAnterior; NumImagenActual = 0; numImagenes = 0; } Cuando el manejador es construido, comenzamos estableciendo su estado. Establecemos el nmero de la imagen actual del cursor para el primer cursor, pero como establecemos numImagenes en 0, es claro que no poseemos imgenes todava. bool ManejadorCursor::canRead() const { if (estado == CabeceraAnterior) { return device()->peek(4) == QByteArray("\0\0\2\0", 4); } else { return estado != Error; } } La funcin canRead() puede ser llamada en cualquier momento para determinar si el manejador de imagen puede leer ms datos desde el objeto (device). Si la funcin es llamada antes de que hayamos ledo cualquier dato, mientras estemos en el estado CabeceraAnterior, chequeamos por la signatura particular que identifique los archivos de cursores de Windows. El llamado a QIODevice::peek() lee los primero cuatro bytes sin cambiar el puntero al archivo del objeto (device). Si canRead() es llamada luego, retornamos true a menos que ocurra un error. int ManejadorCursor::NumeroDeImagenActual() const { return NumImagenActual; } Esta funcin trivial retorna el nmero del cursor en donde el puntero al archivo de objeto ( device) se encuentra posicionado. Una vez que el manejador es construido, es posible para el usuario llamar a cualquiera de sus funciones pblicas, en cualquier orden. Esto es un problema potencial ya que debemos asumir que solamente podemos leer en serie, as que necesitamos leer la cabecera del archivo una vez antes de hacer cualquier otra cosa. Resolveremos el problema haciendo un llamado a la funcin LeerCabeceraSiEsNecesario() en aquellas funciones que dependan de que la cabecera haya sido leda. int ManejadorCursor::ContarImagenes() const { LeerCabeceraSiEsNecesario(); return numImagenes; } Esta funcin retorna el nmero de imgenes en el archivo. Para un archivo vlido donde ningn error de lectura haya ocurrido, sta retornar un nmero mayor o igual a 1.

19. Creando Plugins

233

Figura 19.2 Formato de archivo .cur

La siguiente funcin es muy compleja, as que vamos a revisarla por partes: bool ManejadorCursor::leer(QImage *imagen) { LeerCabeceraSiEsNecesario(); if (estado != ImagenAnterior) return false; La funcin leer() lee los datos para cualquier imagen que comience en la posicin actual del puntero al objeto (device). Si la cabecera del archivo es leda satisfactoriamente, o despus de que una imagen haya sido leda y el puntero al objeto (device) est en el comienzo de otra imagen, podemos leer la siguiente imagen. quint32 tamao; quint32 ancho; quint32 alto; quint16 numPlanos; quint16 bitsPorPixel; quint32 compresion; QDataStream in(device()); in.setByteOrder(QDataStream::LittleEndian); in >> tamao; if (size != 40) { entrarEstadoError(); return false; } in >> ancho >> alto >> numPlanos >> bitsPorPixel >> alto /= 2; if (numPlanos != 1 || bitsPorPixel != 1 || compresion != 0) { entrarEstadoError(); return false; } in.skipRawData((tamao - 20) + 8); Creamos un QDataStream para leer el objeto (device). Debemos establecer el orden de bytes para encontrar el especificado por las especificaciones del formato de archivo .cur. No es necesario establecer un numero de versin para un QDataStream ya que el formato de nmeros enteros y de punto flotante no varan entre las versiones de data stream. A continuacin, leemos en varios tems de datos de cabecera del compresion;

19. Creando Plugins

234

cursor, y obviamos las partes irrelevantes de la cabecera y la tabla de colores de 8-bytes usando QDataStream::skipRawData(). Debemos darnos cuenta de todas las particularidades del formato por ejemplo, dividiendo entre dos (2) la altura, ya que el formato .cur proporciona una altura que es dos veces el alto de la altura de la imagen actual. Los valores bitsPorPixel y compresion son siempre 1 y 0 en un archivo .cur monocromtico. Si tenemos algn problema, llamamos a entrarEstadoError() y retornamos false. QBitArray xorBitmap = leerdBitmap(ancho, alto, in); QBitArray andBitmap = leerBitmap(ancho, alto, in); if (in.status() != QDataStream::Ok) { enterEstadoError (); return false; } Los siguientes tems en el archivo son dos bitmaps, uno es una mscara XOR y la otra una mscara AND. Las leemos dentro de QBitArray y no en QBitmaps. Por qu? Porque un QBitmap es una clase diseada para ser dibujada y pintadas sobre la pantalla, pero lo que necesitamos aqu es un arreglo plano de bits. Cuando hayamos terminado con la lectura del archivo, verificamos el estatus del QDataStream. Esto funciona as porque si un QDataStream entra en un estado de error, permanecer en ese estado y solo retornar ceros. Por ejemplo, si la lectura falla en el primer arreglo de bits, el intento de leer el segundo resultar en un QBitArray vacio. *imagen = QImage(ancho, alto, QImage::Format_ARGB32); for (int i = 0; i < int(alto); ++i) { for (int j = 0; j < int(ancho); ++j) { QRgb color; int bit = (i * ancho) + j; if (andBitmap.testBit(bit)) { if (xorBitmap.testBit(bit)) { color = 0x7F7F7F7F; } else { color = 0x00FFFFFF; } } else { if (xorBitmap.testBit(bit)) { color = 0xFFFFFFFF; } else { color = 0xFF000000; } } imagen->setPixel(j, i, color); } } Construimos una nueva QImage del tamao correcto y la asignamos a *imagen para que apunte a ella. Luego iteramos sobre cada pixel en los arreglos XOR y AND y los convertimos con las especificaciones de color 32-bit ARGB. Los arreglos AND y XOR son usados como se muestra en la siguiente tabla para obtener el color de cada pixel del cursor:

19. Creando Plugins

235

Los pixeles blancos, negros y transparentes no son un problema, pero no existe una manera de obtener un pixel de fondo invertido usando la especificacin de color ARGB sin saber el color original del pixel de fondo. Como un sustituto, usamos un color gris semitransparente (0x7F7F7F7F). ++NumImagenActual; if (NumImagenActual == numImagenes) estado = DespuesDeUltimaImagen; return true; } Una vez que terminemos de leer la imagen, actualizamos el nmero actual de imgenes y actualizamos el estado si ya hemos alcanzado la ltima imagen. En este punto, el objeto (device) ser posicionado en la siguiente imagen o al final del archivo. bool ManejadorCursor::SaltarASiguienteImagen() { QImage imagen; return leer(&imagen); } La funcin SaltarASiguienteImagen() es usada para saltar una imagen. Por simplicidad, simplemente llamamos a leer() e ignoramos la QImage resultante. Una implementacin ms eficiente usara la informacin guardada en la cabecera del archivo .cur para saltar directamente a la seccin apropiada en el archivo. void ManejadorCursor::LeerCabeceraSiEsNecesario() const { if (estado != CabeceraAnterior) return; quint16 reservado; quint16 tipo; quint16 cuenta; QDataStream in(device()); in.setByteOrder(QDataStream::LittleEndian); in >> reservado >> tipo >> cuenta; in.skipRawData(16 * cuenta); if (in.status() != QDataStream::Ok || reservado != 0 || tipo != 2 || cuenta == 0) { entrarEstadoError(); return; } estado = ImagenAnterior; NumImagenActual = 0; numImagenes = int(cuenta); } La funcin privada LeerCabeceraSiEsNecesario() es llamada desde ContarImagenes() y desde leer(). Si la cabecera del archivo ya ha sido leda, el estado no ser CabeceraAnterior y

19. Creando Plugins

236

retornamos inmediatamente. De otra forma, abrimos un data stream en el objeto (device), leemos en alguna data genrica (incluyendo el numero de cursores en el archivo), y establecemos el estado a ImagenAnterior. Al final, el puntero al archivo del objeto (device) es posicionado antes de la primera imagen. void ManejadorCursor::enterEstadoError () const { estado = Error; NumImagenActual = 0; numImagenes = 0; } Si ocurre un error, asumimos que no hay imgenes vlidas y establecemos el estado a Error. Una vez en el estado Error, El estado del manejador no puede cambiar. Vista del cdigo: QBitArray ManejadorCursor::leerBitmap(int ancho, int alto, QDataStream &in) const { QBitArray bitmap(ancho * alto); quint8 byte; quint32 palabra; for (int i = 0; i < alto; ++i) { for (int j = 0; j < ancho; ++j) { if ((j % 32) == 0) { palabra = 0; for (int k = 0; k < 4; ++k) { in >> byte; palabra = (palabra << 8) | byte; } } bitmap.setBit(((alto - i - 1) * ancho) + j, info & 0x80000000); palabra <<= 1; } } return bitmap; } La funcin leerBitmap() es usada para leer una mscara de cursor AND y una mscara de XOR. Estas mscaras tienen dos caractersticas inusuales. Primero, estas guardan las filas desde abajo hacia arriba, en lugar de usar el mtodo ms comn que es de arriba hacia abajo. Segundo, el endianness de la data aparece para ser revertido de ser usado en cualquier otra parte dentro de los archivos .cur. En vista de esto, debemos invertir la coordenada Y en el llamado a setBit(), y leemos en los valores de la mscara un byte a la vez, debemos ir alternando bits e ir usando mscaras para extraer sus valores correctos. Esto completa la implementacin del plugin de cursor de Windows. Los plugins para otro tipo de formatos de imagen deberan seguir el mismo modelo, aunque puede que se tengan que implementar ms contenido de la API de QImageIOHandler, en particular, las funciones usadas para la escritura de imgenes. Los plugins de otro tipo, por ejemplo, cdecs de texto o drivers de base de datos, siguen el mismo patrn de tener un contenedor de plugin que provea una API que la las aplicaciones puedan usar y un manejador para proporcionar la funcionalidad fundamental. El archivo .pro para plugins es diferente al de las aplicaciones, as que terminaremos con eso:

19. Creando Plugins

237

TEMPLATE = lib CONFIG += plugin HEADERS = manejadorcursor.h \ cursorplugin.h SOURCES = manejadorcursor.cpp \ cursorplugin.cpp DESTDIR = $(QTDIR)/plugins/imageformats Por defecto, los archivos .pro usan la plantilla app, pero aqu debemos especificar la plantilla lib porque un plugin es una librera, no una aplicacin stand-alone. La lnea CONFIG es usada para decirle a Qt que la librera no es slo una librera plana, sino una librera plugin. La lnea DESTDIR especifica el directorio donde el plugin debe ir. Todos los plugin Qt deben ir en el subdirectorio apropiado plugins donde Qt fue instalado, y como nuestro plugin provee un nuevo formato de imagen lo ponemos en plugins/imageformats. La lista de nombres de directorios y tipos plugins puede verse en http://doc.trolltech.com/4.1/plugins-howto.html. Para este ejemplo, asumimos que la variable de entorno QTDIR est establecida en el directorio donde Qt est instalado. Los plugins construidos por Qt en modo release y modo debug son diferentes, as que si ambas versiones de Qt estn instaladas, es mejor especificar cul de ellas usar en el archivo .pro por ejemplo, agregando la lnea CONFIG += release Las aplicaciones que usen plugins de Qt deben ser mostradas con los plugins que estn intentando usar. Los plugins de Qt deben estar ubicados en subdirectorios especficos (por ejemplo, imageformats para los formatos de imagen). Las aplicaciones Qt buscan plugins en el directorio plugins; en el directorio donde el ejecutable de la aplicacin reside, de modo que para plugins de imgenes ellas buscan el directorio aplicacin_dir/plugins/imageformats. Si queremos mostrar plugins Qt en un directorio diferente, el path de bsqueda de plugins puede ser aumentado usando QCoreApplication::addLibraryPath().

Haciendo Aplicaciones que Acepten Plugins


Un plugin de aplicacin es una librera dinmica que implementa una o ms interfaces. Una interfaz es una clase que consiste exclusivamente de funciones virtuales. La comunicacin entre la aplicacin y los plugins es hecha a travs de la tabla virtual de la interfaz. En esta seccin, nos centraremos en cmo usar un plugin en una aplicacin Qt a travs de sus interfaces, y en la siguiente seccin mostraremos cmo implementar un plugin. Para proporcionar un ejemplo concreto, crearemos la aplicacin sencilla llamada Text Art mostrada en la Figura 19.3. Los efectos de texto son proporcionados por plugins; la aplicacin retorna la lista de efectos de texto provista por cada plugin e itera sobre ellas para mostrarlas como un tem en un QListWidget. La aplicacin Text Art define una interfaz: class InterfazTextArt { public: virtual ~interfazTextArt() { } virtual QStringList efectos() const = 0; virtual QPixmap applicarEfecto(const QString &efecto, const QString &texto, const QFont &fuente, const QSize &tamao, const QPen &pluma, const QBrush &pincel) = 0; }; Q_DECLARE_INTERFACE(InterfazTextArt, "com.softwareinc.TextArt.InterfazTextArt/1.0")

19. Creando Plugins

238

Figura 19.3 La aplicacin Text Art

Una clase de interfaz normalmente declara un destructor virtual, una funcin virtual que retorna un QStringList, y una o ms funciones virtuales. El destructor est all primeramente para no molestar al compilador, quien pudiera de otra forma advertir sobre la falta de un destructor virtual en una clase que posee funciones virtuales. En este ejemplo, la funcin efectos() retorna una lista de los efectos de texto que el plugin puede proporcionar. Podemos pensar en esta lista como una lista de claves. Cada vez que llamemos a una de las otras funciones, pasamos una de esas claves como primer argumento, haciendo posible la implementacin de mltiples efectos en un solo plugin. Al final, usamos el macro Q_DECLARE_INTERFACE() para asociar un identificador a la interfaz. El identificador normalmente tiene cuatro componentes: un nombre de dominio invertido especificando el creador de la interfaz, el nombre de la aplicacin, el nombre de la interfaz, y un nmero de versin. En cualquier momento que modifiquemos la interfaz (p. e., si agregamos una funcin virtual o cambiamos la signatura de una funcin existente), debemos recordar que tenemos que incrementar el nmero de versin; de otra forma, la aplicacin puede colapsar tratando de acceder a un plugin desactualizado. La aplicacin es implementada en una clase llamada DialogoTextArt. Mostraremos solamente el cdigo que es relevante para hacer una aplicacin que sea sensible a plugins. Empecemos con el constructor: DialogoTextArt::DialogoTextArt (const QString &texto, QWidget *parent): QDialog(parent) { listWidget = new QListWidget; listWidget->setViewMode(QListWidget::IconMode); listWidget->setMovement(QListWidget::Static); listWidget->setIconSize(QSize(260, 80)); ... cargarPlugins(); llenarListWidget(texto); ... } El constructor crea un QListWidget para listar los efectos disponibles. Este llama a la funcin privada cargarPlugins() para encontrar y cargar cualquier plugin que implemente la clase InterfazTextArt y llena el list widget respectivamente llamando a otra funcin privada, llenarListWidget(). void DialogoTextArt::cargarPlugins() { QDir pluginDir(QApplication::applicationDirPath());

19. Creando Plugins

239

#if defined(Q_OS_WIN) if (pluginDir.dirName().toLower() == "debug" || pluginDir.dirName().toLower() == "release") pluginDir.cdUp(); #elif defined(Q_OS_MAC) if (pluginDir.dirName() == "MacOS") { pluginDir.cdUp(); pluginDir.cdUp(); pluginDir.cdUp(); } #endif if (!pluginDir.cd("plugins")) return; foreach (QString fileName,pluginDir.entryList(QDir::Files)){ QPluginLoader loader (pluginDir.absoluteFilePath(fileName)); if (TextArtInterface *interface = qobject_cast<TextArtInterface*>(loader.instance())) interfaces.append(interface); } } En cargarPlugins(), intentamos cargar todos los archivos en el directorio plugins de la aplicacin. La funcin directoryOf(). (En Windows, el ejecutable de la aplicacin usualmente reside en un subdirectorio release o debug, asi que nos movemos un directorio arriba.En Mac OS X, tomamos en cuenta la estructura del directorio entero). Si el archivo que estamos intentando cargar es un plugin de Qt que usa la misma versin de Qt que la aplicacin, QPluginLoader::instance() retornar un QObject * que apunta hacia un plugin de Qt. Usamos qobject_cast<T>() para verificar si el plugin implementa la clase InterfazTextArt. Cada vez que el cast sea exitoso, agregamos la interfaz a la lista de interfaces del DialogoTextArt (de tipo QList<InterfazTextArt* >). Algunas aplicaciones querrn cargar una o ms interfaces distintas. En estos casos, el cdigo para obtener las interfaces lucira algo como esto: QObject *plugin = loader.instance(); if (InterfazTextArt *i = qobject_cast<InterfazTextArt *>(plugin)) textArtInterfaces.append(i); if (BorderArtInterface *i = qobject_cast<BorderArtInterface *>(plugin)) borderArtInterfaces.append(i); if (TextureInterface *i = qobject_cast<TextureInterface *>(plugin)) textureInterfaces.append(i); El mismo plugin puede hacer cast para ms de un puntero de interfaz, ya que es posible para los plugins proporcionar mltiples interfaces usando herencia mltiple. Vista del Cdigo: void DialogoTextArt::llenarListWidget(const QString &texto) { QFont fuente("Helvetica", iconSize.height(), QFont::Bold); QSize tamIcono = listWidget->iconSize(); QPen pluma(QColor("darkseagreen"));

19. Creando Plugins

240

QLinearGradient gradiente(0, 0, iconSize.width() / 2, iconSize.height() / 2); gradiente.setColorAt(0.0, QColor("darkolivegreen")); gradiente.setColorAt(0.8, QColor("darkgreen")); gradiente.setColorAt(1.0, QColor("lightgreen")); foreach (InterfazTextArt *interfaz, interfaces) { foreach (QString efecto, interfaz->efectos()) { QListWidgetItem *item = new QListWidgetItem(efecto, listWidget); QPixmap pixmap = interfaz->apicarEfecto (efecto,texto, fuente,tamIcono,pluma,gradiente); item->setData(Qt::DecorationRole, pixmap); } } listWidget->setCurrentRow(0); } La funcin llenarListWidget() comienza con la creacin de algunas variables para pasarlas a la funcin aplicarEfecto(), en particular una fuente, una pluma, y un gradiente lineal. Luego se itera sobre cada InterfazTextArt que sea encontrado por cargarPlugins(). Para cada efecto proporcionado por cada interfaz, se crear un QListWidgetItem nuevo con su texto establecido con el nombre del efecto, y con un QPixmap creado usando aplicarEfecto(). En esta seccin, hemos visto cmo cargar plugins llamando a cargarPlugins() en el constructor, y cmo hacer uso de ellos en llenarListWidget(). El cdigo sale airoso si no hay plugins proporcionando objetos tipo InterfazTextArt, si se proporciona uno, o ms de uno. Adems, los plugins adicionales podran ser agregados luego: Cada vez que la aplicacin comience, debe cargar cuantos plugin encuentre que proporcionen las interfaces que la aplicacin quiera. Esto facilita la extensin de la funcionalidad de la aplicacin sin cambiar la aplicacin en s misma.

Escribiendo Plugins para Aplicaciones


Un plugin de aplicacin es una subclase de QObject y de las interfaces que quiera proporcionar. Los ejemplos que acompaan este libro incluyen dos plugins para la aplicacin Text Art presentada en la seccin anterior, para mostrar que la aplicacin maneja correctamente mltiples plugins. Aqu, revisaremos el cdigo para uno solo de ellos solamente, El plugin de Efectos Bsicos. Supondremos que el cdigo fuente del plugin est localizado en un directorio llamado pluginefectosbasicos y que la aplicacin Text Art est ubicada en un directorio paralelo llamado textart. Aqu est la declaracin de la clase del plugin: class PluginEfectosBasicos : public QObject, public InterfazTextArt{ Q_OBJECT Q_INTERFACES(InterfazTextArt) public: QStringList efectos() const; QPixmap aplicarEfectos(const QString &efecto, const QString &texto, const QFont &fuente, const QSize &tamao,const QPen &pluma, const QBrush &pincel); El plugin implementa solamente uan interfaz, InterfazTextArt. Adicionalmente a Q_OBJECT, debemos usar el macro Q_INTERFACES() para cada una de las interfaces que estn subclaseadas para asegurar la cooperacin sin dificultades entre moc y qobject_cast<T>().

19. Creando Plugins

241

QStringList PluginEfectosBasicos::efectos() const { return QStringList() << "Plano" << "Contorno" << "Sombra"; } La funcin efectos() retorna una lista de efectos de texto soportados por el plugin. Este plugin soporta tres efectos, as que solo retornamos una lista conteniendo los nombres de cada uno. La funcin aplicarEfecto() proporciona la funcionalidad del plugin y posee una complejidad leve, as que lo revisaremos por partes: QPixmap PluginEfectosBasicos::aplicarEfecto(const QString &efecto, const QString &texto, const QFont &fuente, const QSize &tamao, const QPen &pluma, const QBrush &pincel) { QFont miFuente = fuente; QFontMetrics medidas(miFuente); while ((medidas.width(texto) > tamao.width() || medidas.height() > tamao.height()) && miFuente.pointSize() > 9) { miFuente.setPointSize(miFuente.pointSize() - 1); medidas = QFontMetrics(miFuente); } Queremos asegurarnos de que el texto dado encajar en el tamao especificado, de ser posible. Por esta razn, usamos las medidas de la fuente para ver si el texto es muy grande para encajar, y si lo es, insertamos un ciclo donde reducimos el tamao de la fuente hasta encontrar un tamao que encaje, o hasta que lleguemos a los 9 puntos, nuestro tamao mnimo fijo. QPixmap pixmap(tamao); QPainter pintor(&pixmap); pintor.setFont(miFuente); pintor.setPen(pluma); pintor.setBrush(pincel); pintor.setRenderHint(QPainter::Antialiasing, true); pintor.setRenderHint(QPainter::TextAntialiasing, true); pintor.setRenderHint(QPainter::SmoothPixmapTransform, true); pintor.eraseRect(pixmap.rect()); Creamos un pixmap del tamao requerido y un pintor (QPainter) para pintar sobre el pixmap. Tambin establecemos algunas indicaciones de dibujado para asegurarnos de obtener los resultados tan suaves como sea posible. La llamada a eraseRect() limpia el pixmap con el color de fondo. if (efecto == "Plano") { pintor.setPen(Qt::NoPen); } else if (efecto == "Contorno") { QPen pluma(Qt::black); pluma.setWidthF(2.5); pintor.setPen(pluma); } else if (efecto == "Sombra") { QPainterPath path; pintor.setBrush(Qt::darkGray); path.addText(((tamao.width() medidas.width(texto)) / 2) + 3, (tamao.height() -medidas.descent()) + 3, miFuente,texto);

19. Creando Plugins

242

pintor.drawPath(path); pintor.setBrush(pincel); } Para el efecto Plano, ningn contorno es requerido. Para el efecto Contorno, ignoramos la pluma original y creamos nuestro propia pluma negra de 2.5 pixeles de ancho. Para el efecto Sombra, necesitamos dibujar la sombra primero para que el texto pueda ser pintado sobre ella. QPainterPath path; path.addText((tamao.width() - medidas.width(texto)) / 2, tamao.height() - medidas.descent(),mFuente, texto); pintor.drawPath(path); return pixmap; } Ahora tenemos la pluma y los pinceles establecidos como es debido para cada efecto de texto, y en el caso del efecto Sombra hemos dibujado la sombra. Ahora s estamos listos para dibujar el texto. El texto est centrado horizontalmente y dibujado lo suficientemente alejado de la parte baja del pixmap para permitir espacio para los caracteres descendientes (que hacen uso de espacio hacia abajo: por ejemplo g, y, j, etc.) Q_EXPORT_PLUGIN2(pluginefectosbasicos, PluginEfectosBasicos) Al final del archivo .cpp, usamos el macro Q_EXPOR_PLUGIN2() para hacer que el plugin est disponible para Qt. El archivo .pro es similar al que usamos para el plugin de cursor de Windows, anteriormente en este captulo: TEMPLATE = lib CONFIG += plugin HEADERS = ../textart/interfaztextart.h \ pluginefectosbasicos.h SOURCES = pluginefectosbasicos.cpp DESTDIR = ../textart/plugins Si este captulo ha agudizado tu apetito por los plugins de aplicacin, deberas estudiar los ejemplos ms avanzados de Plug & Paint provistos con Qt. La aplicacin soporta tres interfaces diferentes e incluye un dialogo muy til de Informacin del Plugin que lista los plugins y las interfaces que estn disponibles para la aplicacin.

20. Caractersticas Especficas de Plataformas

243

20. Caractersticas Especficas de Plataformas

Haciendo Interfaces con APIs Nativas Usando Activex en Windows Manejando la Administracin de Sesin en X11

En este captulo, haremos una revisin de las opciones especficas de plataformas (o de plataformas especificas) disponibles para los programadores en Qt. Comenzamos viendo cmo acceder a las APIs nativas, como la API Win32 de Windows, la API Carbon de Mac OS X y Xlib en X11. Luego avanzaremos con la exploracin de la extensin ActiveQt, mostrando cmo usar los controles ActiveX junto con aplicaciones Qt/Windows y cmo crear aplicaciones que acten como servidores ActiveX. En la ltima seccin, explicaremos cmo hacer que las aplicaciones Qt cooperen con el administrador de sesin en X11. Adicionalmente a las caractersticas presentadas aqu, Trolltech ofrece muchas soluciones de plataformas especficas, incluyendo los frameworks de migracin Qt/Motif y Qt/MFC para facilitar la migracin de aplicaciones Motif/Xt y MFC a Qt. Una extensin similar para aplicaciones Tcl/Tk es proporcionada por froglogic, y un convertidor de recursos Microsoft Windows est disponible desde Klarlvdalens Datakonsult. Vaya a las siguientes pginas web para ms detalles: http://www.trolltech.com/products/solutions/catalog/ http://www.froglogic.com/tq/ http://www.kdab.net/knut/

Para el desarrollo embebido, Trolltech ofrece la aplicacin de plataforma Qtopia. Esta es estudiada en el Capitulo 21.

Creando Interfaces con APIs Nativas


La API de Qt satisface la mayora de las necesidades en todas las plataformas, pero en algunas circunstancias, quiz queramos usar la API especifica de la plataforma. En esta seccin, mostraremos cmo usar las APIs nativas para las diferentes plataformas soportadas por Qt para realizar tareas particulares. En cada plataforma, QWidget proporciona una funcin windId() que retorna el ID de la ventana o del manejador. QWidget tambin provee una funcin esttica llamada find() que retorna el QWidget con un ID de ventana particular. Podemos pasar este ID a las funciones nativas del API para obtener efectos especficos de la plataforma. Por ejemplo, el siguiente cdigo usa winId() para mover la barra de titulo de una ventana de herramientas a la izquierda usando funciones nativas de Mac OS X: #ifdef Q_WS_MAC ChangeWindowAttributes(HIViewGetWindow(HIViewRef(toolWin. winId())),kWindowSideTitlebarAttribute,kWindowNoAttributes); #endif

20. Caractersticas Especficas de Plataformas

244

Figura 20.1 Una ventana de herramientas en Mac OS X con la barra de titulo al lado

En X11, aqu est la manera cmo modificaramos una propiedad de ventana: #ifdef Q_WS_X11 Atom atom = XInternAtom(QX11Info::display(), "MY_PROPERTY", False); long data = 1; XChangeProperty(QX11Info::display(), window->winId(), atom, atom,32, PropModeReplace, reinterpret_cast<uchar *>(&data), 1); #endif Las directivas #ifdef y #endif que encierran el cdigo especfico para la plataforma, asegura que la aplicacin seguir compilando en otras plataformas. Para una aplicacin slo para Windows, aqu hay un ejemplo de cmo podemos usar llamados GDI para dibujar un widget Qt: void GdiControl::paintEvent(QPaintEvent * /* evento */) { RECT rect; GetClientRect(winId(), &rect); HDC hdc = GetDC(winId()); FillRect(hdc, &rect, HBRUSH(COLOR_WINDOW + 1)); SetTextAlign(hdc, TA_CENTER | TA_BASELINE); TextOutW(hdc, width() / 2, height() / 2, text.utf16(), text.size()); ReleaseDC(winId(), hdc); } Para este trabajo, debemos re implementar el mtodo QPaintDevice::paintEngine() para retornar un puntero nulo y establecer el atributo Qt::WA_PaintOnScreen en el constructor del widget. El prximo ejemplo muestra cmo combinar QPainter y llamadas GDI en un manejador de eventos de pintado usando las funciones getDC() y releaseDC() de QPAintEngine: void MyWidget::paintEvent(QPaintEvent * /* evento */) { QPainter painter(this); painter.fillRect(rect().adjusted(20, 20, -20, -20), Qt::red); #ifdef Q_WS_WIN HDC hdc = painter.paintEngine()->getDC(); Rectangle(hdc, 40, 40, width() - 40, height() - 40); painter.paintEngine()->releaseDC();

20. Caractersticas Especficas de Plataformas

245

#endif } Combinando QPainter y llamados GDI de esta forma, algunas veces induce a resultados extraos, especialmente cuando las llamadas a QPainter ocurren despus de los llamados GDI, porque QPainter hace algunas conjeturas o suposiciones acerca del estado de la capa bsica de dibujo. Qt define uno de las siguientes cuatro smbolos sistemas de ventanas: Q_WS_WIN, Q_WS_X11, Q_WS_MAC y Q_WS_QWS (Qtopia). Debemos incluir al menos una cabecera Qt antes de que podamos usarlas en las aplicaciones. Qt tambin proporciona smbolos de procesamiento para identificar el sistema operativo: Q_OS_AIX Q_OS_BSD4 Q_OS_BSDI Q_OS_CYGWIN Q_OS_DGUX Q_OS_DYNIX Q_OS_FREEBSD Q_OS_HPUX Q_OS_HURD Q_OS_IRIX Q_OS_LINUX Q_OS_LYNX Q_OS_MAC Q_OS_NETBSD Q_OS_OPENBSD Q_OS_OS2EMX Q_OS_OSF Q_OS_QNX6 Q_OS_QNX Q_OS_RELIANT Q_OS_SCO Q_OS_SOLARIS Q_OS_ULTRIX Q_OS_UNIXWARE Q_OS_WIN32 Q_OS_WIN64 Podemos asumir que, a lo sumo, una de estas ser definida. Por conveniencia, Qt tambin define Q_OS_WIN cuando Win32 o Win64 es detectado, y Q_OS_UNIX cuando un sistema operativo basado en Unix (incluyendo Linux y Mac OS X) es detectado. En tiempo de ejecucin, podemos verificar QSysInfo::WindowsVersion o QSystemInfo::MacintoshVersion para distinguir entre diferentes versiones de Windows (2000,ME, etc) o Mac OS X (10.2, 10.3, etc.) . Adicionalmente a los macros de sistemas operativos y de ventanas, existe un conjunto de macros de compilador. Por ejemplo, Q_CC_MSVC es definido si el compilador es Microsoft Visual C++. Estos pueden ser muy tiles para trabajar sin complicaciones por lo bugs del compilador. Muchos de las clases de Qt relacionadas a GUIs proporcionan funciones de plataformas especficas que retornan los manejadores de bajo nivel al objeto bsico. Estas estn listadas en la Figura 20.2.

20. Caractersticas Especficas de Plataformas

246

Figura 20.2 Funciones especificas de plataformas para acceder a los manejadores de bajo nivel

En X11, QPixmap::x11Info() y QWidget::x11Info() retorna un objeto QX11Info que proporciona varios punteros o manejadores, tales como display(), screen(), colormap() y visual(). Podemos usar estos para establecer un contexto grfico X11 en un QPixmap o QWidget, por ejemplo. Las aplicaciones Qt que necesiten en su interfaz otros kits de herramientas o libreras necesitan muy frecuentemente acceder a los eventos de bajo nivel (XEvents en X11, MSGs en Windows, EventRef en Mac OS X, QWSEventes en Qtopia) antes de ellos sean convertidos en QEvents. Podemos hacer esto haciendo una subclase de QApplication y re implementar los filtros de eventos especficos de plataforma ms relevantes, uno de x11EventFilter(), winEventFilter(), macEventFilter() y qwsEventFilter(). Alternativamente, podemos acceder a los eventos especficos de plataforma que son enviados a un QWidget dado por medio de la re implementacin de x11Event(), winEvent(), y qwsEvent(). Esto puede ser muy til para manejar ciertos tipos de eventos que Qt normalmente ignora, como los eventos de joystick. Para ms informacin acerca de los asuntos especficos de plataformas, incluyendo cmo desarrollar aplicaciones Qt en diferentes plataformas, vea http://doc.trolltech.com/4.1/winsystem.html.

20. Caractersticas Especficas de Plataformas

247

Usando Activex en Windows


La tecnologa ActiveX de Microsoft permite a las aplicaciones incorporar componentes de interfaz de usuario proporcionados por otras aplicaciones o libreras. Esta construido en Microsoft COM y define un conjunto de interfaces para las aplicaciones que usen componentes y otro conjunto de interfaces para aplicaciones y libreras que proveen componentes. La Edicin de Escritorio (Qt/Windows Desktop Edition) Qt/Windows proporciona el framework ActiveQt para combinar limpiamente ActiveX y Qt. ActiveQt consta de dos mdulos: El mdulo QAxContainer nos permite usar objetos COM e incrustar embebidamente controles ActiveX en aplicaciones Qt. El mdulo QAxServer nos permite exportar objetos COM personalizados y controles AciveX escritos usando Qt.

Nuestro primer ejemplo incrustara embebidamente el Reproductor Windows Media (Windows Media Player) en una aplicacin Qt usando el modulo QAxContainer. La aplicacin Qt agrega un botn Open, un botn Play/Pausa, un botn Stop y un slider al control ActiveX del Reproductor Windows Media. Figura 20.3 La aplicacin Media Player

La ventana principal de la aplicacin es de tipo PlayerWindow: class PlayerWindow : public QWidget { Q_OBJECT Q_ENUMS(ReadyStateConstants) public: enum PlayStateConstants { Stopped = 0, Paused = 1, Playing=2 }; enum ReadyStateConstants { Uninitialized = 0, Loading = 1, Interactive = 3, Complete = 4 }; PlayerWindow(); protected: void timerEvent(QTimerEvent *event); private slots: void onPlayStateChange(int oldState, int newState); void onReadyStateChange(ReadyStateConstants readyState); void onPositionChange(double oldPos, double newPos);

20. Caractersticas Especficas de Plataformas

248

void sliderValueChanged(int newValue); void openFile(); private: QAxWidget *wmp; QToolButton *openButton; QToolButton *playPauseButton; QToolButton *stopButton; QSlider *seekSlider; QString fileFilters; int updateTimer; }; La clase PlayerWindow hereda de QWidget. El macro Q_ENUMS() (justo debajo de Q_OBJECT) es necesario para decirle al moc que el tipo ReadyStateConstants usado en el slot onReadyStateChange() es un tipo enum. En la seccin de datos privados, declaramos un dato miembro QAxWidget *: PlayerWindow::PlayerWindow() { wmp = new QAxWidget; wmp->setControl("{22D6F312-B0F6-11D0-94AB-0080C74C7E95}"); En el constructor, comenzamos con la creacin de un objeto QAxWidget para encapsular el control ActiveX Windows Media Player. El modulo QAxContainer consta de tres clases: QAxObject que encapsula un objeto COM, QAxWidget que encapsula un control ActiveX y QAxBase que implementa el ncleo de funcionalidad COM para QAxObject y QAxWidget. Llamamos a setControl() en el objeto QAxWidget (wmp) con el ID de la clase del control de Windows Media Player 6.4. Esto creara una instancia del componente requerido. A partir de all, todas las propiedades, eventos y mtodos del control ActiveX estn disponibles como propiedades Qt, signals y slots mediante el objeto QAxWidget. Figura 20.4 rbol de herencia del mdulo QAxContainer

Los tipos de datos COM son convertidos automticamente a los tipos de datos Qt correspondientes, como se resume en la Figura 20.5. Por ejemplo, un parmetro de entrada de tipo VARIANT_BOOL se transforma en bool, y un parmetro de salida de tipo VARIANT_BOOL se convierte en bool &. Si el tipo resultante es una clase Qt (QString, QDateTime, etc.), el parmetro de entrada es una referencia constante (por ejemplo, const QString &). Para obtener la lista de todas las propiedades, signals y slots disponibles en un objeto QAxObject o un QAxWidget con sus tipos de datos Qt, llame a QAxBase::generateDocumentation() o use la herramienta de lnea de comando de Qt dumpdoc, ubicada en el directorio de Qt tools\activeqt\dumpdoc. Continuemos con el constructor de PlayerWindow: wmp->setProperty("ShowControls", false); wmp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

20. Caractersticas Especficas de Plataformas

249

connect(wmp, SIGNAL(PlayStateChange(int, int)), this, SLOT(onPlayStateChange(int, int))); connect(wmp, SIGNAL(ReadyStateChange(ReadyStateConstants)), this,SLOT(onReadyStateChange(ReadyStateConstants))); connect(wmp, SIGNAL(PositionChange(double, double)), this, SLOT(onPositionChange(double, double))); Figura 20.5 Relacin entre tipos COM y tipos Qt

Despus del llamado a QAxWidget::setControl(), llamamos a QObject::setProperty() para establecer la propiedad ShowControls del Reproductor Windows Media a false, puesto que proveeremos nuestros propios botones para manipular el componente. QObject::setProperty() puede ser usado tanto para propiedades COM como para propiedades Qt normales. Su segundo parmetro es un tipo QVariant. Ahora, llamamos a setSizePolicy() para hacer que el control ActiveX tome todo el espacio disponible en el layout, y conectamos tres eventos ActiveX del componente COM a tres slots. stopButton = new QToolButton; stopButton->setText(tr("&Stop")); stopButton->setEnabled(false); connect(stopButton, SIGNAL(clicked()), wmp, SLOT(Stop())); } El resto del constructor de PlayerWindow sigue el patrn usual, excepto que conectamos algunas seales Qt a slots proporcionados por el objeto COM (Play(), Pause() y Stop()). Como los botones son similares, nosotros hemos mostrado aqu solamente la implementacin del botn Stop. Dejemos al constructor y veamos la funcin timerEvent():

20. Caractersticas Especficas de Plataformas

250

void PlayerWindow::timerEvent(QTimerEvent *event) { if (event->timerId() == updateTimer) { double curPos=wmp->property("CurrentPosition").toDouble(); onPositionChange(-1, curPos); } else { QWidget::timerEvent(event); } } La funcin timerEvent() es llamada en intervalos regulares mientras un clip media este siendo reproducido. Nosotros lo usamos para adelantar el slider. Esto se hace llamando a property() en el control ActiveX para obtener el valor de la propiedad CurrentPosition como una QVariant y llamando a toDouble() para convertirlo a double. Luego llamamos a el mtodo onPositionChange() para realizar la actualizacin. No vamos a revisar el resto del cdigo porque la mayora de este no es directamente relevante para el tema de ActiveX y no muestra nada que no hayamos cubierto ya. En el archivo .pro, necesitamos esta entrada para enlazar con el modulo QAxContainer: CONFIG += qaxcontainer Una necesidad muy frecuente cuando se trata con Objetos COM es la capacidad de llamar un mtodo COM directamente (contrario a conectarlo a un signal de Qt). La manera ms fcil de hacer esto es invocar QAxBase::dynamicCall() con el nombre y la firma de el mtodo como primer parmetro y los argumentos al mtodo como parmetros adicionales. Por ejemplo: wmp->dynamicCall("TitlePlay(uint)", 6); La funcin dynamicCall()ocupa hasta 8 parmetros de tipo QVariant y retorna una QVariant. Si necesitamos pasar un IDispatch * o un IUnknown * asi, podemos encapsular el componente en un QAxObject y llamamos a asVariant() en este para convertitlo a una QVariant. Si necesitamos llamar a un mtodo COM que retorne un IDispatch * o un IUnknown, o si necesitamos acceder a una propiedad COM de uno de esos tipos, entonces podemos usar querySubObject() en lugar de hacer lo anterior: QAxObject *session = outlook.querySubObject("Session"); QAxObject *defaultContacts = session->querySubObject ("GetDefaultFolder(OlDefaultFolders)", "olFolderContacts"); Si queremos llamar mtodos que tengan tipos de datos no soportados en su lista de parmetros, podemos usar QAxBase::queryInterface() para recuperar la interfaz COM y llamar el mtodo directamente. Como es costumbre con COM, debemos llamar a Release() cuando hayamos finalizado usando la interfaz. Si necesitamos llamar muy a menudo tales mtodos, podemos hacer una subclase de QAxObject o de QAxWidget y proporcionar funciones miembros que encapsulen los llamados a las interfaces COM. Nosotros sabemos que las subclases QAxObject y QAxWidget no pueden definir sus propias propiedades, seales o slots. Ahora revisaremos el modulo QAxServer. Este modulo nos habilita para convertir un programa Qt estndar a un servidor ActiveX. El servidor puede tambin ser una librera compartida o una aplicacin stand-alone. Los servidores construidos como libreras compartidas son llamados a menudo servidores dentro-de-proceso; las aplicaciones stand-alone son llamadas servidores fuera-de-proceso. Nuestro primer ejemplo QAxServer es un servidor dentro-de-proceso que proporciona un widget que muestra una bola rebotando de izquierda a derecha. Tambin veremos cmo incrustar el widget en Internet Explorer. Aqu est el comienzo de la definicin de la clase del widget AxBouncer:

20. Caractersticas Especficas de Plataformas

251

class AxBouncer : public QWidget, public QAxBindable { Q_OBJECT Q_ENUMS(SpeedValue) Q_PROPERTY(QColor color READ color WRITE setColor) Q_PROPERTY(SpeedValue speed READ speed WRITE setSpeed) Q_PROPERTY(int radius READ radius WRITE setRadius) Q_PROPERTY(bool running READ isRunning) AxBouncer hereda tanto de QWidget y QAxBindable. La clase QAxBindable proporciona una interfaz entre el widget y un cliente ActiveX. Cualquier QWidget puede ser exportado como un control ActiveX, pero por medio de subclasear a QAxBindable podemos notificar al cliente cuando un valor de una propiedad cambia, y podemos implementar interfaces COM para suplementar aquellas que ya estn implementadas por QAxServer. Cuando hacemos herencia mltiple incluyendo una clase derivada de QObject, debemos poner siempre primero la clase derivada de QObject de manera que moc pueda recogerla. Figura 20.6 El widget AxBouncer en Internet Explorer

Declaramos tres propiedades de lectura-escritura y una propiedad de solo-lectura. El macro Q_ENUMS() es necesario para decirle a moc que el tipo SpeedValue es un tipo enum. El enum es declarado en la seccin pblica de la clase: public: enum SpeedValue { Slow, Normal, Fast }; AxBouncer(QWidget *parent = 0); void setSpeed(SpeedValue newSpeed); SpeedValue speed() const { return ballSpeed; } void setRadius(int newRadius); int radius() const { return ballRadius; } void setColor(const QColor &newColor); QColor color() const { return ballColor; } bool isRunning() const { return myTimerId != 0; } QSize sizeHint() const; QAxAggregated *createAggregate(); public slots: void start(); void stop(); signals: void bouncing();

20. Caractersticas Especficas de Plataformas

252

El constructor de AxBouncer es un constructor estndar para un widget, con parmetro parent. El macro QAXFACTORY_DEFAULT(), el cual usaremos para exportar el componente, espera un constructor con esta firma. La funcin createAggregate() es re implementada desde QAxBindable. Lo explicaremos en un momento. protected: void paintEvent(QPaintEvent *event); void timerEvent(QTimerEvent *event); private: int intervalInMilliseconds() const; QColor ballColor; SpeedValue ballSpeed; int ballRadius; int myTimerId; int x; int delta; }; Las secciones privadas y protegidas de la clase son las mismas que aquellas que tendramos si esto fuera un widget Qt estndar. AxBouncer::AxBouncer(QWidget *parent) : QWidget(parent) { ballColor = Qt::blue; ballSpeed = Normal; ballRadius = 15; myTimerId = 0; x = 20; delta = 2; } El constructor de AxBouncer inicializa las variables privadas de la clase. void AxBouncer::setColor(const QColor &newColor) { if (newColor!= ballColor && equestPropertyChange("color")){ ballColor = newColor; update(); propertyChanged("color"); } } La funcin setColor() establece el valor de la propiedad color. Esta llama a update() para redibujar el widget. La parte inusual son los llamados a requestPropertyChange() y a propertyChanged(). Estas funciones son heredadas desde QAxBindable y deberan ser idealmente llamadas cuando sea que cambie una propiedad. El mtodo requestPropertyChange() le solicita permiso al cliente para cambiar una propiedad, y retorna true si el cliente permite el cambio. La funcin propertyChanged() notifica al cliente que la propiedad ha sido cambiada. Las propiedades seteadoras setSpeed() y setRadius() tambin siguen este patrn, y de igual forma lo hacen los slots start() y stop(), ya que estos cambian el valor de la propiedad running. Aun queda una funcin miembro de AxBouncer muy interesante:

20. Caractersticas Especficas de Plataformas

253

QAxAggregated *AxBouncer::createAggregate() { return new ObjectSafetyImpl; } La funcin createAggregate() es re implementada desde QAxBindable. Esta nos permite implementar interfaces COM que el modulo QAxSever no ha implementado o para evadir las interfaces COM por defectos del modulo QAxServer. Aqu, lo hacemos para proporcionar la interface IObjectSafety, la cual es usada por Internet Explorer para acceder a las opciones de seguridad de un componente. Este es el truco estndar para deshacerse del infame mensaje de error de Internet Explorer: Object not safe for scrpting. Aqu est la definicin de la clase que implementa la interface IObjectSafety(): class ObjectSafetyImpl : public QAxAggregated, public IObjectSafety { public: long queryInterface(const QUuid &iid, void **iface); QAXAGG_IUNKNOWN HRESULT WINAPI GetInterfaceSafetyOptions(REFIID riid, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions); HRESULT WINAPI SetInterfaceSafetyOptions(REFIID riid, DWORD pdwSupportedOptions, DWORD pdwEnabledOptions); }; La clase ObjectSafetyImpl hereda tanto de QAxAggregated como de IObjectSafety. La clase QAxAggregated es una clase base abstracta para implementaciones de interfaces COM adicionales. El objeto COM que QAxAggregated extiende es accesible a travs de controllingUnknown(). Este objeto COM es creado detrs de escena por el modulo QAxServer. El macro QAXAGG_IUNKNOWN provee implementaciones estndares de QueryInterface(), AddRef() y Release(). Estas implementaciones simplemente llaman a las mismas funciones sobre el objeto COM controlante. long ObjectSafetyImpl::queryInterface(const QUuid &iid, void **iface) { *iface = 0; if (iid == IID_IObjectSafety) { *iface = static_cast<IObjectSafety *>(this); } else { return E_NOINTERFACE; } AddRef(); return S_OK; } La funcin queryInterface() es una funcin virtual pura de QAxAggregated. Esta es llamada por el objeto COM controlador para darle acceso a las interfaces proporcionadas por la subclase de QAxAggregated. Debemos retornar E_NOINTERFACE para interfaces que no implementamos y para IUnknown. HRESULT WINAPI ObjectSafetyImpl::GetInterfaceSafetyOptions( REFIID /* riid */, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions) { *pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_DATA

20. Caractersticas Especficas de Plataformas

254

| NTERFACESAFE_FOR_UNTRUSTED_CALLER; *pdwEnabledOptions = *pdwSupportedOptions; return S_OK; } HRESULT WINAPI ObjectSafetyImpl::SetInterfaceSafetyOptions( REFIID /* riid */, DWORD /* pdwSupportedOptions */, DWORD /* pdwEnabledOptions */) { return S_OK; } Las funciones GetInterfaceSafetyOptions() y SetInterfaceSafetyOptions() son declaradas en IObjectSafety. Nosotros la implementamos para decirle al mundo que nuestro objeto es seguro para scripting. Revisemos ahora el main.cpp: #include <QAxFactory> #include "axbouncer.h" QAXFACTORY_DEFAULT(AxBouncer, "{5e2461aa-a3e8-4f7a-8b04-307459a4c08c}", "{533af11f-4899-43de-8b7f-2ddf588d1015}", "{772c14a5-a840-4023-b79d-19549ece0cd9}", "{dbce1e56-70dd-4f74-85e0-95c65d86254d}", "{3f3db5e0-78ff-4e35-8a5d-3d3b96c83e09}") El macro QAXFACTORY_DEFAULT() exporta un control ActiveX. Podemos usarlo para servidores ActiveX que exporten solamente un control. El siguiente ejemplo en esta seccin mostrara como exportar muchos controles ActiveX. El primer argumento de QAXFACTORY_DEFAULT() es el nombre de la clase Qt a exportar. Este es tambin el nombre bajo el cual el control es exportado. Los otros cinco argumentos son el ID de la clase, el ID de la interfaz, el ID del evento de interfaz, el ID del tipo de librera y el ID de la aplicacin. Podemos usar herramientas estndares como guidgen o uuidgen para generar estos identificadores. Como el server es una librera, no necesitamos una funcin main(). Aqu est el archivo .pro para nuestro servidor ActiveX en-proceso: TEMPLATE CONFIG HEADERS SOURCES RC_FILE DEF_FILE = lib += dll qaxserver = axbouncer.h \ objectsafetyimpl.h = axbouncer.cpp \ main.cpp \ objectsafetyimpl.cpp = qaxserver.rc = qaxserver.def

Los archivos qaxserver.rc y qaxserver.def referido en el archivo .pro son archivos estndares que pueden ser copiados desde el directorio de Qt src\activeqt\control. El archivo makefile o archivo de proyecto Visual C++ generado por qmake contiene reglas para registrar el servidor en el registro de Windows. Para registrar el servidor en maquinas de usuarios finales, podemos usar la herramienta regsvr32 disponible en todos los sistemas Windows. Podemos incluir el componente Bouncer en una pgina HTML usando la etiqueta <object>:

20. Caractersticas Especficas de Plataformas

255

<object id="AxBouncer" classid="clsid:5e2461aa-a3e8-4f7a-8b04-307459a4c08c"> <b>El control ActiVeX no est disponible. Asegurate de haber construido y registrado el servidor componente.</b> </object> Podemos crear botones que invoquen slots: <input type="button" value="Start" onClick="AxBouncer.start()"> <input type="button" value="Stop" onClick="AxBouncer.stop()"> Nosotros podemos manipular el widget usando JavaScript o VBScript como cualquier otro control ActiveX. Nuestro ltimo ejemplo es una aplicacin de Libro de Direcciones (Address Book) escriptable. La aplicacin puede servir como una aplicacin Qt/Windows estndar o un servidor ActiveX fuera-de-proceso. La otra posibilidad nos permite escriptar la aplicacin usando, por ejemplo, Visual Basic. class AddressBook : public QMainWindow { Q_OBJECT Q_PROPERTY(int count READ count) Q_CLASSINFO("ClassID", "{588141ef-110d-4beb-95abee6a478b576d}") Q_CLASSINFO("InterfaceID", "{718780ec-b30c-4d88-83b379b3d9e78502}") Q_CLASSINFO("ToSuperClass", "AddressBook") public: AddressBook(QWidget *parent = 0); ~AddressBook(); int count() const; public slots: ABItem *createEntry(const QString &contact); ABItem *findEntry(const QString &contact) const; ABItem *entryAt(int index) const; private slots: void addEntry(); void editEntry(); void deleteEntry(); private: void createActions(); void createMenus(); QTreeWidget *treeWidget; QMenu *fileMenu; QMenu *editMenu; QAction *exitAction; QAction *addEntryAction; QAction *editEntryAction; QAction *deleteEntryAction; }; El widget AddressBook es la ventana principal de la aplicacin. La propiedad y los slots que este provee estarn disponibles para escripting. El macro Q_CLASSINFO() es usado para especificar los ID de la clase y de la interfaz asociadas con la clase. Estas fueron generadas usando una herramienta tal como guid o uuid. En el ejemplo anterior, especificamos los ID de la clase y de la interfaz cuando exportamos la clase QAxBouncer usando el macro QAXFACTORY_DEFAULT(). En este ejemplo, queremos exportar muchas clases, de manera que no podremos usar el macro QAXFACTORY_DEFAULT(). Existen dos opciones disponibles para nosotros:

20. Caractersticas Especficas de Plataformas

256

Podemos hacer subclases de QAxFactory, re implementando sus funciones virtuales para proporcionar informacin acerca de los tipos que queremos exportar, y usar el macro QAXFACTORY_EXPORT() para registrar la fbrica. Podemos usar los macros QAXFACTORY_BEGIN(), QAXFACTORY_END(), QAXCLASS() y QAXTYPE() para declarar y registrar la fabrica. Este mtodo requiere que especifiquemos los ID de la clase y de la interfaz usando Q_CLASSINFO().

De vuelta a la definicin de la clase AddressBook: la tercera ocurrencia de Q_CLASSINFO() puede parecer un poco misterioso. Por defecto, los controles ActiveX exponen no solo sus propias propiedades, seales y slots a los clientes, sino tambin aquellos de sus superclases hasta QWidget. El atributo toSuperClass nos permite especificar la clase ms alta (en el rbol de herencia) que queremos mostrar. Aqu, especificamos el nombre de la clase del componente (AddressBook) como la superclase ms alta a exportar, significando esto que las propiedades, seales, y slots definidos en las superclases de AddresBook no sern exportadas. class ABItem : public QObject, public QTreeWidgetItem { Q_OBJECT Q_PROPERTY(QString contact READ contact WRITE setContact) Q_PROPERTY(QString address READ address WRITE setAddress) Q_PROPERTY(QString phoneNumber READ phoneNumber WRITE setPhoneNumber) Q_CLASSINFO("ClassID", "{bc82730e-5f39-4e5c-96be461c2cd0d282}") Q_CLASSINFO("InterfaceID", "{c8bc1656-870e-48a9-9937fbe1ceff8b2e}") Q_CLASSINFO("ToSuperClass", "ABItem") public: ABItem(QTreeWidget *treeWidget); void setContact(const QString &contact); QString contact() const { return text(0); } void setAddress(const QString &address); QString address() const { return text(1); } void setPhoneNumber(const QString &number); QString phoneNumber() const { return text(2); } public slots: void remove(); }; La clase ABItem representa una entrada en el libro de direcciones. Este hereda desde QTreeWidgetItem de manera que puede ser mostrado en un QTreeWidget y tambin hereda de QObject as que puede ser exportado como un objeto COM. int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!QAxFactory::isServer()) { AddressBook addressBook; addressBook.show(); return app.exec(); } return app.exec(); } En la funcin main(), verificamos si la aplicacin est siendo ejecutada como stand-alone o como un servidor. La opcin de lnea de comando activex es reconocida por QApplication y hace que la

20. Caractersticas Especficas de Plataformas

257

aplicacin corra como un servidor. Si la aplicacin no corre como un servidor, creamos el widget principal y lo mostramos como lo haramos normalmente en una aplicacin stand-alone de Qt. Adicionalmente a activex, los servidores ActiveX entienden las siguientes opciones de lnea de comandos: -regserver registra el servidor en el registro del sistema. -unregserver quita del registro del sistema al servidor. -dumpidlfile escribe el IDL del servidor al archivo especificado.

Cuando la aplicacin es ejecutada como un servidor, debemos exportar las clases AddressBook y ABItem como componentes COM: QAXFACTORY_BEGIN("{2b2b6f3e-86cf-4c49-9df5-80483b47f17b}", "{8e827b25-148b-4307-ba7d-23f275244818}") QAXCLASS(AddressBook) QAXTYPE(ABItem) QAXFACTORY_END() Los macros de arriba exportan una fbrica para crear objetos COM. Ya que queremos exportar dos tipos de objetos COM, no podemos usar simplemente QAXFACTORY_DEFAULT() como lo hicimos en el ejemplo anterior. El primer argumento a QAXFACTORY_BEGIN() es el ID del tipo de librera; el segundo argumento es el ID de la aplicacin. Entre QAXFACTORY_BEGIN() y QAXFACTORY_END(), especificamos todas las clases que pueden ser instanciadas y todos los tipos de datos que queremos hacer accesibles como objetos COM. Este es el archivo .pro para nuestro servidor ActiveX fuera-de-proceso: TEMPLATE CONFIG HEADERS SOURCES = app += qaxserver = abitem.h \ addressbook.h \ editdialog.h = abitem.cpp \ addressbook.cpp \ editdialog.cpp \ main.cpp = editdialog.ui = qaxserver.rc

FORMS RC_FILE

El archivo qaxserver.rc referido en el archivo .pro es un archivo estndar que puede ser copiado desde el directorio de Qt src\activeqt\control. Mira en el directorio de ejemplos vb para un proyecto de Visual Basic que use el servidor de Libro de Direcciones (Address Book). Esto completa nuestra revisin al framework ActiveQt. La distribucin de Qt incluye ejemplos adicionales, y la documentacin contiene informacin acerca de cmo construir los mdulos QAxContainer y QAxServer y cmo resolver asuntos comunes de interoperabilidad.

Manejando la Administracin de Sesin en X11


Cuando cerramos sesin en X11, algunos manejadores de ventanas nos preguntan si queremos guardar la sesin. Si decimos que si, las aplicaciones que estaban ejecutndose son automticamente reiniciadas la siguiente vez que iniciemos sesin, con la misma posicin de pantalla e, idealmente, con el mismo estado que stas tenan cuando cerramos sesin.

20. Caractersticas Especficas de Plataformas

258

El componente especifico de X11 que se encarga de guardar y restaurar la sesin es llamado el administrador de sesin. Para hacer una aplicacin Qt/X11 sensitiva al administrador de sesin, debemos re implementar QApplication::saveState() y guardar el estado de la aplicacin all. Windows 2000 y XP, y algunos sistemas Unix, ofrecen un mecanismo diferente llamado hibernacin. Cuando el usuario coloca la computadora en hibernacin, el sistema operativo simplemente descarga el contenido de la memoria de la computadora al disco y lo recarga cuando despierte. Las aplicaciones no necesitan hacer nada ni siquiera ser sensitivas para que esto ocurra. Figura 20.7 Cerrando sesin en KDE

Cuando el usuario inicia un shutdown; osea, cuando inicia el proceso de apagar la mquina, podemos tomar el control justo antes de que el apagado ocurra por medio de la re implementacin de QApplication::commitData(). Esto nos permite guardar cualquier data no guardada e interactuar con el usuario si se requiere. Esta parte de la administracin de sesin es soportada en X11 y Windows. Exploraremos la administracin de sesin hiendo a travs de cdigos de una aplicacin llamada Tic-Tac-Toe sensible a eventos de sesin. Primero, veamos la funcin main(): int main(int argc, char *argv[]) { Application app(argc, argv); TicTacToe toe; toe.setObjectName("toe"); app.setTicTacToe(&toe); toe.show(); return app.exec(); } Creamos un objeto Application. La clase Application hereda de QApplication y re implementa los mtodos commitData() y saveState() para soportar la administracin de sesin. Lo siguiente que se hace es crear un widget TicTacToe, hacer el objeto Application sensible a este, y mostrarlo. Hemos llamado al widget TicTacToe como toe. Debemos darle nombres nicos a los objetos para widget de ltimo nivel si queremos que el administrador de sesin restaure los taaos de la ventana y la posicin.

20. Caractersticas Especficas de Plataformas

259

Figura 20.8 La aplicacin Tic-Tac-Toe

Aqu est la definicin de la clase Application: class Application : public QApplication { Q_OBJECT public: Application(int &argc, char *argv[]); void setTicTacToe(TicTacToe *tic); void saveState(QSessionManager &sessionManager); void commitData(QSessionManager &sessionManager); private: TicTacToe *ticTacToe; }; La clase Application mantiene un puntero al widget TicTacToe como una variable privada. void Application::saveState(QSessionManager &sessionManager) { QString fileName = ticTacToe->saveState(); QStringList discardCommand; discardCommand << "rm" << fileName; sessionManager.setDiscardCommand(discardCommand); } En X11, la funcin saveState() es llamada cuando el administrado de sesin quiere que la aplicacin guarde su estado. La funcin est disponible en otras plataformas de todas maneras, pero nunca es llamada. El parmetro QSessionManager nos permite comunicarnos con el administrador de sesin. Comenzamos con pedirle al widget TicTacToe que guarde su estado en un archivo. Luego establecemos el comando de exclusin del administrador de sesin (setdiscardCommand). Un comando de exclusin (discard command) es un comando que el administrador de sesin debe ejecutar para eliminar cualquier informacin guardada relativa al estado actual. Para este ejemplo, lo establecemos a rm sessionfile Donde sessionfile es el nombre del archivo que contiene el estado guardado para la sesin, y rm es el comando Unix estndar para remover archivos. El administrador de sesin tambin posee un comando de reiniciar (restart command). ste es el comando que el administrador de sesin debe ejecutar para reiniciar la aplicacin. Por defecto, Qt proporciona los siguientes comandos de reiniciado:

20. Caractersticas Especficas de Plataformas

260

appname -session id_key La primera parte, appname, es derivada de argv[0]. La parte id es el ID de sesin provisto por el administrador de sesin; est garantizado que ste sea nico entre las diferentes aplicaciones y entre las diferentes instancias ejecutadas de la misma aplicacin. La parte key es aadida nicamente para identificar el momento en el cual el estado fue guardado. Por distintos motivos, el administrador de sesin puede llamar a saveState() en mltiples ocasiones durante la misma sesin, y los diferentes estados deben ser distinguidos uno de otros. Por las limitaciones en los existentes administradores de sesiones, debemos asegurarnos que el directorio de la aplicacin est en la variable de entorno PATH si queremos que la aplicacin se reinicie correctamente. En particular, si quieres intentar realizar el ejemplo de Tic-Tac-Toe por ti mismo, debes instalarlo en, digamos, /usr/bin e invocarlo como tictactoe. Para aplicaciones simples, incluyendo Tic-Tac-Toe, podramos guardar el estado como un argumento de comando de lnea adicional para el comando de reiniciar. Por ejemplo: tictactoe -state OX-XO-X-O Esto nos libra de guardar los datos en un archivo y proporcionar un comando de exclusin para remover el archivo. void Application::commitData(QSessionManager &sessionManager) { if (ticTacToe->gameInProgress() && sessionManager.allowsInteraction()) { int r = QMessageBox::warning(ticTacToe, tr("Tic-TacToe"),tr("El juego no ha finalizado.\n" "Deseas quitarlo?"), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape); if (r == QMessageBox::Yes) { sessionManager.release(); } else { sessionManager.cancel(); } } } La funcin commitData() es llamada cuando el usuario cierra su sesin. Podemos re implementarla para que sea un mensaje emergente (pop up message) de alerta o advertencia al usuario acerca del la potencial prdida de datos. La implementacin por defecto cierra todos los widget de ultimo nivel, los cuales resultan en el mismo comportamiento como cuando el usuario cierra las ventanas una detrs de otra haciendo clic en el botn de cerrar en sus pequeas barras. En el Capitulo 3, vimos como re implementar el mtodo closeEvent() para captar cuando eso pase y mostrar un mensaje emergente (pop up). Para los propsitos de este ejemplo, re implementamos el mtodo commitData() y mostramos un mensaje emergente preguntando al usuario que confirme si desea cerrar sesin si una partida est en progreso y si el administrador de sesin nos permite interactuar con el usuario. Si el usuario hace clic en Yes, lamamos a release() para decirle al administrador de sesin que contine con el cierre de sesin; si el usuario hace clic en No, llamamos a cancel() para cancelar el cierre de sesin.

20. Caractersticas Especficas de Plataformas

261

Figura 20.9 Deseas quitarlo?

Ahora echemos un vistazo a la clase TicTacToe: class TicTacToe : public QWidget { Q_OBJECT public: TicTacToe(QWidget *parent = 0); bool gameInProgress() const; QString saveState() const; QSize sizeHint() const; protected: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); private: enum { Empty = -, Cross = X, Nought = O }; void clearBoard(); void restoreState(); QString sessionFileName() const; QRect cellRect(int row, int column) const; int cellWidth() const { return width() / 3; } int cellHeight() const { return height() / 3; } bool threeInARow(int row1, int col1, int row3, int col3)const; char board[3][3]; int turnNumber; }; La clase TicTacToe hereda de QWidget y re implementa los mtodos sizeHint(), paintEvent(), y mousePressEvent(). Este tambin proporciona las funciones gameInProgress() y saveState() que usamos en nuestra clase Application. TicTacToe::TicTacToe(QWidget *parent) : QWidget(parent) { clearBoard(); if (qApp->isSessionRestored()) restoreState(); setWindowTitle(tr("Tic-Tac-Toe")); } En el constructor, limpiamos el tablero, y si la aplicacin fue invocada con la opcin session, llamamos a la funcin privada restoreState() para recargar la sesin antigua.

20. Caractersticas Especficas de Plataformas

262

void TicTacToe::clearBoard() { for (int row = 0; row < 3; ++row) { for (int column = 0; column < 3; ++column) { board[row][column] = Empty; } } turnNumber = 0; } En la funcin clearBoard(), limpiamos todas las celdas y establecemos la variable turnNumber en 0. QString TicTacToe::saveState() const { QFile file(sessionFileName()); if (file.open(QIODevice::WriteOnly)) { QTextStream out(&file); for (int row = 0; row < 3; ++row) { for (int column = 0; column < 3; ++column) out << board[row][column]; } } return file.fileName(); } En saveState(), escribimos el estado el del tablero al disco. El formato no es algo complicado, con las X para las cruzadas, con O para los ceros o redondos y con - para las celdas vacas. QString TicTacToe::sessionFileName() const { return QDir::homePath() + "/.tictactoe_" + qApp->sessionId()+ "_" + qApp->sessionKey(); } La funcin privada sessionFileName() retorna el nombre de archivo para el ID de sesin actual y la clave de sesin. Esta funcin es usada por saveState() y restoreState(). El nombre de archivo es derivado del ID de sesin y de la clave de sesin. void TicTacToe::restoreState() { QFile file(sessionFileName()); if (file.open(QIODevice::ReadOnly)) { QTextStream in(&file); for (int row = 0; row < 3; ++row) { for (int column = 0; column < 3; ++column) { in >> board[row][column]; if (board[row][column] != Empty) ++turnNumber; } } } update(); }

20. Caractersticas Especficas de Plataformas

263

En restoreState(), leemos el archivo que corresponde a la sesin restaurada y rellenamos el tablero con esa informacin. Deducimos el valor de turnNumber a partir del nmero de X y de O en el tablero. En el constructor de TicTacToe, nosotros lamamos a restoreState() si QApplication::isSessionRestored() retorna true. En ese caso, sessionId() y sessionKey() retornan los mismos valores a cuando el estado de la aplicacin fue guardado, y as sessionFileName() retorna el nombre del archivo para esa sesin. Probar y debuguear la administracin de sesin puede llegar a ser frustrante, porque necesitamos loguearnos y desloguearnos todo el tiempo. Una manera de evitar esto es usar la utilidad estndar xsm provista por X11. La primera vez que invoquemos a xsm, este crea ventanas emergentes para un administrador de sesin y una terminal. Las aplicaciones que iniciemos desde ese terminal usaran a xsm como su administrador de sesin en lugar del usual, el administrador de sesin del sistema. Podemos usar la ventana de xsm para terminar, reiniciar o excluir una sesin, y ver si nuestra aplicacin se comporta como debera. Para ms detalles acerca de cmo hacer esto, vea http://doc.trolltech.com/4.1/session.html.

21. Programacin Embebida

264

21. Programacin Embebida

Iniciando con Qtopia Personalizando Qtopia Core

Desarrollar software para ejecutarlo en dispositivos mviles tales como PDAs y telfonos celulares puede ser muy retador ya que los sistemas embebidos generalmente poseen procesadores lentos, menos almacenamiento permanente (memorias flash o disco duro), menos memoria de trabajo, y visualizaciones ms pequeas que las computadoras de escritorio. Qtopia Core (anteriormente llamado Qt/Embedded) es una versin de Qt optimizada para Linux embebido. Qtopia Core proporciona la misma API y herramientas que la versin de escritorio de Qt (Qt/Windows, Qt/X11 y Qt/Mac), y aade las clases y herramientas necesarias para la programacin embebida. A travs de licenciamiento dual, ste est disponible tanto para open source como para el desarrollo comercial. Qtopia Core puede correr en cualquier hardware donde Linux pueda correr (incluyendo arquitecturas Intel x86, Motorola 68000 y PowerPc). Este tiene un frame buffer de mapeado de memoria y soporta un compilador C++. A distincin de Qt/X11, este no necesita el sistema X Window System; en lugar de ello, este implementa sus propio sistema de ventana (QWS), permitiendo almacenamiento significante y ahorros de memoria. Para reducir su consumo de memoria aun ms, Qtopia Core puede ser recompilado para excluir caractersticas en desuso. Si las aplicaciones y componentes usados en un dispositivo son conocidos de antemano, estos pueden ser compilados juntos en un ejecutable que enlaza estticamente nuevamente a las libreras de Qtopia Core. Qtopia Core tambin se beneficia de varias caractersticas que son tambin parte de las versiones de escritorio de Qt, incluyendo el uso extensivo del compartimiento de datos implcito (copiar sobre escritura; en ingles: copy on write) como una tcnica de ahorro de memoria, soporte para estilos de widgets personalizados a travs de QStyle, y un sistema de layout que se adapta para hacer el mejor uso del espacio disponible en pantalla. Qtopia Core forma las bases de la oferta de Trolltech acerca de la programacin embebida, lo cual tambin incluye la Plataforma Qtopia, Qtopia PDA y Qtopia Phone. Estos proporcionan clases y aplicaciones diseadas especficamente para dispositivos porttiles y puede ser integrados con muchas maquinas virtuales Java como terceros.

Iniciando con Qtopia


Las aplicaciones hechas en Qtopia Core pueden ser desarrolladas en cualquier plataforma equipada con una cadena de herramientas multi plataforma. La opcin ms comn es construir un compilador cruzado GNU C++ sobre un sistema Unix. Este proceso simplificado por un script y un conjunto de parches provistos por Dan Kegel en http://kegel.com/crosstool/. Ya que Qtopia Core contiene el API de Qt, usualmente es posible usar una versin de escritorio de Qt, tal como Qt/X11 o Qt/Windows, para la mayora del desarrollo.

21. Programacin Embebida

265

El sistema de configuracin de Qtopia Core soporta compiladores cruzados, a travs de configure y de la opcin de script embedded. Por ejemplo para construir para una arquitectura ARM escribiramos ./configure -embedded arm Podemos crear configuraciones personalizadas agregando nuevos archivos al directorio de Qt mkspecs/qws. Qtopia Core dibuja directamente al frame buffer de Linux (el rea de memoria asociada con la visualizacin de video). Para acceder al frame buffer, pudieras necesitar conceder permisos de escritura al dispositivo /dev/fb0. Para ejecutar aplicaciones Qtopia Core - como llamaremos de ahora en adelante a las aplicaciones hechas con Qtopia Core-, primero debemos iniciar un proceso que acte como servidor. El servidor es responsable de la asignacin de regiones de pantalla a clientes para generar eventos de mouse y de teclado. Cualquier aplicacin Qtopia Core puede volverse un servidor especificando el comando qws en su lnea de comando o pasando a QApplication::GuiServer como el tercer parmetro al constructor de QApplication. Las aplicaciones Clientes se comunican con el servidor Qtopia Core usando memoria compartida. Detrs e bastidores, los clientes se dibujan ellos mismos en la memoria compartida y son responsables de dibujar sus propias decoraciones de ventana. Esto mantiene la comunicacin entre los clientes y sus servidores en un mnimo, resultando en una interfaz de usuario concisa. Las aplicaciones Qtopia Core normalmente usan QPainter para dibujarse a s mismas, pero tambin pueden acceder al hardware de video directamente usando QDirectPainter. Los clientes pueden comunicarse entre ellos usando el protocolo QCOP. Un cliente puede escuchar en una canal nombrado creando un objeto QCopChannel y conectando a su seal received(). Por ejemplo: QCopChannel *channel = new QCopChannel("System", this); connect(channel, SIGNAL(received(const QString &, const QByteArray &)), this, SLOT(received(const QString &, const QByteArray &))); Un mensaje QCOP consta de un nombre y un QByteArray opcional. El mtodo esttico QCopChannel::send() difunde o trasmite un mensaje en un canal. Por ejemplo: QByteArray data; QDataStream out(&data, QIODevice::WriteOnly); out << QDateTime::currentDateTime(); QCopChannel::send("System", "clockSkew(QDateTime)", data); El ejemplo anterior ilustra un idioma comn: Usamos QDtaStream para codificar los datos, y para asegurar que el QByteArray es interpretado correctamente por el receptor, ponemos el formato de datos en el nombre del mensaje como si fuera una funcin C++. Varias variables de entorno afectan las aplicaciones Qtopia Core. Las ms importantes son QWS_MOUSE_PROTO y QWS_KEYBOARD, las cuales especifican el dispositivo de mouse y el tipo de teclado respectivamente. Vea http://doc.trolltech.com/4.1/emb-envvars.html para una lista completa de variables de entorno. Si usamos Unix como nuestra plataforma de desarrollo, podemos probar la aplicacin usando el frame buffer virtual de Qtopia (qvfb), una aplicacin X11 que simula, pixel por pixel, el frame buffer actual. Para habilitar el soporte a buffers virtuales en Qtopia Core, hay que pasar la opcin qvfb al script configure. Sea consciente de que esta opcin no est pensada para uso de produccin. El frame buffer virtual est localizado en tools/qvfb y puede ser invocado como sigue: qvfb -width 320 -height 480 -depth 32 Otra opcin que puede funcionar en la mayora de las plataformas es usar VNC (Virtual Network Computing) para ejecutar las aplicaciones remotamente. Para activar o habilitar el soporte VNC en Qtopia

21. Programacin Embebida

266

Core, pasa la opcin qt gfx vnc a configure. Luego lanza tus aplicaciones Qtopia Core con la opcin de lnea de comando display VNC=0 y ejecuta un cliente VNC apuntando al host donde tu aplicacin este ejecutndose. El tamao de la visualizacin y profundidad de bits puede ser especificada estableciendo las variables de entorno QWS_SIZE y QWS_DEPTH en el host que ejecuta las aplicaciones Qtopia Core (Por ejemplo, QWS_SIZE=320x480 y QWS_DEPTH=32).

Personalizando Qtopia Core


Cuando instalamos Qtopia Core, podemos especificar caractersticas que queremos dejar fuera para reducir el consumo de memoria. Qtopia Core incluye ms de un centenar de caractersticas configurables, cada una de las cuales est asociada a un smbolo de pre procesamiento. Por ejemplo, QT_NO_FILEDIALOG excluye QFileDialog de la librera QtGui, y QT_NO_I18N deja fuera todo el soporte para la internacionalizacin. Las caractersticas estn listadas en src/corelib/qfeatures.txt. Qtopia Core provee cinco configuraciones de ejemplo (minimun, small, mdium, large y dist) que est alojado en los archivos src/corelib/qconfig_xxx.h. Estas configuraciones pueden ser especificadas usando la opcin qconfig del script configure, por ejemplo: ./configure -qconfig small Para crear configuraciones personalizadas, podemos proporcionar manualmente un archivo qconfigxxx.h y usarlo como si fuera una configuracin estndar. Alternativamente, podemos usar la herramienta grafica de qconfig, localizada en el subdirectorio tools de Qt. Qtopia Core proporciona las siguientes clases para la interfaz con dispositivos de entrada y de salida y para personalizar la apariencia del sistema de ventana:

Para obtener la lista de los controladores predefinidos, mtodos de entrada y de estilos de decoraciones de ventana, ejecuta el script configure con la opcin help. El controlador de pantalla puede ser especificado usando la opcin de lnea de comando display cuando se inicia el servidor Qtopia Core, como se vio en la seccin anterior, o estableciendo la variable de entorno QWS_DISPLAY. El controlador del ratn o mouse y los dispositivos asociados pueden ser especificados usando la variable de entorno QWS_MOUSE_PROTO, cuyo valor debe tener la sintaxis type: device, donde type es uno de los controladores soportados y device el path o ruta del dispositivo (por ejemplo, QWS_MOUSE_PROTO_IntelliMouse:/dev/mouse). Los teclados son manejados similarmente mediante la variable de entorno QWS_KEYBOARD. Los mtodos de entrada y decoraciones son establecidos programticamente en el servidor usando QWSServer::setCurrentInputMethod() y QApplication::qwsSetDecoration().

21. Programacin Embebida

267

Los estilos de decoracin de ventana pueden ser establecidos independientemente del estilo del widget, el cual hereda de QStyle. Por ejemplo, es totalmente posible establecer el estilo Windows como el estilo de decoracin de ventana y Plastique como el estilo del widget. Si se desea, las decoraciones pueden ser establecidas en una base por ventana. La clase QWSServer provee varas funciones para personalizar el sistema de ventanas. Las aplicaciones que se ejecuten como servidores Qtopia Core pueden acceder a la nica instancia de QWSServer a travs de la variable global qwsServer, la cual es inicializada por el constructor de QApplication. Qtopia Core soporta los siguientes formatos de fuente: TrueType (TTF), Pst-Script Type 1, Bitmap Distribution Format (BDF) y Qt Pre-rendered Fonts (QPF). Ya que QPF es un formato raster, es ms rpido y usualmente ms compacto que los formatos de vectores tales como TTD y Type 1, si lo necesitamos solo a uno o dos tamaos distintos. La herramienta makeqpf nos permite pre dibujar un archivo TTF o Type 1 y guardarlo el resultado en un formato QPF. Una alternativa es ejecutar nuestras aplicaciones con la opcin de lnea de comando savefonts. Al momento de la escritura, Trolltech est desarrollando una capa adicional sobre Qtopia Core para hacer el desarrollo de aplicaciones embebidas aun ms rpido y ms conveniente. Se espera que en una versin futura de este libro se incluya ms informacin en este tema.

Glosario

268

Glosario Endiannes
El trmino ingls Endianness designa el formato en el que se almacenan los datos de ms de un byte en un ordenador. Big-endian es un mtodo de ordenacin en el que el byte de mayor peso se almacena en la direccin ms baja de memoria y el byte de menos peso en la direccin ms alta. Little-endian es un mtodo de ordenacin en el que el byte de mayor peso se almacena en la direccin ms alta de memoria y el byte de menos peso en la direccin ms baja.

Mutex (Mutual Exclusin)


MUTEX es un sistema de sincronizacin de acceso mltiple para orgenes de informacin comn (por medio del mecanismo de cerrar y abrir: "lock-unlock").